Chapter 12: Key-Value Observing
Version
Swift 5.3, iOS 14, Xcode 12
변경(change)에 대처(dealing with)하는 것은 Combine의 핵심(core)이다. Publishers를 구독(subscribe)하면, 비동기(asynchronous) 이벤트(events)를 처리(handle)할 수 있다. 이전(earlier) 장(chapters)에서는 publisher가 새 값(value)을 내보낼(emits) 때마다 지정된 객체(object)의 속성(property) 값(value)을 업데이트(update)할 수 있는 assign(to:on:)
에 대해 배웠다.
그러나 단일 변수(variables)의 변경(changes)을 관찰(observe)하는 메커니즘(mechanism)을 구현해야 할 때도 있다.
Combine에는 이 문제에 대한 몇 가지 해결 방법이 있다:
- KVO(Key-Value Observing)를 준수(compliant)하는 객체(object)의 모든(any) 속성(property)에 대한 publisher를 제공한다.
ObservableObject
프로토콜(protocol)은 여러(multiple) 변수(variables)가 변경(change)될 수 있는 경우를 처리한다.
Introducing publisher(for:options:)
KVO는 항상 Objective-C의 필수적인(essential) 구성 요소(component)였다. Foundation, UIKit, AppKit 클래스(classes)의 많은 속성(properties)은 KVO를 준수(compliant)한다. 따라서 KVO를 사용하여 변경 사항(changes)을 관찰(observe)할 수 있다.
KVO를 준수(compliant)하는 속성(properties)은 쉽게 관찰(observe)할 수 있다. 다음은 Foundation의 클래스(class)인 OperationQueue
를 사용하는 예제이다:
let queue = OperationQueue()
let subscription = queue.publisher(for: \.operationCount)
.sink {
print("Outstanding operations in queue: \($0)")
}
대기열(queue)에 새 작업(operation)을 추가 할 때마다, 해당 operationCount
가 증가하고 sink에서 새로운 개수(count)를 수신(receives)한다. 대기열(queue)이 작업(operation)을 마치면(consumed), 개수(count)가 감소(decrements)하고 다시 sink가 업데이트(updated)된 개수(count)를 받는다(receives).
다른 프레임 워크(framework)에도 KVO를 준수(compliant)하는 속성(properties)을 노출(exposing)하는 클래스(classes)가 많이 있다. 관찰할 속성(property)에 대한 키 경로(key path)와 함께 publisher(for:)
를 사용하면, 값(value)의 변화(changes)를 방출(emitting)할 수 있는 publisher를 얻게 된다. 이 장(chapter)의 뒷부분에서 이 방법과 사용 가능한 옵션(options)에 대해 자세히 알아본다.
Note: Apple은 프레임 워크(frameworks) 전체(throughout)에 대해 KVO를 준수(compliant)하는 속성(properties)의 전체 목록(central list)을 제공하지는 않는다. 각 클래스(class)에 대한 문서(documentation)는 일반적으로 KVO를 준수하는 속성(properties)을 나타낸다. 그러나 때로는 문서(documentation)가 부실하게(sparse) 작성되었을 수도 있으며, 이 경우에는 일부 속성(properties)에 대한 간단한 설명(quick note)만 문서(documentation)에서 찾아 볼 수 있을 것이다.
Preparing and subscribing to your own KVO-compliant propertie
또한, 다음과 같은 경우에 Key-Value Observing을 사용할 수 있다:
- 객체(objects)는 구조체(structs)가 아닌 클래스(classes)이며,
NSObject
를 상속(inherit)한다. - 그 경우
@objc dynamic
특성(attributes)을 사용하여, 속성(properties)을 관찰 가능(observable)하게 표시한다.
이렇게 하면 표시한 객체(objects)와 속성(properties)이 KVO를 준수(compliant)하게 되며, Combine으로 관찰(observed)할 수 있다.
Note: Swift는 KVO를 직접 지원하지 않지만,@objc dynamic
속성(properties)을 표시하면 컴파일러(compiler)가 KVO를 실행(trigger)하는 숨은(hidden) 메서드(methods)를 생성(generate)한다. 이에 대한 설명은 이 책의 범위를 벗어난다(out of the scope). 이는NSObject
프로토콜(protocol)의 특정(specific) 메서드(methods)에 크게 의존하고 있으며, 객체(objects)가 이 프로토콜(protocol)을 상속(inherit)해야 하는 이유를 설명(explains)한다.
playground에서 예제를 작성한다:
class TestObject: NSObject { //NSObject 프로토콜을 상속하는 클래스 생성 //KVO에 필요하다.
@objc dynamic var integerProperty: Int = 0
//관찰할 속성을 @objc dynamic로 표시한다.
}
let obj = TestObject()
let subscription = obj.publisher(for: \.integerProperty)
//obj의 integerProperty 속성을 관찰하는 publisher를 생성하고 구독한다.
.sink {
print("integerProperty changes to \($0)")
}
obj.integerProperty = 100
obj.integerProperty = 200
//값을 업데이트 한다.
playground에서 이 코드를 실행(running)하면, 디버그(debug) 콘솔(console)에는 다음과 같은 출력이 표시된다:
integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 200
먼저 integerProperty
의 초기값(initial value)인 0
을 얻은 다음, 두 가지 변경 사항을 수신(receive)한다. 이 초기값(initial value)에 관심이 없다면 생략할 수 있다.
TestObject
에서 일반(plain) Swift 유형인 Int
를 사용하고 있지만, Objective-C 의 기능(feature)인 KVO가 여전히 작동하고 있다. KVO는 Objective-C 유형 및 Objective-C에 연결(bridged)된 모든 Swift 유형(type)에서 잘 작동한다. 여기에는 모든 기본 Swift 유형(types)과 배열(arrays) 및 사전(dictionaries)이 포함된다. 단, 이 해당 값(values)이 모두 Objective-C에 연결(bridgeable)된 경우여야 한다.
TestObject
에 몇 가지 속성을 더 추가한다:
@objc dynamic var stringProperty: String = ""
@objc dynamic var arrayProperty: [Float] = []
publishers에 대한 구독(subscriptions)도 추가한다:
let subscription2 = obj.publisher(for: \.stringProperty)
.sink {
print("stringProperty changes to \($0)")
}
let subscription3 = obj.publisher(for: \.arrayProperty)
.sink {
print("arrayProperty changes to \($0)")
}
마지막으로 일부 속성(property)을 다음과 같이 변경(changes)한다:
obj.stringProperty = "Hello"
obj.arrayProperty = [1.0]
obj.stringProperty = "World"
obj.arrayProperty = [1.0, 2.0]
디버그 영역에 초기값(initial values)과 변경 사항(changes)이 모두 나타나는 것을 볼 수 있다.
Objective-C에 연결(bridged)되지 않은 pure-Swift 유형(type)을 사용하면 문제가 발생할 수 있다:
struct PureSwift {
let a: (Int, Bool)
}
그런 다음 TestObject
에 속성(property)을 추가한다:
@objc dynamic var structProperty: PureSwift = .init(a: (0,false))
“Property cannot be marked @objc because its type cannot be represented in Objective-C.”는 오류(error)가 즉시(immediately) 나타난다. 여기서, Key-Value Observing의 한계(limits)가 나타난다.
Note: 시스템(system) 프레임 워크(frameworks) 객체(objects)의 변경 사항(changes)을 관찰(observing)할 때 주의해야 한다. 시스템(system) 객체(object)의 속성(property) 목록(list)을 보는 것만으로는 단서(clue)를 찾을 수 없기 때문에, 문서(documentation)에 해당 속성(property)이 관찰 가능(observable)하다고 언급되어 있는지 확인해야 한다. 이는 Foundation, UIKit, AppKit 등에 해당된다. 역사적으로 속성(properties)은 관찰 가능(observable)하도록 "KVO-aware"으로 만들어야 했다.
Observation options
변경 사항(changes)을 관찰(observe)하기 위해 호출(calling)하는 메서드(method)의 전체 시그니처(signature)는 publisher(for:options:)
이다. options
매개 변수(parameter)는 .initial
, .prior
, .old
, .new
4가지 값(values)이 있는 옵션(option) 집합(set)이다. 기본값(default)은 [.initial]
이며, publisher가 변경 사항(changes)을 내보내기(emit) 전에 초기값(initial value)을 내보낸다(emitting). 다른 옵션(options)에 대한 설명(breakdown)은 다음과 같다:
.initial
은 초기값(initial value)을 내보낸다(emits)..prior
은 변경(change)이 발생(occurs)하면, 이전(previous) 값과 새(new) 값을 모두 내보낸다(emits)..old
및.new
는 이 publisher에서 사용되지 않으며, 둘 다 아무 작업도 수행하지 않는다. 단지 새(new) 값만 전달(through)한다.
초기값(initial value)을 원하지 않는 경우, 간단히 다음과 같이 작성할 수 있다:
obj.publisher(for: \.stringProperty, options: []) //초기값 생략
.prior
을 지정하면 변경(change)이 발생(occurs)할 때마다, 두 개의 개별(separate) 값(values)을 얻게 된다. integerProperty
를 다음과 같이 변경한다:
let subscription = obj.publisher(for: \.integerProperty, options: [.prior])
이제 integerProperty
구독(subscription)에 대한 출력은 디버그(debug) 콘솔(console)에 다음과 같이 표시된다:
integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 100
integerProperty changes to 200
속성(property)은 먼저 0
에서 100
으로 변경되므로, 0
과 100
의 두 가지 값(values)을 얻게 된다. 그런 다음 100
에서 200
으로 변경(changes)되어, 다시 100
과 200
두 값(values)을 얻는다.
ObservableObject
Combine의 ObservableObject
프로토콜(protocol)은 NSObject
를 상속(deriving from)한 객체(objects)뿐 아니라, Swift 객체(objects)에서도 작동한다. @Published
속성(property) 래퍼(wrapper)와 짝을 이루어(teams up with), 컴파일러가 생성하는(compiler-generated) objectWillChange
publisher로 클래스(classes)를 만드는 데 도움을 준다.
많은 상용구(boilerplate)를 작성하지 않아도 되고, 속성(properties)을 자체 모니터링(self-monitor)하며, 변경(change)될 때 이를 알리는(notify) 객체(objects)를 만들 수 있다.
예제는 다음과 같다:
class MonitorObject: ObservableObject {
@Published var someProperty = false
@Published var someOtherProperty = ""
}
let object = MonitorObject()
let subscription = object.objectWillChange.sink {
print("object will change")
}
object.someProperty = true
object.someOtherProperty = "Hello world"
ObservableObject
프로토콜(protocol)을 준수(conformance)하면, 컴파일러(compiler)가 objectWillChange
속성(property)을 자동으로(automatically) 생성(generate)한다. 이는 Void
항목을 내보내고(emits), 실패하지 않는(Never
) ObservableObjectPublisher
이다(<Void, Never>).
객체(object)의 @Published
변수(variables) 중 하나가 변경(change)될 때마다 objectWillChange
가 실행(firing)된다. 안타깝게도 어떤 속성(property)이 실제로(actually) 변경(changed)되었는지는 알 수 없다. 이것은 화면 업데이트(updates)를 간소화(streamline)하기 위해 이벤트(events)를 통합(coalesces)하는 SwiftUI와 매우 잘 작동하도록 설계(designed)되었다.
Key points
- Key-Value Observing은 주로 Objective-C 런타임(runtime)과
NSObject
프로토콜(protocol)의 메서드(methods)에 의존(relies on)한다. - Apple 프레임 워크(frameworks)의 많은 Objective-C 클래스(classes)는 KVO를 준수(compliant)하는 속성(properties)을 제공한다.
NSObject
를 상속(inheriting)하는class
에 속해있고@objc dynamic
특성(attributes)으로 표시된 고유한 속성(properties)을 관찰가능(observable)하게 만들 수 있다.ObservableObject
를 상속(inherit)하고, 속성(properties)에@Published
를 사용할 수도 있다. 컴파일러에서 생성한(compiler-generated)objectWillChange
publisher는@Published
속성 중 하나가 변경(changes)될 때마다 실행(triggers)된다. 하지만 어떤 속성이 변경(changed)되었는지는 알려주지 않는다.