Swift - RxSwift的使用詳解58(DelegateProxy樣例1:獲取地理定位信息 )

????委托(delegate) iOS 開發中十分常見。不管是使用系統自帶的庫,還是一些第三方組件時,我們總能看到 delegate 的身影。使用 delegate 可以實現代碼的松耦合,減少代碼復雜度。但如果我們項目中使用 RxSwift,那么原先的 delegate 方式與我們鏈式編程方式就不相稱了。

????解決辦法就是將代理方法進行一層 Rx 封裝,這樣做不僅會減少許多不必要的工作(比如原先需要遵守不同的代理,并且要實現相應的代理方法),還會使得代碼的聚合度更高,更加符合響應式編程的規范。

????其實在 RxCocoa 源碼中我們也可以發現,它已經對標準的 Cocoa 做了大量的封裝(比如 tableViewitemSelected)。下面我將通過樣例演示如何將代理方法進行 Rx 化。

一、對 Delegate進行Rx封裝原理

1,DelegateProxy

(1)DelegateProxy 是代理委托,我們可以將它看作是代理的代理。

(2)DelegateProxy 的作用是做為一個中間代理,他會先把系統的 delegate 對象保存一份,然后攔截 delegate 的方法。也就是說在每次觸發 delegate 方法之前,會先調用 DelegateProxy 這邊對應的方法,我們可以在這里發射序列給多個訂閱者。

2,流程圖

這里以 UIScrollView 為例,Delegate proxy 便是其代理委托,它遵守 DelegateProxyTypeUIScrollViewDelegate,并能響應 UIScrollViewDelegate 的代理方法,這里我們可以為代理委托設計它所要響應的方法(即為訂閱者發送觀察序列)。

/***
  
 +-------------------------------------------+
 |                                           |
 | 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)    |
 |                                           |
 +-------------------------------------------+
 
 **/

二、獲取地理定位信息樣例

這個是 RxSwift 的一個官方樣例,演示的是如何對 CLLocationManagerDelegate 進行 Rx 封裝。

1,效果圖

(1)第一次運行時會申請定位權限,如果當前App可以使用定位信息時,界面上會實時更新顯示當前的經緯度。

(2)如果當前 App 被禁止使用定位信息,界面上會出現一個提示按鈕,點擊后會自動跳轉到系統權限設置頁面。

2,準備工作

(1)RxCLLocationManagerDelegateProxy.swift

首先我們繼承 DelegateProxy 創建一個關于定位服務的代理委托,同時它還要遵守 DelegateProxyTypeCLLocationManagerDelegate 協議。

import CoreLocation
import RxSwift
import RxCocoa
 
extension CLLocationManager: HasDelegate {
    public typealias Delegate = CLLocationManagerDelegate
}
 
public class RxCLLocationManagerDelegateProxy
    : DelegateProxy<CLLocationManager, CLLocationManagerDelegate>
    , DelegateProxyType , CLLocationManagerDelegate {
     
    public init(locationManager: CLLocationManager) {
        super.init(parentObject: locationManager,
                   delegateProxy: RxCLLocationManagerDelegateProxy.self)
    }
     
    public static func registerKnownImplementations() {
        self.register { RxCLLocationManagerDelegateProxy(locationManager: $0) }
    }
     
    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)
    }
     
    deinit {
        self.didUpdateLocationsSubject.on(.completed)
        self.didFailWithErrorSubject.on(.completed)
    }
}

(2)CLLocationManager+Rx.swift

接著我們對 CLLocationManager 進行Rx 擴展,作用是將CLLocationManager與前面創建的代理委托關聯起來,將定位相關的 delegate 方法轉為可觀察序列。

注意:下面代碼中將 methodInvoked方法替換成 sentMessage 其實也可以,它們的區別可以看另一篇文章:

import CoreLocation
import RxSwift
import RxCocoa
 
extension Reactive where Base: CLLocationManager {
     
    /**
     Reactive wrapper for `delegate`.
      
     For more information take a look at `DelegateProxyType` protocol documentation.
     */
    public var delegate: DelegateProxy<CLLocationManager, CLLocationManagerDelegate> {
        return RxCLLocationManagerDelegateProxy.proxy(for: base)
    }
     
    // MARK: Responding to Location Events
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didUpdateLocations: Observable<[CLLocation]> {
        return RxCLLocationManagerDelegateProxy.proxy(for: base)
            .didUpdateLocationsSubject.asObservable()
    }
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didFailWithError: Observable<Error> {
        return RxCLLocationManagerDelegateProxy.proxy(for: base)
            .didFailWithErrorSubject.asObservable()
    }
     
