Programmgesteuertes Erstellen von Schnittstellenelementen mit PureLayout (Teil 2)

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels Programmgesteuertes Erstellen von UI-Elementen mit PureLayout von Aly Yaka.

Bild

Willkommen zum zweiten Teil des Artikels über das programmgesteuerte Erstellen einer Schnittstelle mit PureLayout. Im ersten Teil haben wir die Benutzeroberfläche einer einfachen mobilen Anwendung vollständig im Code erstellt, ohne Storyboards oder NIBs zu verwenden. In diesem Handbuch werden einige der am häufigsten verwendeten Elemente der Benutzeroberfläche in allen Anwendungen behandelt:

  • UINavigationController / Bar
  • UITableView
  • UITableViewCell in Selbstgröße


UINavigationController


In unserer Anwendung benötigen Sie wahrscheinlich eine Navigationsleiste, damit der Benutzer von der Kontaktliste zu detaillierten Informationen über einen bestimmten Kontakt wechseln und dann zur Liste zurückkehren kann. UINavigationController kann dieses Problem mithilfe der Navigationsleiste problemlos lösen.

UINavigationController ist nur ein Stapel, in dem Sie viele Ansichten verschieben. Der Benutzer sieht gerade die oberste Ansicht (die zuletzt verschobene) (es sei denn, Sie haben darüber eine andere Ansicht angezeigt, sagen wir Favoriten). Wenn Sie die Draufsicht-Controller des Navigations-Controllers drücken, erstellt der Navigations-Controller automatisch eine Schaltfläche „Zurück“ (je nach den aktuellen Spracheinstellungen des Geräts oben links oder rechts). Wenn Sie auf diese Schaltfläche klicken, kehren Sie zur vorherigen Ansicht zurück.

All dies wird vom Navigationscontroller sofort erledigt. Wenn Sie eine weitere hinzufügen, wird nur eine zusätzliche Codezeile benötigt (wenn Sie die Navigationsleiste nicht anpassen möchten).
Gehen Sie zu AppDelegate.swift und fügen Sie die folgende Codezeile hinzu. Lassen Sie viewController = ViewController ():

 let navigationController = UINavigationController(rootViewController: viewController) 

Jetzt self.window? .RootViewController = viewController self.window? .RootViewController = navigationController ändern self.window? .RootViewController = viewController self.window? .RootViewController = navigationController self.window? .RootViewController = viewController self.window? .RootViewController = navigationController self.window? .RootViewController = viewController self.window? .RootViewController = navigationController . In der ersten Zeile haben wir eine Instanz des UINavigationController und ihm unseren viewController als rootViewController ist der Ansichtscontroller ganz unten im Stapel. Dies bedeutet, dass in der Navigationsleiste dieser Ansicht niemals eine Zurück-Schaltfläche rootViewController wird. Dann geben wir unserem Fenster einen Navigationscontroller als rootViewController , da dieser nun alle Ansichten in der Anwendung enthält.

Führen Sie nun Ihre Anwendung aus. Das Ergebnis sollte folgendermaßen aussehen:

Bild

Leider ist etwas schief gelaufen. Es scheint, dass die Navigationsleiste unsere obere Ansicht überlappt, und wir haben verschiedene Möglichkeiten, dies zu beheben:

  • upperView die Größe unserer upperView Ansicht, um sie an die Höhe der Navigationsleiste upperView .
  • Setzen Sie die Eigenschaft isTranslucent der Navigationsleiste auf false . Dadurch wird die Navigationsleiste undurchsichtig (wenn Sie nicht bemerkt haben, dass sie etwas transparent ist), und jetzt wird der obere Rand der Übersicht zum unteren Rand der Navigationsleiste.

Ich persönlich werde die zweite Option wählen, aber Sie werden die erste studieren. Ich empfehle außerdem, die Dokumente von Apple auf dem UINavigationController und der UINavigationBar überprüfen und sorgfältig zu lesen:


Gehen Sie nun zur viewDidLoad-Methode und fügen Sie diese Zeile hinzu self.navigationController? .NavigationBar.isTranslucent = false super.viewDidLoad () self.navigationController? .NavigationBar.isTranslucent = false super.viewDidLoad () , so dass es so aussehen würde:

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

Sie können diese Zeile auch self.title = "John Doe" viewDidLoad hinzufügen, wodurch der Navigationsleiste ein "Profil" self.title = "John Doe" viewDidLoad wird, damit der Benutzer weiß, wo er sich gerade befindet. Führen Sie die Anwendung aus, und das Ergebnis sollte folgendermaßen aussehen:

Bild

Refactoring unseres View Controllers


