# RxCocoa

Cocoa touch framework를 Rx방식으로 확장하여 사용하는 것이 RxCocoa라이브러리이다. RxSwift에서 제공하는 Reactive구조체를 확장하되, UI 특정 요소를 베이스로 한다.

extension Reactive where Base: UIButton{
    // 기본 코드
    public var tap: ControlEvent<Void> {
        controlEvent(.touchUpInside)
    }
}

RxCocoa에서는 ReactiveCompatible 프로토콜이 함께 사용된다. rx라는 타입속성이 추가된다. NSObject는 코코아터치 프레임워크의 모든 요소가 상속하는 클래스인데, 해당 클래스에서 ReactiveCompatible 프로토콜을 채택하므로 모든 요소가 rx 속성을 갖게된다.

class HelloRxCocoaViewController: UIViewController {

    let bag = DisposeBag()

    @IBOutlet weak var valueLabel: UILabel!

    @IBOutlet weak var tapButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        tapButton.rx.tap
            .map{ "Hello, RxCocoa. "}
//            .subscribe(onNext: { [weak self] str in
//                self?.valueLabel.text = str
//            })
            .bind(to: valueLabel.rx.text)
            .disposed(by: bag)
    }
}

구독을 통해 연산된 문자열을 직접 레이블로 방출하는 방법도 있지만, bind연산자를 사용하면 훨씬 간편하게 UI에 데이터를 바인딩 할 수 있다.

bind 연산자의 파라미터에 전달되는 속성들은 바인더 속성이며, 코코아 터치 프레임워크에서 제공하는 기본 속성들이 모두 자동으로 합성되어 사용된다.

# binding

데이터 바인딩에는 데이터 생산자와 데이터 소비자가 있다. RxSwift에서는 옵저버블을 채택한 모든 형식이 데이터 생산자이다. 데이터 소비자는 UI컴포넌트이다.

바인더는 에러 이벤트를 받지 않는다. next, onCompleted 이벤트만 받는다. 다음은 텍스트필드에 입력되는 문자열을 레이블에 바인딩하는 코드이다.

    override func viewDidLoad() {
        super.viewDidLoad()

        valueLabel.text = ""
        valueField.becomeFirstResponder()


//        valueField.rx.text
//            .observe(on: MainScheduler.instance)
//            .subscribe(onNext: { [weak self] str in
//                self?.valueLabel.text = str
//            })
//            .disposed(by: disposeBag)

        valueField.rx.text
            .bind(to: valueLabel.rx.text)
            .disposed(by: disposeBag)
    }

직접 구독하여 next로 전달된 문자열을 삽입해도 되지만, bind연산자를 사용하면 훨씬 간편하다.

위의 주석처리된 observe(on:)의 경우 스케줄러를 메인으로 지정해줘야 크래시 가능성을 없앨 수 있다. 텍스트 업데이트는 메인 스레드에서 주관하기 때문이다.

# Traits

트레잇은 UI 업데이트에 특화된 옵저버블이다. 모든 작업은 메인 스레드에서 실행된다.

트레잇은 옵저버블이지만 share연산자 기반의 옵저버블과 동일한 형태를 갖기 때문에 구독해도 시퀀싱이 새로 시작되지는 않는다.

# ControlEvent, ControlProperty

컨트롤 프로퍼티는 옵저버블과 옵저버 두 타입을 모두 상속하는 타입이다. share(replay: 1) 기반으로 구현되어 있다.

컨트롤이벤트는 옵저버블 타입만 상속한다. share() 기반의 구현이기 때문에 구독시 시퀀싱이 새로 시작된다. (버퍼에 데이터를 저장하지 않는다.)

Observable<Float>.combineLatest([redSlider.rx.value, greenSlider.rx.value, blueSlider.rx.value])
    .map{ UIColor(red: CGFloat($0[0]) / 255, green: CGFloat($0[1]) / 255, blue: CGFloat($0[2]) / 255, alpha: 1.0)}
    .bind(to: colorView.rx.backgroundColor)
    .disposed(by: bag)

resetButton.rx.tap
    .subscribe(onNext: { [weak self] in
        self?.colorView.backgroundColor = .black

        self?.redSlider.value = 0
        self?.greenSlider.value = 0
        self?.blueSlider.value = 0
        self?.updateComponentLabel()
    })
    .disposed(by: bag)

위 코드는 슬라이더 컴포넌트의 바인더를 활용하여 컬러뷰의 배경색을 바꿔주는 코드이다. 슬라이더의 rx.value가 컨트롤 프로퍼티이다.

아래의 리셋버튼 rx.tap의 경우는 컨트롤 이벤트이다. 컨트롤 이벤트와 컨트롤 프로퍼티는 모두 메인 스레드에서의 UI업데이트를 보장하기 때문에 별도의 스레드 관리가 불필요하다.

# Driver

드라이버는 RxCocoa에서 제공하는 트레잇중 가장 핵심적인 역할을 한다. 드라이버 역시 구독자에게 에러를 전달하지 않는다. 작업은 메인스레드에서 이루어진다.

share(replay: 1, scope: .whileConnected) 구현과 동일하게 동작한다. 구독자에게 버퍼에 저장했던 최신 이벤트를 전달한다.

let result = inputField.rx.text
    .flatMapLatest { validateText($0) }

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)

flatMapLatest는 동일한 옵저버블이 전달된다는 가정 하에 가장 최신 이벤트를 방출한다.validateText 함수는 불리언 타입의 옵저버블을 리턴한다.

함수에 전달된 문자열값이 Double타입으로 캐스팅 되지 않으면 에러를 방출하고, 그게 아니면 true를 방출하는 옵저버블이 정의되어 있다.

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()
    }
}

에러 방출시 bind는 에러를 받지 않기 때문에 앱에서 크래시가 발생한다.

또한 result 변수는 validateText에서 리턴한 불리언 옵저버블을 갖는데, 각각 resultLabel의 텍스트 속성과 배경색, sendButton enabled 속성에 바인딩을 진행한다.

이때 바인딩을 진행하는 세번에 걸쳐 매번 옵저버블의 시퀀싱이 새롭게 시작된다. 이는 비효율적인 방식이다.

위 코드를 asDriver를 통해 개선할 수 있다.

let result = inputField.rx.text.asDriver()
    .flatMapLatest {
        validateText($0)
            .asDriver(onErrorJustReturn: false)
    }

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)

UI관련 바인딩 작업에는 드라이버를 주로 채택하여 사용하면 된다. 스레드 지정이 필요없고, 구독 공유기능도 자체적으로 지원하기 때문이다.

에러처리도 기본값 방식또는 대체 옵저버블을 사용하는 방식 둘 중에 선택하여 사용할 수 있다.