KeyPath List Observing - kirseia/study GitHub Wiki
- KVO 하려면 NSObject 여야하고 (swift 스럽지 않긴 하지만), property 도 @objc dynamic var 여야 한다.
- 간단히 사용하려면 아래와 같이 하면 됨
class ABC: NSObject {
@objc dynamic var a: Int = 0
@objc dynamic var b: String = ""
}
let abc = ABC()
var observeList: [NSKeyValueObservation] = []
let options: NSKeyValueObservingOptions = [.new, .old]
observeList.append(abc.observe(\ABC.a, options: options, changeHandler: { [weak self] (_, change) in
print(change)
self?.updateModel()
}))
observeList.append(abc.observe(\ABC.b, options: options, changeHandler: { [weak self] (_, change) in
print(change)
self?.updateModel()
}))
abc.a = 5
abc.b = "hello"
이렇게 하면 되는데, KVO 할 Property 가 많은데 처리해야할 함수가 같을 경우 타이핑 하기 번거로워진다.
간단히 생각하면 KeyPath를 리스트로 만들어서 loop를 돌면 될 것 같다.
let keyPathList = [\ABC.a, \ABC.b] // PartialKeyPath<ABC>
let keyPath = \ABC.a // ReferenceWritableKeyPath<ABC, Int>
실제로 KeyPath 리스트를 만들어보면 위와 같이 된다. Type이 바뀌어버린다.
왜냐하면 \ABC.a 와 \ABC.b 가 서로 다른 타입이라서 PartialKeyPath가 되고,
이 경우 각 keyPath가 어떤 Value 타입을 가지고 있는지 알 수 없게 되어서 사용할 수가 없게 된다. (참고)
for keyPath in keyPaths {
switch keyPath {
case let intPath as WritableKeyPath<ABC, Int>:
observeList.append(abc.observe(intPath, options: [.new, .old]) { (model, change) in
print(change)
})
case let stringPath as WritableKeyPath<ABC, String>:
observeList.append(abc.observe(stringPath, options: [.new, .old]) { (model, change) in
print(change)
})
default:
preconditionFailure("must be implement unknown type")
}
}
이런식으로 각 타입을 하나하나 casting 해서 구현해주면 해결. 간단하지만 복잡함-_-);
protocol ObservableProtocol: NSObject {
}
extension ObservableProtocol {
func addObserve(keyPaths: [PartialKeyPath<Self>]) -> [NSKeyValueObservation?] {
let options: NSKeyValueObservingOptions = [.new, .old]
var observations: [NSKeyValueObservation?] = []
for keyPath in keyPaths {
switch keyPath {
// always fails 할거라고 warning 이 뜨는데 동작은 잘 됨.
case let intPath as WritableKeyPath<Self, Int>:
observations.append(observe(intPath, options: options) { (_, change) in
print(change)
})
break
default:
assertionFailure("must be implement unknown type")
observations.append(nil)
}
}
return observations
}
}
class JKL: NSObject, ObservableProtocol {
@objc dynamic var a:Int = 0
}
let jkl = JKL()
jkl.addObserve(keyPaths: [\JKL.a])
jkl.a = 3
간단히 이런식으로 만들면 좀 더 쉽게 사용할 수 있긴 하다. 쉬운건지 모르겠지만 -_-);;;
- 위 코드를 공유했더니 팀 분이 다른 접근 방식을 알려주었음.
- Swift 4 이전까지 KeyPath는 #keyPath(ABC.d) 이런식으로 생성하고 있었고, 이 값은 String 이다.
- String 이면 좀 더 쉽게 List 로 관리가 가능하다.
class ObserveItem<Value>: NSObject {
private weak var item: NSObject?
private var keyPath: String
private var changeHandler: (_ old:Value, _ new:Value) -> Void
init(item: NSObject, keyPath: String, changeHandler: @escaping (_ old:Value, _ new:Value) -> Void) {
self.item = item
self.keyPath = keyPath
self.changeHandler = changeHandler
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == self.keyPath {
changeHandler(change?[.oldKey] as! Value, change?[.newKey] as! Value)
}
}
func invalidate() {
if let item = item {
item.removeObserver(self, forKeyPath: keyPath)
self.item = nil
}
}
}
extension NSObject {
func add<Value>(keyPath: String, changeHandler: @escaping (_ old:Value, _ new:Value) -> Void) -> ObserveItem<Value> {
let observeItem = ObserveItem(item: self, keyPath: keyPath, changeHandler: changeHandler)
self.addObserver(observeItem, forKeyPath: keyPath, options: [.new, .old], context: nil)
return observeItem
}
}
class TestClass: NSObject {
@objc dynamic var a: Int = 0
}
let testClass = TestClass()
let obs = testClass.add(keyPath: #keyPath(TestClass.a)) { (_ old: Int, _ new: Int) in
print("change : \(new), \(old)")
}
testClass.a = 10
obs.invalidate()
testClass.a = 15