Хабрахабр

[Из песочницы] Контроллер, полегче! Выносим код в UIView

У вас большой UIViewController? У многих да. С одной стороны, в нём работа с данными, с другой — с интерфейсом.

Они решают проблему потока данных, но не отвечают на вопрос как работать с интерфейсом: в одном месте остается создание элементов, лейаут, настройка, обработка ввода и анимации. Задачи отделения логики от интерфейса описаны в сотнях статей про архитектуру: MVP, MVVM, VIPER.

Давайте отделим view от controller и посмотрим чем нам поможет loadView().

Задачи каждой view: создать элементы, настроить, разложить по местам, анимировать.
Интерфейс приложения для iOS — это иерархия UIView. Это видно из методов, которые есть в классе UIView: addSubview(), drawRect(), layoutSubviews().

Часто код, который должен быть в UIView, мы пишем в подклассах UIViewController, от этого он становится слишком большим. Если посмотреть на методы класса UIViewController, то видно, что он занимается управлением view: загружает, реагирует на загрузку экранов и действия пользователя, показывает новые экраны. Отделим его.

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
}

Контроллер загрузит 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) let center = CGPoint(x: bounds.width, y: bounds.height / 2) square.center = center
}

Приватные контролы, публичные свойства

Если есть время, то я делаю 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).

Писать собственный loadView() не нужно, контроллер сделает это сам. Нужно выделить объект view (не контроллер) и задать его класс. Но приводить тип 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, но инкапсуляция упрощает взаимодействие и чтение кода. Чем сложнее экран, тем лучше эффект: контроллер уменьшается, код переносится на свои места. Например, разные контроллеры для iPhone и iPad по-своему реагируют на появление клавиатуры, но это никак не меняет код view. Иногда view можно переиспользовать с другим контроллером.

Надеюсь, что и вам она понравится. Я использовал этот код в разных проектах и с разными людьми, каждый раз команда приветствовала упрощение и подхватывала практику. Всем лёгких UIViewController!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть