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

????委托(delegate) iOS 開發(fā)中十分常見韩脏。不管是使用系統(tǒng)自帶的庫垒探,還是一些第三方組件時苍日,我們總能看到 delegate 的身影贷痪。使用 delegate 可以實現(xiàn)代碼的松耦合腮出,減少代碼復雜度帖鸦。但如果我們項目中使用 RxSwift,那么原先的 delegate 方式與我們鏈式編程方式就不相稱了胚嘲。

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

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

一械姻、對 Delegate進行Rx封裝原理

1妒蛇,DelegateProxy

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

(2)DelegateProxy 的作用是做為一個中間代理绣夺,他會先把系統(tǒng)的 delegate 對象保存一份吏奸,然后攔截 delegate 的方法。也就是說在每次觸發(fā) delegate 方法之前陶耍,會先調(diào)用 DelegateProxy 這邊對應的方法奋蔚,我們可以在這里發(fā)射序列給多個訂閱者。

2烈钞,流程圖

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

/***
  
 +-------------------------------------------+
 |                                           |
 | 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可以使用定位信息時,界面上會實時更新顯示當前的經(jīng)緯度偎巢。

(2)如果當前 App 被禁止使用定位信息蔼夜,界面上會出現(xiàn)一個提示按鈕,點擊后會自動跳轉(zhuǎn)到系統(tǒng)權限設置頁面压昼。

2求冷,準備工作

(1)RxCLLocationManagerDelegateProxy.swift

首先我們繼承 DelegateProxy 創(chuàng)建一個關于定位服務的代理委托,同時它還要遵守 DelegateProxyTypeCLLocationManagerDelegate 協(xié)議窍霞。

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與前面創(chuàng)建的代理委托關聯(lián)起來,將定位相關的 delegate 方法轉(zhuǎn)為可觀察序列但金。

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

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

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

  • 自動申請定位權限似枕,以及授權判斷盖淡。
  • 自動開啟定位服務更新。
  • 自動實現(xiàn)經(jīng)緯度數(shù)據(jù)的轉(zhuǎn)換凿歼。
import CoreLocation
import RxSwift
import RxCocoa
 
//地理定位服務層
class GeolocationService {
    //單例模式
    static let instance = GeolocationService()
     
    //定位權限序列
    private (set) var authorized: Driver<Bool>
     
    //經(jīng)緯度信息序列
    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
                }
        }
         
        //獲取經(jīng)緯度信息序列
        location = locationManager.rx.didUpdateLocations
            .asDriver(onErrorJustReturn: [])
            .flatMap {
                return $0.last.map(Driver.just) ?? Driver.empty()
            }
            .map { $0.coordinate }
         
        //發(fā)送授權申請
        locationManager.requestAlwaysAuthorization()
        //允許使用定位服務的話褪迟,開啟定位服務更新
        locationManager.startUpdatingLocation()
    }
}

3冗恨,使用樣例

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

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

(2)Main.storyboard

StoryBoard 中添加一個LabelButton牵咙,分別用來顯示經(jīng)緯度信息派近,以及沒有權限時的提示。并將它們與代碼做 @IBOutlet 綁定洁桌。

(3)UILabel+Rx.swift

為了能讓 Label 直接綁定顯示經(jīng)緯度信息,這里對其做個擴展侯嘀。

import RxSwift
import RxCocoa
import CoreLocation
 
//UILabel的Rx擴展
extension Reactive where Base: UILabel {
    //實現(xiàn)CLLocationCoordinate2D經(jīng)緯度信息的綁定顯示
    var coordinates: Binder<CLLocationCoordinate2D> {
        return Binder(base) { label, location in
            label.text = "經(jīng)度: \(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)
         
        //經(jīng)緯度信息綁定到label上顯示
        geolocationService.location
            .drive(label.rx.coordinates)
            .disposed(by: disposeBag)
         
        //按鈕點擊
        button.rx.tap
            .bind { [weak self] _ -> Void in
                self?.openAppPreferences()
            }
            .disposed(by: disposeBag)
    }
     
    //跳轉(zhuǎn)到應有偏好的設置頁面
    private func openAppPreferences() {
        UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!)
    }
}

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末戒幔,一起剝皮案震驚了整個濱河市吠谢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诗茎,老刑警劉巖工坊,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異敢订,居然都是意外死亡王污,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門楚午,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昭齐,“玉大人,你說我怎么就攤上這事矾柜≮寮荩” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵怪蔑,是天一觀的道長里覆。 經(jīng)常有香客問我,道長缆瓣,這世上最難降的妖魔是什么喧枷? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮捆愁,結果婚禮上割去,老公的妹妹穿的比我還像新娘。我一直安慰自己昼丑,他們只是感情好呻逆,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著菩帝,像睡著了一般咖城。 火紅的嫁衣襯著肌膚如雪茬腿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天宜雀,我揣著相機與錄音切平,去河邊找鬼。 笑死辐董,一個胖子當著我的面吹牛悴品,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播简烘,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼苔严,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了孤澎?” 一聲冷哼從身側(cè)響起届氢,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎覆旭,沒想到半個月后退子,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡型将,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年寂祥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茶敏。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡壤靶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惊搏,到底是詐尸還是另有隱情贮乳,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布恬惯,位于F島的核電站向拆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏酪耳。R本人自食惡果不足惜浓恳,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碗暗。 院中可真熱鬧颈将,春花似錦、人聲如沸言疗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽噪奄。三九已至死姚,卻和暖如春人乓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背都毒。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工色罚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人账劲。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓戳护,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瀑焦。 傳聞我的和親對象是個殘疾皇子姑尺,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

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