В процессе разработки приложений часто требуется изменять данные на одном экране на основе взаимодействия с другим или устанавливать связь между разными экранами. Для эффективного решения этих сценариев шаблон делегирования становится ценным инструментом в наборе инструментов 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.

Преимущества и недостатки использования делегатов

Преимущества

  1. Слабая связь. Делегаты помогают достичь слабой связи между объектами. Разделяя обязанности и определяя протоколы, делегаты позволяют объектам взаимодействовать, не зная друг друга напрямую.
  2. Модульный и расширяемый. Делегаты продвигают модульный и расширяемый код, обеспечивая четкое разделение задач. Различные объекты могут действовать как делегаты, что обеспечивает гибкую настройку и расширение поведения.
  3. Повторное использование кода. Делегаты облегчают повторное использование кода. Поскольку делегаты могут быть реализованы несколькими объектами, одна и та же реализация делегата может использоваться в разных контекстах или сценариях.
  4. Механизм обратного вызова. Делегаты включают механизмы обратного вызова, когда один объект уведомляет другой объект об определенных событиях или действиях. Это полезно для реализации архитектур, управляемых событиями, или для обработки асинхронных операций.

Недостатки

  1. Сложное управление потоком. По мере увеличения числа делегатов и протоколов поток управления может усложняться. Может потребоваться тщательное управление и координация, чтобы обеспечить назначение и вызов правильных делегатов в нужное время.
  2. Возможность утечек памяти. При неправильном обращении использование делегатов может привести к сохранению циклов и утечкам памяти. Чтобы избежать этих проблем, важно правильно управлять ссылочными отношениями между объектами.
  3. Ограничено общением один на один. Делегаты обычно упрощают общение между объектами один на один. Если вам нужно реализовать связь «один ко многим» или «многие ко многим», могут потребоваться дополнительные шаблоны или методы.

Заключение

Делегаты распространены в разработке приложений для iOS, а когда дело доходит до UIKit, они повсюду. Как и любой шаблон проектирования, они имеют свои преимущества и недостатки.

Если вы заметили какие-либо ошибки, я был бы признателен, если бы вы могли оставить их в комментариях (кстати, английский не мой родной язык 😬). Я надеюсь, что эта статья была полезной, и если вы можете поддержать меня, чтобы я продолжал писать эти статьи.