ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [RxSwift] UILabel이 subscribe를 사용할 수 없는 이유
    iOS 2024. 7. 31. 12:49

     

    비동기 프로그래밍을 효과적으로 구현하고자 RxSwift와 RxCocoa에 의존성을 갖는 Swift 프로젝트에서는 UI객체들의 프로퍼티 값을 조회하여 값을 조작하거나 새로 설정하는 이벤트를 작성하기 위해 아래와 같은 코드를 자주 사용하게 된다.

     

    예시 코드

     

    testTextField.rx.text.subscribe { value in
        print("next - ", value)
    } onError: { error in
        print("error - ", error)
    } onCompleted: {
        print("competed")
    } onDisposed: {
        print("disposed")
    }

     

    그런데 UITextField와 마찬가지로 UILabel 역시 text프로퍼티를 갖고 있지만 이 경우에는 아래 이미지와 같이 subscribe메서드를 사용할 수 없다. 

     

    멤버들 중 subscribe 메서드는 찾을 수 없다

     

     

    연습 프로젝트를 개발하던 도중 이 부분이 의아해서 RxSwift와 RxCocoa의 소스코드를 살펴본 기록을 남기려 이번 포스트를 작성하게 되었다.

     

    먼저 결론부터 말하자면 아래와 같다.

     

     

    결론

    1. UITextField.rx.text는 UITextField의 프로퍼티의 값을 조회하고 수정할 수 있는 ControlProperty구조체를 반환하고 이 구조체는 ObservableType 프로토콜을 준수하기 때문에 subscribe의 호출이 가능하다.

    2. UILabel.rx.text는 UILabel의 프로퍼티에 새로운 값을 할당하기만 할 수 있는 Binder 구조체를 반환하고 이 구조체는 ObserverType프로토콜을 준수하기 때문에 subscribe의 호출이 불가하다

     

     

     

    UILabel이 rx.text.subscribe메서드를 사용할 수 없는 이유를 알아보기 위해선 먼저 UITextFielde가 rx.text.subsribe를 호출할 수  있는 이유부터 알아볼 필요가 있다.

     

    예시 코드

     

    testTextField.rx.text.subscribe { value in
        print("next - ", value)
    } onError: { error in
        print("error - ", error)
    } onCompleted: {
        print("competed")
    } onDisposed: {
        print("disposed")
    }

     

     

    위 예시 코드에는 2개의 객체와 2개의 프로토콜, 1개의 enum, 4개의 클로저가 사용되고 있다

    • ReactiveCompatible 프로토콜  ( = rx )
    • Reactive 구조체
    • ControlProperty 구조체 ( = text )
    • ObservableType 프로토콜에 구현된 subscribe 메서드
    • Event enum의 각 case 및 Obervable.disposed 호출시 실행되는 클로저

     

    또한 예제코드에 구현된 Observable Stream 속에서 위 객체들의 역할을 풀어서 설명하면 아래와 같다.

     

    1. ReativeCompatible 프로토콜의 rx 연산 프로퍼티를 호출하여 Reactive<UITextField>구조체 반환받음
    2. Reactive<UITextField>구조체의 text 연산 프로퍼티를 호출하여 ControlProperty<String?>구조체 반환받음
    3. ControlProperty<String?>구조체가 준수하고 있는 ObservableType프로토콜의 extension에 구현된 subscribe 메서드의 각 파라미터(onNext, onError, onCopleted, onDispoesd)에 클로저를 할당하여 호출

     

     

    즉 지금까지 우리는 2개의 프로토콜과 2개의 구조체를 사용하여 여러겹으로 wrapping되어 있는 복잡한 로직을 간단하게 코드 몇 줄로 호출해왔던 것이다.

     

    그러므로 각 프로토콜과 객체가 정의된 RxSwift와 RxCocoa 패키지의 소스코드를 하나씩 살펴보며 정확한 흐름을 파악할 필요가 있다.

     

     

    RxSwift.Reactive 구조체의 NSObject extension

     

    import Foundation
    
    /// Extend NSObject with `rx` proxy.
    extension NSObject: ReactiveCompatible { }

     

    Swift 프로젝트가 RxSwift 패키지와 의존성을 수립하면 Swift에서 모든 Object들의 부모 클래스인 NSObject가 ReactiveCompatible 프로토콜을 준수하게 된다.

     

    그리고 ReactiveCompatible 프로토콜은 아래와 같이 rx 연산프로퍼티를 구현하고 있다.

     

     

    Reactive 구조체와 ReactiveCompatible 프로토콜 

     

    @dynamicMemberLookup
    public struct Reactive<Base> {
        /// Base object to extend.
        public let base: Base
    
        /// Creates extensions with base object.
        ///
        /// - parameter base: Base object.
        public init(_ base: Base) {
            self.base = base
        }
    
        /// Automatically synthesized binder for a key path between the reactive
        /// base and one of its properties
        public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Property>) -> Binder<Property> where Base: AnyObject {
            Binder(self.base) { base, value in
                base[keyPath: keyPath] = value
            }
        }
    }
    
    /// A type that has reactive extensions.
    public protocol ReactiveCompatible {
        /// Extended type
        associatedtype ReactiveBase
    
        /// Reactive extensions.
        static var rx: Reactive<ReactiveBase>.Type { get set }
    
        /// Reactive extensions.
        var rx: Reactive<ReactiveBase> { get set }
    }
    
    extension ReactiveCompatible {
        /// Reactive extensions.
        public static var rx: Reactive<Self>.Type {
            get { Reactive<Self>.self }
            // this enables using Reactive to "mutate" base type
            // swiftlint:disable:next unused_setter_value
            set { }
        }
    
        /// Reactive extensions.
        public var rx: Reactive<Self> {
            get { Reactive(self) }
            // this enables using Reactive to "mutate" base object
            // swiftlint:disable:next unused_setter_value
            set { }
        }
    }

     

     

    UITextField의 인스턴스가 ReactiveCompatible 프로토콜의 rx 연산프로퍼티를 호출하게 되면 Reactive<UITextField> 구조체의 인스턴스가 생성된다.

     

    이때  Reactive<UITextField> 구조체는 subscript 연산자를 통해 자신의 각 프로퍼티에 새로운 값을 할당할 수 있게 해주는 Binder<Property> 구조체를 반환할 수 있는 상태가 된다.

     

    단, 이는 프로젝트가 RxSwift에만 의존성을 가질 경우엔 모든 UI 객체에 적용되지만 RxCocoa에도 의존성을 가질 경우엔 RxCocoa의 구현을 따른다. 

     

    따라서 예시코드의 textTextField의 경우엔 RxCocoa의 구현대로 ControlProperty를 반환하기 때문에 subscribe 메서드를 호출할  수 있는 것이다.

     

    만약 예시코드가 RxSwift에만 의존성을 가진 프로젝트에서 쓰였다면 앞서 살펴본 바와 같이 Binder<text> 구조체를 반환하게 되어 subscribe메서드를 사용하지 못했을 것이다.


    아래는 Reactive의 subscript 연산자를 활용해 UITexfield의 text 프로퍼티를 호출하는 예시이다.

     

    RxSwift에만 의존성을 가진 프로젝트에서의 UITextField.rx

     

    Reactive의 subscript 연산자는 keyPath를 통해 프로퍼티에 접근하여 값을 설정하는 Binder 구조체를 반환한다

     

     

    RxCocoa에 구현된 Reactive 구조체의 UITextField Extension

     

    import RxSwift
    import UIKit
    
    extension Reactive where Base: UITextField {
        /// Reactive wrapper for `text` property.
        public var text: ControlProperty<String?> {
            value
        }
        
        /// Reactive wrapper for `text` property.
        public var value: ControlProperty<String?> {
            return base.rx.controlPropertyWithDefaultEvents(
                getter: { textField in
                    textField.text
                },
                setter: { textField, value in
                    // This check is important because setting text value always clears control state
                    // including marked text selection which is important for proper input
                    // when IME input method is used.
                    if textField.text != value {
                        textField.text = value
                    }
                }
            )
        }
        
        /// Bindable sink for `attributedText` property.
        public var attributedText: ControlProperty<NSAttributedString?> {
            return base.rx.controlPropertyWithDefaultEvents(
                getter: { textField in
                    textField.attributedText
                },
                setter: { textField, value in
                    // This check is important because setting text value always clears control state
                    // including marked text selection which is important for proper input
                    // when IME input method is used.
                    if textField.attributedText != value {
                        textField.attributedText = value
                    }
                }
            )
        }
    }

     

    예시코드에서 testTextField.rx.text 구문을 통해 호출하는 것은 위 소스코드의 text 연산프로퍼티이며 그 반환값으로 ControlProperty<String?> 이 선언되어 있는 것을 확인할 수 있다.

     

    따라서 아래 이미지와 같이 ContorlProperty 구조체가 반환되고 예시코드는 이를 사용하고 있는 것이다.

     

    RxSwift와 RxCocoa 양쪽에 의존성을 가진 프로젝트에서의 UITextField.rx 

     

    프로젝트에 RxCocoa의 의존성이 설정되어 있는 경우엔 import를 하지 않더라도 RxCocoa의 구현을 따른다

     

     

    그렇다면 Reactive 구조체가 반환하는 Binder와 ControlProperty 구조체는 어떤 역할을 하는 객체일까.

     

    또한 앞서 결론에서 서술했듯 ObservableType과 ObserverType에 대해서도 살펴볼 필요가 있다.

     

    여기서부터는 본격적으로 RxSwift와 RxCocoa의 내부를 들여다보게 되는데, 이어지는 질문에 대한 답은 다음 포스트에서 다뤄보겠다.

    댓글

All Posts are written by Tunastorm