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阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • RxSwift_v1.0笔记——13 Intermediate RxCocoa 这章将学习一些高级的RxCocoa...
    大灰很阅读 648评论 1 1
  • 24号可以说是成都之旅的开始,一大早起来看到院子里下着大雪,心情很畅快,吃了妈妈的早餐就送我到车站,为了赶时间,中...
    牛小保阅读 103评论 0 0
  • 清晨, 绿树掩映的森林, 万籁俱寂。 调皮的黎明酣睡在床,久久不肯起来。 窗明几净的教室里, 手中的笔,沙沙不停。...
    刘婧_阅读 274评论 15 17
  • 今天是早睡早起的第一天 忍不住看了微博 到现在第八章还是没有看完 你和我只说了一句早啦 我以为和别人聊天加语气词都...
    鲛小水阅读 270评论 0 0