Contrôleur d'oignon. Nous cassons les écrans en plusieurs parties

La conception atomique et la conception de systèmes sont populaires dans la conception: c'est à ce moment-là que tout est composé de composants, des commandes aux écrans. Il n'est pas difficile pour un programmeur d'écrire des contrôles séparés, mais que faire avec des écrans entiers?


Jetons un œil à l'exemple du Nouvel An:


  • collons tout ensemble;
  • divisé en contrôleurs: sélectionnez la navigation, le modèle et le contenu;
  • réutiliser le code pour d'autres écrans.


Tout en un tas


L'écran de cette nouvelle année parle des heures d'ouverture spéciales des pizzerias. C'est assez simple, donc ce ne sera pas un crime d'en faire un contrôleur:



Mais. La prochaine fois, lorsque nous aurons besoin d'un écran similaire, nous devrons le répéter à nouveau, puis apporter les mêmes modifications à tous les écrans. Eh bien, cela ne se produit pas sans modifications.


Par conséquent, il est plus raisonnable de le diviser en parties et de l'utiliser pour d'autres écrans. J'en ai souligné trois:


  • navigation
  • un modèle avec une zone de contenu et une place pour les actions en bas de l'écran,
  • contenu unique au centre.

Sélectionnez chaque pièce dans son propre UIViewController .


Navigation en conteneurs


Les exemples les plus frappants de conteneurs de navigation sont UINavigationController et UITabBarController . Chacun occupe une bande sur l'écran sous ses propres contrôles et laisse l'espace restant pour un autre UIViewController .


Dans notre cas, il y aura un conteneur pour tous les écrans modaux avec un seul bouton de fermeture.


À quoi ça sert?

Si nous voulons déplacer le bouton vers la droite, nous n'aurons besoin de le modifier que dans un seul contrôleur.


Ou, si nous décidons d'afficher toutes les fenêtres modales avec une animation spéciale, et de fermer de manière interactive avec un glissement, comme dans les cartes d'histoire de l'AppStore. UIViewControllerTransitioningDelegate devra alors être défini uniquement pour ce contrôleur.



Vous pouvez utiliser une container view pour séparer les contrôleurs: elle créera une UIView dans le parent et y insérera l' UIView contrôleur enfant.



Étirez la container view jusqu'au bord de l'écran. Safe area s'appliquera automatiquement au contrôleur enfant:



Modèle d'écran


Le contenu est évident à l'écran: image, titre, texte. Le bouton semble en faire partie, mais le contenu est dynamique sur différents iPhones et le bouton est fixe. Deux systèmes avec des tâches différentes sont visibles: l'un affiche le contenu et l'autre l'incorpore et l'aligne. Ils doivent être divisés en deux contrôleurs.



Le premier est responsable de la disposition de l'écran: le contenu doit être centré et le bouton cloué au bas de l'écran. Le second dessinera le contenu.



Sans modèle, tous les contrôleurs sont similaires, mais les éléments dansent.

Les boutons du dernier écran sont différents - cela dépend du contenu. La délégation aidera à résoudre le problème: le contrôleur-modèle demandera des contrôles du contenu et les affichera dans son UIStackView .


 // OnboardingViewController.swift protocol OnboardingViewControllerDatasource { var supportingViews: [UIView] { get } } // NewYearContentViewController.swift extension NewYearContentViewController: OnboardingViewControllerDatasource { var supportingViews: [UIView] { return [view().doneButton] } } 

Pourquoi afficher ()?

UIViewController pouvez lire comment spécialiser UIView avec UIViewController dans mon dernier article Controller, UIView UIViewController ! Nous sortons le code dans UIView.


Les boutons peuvent être attachés au contrôleur via des objets associés. Leur IBOutlet et IBAction sont stockés dans le contrôleur de contenu, seuls les éléments ne sont pas ajoutés à la hiérarchie.



Vous pouvez obtenir des éléments du contenu et les ajouter au modèle lors de la préparation de UIStoryboardSegue :


 // OnboardingViewController.swift override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let buttonsDatasource = segue.destination as? OnboardingViewControllerDatasource { view().supportingViews = buttonsDatasource.supportingViews } } 

Dans le setter, nous ajoutons des contrôles à UIStackView :


 // OnboardingView.swift var supportingViews: [UIView] = [] { didSet { for view in supportingViews { stackView.addArrangedSubview(view) } } } 

En conséquence, notre contrôleur a été divisé en trois parties: navigation, modèle et contenu. Dans l'image, toute la container view affichée en gris:



Taille du contrôleur dynamique


Le contrôleur de contenu a sa propre taille maximale, il est limité par des constraints internes.


