-Today's Learning Content-

  • extension reactive

1. CALayer 애니메이션 적용 문제

내용정리

오늘은 extension Reactive이 무엇인지, 어떻게 사용하는지에 대해 정리해보려고 한다.

1) Reacative.swift

RxSwift 패키지 내부를 살펴보면 Reactive.swift라는 파일이 존재한다. 이는 constrained protocol extensions를 위한 custom point로 사용된다.

// Reactive.swift 코드
@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
        }
    }
}

Reactive를 extension하는 대표적인 예제

일반적인 패턴은 아래와 같다.

먼저 Reactive를 extension하고 Base에 대한 제한(constrain)을 건다

extension Reactive where Base: SomeType { }

extension 안에서 SomeType에 대한 특정 reactive extension을 구현한다.

예로, UIView의 배경색을 Rx로 설정할 수 있도록 Binder를 정의할 수 있다.

extension Reactive where Base: UIView {
    public var backgroundColor: Binder<UIColor?> {
        return Binder(self.base) { view, color in
            view.backgroundColor = color
        }
    }
}
// 사용 방법
view.rx.backgroundColor.onNext(.red)

Binder에 대한 설명은 아래에서 이어진다.

2) ControlEvent

ControlEvent는 값을 주입할 수는 없지만, 값을 감지할 수 있는 타입이다. 주로 UI 이벤트 감지에 사용된다.

우리가 RxCocoa에서 자주 사용하는 UIButton.rx.tap은 버튼의 탭 이벤트를 감지하는 Rx인데, 이를 사용할 수 있는 이유는 이미 Reactive Extension으로 구현이 되어있기 때문이다.

extension Reactive where Base: UIButton {
    
    /// Reactive wrapper for `TouchUpInside` control event.
    public var tap: ControlEvent<Void> {
        controlEvent(.touchUpInside)
    }
}

이 외에도 UIButton에 대한 많은 속성이 Rx에서 extension으로 구현되어 있다.

#endif

#if os(tvOS)

import RxSwift
import UIKit

extension Reactive where Base: UIButton {

    /// Reactive wrapper for `PrimaryActionTriggered` control event.
    public var primaryAction: ControlEvent<Void> {
        controlEvent(.primaryActionTriggered)
    }

}

#endif

#if os(iOS) || os(tvOS) || os(visionOS)

import RxSwift
import UIKit

extension Reactive where Base: UIButton {
    /// Reactive wrapper for `setTitle(_:for:)`
    public func title(for controlState: UIControl.State = []) -> Binder<String?> {
        Binder(self.base) { button, title in
            button.setTitle(title, for: controlState)
        }
    }

    /// Reactive wrapper for `setImage(_:for:)`
    public func image(for controlState: UIControl.State = []) -> Binder<UIImage?> {
        Binder(self.base) { button, image in
            button.setImage(image, for: controlState)
        }
    }

    /// Reactive wrapper for `setBackgroundImage(_:for:)`
    public func backgroundImage(for controlState: UIControl.State = []) -> Binder<UIImage?> {
        Binder(self.base) { button, image in
            button.setBackgroundImage(image, for: controlState)
        }
    }
    
}
#endif

#if os(iOS) || os(tvOS) || os(visionOS)
    import RxSwift
    import UIKit
    
    extension Reactive where Base: UIButton {
        /// Reactive wrapper for `setAttributedTitle(_:controlState:)`
        public func attributedTitle(for controlState: UIControl.State = []) -> Binder<NSAttributedString?> {
            return Binder(self.base) { button, attributedTitle -> Void in
                button.setAttributedTitle(attributedTitle, for: controlState)
            }
        }
    }
#endif

3) ControlProperty

ControlProperty는 값을 주입할 수도 있고, 변화를 감지할 수도 있는 타입이다.

ControlProperty의 대표적인 예로 UITextField를 들 수 있는데, UITextField의 text 프로퍼티는 값을 입력할 수도 있고, 값의 변화를 감지할 수도 있는 ControlProperty이다.

그리고 이는 Reactive extension으로 구현이 되어있다.

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
                }
            }
        )
    }

4) Binder

이제 위에서 나온 Binder인데, Binder는 값을 주입할 수 있지만, 변화를 감지할 수 없는 타입이다. 즉, UI를 업데이트할 때 주로 사용된다.

예를 들어 UIButton에 이미지를 삽입하는 rx 코드를 만들고 싶다고 한다면, 이미지를 주입 해줘야 하기 때문에 값을 주입할 수 있어야 하지만, 이미지가 바뀐 것을 감지할 필요는 없기 때문에 변화는 감지하지 않아도 된다. 이럴 때 Binder를 사용한다.
(사실 이미 다 구현이 되어있다.)

UIButton에 setImage를 적용하는 예제

extension Reactive where Base: UIButton {
    public func customImage() -> Binder<UIImage?> {
        return Binder(self.base) { (button, image) in
            button.setImage(image, for: [])
        }
    }
}
// 사용 방법
self.myButton.rx
    .customImage()
    .onNext(UIImage(systemName: "heart"))

5) 결론

✅ ControlEvent vs ControlProperty vs Binder

유형값 주입 가능값 변화 감지 가능특징
ControlEventUI 이벤트 감지 (ex: 버튼 탭)
ControlProperty값 변경 감지 + 값 주입 가능 (ex: UITextField.text)
Binder값 주입만 가능 (ex: UILabel.text, UIButton.setImage)

✅ ControlEvent와 ControlProperty의 공통점

  • RxCocoa Traits의 일종

  • 메인 스레드(MainScheduler)에서 실행됨

  • 절대 실패하거나 에러를 발생시키지 않음

  • 컨트롤이 dealloc되면 자동으로 Complete 이벤트를 방출

✅ 차이점

  • ControlProperty → share(replay: 1) behavior 적용

  • ControlEvent → 구독 시 초기값을 보내지 않음

-Today's Lesson Review-

reactive의 extension을 그냥 사용만 했었지,
타입이 분류되어 있는 줄도 몰랐고 이런 원리인 줄도 몰랐다.
오늘 공부한 덕에 RxSwift와 조금 더 친해진 것 같다.
profile
이유있는 코드를 쓰자!!

0개의 댓글