UITableViewRowAction with icon and text

What I ended up doing was generating an image on the fly as the background. This required the use of a hack/clever-trick or two.

The first part is the standard delegate method:

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {

    let stockWidth = String(repeating: " ", count: 8)
    let rename = UITableViewRowAction(style: .normal, title: stockWidth) { (_, indexPath) in
        self.renameEntry(indexPath)
    }
    self.fixAction(rename, text: "Rename", image: UIImage(named: "pencilEdit")!, color: UIColor(52, 61, 70))
    let locate = UITableViewRowAction(style: .normal, title: stockWidth) { (_, indexPath) in
        self.locateEntry(indexPath)
    }
    self.fixAction(locate, text: "Locate", image: UIImage(named: "locatePin")!, color: UIColor(38, 107, 215))
    let delete = UITableViewRowAction(style: .normal, title: stockWidth) { (_, indexPath) in
        self.deleteEntry(indexPath)
    }
    self.fixAction(delete, text: "Forget", image: UIImage(named: "triggerDeleteSelector")!, color: UIColor(227, 34, 60))
    let gap = UITableViewRowAction(style: .normal, title: "") { (_, _) in
        // pass
    }
    gap.backgroundColor = UIColor.clear
    return [gap, delete, locate, rename]
}

There's two not obvious details there. First, the action derives its width from the text string passed in. If you didn't give it some width via some non-visible space characters, the background image wouldn't have any area to be drawn in. That's the reason for the stockWidth string used in the first 3 actions. It's 8 character width is shared by the fixAction method that generates the background image.

The second detail is the inclusion of the fourth "gap" action at the bottom. For some reason, if the first action has a background paint that is a tile pattern, it will stretch left under the other actions. I found that I had to insert this zero width no op action at the front to avoid that.

func fixAction(_ action:UITableViewRowAction, text:String, image:UIImage, color:UIColor) {
    // make sure the image is a mask that we can color with the passed color
    let mask = image.withRenderingMode(.alwaysTemplate) 
    // compute the anticipated width of that non empty string
    let stockSize = action.title!.sizeWithAttributes([NSFontAttributeName: UIFont.systemFont(ofSize: 18)])
    // I know my row height
    let height:CGFloat = 70
    // Standard action width computation seems to add 15px on either side of the text
    let width = (stockSize.width + 30).ceiling 
    let actionSize = CGSize(width: width, height: height)
    // lets draw an image of actionSize
    UIGraphicsBeginImageContextWithOptions(actionSize, false, 0.0)
    if let context = UIGraphicsGetCurrentContext() {
        context.clear(CGRect(origin: .zero, size: actionSize))
    }
    color.set()
    let attributes = [NSForegroundColorAttributeName: color, NSFontAttributeName: UIFont(name: "Avenir-Book", size: 13)]
    let textSize = text.size(attributes: attributes)
    // implementation of `half` extension left up to the student
    let textPoint = CGPoint(x: (width - textSize.width).half, y: (height - (textSize.height * 3)).half + (textSize.height * 2))
    text.draw(at: textPoint, withAttributes: attributes)
    let  maskHeight = textSize.height * 2
    let maskRect = CGRect(x: (width - maskHeight).half, y: textPoint.y - maskHeight, width: maskHeight, height: maskHeight)
    mask.draw(in: maskRect)
    if let result = UIGraphicsGetImageFromCurrentImageContext() {
        // adjust the passed in action's backgroundColor to a patternImage
        action.backgroundColor = UIColor(patternImage: result)
    }
    else {
        "WTH!!!".logError()
    }
    UIGraphicsEndImageContext()
}