2017년 7월 25일 화요일

KVO (Key-Value Observing) 소개

KVO 는 Key-Value Observing 의 약자, 즉 특정 키의 값의 변화를 감지하기 위한 기능이다. Objective-C 를 위해 만들어진 기능이라 등장한지는 제법 되었지만, 현재의 앱 개발 패러다임에 있어서 - 모델(Model)의 변화를 뷰(View)에 반영하기 위함 등 - 값 변화를 인식하는 것은 굉장히 중요하기 때문에 무시할 수는 없는 기능인 것 같다.

KVO를 이용하기 위해서는 NSObject 에 구현된 메소드를 이용해야 한다. 그래서 필연적으로 NSObject 를 상속받아야 하고 그래서 클래스(Class)에서만 사용이 가능하다는 점을 유의하자.

키 패스

KVO 를 사용하기 위해서는 Key Path 라는 개념을 알아야 한다. 생각보다 단순한데, 그냥 멤버(프로퍼티나 메소드 등)를 특정 문자열로 표기하기 규칙이다.

예를 들어 키패스에 "value" 라는 문자열을 넘겨주면 특정 클래스 오브젝트의 value 라는 프로퍼티를 의미하는 것으로 이해하면 된다.

관련 메소드

옵저버를 추가하기 위해 아래 메소드가 제공된다.
- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(void *)context;
이름 처럼 관찰자(Observer)를 추가하기 위한 메소드이다:
  • observer: 변화를 알려줄 클래스 오브젝트
  • keyPath: 변화를 감지할 프로퍼티 키 패스
  • options: 여러 옵션이 있는데 보통은 new 혹은 old 혹은 이 둘 다를 사용하는 편이다. 상세한 것은 레퍼런스 매뉴얼을 참고하자.
  • context: 컨텍스트 데이터를 받기 위해 포인터를 넘겨야 한다. 이 값은 특정 경우를 빼곤 신경 쓸 필요는 없다. 예를 들어 특정 옵저버를 삭제하는 경우 등에는 활용될 수도 있다.
옵저버의 응답을 받기 위해서는 NSObject 의 아래 메소드를 오버라이드 해야한다.
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change 
                       context:(void *)context;
observeValue 라는 이름처럼 만약 등록한 키패스의 값이 바뀌게 되면 이 메소드가 호출된다:
  • keyPath: 이름 처럼 키패스이다. 어떤 키패스의 값이 변경되었는지 간단히 알 수 있다.
  • object: 말 그대로 키패스가 어디 오브젝트의 것인지를 의미한다. id 즉 'void *' 타입이기 때문에 맞는 타입으로 형변환 해서 참조해야 할 것이다.
  • change: 관련된 키패스와 값들을 사전형으로 돌려주는데 개인적으론 써보질 않아서 잘 모르겠다.
  • context: 컨텍스트. 그냐 무시해도 쓰는데 큰 문제는 없었다.

예제

아래 예제는 특정 뷰 컨트롤러에서 서브뷰를 하나 만들고 이 서브뷰의 frame, 즉 뷰의 위치와 크기를 추적하기 위한 코드이다.
@interface ViewController () {
    UInt8 _context;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIView *someView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 50, 50)];
    [self.view addSubview:someView];
    
    [someView addObserver:self
               forKeyPath:@"frame"
                  options:NSKeyValueObservingOptionNew
                  context:&_context];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context
{
    NSLog(@"keyPath %@ updated", keyPath);
}

@end
옵저버를 등록하고 옵저버의 응답을 받는 메소드 까지 오버라이드 해 둔 상태이다. 이제 아래처럼
someView.frame = CGRectMake(100, 100, 50, 50);
해당 서브뷰의 프레임 값을 바꾸면 observerValueForKeyPath:ofObject:change:context 메소드가 호출된다.

예제코드에서는 그냥 키패스의 값만 로그를 찍도록 해 둔 상태이다. 즉 필요하다면 이 부분에 뷰 데이터를 업데이트 해 주는 루틴이 들어가면 딱 어울릴 만 하다.

Swift 의 경우

Swift 3.x 까지의 기준으로 볼 때 Objective-C 로 쓰는 것과 사실 별 차이는 없다. 그냥 메소드 모양이 Swift 에 맞게 번역만 되어 있을 뿐이다. 아래 예제는 위 예저와 비슷한데 뷰 컨트롤러 기반이 아니라 그냥 특정 아무 클래스(?)로 코딩했음을 유의하자.
class TestClass: NSObject {
  private var context: UInt8 = 0
  lazy var someView: UIView = {
    return UIView(frame: CGRect(x: 20, y: 20, width: 50, height: 50))
  }()
  
  override init() {
    super.init()
    
    someView.addObserver(self, forKeyPath: "frame", options: [.new, .old], context: &context)
  }
  
  override func observeValue(forKeyPath keyPath: String?, 
                             of object: Any?, 
                             change: [NSKeyValueChangeKey : Any]?, 
                             context: UnsafeMutableRawPointer?) {
    if let keyPath = keyPath {
      print("\(keyPath) updated")
    }
  }
}
addObserver 와 observeValue 의 모양을 보면 거의 똑같다는 것을 알 수 있다.

마무리

개인적으로 KVO는 Swift 랑 잘 어울리지 않는 것 같다.

위 예제들 전부 NSObject 에 구현된 특수한 기능들을 이용하는 터라 무거운(?) NSObject 를 상속받아야 한다. 그리고 Swift 에서는 didSet 이나 willSet 같은 프로퍼티 변화에 대응하는 다른 방법이 있다. 그래서 개인적으로 KVO 는 순수한 스위프트 자체 기능이 아니기 때문에 좀 껄끄럽다고 느껴진다.

그런데 Swift 4 들어서는 약간 더 친화적으로 바뀔 예정이다. 이 부분은 다음 글에서 다룰 예정이다.​

[관련글] Swift 4 에서 KVO 사용해보기
[관련글] 스위프트(Swift) 가이드
[관련글] Core Foundation Resources

댓글 없음 :