Swifty하게 KVO 사용하기

이번 ‘네이버 핵데이 2018’에서 멘토님께 배우게 된 Swift스럽게 KVO를 사용하는 방법을 정리하려 한다.

기존의 방법

기존의 방법은 감지하고 싶은 객체에 addObserver(_:forKeyPath:options:context:) 메서드를 통해 옵저버를 달고 변경될 때 이벤트를 받는 객체에 observeValueForKeyPath:ofObject:change:context:를 통해 이벤트를 감지하고 그에 따른 처리를 해준다.

이 방법은 Objective-C부터 사용된 것으로 아직 업데이트되지 않은 애플의 프로그래밍 가이드에서는 지금도 이 방법을 사용하고 있다. 그러나 왠지 안전하지 않은 듯한 UnsafeMutablePointer의 타입의 context의 포인터를 인자로 갖는다는 점에서 Swift를 사용하는 지금 시점에서는 익숙한 모습이 아니다.

더 자세한 이야기는 블로그 내의 글인 Key-Value Observing를 참고하면 좋다.

새로운 방법

Swift는 observe(_:options:changeHandler) 메서드를 통해 KVO를 할 수 있다. 이 메서드의 장점은 기존의 방법과 달리 옵저버의 등록과 그에 따른 처리를 한 번에 할 수 있다는 것이다. 이는 한 번 더 타고 들어가야 하는 기존의 방법보다 가독성을 크게 늘렸다고 볼 수 있다. 또한 굳이 필요하지 않은 context를 사용하지 않는다.

아래는 예제 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1.
private var playerItemObserverList: [NSKeyValueObservation] = []
...
// 2.
let playerItemObserver = playerItem.observe(\.status) { [weak self] (playerItem, _) in
// 3.
guard let strongSelf = self else { return }

strongSelf.playerItemStatus = playerItem.status

switch playerItem.status {
case .readyToPlay:
guard strongSelf.isFirstPlaying else { return }
strongSelf.player.currentItem?.preferredPeakBitRate = 0
strongSelf.player.play()

strongSelf.delegate?.isPlayedFirst()
strongSelf.isFirstPlaying = false
case .failed:
assertionFailure("PlayerItem failed.")
case .unknown:
assertionFailure("PlayerItem is not yet ready.")
}
}
// 4.
playerItemObserverList.append(playerItemObserver)
...
// 5.
playerItemObserverList.removeAll()

과정 별로 살펴보면,

1. 옵저버를 보관하는 배열을 만들어 둔다.

observe(_:options:changeHandler) 함수의 리턴 값은 NSKeyValueObservation로 일종의 옵저버이다. 이것을 보관하는 배열을 이벤트를 감지하는 객체가 레퍼런스로 잡고 있고 객체가 소멸(deinit)될 시에 배열에서 제거함으로써 옵저버를 해제할 수 있다. 기존의 방법에서 removeObserver:forKeyPath:context: 메서드를 통해 제거하는 것보다 훨씬 간단하다.

2. 감지할 객체에 옵저버를 달아둔다.

감지할 프로퍼티의 keyPath는 아래와 같은 방식으로 특정한다.

1
\타입이름.경로.경로.경로

키 경로 타입은 AnyKeyPath 클래스로부터 파생되어 확장된 WritableKeyPath<Root, Value>ReferenceWritableKeyPath<Root, Value> 타입이 쓰이며 각각 값 타입, 참조 타입에 적용된다.

감지할 시에 options 인자는 NSKeyValueObservingOptions 구조체 타입으로 new, old, initial, prior을 프로퍼티로 갖고 있다. OptionSet 프로토콜을 준수하기 때문에 언제 이벤트를 받을지 여러 개를 기입하면 된다.

3. 받은 이벤트의 처리를 클로저를 통해 수행한다.

감지한 값에 따라 적절한 처리를 해준다.

4. 옵저버를 전에 선언한 배열에 넣어준다.

옵저버를 유지하는 역할을 한다.

5. 옵저버를 deinit 시에 제거해준다.

메모리 낭비 방지를 위해 deinit 시에 옵저버를 제거한다.

Reference