swiftuitableviewuitableviewrowaction

UITableViewRowAction with icon and text


There are a couple of similar questions out there, but I think there should be up an up to date answer for iOS 10, using Swift3, that doesn't use private APIs, and doesn't rely on you restricting your icon to the unicode emoji.

I have table rows with three actions right now:

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
    let rename = UITableViewRowAction(style: .normal, title: "Rename") { (_, indexPath) in
        self.renameEntry(indexPath)
    }
    let locate = UITableViewRowAction(style: .normal, title: "Locate") { (_, indexPath) in
        self.locateEntry(indexPath)
    }
    locate.backgroundEffect = UIVisualEffect()
    let delete = UITableViewRowAction(style: .default, title: "Forget") { (_, indexPath) in
        self.deleteEntry(indexPath)
    }
    return [delete, locate, rename]
}

enter image description here

But instead of squares of color with centered text of size and style not of my choosing, I want:

enter image description here

I tried using a backgroundColor, but that just tiled my image all over the button, and didn't change the position or color of the text. So it has to be something more than just

theAction.backgroundColor = UIColor(patternImage: UIImage(named: "renameImage")!) 

Is there a good way of doing this?


Solution

  • 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()
    }