Container view ajoute des constores basés sur le Autoresizing mask et ils entrent en conflit avec les dimensions internes du contenu. Le problème est résolu dans le code: dans le contrôleur de contenu, vous devez indiquer qu'il n'est pas affecté par les constores du Autoresizing mask :


 // NewYearContentViewController.swift override func loadView() { super.loadView() view.translatesAutoresizingMaskIntoConstraints = false } 


Il existe deux autres étapes pour Interface Builder:


Étape 1. Spécifiez la Intrinsic size de l' UIView . Les vraies valeurs apparaîtront après le lancement, mais pour l'instant, nous en mettrons toutes appropriées.



Étape 2. Pour le contrôleur de contenu, spécifiez Simulated Size . Il peut ne pas correspondre à la taille passée.


Il y a eu des erreurs de mise en page, que dois-je faire?

Des erreurs se produisent lorsque AutoLayout ne peut pas comprendre comment décomposer les éléments dans la taille actuelle.


Le plus souvent, le problème disparaît après avoir changé les priorités de la constante. Vous devez les déposer afin que l'un des UIView puisse s'étendre / se contracter plus que les autres.


Nous divisons en parties et écrivons dans le code


Nous avons divisé le contrôleur en plusieurs parties, mais jusqu'à présent, nous ne pouvons pas les réutiliser, l'interface d' UIStoryboard difficile à extraire en plusieurs parties. Si nous devons transférer certaines données vers le contenu, nous devrons alors les utiliser dans toute la hiérarchie. Cela devrait être l'inverse: prenez d'abord le contenu, configurez-le, puis enveloppez-le dans les conteneurs nécessaires. Comme une ampoule.


Trois tâches apparaissent sur notre chemin:


  1. Séparez chaque contrôleur dans son propre UIStoryboard .
  2. Refuser la container view , ajouter des contrôleurs aux conteneurs dans le code.
  3. Attachez tout en arrière.

Partager UIStoryboard


Vous devez créer deux UIStoryboard supplémentaires et y copier-coller le contrôleur de navigation et le contrôleur de modèle. Embed segue enchaînement Embed segue sera interrompu, mais la container view du container view avec les contraintes configurées sera transférée. Les contraintes doivent être enregistrées et la container view du container view doit être remplacée par une UIView .


Le moyen le plus simple consiste à modifier le type de vue Conteneur dans le code UIStoryboard.
  • ouvrir UIStoryboard tant que code (menu contextuel du fichier → Ouvrir en tant que ... → Code source);
  • changez le type de containerView en view . Il est nécessaire de modifier les balises d'ouverture et de fermeture .


    De la même manière, vous pouvez changer, par exemple, UIView en UIScrollView , si nécessaire. Et vice versa.




Nous définissons le contrôleur sur la propriété is initial view controller , et UIStoryboard appellerons UIStoryboard tant que contrôleur.


Nous chargeons le contrôleur depuis UIStoryboard.

Si le nom du contrôleur correspond au nom de UIStoryboard , le téléchargement peut être encapsulé dans une méthode qui trouvera elle-même le fichier souhaité:


 protocol Storyboardable { } extension Storyboardable where Self: UIViewController { static func instantiateInitialFromStoryboard() -> Self { let controller = storyboard().instantiateInitialViewController() return controller! as! Self } static func storyboard(fileName: String? = nil) -> UIStoryboard { let storyboard = UIStoryboard(name: fileName ?? storyboardIdentifier, bundle: nil) return storyboard } static var storyboardIdentifier: String { return String(describing: self) } static var storyboardName: String { return storyboardIdentifier } } 

Si le contrôleur est décrit en .xib , le constructeur standard se chargera sans ces danses. Hélas, .xib ne peut contenir qu'un seul contrôleur, souvent cela ne suffit pas: dans un bon cas, un écran est composé de plusieurs. Par conséquent, nous utilisons UIStoryborad , il est facile de diviser l'écran en plusieurs parties.


Ajouter un contrôleur dans le code


Pour que le contrôleur fonctionne correctement, nous avons besoin de toutes les méthodes de son cycle de vie: will/did-appear/disappear .


Pour un affichage correct, vous devez appeler 5 étapes:


  willMove(toParent parent: UIViewController?) addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?) 

Apple suggère de réduire le code à 4 étapes, car addChild() lui-même appellera willMove(toParent) . En résumé:


  addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?) 

Pour plus de simplicité, vous pouvez envelopper le tout en extension . Pour notre cas, nous avons besoin d'une version avec insertSubview() .


 extension UIViewController { func insertFullframeChildController(_ childController: UIViewController, toView: UIView? = nil, index: Int) { let containerView: UIView = toView ?? view addChild(childController) containerView.insertSubview(childController.view, at: index) containerView.pinToBounds(childController.view) childController.didMove(toParent: self) } } 

