4

I have a use case where I need to change the title of the UITableViewRowAction. For example, I have a restaurant cell, and when swipe right I show "bookmark(104)" where "bookmark" is the action and 104 means there have been 104 people bookmarked it. When click on it, I want it to change to "bookmark(105)" because obviously there's a new user(the current user himself) has bookmarked it. How do I do that? Tried the below code and it doesn't work.

let likeAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "bookmark\n\(count)", handler:{(action, indexpath) -> Void in
        ....
        count++
        action.title = "bookmark\n\(count)"
    });
JAL
  • 41,701
  • 23
  • 172
  • 300
Edmond
  • 614
  • 2
  • 11
  • 26
  • What is bookmark(104)? Title in NavigationBar? – Saqib Omer Jan 18 '16 at 07:30
  • @SaqibOmer title in row action view – Edmond Jan 18 '16 at 08:16
  • @the_UB There's no more code. I don't know how to code it. – Edmond Jan 18 '16 at 08:17
  • @SaqibOmer I have edited the question a little bit. – Edmond Jan 18 '16 at 08:20
  • When the user swipes the cell it shows the button with title `bookmark (104)` and when he taps it the cell swipes back hiding the button, right? So the next time the user swipes you are still seeing the text `bookmark (104)`? Have you inspected your count property to see if it actually has the value 105 on the second iteration? – Raphael Oliveira Jan 20 '16 at 22:41
  • I want the number to change to 105 before the cell swipes back. @RaphaelOliveira – Edmond Jan 20 '16 at 22:42
  • Got it, I don't know if it's possible to do that, sorry. Either way, I think the expected behaviour is for the cell to swipe back when you tap an action. At least that is what I see in Apple apps. – Raphael Oliveira Jan 20 '16 at 22:44
  • @Edmond that's not possible. You need to reload the cell in order for the text to change on the edit action. – JAL Jan 20 '16 at 22:46
  • @RaphaelOliveira I want to give the user some immediate feedback that their action is taking effect. – Edmond Jan 20 '16 at 22:48
  • @JAL That's possible. I've seen apps doing that. Just not sure what's the trick. maybe add some subview to it? – Edmond Jan 20 '16 at 22:49
  • @Edmond see my comment below on my answer. This is not possible with `UITableViewRowAction`, you would need to create your own custom sliding view. – JAL Jan 20 '16 at 22:49
  • @Edmond I've mentioned in my answer that there are third party libraries available to accomplish this. My answer aims to provide a pure Swift solution using `UITableViewRowAction`. – JAL Jan 21 '16 at 00:51

3 Answers3

3

Here's a quick and dirty example.

Say you have a class Restaurant with a name and likes value:

class Restaurant {
    var name: String?
    var likes: Int = 0
}

You initialize a bunch of Restaurant objects, and put them in an array called dataSource. Your table view data source methods will look like this:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.dataSource.count
}


override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = UITableViewCell(style: .Default, reuseIdentifier: "cell");
    cell.textLabel?.text = dataSource[indexPath.row].name

    return cell
}


// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    // This can be empty if you're not deleting any rows from the table with your edit actions
}

override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {

    // First, create a share action with the number of likes
    let shareAction = UITableViewRowAction(style: .Default, title: "\(self.dataSource[indexPath.row].likes)") { (action, indexPath) -> Void in

        // In your closure, increment the number of likes for the restaurant, and slide the cell back over
        self.dataSource[indexPath.row].likes++
        self.tableView.setEditing(false, animated: true)
    }

    return [shareAction] // return your array of edit actions for your cell.  In this case, we're only returning one action per row.
}

I'm not going to write a scrollable cell from scratch, since this question has a bunch of options you can use.

I was, however, intrigued by Andrew Carter's attempt to iterate through subviews to access the UIButton in the edit action directly. Here is my attempt:

First, create a reference to the UITableViewCell (or an array of cells), you wish to modify, for this example, I'll be using a single cell:

var cellRef: UITableViewCell?

// ...

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = UITableViewCell(style: .Default, reuseIdentifier: "cell");
    cell.textLabel?.text = dataSource[indexPath.row].name

    cellRef = cell;

    return cell
}

In your share action, iterate through the button's subviews. We're looking for the UITableViewCellDeleteConfirmationView and _UITableViewCellActionButton objects (private headers linked for reference).

