KeyPath List Observing - kirseia/study GitHub Wiki

KeyPath Observing 하기

KVO 간단히 등록하기

  • 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를 리스트로 만들어서 KVO 하기 (1)

간단히 생각하면 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 타입을 가지고 있는지 알 수 없게 되어서 사용할 수가 없게 된다. (참고)

해결 방법 - Casting 하기
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 해서 구현해주면 해결. 간단하지만 복잡함-_-);

KeyPath를 리스트로 만들어서 KVO 하기 (2) - Protocol로 만들어서 간단히 사용하기

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

간단히 이런식으로 만들면 좀 더 쉽게 사용할 수 있긴 하다. 쉬운건지 모르겠지만 -_-);;;

전통적인 방식으로 KVO 하기

  • 위 코드를 공유했더니 팀 분이 다른 접근 방식을 알려주었음.
  • 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

Ref.

⚠️ **GitHub.com Fallback** ⚠️