    #if os(iOS) || os(macOS)
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didFinishDeferredUpdatesWithError: Observable<Error?> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didFinishDeferredUpdatesWithError:)))
            .map { a in
                return try castOptionalOrThrow(Error.self, a[1])
        }
    }
    #endif
     
    #if os(iOS)
     
    // MARK: Pausing Location Updates
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didPauseLocationUpdates: Observable<Void> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManagerDidPauseLocationUpdates(_:)))
            .map { _ in
                return ()
        }
    }
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didResumeLocationUpdates: Observable<Void> {
        return delegate.methodInvoked( #selector(CLLocationManagerDelegate
            .locationManagerDidResumeLocationUpdates(_:)))
            .map { _ in
                return ()
        }
    }
     
    // MARK: Responding to Heading Events
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didUpdateHeading: Observable<CLHeading> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didUpdateHeading:)))
            .map { a in
                return try castOrThrow(CLHeading.self, a[1])
        }
    }
     
    // MARK: Responding to Region Events
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didEnterRegion: Observable<CLRegion> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didEnterRegion:)))
            .map { a in
                return try castOrThrow(CLRegion.self, a[1])
        }
    }
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didExitRegion: Observable<CLRegion> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didExitRegion:)))
            .map { a in
                return try castOrThrow(CLRegion.self, a[1])
        }
    }
     
    #endif
     
    #if os(iOS) || os(macOS)
     
    /**
     Reactive wrapper for `delegate` message.
     */
    @available(OSX 10.10, *)
    public var didDetermineStateForRegion: Observable<(state: CLRegionState,
        region: CLRegion)> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didDetermineState:for:)))
            .map { a in
                let stateNumber = try castOrThrow(NSNumber.self, a[1])
                let state = CLRegionState(rawValue: stateNumber.intValue)
                    ?? CLRegionState.unknown
                let region = try castOrThrow(CLRegion.self, a[2])
                return (state: state, region: region)
        }
    }
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var monitoringDidFailForRegionWithError:
        Observable<(region: CLRegion?, error: Error)> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:monitoringDidFailFor:withError:)))
            .map { a in
                let region = try castOptionalOrThrow(CLRegion.self, a[1])
                let error = try castOrThrow(Error.self, a[2])
                return (region: region, error: error)
        }
    }
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didStartMonitoringForRegion: Observable<CLRegion> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didStartMonitoringFor:)))
            .map { a in
                return try castOrThrow(CLRegion.self, a[1])
        }
    }
     
    #endif
     
    #if os(iOS)
     
    // MARK: Responding to Ranging Events
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didRangeBeaconsInRegion: Observable<(beacons: [CLBeacon],
        region: CLBeaconRegion)> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didRangeBeacons:in:)))
            .map { a in
                let beacons = try castOrThrow([CLBeacon].self, a[1])
                let region = try castOrThrow(CLBeaconRegion.self, a[2])
                return (beacons: beacons, region: region)
        }
    }
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var rangingBeaconsDidFailForRegionWithError:
        Observable<(region: CLBeaconRegion, error: Error)> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:rangingBeaconsDidFailFor:withError:)))
            .map { a in
                let region = try castOrThrow(CLBeaconRegion.self, a[1])
                let error = try castOrThrow(Error.self, a[2])
                return (region: region, error: error)
        }
    }
     
    // MARK: Responding to Visit Events
     
    /**
     Reactive wrapper for `delegate` message.
     */
    @available(iOS 8.0, *)
    public var didVisit: Observable<CLVisit> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didVisit:)))
            .map { a in
                return try castOrThrow(CLVisit.self, a[1])
        }
    }
     
    #endif
     
    // MARK: Responding to Authorization Changes
     
    /**
     Reactive wrapper for `delegate` message.
     */
    public var didChangeAuthorizationStatus: Observable<CLAuthorizationStatus> {
        return delegate.methodInvoked(#selector(CLLocationManagerDelegate
            .locationManager(_:didChangeAuthorization:)))
            .map { a in
                let number = try castOrThrow(NSNumber.self, a[1])
                return CLAuthorizationStatus(rawValue: Int32(number.intValue))
                    ?? .notDetermined
        }
    }
}
 
 
fileprivate 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
}
 
fileprivate func castOptionalOrThrow<T>(_ resultType: T.Type,
                                        _ object: Any) throws -> T? {
    if NSNull().isEqual(object) {
        return nil
    }
     
    guard let returnValue = object as? T else {
        throw RxCocoaError.castingError(object: object, targetType: resultType)
    }
     
    return returnValue
}

(3)GeolocationService.swift

雖然現在我們已經可以直接 CLLocationManagerrx 擴展方法獲取位置信息了。但為了更加方便使用,我們這里對 CLLocationManager 再次進行封裝,定義一個地理定位的 service 層,作用如下:

  • 自動申請定位權限,以及授權判斷。
  • 自動開啟定位服務更新。
  • 自動實現經緯度數據的轉換。