let shareAction = UITableViewRowAction(style: .Default, title: "\(self.dataSource[indexPath.row].likes)") { (action, indexPath) -> Void in

    var deleteConfirmationView: UIView? // UITableViewCellDeleteConfirmationView

        if let subviews = self.cellRef?.subviews {
            for subview in subviews {
                if NSClassFromString("UITableViewCellDeleteConfirmationView") != nil {

                    if subview.isKindOfClass(NSClassFromString("UITableViewCellDeleteConfirmationView")!) {
                        deleteConfirmationView = subview
                        break
                    }
                }
            }
        }

    if let unwrappedDeleteView = deleteConfirmationView {
        if unwrappedDeleteView.respondsToSelector("_actionButtons") {
            let actionbuttons = unwrappedDeleteView.valueForKey("_actionButtons") as? [AnyObject]
            if let actionButton = actionbuttons?.first as? UIButton { // _UITableViewCellActionButton
                actionButton.setTitle("newText", forState: .Normal)
            }
        }

    }
}
Community
  • 1
  • 1
JAL
  • 41,701
  • 23
  • 172
  • 300
  • that's exactly the approach I tried but it doesn't work. The cell will swipe back, then if you swipe right again, you'll see the change. But I want it to be changed with swipe it back and forth. I want to give users some immediate feedback. – Edmond Jan 20 '16 at 22:46
  • 1
    @Edmond You can't change the text of the edit action without reloading the cell if you use `UITableViewRowAction`. You would have to create your own custom sliding view. – JAL Jan 20 '16 at 22:47
  • Is this approach safe? @JAL – Edmond Jan 21 '16 at 00:25
  • @Edmond App Store safe? Sure, you're iterating through subviews and using KVO. You're not using any private APIs. I've shipped plenty of apps where I've iterated through and modified/removed subviews of private classes. – JAL Jan 21 '16 at 00:37
  • I'm not sure if I agree it's safe @JAL - @Edmond. On the line `.valueForKey("_actionButtons")![0])!` - `valueForKey` will thrown an exception if the class is not key value compliant and you are also using implicit unwrapped optionals `!` which will crash if it finds a nil (which can happen if Apple change the hierarchy as I mentioned). – Raphael Oliveira Jan 21 '16 at 00:40
  • @RaphaelOliveira I'm not iterating through subviews there. `_actionButtons` is an array, and will always contain at least one object if your cell has an edit action. Look at the header I provided for `UITableViewCellDeleteConfirmationView`. – JAL Jan 21 '16 at 00:41
  • Yes @JAL, and this can change on iOS 10 and you might not even have a property called "_actionButtons". – Raphael Oliveira Jan 21 '16 at 00:43
  • @RaphaelOliveira I guess I can do a nil check at that line, if it's nil then do nothing and wait for the next swipe to show the change. If not nil then update the number. – Edmond Jan 21 '16 at 00:43
  • @RaphaelOliveira Yes, obviously both of our answers could break if Apple choses to change their APIs up. – JAL Jan 21 '16 at 00:44
  • Yeah, don't use any implicit unwrapped optionals. I'm not sure about the `valueForKey` though, I'm worrier about it throwing an exception. – Raphael Oliveira Jan 21 '16 at 00:44
  • That's why I disagree when you said it's safe to use on the App Store @JAL. "@Edmond App Store safe? Sure,...." – Raphael Oliveira Jan 21 '16 at 00:45
  • @RaphaelOliveira you can use `respondsToSelector` to check to make sure that `valueForKey` will now throw an exception. – JAL Jan 21 '16 at 00:46
  • 1
    @RaphaelOliveira edited my answer with a "safer" approach with `respondsToSelector` and unwrapping optionals. – JAL Jan 21 '16 at 00:49
  • `NSClassFromString("UITableViewCellDeleteConfirmationView")!` is still potentially dangerous but well, good job mate! – Raphael Oliveira Jan 21 '16 at 00:53
  • @RaphaelOliveira Added an additional check to make sure that `NSClassFromString` actually returns a valid class... – JAL Jan 21 '16 at 00:56
  • I think we have different definitions of App Store safe. App Store safe means that this app will pass Apple's inspection. I didn't ensure that the app wouldn't crash. – JAL Jan 21 '16 at 00:57
  • @JAL unwrappedDeleteView.respondsToSelector("_actionButtons") returns false. I have to remove this line. I think it's fine. It's good enough for the first version of my app. Thank you – Edmond Jan 21 '16 at 00:58
  • Oh got it, yeah, by safety I was meaning not start crashing after an iOS update. Got your point. – Raphael Oliveira Jan 21 '16 at 01:49
