iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0
Mobile Development

RxSwift / 30天探索之旅系列 第 25

第 25 天 - DelegateProxy

  • 分享至 

  • xImage
  •  

今天這篇是延續昨天的Reactive Extensions,在iOS設計中,delegate pattern很常見,但要套用到functional programming,似乎有點不知道串,所以,我們今天就來看看,大神們是怎麼將CLLocationManager封裝成Rx的樣子,官方程式在RxSwift/RxCLLocationManagerDelegateProxy.swiftCLLocationManager+Rx.swift

DelegateProxy

原本我們做delegate時,只能給一個人,但封裝之後,delegate被佔據,我們還是可以使用setDelegate跟給Rx Observable使用,這是因為有DelegateProxy,顧名思義是代理Delegate。

下方流程圖被記錄在DelegateProxyType.swift當中,UIScrollView的delegate交給proxy,再由proxy去實現delegate內容,所以對外來說,只能接觸到proxy。

// DelegateProxyType.swift
+-------------------------------------------+
|                                           |                           
| UIView subclass (UIScrollView)            |                           
|                                           |
+-----------+-------------------------------+                           
            |                                                           
            | Delegate                                                  
            |                                                           
            |                                                           
+-----------v-------------------------------+                           
|                                           |                           
| Delegate proxy : DelegateProxyType        +-----+---->  Observable<T1>
|                , UIScrollViewDelegate     |     |
+-----------+-------------------------------+     +---->  Observable<T2>
            |                                     |                     
            |                                     +---->  Observable<T3>
            |                                     |                     
            | forwards events                     |
            | to custom delegate                  |
            |                                     v                     
+-----------v-------------------------------+                           
|                                           |                           
| Custom delegate (UIScrollViewDelegate)    |                           
|                                           |
+-------------------------------------------+                           

RxCLLocationManagerDelegateProxy

第一步,要為CLLocationManager建立DelegateProxy,需要繼承DelegateProxy<CLLocationManager, CLLocationManagerDelegate>,實作DelegateProxyTypeCLLocationManagerDelegate

 RxCLLocationManagerDelegateProxy
    : DelegateProxy<CLLocationManager, CLLocationManagerDelegate>
    , DelegateProxyType
    , CLLocationManagerDelegate {

第二步,完成init()部分

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

這部分被定義在DelegateProxy.swift當中

public init<Proxy: DelegateProxyType>(parentObject: ParentObject, delegateProxy: Proxy.Type)
    where Proxy: DelegateProxy<ParentObject, Delegate>, Proxy.ParentObject == ParentObject, Proxy.Delegate == Delegate {
    …
}

第三步,CLLocationManager需要實現HasDelegate protocol,並將Delegate型態具體採用為CLLocationManagerDelegate

extension CLLocationManager: HasDelegate {
    public typealias Delegate = CLLocationManagerDelegate
}

如此一來DelegateProxyType就可以set, get Delegate

extension DelegateProxyType where ParentObject: HasDelegate, Self.Delegate == ParentObject.Delegate {
    public static func currentDelegate(for object: ParentObject) -> Delegate? {
        return object.delegate
    }

    public static func setCurrentDelegate(_ delegate: Delegate?, to object: ParentObject) {
        object.delegate = delegate
    }
}

第四步
呼叫DelegateProxyType中的register<Parent>,將RxCLLocationManagerDelegateProxy的instance儲存到DelegateProxyFactory

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

(使用起來很簡單,但看這邊的Source Code是真的看不太懂...)

第四步已經完成DelegateProxy的建立,已經可以進入到下一段,對CLLocationManager進行Rx extension。

CLLocationManager+Rx

第五步
對CLLocationManager進行Rx extension,並透過proxy來取得RxCLLocationManagerDelegateProxy,在文件中明確提醒,絕對不要直接初始化RxCLLocationManagerDelegateProxy,而是要用proxy來取得。

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

第六步
現在就可以針對每一個delegate方法進行Rx化,將delegate的方法轉換成Observable,實際做法有兩種,第一種是在RxCLLocationManagerDelegateProxy.swift所看到的,實現CLLocationManagerDelegate方法,並以Subject作傳遞

internal lazy var didUpdateLocationsSubject = PublishSubject<[CLLocation]>()
internal lazy var didFailWithErrorSubject = PublishSubject<Error>()

public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    _forwardToDelegate?.locationManager?(manager, didUpdateLocations: locations)
    didUpdateLocationsSubject.onNext(locations)
}

public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    _forwardToDelegate?.locationManager?(manager, didFailWithError: error)
    didFailWithErrorSubject.onNext(error)
}

實際使用起來像這樣

public var didUpdateLocations: Observable<[CLLocation]> {
        RxCLLocationManagerDelegateProxy.proxy(for: base).didUpdateLocationsSubject.asObservable()
}

另一種方式,使用methodInvoked來取的Observable,a即是[Any]型態,表示著didUpdateLocations的參數,以這裡來說,a[1]就代表locations: [CLLocation]

public var didUpdateLocations: Observable<[CLLocation]> {
    return delegate.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)))
        .map { a in
            return try castOrThrow([CLLocation].self, a[1])
    }
}

透過castOrThrowa[1]轉成[CLLocation]型態,若不成功則拋出錯誤

private func castOrThrow<T>(_ resultType: T.Type, _ object: Any) throws -> T {
    guard let returnValue = object as? T else {
        throw RxCocoaError.castingError(object: object, targetType: resultType)
    }

    return returnValue
}

第七步
原本使用起來會像這樣

extension LocationDemoViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        viewModel.location.onNext(locations.last!)
    }

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        viewModel.currentAuthorizationStatus.onNext(status)
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        viewModel.error.onNext(error)
    }
}

經過Rx extension後,實際使用起來會像下面這樣,是不是更符合Rx了呢!

func bindViewModel() {
    viewModel.locationSignal.bind(to: infoLabel.rx.text).disposed(by: disposeBag)

    locationManager.rx.didUpdateLocations.map { $0.last! }.bind(to: viewModel.location).disposed(by: disposeBag)

    locationManager.rx.didChangeAuthorizationStatus.bind(to: viewModel.currentAuthorizationStatus).disposed(by: disposeBag)

    locationManager.rx.didFailWithError.bind(to: viewModel.error).disposed(by: disposeBag)
}

到此為止,對Delegate Rx化已經講完了,在這次研究DelegateProxy過程中,參考了許多文章,其中有一篇講得特別清楚,受到許多幫助,也分享給大家,RxSwift: How to make your favorite Delegate-based-APIs Reactive ?


在看Source code的過程中,道行不夠,遇到許多困難,相對也學習到很多,大神們把程式變得很抽象,讓任何人都可以擴充自己的Rx extension,真的厲害的跟鬼一樣,大概就是這樣,明天開始,剩下時間會探索一些常用的library,掰掰


上一篇
第 24 天 - Reactive Extensions
下一篇
第 26 天 - Two Way Binding
系列文
RxSwift / 30天探索之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言