[Перевод] Создаем элементы интерфейса программно с помощью PureLayout (Часть 2)

Привет, Хабр! Представляю вашему вниманию перевод статьи Creating UI Elements Programmatically Using PureLayout автора Aly Yaka.

image

Добро пожаловать во вторую часть статьи по программному созданию интерфейса с использованием PureLayout. В первой части мы создали пользовательский интерфейс простого мобильного приложения полностью кодом, без использования Storyboards или NIB’ов. В этом руководстве мы рассмотрим некоторые наиболее часто используемые элементы пользовательского интерфейса во всех приложениях:

  • UINavigationController/Bar
  • UITableView
  • Self-sizing UITableViewCell


UINavigationController


В нашем приложении, вероятно, понадобится панель навигации, чтобы пользователь мог перейти от списка контактов к подробной информации об определенном контакте, а затем вернуться обратно к списку. UINavigationController легко решит эту проблему с помощью панели навигации.

UINavigationController — это просто стек, в который вы перемещаете множество представлений. Самый верхний вид (тот, который перемещен последний) пользователь видит прямо сейчас (кроме случаев, когда у вас есть другое представление, представленное поверх этого, допустим избранные). А когда вы нажимаете контроллеры вида сверху навигационного контроллера, навигационный контроллер автоматически создает кнопку «назад» (верхняя левая или правая сторона в зависимости от текущих языковых предпочтений устройства), и нажатие этой кнопки возвращает вас к предыдущему просмотру.

Все это обрабатывается из коробки контроллером навигации. А добавление еще одного займет всего одну дополнительную строку кода (если вы не хотите настраивать панель навигации).
Перейдите к AppDelegate.swift и добавьте следующую строку кода ниже, пусть viewController = ViewController ():

let navigationController = UINavigationController(rootViewController: viewController)


А теперь измените self.window? .RootViewController = viewController на self.window? .RootViewController = navigationController. В первой строке мы создали экземпляр UINavigationController и передали ему наш viewController в качестве rootViewController, который является контроллером представления в самом низу стека, что означает, что на панели навигации этого представления никогда не будет кнопки «назад». Затем мы даем нашему окну контроллер навигации как rootViewController, поскольку теперь он будет содержать все представления в приложении.

Теперь запустите ваше приложение. Результат должен выглядеть так:

image


К сожалению, что-то пошло не так. Похоже, что панель навигации перекрывает наш upperView, и у нас есть несколько способов исправить это:

  • Увеличьте размер нашего upperView, чтобы он соответствовал высоте панели навигации.
  • Установите для свойства isTranslucent панели навигации значение false. Это сделает панель навигации непрозрачной (в случае, если вы не заметили, она немного прозрачна), и теперь верхний край superview станет нижней частью панели навигации.


Я лично выберу второй вариант, но, вы изучите и первый. Я также рекомендую проверить и внимательно прочитать документы Apple по UINavigationController и UINavigationBar:
Теперь перейдите к методу viewDidLoad и добавьте эту строку self.navigationController? .NavigationBar.isTranslucent = false ниже super.viewDidLoad (), так что бы это выглядело так:

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


Вы также можете добавить эту строку self.title = "John Doe" в viewDidLoad, что добавит «Профиль» на панель навигации, чтобы пользователь знал, где он находится в данный момент. Сделайте запуск приложения, и результат должен выглядеть следующим образом:

image


Рефакторинг нашего View Controller


Прежде чем продолжить, нам нужно уменьшить наш файл ViewController.swift, чтобы иметь возможность использовать только реальную логику, а не только код для элементов пользовательского интерфейса. Мы можем сделать это, создав подкласс UIView и переместив туда все наши элементы пользовательского интерфейса. Причина, по которой мы делаем это, заключается в том, чтобы следовать архитектурному шаблону Model-View-Controller или MVC для краткости. Подробнее о MVC Model-View-Controller (MVC) в iOS: современный подход.

Теперь щелкните правой кнопкой мыши на папку ContactCard в Project Navigator и выберите «New File»:

image

Нажмите на Cocoa Touch Class, а затем Next. Теперь напишите «ProfileView» в качестве имени класса, а рядом с «Subclass of:» обязательно введите «UIView». Это просто говорит XCode автоматически сделать наш класс наследуемым от UIView, и он добавит некоторый шаблонный код. Теперь нажмите Next, затем Create и удалите закомментированный код:

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


И теперь мы готовы к рефакторингу.

Вырежьте и вставьте все ленивые переменные из контроллера представления в наш новый вид.
Ниже последней отложенной переменной переопределите init(frame :), набрав init, а затем выбрав первый результат автозаполнения из Xcode.

image

