UITableViewRowAction image for title
Asked Answered
K

9

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!

Kermitkermy answered 2/4, 2015 at 20:29 Comment(3)
Answered here -#44772278Eider
Update answered for both iOS 11 and earlier #27741384Holocaust
the size of the image and row height should be match???Besotted
B
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"]];
Bowlin answered 2/4, 2015 at 20:48 Comment(11)
For the images, I put together a CocoaPod that would try to solve this as elegantly as possible for the time being: github.com/benguild/BGTableViewRowActionWithImageSimulated
@dimpiax: Great. This works for image. But how to put both background color and image together.Seda
@AnoojKrishnanG, hi! I propose two solutions: 1. background color you set concretely in image; 2. set for UIImage imageWithRenderingMode(.AlwaysTemplate) special color.Bowlin
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.Nightingale
I can't seem to make this size the image the way I want, regardless of how many spaces I use.Molini
@Molini what is size of your image?Bowlin
It's a .pdf vectorMolini
Do you think it would work better if it was a .png? @BowlinMolini
@Molini firstly get bounds of image that you put inside, secondly try like png.Bowlin
Now your answer does not work: Setting a pattern color as backgroundColor of UITableViewRowAction is no longer supported.Borszcz
@Borszcz tnx, check the solution from iOS 11.Bowlin
C
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])
    }
}
Causey answered 3/7, 2018 at 11:9 Comment(0)
A
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!)
  }
}
Animism answered 5/1, 2018 at 22:32 Comment(3)
Lovely ... Thanks !Honniball
please can you show some code sample of how to use this, plus what value should i put for the iconSizePercentage is it 0-100%?Tarshatarshish
Sorry for the delay. You should use this helper in the tableView editActionsForRowAt delegate. You create an UITableViewRowAction and you apply the setIcon to it. The percentage is from 0 to 1.0Animism
S
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]
}
Statist answered 29/7, 2016 at 9:58 Comment(5)
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...:)Haff
@Jayesh Miruliya, how did you know that UITableViewRowAction had a button property? This is NOT in the documentation: developer.apple.com/reference/uikit/uitableviewrowaction. Is this technically a hack ?Frescobaldi
@Frescobaldi sadly _setButton is a private API.Ornstead
No one should use this solution. You have no way of knowing if Apple stops calling setButton.Poundfoolish
try above commentStatist
S
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])
}
Steviestevy answered 8/4, 2021 at 13:54 Comment(0)
R
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!)
    } 
Ripieno answered 8/5, 2018 at 13:16 Comment(0)
H
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)
Heinrich answered 12/12, 2018 at 5:13 Comment(0)
S
0
delActions.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"delete.png"]];
Scenic answered 9/10, 2015 at 13:13 Comment(0)
R
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.

Reader answered 1/12, 2017 at 16:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.