RxSwift — Từ Delegate sang Observable

1. Giới thiệu

Hãy thử một ví dụ như sau:

final class ViewController: UIViewController {

    @IBOutlet weak var mapView: MKMapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        configView()
    }

    private func configView() {
        mapView.delegate = self
    }
}

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated: Bool) {
        print(mapView.centerCoordinate)
    }
}

Đây là một triển khai cơ bản của MKMapViewDelegate nơi bạn muốn bắt các in toạ độ (coordinate) mới khi thay đổi region trên bản đồ.

Tuy nhiên, phong cách này rất mâu thuẫn với tư tưởng Observables của RxSwift. Liệu chúng ta có thể chuyển đổi delegate này thành Observable<CLLocationCoordinate2D> để có thể observe được không? Chẳng phải sẽ tuyệt vời hơn đếu chúng ta có thể áp dụng như sau:

mapView.rx.centerDidChange
    .subscribe(onNext: { coordinate in
        /// Do something
    })

Hãy cùng thử thực hiện việc chuyển hoá delegate nhé:

2. Tạo Proxy Class

Đầu tiên chúng ta cần tạo một lớp proxy chứa các phương thức delegate. Proxy class này sẽ mang nhiệm vụ trung gian chuyển đổi giữa Delegate và Observable. Bạn sẽ cần phải cho proxy class kế thừa từ DelegateProxy, MKMapViewDelegate và DelegateProxyType.

import RxSwift
import RxCocoa
import MapKit

final class RxMKMapViewDelegateProxy: DelegateProxy<MKMapView, MKMapViewDelegate>, MKMapViewDelegate, DelegateProxyType {

    /// Typed parent object.
    public weak private(set) var mapView: MKMapView?

    /// - parameter mapView: Parent object for delegate proxy.
    public init(mapView: ParentObject) {
        self.mapView = mapView
        super.init(parentObject: mapView, delegateProxy: RxMKMapViewDelegateProxy.self)
    }

    /// Factory method
    static func registerKnownImplementations() {
        self.register { RxMKMapViewDelegateProxy(mapView: $0) }
    }

    /// Getter for delegate proxy
    static func currentDelegate(for object: MKMapView) -> MKMapViewDelegate? {
        let mapView: MKMapView = object
        return mapView.delegate
    }

    /// Setter for delegate proxy
    static func setCurrentDelegate(_ delegate: MKMapViewDelegate?, to object: MKMapView) {
        let mapView: MKMapView = object
        mapView.delegate = delegate
    }
}

3. Tạo các Extension Method cho các Delegate Method

Tiếp theo, chúng ta sẽ tạo một extension method cho các delegate method mà bạn mong muốn chuyển đổi sang Observable.

Trước tiên, chúng ta cần tạo một biến có tên là delegate, loại DelegateProxy. Đây là biến cung cấp cho bạn trở lại class proxy mà chúng ta đã tạo phía trên:

var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> {
    return RxMKMapViewDelegateProxy.proxy(for: base)
}

Sau đó chúng ta có thể quan sát các phương thức qua selector. Chúng ta sẽ quan sát selector và lấy một mảng các parameter qua dưới dạng [AnyObject?]. Tạo một biến mới có tên là regionDidChangeAnimated: ControlEvent<Bool>. Vì chúng ta chỉ cần biết nếu mapView có animate hay không nên chỉ cần lấy param ở vị trí số 2.

func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool)
var regionDidChangeAnimated: ControlEvent<Bool> {
    let source = delegate
        .methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
        .map { annotation in
            return try castOrThrow(Bool.self, annotation[1])
    }
    return ControlEvent(events: source)
}    

Từ đó chúng ta có thể mở rộng thêm để để bắt toạ độ dưới dạng Observable<CLLocationCoordinate2D> của mapView khi region được thay đổi.

var centerDidChange: Observable<CLLocationCoordinate2D> {
    return regionDidChangeAnimated
        .map { [base] _ in base.centerCoordinate }
        .startWith(base.centerCoordinate)
}

Đây là toàn bộ :

extension Reactive where Base: MKMapView {
    /// Reactive wrapper for `delegate`.
    var delegate: DelegateProxy<MKMapView, MKMapViewDelegate> {
        return RxMKMapViewDelegateProxy.proxy(for: base)
    }

    var regionDidChangeAnimated: ControlEvent<Bool> {
        let source = delegate
            .methodInvoked(#selector(MKMapViewDelegate.mapView(_:regionDidChangeAnimated:)))
            .map { annotation in
                return try castOrThrow(Bool.self, annotation[1])
        }
        return ControlEvent(events: source)
    }

    var centerDidChange: Observable<CLLocationCoordinate2D> {
        return regionDidChangeAnimated
            .map { [base] _ in base.centerCoordinate }
            .startWith(base.centerCoordinate)
    }
}

Bạn có thể sử dụng pattern này với hầu hết mọi loại delegate.

4. Sử dụng để lắng nghe thay đổi

mapView.rx.centerDidChange
    .subscribe(onNext: { coordinate in
        print(coordinate)
    })

GitHub Project: https://github.com/tienpx-1643/RxSwiftMapDelegateExample

5. Tham khảo

Max Alexander. 2019. RxSwift — Migrate Delegates to BEAUTIFUL Observables. [ONLINE] Available at: https://medium.com/@sudomax/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048. [Accessed 21 August 2019].


All Rights Reserved