32

I made a custom UITableViewRowAction. Now I'd like to add an image instead of the text. I know that it's possible but don't know how to do it. Does someone of you knows how to do this in Swift and would like to help me? Thanks for your answers!

user3592462
  • 343
  • 1
  • 4
  • 9
  • Answered here -https://stackoverflow.com/questions/44771778/how-to-add-image-in-uitableviewrowaction/45301272#45301272 – Jack Jul 25 '17 at 10:57
  • Update answered for both iOS 11 and earlier https://stackoverflow.com/questions/27740884/uitableviewrowaction-title-as-image-icon-instead-of-text/46337919#46337919 – Martin Le Sep 21 '17 at 07:50
  • the size of the image and row height should be match??? – ArgaPK May 10 '18 at 13:46

9 Answers9

52

iOS 11.0

Swift

Apple introduced flexible way to declare row actions with great benefits.

extension ViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let askAction = UIContextualAction(style: .normal, title: nil) { action, view, complete in
      print("Ask!")
      complete(true)
    }

    // here set your image and background color
    askAction.image = IMAGE
    askAction.backgroundColor = .darkGray

    let blockAction = UIContextualAction(style: .destructive, title: "Block") { action, view, complete in
      print("Block")
      complete(true)
    }

    return UISwipeActionsConfiguration(actions: [blockAction, askAction])
  }

  func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cell.textLabel?.text = "row: \(indexPath.row)"
  }
}

Example:

enter image description here

iOS 8.0

You need to set UIImage to backgroundColor of row action, concretely by:

Swift:

UIColor(patternImage: UIImage(named: "IMAGE_NAME"))

Objective-C:

[UIColor colorWithPatternImage:[UIImage imageNamed:@"IMAGE_NAME"]];
dimpiax
  • 12,093
  • 5
  • 62
  • 45
  • 3
    For the images, I put together a CocoaPod that would try to solve this as elegantly as possible for the time being: https://github.com/benguild/BGTableViewRowActionWithImage – Ben Guild Aug 21 '15 at 16:54
  • 3
    @dimpiax: Great. This works for image. But how to put both background color and image together. – Anooj Krishnan G Aug 27 '16 at 05:55
  • @AnoojKrishnanG, hi! I propose two solutions: 1. background color you set concretely in image; 2. set for UIImage imageWithRenderingMode(.AlwaysTemplate) special color. – dimpiax Aug 28 '16 at 14:11
  • 2
    When using an image, the size of the button must be determined and applied to the image. The text must also be a certain number of "spaces". If title text is set in `rowActionWithStyle: title: handler:`, the image and text may overlap. The number of spaces in title text determines the width of the image. – David Jan 19 '17 at 14:24
  • I can't seem to make this size the image the way I want, regardless of how many spaces I use. – Gandalf458 Jun 23 '17 at 21:29
  • @Gandalf458 what is size of your image? – dimpiax Jun 23 '17 at 22:04
  • It's a .pdf vector – Gandalf458 Jun 23 '17 at 22:12
  • Do you think it would work better if it was a .png? @dimpiax – Gandalf458 Jun 23 '17 at 22:15
  • @Gandalf458 firstly get bounds of image that you put inside, secondly try like png. – dimpiax Jun 24 '17 at 06:51
  • Now your answer does not work: `Setting a pattern color as backgroundColor of UITableViewRowAction is no longer supported.` – Vergiliy Apr 22 '19 at 15:01
  • @Vergiliy tnx, check the solution from iOS 11. – dimpiax Apr 22 '19 at 16:46
12

Swift 4 (iOS 11+):

iOS 11 now supports images (only) to display in action buttons. You simply have to initialize a UISwipeActionsConfiguration object in your table view delegate object:

extension MyTableViewController:UITableViewDelegate {

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        
        let deleteAction = UIContextualAction(style: .normal, title:  nil, handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in
                
                debugPrint("Delete tapped")

                success(true)
            })

        deleteAction.image = UIImage(named: "icon_delete.png")
        deleteAction.backgroundColor = UIColor.red

        return UISwipeActionsConfiguration(actions: [deleteAction])
    }
}
Fabio
  • 5,432
  • 4
  • 22
  • 24
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
7

I made this simple UITableViewRowAction category, in order to set the icon for my actions. You can set the image, the background color, the cell height (to manage dynamic cells) and the icon size in percentage.

extension UITableViewRowAction {

  func setIcon(iconImage: UIImage, backColor: UIColor, cellHeight: CGFloat, iconSizePercentage: CGFloat)
  {
    let iconHeight = cellHeight * iconSizePercentage
    let margin = (cellHeight - iconHeight) / 2 as CGFloat

    UIGraphicsBeginImageContextWithOptions(CGSize(width: cellHeight, height: cellHeight), false, 0)
    let context = UIGraphicsGetCurrentContext()

    backColor.setFill()
    context!.fill(CGRect(x:0, y:0, width:cellHeight, height:cellHeight))

    iconImage.draw(in: CGRect(x: margin, y: margin, width: iconHeight, height: iconHeight))

    let actionImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    self.backgroundColor = UIColor.init(patternImage: actionImage!)
  }
}
Aryan Firouzian
  • 1,940
  • 5
  • 27
  • 41