Bevor wir fortfahren, müssen wir unsere ViewController.swift Datei reduzieren, ViewController.swift wir nur echte Logik verwenden können, nicht nur Code für Benutzeroberflächenelemente. Wir können dies tun, indem wir eine Unterklasse von UIView und alle Elemente unserer Benutzeroberfläche dorthin verschieben. Der Grund dafür ist, dass wir kurz dem Model-View-Controller- oder MVC-Architekturmuster folgen. Erfahren Sie mehr über MVC Model-View-Controller (MVC) unter iOS: ein moderner Ansatz .

Klicken Sie nun mit der rechten Maustaste auf den ContactCard Ordner in Project Navigator und wählen Sie "Neue Datei":

Bild

Klicken Sie auf Cocoa Touch Class und dann auf Weiter. Schreiben Sie nun "ProfileView" als Klassennamen und geben Sie neben "Subclass of:" unbedingt "UIView" ein. Es weist Xcode lediglich an, unsere Klasse automatisch von UIView erben zu UIView , und es wird etwas Boilerplate-Code hinzugefügt. Klicken Sie nun auf Weiter und dann auf Auskommentierten Code und löschen Sie ihn:

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

Und jetzt sind wir bereit für das Refactoring.

Schneiden Sie alle faulen Variablen aus dem Ansichts-Controller aus und fügen Sie sie in unsere neue Ansicht ein.
Überschreiben Sie unter der letzten ausstehenden Variablen init(frame :) indem Sie init eingeben und dann das erste Ergebnis der automatischen Vervollständigung aus Xcode auswählen.

Bild

Es wird ein Fehler angezeigt, der besagt, dass der "erforderliche" Initialisierer "init (coder :)" von einer Unterklasse von "UIView" bereitgestellt werden sollte:

Bild

Sie können dies beheben, indem Sie auf den roten Kreis und dann auf Beheben klicken.

Bild

In jedem überschriebenen Initialisierer sollten Sie fast immer den Superklassen-Initialisierer aufrufen. super.init (frame: frame) daher diese Codezeile oben in der Methode hinzu: super.init (frame: frame) .
Schneiden Sie die addSubviews() -Methode aus und fügen addSubviews() unter den Initialisierern ein. Löschen Sie self.view vor jedem addSubview Aufruf.

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

Rufen Sie dann diese Methode vom Initialisierer aus auf:

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

Überschreiben updateConstraints() für Einschränkungen updateConstraints() und fügen Sie am Ende dieser Funktion einen Aufruf hinzu (wo er immer verbleibt):

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

Wenn Sie eine Methode überschreiben, ist es immer nützlich, die Dokumentation zu überprüfen, indem Sie Apple-Dokumente aufrufen oder einfacher die Wahltaste (oder die Alt-Taste) gedrückt halten und auf den Funktionsnamen klicken:

Bild

Schneiden Sie den Einschränkungscode aus dem Ansichts-Controller aus und fügen Sie ihn in unsere neue Methode ein:

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

ProfileView nun zum View Controller zurück und initialisieren Sie die ProfileView Instanz über die viewDidLoad Methode. let profileView = ProfileView(frame: .zero) und fügen Sie sie dem ViewController als Unteransicht ViewController .

Jetzt wurde unser View Controller auf wenige Codezeilen reduziert!

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

Um sicherzustellen, dass alles wie beabsichtigt funktioniert, starten Sie Ihre Anwendung und überprüfen Sie, wie sie aussieht.

Ein dünner, ordentlicher Überprüfungscontroller sollte immer Ihr Ziel sein. Dies kann viel Zeit in Anspruch nehmen, erspart Ihnen jedoch unnötige Probleme bei der Wartung.

UITableView


Als nächstes fügen wir eine UITableView hinzu, um Kontaktinformationen wie Telefonnummer, Adresse usw. anzuzeigen.

Wenn Sie dies noch nicht getan haben, lesen Sie UITableView, UITableViewDataSource und UITableViewDelegate in der Apple-Dokumentation.


Gehen Sie zu ViewController.swift und fügen Sie lazy var für tableView über viewDidLoad() :

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

Wenn Sie versuchen, die Anwendung auszuführen, beschwert sich Xcode, dass diese Klasse weder ein Delegat noch eine Datenquelle für den UITableViewController ist. Daher fügen wir der Klasse diese beiden Protokolle hinzu:

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

Erneut beschwert sich Xcode über eine Klasse, die nicht dem UITableViewDataSource Protokoll entspricht. Dies bedeutet, dass dieses Protokoll obligatorische Methoden enthält, die nicht in der Klasse definiert sind. Um herauszufinden, welche dieser Methoden Sie implementieren sollten, während Sie Cmd + Control UITableViewDataSource halten, klicken UITableViewDataSource in der Klassendefinition auf das UITableViewDataSource Protokoll, und Sie fahren mit der Protokolldefinition fort. Für jede Methode, der das Wort optional nicht vorangestellt ist, muss eine diesem Protokoll entsprechende Klasse implementiert werden.

Hier haben wir zwei Methoden, die wir implementieren müssen:

  1. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int - Diese Methode teilt der Tabellenansicht mit, wie viele Zeilen public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int .
  2. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell - Diese Methode fragt die Zelle in jeder Zeile ab. Hier initialisieren wir die Zelle (oder verwenden sie wieder) und fügen die Informationen ein, die wir dem Benutzer anzeigen möchten. In der ersten Zelle wird beispielsweise die Telefonnummer angezeigt, in der zweiten Zelle die Adresse usw.

ViewController.swift nun zu ViewController.swift zurück, geben ViewController.swift numberOfRowsInSection und wählen Sie die erste Option aus, wenn die automatische Vervollständigung angezeigt wird.

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

Löschen Sie den Wortcode und kehren Sie jetzt zurück 1.

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

cellForRowAt unter dieser Funktion mit der Eingabe von cellForRowAt und wählen Sie die erste Methode aus der automatischen Vervollständigung erneut aus.

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

Und geben Sie UITableViewCell wieder eine UITableViewCell .

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

ProfileView nun unsere Tabellenansicht in ProfileView zu verbinden, definieren wir einen neuen Initialisierer, der die Tabellenansicht als Parameter verwendet, damit er sie als Unteransicht hinzufügen und die entsprechenden Einschränkungen dafür festlegen kann.

Gehen Sie zu ProfileView.swift und fügen Sie das Attribut für die Tabellenansicht direkt über dem Initialisierer hinzu:

var tableView: UITableView! Daher sind wir nicht sicher, ob es konstant sein wird.

Ersetzen Sie nun die alte init (frame :) Implementierung durch:

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

Xcode wird sich jetzt über den fehlenden init (frame :) für ProfileView . Gehen Sie also zurück zu ViewController.swift und ersetzen Sie let profileView = ProfileView (frame: .zero) durch

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

Jetzt hat unsere ProfileView einen Link zu einer Tabellenansicht, und wir können sie als Unteransicht hinzufügen und die richtigen Einschränkungen dafür festlegen.
Zurück zu ProfileView.swift , fügen Sie addSubview(tableView) am Ende von addSubviews() und setzen Sie diese Einschränkungen auf updateConstraints() über super.updateConstraints :

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

In der ersten Zeile werden drei Einschränkungen zwischen der Tabellenansicht und ihrer Übersicht hinzugefügt: Die rechte, linke und untere Seite der Tabellenansicht sind an die rechte, linke und untere Seite der Profilansicht angehängt.

In der zweiten Zeile wird der obere Rand der Tabellenansicht mit einem Abstand von acht Punkten am unteren Rand des segmentierten Steuerelements angehängt. Starten Sie die Anwendung und das Ergebnis sollte folgendermaßen aussehen:

Bild

Großartig, jetzt ist alles an Ort und Stelle und wir können beginnen, unsere Zellen vorzustellen.

UITableViewCell


Um UITableViewCell zu implementieren, müssen wir diese Klasse fast immer in Unterklassen unterteilen. Klicken Sie daher im Projektnavigator mit der rechten ContactCard Ordner UITableViewCell , dann auf "Neue Datei ...", dann auf "Cocoa Touch Class" und "Weiter".

Geben Sie "UITableViewCell" in das Feld "Unterklasse von:" ein, und Xcode füllt automatisch den Klassennamen "TableViewCell" aus. Geben Sie vor der automatischen Vervollständigung "ProfileView" ein, sodass der endgültige Name "ProfileInfoTableViewCell" lautet, und klicken Sie dann auf "Weiter" und "Erstellen". Löschen Sie die erstellten Methoden, da wir sie nicht benötigen. Wenn Sie möchten, können Sie zuerst die Beschreibungen lesen, um zu verstehen, warum wir sie derzeit nicht benötigen.

Wie bereits erwähnt, enthält unsere Zelle grundlegende Informationen, nämlich den Namen des Felds und seine Beschreibung. Daher benötigen wir Beschriftungen für diese Felder.

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

Und jetzt definieren wir den Initialisierer neu, damit die Zelle konfiguriert werden kann:

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

In Bezug auf die Einschränkungen werden wir etwas anderes machen, aber dennoch sehr nützlich:

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

