Universal solution for UITableViewRowAction and UIContextualAction

Many were faced with the problem of implementing a beautiful display of buttons for UITableViewCell when shifting to the left. Some used the standard out-of-the-box functionality, others bothered on their own implementation, while others cost Unicode characters. In this article I will tell how to achieve maximum customization of UITableViewRowAction.
First, a little theory: UITableViewRowAction is a class that allows the user to perform certain actions on a given cell (such as “delete”, “change”, etc.) when the cell is shifted to the left. Each instance of this class represents one action to perform and includes text, style, and a click handler. The iOS SDK allows us to configure the UITableViewRowAction as follows:
publicconvenienceinit(style: UITableViewRowActionStyle, title: String?, handler: @escaping (UITableViewRowAction, IndexPath) -> Swift.Void)
You can also set backgroundColor.
Existing methods.
Consider existing examples of implementation, or what I found on the Internet.
Method number 1 . Use the functionality "out of the box." This is enough to use ordinary buttons with text and background. It looks too simple and boring:

Method number 2 . Set backgroundColor using a UIColor (patternImage: UIImage) . This will achieve beauty, but will deprive of universality, will add a number of limitations and difficulties. Since the width of the UITableViewRowAction is calculated by the size of the text, you need to set the title as a set of whitespace characters of a certain width. We will not be able to use this method for cells with dynamic height. If the cell increases in height, it will look like this:

Method number 3. Use Unicode characters. There is no need to calculate the size of the text with spaces and this method is suitable for cells with dynamic height. It looks nice:

But if we want to make the text under the icon, this method will not work for us.
Method number 4 . Use CoreGraphics to draw UI components. The idea is to manually draw an image with the desired location of elements (text and images). It is necessary to calculate the dimensions and coordinates for the location of each element. Using this method, we can make both text and image, arranging them as we need. But this method is difficult to implement. We want something simpler.
Other implementations. There are some more ready-made solutions. They use their own implementation of the cell with the addition of ScrollView and other logic. For example, SwipeCellKit or MGSwipeTableCell . But these solutions do not allow to achieve the standard behavior of iOS, they cover only one cell, in contrast to the UITableViewRowAction, which is created in the table delegate. For example, using one of these solutions, if you open actions when you shift to the left for several cells, in each of them you will have to manually close these actions, which is extremely inconvenient. To do this, you have to add your logic to control open swipe actions. We want to create a simple solution that does not require additional costs, as if we are using a standard implementation of UITableViewRowAction.
Let's get started
Let's try to create a universal solution that allows you to make a UITableViewRowAction with any design that we need.
Create our inheritance class from UITableViewRowAction and define the main instance creation method:
typealiasRowActionTapHandler = (RowAction, IndexPath) -> VoidclassRowAction: UITableViewRowAction{
staticfuncrowAction(with actionHandler: @escaping RowActionTapHandler) -> Self {
let rowAction = self.init(style: .default, title: nil) { action, indexPath inguardlet action = action as? RowActionelse { return }
actionHandler(action, indexPath)
}
return rowAction
}
}
Since UITableViewRowAction has no primary initializer, for convenience, we can make a static method, which will create UITableViewRowAction in a standard way and Prokin it actionHandler for depression treatment.
Now let's try to achieve universal customization of the UITableViewRowAction button. The idea is to get the look of the button from a ready-made UIView (which can be configured in the usual xib-ke). So we can make a button of any kind, based on individual preferences (any font of text, size and location of elements, etc.).
We have two things we can change in UITableViewRowAction: backgroundColor and title. At the moment, the only way to change the appearance of the UITableViewRowAction button is to change the backgroundColor property. Therefore, our goal is to transform: UIView -> UIImage -> UIColor. We can get the color using the image as follows: UIColor (patternImage: UIImage) . We can get the image from UIView by rendering it using the standard CoreGraphics framework.
But first, let's define how we set the button sizes: “out of the box” there is no this functionality, and the size in UITableViewRowAction is calculated by the text size of the title field.
After conducting the experiments (measuring the calculated and real width of the button), I found a white space Unicode character that has the smallest non-zero width: U + 200A
We will use it and create a constant:
privateenumConstants{
staticlet placeholderSymbol = "\u{200A}"
}
Let's write a function to get a string of whitespace characters corresponding to the size we need:
privatestaticlet placeholderSymbolWidth: CGFloat = {
let flt_max = CGFloat.greatestFiniteMagnitude
let maxSize = CGSize(width: flt_max, height: flt_max)
let attributes = [NSFontAttributeName : UIFont.systemFont(ofSize: UIFont.systemFontSize)]
let boundingRect = Constants.placeholderSymbol.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
return boundingRect.width
}()
privatefuncemptyTitle(for size: CGSize) -> String {
var usefulWidth = size.width - Constants.minimalActionWidth
usefulWidth = usefulWidth < 0 ? 0 : usefulWidth
let countOfSymbols = Int(floor(usefulWidth * Constants.shiftFactor / RowAction.placeholderSymbolWidth))
returnString(repeating: Constants.placeholderSymbol, count: countOfSymbols)
}
The variable placeholderSymbolWidth gets the width of our Unicode character, and the function emptyTitle (for size: CGSize) -> String calculates the number of whitespace characters corresponding to size and returns a string of these characters. The formula above uses the constant shiftFactor . This is the coefficient of deviation of the width from the nominal value. The fact is that as the width of the UITableViewRowAction increases, a small error increases (that is, the actual width of the button differs from the calculated width by the usual formula). To avoid this, we use this constant, derived experimentally.
Also, UITableViewRowAction has a minimum width of 30 (For example, if you specify title = ""). This width is the minimum required for indents to the left and right of the header.
Update our constants:
privateenumConstants{
staticlet placeholderSymbol = "\u{200A}"staticlet minimalActionWidth: CGFloat = 30staticlet shiftFactor: CGFloat = 1.1
}
Now let's get to the image from UIView. We write the following function:
privatefuncimage(from view: UIView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, UIScreen.main.scale)
guardlet context = UIGraphicsGetCurrentContext() else {
assertionFailure("Something wrong with CoreGraphics image context");
returnnil
}
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: view.bounds.size))
view.layer.render(in: context)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
This function converts our view into a picture. At the same time, the size of the image corresponds to the size of the view .
The last step remains, where we will implement a method for customizing a UITableViewRowAction using a UIView:
funcsetupForCell(with view: UIView) {
guardlet img = image(from: view) else { return }
self.title = emptyTitle(for: img.size)
self.backgroundColor = UIColor(patternImage: img)
}
Here we use the previously written functions to configure our button and set the backgroundColor using the resulting image.
That's all. We got a one-stop solution for UITableViewRowAction and now we can customize it in any way using UIView.

