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)
부족한 점 피드백해주시면 감사합니다👍
반응형