extension Reactive의 사용
이번 포스팅에서는 아래 예시처럼 일반적으로 많은 프로젝트에서 Reactive를 extension해서 사용하는데, Reactive가 무엇이고 왜 사용하는지에 대해 공부한 것을 정리하고자 합니다.
// 예시
class MainViewController: UIViewController {
}
extension Reactive where Base: MainViewController {
var setAlert: Binder<Alert> {
...
}
}
Reactive.swift 파일의 코드는 다음과 같습니다. 한줄 한줄 천천히 이해해보도록 하겠습니다.
/* Reactive.swift */
@dynamicMemberLookup
public struct Reactive<Base> {
public let base: Base
public init(_ base: Base) {
self.base = base
}
public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Property>) -> Binder<Property> where Base: AnyObject {
Binder(self.base) { base, value in
base[keyPath: keyPath] = value
}
}
}
public protocol ReactiveCompatible {
/// Extended type
associatedtype ReactiveBase
static var rx: Reactive<ReactiveBase>.Type { get set }
var rx: Reactive<ReactiveBase> { get set }
}
extension ReactiveCompatible {
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 { }
}
public var rx: Reactive<Self> {
get { Reactive(self) }
// this enables using Reactive to "mutate" base object
// swiftlint:disable:next unused_setter_value
set { }
}
}
import Foundation
/// Extend NSObject with `rx` proxy.
extension NSObject: ReactiveCompatible { }
Reactive 구조체는 base 프로퍼티를 가지고 있으며, Base 클래스에 대해 초기화를 수행합니다.
그 다음은 ReactiveCompatible 프로토콜입니다. ReactiveCompatible 의 extension에는 rx 프로퍼티가 선언되어 있으며, get은 Reactive(self)입니다.
그러므로 ReactiveCompatible 프로토콜을 채택하는 class에는 rx 프로퍼티를 필수로 선언해줘야 하며, 이 rx 프로퍼티는 Reactive 구조체입니다.
// 예시 코드
extension TestClass: ReactiveCompatible { }
let testClass = TestClass()
print(type(of: testClass.rx.base)) // TestClass 출력
예시로 ReactiveCompatible 프로토콜을 준수하는 TestClass의 인스턴스인 testClass를 선언했습니다. 이 testClass에는 rx 프로퍼티로 접근할 수 있는 어떤 struct의 인스턴스가 생기고, 이 인스턴스는 testClass 타입의 base를 가집니다.
즉, Reactive라는 struct의 인스턴스인 testClass.rx가 base를 갖는 것이죠.
다시 Reactive.swift 파일로 돌아가보죠. 마지막에 다음과 같이 작성되어 있습니다.
extension NSObject: ReactiveCompatible { }
NSObject에 rx 속성을 추가하는 것입니다. 그러므로 NSObject를 상속받는 모든 Class는 자기 자신의 타입을 갖는 rx.base를 가지게 됩니다. 결국 UI 관련된 Class들이 NSObject를 상속 받고 있고, 이 NSObject가 ReactiveCompatible를 채택하고 있기 떄문에 자동으로 UI 관련된 Class에 rx 속성이 붙습니다.
그렇다면 어떻게 사용할 수 있을까요?
UIButton의 tap을 감지하기 위해 다음과 같이 직접 controlEvent를 호출하여 tap을 감지할 수 있지만,
self.testButton.rx
.controlEvent(.touchUpInside)
.bind {
print("button tapped")
}
.disposed(by: disposeBag)
UIButton의 tap이라는 프로퍼티를 통해 이를 처리할 수 있습니다.
self.testButton.rx
.tap
.bind {
print("button tapped")
}
.disposed(by: disposeBag)
Reactive를 extension하여 tap이라는 프로퍼티를 UIButton+Rx.swift 파일에 구현해놓았기 때문입니다.
/* UIButton+rx.swift */
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
controlEvent(.touchUpInside)
}
}
이렇게 Reactive에 tap 속성을 확장시킨 것입니다.
ControlEvent, ControlProperty, Binder
1. ControlEvent
UIButton+rx 파일에 구현한 것처럼, 값을 관찰할 수는 있지만, 값을 주입시킬 수는 없는 타입
/* UIButton+rx.swift */
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
controlEvent(.touchUpInside)
}
}
2. ControlProperty
값을 관찰할 수도 있고, 값을 주입시킬수도 있는 타입
/* UITextField+rx.swift */
extension Reactive where Base: UITextField {
public var text: ControlProperty<String?> {
value
}
}
값 관찰
textField.rx.text.subscribe( onNext: { _ in } )
값 주입
textField.rx.text.onNext("HelloWorld")
3. Binder
값을 관찰할 수는 없지만, 값을 주입시킬수 있는 타입
typealias Alert = (title: String, message: String?)
extension Reactive where Base: MainViewController {
var setAlert: Binder<Alert> {
return Binder(base) { base, data in
let alertController = UIAlertController(title: data.title, message: data.message, preferredStyle: .alert)
let action = UIAlertAction(title: "확인", style: .cancel)
alertController.addAction(action)
base.present(alertController, animated: true, completion: nil)
}
}
}
위 코드는 typealias로 선언한 Alert 타입, 그리고 extension Reactive를 사용해 MainViewController을 Base로 하여 Alert 타입에 대해 setAlert 프로퍼티를 정의하고 값을 주입시킨 예제 코드입니다. 다음 코드와 같이 어떤 특정 이벤트에 대해 self.rx.setAlert를 방출할 수 있게 구현할 수 있습니다.
class MainViewController: UIViewController {
...
func bind(_ viewModel: MainViewModel) {
viewModel.presentAlert
.emit(to: self.rx.setAlert)
.disposed(by: disposeBag)
}
}
자료 출처
https://docs.swift.org/swift-book/ReferenceManual/Attributes.html
https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#controlproperty--controlevent
' iOS > RxSwift' 카테고리의 다른 글
RxSwift - Relay와 Drive (0) | 2023.01.29 |
---|---|
RxSwift - Error 관리 (0) | 2022.11.07 |
RxSwift - TimeBasedOperators (0) | 2022.11.07 |
RxSwift - Sequence 내부의 요소들간의 결합 연산자(reduce, scan) (0) | 2022.11.07 |
RxSwift - 하나의 Observable 가 Trigger 역할 후 Observable 들을 조합하는 방법(withLatestFrom, sample, amb, switchLatest) (0) | 2022.11.06 |