В iOS-приложениях часто нужно добавлять различную базовую функциональность для таких классов как UIViewController
, AppDelegate
, UIView
и прочих. Паттерн Pluginable позволяет сделать это очень гибко с помощью плагинов, которые подключаются к классу и реагируют на его события.
Проблема
Рассмотрим типичное приложение, не iOS, где каждый экран представлен классом UIViewController
. Скорее всего, у нас будет несколько экранов, где нам прийдется отслеживать появление клавиатуры, отслеживать жизненный цикл контроллера + приложения, показывать ошибки, подключать ViewModel
и делать еще кучу других вещей, которые связаны именно с UIViewController
и его жизненным циклом.
Писать одинаковый код в каждом из UIViewController
– лень и, откровенно, неправильно, ведь каждая операция обрабатывается одинаково.
Типичное решение – создать собственный базовый BaseViewController
, поместить весь общий код в него и наследовать все остальные контроллеры нет от UIViewController
, а от BaseViewController
.
Это избавит от дублирования и позволит использовать общую функциональность на каждом экране. Но у данного подхода есть ряд проблем:
BaseViewController
разрастается, и становится все сложнее вносить изменения и понимать, что там происходит;- у нас появляются контроллеры, которым требуется несколько иное поведение, чем заложено в
BaseViewController
. Приходится использовать различные флаги, переопределения и прочие ухищрения, что бы «подогнать» функциональность под конкретный случай; - появляются группы контроллеров, которые требуют уникального поведения (те есть не нужного больше нигде), но его приходится вносить в
BaseViewController
.
Другим решением проблемы может должна быть агрегация общей функциональности в отдельные классы, которые можно подключать к отдельным UIViewController
по мере необходимости.
Этот способ лучше предыдущего, но есть нюанс: если для решения проблемы нужно производить какие-то действия, на основе жизненного цикла контроллера, то в каждом контроллере придется повторять реализацию методов жизненного цикла, что бы вызывать общие методы.
Решение
Создать BaseViewController
, но не добавлять в него конкретную базовую функциональность, а сделать возможность добавлять любую функциональность на основе плагинов.
Для этого необходимо:
- описать протокол плагина;
- перечислить в нем все методы из
UIViewController
, на которые плагину может потребоваться реакция; - добавить в
BaseViewController
массив типа протокола; - в каждом нужном методе
BaseViewController
итерировать по массиву плагинов и вызывать соответствующий метод у каждого плагина.
Пример
Для начала создадим протокол BaseViewControllerPlugin
, который будет реализовывать плагины.
Перечислим в нем методы, которые нам нужны. Для примера ограничимся базовым жизненным циклом. Но при желании, можно добавить любые методы BaseViewController
.
protocol BaseViewControllerPlugin {
func onInit(controller: BaseViewController)
func onViewDidLoad()
func onViewWillAppear(_ animated: Bool)
func onViewDidAppear(_ animated: Bool)
func onViewWillDisappear(_ animated: Bool)
func onViewDidDisappear(_ animated: Bool)
func onViewWillLayoutSubviews()
func onViewDidLayoutSubviews()
}
Добавим реализацию по умолчанию, чтобы каждый конкретный плагин мог реализовывать только нужные ему методы.
extension BaseViewControllerPlugin {
func onInit(controller: BaseViewController) {}
func onViewDidLoad() {}
func onViewWillAppear(_ animated: Bool) {}
func onViewDidAppear(_ animated: Bool) {}
func onViewWillDisappear(_ animated: Bool) {}
func onViewDidDisappear(_ animated: Bool) {}
func onViewWillLayoutSubviews() {}
func onViewDidLayoutSubviews() {}
}
Добавим в BaseViewController
массив для хранения плагинов private let plugins: [BaseViewControllerPlugin]
, конструктор, для регистрации плагинов init(plugins: [BaseViewControllerPlugin])
и переопределим нужные методы, уведомляя в каждом из них наши плагины, о том что событие наступило.
class BaseViewController: UIViewController {
private let plugins: [BaseViewControllerPlugin]
init(plugins: [BaseViewControllerPlugin]) {
self.plugins = plugins
super.init(nibName: nil, bundle: nil)
plugins.forEach { $0.onInit(controller: self) }
}
@available(*, unavailable, message: "Use init() insted")
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
plugins.forEach { $0.onViewDidLoad() }
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
plugins.forEach { $0.onViewWillAppear(animated) }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
plugins.forEach { $0.onViewDidAppear(animated) }
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
plugins.forEach { $0.onViewWillDisappear(animated) }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
plugins.forEach { $0.onViewDidDisappear(animated) }
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
plugins.forEach { $0.onViewWillLayoutSubviews() }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
plugins.forEach { $0.onViewDidLayoutSubviews() }
}
}
Напишем для примера примитивный менеджер клавиатуры, который просто будет менять какой либо констрейнт при показе или скрытии клавиатуры.
Главное в нем то, что он начинает слушать события при показе контроллера и перестает – при скрытии.
class ExampleKeyboardManager: BaseViewControllerPlugin {
private let constraint: NSLayoutConstraint
init(constraint: NSLayoutConstraint) {
self.constraint = constraint
}
func onViewDidAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillChangeFrame),
name: UIApplication.keyboardWillChangeFrameNotification,
object: nil
)
}
func onViewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(
self,
name: UIApplication.keyboardWillChangeFrameNotification,
object: nil
)
}
@objc
func keyboardWillChangeFrame(_ notification: NSNotification) {
guard
let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
else { return }
constraint.constant = frame.cgRectValue.height
}
}
Теперь мы можем добавлять слежение за клавиатурой в любой контроллер. Достаточно только унаследоваться от BaseViewController
и в конструкторе зарегистрировать необходимый плагин.
final class AuthViewController: BaseViewController {
private var scrollViewBottomConstraint: NSLayoutConstraint!
init() {
super.init(plugins: [
ExampleKeyboardManager(constraint: scrollViewBottomConstraint)
])
}
}
Итог
"Плагинизация" базового контроллера позволила:
- избавиться от дублирования кода;
- разгрузить базовый контроллер;
- сделать конфигурацию разных контроллеров гибкой и в то же время удобной.
Разумеется, такой же подход можно применять к любому классу UIViewController
, UINavigationViewController
, AppDelegate
, UIView
и так далее…