Появится ошибка, говорящая о том, что «требуемый» инициализатор «init (coder:)» должен быть предоставлен подклассом «UIView»:

image

Вы можете исправить это, нажав на красный круг, а затем Fix.

image

В любом переопределенном инициализаторе вы почти всегда должны вызывать инициализатор суперкласса, поэтому добавьте эту строку кода вверху метода: super.init (frame: frame).
Вырежьте и вставьте метод addSubviews() под инициализаторами и удалите self.view перед каждым вызовом addSubview.

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


Затем вызовите этот метод из инициализатора:

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


Для ограничений переопределите updateConstraints() и добавьте вызов в конце этой функции (где он всегда будет оставаться):

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


При переопределении любого метода всегда полезно проверить его документацию, посетив документы Apple или, проще, удерживая нажатой клавишу Option (или Alt) и щелкнуть на имя функции:

image

Вырежьте и вставьте код ограничений из контроллера представления в наш новый метод:

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 над viewDidLoad метод let profileView = ProfileView(frame: .zero), добавьте его как подпредставление к представлению ViewController .

Теперь наш контроллер представления уменьшен до нескольких строк кода!

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


Чтобы убедиться, что все работает, как задумано, запустите свое приложение и проверьте, как оно выглядит.

Наличие тонкого, аккуратного контроллера обзора всегда должно быть вашей целью. Это может занять много времени, но это избавит вас от лишних хлопот во время обслуживания.

UITableView


Далее мы добавим UITableView, чтобы представить информацию о контакте, такую ​​как номер телефона, адрес и т.д.

Если вы еще этого не сделали, отправляйтесь в документацию Apple, чтобы ознакомиться с UITableView, UITableViewDataSource и UITableViewDelegate.


Перейдите к ViewController.swift и добавьте lazy var для tableView над viewDidLoad():

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


Если вы попытаетесь запустить приложение, XCode пожалуется, что этот класс не является ни делегатом, ни источником данных для UITableViewController, и поэтому мы добавим эти два протокола в класс:

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


Еще раз, Xcode будет жаловаться на класс, не соответствующий протоколу UITableViewDataSource, что означает, что в этом протоколе есть обязательные методы, которые не определены в классе. Чтобы выяснить, какой из этих методов вы должны реализовать, удерживая Cmd+Control, щелкните по протоколу UITableViewDataSource в определении класса и вы перейдете к определению протокола. Для любого метода, которому не предшествует слово optional, должен быть реализован класс, соответствующий этому протоколу.

Здесь у нас есть два метода, которые нам нужно реализовать:

  1. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int — этот метод сообщает табличному виду, сколько строк мы хотим показать.
  2. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell — этот метод запрашивает ячейку в каждой строке. Здесь мы инициализируем (или повторно используем) ячейку и вставим информацию, которую мы хотим показать пользователю. Например, первая ячейка будет отображать номер телефона, вторая ячейка будет отображать адрес и так далее.


Теперь вернитесь к ViewController.swift, начните вводить numberOfRowsInSection, и когда появится автозаполнение, выберите первый вариант.

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


Удалите код слова и верните сейчас 1.

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


Под этой функцией начните печатать cellForRowAt и снова выберите первый метод из автозаполнения.

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


И, опять же, пока, верните UITableViewCell.

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


Теперь, чтобы подключить наше табличное представление внутри ProfileView, мы определим новый инициализатор, который принимает табличное представление в качестве параметра, чтобы он мог добавить его в качестве подпредставления и установить для него соответствующие ограничения.

Перейдите в ProfileView.swift и добавьте атрибут для табличного представления прямо над инициализатором:

var tableView: UITableView! определяется, поэтому мы не уверены, что он будет постоянно.

Теперь замените старую реализацию init (frame :) на:

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


Xcode теперь будет жаловаться на отсутствующий init (frame :) для ProfileView, поэтому вернитесь к ViewController.swift и замените let profileView = ProfileView (frame: .zero) на

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


Теперь у нашего ProfileView есть ссылка на табличное представление, и мы можем добавить его как подпредставление, и установить для него правильные ограничения.
Вернемся к ProfileView.swift, добавьте addSubview(tableView) в конец addSubviews() и установите эти ограничения в updateConstraints() над super.updateConstraints:

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


Первая строка добавляет три ограничения между табличным представлением и его суперпредставлением: правая, левая и нижняя стороны табличного представления прикреплены к правой, левой и нижней сторонам вида профиля.

Вторая строка прикрепляет верхнюю часть представления таблицы к нижней части сегментированного элемента управления с интервалом в восемь точек между ними. Запустите приложение, и результат должен выглядеть следующим образом:

image


Отлично, теперь все на месте, и мы можем начать внедрять наши ячейки.

UITableViewCell


