# Custom Binder

Binder 생성자 함수를 살펴보자.

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
        }
    }
}
  1. target: 바인딩을 진행할 타겟 객체이다. 보통 UI 컴포넌트가 전달된다.
  2. scheduler: UI 업데이트와 관련된 로직을 주로 수행하므로 스케줄러는 메인 스케줄러를 사용한다.
  3. binding: 바인딩을 위한 클로저이다. 클로저 파라미터로 타겟 객체와 바인딩을 진행하는 값이 전달된다.

next 이벤트가 전달될때마다 클로저를 호출한다. 에러 호출시 디버그모드에서는 fatalError를 발생시키지만 릴리즈모드에서는 에러를 출력만 해준다.

아래는 바인더 클래스를 활용하여 실제로 구현되어 있는 UILabel 익스텐션이다.

public var text: Binder<String?> {
    return Binder(self.base) { label, text in
        label.text = text
    }
}

직접 바인딩 로직을 구현하는 예시 코드를 살펴보자.

extension Reactive where Base: UILabel{
    var segmentedValue: Binder<Int>{
        return Binder(self.base) { label, index in
            switch index{
            case 0:
                label.textColor = .red
                label.text = "Red"
            case 1:
                label.textColor = .green
                label.text = "Green"
            case 2:
                label.textColor = .blue
                label.text = "Blue"
            default:
                label.textColor = .black
                label.text = "Unknown"
            }
        }
    }
}

바인더 클래스는 옵저버이다. 이벤트를 방출하는 옵저버블에서 바인더 객체로 데이터를 방출하는 형태로 코드를 작성하면 된다.

// 컬러피커 seletedSegmentIndex는 ControlProperty<Int>를 방출하는 옵저버블
colorPicker.rx.selectedSegmentIndex
    .bind(to: self.valueLabel.rx.segmentedValue)
    .disposed(by: bag)

where

where는 Reactive 클래스의 제네릭 타입인 Base가 어떤 클래스일때 확장을 사용할 지에 대해 분기처리해줄 수 있다.

# Custom ControlProperty

쓰기만 필요하면 커스텀 속성이 필요하다면 바인더를 사용하고, 읽기 쓰기 모두 가능한 커스텀 속성이 필요하면 커스텀 컨트롤 프로퍼티를 사용한다.

whiteSlider.rx.value
    .map { UIColor(white: CGFloat($0), alpha: 1.0) }
    .bind(to: view.rx.backgroundColor)
    .disposed(by: bag)

위 코드를 보면 슬라이더 위치 변경에 따라 값이 next 이벤트로 방출된다. 방출되는 각각의 값을 map으로 UIColor와 매핑시킨다. 밸류-컬러 변환 로직을 컨트롤 프로퍼티를 통해 구현하게 된다.

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. 베이스 객체의 controlPropertyWithDefaultEvents 함수를 실행한다.
  2. getter에서는 값을 리턴한다.
  3. setter에서는 클로저 파라미터에 전달된 베이스 객체의 값을 세팅한다.
internal func controlPropertyWithDefaultEvents<T>(
    editingEvents: UIControl.Event = [.allEditingEvents, .valueChanged],
    getter: @escaping (Base) -> T,
    setter: @escaping (Base, T) -> Void
    ) -> ControlProperty<T> {
    return controlProperty(
        editingEvents: editingEvents,
        getter: getter,
        setter: setter
    )
}

위 함수를 보면 controlPropertyWithDefaultEvents의 파라미터는 세개이다. 첫번째 파라미터는 기본값으로 설정되어 있다. 함수 호출 즉시 controlProperty함수가 리턴되는데, 이 지점이 우리가 직접 구현할 부분이다.

public func controlProperty<T>(
    editingEvents: UIControl.Event,
    getter: @escaping (Base) -> T,
    setter: @escaping (Base, T) -> Void
) -> ControlProperty<T> {
    let source: Observable<T> = Observable.create { [weak weakControl = base] observer in
            guard let control = weakControl else {
                observer.on(.completed)
                return Disposables.create()
            }

            observer.on(.next(getter(control)))

            // 지정된 이벤트 감지때마다 next 방출, getter를 전달
            let controlTarget = ControlTarget(control: control, controlEvents: editingEvents) { _ in
                if let control = weakControl {
                    observer.on(.next(getter(control)))
                }
            }

            return Disposables.create(with: controlTarget.dispose)
        }
        .take(until: deallocated)

    // 바인더 생성 후 컨트롤 프로퍼티에 할당 및 리턴
    let bindingObserver = Binder(base, binding: setter)

    return ControlProperty<T>(values: source, valueSink: bindingObserver)
}

