Как реализовать делегаты со слабой ссылкой

17 апреля 2018

Представим типичный пример из жизни. У вас есть ViewController со множеством представлений, и вы хотите делегировать логику действий из View в ViewController.

У вас есть представление с кнопкой, после нажатия на которую вы хотите составить и отправить запрос к API сервера в самом ViewController для того, чтобы понять, какие действия нужно сделать дальше.

Ваша ситуация выглядит примерно следующим образом:

import UIKit

// Протокол для делегирования нажатия кнопки в ViewController
protocol ButtonDelegate {
    func onButtonTap(sender: UIButton)
}

class ViewWithTextAndButton: UIView{
    
    // Объявляем делегат для использования
    var delegate:ButtonDelegate?
    
    func onButtonTap(sender: UIButton) {
        // Вызываем делегат в тот момент, когда кнопка нажата
        delegate?.onButtonTap(sender: sender)
    }
}

class MyViewController: UIViewController, ButtonDelegate{
    
    let viewWithTextAndButton = ViewWithTextAndButton(frame:CGRect(x:0, y:0, width:100, height:100))
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Присваиваем делегат
        viewWithTextAndButton.delegate = self
        view.addSubview(viewWithTextAndButton)
    }
    
    // MARK: ButtonDelegate
    // Реализация делегируемой логики здесь
    func onButtonTap(sender: UIButton) {
        print("This button was clicked in the subview!")
    }
}

Вы наверняка заметили, что здесь есть одна большая проблема! С того времени, как View держит сильную ссылку на ViewController в роли делегата, получается, ViewController держит сильную ссылку на View, а данная ситуация приводит к зацикливанию. ViewController ссылается на View, а тот ссылается на ViewController. Данные обстоятельства показывают нам то, что ни один из этих классов не будет иметь ноль ссылок на себя, и это означает что эти объекты не будут высвобождены из памяти.

Решение подразумевает то, что сильную ссылку на один из объектов мы преобразуем в слабую. Но как это реализуется в Swift? К протоколу необходимо применить такие ограничения, чтобы протокол мог использоваться только на ссылочных типах. Данный эффект достигается с помощью ключевого слова class:

// Теперь данный протокол может использоваться только классами!
protocol ButtonDelegate: class {
    func onButtonTap(sender: UIButton)
}
class ViewWithTextAndButton: UIView {
    
    // Заметьте, сейчас мы можем использовать ключевое слово weak!
    // этот делегат теперь ссылается только на ссылочный тип (UIViewController)
    // и этот класс будет иметь слабую ссылку
    
    weak var delegate: ButtonDelegate?
    func onButtonTap (sender: UIButton) {
        delegate?.onButtonTap(sender)
    }
}

И вот теперь вы наблюдаете делегат со слабой ссылкой.
Это был еще один пример того, как вы можете обойти проблему утечки памяти. При типах значений, значения копируются, поэтому утечки из примера выше не возникают. Работая с фреймворком Cocoa и с унаследованными классами, подобными UIView или UIViewController, нам помогут протоколы, с примением только для классов.

Ссылка на оригинал статьи.
Перевел статью: Дмитрий Петухов (mail@dphov.com)

Содержание