У вас большой UIViewController? У многих да. С одной стороны, в нём работа с данными, с другой — с интерфейсом.
Задачи отделения логики от интерфейса описаны в сотнях статей про архитектуру: MVP, MVVM, VIPER. Они решают проблему потока данных, но не отвечают на вопрос как работать с интерфейсом: в одном месте остается создание элементов, лейаут, настройка, обработка ввода и анимации.
Давайте отделим view от controller и посмотрим чем нам поможет loadView().
Интерфейс приложения для iOS — это иерархия
Если посмотреть на методы класса
Жизненный цикл
Мы можем переопределить метод и указать свой класс.
Контроллер загрузит
Но пока компилятор не знает о классе и считает, что там обычный
Теперь можно видеть переменные
Шрифты, цвета, констрейнты и иерархию можно настроить прямо в конструкторе CustomView:
Лучшее место для ручного лейаута — метод
Если есть время, то я делаю
Преимущество в инкапсуляции: внутренняя логика скрыты за интерфейсом. Например, валидность объекта может показываться цветом области, а не квадрата, но контроллер ничего об этом не узнает.
Если использовать Interface Builder, то часто
Подкласс для
Нужно выделить объект
Если выбрать контрол внутри
Если создавать интерфейс в коде, то все объекты доступны после
На мой вкус, контроллеру стоит оставлять все действия пользователя. Из стандартных:
При этом
В Objective-C можно полноценно заменить тип
В примере на GitHub можно посмотреть на разделение классов для простой задачи: цвет квадрата зависит от его положения (в зелёной области он зелёный, вне её — красный).
Чем сложнее экран, тем лучше эффект: контроллер уменьшается, код переносится на свои места. Код просто переносится во
Я использовал этот код в разных проектах и с разными людьми, каждый раз команда приветствовала упрощение и подхватывала практику. Надеюсь, что и вам она понравится. Всем лёгких
Задачи отделения логики от интерфейса описаны в сотнях статей про архитектуру: MVP, MVVM, VIPER. Они решают проблему потока данных, но не отвечают на вопрос как работать с интерфейсом: в одном месте остается создание элементов, лейаут, настройка, обработка ввода и анимации.
Давайте отделим view от controller и посмотрим чем нам поможет loadView().
Интерфейс приложения для iOS — это иерархия
UIView
. Задачи каждой view
: создать элементы, настроить, разложить по местам, анимировать. Это видно из методов, которые есть в классе UIView: addSubview(), drawRect(), layoutSubviews().
Если посмотреть на методы класса
UIViewController
, то видно, что он занимается управлением view:
загружает, реагирует на загрузку экранов и действия пользователя, показывает новые экраны. Часто код, который должен быть в UIView
, мы пишем в подклассах UIViewController
, от этого он становится слишком большим. Отделим его.loadView()
Жизненный цикл
UIViewController
начинается с loadView()
. Упрощённая реализация выглядит так: // CustomViewController.swift
func loadView() {
self.view = UIView()
}
Мы можем переопределить метод и указать свой класс.
super.loadView()
вызывать не нужно!// CustomViewController.swift
override func loadView() {
self.view = CustomView()
}
Реализация CustomView.swift
// CustomView.swift
final class CustomView {
let square: UIView = UIView()
init() {
super.init()
square.backgroundColor = .red
addSubview(square)
}
}
Контроллер загрузит
CustomView,
добавит его в иерархию, выставит .frame
. Свойство .view
будет нужного нам класса: // CustomViewController.swift
print(view) // CustomView
Но пока компилятор не знает о классе и считает, что там обычный
UIView
. Поправим это функцией с приведением типа:// CustomViewController.swift
func view() -> CustomView {
return self.view as! CustomView
}
Теперь можно видеть переменные
CustomView
:// CustomViewController.swift
func viewDidLoad() {
super.viewDidLoad()
view().square // Работает
}
Упрощаем с помощью associatedtype
Руслан Кавецкий предложил убрать дублирование кода с помощью расширения протокола:
protocol ViewSpecificController { associatedtype RootView: UIView } extension ViewSpecificController where Self: UIViewController { func view() -> RootView { return self.view as! RootView } }
Для каждого нового контроллера нужно только указать протокол и подкласс для егоUIView
черезtypealias
:
// CustomViewController.swift final class CustomViewController: UIViewController, ViewSpecificController { typealias RootView = CustomView func viewDidLoad() { super.viewDidLoad() view().square // Работает } }
Код в подклассе UIView
Создание и настройка контролов
Шрифты, цвета, констрейнты и иерархию можно настроить прямо в конструкторе CustomView:
// CustomView.swift
init() {
super.init()
backgroundColor = .lightGray
addSubview(square)
}
layoutSubviews()
Лучшее место для ручного лейаута — метод
layoutSubviews()
. Он вызывается каждый раз, когда меняется размер view
, поэтому можно опираться на размер bounds
для правильных расчётов:// CustomView.swift
override func layoutSubviews() {
super.layoutSubviews()
square.frame = CGRect(x: 0, y: 0: width: 200, height: 200)
square.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
}
Приватные контролы, публичные свойства
Если есть время, то я делаю
property
контролов приватными, но управляю ими через публичные переменные или функции «в области знаний». Проще на примере:// CustomView.swift
private let square = UIView()
var squarePositionIsValid: Bool {
didSet {
square.backgroundColor = squarePositionIsValid? .green : .red
}
}
func moveSquare(to newCenter: CGPoint) {
square.center = newCenter
}
Преимущество в инкапсуляции: внутренняя логика скрыты за интерфейсом. Например, валидность объекта может показываться цветом области, а не квадрата, но контроллер ничего об этом не узнает.
Что остаётся во viewDidLoad()?
Если использовать Interface Builder, то часто
viewDidLoad()
пустой. Если view
создавать в коде, то нужно привязать их действия через target-action паттерн, добавить UIGestureRecognizer
или связать делегаты. Настраиваем через Interface Builder
Подкласс для
view
можно настроить через Interface Builder (далее IB). Нужно выделить объект
view
(не контроллер) и задать его класс. Писать собственный loadView()
не нужно, контроллер сделает это сам. Но приводить тип UIView
всё ещё приходится. IBOutlet в UIView
Если выбрать контрол внутри
view
, то Assistant Editor распознает класс UIView
и предложит его в качестве второго файла в Automatic режиме. Так можно переносить IBOutlet
во view
.Если не работает
Открыть класс
CustomView
вручную, написать IBOutlet
. Теперь можно потащить за маркер и навести на элемент в IB.Если создавать интерфейс в коде, то все объекты доступны после
init()
, но при работе с IB доступ к IBOutlet
появляется только после загрузки интерфейса из UIStoryboard
в методе awakeFromNib()
:// CustomView.swift
func awakeFromNib() {
super.awakeFromNib()
square.layer.cornerRadius = 8
}
IBAction в UIViewController
На мой вкус, контроллеру стоит оставлять все действия пользователя. Из стандартных:
- target-action от контролов
- реализация delegate в
UIViewController
- реализация блоков
- реакция на
Notification
При этом
UIViewController
управляет только интерфейсом. Всё что касается бизнес-логики стоит выносить из контроллера, но это уже на выбор: MVP, VIPER и т.д.Objective-C
В Objective-C можно полноценно заменить тип
UIView
. Для этого нужно объявить property с нужным классом, переопределить setter
и getter
, указав класс:// CustomViewController.m
@interface CustomViewController
@property (nonatomic) CustomView *customView;
@end
@implementation
- (void)setView:(CustomView *)view{
[super setView:view];
}
- (CustomView *)view {
return (CustomView *)super.view;
}
@end
Конец
В примере на GitHub можно посмотреть на разделение классов для простой задачи: цвет квадрата зависит от его положения (в зелёной области он зелёный, вне её — красный).
Чем сложнее экран, тем лучше эффект: контроллер уменьшается, код переносится на свои места. Код просто переносится во
view
, но инкапсуляция упрощает взаимодействие и чтение кода. Иногда view
можно переиспользовать с другим контроллером. Например, разные контроллеры для iPhone и iPad по-своему реагируют на появление клавиатуры, но это никак не меняет код view
.Я использовал этот код в разных проектах и с разными людьми, каждый раз команда приветствовала упрощение и подхватывала практику. Надеюсь, что и вам она понравится. Всем лёгких
UIViewController
! Комментарии (12)
Rogaven
11.12.2018 11:35На самом деле, MVC так и задумывался. В оригинале модели еще и активные были, а не просто структуры данных.
anioutka
12.12.2018 16:15В принципе вынесение UI в UIView и кода по его обработке аналогично реализации фрагментов в Android и применяется в тех же случаях и целях. Я подобный подход использую уже несколько лет. Только верстку я делаю на xib именно отдельного UIView, а не UIViewController. В случае, когда нужно менять в одном и том же контроллере разные виды, это более корректно.
storoj
А теперь смотрим, как сделан UITableViewController, что-то я там не видел property типа UITableView *view.
akaDuality Автор
Apple меняет подклассы.
Я поставил брейкпойнт во
viewDidLoad
и посмотрел типыview
(черезpo view
в консоли). Результаты:UITableViewController — UITableView
UICollectionViewController — UICollectionViewControllerWrapperView
у которого только один дочернийUICollectionView
Более того, можно заменить даже класс CALayer для UIView. Для этого есть
type property
в классеUIView
:storoj
можно, но никто не перекрывает UIView *view, а добавляют новое типизированное свойство
storoj
С UICollectionViewController кстати отличный пример: тип self.view никого не должен интересовать, это свойство как правило используется только для создания иерархии childViewController или subviews на этом экране. А для доступа к collectionView есть отдельное свойство. С таким подходом можно как угодно менять иерархию внутри view этого контроллера, а для клиентского кода эти изменения пройдут незамеченными.
Krypt
Это хвост из Obj-C, по видимому. Там возможно переопределить тип в заголовочном файле таким же способов. Так как objective-c использует селекторы настоящий класс не важен, важно лишь чтобы класс отзывался на нужные.
storoj
только так, как в статье в последнем параграфе тоже не делают, а используют dynamic
Krypt
@dynamic
нужен только для того, чтобы сказать компилятору, что не нужно генерировать getter/setter. Вы можете изменить свойство у существующего поля. Помнится, я так с datasource делал, так как потомку от класса нужны были дополнительные данные.Впрочем, в случае с кастомными контроллерами, я бы поступал по аналогии с системными классами — для единообразия.
storoj
Правильно, это как раз тот случай, когда ничего не надо генерировать, т.к. UIViewController уже всё что надо сгенерировал.
Krypt
> не видел property типа UITableView *view
К слову, раньше view и tableView возвращали один и тот же объект. На данный момент не знаю, не перепроверял.
storoj
Это лишь частный случай. А в UICollectionViewController уже не так.