Swift - RxSwift的使用详解58(DelegateProxy样例1:获取地理定位信息 )
委托(delegate)在 iOS 开发中十分常见。不管是使用系统自带的库,还是一些第三方组件时,我们总能看到 delegate 的身影。使用 delegate 可以实现代码的松耦合,减少代码复杂度。但如果我们项目中使用 RxSwift,那么原先的 delegate 方式与我们链式编程方式就不相称了。
(2)CLLocationManager+Rx.swift
(3)GeolocationService.swift
(4)ViewController.swift
主视图控制器代码如下,可以看到我们获取定位信息变得十分简单。
解决办法就是将代理方法进行一层 Rx 封装,这样做不仅会减少许多不必要的工作(比如原先需要遵守不同的代理,并且要实现相应的代理方法),还会使得代码的聚合度更高,更加符合响应式编程的规范。
其实在 RxCocoa 源码中我们也可以发现,它已经对标准的 Cocoa 做了大量的封装(比如 tableView 的 itemSelected)。下面我将通过样例演示如何将代理方法进行 Rx 化。
一、对 Delegate 进行 Rx 封装原理
1,DelegateProxy
(1)DelegateProxy 是代理委托,我们可以将它看作是代理的代理。
(2)DelegateProxy 的作用是做为一个中间代理,他会先把系统的 delegate 对象保存一份,然后拦截 delegate 的方法。也就是说在每次触发 delegate 方法之前,会先调用 DelegateProxy 这边对应的方法,我们可以在这里发射序列给多个订阅者。
2,流程图
这里以 UIScrollView 为例,Delegate proxy 便是其代理委托,它遵守 DelegateProxyType 与 UIScrollViewDelegate,并能响应 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 创建一个关于定位服务的代理委托,同时它还要遵守 DelegateProxyType 和 CLLocationManagerDelegate 协议。
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
虽然现在我们已经可以直接 CLLocationManager 的 rx 扩展方法获取位置信息了。但为了更加方便使用,我们这里对 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 中添加一个 Label 和 Button,分别用来显示经纬度信息,以及没有权限时的提示。并将它们与代码做 @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)!) } }