Pour supprimer, vous avez besoin des mêmes étapes, mais au lieu du contrôleur parent, vous devez définir nil . Maintenant, removeFromParent() appelle didMove(toParent: nil) , et la mise en page n'est pas nécessaire. La version raccourcie est très différente:


  willMove(toParent: nil) view.removeFromSuperview() removeFromParent() 

Disposition


Définir des contraintes


Pour définir correctement la taille du contrôleur, nous utiliserons AutoLayout . Nous devons clouer tous les côtés de tous les côtés:


 extension UIView { func pinToBounds(_ view: UIView) { view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: topAnchor), view.bottomAnchor.constraint(equalTo: bottomAnchor), view.leadingAnchor.constraint(equalTo: leadingAnchor), view.trailingAnchor.constraint(equalTo: trailingAnchor) ]) } } 

Ajouter un contrôleur enfant dans le code


Maintenant, tout peut être combiné:


 // ModalContainerViewController.swift public func embedController(_ controller: UIViewController) { insertFullframeChildController(controller, index: 0) } 

En raison de la fréquence d'utilisation, nous pouvons envelopper tout cela en extension :


 // ModalContainerViewController.swift extension UIViewController { func wrapInModalContainer() -> ModalContainerViewController { let modalController = ModalContainerViewController.instantiateInitialFromStoryboard() modalController.embedController(self) return modalController } } 

Une méthode similaire est également nécessaire pour le contrôleur de modèle. prepare(for segue:) auparavant configurées dans prepare(for segue:) , mais vous pouvez maintenant les lier dans la méthode d'intégration du contrôleur:


 // OnboardingViewController.swift public func embedController(_ controller: UIViewController, actionsDatasource: OnboardingViewControllerDatasource) { insertFullframeChildController(controller, toView: view().contentContainerView, index: 0) view().supportingViews = actionsDatasource.supportingViews } 

La création d'un contrôleur ressemble à ceci:


 // MainViewController.swift @IBAction func showModalControllerDidPress(_ sender: UIButton) { let content = NewYearContentViewController.instantiateInitialFromStoryboard() //     let onboarding = OnboardingViewController.instantiateInitialFromStoryboard() onboarding.embedController(contentController, actionsDatasource: contentController) let modalController = onboarding.wrapInModalContainer() present(modalController, animated: true) } 

La connexion d'un nouvel écran au modèle est simple:


  • supprimer ce qui n'est pas pertinent pour le contenu;
  • spécifier des boutons d'action en implémentant le protocole OnboardingViewControllerDatasource;
  • écrire une méthode qui relie un modèle et un contenu.

En savoir plus sur les conteneurs


Barre d'état


Il est souvent nécessaire que la visibilité de la status bar soit contrôlée par un contrôleur avec du contenu, pas un conteneur. Il y a quelques property pour cela:


 // UIView.swift var childForStatusBarStyle: UIViewController? var childForStatusBarHidden: UIViewController? 

En utilisant ces property vous pouvez créer une chaîne de contrôleurs, ce dernier sera responsable de l'affichage de la status bar .


Zone sûre


Si les boutons de conteneur chevauchent le contenu, vous devez augmenter la zone safeArea . Cela peut être fait dans le code: définissez additinalSafeAreaInsets pour les contrôleurs enfants. Vous pouvez l'appeler depuis embedController() :


 private func addSafeArea(to controller: UIViewController) { if #available(iOS 11.0, *) { let buttonHeight = CGFloat(30) let topInset = UIEdgeInsets(top: buttonHeight, left: 0, bottom: 0, right: 0) controller.additionalSafeAreaInsets = topInset } } 

Si vous ajoutez 30 points au-dessus, le bouton cessera de chevaucher le contenu et safeArea occupera la zone verte:



Marges. Préserver les marges de superview


Les contrôleurs ont des margins standard. Habituellement, ils sont égaux à 16 points de chaque côté de l'écran et uniquement sur les tailles Plus, ils sont de 20 points.


En fonction des margins vous pouvez créer des constantes, l'indentation sur le bord sera différente pour différents iPhones:



Lorsque nous mettons un UIView dans un autre, les margins sont divisées par deux: à 8 points. Pour éviter cela, vous devez inclure Preserve superview margins . Les margins UIView enfant seront alors égales aux margins parent. Il convient aux conteneurs plein écran.


La fin


Les contrôleurs de conteneurs sont un outil puissant. Ils simplifient le code, séparent les tâches et peuvent être réutilisés. Vous pouvez écrire des contrôleurs imbriqués de n'importe quelle manière: dans UIStoryboard , en .xib ou simplement en code. Plus important encore, ils sont faciles à créer et amusants à utiliser.


Un exemple d'un article sur GitHub


Avez-vous des écrans à partir desquels il serait utile de créer un modèle? Partagez dans les commentaires!

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


All Articles