RxSwift

[RxSwift] Extension Reactive

thoonk: 2022. 7. 14. 18:11
반응형

Extension Reactive에 관한 내용을 기록합니다. 

 

RxSwift 라이브러리 코드 중 Reactive.swift 안에 코드와 주석을 보면

  • Extend Reactive protocol with constrain on Base
  • Put any specific reactive extension for SomeType here

Base에 특정 타입을 지정해서 Reactive를 확장해서 사용하라 합니다.

@dynamicMemberLookup
public struct Reactive<Base: AnyObject> {
    /// 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> {
        Binder(self.base) { base, value in
            base[keyPath: keyPath] = value
        }
    }
}

 

기본적인 패턴은 다음과 같습니다.

extension Reactive where Base: SomeType {}

ControlEvent

  • ControlEvent는 이벤트를 관찰할 수 있습니다.
  • 또한, 아래 코드와 같이 ObservableType인 것을 확인할 수 있습니다.
  • 에러가 발생하지 않고 deallocate 되었을 때 Complete 됩니다.
  • MainScheduler 스레드에서 작동합니다.
public protocol ControlEventType : ObservableType {

    /// - returns: `ControlEvent` interface
    func asControlEvent() -> ControlEvent<Element>
}

 

RxSwift에서 버튼의 클릭을 바인딩할 때 사용하는 예를 보면, tap을 사용해서 구현하지만

이 또한 위 Reactive를 확장한 코드가 구현되어 있어 사용할 수 있습니다.

아래 코드와 같이, RxCocoa 라이브러리에 구현되어 있는 것을 확인할 수 있습니다.

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

// 아래 코드처럼 이벤트를 바인딩하여 사용할 수 있습니다.
button.rx.tap
    .bind(to: input.buttonTapped)
    .disposed(by: bag)

 


ControlProperty

  • ControlProperty는 프로퍼티 값을 관찰 및 주입할 수 있는 타입입니다. (Observer, Observable)
  • 해당 프로퍼티.rx를 통해 사용할 수 있으며, 에러가 발생하지 않고 deallocate 되었을 때 Complete 됩니다.
  • MainScheduler 스레드에서 작동합니다.
public protocol ControlPropertyType : ObservableType, ObserverType {

    /// - returns: `ControlProperty` interface
    func asControlProperty() -> ControlProperty<Element>
}

 

아래 코드는 ControlProperty 예시이며, UITextField 타입을 확장한 코드입니다.

extension Reactive where Base: UITextField {
    /// Reactive wrapper for `text` property.
    public var text: ControlProperty<String?> {
        value
    }
}

// 이 프로퍼티는 아래 코드처럼 바인딩하여 사용할 수 있습니다.
someText
    .bind(to: textField.rx.text)
    .disposed(by: bag)

Binder

  • Binder는 프로퍼티에 값을 주입할 수 있습니다.
  • 또한, 아래 코드와 같이 ObserverType인 것을 확인할 수 있습니다.
  • MainScheduler 스레드에서 작동합니다. (default)
public struct Binder<Value>: ObserverType {
    public typealias Element = Value
    
    private let binding: (Event<Value>) -> Void

    /// Initializes `Binder`
    ///
    /// - parameter target: Target object.
    /// - parameter scheduler: Scheduler used to bind the events.
    /// - parameter binding: Binding logic.
    public init<Target: AnyObject>(_ target: Target, scheduler: ImmediateSchedulerType = MainScheduler(), binding: @escaping (Target, Value) -> Void) {
        weak var weakTarget = target

        self.binding = { event in
            switch event {
            case .next(let element):
                _ = scheduler.schedule(element) { element in
                    if let target = weakTarget {
                        binding(target, element)
                    }
                    return Disposables.create()
                }
            case .error(let error):
                rxFatalErrorInDebug("Binding error: \(error)")
            case .completed:
                break
            }
        }
    }

    /// Binds next element to owner view as described in `binding`.
    public func on(_ event: Event<Value>) {
        self.binding(event)
    }

    /// Erases type of observer.
    ///
    /// - returns: type erased observer.
    public func asObserver() -> AnyObserver<Value> {
        AnyObserver(eventHandler: self.on)
    }
}

 

커스텀 확장은 아래 코드와 같이 할 수 있습니다.

extension Reactive where Base: UITextField {
    var text: Binder<String> {
        return Binder(self.base) { textField, text in
            textField.text = text
        }
    }
}

// 이 프로퍼티는 아래 코드처럼 바인딩하여 사용할 수 있습니다.
text
    .bind(to: textField.rx.text)
    .disposed(by: bag)

만약 라이브러리에서 지원하지 않는 메서드가 있다면, Reactive를 확장해서 사용해야 합니다.

 

예로, 라이브러리에서 지원해주지 않는 UIScrollView의 현재 페이지 인덱스를 구하여 Observable 타입으로 선언하고 싶을 때, 아래 코드처럼 커스텀할 수 있습니다.

스크롤 뷰가 멈췄을 때의 이벤트를 이용해서 현재 페이지 인덱스를 구할 수 있습니다.

extension Reactive where Base: UIScrollView {
    var currentPage: Observable<Int> {
        return didEndDecelerating.map({
            let pageWidth = self.base.frame.width
            let page = floor((self.base.contentOffset.x - pageWidth / 2) / pageWidth) + 1
            return Int(page)
        })
    }
}

 

그리고 아래 코드처럼 바인딩해서 ViewModel에서 로직 처리가 가능합니다.

scrollView.rx.currentPage
	.distinctUntilChanged()
	.bind(to: input.currentIndex)
	.disposed(by: bag)

 

부족한 점 피드백해주시면 감사합니다👍

반응형