Il Pisanello
  • 117
  • 2
  • 4
4
class TableViewRowAction: UITableViewRowAction 
{
    var image: UIImage?

    func _setButton(button: UIButton) 
    {
        if let image = image, let titleLabel = button.titleLabel
        {
            let labelString = NSString(string: titleLabel.text!)
            let titleSize = labelString.sizeWithAttributes([NSFontAttributeName: titleLabel.font])

            button.tintColor = UIColor.whiteColor()
            button.setImage(image.imageWithRenderingMode(.AlwaysTemplate), forState: .Normal)
            button.imageEdgeInsets.right = -titleSize.width
        }
    }
}


func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? 
{
    let delete = TableViewRowAction(style: UITableViewRowActionStyle.Default, title: "         ") { action, indexPath in }
    delete.image = UIImage(named: "trashImg")

    let sharing = TableViewRowAction(style: UITableViewRowActionStyle.Default, title: "         ") { action, indexPath in }
    sharing.backgroundColor = UIColor.lightGrayColor()
    sharing.image = UIImage(named: "sharingImg")

    return [delete, sharing]
}
Jayesh Miruliya
  • 3,279
  • 2
  • 25
  • 22
  • FYI this won't let you just add an image, there has to be text as well. It was crashing when I put "" into the title argument (due to force unwrap of titleLabel.text). I had to put " " into the title argument to get an image only swipe action...on the plus side you can control the width of the button with the number of blank spaces though...:) – Natalia Sep 09 '16 at 18:57
  • 1
    @Jayesh Miruliya, how did you know that UITableViewRowAction had a button property? This is NOT in the documentation: https://developer.apple.com/reference/uikit/uitableviewrowaction. Is this technically a hack ? – etayluz Sep 19 '16 at 16:02
  • @etayluz sadly _setButton is a private API. – Keab42 Sep 29 '16 at 13:34
  • 4
    No one should use this solution. You have no way of knowing if Apple stops calling setButton. – mattsson May 03 '17 at 09:21
  • try above comment – Jayesh Miruliya May 03 '17 at 09:32
2

For those who want to create this effect;

enter image description here

I have created an extension for UISwipeActionsConfiguration which you can use without any third-party library. Basically, the idea is to create an Attributed String from the image and the text and set it to label and create an image from that label. And append it to UIContextualAction's image property.

extension UISwipeActionsConfiguration {

    public static func makeTitledImage(
        image: UIImage?,
        title: String,
        textColor: UIColor = .white,
        font: UIFont = .systemFont(ofSize: 14),
        size: CGSize = .init(width: 50, height: 50)
    ) -> UIImage? {
        
        /// Create attributed string attachment with image
        let attachment = NSTextAttachment()
        attachment.image = image
        let imageString = NSAttributedString(attachment: attachment)
        
        /// Create attributed string with title
        let text = NSAttributedString(
            string: "\n\(title)",
            attributes: [
                .foregroundColor: textColor,
                .font: font
            ]
        )
        
        /// Merge two attributed strings
        let mergedText = NSMutableAttributedString()
        mergedText.append(imageString)
        mergedText.append(text)
        
        /// Create label and append that merged attributed string
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        label.textAlignment = .center
        label.numberOfLines = 2
        label.attributedText = mergedText
        
        /// Create image from that label
        let renderer = UIGraphicsImageRenderer(bounds: label.bounds)
        let image = renderer.image { rendererContext in
            label.layer.render(in: rendererContext.cgContext)
        }
        
        /// Convert it to UIImage and return
        if let cgImage = image.cgImage {
            return UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up)
        }
        
        return nil
    }
}

And you can use it like this;

public func tableView(
        _ tableView: UITableView,
        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? 
{
        let deleteAction = UIContextualAction(
            style: .normal,
            title:  nil,
            handler: { [weak self] (_, _, success: (Bool) -> Void) in
                success(true)
                print("Your action in here")
            }
        )
        
        deleteAction.image = UISwipeActionsConfiguration.makeTitledImage(
            image: UIImage(named: "delete_icon"),
            title: "Delete")
        )
        deleteAction.backgroundColor = .orange
        return UISwipeActionsConfiguration(actions: [deleteAction])
}
ysnzlcn
  • 558
  • 6
  • 18
1

I just implement better version of https://stackoverflow.com/a/48122210 1. In majority of cases height of cell and width of custom swipe part are different, you have to calculate this in your function 2. Image can have different size than square, so you have to calculate proportions not just for height. So, code with my fixes

