Création d'éléments d'interface par programme à l'aide de PureLayout (Partie 2)

Bonjour, Habr! Je vous présente la traduction de l'article Création d'éléments d'interface utilisateur à l'aide de PureLayout par Aly Yaka.

image

Bienvenue dans la deuxième partie de l'article sur la création par programmation d'une interface à l'aide de PureLayout. Dans la première partie, nous avons créé l'interface utilisateur d'une simple application mobile entièrement en code, sans utiliser de Storyboards ou NIBs. Dans ce guide, nous couvrirons certains des éléments d'interface utilisateur les plus couramment utilisés dans toutes les applications:

  • UINavigationController / Bar
  • UITableView
  • UITableViewCell à dimensionnement automatique


UINavigationController


Dans notre application, vous avez probablement besoin d'une barre de navigation pour que l'utilisateur puisse passer de la liste de contacts à des informations détaillées sur un contact spécifique, puis revenir à la liste. UINavigationController peut facilement résoudre ce problème en utilisant la barre de navigation.

UINavigationController n'est qu'une pile dans laquelle vous déplacez de nombreuses vues. L'utilisateur voit la vue la plus élevée (celle qui a été déplacée en dernier) en ce moment (sauf lorsque vous avez une autre vue présentée en plus de cela, disons favoris). Et lorsque vous appuyez sur les contrôleurs de vue de dessus du contrôleur de navigation, le contrôleur de navigation crée automatiquement un bouton «retour» (en haut à gauche ou à droite selon les préférences linguistiques actuelles de l'appareil), et en appuyant sur ce bouton, vous revenez à la vue précédente.

Tout cela est pris en charge par le contrôleur de navigation. Et l'ajout d'un autre ne prendra qu'une seule ligne de code supplémentaire (si vous ne souhaitez pas personnaliser la barre de navigation).
Accédez à AppDelegate.swift et ajoutez la ligne de code suivante ci-dessous, laissez viewController = ViewController ():

 let navigationController = UINavigationController(rootViewController: viewController) 

Maintenant, changez self.window? .RootViewController = viewController self.window? .RootViewController = navigationController self.window? .RootViewController = viewController self.window? .RootViewController = navigationController self.window? .RootViewController = viewController self.window? .RootViewController = navigationController . Dans la première ligne, nous avons créé une instance de UINavigationController et lui avons viewController notre viewController tant que rootViewController , qui est le contrôleur de vue tout en bas de la pile, ce qui signifie qu'il n'y aura jamais de bouton de retour sur la barre de navigation de cette vue. Ensuite, nous donnons à notre fenêtre un contrôleur de navigation en tant que rootViewController , car maintenant il contiendra toutes les vues de l'application.

Exécutez maintenant votre application. Le résultat devrait ressembler à ceci:

image

