Nous avons tous souvent affaire à des tableaux statiques, il peut s'agir des paramètres de notre application, des écrans d'autorisation, des écrans «à propos de nous» et bien d'autres. Mais souvent, les développeurs novices n'appliquent aucun modèle de développement pour de telles tables et écrivent tous dans une même classe un système non évolutif et inflexible.
Sur la façon dont je résous ce problème - sous la coupe.
De quoi tu parles?
Avant de résoudre le problème des tables statiques, vous devez comprendre de quoi il s'agit. Les tableaux statiques sont des tableaux où vous connaissez déjà le nombre de lignes et le contenu qu'elles contiennent. Exemples de tableaux similaires ci-dessous.

Le problème
Pour commencer, il convient d'identifier le problème: pourquoi ne pouvons-nous pas simplement créer un ViewController qui sera UITableViewDelegate et UITableViewDatasource et décrire simplement toutes les cellules dont vous avez besoin? Au moins - il y a 5 problèmes avec notre table:
- Difficile à mettre à l'échelle
- Dépendant de l'index
- Pas flexible
- Manque de réutilisation
- Nécessite beaucoup de code pour initialiser
Solution
La méthode de résolution du problème repose sur les fondements suivants:
- Suppression de la responsabilité de la configuration de la table dans une classe distincte ( Constructeur )
- Wrapper personnalisé sur UITableViewDelegate et UITableViewDataSource
- Connexion de cellules à des protocoles personnalisés pour réutilisation
- Création de vos propres modèles de données pour chaque table
Je veux d'abord montrer comment cela est utilisé dans la pratique - puis je montrerai comment tout cela est mis en œuvre sous le capot.
Implémentation
La tâche consiste à créer un tableau avec deux cellules de texte et une vide entre elles.
Tout d'abord, j'ai créé un
TextTableViewCell régulier avec
UILabel .
Ensuite, chaque UIViewController avec une table statique a besoin de son propre constructeur, créons-le:
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = <#type#> }
Lorsque nous l'avons hérité de
StaticConstructorContainer , tout d'abord, le protocole générique nous oblige à taper le modèle (
ModelType ) - c'est le type de modèle de cellule que nous devons également créer, faisons-le.
J'utilise enum pour cela, car il convient mieux à nos tâches et ici le plaisir commence. Nous remplirons notre tableau avec du contenu en utilisant des protocoles tels que:
Titré, Sous-titré, Coloré, Fonté et ainsi de suite. Comme vous pouvez le deviner, ces protocoles sont responsables de l'affichage du texte. Supposons que le protocole intitulé nécessite un
titre: chaîne? , et si notre cellule prend en charge les affichages de
titre , elle le remplira. Voyons à quoi ça ressemble:
protocol Fonted { var font: UIFont? { get } } protocol FontedConfigurable { func configure(by model: Fonted) } protocol Titled { var title: String? { get } } protocol TitledConfigurable { func configure(by model: Titled) } protocol Subtitled { var subtitle: String? { get } } protocol SubtitledConfigurable { func configure(by model: Subtitled) } protocol Imaged { var image: UIImage? { get } } protocol ImagedConfigurable { func configure(by model: Imaged) }
En conséquence, seule une petite partie de ces protocoles est présentée ici, vous pouvez le créer vous-même, comme vous le voyez - c'est très simple. Je vous rappelle que nous les créons 1 fois pour 1 but, puis les oublions et les utilisons calmement.
Notre cellule (
avec du texte ) prend essentiellement en charge les éléments suivants: la police du texte, le texte lui-même, la couleur du texte, la couleur d'arrière-plan de la cellule et généralement toutes les choses qui vous viennent à l'esprit.
Jusqu'à présent, nous n'avons besoin que du
titre . Par conséquent, nous héritons de notre modèle de Titled. À l'intérieur du modèle au cas où, nous indiquerons quels types de cellules nous aurons.
enum CellModel: Titled { case firstText case emptyMiddle case secondText var title: String? { switch self { case .firstText: return " - " case .secondText: return " - " default: return nil } } }
Puisqu'il n'y a pas d'étiquette au milieu (cellule vide), vous pouvez retourner nil.
Nous avons terminé la cellule C et vous pouvez l'insérer dans notre constructeur.
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel]
Et en fait, c'est tout notre code. On peut dire que notre table est prête. Remplissons les données et voyons ce qui se passe.
Oh oui, j'ai presque oublié. Nous devons hériter notre cellule du protocole TitledConfigurable afin qu'elle puisse insérer un titre en elle-même. Les cellules prennent également en charge les hauteurs dynamiques.
extension TextTableViewCell: TitledConfigurable { func configure(by model: Titled) { label.text = model.title } }
À quoi ressemble le constructeur rempli:
class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel] = [.firstText, .emptyMiddle, .secondText] func cellType(for model: CellModel) -> StaticTableViewCellClass.Type { switch model { case .emptyMiddle: return EmptyTableViewCell.self case .firstText, .secondText: return TextTableViewCell.self } } func configure(cell: UITableViewCell, by model: CellModel) { cell.selectionStyle = .none } func itemSelected(item: CellModel) { switch item { case .emptyMiddle: print(" ") default: print(" ...") } } }
Semble assez compact, non?
En fait, la dernière chose qu'il nous reste à faire est de tout connecter au ViewController'e:
class ViewController: UIViewController { private let tableView: UITableView = { let tableView = UITableView() return tableView }() private let constructor = ViewControllerConstructor() private lazy var delegateDataSource = constructor.delegateDataSource() override func viewDidLoad() { super.viewDidLoad() constructor.setup(at: tableView, dataSource: delegateDataSource) } }
Tout est prêt, nous devons faire de
delegateDataSource une propriété distincte dans notre classe afin que le maillon faible ne se casse dans aucune fonction.
Nous pouvons exécuter et tester:

Comme vous pouvez le voir, tout fonctionne.
Maintenant, résumons et comprenons ce que nous avons accompli:
- Si nous créons une nouvelle cellule et que nous voulons remplacer la cellule actuelle par celle-ci, nous le faisons en changeant une variable. Nous avons un système de table très flexible
- Nous réutilisons toutes les cellules. Plus vous liez de cellules à ce tableau, plus il est facile et facile de travailler avec lui. Idéal pour les grands projets.
- Nous avons réduit la quantité de code pour créer la table. Et nous devrons l'écrire encore moins quand nous avons beaucoup de protocoles et de cellules statiques dans le projet.
- Nous avons apporté la construction de tables statiques de l' UIViewController au constructeur
- Nous avons cessé de dépendre des indices, nous pouvons échanger en toute sécurité les cellules du tableau et la logique ne se cassera pas.
Code pour un projet de test à la fin de l'article.
Comment ça marche de l'intérieur?
Comment les protocoles fonctionnent, nous avons déjà discuté. Maintenant, nous devons comprendre comment fonctionne le constructeur entier et ses classes associées.
Commençons par le constructeur lui-même:
protocol StaticConstructorContainer { associatedtype ModelType var models: [ModelType] { get } func cellType(for model: ModelType) -> StaticTableViewCellClass.Type func configure(cell: UITableViewCell, by model: ModelType) func itemSelected(item: ModelType) }
Il s'agit d'un protocole commun qui nécessite des fonctionnalités que nous connaissons déjà.
Son
extension est plus intéressante:
extension StaticConstructorContainer { typealias StaticTableViewCellClass = StaticCell & NibLoadable func delegateDataSource() -> StaticDataSourceDelegate<Self> { return StaticDataSourceDelegate<Self>.init(container: self) } func setup<T: StaticConstructorContainer>(at tableView: UITableView, dataSource: StaticDataSourceDelegate<T>) { models.forEach { (model) in let type = cellType(for: model) tableView.register(type.nib, forCellReuseIdentifier: type.name) } tableView.delegate = dataSource tableView.dataSource = dataSource dataSource.tableView = tableView } }
La fonction de
configuration que nous avons appelée dans notre ViewController enregistre toutes les cellules pour nous et délègue
dataSource et
delegate .
Et
delegateDataSource () crée pour nous un wrapper
UITableViewDataSource et
UITableViewDelegate . Regardons ça:
class StaticDataSourceDelegate<Container: StaticConstructorContainer>: NSObject, UITableViewDelegate, UITableViewDataSource { private let container: Container weak var tableView: UITableView? init(container: Container) { self.container = container } func reload() { tableView?.reloadData() } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.estimatedHeight ?? type.height } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.height } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return container.models.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = container.models[indexPath.row] let type = container.cellType(for: model) let cell = tableView.dequeueReusableCell(withIdentifier: type.name, for: indexPath) if let typedCell = cell as? TitledConfigurable, let titled = model as? Titled { typedCell.configure(by: titled) } if let typedCell = cell as? SubtitledConfigurable, let subtitle = model as? Subtitled { typedCell.configure(by: subtitle) } if let typedCell = cell as? ImagedConfigurable, let imaged = model as? Imaged { typedCell.configure(by: imaged) } container.configure(cell: cell, by: model) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = container.models[indexPath.row] container.itemSelected(item: model) } }
Je pense qu'il n'y a pas de questions sur les fonctions
heightForRowAt ,
numberOfRowsInSection ,
didSelectRowAt , elles implémentent simplement des fonctionnalités claires. La méthode la plus intéressante ici est
cellForRowAt .
Dans ce document, nous n'implémentons pas la plus belle logique. Nous sommes obligés d'écrire chaque nouveau protocole dans les cellules ici, mais nous le faisons une fois - donc ce n'est pas si effrayant. Si le modèle est conforme au protocole, tout comme notre cellule, nous le configurerons. Si quelqu'un a des idées sur la façon d'automatiser cela, je serai heureux d'écouter dans les commentaires.
Cela met fin à la logique. Je n'ai pas touché de classes utilitaires tierces dans ce système,
vous pouvez lire le code complet ici .
Merci de votre attention!