func setIcon(iconImage: UIImage, backColor: UIColor, cellHeight: CGFloat, customSwipPartWidth: CGFloat, iconSizePercentage: CGFloat) {
        let iconWidth = customSwipPartWidth * iconSizePercentage
        let iconHeight = iconImage.size.height / iconImage.size.width * iconWidth
        let marginY = (cellHeight - iconHeight) / 2 as CGFloat
        let marginX = (customSwipPartWidth - iconWidth) / 2 as CGFloat


        UIGraphicsBeginImageContextWithOptions(CGSize(width: customSwipPartWidth, height: cellHeight), false, 0)
        let context = UIGraphicsGetCurrentContext()

        backColor.setFill()
        context!.fill(CGRect(x:0, y:0, width:customSwipPartWidth, height:cellHeight))

        iconImage.draw(in: CGRect(x: marginX, y: marginY, width: iconWidth, height: iconHeight))

        let actionImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        self.backgroundColor = UIColor.init(patternImage: actionImage!)
    } 
1

My variation tries to use UImageView's contentMode behavior and when that was not enough, I found some good use for a couple of the Core Geometry Rect methods because they keep the target frame centered within the cell frame. Using an eye test, It appears that swiping was removing half of my normal cell width so that's where the magic numbers show up.

Swift 3.0

extension UITableViewRowAction {

    func setIcon(iconImage: UIImage, backColor: UIColor, cellHeight: CGFloat, cellWidth:CGFloat) ///, iconSizePercentage: CGFloat)
    {
        let cellFrame = CGRect(origin: .zero, size: CGSize(width: cellWidth*0.5, height: cellHeight))
        let imageFrame = CGRect(x:0, y:0,width:iconImage.size.width, height: iconImage.size.height)
        let insetFrame = cellFrame.insetBy(dx: ((cellFrame.size.width - imageFrame.size.width) / 2), dy: ((cellFrame.size.height - imageFrame.size.height) / 2))
        let targetFrame = insetFrame.offsetBy(dx: -(insetFrame.width / 2.0), dy: 0.0)
        let imageView = UIImageView(frame: imageFrame)
        imageView.image = iconImage
        imageView.contentMode = .left
        guard let resizedImage = imageView.image else { return }
        UIGraphicsBeginImageContextWithOptions(CGSize(width: cellWidth, height: cellHeight), false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return }
        backColor.setFill()
        context.fill(CGRect(x:0, y:0, width:cellWidth, height:cellHeight))
        resizedImage.draw(in: CGRect(x:(targetFrame.origin.x / 2), y: targetFrame.origin.y, width:targetFrame.width, height:targetFrame.height))
        guard let actionImage = UIGraphicsGetImageFromCurrentImageContext() else { return }
        UIGraphicsEndImageContext()
        self.backgroundColor = UIColor.init(patternImage: actionImage)
    }
}

Usage: from the editActions... method of the tableview delegate.

let cellHeight = (self.tableView(tableView, cellForRowAt: indexPath)).frame.size.height
let cellWidth = (self.tableView(tableView, cellForRowAt: indexPath)).frame.size.width
let favorite = UITableViewRowAction(style: .normal, title: nil) { action, index in
                //perform action
                debugPrint("Test")

            }
favorite.setIcon(iconImage: #imageLiteral(resourceName: "favorite"), backColor: .green, cellHeight: cellHeight, cellWidth:cellWidth)
Tommie C.
  • 12,895
  • 5
  • 82
  • 100
0
delActions.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"delete.png"]];
Taryn
  • 242,637
  • 56
  • 362
  • 405
Subhro
  • 9
0

The problem with the basic pattern-color approach is that your image need to be of the same size than the action button, or at least to have some more flat background at the bottom to prevent the image repetition (even though it will be anchored on top, which is not really nice).

I had to deal with dynamic-height cells so I implemented the following:

- (UIColor *)backgroundImageForActionAtIndexPath:(NSIndexPath *)indexPath withImage:(UIImage *)image tintColor:(UIColor *)tintColor backgroundColor:(UIColor *)backgrounfColor expectedWith:(CGFloat)width {
//
CGRect cellFrame = [self.tableView rectForRowAtIndexPath:indexPath];
CGSize expectedSize = CGSizeMake(width, cellFrame.size.height);

UIGraphicsBeginImageContextWithOptions(expectedSize, NO, 0.0);

CGContextRef ctx = UIGraphicsGetCurrentContext ();
if (ctx) {
    // set the background
    CGContextSetFillColorWithColor(ctx, backgrounfColor.CGColor);
    CGRect fillRect = CGRectZero;
    fillRect.size = expectedSize;
    CGContextFillRect(ctx, fillRect);
    // put the image
    CGContextSetFillColorWithColor(ctx, tintColor.CGColor);
    CGRect rect = CGRectMake((expectedSize.width - image.size.width) / 2., (expectedSize.height - image.size.height) / 2., image.size.width, image.size.height);
    [image drawInRect:rect];
}

UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return [UIColor colorWithPatternImage:newImage];

}

You still have to set the expectedWidth so it matches the empty label you put in the action's title. e.g: @" " -> 64.f

As you see with this approach you can set the button's background and tint color in code and not in the artwork itself.

Jim75
  • 737
  • 8
  • 13