Свой Observable в Swift
Существует довольно-таки распространенная необходимость сигнализировать о каких-либо событиях от одного к нескольким объектам. В Swift это решается двумя способами из коробки:
KVO
, которое тянет за собой обязательство наследоваться отNSObject
NotificationCenter
, который до iOS 11 SDK хранил сильную ссылку на объект-наблюдатель, что приводило к утечке памяти, если вовремя не отписаться. Еще он неудобен тем, что необходимо отписываться явным образом от конкретной сигнатуры нотификации, чтобы случайно не отписать от других важных событий текущий объект, если использоватьremoveObserver(self)
Еще один годный вариант - сделать свой Observable
, который будет:
- Работать на кложурах, без использования
NSObject
-based классов или@objc
-методов - Самостоятельно больше не трекать умерших наблюдателей и не слать нотификации мертвому объекту
Идея не нова, в С++ уже давно используется 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 | |
} | |
} |
Теперь осталось сделать сам 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
. Ну почти