Solution universelle pour UITableViewRowAction et UIContextualAction



Beaucoup ont rencontré le problème de l'implémentation d'un magnifique mappage de bouton pour un UITableViewCell lorsqu'il est déplacé vers la gauche. Certains ont utilisé la fonctionnalité standard «prête à l'emploi», d'autres se soucient de leur propre implémentation, tandis que d'autres ont géré avec des caractères Unicode. Dans cet article, je vais vous expliquer comment obtenir une personnalisation maximale de UITableViewRowAction.

Tout d'abord, une petite théorie: UITableViewRowAction est une classe qui permet à l'utilisateur d'effectuer certaines actions avec cette cellule lors du déplacement d'une cellule vers la gauche (comme la suppression, la modification, etc.). Chaque instance de cette classe représente une action à exécuter et comprend du texte, un style et un gestionnaire de clics. Le SDK iOS nous permet de personnaliser l'UITableViewRowAction comme suit:

public convenience init(style: UITableViewRowActionStyle, title: String?, handler: @escaping (UITableViewRowAction, IndexPath) -> Swift.Void) 

Vous pouvez également définir backgroundColor.

Méthodes existantes.


Considérez les exemples d'implémentation existants ou ce que j'ai trouvé sur Internet.
Méthode numéro 1 . Utilisez la fonctionnalité «prête à l'emploi». Cela suffit pour utiliser des boutons ordinaires avec du texte et un arrière-plan. Cela semble trop simple et ennuyeux:


Méthode numéro 2 . Définissez backgroundColor à l'aide de UIColor (patternImage: UIImage) . Cela atteindra la beauté, mais cela privera de polyvalence, ajoutera un certain nombre de restrictions et de difficultés. Étant donné que la largeur de UITableViewRowAction est calculée par la taille du texte, vous devez spécifier le titre comme un ensemble de caractères d'espacement blanc d'une certaine largeur. Nous ne pourrons pas utiliser cette méthode pour les cellules avec des hauteurs dynamiques. Si la cellule augmente en hauteur, elle ressemblera à ceci:



Méthode numéro 3 . Utilisez des caractères Unicode. Il n'est pas nécessaire de calculer la taille du texte avec des espaces, et cette méthode convient aux cellules à hauteur dynamique. C'est joli:



Mais si nous voulons faire le texte sous l'icône, cette méthode ne nous conviendra pas.

Méthode numéro 4 . Utilisez CoreGraphics pour rendre les composants de l'interface utilisateur. L'idée est de dessiner manuellement une image avec la disposition souhaitée des éléments (texte et image). Dans ce cas, vous devez calculer les tailles et les coordonnées de l'emplacement de chaque élément. En utilisant cette méthode, nous pouvons créer à la fois du texte et une image, en les organisant selon nos besoins. Mais cette méthode est difficile à mettre en œuvre. Nous voulons quelque chose de plus simple.

Autres implémentations . Il existe quelques autres solutions toutes faites. Ils utilisent leur propre implémentation de la cellule avec l'ajout de ScrollView et d'autres logiques. Par exemple, SwipeCellKit ou MGSwipeTableCell . Mais ces solutions ne permettent pas d'obtenir un comportement iOS standard; elles ne couvrent qu'une seule cellule, contrairement à UITableViewRowAction, qui est créé dans le délégué de table. Par exemple, en utilisant l'une de ces solutions, si vous ouvrez des actions de décalage vers la gauche pour plusieurs cellules, vous devrez fermer manuellement ces actions dans chacune d'elles, ce qui est extrêmement gênant. Pour ce faire, vous devez ajouter votre propre logique pour contrôler les actions ouvertes sur un balayage. Nous voulons créer une solution simple qui ne nécessite pas de coûts supplémentaires, comme si nous utilisons l'implémentation standard de UITableViewRowAction.

Commençons


Essayons de créer une solution universelle qui vous permet de créer UITableViewRowAction avec n'importe quelle conception dont nous avons besoin.

Créez notre classe descendante à partir de UITableViewRowAction et définissez la méthode de création d'instance principale:

 typealias RowActionTapHandler = (RowAction, IndexPath) -> Void class RowAction: UITableViewRowAction { static func rowAction(with actionHandler: @escaping RowActionTapHandler) -> Self { let rowAction = self.init(style: .default, title: nil) { action, indexPath in guard let action = action as? RowAction else { return } actionHandler(action, indexPath) } return rowAction } } 

Étant donné que UITableViewRowAction n'a pas d'initialiseur principal, pour plus de commodité, nous pouvons créer une méthode statique dans laquelle nous créons un UITableViewRowAction de la manière standard et y lançons un actionHandler pour gérer le clic.

