Traits은 UI에 특화된 옵저버블이다.
모든 작업은 Main Thread에서 작동해서 스케줄러를 지정할 필요가 없다.
UI가 항상 올바른 Thread에서 동작하는 걸 보장한다.
Traits을 구독하는 모든 구독자는 동일한 시퀀스를 구독한다. like share()
Error Event를 전달하지 않는다.
Traits의 종류로는 ControlProperty, ControlEvent, Driver, Signal이 있다.
이게 종류가 많은데 언제 뭘 쓰는지, 어떻게 적용해야 하는지가 어렵다. 어렵냐? 나도 어렵다.
이 글에서는 ControlProperty와 ControlEvent에 대해서 자세히 작성해보겠당.. Driver와 Signal은 나도 어렵당..
1. ControlProperty
ControlPropertyType 프로토콜을 채택하고 있다.
ControlPropertyType 프로토콜은 ObservableType과 ObserverType을 채택한다.
새로운 구독자가 추가되면 가장 최근 속성값이 전달된다.
파라미터로 getter와 setter를 전달한다.
getter - value에 저장된 값을 (Observable로 사용될 때 호출)
setter - (Binding될 때 = 즉, Observer로 사용될 때)
extension Reactive where Base: UISlider {
/// Reactive wrapper for `value` property.
public var value: ControlProperty<Float> {
return base.rx.controlPropertyWithDefaultEvents(
getter: { slider in
slider.value
}, setter: { slider, value in
slider.value = value
}
)
}
}
1-1. Custom ControlProperty
1). Slider를 움직일 때마다 색상값을 방출해서 배경색을 바꿔야 한다?
이 경우에는 ControlProperty를 써야 한다.
왜냐하면 Binder는 옵저버블이 아니기 때문에 이벤트를 방출하지는 못한다. 프로퍼티에 값을 주입할 수는 없다.
Binder는 이벤트를 전달받아 구독하는 즉, 데이터를 받아서 적용하는 옵저버 역할만 가능하기 때문이다.
쓰기만 필요한 속성은 Binder(ObserverType)로 구현하고 /
읽기와 쓰기 모두 가능해야 하면 ControlProperty로 구현해야 한다.
UISlider를 Reactive로 확장시켜 color라는 속성을 만들어줬고 ControlProperty<UIColor?>라는 타입을 줬다.
위 과정을 고대로 적용해줘서 getter에는 slider의 value값을 주고
setter에서는 slider와 color에 값을 준다.
extension Reactive where Base: UISlider {
var color: ControlProperty<UIColor?> {
return base.rx.controlProperty(editingEvents: .valueChanged) { slider in
UIColor(white: CGFloat(slider.value), alpha: 1.0)
} setter: { slider, color in
var white = CGFloat(1)
color?.getWhite(&white, alpha: nil)
slider.value = Float(white)
}
}
}
whiteSlider.rx.color
.bind(to: view.rx.backgroundColor)
.disposed(by: bag)
resetButton.rx.tap
.map { _ in UIColor(white: 0.5, alpha: 1.0) }
.bind(to: whiteSlider.rx.color.asObserver(), view.rx.backgroundColor.asObserver())
.disposed(by: bag)
2). textField에 글을 쓸 때마다 글자수를 계산해서 countLabel에 글자수를 적용한다?
기본 Rx로 구현을 하면 사실 간단하긴 하다.
inputField.rx.text
.map { $0?.count ?? 0 }
.map { "\($0)" }
.bind(to: countLabel.rx.text)
.disposed(by: bag)
근데 그게 아니잖아!!!!!!!!
여튼, 그렇기 위해서는 우선 생각을 해야한다. 당연한 말이지만,, 냅다 코드를 치지말고,, 생각을 하면
1. inputTextField의 글자수를 방출해서 그 데이터를 가져다가 countLabel에 바인딩을 해줘야겠구나
- UITextField를 확장해서 count 속성을 만들어주자
- 글자수 방출이니까 ControlProperty겠지
- 바인딩이 필요하네 그렇다면 UILabel에는 Binder가 필요하겠네
2. 글자수는 int고 label.text는 string이니까 타입을 맞춰줘야 하네
- ControlProperty<Int>겠구나
- Binder에서 만들어 줄 속성의 타입은 결국 Binder<Int>겠구나
extension Reactive where Base: UILabel {
var countText: Binder<Int> {
return Binder(self.base) { label, count in
label.text = "\(String(describing: count))"
}
}
}
extension Reactive where Base: UITextField {
var count: ControlProperty<Int> {
return base.rx.controlProperty(editingEvents: .editingChanged) { textField in
if let text = textField.text {
return text.count
} else {
return 0
}
} setter: { textField, count in
textField.text = "\(String(describing: count))"
}
}
}
inputField.rx.count
.bind(to: countLabel.rx.countText)
.disposed(by: bag)
좀 더 코드가 간결해진 거... 겠지...?
2. ControlEvent
Event를 옵저버블로 래핑한 것
ControlEventType 프로토콜을 채택한 제네릭 구조체이고
ControlEventType은 ObservableType을 상속해서 옵저버블의 역할을 수행하지만 옵저버의 역할은 수행하지 못한다..
가장 최근 이벤트를 replay하지 않아서 그래서 새로운 구독자는 구독 이후의 이벤트만 전달 받는다.
방출되는 요소는 Void이다. ControlProperty랑 다르게!
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
controlEvent(.touchUpInside)
}
}
ControlEvent로 가장 많이 쓰는 게 버튼 탭이지 싶다.
다른 예시로는 UITextField 에서 많이 사용되는 editingDidBegin, editingDidEnd가 있다.
extension Reactive where Base: UITextField {
var borderColor: Binder<UIColor?> {
return Binder(self.base) { textField, color in
textField.layer.borderColor = color?.cgColor
}
}
var editingDidBegin: ControlEvent<Void> {
return controlEvent(.editingDidBegin)
}
var editingDidEnd: ControlEvent<Void> {
return controlEvent(.editingDidEnd)
}
}
inputField.rx.editingDidBegin
.map { UIColor.red }
.bind(to: inputField.rx.borderColor)
.disposed(by: bag)
inputField.rx.editingDidEnd
.map { UIColor.gray }
.bind(to: inputField.rx.borderColor)
.disposed(by: bag)
3. Driver
Driver는 시퀀스를 알아서 공유한다. like share 그래서 구독 후 가장 최근 이벤트를 공유한다.
- 아래 코드는 share를 써주지 않으면 bind가 3번 처리되어 시퀀스가 3번 발생된다.
func bind() {
let result = inputField.rx.text
.flatMapLatest {
self.validateText($0)
.observe(on: MainScheduler.instance) // Main Scheduler를 직접 지정해서 잠재적인 오류 발생 제어
.catchAndReturn(false)
}
.share() // 모든 구독자가 하나의 시퀀스를 구독
/// 3번 bind 구독처리가 되어 시퀀스가 3번 발생
result
.map { $0 ? "OK" : "Error" }
.bind(to: resultLabel.rx.text)
.disposed(by: bag)
result
.map { $0 ? UIColor.blue : UIColor.red }
.bind(to: resultLabel.rx.backgroundColor)
.disposed(by: bag)
result
.bind(to: sendButton.rx.isEnabled)
.disposed(by: bag)
}
func validateText(_ value: String?) -> Observable<Bool> {
return Observable<Bool>.create { observer in
print("== \(value ?? "") Sequence Start ==")
// 작성된 위치랑 상관없이 함수 종료 직전에 실행되는 구문
defer {
print("== \(value ?? "") Sequence End ==")
}
guard let str = value, let _ = Double(str) else {
observer.onError(ValidationError.notANumber)
return Disposables.create()
}
observer.onNext(true)
observer.onCompleted()
return Disposables.create()
}
}
위 코드를 Driver를 사용해 바꿔보자.
asDriver() 메소드를 통해 일반 옵저버블을 Driver로 변환
Driver는 시퀀스를 공유하기 때문에 share()가 필요하지 않다.
모든 스레드가 항상 메인에서 동작하니까 스케줄러를 지정해주지 않아도 된다.
UI Binding 코드는 Driver를 적극 활용하자.
func bind() {
let result = inputField.rx.text.asDriver()
.flatMapLatest {
self.validateText($0)
.asDriver(onErrorJustReturn: false)
}
// .share() // 모든 구독자가 하나의 시퀀스를 구독 -> driver를 사용하면 쓰지 않아도 된다.
/// 3번 bind 구독처리가 되어 시퀀스가 3번 발생
result
.map { $0 ? "OK" : "Error" }
.drive(resultLabel.rx.text)
.disposed(by: bag)
result
.map { $0 ? UIColor.blue : UIColor.red }
.drive(resultLabel.rx.backgroundColor)
.disposed(by: bag)
result
.drive(sendButton.rx.isEnabled)
.disposed(by: bag)
}
'⭐️ 개발 > Rx' 카테고리의 다른 글
[Rx Operator 시리즈] 2. CombineLatest (0) | 2023.01.16 |
---|---|
[Rx Operator 시리즈] 1. map (0) | 2023.01.16 |
6. Binder (0) | 2023.01.16 |
[Rx] Error Operator (0) | 2023.01.16 |
[Rx] Input/Output 패턴 적용하기 - 비즈니스 로직 분리!! (4) | 2022.11.02 |