Malheureusement, quelque chose s'est mal passé. Il semble que la barre de navigation chevauche notre vue supérieure, et nous avons plusieurs façons de résoudre ce problème:

  • Augmentez la taille de notre vue upperView pour l'adapter à la hauteur de la barre de navigation.
  • Définissez la propriété isTranslucent de la barre de navigation sur false . Cela rendra la barre de navigation opaque (si vous ne l'avez pas remarqué, elle est un peu transparente), et maintenant le bord supérieur de la vue d'ensemble deviendra le bas de la barre de navigation.

Personnellement, je choisirai la deuxième option, mais vous étudierez la première. Je recommande également de vérifier et de lire attentivement les documents d'Apple sur UINavigationController et UINavigationBar :


Accédez maintenant à la méthode viewDidLoad et ajoutez cette ligne self.navigationController? .NavigationBar.isTranslucent = false super.viewDidLoad () self.navigationController? .NavigationBar.isTranslucent = false super.viewDidLoad () , pour que cela ressemble à ceci:

 override func viewDidLoad() { super.viewDidLoad() self.navigationController?.navigationBar.isTranslucent = false self.view.backgroundColor = .white self.addSubviews() self.setupConstraints() self.view.bringSubview(toFront: avatar) self.view.setNeedsUpdateConstraints() } 

Vous pouvez également ajouter cette ligne self.title = "John Doe" viewDidLoad , qui ajoutera un "Profil" à la barre de navigation afin que l'utilisateur sache où il se trouve actuellement. Exécutez l'application et le résultat devrait ressembler à ceci:

image

Refonte de notre contrôleur de vue


Avant de continuer, nous devons réduire notre fichier ViewController.swift que nous ne puissions utiliser que de la vraie logique, pas seulement du code pour les éléments de l'interface utilisateur. Nous pouvons le faire en créant une sous-classe d' UIView et en y déplaçant tous nos éléments d'interface utilisateur. La raison pour laquelle nous faisons cela est de suivre le modèle architectural Model-View-Controller ou MVC pour faire court. En savoir plus sur MVC Model-View-Controller (MVC) sur iOS: une approche moderne .

Maintenant, ContactCard clic droit sur le dossier ContactCard dans Project Navigator et sélectionnez «Nouveau fichier»:

image

Cliquez sur Cocoa Touch Class puis sur Suivant. Maintenant écrivez «ProfileView» comme nom de classe, et à côté de «Sous-classe de:» assurez-vous d'entrer «UIView». Il dit simplement à Xcode de faire automatiquement hériter notre classe de UIView , et il ajoutera du code passe-partout. Maintenant, cliquez sur Suivant, puis sur Créer et supprimer le code mis en commentaire:

 /* // Only override draw() if you perform custom drawing. // An empty implementation adversely affects performance during animation. override func draw(_ rect: CGRect) { // Drawing code } */ 

Et maintenant, nous sommes prêts pour la refactorisation.

Coupez et collez toutes les variables paresseuses du contrôleur de vue dans notre nouvelle vue.
Sous la dernière variable en attente, remplacez init(frame :) en tapant init puis en sélectionnant le premier résultat de saisie semi-automatique à partir de Xcode.

image

Une erreur apparaîtra indiquant que l'initialiseur «requis» «init (coder :)» devrait être fourni par une sous-classe de «UIView»:

image

Vous pouvez résoudre ce problème en cliquant sur le cercle rouge, puis sur Corriger.

image

Dans tout initialiseur surchargé, vous devez presque toujours appeler l'initialiseur de superclasse, alors ajoutez cette ligne de code en haut de la méthode: super.init (frame: frame) .
Coupez et collez la méthode addSubviews() sous les initialiseurs et supprimez self.view avant chaque appel addSubview .

 func addSubviews() { addSubview(avatar) addSubview(upperView) addSubview(segmentedControl) addSubview(editButton) } 

Appelez ensuite cette méthode à partir de l'initialiseur:

 override init(frame: CGRect) { super.init(frame: frame) addSubviews() bringSubview(toFront: avatar) } 

Pour les restrictions, remplacez updateConstraints() et ajoutez un appel à la fin de cette fonction (où il restera toujours):

 override func updateConstraints() { // Insert code here super.updateConstraints() // Always at the bottom of the function } 

Lors du remplacement d'une méthode, il est toujours utile de vérifier sa documentation en visitant les documents Apple ou, plus simplement, en maintenant la touche Option (ou Alt) enfoncée et en cliquant sur le nom de la fonction:

image

Coupez et collez le code de contrainte du contrôleur de vue dans notre nouvelle méthode:

 override func updateConstraints() { avatar.autoAlignAxis(toSuperviewAxis: .vertical) avatar.autoPinEdge(toSuperviewEdge: .top, withInset: 64.0) upperView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom) segmentedControl.autoPinEdge(toSuperviewEdge: .left, withInset: 8.0) segmentedControl.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0) segmentedControl.autoPinEdge(.top, to: .bottom, of: avatar, withOffset: 16.0) editButton.autoPinEdge(.top, to: .bottom, of: upperView, withOffset: 16.0) editButton.autoPinEdge(toSuperviewEdge: .right, withInset: 8.0) super.updateConstraints() } 

Revenez maintenant au contrôleur de vue et initialisez l'instance ProfileView sur la méthode viewDidLoad let profileView = ProfileView(frame: .zero) , ajoutez-la en tant que sous-vue au ViewController .