Essayons maintenant de réaliser la personnalisation universelle du bouton UITableViewRowAction. L'idée est d'obtenir l'apparence du bouton à partir de l'UIView terminé (qui peut être configuré dans un xib normal). Nous pouvons donc créer un bouton de toute nature, en fonction des préférences individuelles (n'importe quelle police de texte, taille et disposition des éléments, etc.).

Nous avons deux choses que nous pouvons changer dans UITableViewRowAction: backgroundColor et title . Pour le moment, la seule façon de modifier l'apparence du bouton UITableViewRowAction consiste à modifier la propriété backgroundColor. Par conséquent, notre objectif est de transformer: UIView -> UIImage -> UIColor. Nous pouvons obtenir la couleur en utilisant l'image comme suit: UIColor (patternImage: UIImage) . Et nous pouvons obtenir l'image d'UIView en la rendant à l'aide du framework CoreGraphics standard.

Mais nous allons d'abord décider comment définir la taille du bouton: "prêt à l'emploi", il n'y a pas cette fonctionnalité, et la taille dans UITableViewRowAction est calculée par la taille du texte du champ de titre.

Après avoir effectué des expériences (mesure de la largeur calculée et réelle du bouton), j'ai trouvé un caractère Unicode vierge qui a la plus petite largeur non nulle: U + 200A

Nous allons l'utiliser et créer une constante:

 private enum Constants { static let placeholderSymbol = "\u{200A}" } 

Nous allons écrire une fonction pour obtenir une chaîne de caractères blancs correspondant à la taille dont nous avons besoin:

 private static let placeholderSymbolWidth: CGFloat = { let flt_max = CGFloat.greatestFiniteMagnitude let maxSize = CGSize(width: flt_max, height: flt_max) let attributes = [NSFontAttributeName : UIFont.systemFont(ofSize: UIFont.systemFontSize)] let boundingRect = Constants.placeholderSymbol.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) return boundingRect.width }() private func emptyTitle(for size: CGSize) -> String { var usefulWidth = size.width - Constants.minimalActionWidth usefulWidth = usefulWidth < 0 ? 0 : usefulWidth let countOfSymbols = Int(floor(usefulWidth * Constants.shiftFactor / RowAction.placeholderSymbolWidth)) return String(repeating: Constants.placeholderSymbol, count: countOfSymbols) } 

La variable placeholderSymbolWidth obtient la largeur de notre caractère Unicode et la fonction emptyTitle (pour la taille: CGSize) -> String calcule le nombre de caractères d'espacement correspondant à la taille et renvoie une chaîne de ces caractères. La formule ci-dessus utilise la constante shiftFactor . Il s'agit du coefficient de déviation de la largeur par rapport à la valeur nominale. Le fait est qu'à mesure que la largeur de UITableViewRowAction augmente, une petite erreur augmente (c'est-à-dire que la largeur réelle du bouton diffère de la largeur calculée par la formule habituelle). Pour éviter cela, nous utilisons cette constante, dérivée expérimentalement.

UITableViewRowAction a également une largeur minimale de 30 (par exemple, si vous définissez title = ""). Cette largeur est le minimum requis pour l'indentation à gauche et à droite du titre.
Mettez à jour nos constantes:

 private enum Constants { static let placeholderSymbol = "\u{200A}" static let minimalActionWidth: CGFloat = 30 static let shiftFactor: CGFloat = 1.1 } 

Maintenant, récupérons l'image de l'UIView. Nous écrirons la fonction suivante:

 private func image(from view: UIView) -> UIImage? { UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, UIScreen.main.scale) guard let context = UIGraphicsGetCurrentContext() else { assertionFailure("Something wrong with CoreGraphics image context"); return nil } context.setFillColor(UIColor.white.cgColor) context.fill(CGRect(origin: .zero, size: view.bounds.size)) view.layer.render(in: context) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img } 

Cette fonction convertit notre vue en image. Dans ce cas, la taille de l'image correspond à la taille de la vue .

La dernière étape reste, où nous implémentons la méthode pour définir UITableViewRowAction en utilisant UIView:

 func setupForCell(with view: UIView) { guard let img = image(from: view) else { return } self.title = emptyTitle(for: img.size) self.backgroundColor = UIColor(patternImage: img) } 

Ici, nous utilisons les fonctions écrites plus tôt pour configurer notre bouton et définir le backgroundColor en utilisant l'image résultante.

C'est tout. Nous avons obtenu une solution universelle pour UITableViewRowAction et maintenant nous pouvons la personnaliser de n'importe quelle manière en utilisant UIView.



À partir d'iOS 11, Apple a ajouté une nouvelle classe pour l'action de balayage pour les cellules du tableau - UIContextualAction. Il vous permet de spécifier immédiatement l'image qui sera affichée sur l'action sur le balayage (c'est ce qui nous manquait depuis toutes ces années d'utilisation de UITableViewRowAction). Il était censé nous faciliter la vie, mais en plus d'une nouvelle opportunité, il a créé des problèmes supplémentaires. Cette classe impose des restrictions sur la taille de l'image, ne permet pas l'utilisation de texte et d'image en même temps, et aussi, l'image doit avoir un format spécial (image modèle). Par conséquent, j'ai finalisé ma solution pour prendre en charge les actions de balayage dans iOS 11. Maintenant, afin de créer une UITableViewRowAction ou UIContextualAction, vous devez d'abord créer la fabrique RowActionFactory, la configurer à l'aide de notre vue (comme nous l'avons fait ci-dessus dans la méthode setupForCell (avec vue: UIView)) et renvoyez l'entité dont nous avons besoin: UITableViewRowAction ou UIContextualAction (en utilisant respectivement les méthodes rowAction () ou contextualAction (pour indexPath: IndexPath) ). Voici un exemple d'utilisation de RowActionFactory:

 @available(iOS 11.0, *) func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { var contextualActions: [UIContextualAction] = [] for actionType in rowActionTypes { // actionType -     let rowActionFactory = RowActionFactory { (indexPath) in tableView.setEditing(false, animated: true) } let rowActionView = <...> //  UIView     rowActionFactory.setupForCell(with: rowActionView) if let contextualAction = rowActionFactory.contextualAction(for: indexPath) { contextualActions.append(contextualAction) } } let swipeActionsConfiguration = UISwipeActionsConfiguration(actions: contextualActions) swipeActionsConfiguration.performsFirstActionWithFullSwipe = false return swipeActionsConfiguration } 

La solution finale peut être trouvée ici: TCSCustomRowActionFactory (github.com) .

Source: https://habr.com/ru/post/fr414019/


All Articles