controlPropertyeditingEvents파라미터는 next이벤트를 방출하기 위한 이벤트이다.

커스텀 컨트롤 프로퍼티 익스텐션을 활용하여 실제 구현한 코드를 살펴보자.

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

    }
}
  1. 베이스 객체의 controlProperty 함수를 호출하며, 넥스트 이벤트를 방출할 이벤트를 지정한다.
  2. getter와 setter를 각각 구현한다.

컨트롤프로퍼티 정의 후 실제 활용하는 코드이다.

whiteSlider.rx.color
    .bind(to: view.rx.backgroundColor)
    .disposed(by: bag)

resetButton.rx.tap
    .map{ UIColor(white:0.5, alpha: 1.0) }
    .bind(to: view.rx.backgroundColor.asObserver(), whiteSlider.rx.color.asObserver())
    .disposed(by: bag)

바인딩을 진행해주는데, 새로 정의한 슬라이더의 color 컨트롤 프로퍼티는 옵저버블이면서 옵저버이므로, 바인딩시 asObserver로 명시를 해줘야 한다. 이는 기본 컨트롤 프로퍼티인 view.rx.backgroundColor도 동일하다.

# Custom Event

컨트롤이벤트는 아래의 내부 구조를 가지고 있다. UIButtontap 컨트롤 이벤트이다.

extension Reactive where Base: UIButton {

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

public func controlEvent(_ controlEvents: UIControl.Event) -> ControlEvent<()> {
    let source: Observable<Void> = Observable.create { [weak control = self.base] observer in
            // 메인 스케줄러에서 실행되는지 확인
            MainScheduler.ensureRunningOnMainThread()

            guard let control = control else {
                observer.on(.completed)
                return Disposables.create()
            }

            // 지정한 이벤트 발생 시점에 next이벤트 방출
            let controlTarget = ControlTarget(control: control, controlEvents: controlEvents) { _ in
                observer.on(.next(()))
            }

            return Disposables.create(with: controlTarget.dispose)
        }
        .take(until: deallocated)

    return ControlEvent(events: source)
}
  1. 컨트롤이벤트 객체는 내부에서 controlEvent 함수를 호출한다. 해당 함수의 파라미터에는 이벤트를 전달한다.
  2. controlEvent 호출 이후에 옵저버블을 생성하는 것은 내부 팩토리 메서드를 통해 이루어진다. 아래의 실제 활용 코드를 확인해보자.
extension Reactive where Base: UITextField{
    var borderColor: Binder<UIColor>{
        return Binder(self.base) { tf, color in
            tf.layer.borderColor = color.cgColor
        }
    }

    // 커스텀 컨트롤 이벤트
    var editingDidBegin: ControlEvent<Void>{
        return controlEvent(.editingDidBegin)
    }

    // 커스텀 컨트롤 이벤트
    var editingDidEnd: ControlEvent<Void>{
        return controlEvent(.editingDidEnd)
    }
}

# Delegate Proxy

컨트롤 이벤트, 컨트롤 프로퍼티, 바인더 객체를 사용하면 기본 코코아터치 프레임워크의 많은 부분을 대체할 수 있다. 하지만 델리게이트 패턴의 경우 대체가 어려운 부분이 존재하는데, 이때 사용되는 것이 RxCocoa의 델리게이트 프록시(Delegate proxy)이다.

델리게이트 프록시는 특정 델리게이트 메서드가 호출되는 시점에 구독자에게 넥스트 이벤트를 전달해준다.

델리게이트 프록시를 구현할때는 보통 하나의 클래스와 두개의 익스텐션을 구현한다.

// 익스텐션 1
extension CLLocationManager: HasDelegate{
    public typealias Delegate = CLLocationManagerDelegate
}

먼저 위와 같이 확장 대상 클래스에 HasDelegate 프로토콜을 채택한 뒤 델리게이트 연관 형식을 확장 대상 클래스의 델리게이트 프로토콜로 지정해준다.

이후 클래스를 정의해준다. 클래스 이름은 접두어로 Rx, 접미어로 DelegateProxy를 붙인다.

