Zwiebelkontrolleur. Wir zerlegen Bildschirme in Teile

Atomic Design und System Design sind im Design beliebt: Hier besteht alles aus Komponenten, von Steuerelementen bis zu Bildschirmen. Für einen Programmierer ist es nicht schwierig, separate Steuerelemente zu schreiben, aber was tun mit ganzen Bildschirmen?


Lassen Sie uns das Neujahrsbeispiel analysieren:


  • lasst uns alles zusammenhalten;
  • unterteilt in Controller: Navigation, Vorlage und Inhalt auswählen;
  • Code für andere Bildschirme wiederverwenden.


Alles in einem Haufen


Auf dem Neujahrsbildschirm werden die besonderen Öffnungszeiten von Pizzerien beschrieben. Es ist ganz einfach, daher wird es kein Verbrechen sein, es zu einem einzigen Controller zu machen:



Aber. Wenn wir das nächste Mal einen ähnlichen Bildschirm benötigen, müssen wir ihn noch einmal wiederholen und dann an allen Bildschirmen die gleichen Änderungen vornehmen. Nun, es geht nicht ohne Änderungen.


Daher ist es sinnvoller, es in Teile zu unterteilen und für andere Bildschirme zu verwenden. Ich habe drei hervorgehoben:


  • Navigation
  • eine Vorlage mit einem Bereich für Inhalte und einem Platz für Aktionen am unteren Bildschirmrand,
  • einzigartiger Inhalt in der Mitte.

Wählen Sie jedes Teil in einem eigenen UIViewController .


Containernavigation


Die auffälligsten Beispiele für Navigationscontainer sind der UINavigationController und der UITabBarController . Jeder belegt unter seinen eigenen Steuerelementen einen Streifen auf dem Bildschirm und lässt den verbleibenden Platz für einen anderen UIViewController .


In unserem Fall gibt es einen Container für alle modalen Bildschirme mit nur einer Schaltfläche zum Schließen.


Was ist der Sinn?

Wenn wir die Schaltfläche nach rechts verschieben möchten, müssen wir sie nur in einem Controller ändern.


Oder wenn wir alle modalen Fenster mit einer speziellen Animation anzeigen und interaktiv mit einem Wisch schließen möchten, wie in den AppStore-Storykarten. Dann muss UIViewControllerTransitioningDelegate nur für diesen Controller festgelegt werden.



Sie können eine container view , um Controller zu trennen: Sie erstellt eine UIView im übergeordneten Controller und fügt die UIView Controllers darin ein.



Strecken Sie die container view bis zum Bildschirmrand. Safe area gilt automatisch für den untergeordneten Controller:



Bildschirmmuster


Der Inhalt ist auf dem Bildschirm offensichtlich: Bild, Titel, Text. Die Schaltfläche scheint Teil davon zu sein, aber der Inhalt ist auf verschiedenen iPhones dynamisch und die Schaltfläche ist fest. Es sind zwei Systeme mit unterschiedlichen Aufgaben sichtbar: eines zeigt Inhalte an und das andere bettet sie ein und richtet sie aus. Sie sollten in zwei Controller unterteilt werden.



Der erste ist für das Layout des Bildschirms verantwortlich: Der Inhalt sollte zentriert und die Schaltfläche am unteren Bildschirmrand festgenagelt sein. Der zweite wird den Inhalt zeichnen.



Ohne Vorlage sind alle Controller ähnlich, aber die Elemente tanzen.

Die Schaltflächen auf dem letzten Bildschirm sind unterschiedlich - dies hängt vom Inhalt ab. Die Delegierung hilft bei der Lösung des Problems: Die Controller-Vorlage UIStackView Steuerelemente aus dem Inhalt an und zeigt sie in ihrer UIStackView .


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

Warum view ()?

In meinem letzten Artikel Controller UIView UIViewController , wie Sie UIView mit UIViewController spezialisieren UIView Wir nehmen den Code in UIView heraus.