Starting with iOS 11, Apple has added a new swipe action class for cells in the table - UIContextualAction. It allows you to immediately set the image that will be displayed on the action on the svaypu (this is what we lacked all these years of using UITableViewRowAction). He was supposed to make life easier for us, but in addition to a new opportunity, he created additional problems. This class imposes restrictions on the size of the image, does not allow the use of both text and image, and also, the image must have a special format (template image). Therefore, I modified my solution to support svayp actions in iOS 11. Now in order to create a UITableViewRowAction or UIContextualAction, you first need to create a RowActionFactory factory, configure it using our view (as we did in the above methodsetupForCell (with view: UIView)) and return the entity we need: UITableViewRowAction or UIContextualAction (using the rowAction () or contextualAction (for indexPath: IndexPath) methods, respectively). Below is an example of using RowActionFactory:
@available(iOS 11.0, *)
functableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
var contextualActions: [UIContextualAction] = []
for actionType in rowActionTypes { // actionType - тип действия по свайпуlet rowActionFactory = RowActionFactory { (indexPath) in
tableView.setEditing(false, animated: true)
}
let rowActionView = <...> // создание UIView для действия по свайпу
rowActionFactory.setupForCell(with: rowActionView)
iflet contextualAction = rowActionFactory.contextualAction(for: indexPath) {
contextualActions.append(contextualAction)
}
}
let swipeActionsConfiguration = UISwipeActionsConfiguration(actions: contextualActions)
swipeActionsConfiguration.performsFirstActionWithFullSwipe = falsereturn swipeActionsConfiguration
}
A complete solution can be found here: TCSCustomRowActionFactory (github.com) .