Maintenant, notre contrôleur de vue a été réduit à quelques lignes de code!

 import UIKit import PureLayout class ViewController: UIViewController { let profileView = ProfileView(frame: .zero) override func viewDidLoad() { super.viewDidLoad() self.navigationController?.navigationBar.isTranslucent = false self.title = "Profile" self.view.backgroundColor = .white self.view.addSubview(self.profileView) self.profileView.autoPinEdgesToSuperviewEdges() self.view.layoutIfNeeded() } } 

Pour vous assurer que tout fonctionne comme prévu, lancez votre application et vérifiez à quoi elle ressemble.

Avoir un contrôleur d'examen mince et soigné devrait toujours être votre objectif. Cela peut prendre beaucoup de temps, mais cela vous évitera des problèmes inutiles pendant la maintenance.

UITableView


Ensuite, nous ajouterons un UITableView pour présenter les informations de contact telles que le numéro de téléphone, l'adresse, etc.

Si vous ne l'avez pas déjà fait, rendez-vous dans la documentation Apple pour consulter UITableView, UITableViewDataSource et UITableViewDelegate.


Accédez à ViewController.swift et ajoutez lazy var pour tableView ci-dessus viewDidLoad() :

 lazy var tableView: UITableView = { let tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self tableView.dataSource = self return tableView }() 

Si vous essayez d'exécuter l'application, Xcode se plaindra que cette classe n'est ni un délégué ni une source de données pour UITableViewController , et donc nous ajouterons ces deux protocoles à la classe:

 class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { . . . 

Encore une fois, Xcode se plaindra d'une classe qui n'est pas conforme au protocole UITableViewDataSource , ce qui signifie qu'il existe des méthodes obligatoires dans ce protocole qui ne sont pas définies dans la classe. Pour savoir laquelle de ces méthodes vous devez implémenter tout en maintenant Cmd + Control, cliquez sur le protocole UITableViewDataSource dans la définition de classe et vous passerez à la définition de protocole. Pour toute méthode qui n'est pas précédée du mot optional , une classe correspondant à ce protocole doit être implémentée.

Ici, nous avons deux méthodes que nous devons implémenter:

  1. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int - cette méthode indique à la table vue le nombre de lignes que nous voulons afficher.
  2. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell - cette méthode interroge la cellule de chaque ligne. Ici, nous initialisons (ou réutilisons) la cellule et insérons les informations que nous voulons montrer à l'utilisateur. Par exemple, la première cellule affichera le numéro de téléphone, la deuxième cellule affichera l'adresse, etc.

Revenez maintenant à ViewController.swift , commencez à taper numberOfRowsInSection et lorsque la saisie numberOfRowsInSection -automatique apparaît, sélectionnez la première option.

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { <#code#> } 

Supprimez le mot code et revenez maintenant 1.

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

Sous cette fonction, commencez à taper cellForRowAt et sélectionnez à nouveau la première méthode de la saisie semi-automatique.

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { <#code#> } 

Et, encore une fois, pour l'instant, renvoyez un UITableViewCell .

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return UITableViewCell() } 

Maintenant, pour connecter notre vue de table à l'intérieur de ProfileView , nous allons définir un nouvel initialiseur qui prend la vue de table comme paramètre, afin qu'il puisse l'ajouter comme sous-vue et définir les restrictions correspondantes pour cela.

Accédez à ProfileView.swift et ajoutez l'attribut pour la vue de table juste au-dessus de l'initialiseur:

var tableView: UITableView! déterminés, nous ne sommes donc pas sûrs que ce sera constant.

Remplacez maintenant l'ancienne implémentation init (frame :) par:

 init(tableView: UITableView) { super.init(frame: .zero) self.tableView = tableView addSubviews() bringSubview(toFront: avatar) } 

Xcode va maintenant se plaindre de l' init (frame :) manquante init (frame :) pour ProfileView , alors revenez à ViewController.swift et remplacez let profileView = ProfileView (frame: .zero) par

 lazy var profileView: UIView = { return ProfileView(tableView: self.tableView) }() 

Maintenant, notre ProfileView a un lien vers une vue de table, et nous pouvons l'ajouter en tant que sous-vue et définir les restrictions appropriées pour cela.
De retour à ProfileView.swift , ajoutez addSubview(tableView) à la fin de addSubviews() et définissez ces restrictions sur updateConstraints() sur super.updateConstraints :

 tableView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) tableView.autoPinEdge(.top, to: .bottom, of: segmentedControl, withOffset: 8) 

La première ligne ajoute trois restrictions entre la vue de table et sa vue d'ensemble: les côtés droit, gauche et inférieur de la vue de table sont attachés aux côtés droit, gauche et inférieur de la vue de profil.

La deuxième ligne attache le haut de la vue de table au bas du contrôle segmenté avec un intervalle de huit points entre eux. Lancez l'application et le résultat devrait ressembler à ceci:

image

Génial, maintenant tout est en place et nous pouvons commencer à introduire nos cellules.

UITableViewCell


Pour implémenter UITableViewCell , nous aurons presque toujours besoin de sous-classer cette classe, donc UITableViewCell clic droit sur le dossier ContactCard dans le Navigateur du projet, puis "Nouveau fichier ...", puis "Cocoa Touch Class" et "Suivant".

Entrez «UITableViewCell» dans le champ «Sous-classe de:» et Xcode remplira automatiquement le nom de classe «TableViewCell». Saisissez «ProfileView» avant la saisie semi-automatique afin que le nom final soit «ProfileInfoTableViewCell», puis cliquez sur «Suivant» et «Créer». Allez-y et supprimez les méthodes créées, car nous n'en aurons pas besoin. Si vous le souhaitez, vous pouvez d'abord lire leurs descriptions pour comprendre pourquoi nous n'en avons pas besoin pour le moment.

Comme nous l'avons dit plus tôt, notre cellule contiendra des informations de base, qui sont le nom du champ et sa description, et nous avons donc besoin d'étiquettes pour elles.

 lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "Title" return label }() lazy var descriptionLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "Description" label.textColor = .gray return label }() 