Чтобы реализовать UITableViewCell, нам почти всегда нужно будет создавать подклассы этого класса, поэтому щелкните правой кнопкой мыши папку ContactCard в Навигаторе проекта, затем «New file…», затем «Cocoa Touch Class» и «Next».

Введите «UITableViewCell» в поле «Subclass of:», и Xcode автоматически заполнит имя класса «TableViewCell». Введите «ProfileView» перед автозаполнением, чтобы окончательное имя было «ProfileInfoTableViewCell», затем нажмите «Next» и «Create». Идите дальше и удалите созданные методы, поскольку они нам не понадобятся. Если хотите, вы можете сначала прочитать их описания, чтобы понять, почему они нам не нужны прямо сейчас.

Как мы уже говорили ранее, наша ячейка будет содержать основную информацию, которая является названием поля и его описанием, и поэтому нам понадобятся метки для них.

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


И теперь мы переопределим инициализатор, чтобы можно было настроить ячейку:

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


Что касается ограничений, мы собираемся сделать немного другое, но, тем не менее, очень полезное:

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


Здесь мы начинаем использовать UIEdgeInsets, чтобы установить интервалы вокруг каждой метки. Объект UIEdgeInsets может быть создан с использованием метода UIEdgeInsetsMake(top:, left:, bottom:, right:). Например, для titleLabel мы говорим, что хотим, чтобы верхнее ограничение составляло четыре точки, а правое и левое — восемь. Мы не заботимся о дне, потому что исключаем его, так как прикрепим его к верхней части метки описания. Потратьте минуту, чтобы прочитать и визуализировать все constraint’ы в вашей голове.

Отлично, теперь мы можем начать отрисовку ячеек в нашем табличном представлении. Давайте перейдем к ViewController.swift и изменим ленивую инициализацию нашего табличного представления, чтобы зарегистрировать этот класс ячеек в табличном представлении и установить высоту для каждой ячейки.

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


Мы также добавляем константу для идентификатора повторного использования ячейки. Этот идентификатор используется для удаления ячеек из табличного представления, когда они отображаются. Это оптимизация, которую можно (и нужно) использовать, чтобы помочь UITableView повторно использовать ячейки, которые были представлены ранее, для отображения нового контента вместо перерисовки новой ячейки с нуля.
Теперь позвольте мне показать вам, как повторно использовать ячейки в одной строке кода в методе cellForRowAt:

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


Здесь мы сообщаем табличному представлению об изъятии из очереди многократно используемой ячейки с использованием идентификатора, под которым мы зарегистрировали путь к ячейке, которая собирается появиться пользователю. Затем мы принудительно приводим ячейку к ProfileInfoTableViewCell, чтобы иметь возможность доступа к ее свойствам, чтобы мы могли, например, установить заголовок и описание. Это можно сделать с помощью следующего:

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
}


А теперь установите numberOfRowsInSection, чтобы вернуть »3» и запустить ваше приложение.

image


Удивительно, правда?

Self-Sizing Cells


Возможно, и, скорее всего, будет случай, когда вы захотите, чтобы разные ячейки имели высоту в соответствии с информацией внутри них, которая не известна заранее. Для этого вам понадобится табличное представление с автоматически вычисляемыми размерами, и на самом деле существует очень простой способ сделать это.

Прежде всего, в ProfileInfoTableViewCell добавьте эту строку в ленивый инициализатор descriptionLabel:

label.numberOfLines = 0


Вернитесь к ViewController и добавьте эти две строки в инициализатор табличного представления:

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


Здесь мы сообщаем табличному представлению, что высота строки должна иметь автоматически рассчитанное значение на основе ее содержимого.

Что касается расчетной высоты строки:

«Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view.» — Apple Docs


В ViewDidLoad нам нужно перезагрузить табличное представление, чтобы эти изменения вступили в силу:

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


Теперь перейдите и добавьте еще одну ячейку, увеличив количество строк до четырех и добавив еще один оператор switch в cellForRow:

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


Теперь запустите приложение, и оно должно выглядеть примерно так:

image


Заключение


Удивительно, не правда ли? И в качестве напоминания о том, почему мы на самом деле пишем кодом наш пользовательский интерфейс, вот целое сообщение в блоге, написанное нашей мобильной командой, о том, почему мы не используем раскадровки в Instabug.

Что вы сделали в двух частях этого урока:

  • Удален файл main.storyboard из вашего проекта.
  • Создали UIWindow программно и назначил ему rootViewController.
  • Создали различные элементы пользовательского интерфейса в коде, такие как метки, представления изображений, сегментированные элементы управления и представления таблиц со своими ячейками.
  • Вложили UINavigationBar в ваше приложение.
  • Создали динамический размер UITableViewCell.

© Habrahabr.ru