-
Chapter 12: Key-Value ObservingRaywenderlich/Combine: Asynchronous Programming 2020. 8. 14. 04:31
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)되었는지는 알려주지 않는다.
'Raywenderlich > Combine: Asynchronous Programming' 카테고리의 다른 글
Chapter 14: In Practice: Project "News" (0) 2020.08.16 Chapter 13: Resource Management (0) 2020.08.15 Chapter 11: Timers (0) 2020.08.13 Chapter 10: Debugging (0) 2020.08.13 Chapter 9: Networking (0) 2020.08.13