Schaltflächen können über verwandte Objekte an die Steuerung angehängt werden. Ihr IBOutlet und ihre IBAction werden im IBAction gespeichert, nur die Elemente werden nicht zur Hierarchie hinzugefügt.



Sie können Elemente aus dem Inhalt UIStoryboardSegue und in der Vorbereitungsphase von UIStoryboardSegue zur Vorlage UIStoryboardSegue :


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

Im Setter fügen wir UIStackView Steuerelemente UIStackView :


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

Infolgedessen wurde unser Controller in drei Teile unterteilt: Navigation, Vorlage und Inhalt. In der Abbildung werden alle container view grau dargestellt:



Größe des dynamischen Controllers


Der Content Controller hat seine eigene maximale Größe und ist durch interne constraints .


Container view auf der Autoresizing mask der Autoresizing mask , die mit den internen Dimensionen des Inhalts in Konflikt stehen. Das Problem wird im Code gelöst: Im Inhaltscontroller müssen Sie angeben, dass es nicht von den Konstoren der Autoresizing mask betroffen ist:


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


Es gibt zwei weitere Schritte für Interface Builder:


Schritt 1. UIView die UIView für die UIView . Nach dem Start werden echte Werte angezeigt, aber wir werden vorerst alle geeigneten Werte angeben.



Schritt 2. Geben Sie für den Inhaltscontroller die Simulated Size . Es entspricht möglicherweise nicht der vorherigen Größe.


Es gab Layoutfehler. Was soll ich tun?

Fehler treten auf, wenn AutoLayout nicht AutoLayout kann, wie die Elemente in der aktuellen Größe zerlegt werden.


Meistens verschwindet das Problem, nachdem die Prioritäten der Konstante geändert wurden. Sie müssen sie ablegen, damit einer der UIView mehr als die anderen erweitern / UIView kann.


Wir teilen uns in Teile und schreiben Code


Wir haben den Controller in mehrere Teile unterteilt, aber bisher können wir sie nicht wiederverwenden. Die Schnittstelle von UIStoryboard in Teilen schwer zu extrahieren. Wenn wir einige Daten auf den Inhalt übertragen müssen, müssen wir sie durch die gesamte Hierarchie ziehen. Es sollte umgekehrt sein: Nehmen Sie zuerst den Inhalt, konfigurieren Sie ihn und verpacken Sie ihn dann in die erforderlichen Container. Wie eine Glühbirne.


Drei Aufgaben erscheinen auf unserem Weg:


  1. Trennen Sie jeden Controller in ein eigenes UIStoryboard .
  2. container view ablehnen, Controller zu Containern im Code hinzufügen.
  3. Binde alles zurück.

UIStoryboard teilen


Sie müssen zwei zusätzliche UIStoryboard erstellen und den Navigationscontroller und den Vorlagencontroller kopieren und in diese einfügen. Embed segue wird unterbrochen, aber die container view mit den konfigurierten Einschränkungen wird übertragen. Einschränkungen müssen gespeichert und die container view durch eine reguläre UIView .


Am einfachsten ist es, den Typ der Containeransicht im UIStoryboard-Code zu ändern.
  • UIStoryboard als Code öffnen (Dateikontextmenü → Öffnen als ... → Quellcode);
  • Ändern Sie den Typ von containerView in view . Es ist notwendig, sowohl das öffnende als auch das schließende Tag zu ändern.


    Auf die gleiche Weise können Sie beispielsweise UIView bei UIScrollView in UIScrollView ändern. Umgekehrt.




Wir setzen den Controller auf die Eigenschaft " is initial view controller und rufen das UIStoryboard als Controller auf.


Wir laden den Controller von UIStoryboard.

Wenn der Name des Controllers mit dem Namen von UIStoryboard , kann der Download in eine Methode eingeschlossen werden, die selbst die gewünschte Datei findet:


 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 } } 

Wenn der Controller in .xib , wird der Standardkonstruktor ohne solche Tänze geladen. .xib kann .xib nur einen Controller enthalten, oft reicht dies nicht aus: In einem guten Fall besteht ein Bildschirm aus mehreren. Daher verwenden wir UIStoryborad , es ist einfach, den Bildschirm in Teile zu UIStoryborad .