3

This answer uses private API and it's NOT recommended to be used on the App Store. Apparently there is no way to change the title of the native UITableViewRowAction. You may have to implement your custom solution as suggested by others to achieve the result you want.

Here I'm traversing the subviews of UITableViewCell which contains private subviews and are subject to change so your code may crash on future iOS releases if Apple changes the view hierarchy. I found the header of UIButtonLabel here.

The current view hierarchy as per iOS 9.2 is

UITableViewCell
    UITableViewCellDeleteConfirmationView
        _UITableViewCellActionButton
            UIButtonLabel
                UIButton

Here is the code:

func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
    let shareAction = UITableViewRowAction(style: .Default, title: "\(dataList[indexPath.row].likes)") { (action, indexPath) -> Void in
        self.dataList[indexPath.row].likes++
        let cell = tableView.cellForRowAtIndexPath(indexPath)
        let privateView = cell!.subviews[0].subviews[0].subviews[0]
        let privateButton = privateView.valueForKey("_button") as! UIButton
        privateButton.setTitle("\(self.dataList[indexPath.row].likes)", forState: .Normal)
    }

    return [shareAction]
} 
Raphael Oliveira
  • 7,751
  • 5
  • 47
  • 55
  • 1
    This approach is potentially dangerous. You should iterate through the subviews, checking the class of each one, rather than assuming the order will not change. – JAL Jan 21 '16 at 00:06
  • 1
    I know that, this is just a theoretical approach, I stated on my answer the subviews are subject to modification and may cause future crashes and should not be used on the App Store. – Raphael Oliveira Jan 21 '16 at 00:08
  • Ok. Now I think the only way to do this is to create a custom cell, add some button as subview to that cell, and when swipe right, move that button to screen. This library might be able to do that. https://github.com/torryharris/TH-SwipeCell. Can one of you guys summarize an answer so that I can mark that accepted. – Edmond Jan 21 '16 at 00:24
2

@JAL is totally right- you need to make your own sliding view to accomplish this, or be ok with reloading the cell and having it updated on the next slide out. Just for fun I tried to hack my way through the subviews and find the label and was able to find it, but what's funny is Apple has somehow blocked any changes being made to the text of that label. You can change it's background color / other properties but not the text!

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
        return cell
    }

    func findRowActionLabelForCell(cell: UITableViewCell?) -> UILabel? {
        guard let cell = cell else {
            return nil
        }
        var label: UILabel? = nil

        for view in cell.subviews {
            label = findRowActionLabelForView(view)
            if label != nil {
                break
            }
        }

        return label
    }

    func findRowActionLabelForView(view: UIView) -> UILabel? {
        for subview in view.subviews {
            if let label = subview as? UILabel {
                return label
            } else {
                return findRowActionLabelForView(subview)
            }
        }

        return nil
    }

    func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
        let action = UITableViewRowAction(style: .Default, title: "Test", handler: { (action, indexPath) -> Void in

            let cell = self.tableView.cellForRowAtIndexPath(indexPath)
            if let label = self.findRowActionLabelForCell(cell) {
                label.text = "New Value"
                label.backgroundColor = UIColor.blueColor()
            }

        })

        return [action]
    }

    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {

    }

}

Before After

  • 1
    But there is no `UILabel` according to my inspection. Inspecting the subviews I found `cell!.subviews[0]` to be a `UITableViewCellDeleteConfirmationView`, `cell!.subviews[0].subviews[0]` to be a `_UITableViewCellActionButton` and `cell!.subviews[0].subviews[0].subviews[0]` to be a `UIButtonLabel`. Yet, we are dealing with private API here so it doesn't really matter. We probably can't control it. – Raphael Oliveira Jan 20 '16 at 23:25
  • @RaphaelOliveira good to know it. Thanks for digging into it! – Edmond Jan 20 '16 at 23:30
  • @RaphaelOliveira Nice attempt. I've edited my answer with a version that successfully modifies the `_UITableViewCellActionButton`'s `text` value. Check it out. – JAL Jan 21 '16 at 00:05