Hier beginnen wir mit UIEdgeInsets , um den Abstand um jedes Etikett UIEdgeInsets . Ein UIEdgeInsets Objekt kann mit der UIEdgeInsetsMake(top:, left:, bottom:, right:) werden. Zum Beispiel sagen wir für titleLabel dass die Obergrenze bei vier Punkten und rechts und links bei acht liegen soll. Der Boden ist uns egal, weil wir ihn ausschließen, da wir ihn oben am Beschreibungszeichen anbringen. Nehmen Sie sich eine Minute Zeit, um alle Einschränkungen in Ihrem Kopf zu lesen und zu visualisieren.

Ok, jetzt können wir Zellen in unserer Tabellenansicht zeichnen. ViewController.swift wir mit ViewController.swift und ändern die verzögerte Initialisierung unserer Tabellenansicht, um diese Klasse von Zellen in der Tabellenansicht zu registrieren und die Höhe für jede Zelle ViewController.swift .

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

Wir fügen auch eine Konstante für die Kennung der Zellwiederverwendung hinzu. Diese Kennung wird verwendet, um Zellen aus der Tabellenansicht zu entfernen, wenn sie angezeigt werden. Dies ist eine Optimierung, die verwendet werden kann (und sollte), um UITableView zu unterstützen, zuvor präsentierte Zellen wiederzuverwenden, um neuen Inhalt anzuzeigen, anstatt die neue Zelle von Grund auf neu zu zeichnen.
Lassen Sie mich Ihnen nun zeigen, wie Sie Zellen in einer Codezeile in der cellForRowAt Methode cellForRowAt :

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

Hier informieren wir die Tabellenansicht über das Zurückziehen einer wiederverwendbaren Zelle aus der Warteschlange unter Verwendung der Kennung, unter der wir den Pfad zu der Zelle registriert haben, in der der Benutzer erscheinen soll. Dann zwingen wir die Zelle zu ProfileInfoTableViewCell , um auf ihre Eigenschaften zugreifen zu können, damit wir beispielsweise den Titel und die Beschreibung festlegen können. Dies kann folgendermaßen erfolgen:

 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 } 

Setzen numberOfRowsInSection nun numberOfRowsInSection auf "3" und starten Sie Ihre Anwendung.

Bild

Erstaunlich, oder?

Selbstleimungszellen


Möglicherweise und höchstwahrscheinlich wird es einen Fall geben, in dem Sie möchten, dass verschiedene Zellen gemäß den darin enthaltenen Informationen eine Höhe haben, die nicht im Voraus bekannt ist. Dazu benötigen Sie eine Tabellenansicht mit automatisch berechneten Abmessungen. Tatsächlich gibt es eine sehr einfache Möglichkeit, dies zu tun.

ProfileInfoTableViewCell Sie in ProfileInfoTableViewCell diese Zeile zur descriptionLabel des verzögerten Initialisierers hinzu.

 label.numberOfLines = 0 

ViewController Sie zum ViewController und fügen Sie diese beiden Zeilen zum Initialisierer der Tabellenansicht hinzu:

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

Hier teilen wir der Tabellenansicht mit, dass die Zeilenhöhe basierend auf ihrem Inhalt einen automatisch berechneten Wert haben sollte.

Bezüglich der geschätzten Zeilenhöhe:
"Durch die Bereitstellung einer nichtnegativen Schätzung der Zeilenhöhe kann die Leistung beim Laden der Tabellenansicht verbessert werden." - Apple Docs

In ViewDidLoad wir die Tabellenansicht neu laden, damit diese Änderungen wirksam werden:

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

cellForRow nun eine weitere Zelle hinzu, erhöhen Sie die Anzahl der Zeilen auf vier und fügen Sie cellForRow eine weitere switch cellForRow :

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

Führen Sie nun die Anwendung aus, und sie sollte ungefähr so ​​aussehen:

Bild

Fazit


Erstaunlich, oder? Als Erinnerung daran, warum wir unsere Benutzeroberfläche tatsächlich codieren, finden Sie hier einen vollständigen Blog-Beitrag unseres mobilen Teams darüber, warum wir in Instabug keine Storyboards verwenden .

Was Sie in zwei Teilen dieser Lektion getan haben:

  • Die Datei main.storyboard aus Ihrem Projekt entfernt.
  • Wir haben UIWindow programmgesteuert erstellt und ihm einen rootViewController zugewiesen.
  • Erstellt verschiedene Elemente der Benutzeroberfläche im Code, z. B. Beschriftungen, Bildansichten, segmentierte Steuerelemente und Tabellenansichten mit ihren Zellen.
  • Verschachtelte UINavigationBar in Ihrer Anwendung.
  • Erstellt eine dynamische Größe UITableViewCell .

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


All Articles