В процессе разработки приложений часто требуется изменять данные на одном экране на основе взаимодействия с другим или устанавливать связь между разными экранами. Для эффективного решения этих сценариев шаблон делегирования становится ценным инструментом в наборе инструментов iOS-разработчика.
Что вы узнаете:
- Что такое шаблон делегата;
- Как реализовать шаблон делегата с протоколами;
- Как реализовать шаблон делегата в UIKit.
Шаблон делегата
Делегат — это шаблон проектирования, используемый, когда объекту необходимо назначить задачи или обязанности другому. Предположим, у нас есть две сущности Counter и Control:
class Counter { private var value: Int = 0 func increment() { self.value += 1 } } class Control {}
В приведенном выше примере Counter — это простой класс, который отвечает за хранение данных, их обработку и отображение на экране, тогда как Control содержит элементы управления, используемые пользователем для изменения настроек приложения. данные. Очевидно, что только Counter имеет доступ к данным (поскольку в его обязанности входит их хранение). Как мы можем разрешить Control изменять данные счетчика?
Поскольку объект Counter может изменять свои собственные данные, мы можем передать ссылку Counter в Control. Это позволяет нам связываться с Counter, когда данные необходимо изменить:
// Delegate class Counter { private var value: Int = 0 func increment() { self.value += 1 } } // Delegator class Control { private var delegate: Counter init(delegate: Counter) { self.delegate = delegate } }
Теперь, когда у нас есть ссылка на Counter, мы можем взаимодействовать с ним. Это означает, что Контроль может делегировать задачи Счетчику.
Внутри Control у нас может быть метод, который будет вызываться, например, всякий раз, когда нажимается кнопка. В этом методе мы можем инициировать событие в Counter, чтобы увеличить значение его свойства value:
// Delegator class Control { private var delegate: Counter init(delegate: Counter) { self.delegate = delegate } func buttonClicked() { self.delegate.increment() } }
Теперь каждый раз, когда метод buttonClicked вызывается в Control, Counter будет увеличивать свое 'значение'. свойство. Мы можем проверить это, добавив наблюдателя для значения свойства 'value' элемента Counter:
... private var value: Int = 0 { didSet { print("Counter value: \(value)") } } ...
Вот весь код, вы можете протестировать его на быстрой игровой площадке:
// Delegate class Counter { private var value: Int = 0 { didSet { print("Counter value: \(value)") } } func increment() { self.value += 1 } } // Delegator class Control { private var delegate: Counter init(delegate: Counter) { self.delegate = delegate } func buttonClicked() { self.delegate.increment() } } let counter = Counter() let control = Control(delegate: counter) control.buttonClicked() // Counter value: 1 control.buttonClicked() // Counter value: 2
Запустив этот код, мы видим, что хотя Control не имеет доступа к свойству 'value' элемента Counter, он может делегировать ответственность за увеличение значения Counter.
Реализация делегирования с помощью протоколов
В предыдущем подходе очевидно, что объект Control имеет больше прав доступа, чем необходимо. Если Counter содержит другие данные или методы, связанные с отображением значения на экране, Control не отвечает за это и не должен быть ему доступен. Кроме того, сущность, удовлетворяющая требованиям Счетчика, не является достаточно универсальной, поскольку Контроль может использовать любую сущность с методом приращения. Наша цель в использовании протоколов — изолировать обязанности и отделить Контроль от Счетчик.
Поскольку требованием для CounterDelegate является наличие только метода 'increment', мы можем создать наш протокол с ним:
protocol CounterDelegate { func increment() -> Void }
Давайте реорганизуем Control, чтобы он больше не зависел от Counter, а вместо этого от любого объекта, который соответствует протоколу CounterDelegate:
class Control { private var delegate: CounterDelegate init(delegate: CounterDelegate) { self.delegate = delegate } func buttonClicked() { self.delegate.increment() } }
Теперь любой объект, реализующий протокол CounterDelegate, может использоваться Control в качестве делегата.
Приведем класс Counter в соответствие с протоколом:
class Counter: CounterDelegate { private var counter: Int = 0 { didSet { print("Valor do contador: \(counter)") } } func increment() { self.counter += 1 } }
Теперь класс Counter реализует протокол CounterDelegate, и мы ожидаем, что код продолжит работать правильно.
protocol CounterDelegate { func increment() -> Void } // Delegate class Counter: CounterDelegate { private var value: Int = 0 { didSet { print("Counter value: \(value)") } } func increment() { self.value += 1 } } // Delegator class Control { private var delegate: CounterDelegate init(delegate: CounterDelegate) { self.delegate = delegate } func buttonClicked() { self.delegate.increment() } } let counter = Counter() let control = Control(delegate: counter) control.buttonClicked() // Counter value: 1 control.buttonClicked() // Counter value: 2
Реализация шаблона делегирования в UIKit
Теперь, когда мы понимаем, как работает Delegate, давайте рассмотрим одно из его наиболее распространенных приложений, которое используется совместно с UIKit. Давайте возьмем тот же вариант использования, но на этот раз с реальными экранами.
Для начала создадим наш MainViewController, который будет отвечать за счетчик (будем использовать код вида):
class MainViewController: UIViewController { // Counter value private var value: Int = 0 // Counter label view lazy var counterLabel: UILabel = { let frame = CGRect(x: view.bounds.midX - 50, y: view.bounds.midY - 30, width: 100, height: 60) let label = UILabel(frame: frame) label.text = String(value) label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 64, weight: .bold) return label }() // Show Button lazy var navigateButton: UIButton = { let frame = CGRect(x: view.bounds.midX - 100, y: view.bounds.maxY - 140, width: 200, height: 60) let button = UIButton(frame: frame) button.backgroundColor = .black button.setTitle("Show", for: .normal) button.layer.cornerRadius = 16 return button }() override func viewDidLoad() { super.viewDidLoad() // Setting up the view controller view.backgroundColor = .white view.addSubview(counterLabel) view.addSubview(navigateButton) // Setting action to navigateButton navigateButton.addTarget(self, action: #selector(navigate), for: .touchUpInside) } // Function that is called when navigateButton is pressed @objc private func navigate() {} }
На данный момент у нас есть код только для первого экрана. Если все реализовано правильно, у вас должно получиться что-то похожее на следующее изображение:
Теперь добавим код для экрана управления:
class SheetViewController: UIViewController { // Increment button lazy var incrementButton: UIButton = { let frame = CGRect(x: view.bounds.midX - 100, y: 140, width: 200, height: 60) let button = UIButton(frame: frame) button.backgroundColor = .black button.layer.cornerRadius = 16 button.setTitle("Increment", for: .normal) return button }() override func viewDidLoad() { super.viewDidLoad() // Setting up the view controller view.addSubview(incrementButton) view.backgroundColor = .cyan // Setting action to incrementButton incrementButton.addTarget(self, action: #selector(increment), for: .touchUpInside) } // Function that is called when incrementButton is pressed @objc func increment() {} }
Это даст нам модальное представление с элементами управления.
Чтобы представить SheetViewController, нам нужно добавить код навигации и построения экрана в MainViewController:
... @objc private func navigate() { // Createing the control modal let controlViewController = SheetViewController() // Presenting it present(controlViewController, animated: true) } ...
Теперь мы можем переходить с одного экрана на другой:
Однако кнопка увеличения пока ничего не делает. Начнем с создания нашего протокола. Поскольку мы хотим увеличить значение в MainViewController, единственная функция, которая нам нужна в протоколе, — это функция увеличения:
protocol IncrementValueDelegate { func increment() -> Void }
Теперь мы можем добавить эту зависимость в SheetViewController:
class SheetViewController: UIViewController { // Delegate reference var delegate: IncrementValueDelegate? ... }
И когда кнопка в SheetViewController нажата, мы просто делегируем задачу увеличения значения нашему делегату:
... @objc func increment() { delegate?.increment() } ...
Затем нам нужно заставить MainViewController использовать протокол IncrementValueDelegate:
class MainViewController: UIViewController, IncrementValueDelegate { ... func increment() { // Incrementing value value += 1 // Passing value to counterLabel counterLabel.text = String(value) } }
Наконец, нам просто нужно сообщить SheetViewController, что MainViewController будет его делегатом, и все заработает:
... @objc private func navigate(_ sender: UIButton) { // Createing the control modal let controlViewController = SheetViewController() // Passing delegate reference to controlViewController controlViewController.delegate = self // Presenting it present(controlViewController, animated: true) } ...
Теперь SheetViewController делегирует задачу увеличения значения MainViewController.
Полный код на GitHub.
Преимущества и недостатки использования делегатов
Преимущества
- Слабая связь. Делегаты помогают достичь слабой связи между объектами. Разделяя обязанности и определяя протоколы, делегаты позволяют объектам взаимодействовать, не зная друг друга напрямую.
- Модульный и расширяемый. Делегаты продвигают модульный и расширяемый код, обеспечивая четкое разделение задач. Различные объекты могут действовать как делегаты, что обеспечивает гибкую настройку и расширение поведения.
- Повторное использование кода. Делегаты облегчают повторное использование кода. Поскольку делегаты могут быть реализованы несколькими объектами, одна и та же реализация делегата может использоваться в разных контекстах или сценариях.
- Механизм обратного вызова. Делегаты включают механизмы обратного вызова, когда один объект уведомляет другой объект об определенных событиях или действиях. Это полезно для реализации архитектур, управляемых событиями, или для обработки асинхронных операций.
Недостатки
- Сложное управление потоком. По мере увеличения числа делегатов и протоколов поток управления может усложняться. Может потребоваться тщательное управление и координация, чтобы обеспечить назначение и вызов правильных делегатов в нужное время.
- Возможность утечек памяти. При неправильном обращении использование делегатов может привести к сохранению циклов и утечкам памяти. Чтобы избежать этих проблем, важно правильно управлять ссылочными отношениями между объектами.
- Ограничено общением один на один. Делегаты обычно упрощают общение между объектами один на один. Если вам нужно реализовать связь «один ко многим» или «многие ко многим», могут потребоваться дополнительные шаблоны или методы.
Заключение
Делегаты распространены в разработке приложений для iOS, а когда дело доходит до UIKit, они повсюду. Как и любой шаблон проектирования, они имеют свои преимущества и недостатки.
Если вы заметили какие-либо ошибки, я был бы признателен, если бы вы могли оставить их в комментариях (кстати, английский не мой родной язык 😬). Я надеюсь, что эта статья была полезной, и если вы можете поддержать меня, чтобы я продолжал писать эти статьи.