Et maintenant, nous allons redéfinir l'initialiseur afin que la cellule puisse être configurée:

 override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(titleLabel) contentView.addSubview(descriptionLabel) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 

En ce qui concerne les limitations, nous allons faire un peu différent, mais néanmoins très utile:

 override func updateConstraints() { let titleInsets = UIEdgeInsetsMake(16, 16, 0, 8) titleLabel.autoPinEdgesToSuperviewEdges(with: titleInsets, excludingEdge: .bottom) let descInsets = UIEdgeInsetsMake(0, 16, 4, 8) descriptionLabel.autoPinEdgesToSuperviewEdges(with: descInsets, excludingEdge: .top) descriptionLabel.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 16) super.updateConstraints() } 

Ici, nous commençons à utiliser UIEdgeInsets pour définir l'espacement autour de chaque étiquette. Un objet UIEdgeInsets peut être créé à l'aide de la UIEdgeInsetsMake(top:, left:, bottom:, right:) . Par exemple, pour titleLabel nous disons que nous voulons que la limite supérieure soit de quatre points, et la droite et la gauche à huit. Nous ne nous soucions pas du bas, car nous l'excluons, car nous le fixons en haut de la marque de description. Prenez une minute pour lire et visualiser toutes les contraintes dans votre tête.

Ok, maintenant nous pouvons commencer à dessiner des cellules dans notre vue tabulaire. Passons à ViewController.swift et modifions l'initialisation paresseuse de notre vue de table pour enregistrer cette classe de cellules dans la vue de table et définir la hauteur de chaque cellule.

 let profileInfoCellReuseIdentifier = "profileInfoCellReuseIdentifier" lazy var tableView: UITableView = { ... tableView.register(ProfileInfoTableViewCell.self, forCellReuseIdentifier: profileInfoCellReuseIdentifier) tableView.rowHeight = 68 return tableView }() 

Nous ajoutons également une constante pour l'identifiant de réutilisation des cellules. Cet identifiant est utilisé pour supprimer les cellules de la vue tableau lorsqu'elles sont affichées. Il s'agit d'une optimisation qui peut (et devrait) être utilisée pour aider UITableView réutiliser les cellules qui étaient précédemment présentées pour afficher le nouveau contenu au lieu de redessiner la nouvelle cellule à partir de zéro.
Maintenant, laissez-moi vous montrer comment réutiliser des cellules dans une ligne de code dans la méthode cellForRowAt :

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: profileInfoCellReuseIdentifier, for: indexPath) as! ProfileInfoTableViewCell return cell } 

Ici, nous informons la vue de table du retrait d'une cellule réutilisable de la file d'attente en utilisant l'identifiant sous lequel nous avons enregistré le chemin vers la cellule où l'utilisateur est sur le point d'apparaître. Ensuite, nous ProfileInfoTableViewCell la cellule à ProfileInfoTableViewCell pour pouvoir accéder à ses propriétés afin que nous puissions, par exemple, définir le titre et la description. Cela peut être fait en utilisant ce qui suit:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... switch indexPath.row { case 0: cell.titleLabel.text = "Phone Number" cell.descriptionLabel.text = "+234567890" case 1: cell.titleLabel.text = "Email" cell.descriptionLabel.text = "john@doe.co" case 2: cell.titleLabel.text = "LinkedIn" cell.descriptionLabel.text = "www.linkedin.com/john-doe" default: break } return cell } 

Maintenant, définissez numberOfRowsInSection pour retourner «3» et lancez votre application.

image

Incroyable non?

Cellules à dimensionnement automatique


Peut-être, et très probablement, il y aura un cas où vous souhaitez que différentes cellules aient des hauteurs conformément aux informations qu'elles contiennent, ce qui n'est pas connu à l'avance. Pour ce faire, vous avez besoin d'une vue tabulaire avec des dimensions calculées automatiquement, et en fait, il existe un moyen très simple de le faire.

Tout d'abord, dans ProfileInfoTableViewCell ajoutez cette ligne à l'initialisation paresseuse descriptionLabel :

 label.numberOfLines = 0 

Revenez au ViewController et ajoutez ces deux lignes à l'initialiseur de vue de table:

 lazy var tableView: UITableView = { ... tableView.estimatedRowHeight = 64 tableView.rowHeight = UITableViewAutomaticDimension return tableView }() 

Ici, nous informons la vue de table que la hauteur de ligne doit avoir une valeur calculée automatiquement en fonction de son contenu.

Concernant la hauteur de ligne estimée:
«Fournir une estimation non négative de la hauteur des lignes peut améliorer les performances de chargement de la vue de table.» - Apple Docs

Dans ViewDidLoad nous devons recharger la vue de table pour que ces modifications prennent effet:

 override func viewDidLoad() { super.viewDidLoad() ... DispatchQueue.main.async { self.tableView.reloadData() } } 

Maintenant, allez-y et ajoutez une autre cellule, augmentez le nombre de lignes à quatre et ajoutez une autre switch à cellForRow :

 case 3: cell.titleLabel.text = "Address" cell.descriptionLabel.text = "45, Walt Disney St.\n37485, Mickey Mouse State" 

Maintenant, exécutez l'application et elle devrait ressembler à ceci:

image

Conclusion


Incroyable non? Et pour vous rappeler pourquoi nous codons réellement notre interface utilisateur, voici un article de blog entier rédigé par notre équipe mobile sur les raisons pour lesquelles nous n'utilisons pas de storyboards dans Instabug .

Ce que vous avez fait dans deux parties de cette leçon:

  • Suppression du fichier main.storyboard de votre projet.
  • Nous avons créé UIWindow programme et lui avons attribué un rootViewController .
  • Création de divers éléments d'interface utilisateur dans le code, tels que des étiquettes, des vues d'image, des contrôles segmentés et des vues de tableau avec leurs cellules.
  • UINavigationBar imbriqué dans votre application.
  • Création d'une taille dynamique UITableViewCell .

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


All Articles