????委托(delegate
) iOS 開發(fā)中十分常見韩脏。不管是使用系統(tǒng)自帶的庫垒探,還是一些第三方組件時苍日,我們總能看到 delegate
的身影贷痪。使用 delegate
可以實現(xiàn)代碼的松耦合腮出,減少代碼復雜度帖鸦。但如果我們項目中使用 RxSwift
,那么原先的 delegate
方式與我們鏈式編程方式就不相稱了胚嘲。
????解決辦法就是將代理方法進行一層 Rx
封裝作儿,這樣做不僅會減少許多不必要的工作(比如原先需要遵守不同的代理,并且要實現(xiàn)相應的代理方法)馋劈,還會使得代碼的聚合度更高攻锰,更加符合響應式編程的規(guī)范。
????其實在 RxCocoa 源碼中我們也可以發(fā)現(xiàn)妓雾,它已經(jīng)對標準的 Cocoa
做了大量的封裝(比如 tableView
的 itemSelected
)娶吞。下面我將通過樣例演示如何將代理方法進行 Rx
化。
一械姻、對 Delegate進行Rx封裝原理
1妒蛇,DelegateProxy
(1)DelegateProxy
是代理委托,我們可以將它看作是代理的代理。
(2)DelegateProxy
的作用是做為一個中間代理绣夺,他會先把系統(tǒng)的 delegate
對象保存一份吏奸,然后攔截 delegate
的方法。也就是說在每次觸發(fā) delegate
方法之前陶耍,會先調(diào)用 DelegateProxy
這邊對應的方法奋蔚,我們可以在這里發(fā)射序列給多個訂閱者。
2烈钞,流程圖
這里以 UIScrollView
為例泊碑,Delegate proxy
便是其代理委托,它遵守 DelegateProxyType
與 UIScrollViewDelegate
毯欣,并能響應 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)建一個關于定位服務的代理委托,同時它還要遵守 DelegateProxyType
和 CLLocationManagerDelegate
協(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)可以直接 CLLocationManager
的 rx
擴展方法獲取位置信息了。但為了更加方便使用冷溃,我們這里對 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
中添加一個Label
和 Button
牵咙,分別用來顯示經(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)!)
}
}