Шаблон делегата в порядке. Но есть способ получше.

Вы создаете приложение для iOS. У вас есть красивая, ПРОЧНАЯ архитектура. У вас есть модель, сетевой уровень, слой пользовательского интерфейса и, возможно, несколько помощников между ними. Учебный способ передачи данных между этими уровнями - это делегирование, действительно полезный и распространенный шаблон в разработке для iOS.

Делегирование просто означает, что вы передаете себя кому-то еще, когда хотите, чтобы кто-то уведомил вас об изменениях, чтобы вы могли на них отреагировать. Например, если ViewController обращается к сетевой службе и хочет получить уведомление, когда эта служба выполняется с помощью некоторого запроса, он сделает себя делегатом сетевой службы. После этого сетевая служба вызовет методы делегата.

protocol NetworkServiceDelegate {
    func didCompleteRequest(result: String)
}
class NetworkService {
    var delegate: NetworkServiceDelegate?
    
    func fetchDataFromUrl(url: String) {
        API.request(.GET, url) { result in
            delegate?.didCompleteRequest(result)
        }
    }
}
class MyViewController: UIViewController, NetworkServiceDelegate {
    
    let networkService = NetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkService.delegate = self
    }
    func didCompleteRequest(result: String) {
        print("I got \(result) from the server!")
    }
}

Передача ссылок на делегаты - это нормально. В этом нет ничего функционально неправильного. Но есть способ получше, и я расскажу, почему он лучше.

Использование обратных вызовов для делегирования

Обратные вызовы аналогичны по функциям шаблону делегата. Они делают то же самое: сообщают другим объектам, когда что-то произошло, и передают данные.

Что отличает их от шаблона делегата, так это то, что вместо передачи ссылки на себя вы передаете функцию. Функции - это первоклассный продукт Swift, поэтому нет причин, по которым у вас не было бы свойства, которое является функцией!

class MyClass {
  var myFunction: (String)->() = { text in
    print(text)
  }
}

MyClass теперь имеет свойство myFunction, которое он может вызывать, и любой может устанавливать его (поскольку в Swift свойства по умолчанию являются внутренними). Это основная идея использования обратных вызовов вместо делегирования. Вот тот же пример, что и раньше, но с обратными вызовами вместо делегата:

class NetworkService {
    
    var onComplete: ((result: String)->())? //an optional function
    
    func fetchDataFromUrl(url: String) {
        API.request(.GET, url) { result in
            onComplete?(result: result) 
//                    ^ optional unwrapping for functions!
        }
    }
}
class MyViewController: UIViewController {
    
    let networkService = NetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkService.onComplete = { result in
            print("I got \(result) from the server!")
        }
    }
}

Еще один отличный способ использовать обратные вызовы - это когда вы хотите, чтобы данные были изменены. Вы можете сделать это, вызвав обратный вызов в обозревателе свойств:

var onUsernamesChanged: ([String]->())?
    
var loadedUsernames = [String]() {
    didSet {
        onUsernamesChanged?(loadedUsernames)
    }
}

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

Так почему обратные вызовы лучше?

1. Развязка

Делегаты приводят к довольно разобщенному коду. Для NetworkService не имеет значения, кто является его делегатом, если они реализуют протокол. Однако делегат должен реализовать протокол, и если вы используете Swift вместо протоколов @objc, делегат должен реализовать каждый метод в протоколе. (поскольку нет необязательного соответствия протоколу)

С другой стороны, при использовании обратных вызовов NetworkService даже не нужно иметь объект-делегат для вызова методов, и он не знает ничего о том, кто реализует эти методы. . Все, что его волнует, - это когда вызывать эти методы. Кроме того, не все методы необходимо реализовывать.

2. Множественное делегирование

Что, если вы хотите уведомить ViewController о завершении запроса, но, возможно, также какой-то класс регистратора и какой-то класс аналитики.

С делегатами вам понадобится массив делегатов или три разных свойства делегатов, которые могут даже иметь разные протоколы! (Я первым признаю, что сделал это)

Однако с помощью обратных вызовов вы можете определить массив функций (я люблю Swift) и вызывать каждую из них, когда что-то будет сделано. Нет необходимости иметь кучу различных объектов и протоколов, рискующих сохранить циклы и писать шаблонный код.

3. Более четкое разделение проблем

Я вижу разницу между делегатами и обратными вызовами в том, что с делегатами NetworkService сообщает делегату: «Эй, я изменился». С обратными вызовами делегат наблюдает за NetworkService.

На самом деле разница минимальна, но последний способ помогает предотвратить антишаблоны, часто встречающиеся при делегировании, например создание результатов преобразования NetworkService для представления, что не должно быть его работой!

4. Проще тестирование!

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

С обратными вызовами вам не только не нужно имитировать какие-либо делегаты, но и можно использовать любой обратный вызов, который вы хотите в каждом тесте!

В одном тесте вы можете проверить, вызывается ли обратный вызов, а в другом - проверить, был ли он вызван с правильными результатами. И ни один из них не требует сложного имитационного делегата с логическими значениями someFuncDidGetCalled и аналогичными свойствами.

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

Марин - разработчик iOS в COBE, блогер на marinbenc.com и студент компьютерных наук в FERIT, Осиек. Ему нравится программировать, узнавать о вещах, а потом писать о них, кататься на велосипедах и пить кофе. В основном, однако, он просто вызывает сбои SourceKit. У него есть пухлый кот по имени Амиго. Не он сам писал эту биографию.