Краткий рассказ о «композиции над наследованием» (включая дженерики, ура!)
Отказ от ответственности : этот метод актуален только в том случае, если вы создаете свои макеты программно (без Interface Builder и раскадровок). Если вы хотите глубже понять, почему вам следует отказаться от Interface Builder, обязательно прочтите эту замечательную статью команды Zeplin. С другой стороны, если вы хотите укрепить свою любовь к Interface Builder, я настоятельно рекомендую эту вдохновляющую статью от Скотта Берревоетса из Lyft.
Что случилось?
Возможно, вы знаете это разочарование: вы только что создали идеальный подкласс UITableViewCell
, он выглядит и работает так, как вы хотели, и все отлично ... пока вам не понадобится точно такой же макет где-то еще в вашем приложении, а главное - вне табличного представления.
Полный разочарований по поводу того, что UITableViewCell
и UIView
похожи, но на самом деле разные (технически вы можете использовать UITableViewCell
как UIView
, но такая практика довольно сомнительна), вы решаете просто скопировать и вставить код во вновь созданный подкласс UIView
. И здесь все начинает идти в корне неправильно - потому что в тот момент, когда вам нужно внести изменение, вы должны сохранить это изменение в нескольких местах своей кодовой базы.
Так что я могу с этим поделать?
Оказывается, решение невероятно простое, практически без недостатков в будущем. Во-первых, давайте составим очень простой протокол
protocol ViewLayout { func layout(on view: UIView) init() }
Затем мы создаем макет, используя этот протокол.
final class TwoLabelsLayout : ViewLayout { let leftLabel = UILabel() let rightLabel = UILabel() init() { } func layout(on view: UIView) { let stackView = UIStackView(arrangedSubviews: [leftLabel, rightLabel]) stackView.axis = .horizontal stackView.translatesAutoresizingMaskIntoConstraints = false // don't forget this one to enable Auto Layout in code view.addSubview(stackView) stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 10).isActive = true } }
Посмотрите, как вы не имеете дело с какими-либо деталями жизненного цикла представления и - вы просто пишете код для конкретного макета.
Следующим шагом будет создание вспомогательных универсальных базовых классов:
class View<Layout : ViewLayout> : UIView { public let layout: Layout public override init(frame: CGRect) { self.layout = Layout() super.init(frame: frame) layout.layout(on: self) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } class TableViewCell<Layout : ViewLayout> : UITableViewCell { public let layout: Layout override init(style: UITableViewCellStyle, reuseIdentifier: String?) { self.layout = Layout() super.init(style: style, reuseIdentifier: reuseIdentifier) layout.layout(on: contentView) // important! } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
И теперь мы можем безболезненно использовать наши макеты для создания представлений
let twoLabels = View<TwoLabelsLayout>(frame: .zero)
или зарегистрируйте классы ячеек табличного представления
tableView.register(TableViewCell<TwoLabelsLayout>.self, forCellReuseIdentifier: "two-labels")
Это было довольно просто, да?
Конечно, чтобы получить доступ к содержанию представления и изменить его, вы должны использовать синтаксис .layout
twoLabels.layout.leftLabel.text = "AnySuggestion"
Хорошо, но что мне делать с существующей кодовой базой?
Принять этот подход в уже существующих представлениях и ячейках представления таблиц очень просто (хотя и требует немного времени).
Во-первых, конечно, вы должны извлечь весь код макета в эти ViewLayout
объекты. Затем, если, например, у вас есть TextFieldAndImageView
, вы просто меняете его суперкласс и оборачиваете эти .layout
вызовы, чтобы ваш уже существующий код компилировался:
class TextFieldAndImageView : View<TextFieldAndImageLayout> { var textField: UITextField { return layout.textField } var imageView: UIImageView { return layout.imageView } }
Вот и все: вы только что сделали код представления более пригодным для повторного использования, не нарушив ни одной строчки пользовательского кода. Отличная работа!
Я пробовал это, но не работает. Что случилось?
При адаптации принципа «макета просмотра» есть некоторые моменты, в которых следует проявлять осторожность:
- В вашем
layout(on:)
методеViewLayout
вам нужно не забыть вызватьview.addSubview
, чтобы ваши данные действительно отображались на экране. - При создании базового
TableViewCell<Layout : ViewLayout>
класса очень важно писатьlayout.layout(on: contentView)
вместоlayout.layout(on: self)
. translatesAutoresizingMaskIntoConstraints = false
. Всегда актуально.
Звучит здорово, но что, если я использую Interface Builder?
Интерфейсный разработчик, безусловно, отличный инструмент, но он является огромным, огромным врагом возможности повторного использования кода. Широкое использование Interface Builder и Storyboards требует много размышлений и усилий, чтобы все делать «правильно», поэтому полный отказ от Interface Builder может фактически улучшить архитектуру вашего приложения. Просмотрите ссылки в заявлении об отказе от ответственности или ниже, чтобы лучше понять проблему.
В ViewLayout
подходе нет ничего нового, но он применяет некоторые общие архитектурные принципы (такие как возможность повторного использования, разделение задач и «композиция вместо наследования») к специфическому для UIKit коду. Для меня использование этого подхода оказалось гораздо более гибким, масштабируемым и поддерживаемым решением, чем просто наличие UIView
подклассов повсюду.
Спасибо, что прочитали сообщение! Не стесняйтесь спрашивать или предлагать что-либо в разделе Ответы ниже. Вы также можете связаться со мной в Твиттере или найти меня на GitHub. Если вы написали статью (или наткнулись на нее), посвященную аналогичной теме, обязательно разместите ссылку на нее в ответах, чтобы я мог включить ее прямо ниже.
Дальнейшее обучение:
- Протоколное и ценностно-ориентированное программирование в приложениях UIKit Джейкоба Сяо и Алекса Мигиковски (Apple, WWDC 2016)
- Жизнь без Interface Builder от команды Zeplin.
Привет! Я Олег, автор книги Женский футбол 2017 и независимый разработчик iOS / watchOS, страстно увлеченный Swift. Пока я работаю над своим следующим приложением, вы можете проверить мой последний проект под названием The Cleaning App - небольшую утилиту, которая поможет вам отслеживать ваши процедуры очистки. Спасибо за поддержку!