UITableViewRowAction with icon and text

2019-03-21 06:36发布

问题:

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]
}

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

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?

回答1:

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


回答2:

If you don't want to loose auto layout benefits and achieve a good quality UI I suggest to implement the swipe gesture yourself on the cell and three custome action views. You could also play around with UIColor from a pattern image to set it as the background https://developer.apple.com/reference/uikit/uicolor/1621933-init BUT you should have already the icon and text rendered in an image for doing that and won't look perfect for all devices probably, at some point the image could be stretched.