  1. 해당 클래스는 DelegateProxy<T, V> 제네릭 파라미터를 갖는 DelegateProxy를 상속받고
    • 첫번째 형식 파라미터에는 확장할 클래스 타입을 전달한다.
    • 두번째 형식 파라미터에는 연관된 델리게이트 프로토콜을 전달한다.
  2. DelegateProxyType 프로토콜을 채택하며
  3. 확장 대상 클래스의 델리게이트 프로토콜까지 채택한다.
class RxCLLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate{
    weak private(set) var locationManager: CLLocationManager?

    init(locationManager: CLLocationManager){
        self.locationManager = locationManager
        super.init(parentObject: locationManager, delegateProxy: RxCLLocationManagerDelegateProxy.self)
    }

    static func registerKnownImplementations() {
        self.register {
            RxCLLocationManagerDelegateProxy(locationManager: $0)
        }
    }
}

extension Reactive where Base: CLLocationManager{
    var delegate: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>{
        return RxCLLocationManagerDelegateProxy.proxy(for: base)
    }

    var didUpdateLocations: Observable<[CLLocation]> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)))
            .map{ parameters in
                return parameters[1] as! [CLLocation]
            }
    }
}
  1. DelegateProxyType 프로토콜 채택시 6가지 요소에 대해 필수 구현이 필요하다.
  2. registerKnownImplementations 함수 구현 외에는 나머지 다섯가지에 대해 기본 구현이 제공되므로 필요한 상황이 아니면 구현하지 않아도 된다.
  3. weak private(set) 속성으로 외부에서 값 세팅만 하지 못하도록 확장대상 클래스 객체를 저장속성으로 추가해둔다. 델리게이트 프록시 구현시 약한참조로 선언해야한다.
  4. 생성자함수를 정의하고, 파라미터에는 확장대상 클래스 파라미터를 받는다.
    • 델리게이트 프록시 클래스의 저장속성에 파라미터를 할당한다.
    • 슈퍼클래스의 생성자 함수를 호출한다. parentObject에는 클로저의 파라미터를, delegateProxy 파라미터에는 델리게이트 프록시 클래스 메타타입을 전달한다.
  5. registerKnownImplementations 함수를 정의한다.
    • 함수 내부에서 register 함수를 호출하며, 클로저 내에서 델리게이트 프록시의 인스턴스를 리턴한다.
  6. Reactive 클래스를 확장한다. 베이스 모델은 확장 대상 클래스가 된다.
    • 델리게이트 속성을 마련해야한다. 델리게이트 타입은 델리게이트 프록시의 수퍼클래스이다. (제네릭 타입 파라미터 두개받는 클래스)
    • 델리게이트 속성 내에서 델리게이트 프록시 인스턴스를 생성하여 리턴해야하는데, 일반 생성자 함수를 사용하면 인스턴스가 중복되어 여러개 생성되어 의도대로 동작하지 않게 된다.
    • 따라서 .proxy(for: base) 함수를 호출하여 인스턴스 생성의 책임을 내부에 구현된 팩토리 메서드에게 넘겨야 한다.
    • 팩토리 메서드는 인스턴스가 생성되어 있으면 해당 객체를 리턴해주고, 그게 아니면 인스턴스를 새로 생성해준다.
  7. 실제 동작할 델리게이트 메서드를 구현한다.
    • 메서드 이름은 연관된 델리게이트 메서드 이름과 동일하게 가져가는 것이 일반적이다.
    • 델리게이트 프록시의 메서드는 델리게이트 메서드 호출 시점에 넥스트 이벤트를 전달한다.
    • 이러한 동작은 델리게이트 프록시의 수퍼클래스 delegate.methodInvoked 함수 덕분에 가능하다.
    • 해당 함수 파라미터에는 연관 델리게이트 프로토콜의 함수타입을 전달한다.
    • 넥스트 이벤트 전달에 따라 방출되는 대상은 [Any]에 저장되어 있는데, 해당 배열에는 델리게이트 메서드의 파라미터들이 정의되어 있다.
    • 원하는 파라미터를 타입캐스팅 하여 맵 연산자 내에서 리턴해주면 된다.

최종 실행 코드는 아래와 같다.

locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()

locationManager.rx.didUpdateLocations
    .subscribe(onNext: { print($0)} )
    .disposed(by: bag)

새롭게 정의한 델리게이트 프록시 메서드로부터 didUpdateLocations 델리게이트 메서드의 두번째 파라미터 값이 구독자에게 전달된다.

extension DelegateProxyViewController: CLLocationManagerDelegate{
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // ..
    }
}

위는 실제 델리게이트 메서드이다. managerlocations 파라미터를 받고 있는것을 확인할 수 있다.