ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 12: Key-Value Observing
    Raywenderlich/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으로 변경되므로, 0100의 두 가지 값(values)을 얻게 된다. 그런 다음 100에서 200으로 변경(changes)되어, 다시 100200 두 값(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
Designed by Tistory.