Open Uitableview Edit Action Buttons Programmatically

Open UITableView edit action buttons programmatically

Apple has a private API that lets you do this, however, be warned that this may get your app rejected from the App Store unless you obfuscate the usage of said API using something like Method Swizzling. Here are the steps to do so:

  1. Create a protocol called PrivateMethodRevealer that lets you access the required private Apple APIs, namely the ones to show and dismiss edit actions. Credits to this answer for providing this method of exposing private APIs. The methods in the protocol are declared as optional, so that in case Apple changes the name of the method, the app will not crash, but rather, it'll just not show the edit actions.

    @objc protocol PrivateMethodRevealer {
    optional func setShowingDeleteConfirmation(arg1: Bool)
    optional func _endSwipeToDeleteRowDidDelete(arg1: Bool)
    }

    Note that although the methods refer to delete, this shows all the UITableViewRowActions that are on the cell.

  2. Create a function that handles the showing and hiding of the edit actions in your UITableViewCell subclass (if you have one), or create the method in a UITableViewCell extension. I will name this method showActions for demonstrative purposes.

  3. Add the following body to your function:

    func showActions() {
    (superview?.superview as? AnyObject)?._endSwipeToDeleteRowDidDelete?(false)
    (self as AnyObject).setShowingDeleteConfirmation?(true)
    }

    This firstly dismisses any visible cells' editing actions, by calling _endSwipeToDeleteRowDidDelete: on the UITableView (which is the cell's superview's superview), and then shows the cell's own editing actions (by calling setShowingDeleteConfirmation:). Note that we need to dismiss other cells' actions as showing multiple rows with edit actions is extremely buggy.

  4. If you want, you may also create a button in the UIViewController that dismisses any currently editing cells. To do this, just call the following method, where tableView is your reference to the UITableView:

    (tableView as AnyObject)?._endSwipeToDeleteRowDidDelete?(false)

If the swipe gestures between your UIPageViewController and UITableViewCells are conflicting, simply override the tableView:editingStyleForRowAtIndexPath: method to return .None.

In the end, your code might produce the following result
Demo video

EDIT: Here is a quick way to hide the usage of your API using method swizzling. Credits to this website for providing the basic implementation of this method. Be warned that I can't guarantee that it'll work, as it isn't possible to test it live.

To do this, replace the protocols with the following code, and wherever you call setShowingDeleteConfirmation(true) or _endSwipeToDeleteRowDidDelete(false), replace it with showRowActions() and hideRowActions() instead. This method appears to have some unintended effects however, such as the UITableViewCells not responding to user interaction whilst edit actions are visible.

extension UITableViewCell {
func showRowActions(arg1: Bool = true) {}

public override static func initialize() {
struct Static {
static var token: dispatch_once_t = 0
}

guard self === UITableViewCell.self else {return}

dispatch_once(&Static.token) {
let hiddenString = String(":noitamrifnoCeteleDgniwohStes".characters.reverse())
let originalSelector = NSSelectorFromString(hiddenString)
let swizzledSelector = #selector(showRowActions(_:))
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}

extension UITableView {
func hideRowActions(arg1: Bool = false) {}

public override static func initialize() {
struct Static {
static var token: dispatch_once_t = 0
}

guard self === UITableView.self else {return}

dispatch_once(&Static.token) {
let hiddenString = String(":eteleDdiDwoReteleDoTepiwSdne_".characters.reverse())
let originalSelector = NSSelectorFromString(hiddenString)
let swizzledSelector = #selector(hideRowActions(_:))
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}

How to trigger UITableViewCell editActions programmatically?

Short answer is - there is no such way.

However, if you really need something like that, you can mimic this behaviour, though it requires lot more implementation and state handling on your own.

Here is a quick and very dirty solution, which overrides touchesEnded method of your custom cell. Remember to set up Cell as a dynamic prototype of the cell in your table view in relevant storyboard and set its reuse identifier to identitifer.

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, CellDelegate {

@IBOutlet weak var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
}

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

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

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "identifier") as? Cell else {
return UITableViewCell()
}
cell.textLabel?.text = "\(indexPath.row)"
cell.indexPath = indexPath
cell.delegate = self
return cell
}

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return nil
}

func doAction(for cell: Cell) {
let indexPath = cell.indexPath
print("doing action for cell at: \(indexPath!.row)")
// your implementation for action
// maybe delete a cell or whatever?
cell.hideFakeActions()

}

}

protocol CellDelegate: class {
func doAction(for cell: Cell)
}

class Cell: UITableViewCell {

weak var delegate: CellDelegate?
var indexPath: IndexPath!

@IBOutlet weak var buttonAction1: UIButton?
@IBOutlet weak var constraintButtonFakeActionWidth: NSLayoutConstraint?

override func awakeFromNib() {
self.constraintButtonFakeActionWidth?.constant = 0
}
override func touchesEnded(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else {
return
}
if self.point(inside: point, with: event) {

print("event is in cell number \(indexPath.row)")
self.showFakeActions()

}
}

func showFakeActions() {
self.constraintButtonFakeActionWidth?.constant = 80
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
}

func hideFakeActions() {
self.constraintButtonFakeActionWidth?.constant = 0
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
}

@IBAction func fakeAction() {
delegate?.doAction(for: self)
}
}

So how does it work? Each cell is a UIView which inherits from abstract UIResponder interface. You can override its methods to do actions on your own on behalf of events that are dispatched to them. This is the first step where you override touchesEnded.

Take a look at the screenshot from interface builder - you have to hook up the constraint.
Sample Image

I've also implemented the delegate which returns nil for all edit actions of the cells, so they don't interfere with your workaround.

Next, you set up a custom delegate to get a callback from the cell. I also attached IndexPath to the cell for the convenience of managing data in the dataSource, which you have to implement.

Remember that this code lacks a lot, like prepareForReuse method implementation. Also, you probably want to do additional checks in touchesEnded override which would guarantee that this delegate callback is not fired more than once per touch and prevent multiple touches. The logic for disabling user interaction on a cell is not implemented here as well. And interface requires fine-tuning (like text appears to be squashed during the animation).

UITableViewCell Buttons with action

I was resolving this using a cell delegate method within UITableViewCell's subclass.

Quick overview:

1) Create a protocol

protocol YourCellDelegate : class {
func didPressButton(_ tag: Int)
}

2) Subclass your UITableViewCell (if you haven't done so):

class YourCell : UITableViewCell
{
var cellDelegate: YourCellDelegate?
@IBOutlet weak var btn: UIButton!
// connect the button from your cell with this method
@IBAction func buttonPressed(_ sender: UIButton) {
cellDelegate?.didPressButton(sender.tag)
}
...
}

3) Let your view controller conform to YourCellDelegate protocol that was implemented above.

class YourViewController: ..., YourCellDelegate {  ... }

4) Set a delegate, after the cell has been defined (for reusing).

let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! YourCell
cell.cellDelegate = self
cell.btn.tag = indexPath.row

5) In the same controller (where is your implemented UITableView delegate/datasource), put a method from YourCellDelegate protocol.

func didPressButton(_ tag: Int) {
print("I have pressed a button with a tag: \(tag)")
}

Now, your solution is not tag / number dependent. You can add as many buttons as you want, so you are ready to get response via delegate regardless how many buttons you want to install.

This protocol-delegate solution is preferred in iOS logic and it can be used for other elements in table cell, like UISwitch, UIStepper, and so on.



Related Topics



Leave a reply



Submit