Блог :. Свой Observable в Swift

Свой Observable в Swift

11 Dec, 2018

Существует довольно-таки распространенная  необходимость сигнализировать о каких-либо событиях от одного к нескольким объектам. В Swift это решается двумя способами из коробки:

  1. KVO, которое тянет за собой обязательство наследоваться от NSObject
  2. NotificationCenter, который до iOS 11 SDK хранил сильную ссылку на объект-наблюдатель, что приводило к утечке памяти, если вовремя не отписаться. Еще он неудобен тем, что необходимо отписываться явным образом от конкретной сигнатуры нотификации, чтобы случайно не отписать от других важных событий текущий объект, если использовать removeObserver(self)

Еще один годный вариант - сделать свой Observable, который будет:

  1. Работать на кложурах, без использования NSObject-based классов или @objc-методов
  2. Самостоятельно больше не трекать умерших наблюдателей и не слать нотификации мертвому объекту

Идея не нова, в С++ уже давно используется boost::signal, примерно похожее и удобное будем делать на Swift


Для начала нужно научиться хранить кложуру, чтобы впоследствии можно было бы ее вызвать. Создадим некий "держатель кложуры", шаблонным параметром которого будут являтся типы параметров вызова аргументов. Забегая вперед, держатель кложуры будет сообщать о своем уничтожении к Observable, чтобы тот почистил список наблюдателей

protocol ClosureHolder { }
typealias ClosureHolders = [ClosureHolder]
class ClosureHolderImpl<Params>: ClosureHolder {
typealias Callback = (Params) -> Void
private (set) var call: Callback
private var parent: Observable<Params>!
init(_ handler: @escaping Callback, _ observable: Observable<Params>) {
call = handler
parent = observable
}
deinit {
parent.clearObsolete()
}
}

В самом Observable будет храниться список кложур, завернутых в ClosureHolder. Но не все так просто, список держателей кложур будет держать сильную ссылку на держателя кложуры, отсюда она никогда самостоятельно не уничтожится. Для этого нужно научиться складывать объекты в массив, при этом не увеличивая счетчик ссылок на этот объект. Еще один враппер:

class Weak<T: AnyObject> {
private weak var value: T!
init(_ value: T) {
self.value = value
}
func get() -> T! {
return value
}
}
view raw Weak.swift hosted with ❤ by GitHub

Теперь осталось сделать сам Observable, который будет добавлять, очищать и вызывать кложуры:

class Observable<Params> {
private var subscribers = [Weak<ClosureHolderImpl<Params>>]()
static func += (this: Observable, closure: @escaping (Params) -> Void) -> ClosureHolder {
let holder = ClosureHolderImpl<Params>(closure, this)
let weakHolder = Weak<ClosureHolderImpl<Params>>(holder)
this.subscribers.append(weakHolder)
return holder
}
func clearObsolete() {
subscribers = subscribers.filter { holder in holder.get() != nil }
}
func notify(_ param: Params) {
subscribers.forEach { weakHolder in
weakHolder.get()?.call(param)
}
}
}

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

Как вообще этим пользоваться:

class MyObject {
let myEvent = Observable<String>()
func performChanges(with param: String) {
myEvent.notify(param)
}
}
class Observer {
var holder: ClosureHolder!
func subscribe(on obj: MyObject) {
holder = obj.myEvent += { [weak self] param in
self?.handler(param)
}
}
func unsubscribe() {
holder = nil
}
func handler(_ name: String) {
print("handle event \(name)")
}
}
var obj = MyObject()
var observer: Observer! = Observer()
observer.subscribe(on: obj)
obj.performChanges(with: "Test 1")
observer.unsubscribe()
obj.performChanges(with: "Test 2") // no output
observer.subscribe(on: obj)
obj.performChanges(with: "Test 3")
observer = nil
obj.performChanges(with: "Test 4") // no output

Как видим, кейсы с Test 2 и Test 4 выведены не будут, т.к. наблюдатель был отписан от нотификаций. Полный кусок кода лежит на гитхабе.

Прощай, NotificationCenter. Ну почти