import CoreLocation
import RxSwift
import RxCocoa
 
//地理定位服務層
class GeolocationService {
    //單例模式
    static let instance = GeolocationService()
     
    //定位權限序列
    private (set) var authorized: Driver<Bool>
     
    //經緯度信息序列
    private (set) var location: Driver<CLLocationCoordinate2D>
     
    //定位管理器
    private let locationManager = CLLocationManager()
     
    private init() {
         
        //更新距離
        locationManager.distanceFilter = kCLDistanceFilterNone
        //設置定位精度
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
         
        //獲取定位權限序列
        authorized = Observable.deferred { [weak locationManager] in
                let status = CLLocationManager.authorizationStatus()
                guard let locationManager = locationManager else {
                    return Observable.just(status)
                }
                return locationManager
                    .rx.didChangeAuthorizationStatus
                    .startWith(status)
            }
            .asDriver(onErrorJustReturn: CLAuthorizationStatus.notDetermined)
            .map {
                switch $0 {
                case .authorizedAlways:
                    return true
                default:
                    return false
                }
        }
         
        //獲取經緯度信息序列
        location = locationManager.rx.didUpdateLocations
            .asDriver(onErrorJustReturn: [])
            .flatMap {
                return $0.last.map(Driver.just) ?? Driver.empty()
            }
            .map { $0.coordinate }
         
        //發送授權申請
        locationManager.requestAlwaysAuthorization()
        //允許使用定位服務的話,開啟定位服務更新
        locationManager.startUpdatingLocation()
    }
}

3,使用樣例

(1)要獲取定位信息,首先我們需要在 info.plist 里加入相關的定位描述:

  • Privacy - Location Always and When In Use Usage Description:我們需要通過您的地理位置信息獲取您周邊的相關數據
  • Privacy - Location When In Use Usage Description:我們需要通過您的地理位置信息獲取您周邊的相關數據

(2)Main.storyboard

StoryBoard 中添加一個LabelButton,分別用來顯示經緯度信息,以及沒有權限時的提示。并將它們與代碼做 @IBOutlet 綁定。

(3)UILabel+Rx.swift

為了能讓 Label 直接綁定顯示經緯度信息,這里對其做個擴展。

import RxSwift
import RxCocoa
import CoreLocation
 
//UILabel的Rx擴展
extension Reactive where Base: UILabel {
    //實現CLLocationCoordinate2D經緯度信息的綁定顯示
    var coordinates: Binder<CLLocationCoordinate2D> {
        return Binder(base) { label, location in
            label.text = "經度: \(location.longitude)\n緯度: \(location.latitude)"
        }
    }
}

(4)ViewController.swift
主視圖控制器代碼如下,可以看到我們獲取定位信息變得十分簡單。

import UIKit
import RxSwift
import RxCocoa
 
class ViewController: UIViewController {
     
    @IBOutlet weak private var button: UIButton!
    @IBOutlet weak var label: UILabel!
 
    let disposeBag = DisposeBag()
     
    override func viewDidLoad() {
        super.viewDidLoad()
         
        //獲取地理定位服務
        let geolocationService = GeolocationService.instance
         
        //定位權限綁定到按鈕上(是否可見)
        geolocationService.authorized
            .drive(button.rx.isHidden)
            .disposed(by: disposeBag)
         
        //經緯度信息綁定到label上顯示
        geolocationService.location
            .drive(label.rx.coordinates)
            .disposed(by: disposeBag)
         
        //按鈕點擊
        button.rx.tap
            .bind { [weak self] _ -> Void in
                self?.openAppPreferences()
            }
            .disposed(by: disposeBag)
    }
     
    //跳轉到應有偏好的設置頁面
    private func openAppPreferences() {
        UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!)
    }
}

RxSwift使用詳解系列
原文出自:www.hangge.com轉載請保留原文鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • RxSwift_v1.0筆記——13 Intermediate RxCocoa 這章將學習一些高級的RxCocoa...
    大灰很閱讀 667評論 1 1
  • 24號可以說是成都之旅的開始,一大早起來看到院子里下著大雪,心情很暢快,吃了媽媽的早餐就送我到車站,為了趕時間,中...
    牛小保閱讀 107評論 0 0
  • 清晨, 綠樹掩映的森林, 萬籟俱寂。 調皮的黎明酣睡在床,久久不肯起來。 窗明幾凈的教室里, 手中的筆,沙沙不停。...
    劉婧_閱讀 280評論 15 17
  • 今天是早睡早起的第一天 忍不住看了微博 到現在第八章還是沒有看完 你和我只說了一句早啦 我以為和別人聊天加語氣詞都...
    鮫小水閱讀 280評論 0 0