[iOS] - KVO


KVO를 깊게 파보자.


KVO 패턴이란? : 한 객체의 프로퍼티가 변했음을 다른 객체에게 알리는 Cocoa 프로그래밍 패턴. 관찰하고자 하는 프로퍼티의 key path를 사용해 관찰할 수 있다.

KVO

KVO는 Key-Value Observing의 약자다. Key-Value observing은 한 객체에게 다른 객체의 프로퍼티가 변했음을 알리는 Cocoa 프로그래밍 패턴이다.

변화를 앱에서 논리적인 부분들이 서로 알리는데 유용하다. 논리적으로 분리되어 있는 부분들은 model과 view와 같은 것들이 속할 수 있다.

image

Key-value observing은 NSObject을 상속받은 클래스에서만 사용할 수 있다.

NSObject? NSObject는 대부분의 Objective-C 클래스 계층의 root 클래스다. NSObject 클래스를 상속받은 자식 클래스들은 런타임 시스템의 기본 인터페이스와 Objective-C 객체로서 동작할 수 있는 능력을 상속받는다.

KVO를 사용할 프로퍼티 표시하기

KVO는 어떤 객체의 프로퍼티가 변했음을 다른 객체에 알리는 프로그래밍 패턴이라고 했다. 이 프로퍼티가 변했음을 알기 위해서는 프로퍼티를 관찰해야 한다.

KVO로 관찰하고 싶은 프로퍼티를 @objc attribute와 dynamic modifier를 함께 붙여서 표시한다.

class MyObservableObject: NSObject {
    // 관찰할 프로퍼티
    @objc dynamic var level = 1
    
    func levelUp() {
        level += 1
    }
}

위 코드에서는 MyObservableObject 클래스를 정의했고, 이 클래스는 level이라는 관찰할 수 있는 프로퍼티를 가지고 있다.

Observer 정의하기

위에서 변화가 일어나는지 관찰할 프로퍼티를 정했다. 이제 이 프로퍼티가 변했음을 알림받을 관찰자, observer가 필요하다.

observer 클래스의 인스턴스는 하나 이상의 프로퍼티에서 생긴 변화에 대한 정보를 관리한다. Observer를 생성할 때, 내가 관찰하고 싶은 프로퍼티를 나타내는 key path로 observe(_:options:changeHandler:) 메서드를 호출해서 관찰을 시작한다.

class MyObserver: NSObject {
    @objc var observableObject: MyObservableObject
    var observation: NSKeyValueObservation?
    
    init(object: MyObservableObject) {
        self.observableObject = object
        super.init()
        
        // observe(_:options:changeHandler:)는 self.observe~ 로 호출하는 메서드라 이 전에 super.init()을 호출해야 한다.
        self.observation = observe(
            \.observableObject.level,
             options: [.old, .new],
             changeHandler: { object, change in
                 print("level was : \(change.oldValue!), level is now : \(change.newValue!)")
        })
    }
}

MYObserver 클래스에서는 관찰할 객체 observableObject를 프로퍼티로 가지고 있다. 그리고 observe(_:options:changeHandler:) 메서드에서 관찰할 객체의 level 프로퍼티가 변할 때, 변하기 이전의 값과 변하고 난 값을 모두 변한 내용을 담는 딕셔너리에 담을 수 있게 설정했다.

changeHandler에서는 changeoldValuenewValue 프로퍼티를 사용하고 있다. change는 NSKeyValueObservedChange 타입으로, indexes, isPrior, kind, newValue, oldValue 와 같은 프로퍼티를 가지고 있어서 내가 관찰하는 프로퍼티가 어떻게 변했는지 확인할 수 있게 해준다.

만약 프로퍼티가 어떻게 변했는지 알 필요가 없다면 observe(_:options:changeHandler:)options 파라미터를 빼도 된다. 이 options를 빼면 프로퍼티의 변하기 이전 값과 변하고 난 값을 저장하지 않게 되기 때문에 changeoldValuenewValuenil이 된다.

참고

Observer 클래스도 NSObject를 상속해야 한다는 것을 잊지 말자.

Observer와 관찰할 프로퍼티 연결하기

우리는 위에서

  1. 관찰할 프로퍼티를 설정하고
  2. 프로퍼티를 관찰하는 Observer 클래스를 생성했다.

따라서 우리가 구현한 observer 클래스의 이니셜라이저에 관찰할 객체를 전달해서 관찰할 프로퍼티와 observer를 연결하면 된다.

let levelObject = MyObservableObject()
let observer = MyObserver(object: levelObject)

프로퍼티가 변했을 때

KVO를 사용하는 객체들(위에서는 LevelObject)은 프로퍼티의 값이 변할 때 observer에 변화를 알린다.

levelObject.levelUp()
// 자동으로 observer의 change handler가 호출된다.
// level was : 1, level is now : 2

image

KVO와 Struct, Enum

위에서 설정한 변화를 관찰할 수 있는 프로퍼티는 클래스(objective-C 타입)이어야 하기 때문에, 구조체, 열거형은 KVO로 관찰할 수 없다.

전체 코드

import Foundation

class MyObservableObject: NSObject {
    // 관찰할 프로퍼티
    @objc dynamic var level = 1
    
    func levelUp() {
        level += 1
    }
}


class MyObserver: NSObject {
    @objc var observableObject: MyObservableObject
    var observation: NSKeyValueObservation?
    
    init(object: MyObservableObject) {
        self.observableObject = object
        super.init()
        
        // observe(_:options:changeHandler:)는 self.observe~ 로 호출하는 메서드라 이 전에 super.init()을 호출해야 한다.
        self.observation = observe(
            \.observableObject.level,
             options: [.old, .new],
             changeHandler: { object, change in
                 print("level was : \(change.oldValue!), level is now : \(change.newValue!)")
        })
    }
}

let levelObject = MyObservableObject()
let observer = MyObserver(object: levelObject)

levelObject.levelUp()
// 자동으로 observer의 change handler가 호출된다.
// level was : 1, level is now : 2

  • 참조
  • https://developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift