내용정리
오늘은
extension Reactive이 무엇인지, 어떻게 사용하는지에 대해 정리해보려고 한다.
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하고 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에 대한 설명은 아래에서 이어진다.
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
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
}
}
)
}
이제 위에서 나온 Binder인데, Binder는 값을 주입할 수 있지만, 변화를 감지할 수 없는 타입이다. 즉, UI를 업데이트할 때 주로 사용된다.
예를 들어 UIButton에 이미지를 삽입하는 rx 코드를 만들고 싶다고 한다면, 이미지를 주입 해줘야 하기 때문에 값을 주입할 수 있어야 하지만, 이미지가 바뀐 것을 감지할 필요는 없기 때문에 변화는 감지하지 않아도 된다. 이럴 때 Binder를 사용한다.
(사실 이미 다 구현이 되어있다.)
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"))
| 유형 | 값 주입 가능 | 값 변화 감지 가능 | 특징 |
|---|---|---|---|
| ControlEvent | ❌ | ✅ | UI 이벤트 감지 (ex: 버튼 탭) |
| ControlProperty | ✅ | ✅ | 값 변경 감지 + 값 주입 가능 (ex: UITextField.text) |
| Binder | ✅ | ❌ | 값 주입만 가능 (ex: UILabel.text, UIButton.setImage) |
RxCocoa Traits의 일종
메인 스레드(MainScheduler)에서 실행됨
절대 실패하거나 에러를 발생시키지 않음
컨트롤이 dealloc되면 자동으로 Complete 이벤트를 방출
✅ 차이점
ControlProperty → share(replay: 1) behavior 적용
ControlEvent → 구독 시 초기값을 보내지 않음
reactive의 extension을 그냥 사용만 했었지,
타입이 분류되어 있는 줄도 몰랐고 이런 원리인 줄도 몰랐다.
오늘 공부한 덕에 RxSwift와 조금 더 친해진 것 같다.