Fügen Sie einen Controller im Code hinzu


Damit der Controller ordnungsgemäß funktioniert, benötigen wir alle Methoden seines Lebenszyklus: will/did-appear/disappear .


Für die korrekte Anzeige müssen Sie 5 Schritte aufrufen:


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

Apple schlägt vor, den Code auf 4 Schritte zu reduzieren, da addChild() selbst willMove(toParent) . Zusammenfassend:


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

Der Einfachheit halber können Sie alles in extension einwickeln. Für unseren Fall benötigen wir eine Version mit 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) } } 

Zum Löschen benötigen Sie dieselben Schritte, nur müssen Sie anstelle des übergeordneten Controllers nil festlegen. Jetzt ruft removeFromParent() didMove(toParent: nil) , und das Layout wird nicht benötigt. Die verkürzte Version ist sehr unterschiedlich:


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

Layout


Einschränkungen festlegen


Um die Größe des Controllers richtig einzustellen, verwenden wir AutoLayout . Wir müssen alle Seiten an alle Seiten nageln:


 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) ]) } } 

Fügen Sie einen untergeordneten Controller in den Code ein


Jetzt kann alles kombiniert werden:


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

Aufgrund der Häufigkeit der Nutzung können wir all dies in extension :


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

Eine ähnliche Methode wird auch für den Vorlagencontroller benötigt. prepare(for segue:) wurden prepare(for segue:) in prepare(for segue:) , jetzt können Sie sie in die Controller-Einbettungsmethode einbinden:


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

Das Erstellen eines Controllers sieht folgendermaßen aus:


 // 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) } 

Das Anschließen eines neuen Bildschirms an die Vorlage ist einfach:


  • entfernen, was für den Inhalt nicht relevant ist;
  • Geben Sie Aktionsschaltflächen an, indem Sie das OnboardingViewControllerDatasource-Protokoll implementieren.
  • Schreiben Sie eine Methode, die eine Vorlage und einen Inhalt verknüpft.

Mehr über Container


Statusleiste


Die Sichtbarkeit der status bar muss häufig von einem Controller mit Inhalt und nicht von einem Container gesteuert werden. Dafür gibt es ein paar property :


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

Mit diesen property Sie eine Kette von Controllern erstellen, die für die Anzeige der status bar .


Sicherer Bereich


Wenn sich die Container-Schaltflächen mit dem Inhalt überschneiden, sollten Sie die safeArea Zone erhöhen. Dies kann im folgenden Code erfolgen: set additinalSafeAreaInsets für additinalSafeAreaInsets Controller. Sie können es von embedController() aus 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 } } 

Wenn Sie oben 30 Punkte hinzufügen, hört die Schaltfläche auf, Inhalte zu überlappen, und safeArea den grünen Bereich:



Ränder. Beibehalten der Überwachungsränder


Controller haben Standardmargen. Normalerweise entsprechen sie 16 Punkten von jeder Seite des Bildschirms und nur bei Übergrößen sind es 20 Punkte.


Basierend auf den margins Sie Konstanten erstellen. Die Einrückung am Rand ist für verschiedene iPhones unterschiedlich:



Wenn wir eine UIView in eine andere UIView , halbieren sich die margins : auf 8 Punkte. Um dies zu verhindern, müssen Sie die Preserve superview margins . Dann sind die margins UIView gleich den margins übergeordneten UIView . Es ist für Vollbildbehälter geeignet.


Das Ende


Container-Controller sind ein leistungsstarkes Werkzeug. Sie vereinfachen den Code, trennen Aufgaben und können wiederverwendet werden. Sie können verschachtelte Controller auf beliebige Weise schreiben: in UIStoryboard , in .xib oder einfach in Code. Vor allem sind sie einfach zu erstellen und machen Spaß.


Ein Beispiel aus einem Artikel auf GitHub


Haben Sie Bildschirme, auf denen es sich lohnt, eine Vorlage zu erstellen? Teilen Sie in den Kommentaren!

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


All Articles