web-dev-qa-db-fra.com

Ouvrez les boutons d'action d'édition UITableView par programme

J'ai une UIPageViewController qui a UITableViewControllers à l'intérieur, et les gestes de glissement vers la gauche sont en conflit entre la UIPageViewController pour changer entre les vues et le geste UITableViewCells pour ouvrir les actions d'édition. Je dois donc afficher les actions d'édition lorsqu'un clic est effectué sur un bouton donné. la cellule.

Ma question est la suivante: puis-je afficher les boutons d’action de montage par programmation au lieu de les afficher sur le geste de balayage?

28
Firas

Apple dispose d'une API privée qui vous permet de le faire, mais sachez que votre application peut être rejetée de l'App Store à moins que vous ne dissimuliez l'utilisation de cette API en utilisant quelque chose comme Method Swizzling. Voici les étapes à suivre:

  1. Créez un protocole appelé PrivateMethodRevealer qui vous permet d'accéder aux API Apple privées requises, à savoir celles permettant d'afficher et de rejeter les actions de montage. Crédits à cette réponse pour avoir fourni cette méthode d’exposition d’API privées. Les méthodes du protocole sont déclarées en tant que optional, de sorte que si Apple change le nom de la méthode, l'application ne se plante pas, mais ne montrera simplement pas les actions d'édition.

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

    Notez que bien que les méthodes se réfèrent à delete, cela affiche toutes les UITableViewRowActions qui sont sur la cellule.

  2. Créez une fonction qui gère l'affichage et le masquage des actions d'édition dans votre sous-classe UITableViewCell (si vous en avez une) ou créez la méthode dans une UITableViewCellextension. Je nommerai cette méthode showActions à des fins de démonstration.

  3. Ajoutez le corps suivant à votre fonction:

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

    Cela supprime tout d'abord les actions d'édition des cellules visibles, en appelant _endSwipeToDeleteRowDidDelete: sur la UITableView (qui est la vue du dessus de la cellule), puis affiche ses propres actions d'édition (en appelant setShowingDeleteConfirmation:). Notez que nous devons ignorer les actions d'autres cellules car l'affichage de plusieurs lignes avec des actions d'édition est extrêmement bogué.

  4. Si vous le souhaitez, vous pouvez également créer un bouton dans la variable UIViewController qui supprime les cellules en cours d’édition. Pour ce faire, appelez simplement la méthode suivante, où tableView est votre référence à la UITableView:

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

Si les mouvements de balayage entre vos UIPageViewController et UITableViewCells sont en conflit, substituez simplement la méthode tableView:editingStyleForRowAtIndexPath: pour renvoyer .None.

En fin de compte, votre code peut produire le résultat suivant  Demo video

EDIT: Voici un moyen rapide de masquer l'utilisation de votre API à l'aide de la méthode swizzling. Crédits à ce site Web pour la mise en œuvre de base de cette méthode. Soyez averti que je ne peux pas garantir que cela fonctionnera, car il n'est pas possible de le tester en direct.

Pour ce faire, remplacez les protocoles par le code suivant et remplacez-le chaque fois que vous appelez setShowingDeleteConfirmation(true) ou _endSwipeToDeleteRowDidDelete(false) par showRowActions() et hideRowActions(). Cette méthode semble toutefois avoir des effets inattendus, tels que les UITableViewCells ne répondant pas à l’interaction de l’utilisateur alors que les actions d’édition sont visibles.

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)
        }
    }
}
10
kabiroberai

Dans mon cas (Swift 3, iOS11) MGSwipeTableCell fonctionne parfaitement. Vous pouvez configurer les boutons d'appel dans 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: prettyIdentifier, for: indexPath)

    cell.allowsButtonsWithDifferentWidth = true
    cell.rightButtons = [MGSwipeButton(title: "Delete\npermanently", backgroundColor: #colorLiteral(red: 0.9745360017, green: 0.7205639482, blue: 0.3932176828, alpha: 1)), MGSwipeButton(title: "Undo",backgroundColor: .black)]
    cell.rightSwipeSettings.transition = .rotate3D
    cell.delegate = self

    return cell
}

au lieu de

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

et attraper des touches dans

extension RecordingViewController: MGSwipeTableCellDelegate {
    func swipeTableCell(_ cell: MGSwipeTableCell, tappedButtonAt index: Int, direction: MGSwipeDirection, fromExpansion: Bool) -> Bool {
        cell.hideSwipe(animated: true)
        // do your stuff here like
        if index == 0 {
            print("right button")
        }

        return true
    }
}
0
WINSergey

J'étais sur le même chemin que kabiroberai pour trouver une solution à cette réponse, mais j'ai adopté une approche différente avec deux protocoles distincts plutôt qu'un protocole Objective-C/NSObject potentiellement abusif. Cela évite également de rendre les méthodes de protocole facultatives.

Commencez par créer deux protocoles distincts pour exposer les méthodes privées à la fois sur UITableView et UITableViewCell. J'ai trouvé ceux-ci en fouillant dans les en-têtes privés de chaque classe.

@objc protocol UITableViewCellPrivate {
    func setShowingDeleteConfirmation(arg1: Bool)
}

@objc protocol UITableViewPrivate {
    func _endSwipeToDeleteRowDidDelete(arg1: Bool)
}

Dans cellForRowAtIndexPath, conservez une référence à la cellule (ou aux plusieurs cellules) pour laquelle vous souhaitez afficher les actions de modification pour:

class MyTableViewController: UITableViewController {

    var cell: UITableViewCell?

    // ...

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell")!

       // ...        

        if indexPath.row == 1 {
            self.cell = cell
        }

        return cell
    }
}

Maintenant, tirez les méthodes privées. J'ai utilisé performSelector:withObject:afterDelay, ou vous pouvez utiliser un bouton.

override func viewDidLoad() {
    super.viewDidLoad()    
    self.performSelector(#selector(showActionsForCell), withObject: nil, afterDelay: 2.0)
}

func showActionsForCell() {

    if let cell = cell {
        let cellPrivate = unsafeBitCast(cell, UITableViewCellPrivate.self)
        let tableViewPrivate = unsafeBitCast(self.tableView, UITableViewPrivate.self)

        // Dismiss any other edit actions that are open
        tableViewPrivate._endSwipeToDeleteRowDidDelete(false)

        // Open the edit actions for the selected cell
        cellPrivate.setShowingDeleteConfirmation(true)
    }
}

Appeler directement unsafeBitCast est dangereux. Pour des raisons de sécurité, vérifiez si vos UITableView et UITableViewCell répondent à ces sélecteurs ou rendez les fonctions facultatives.

0
JAL