开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(一)第三方框架使用

开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(一)第三方框架使用

1. SwiftHub项目简介

SwiftHub 是大神Khoren Markosyan 写的一个完全采用Rxswift + MVVM + Moya 的架构的项目,代码很精简,想学习MVVM架构的认真去研究这个项目的设计,对你以后的编程思想和习惯都会有很大的帮助。(点击这里下载:SwiftHub源码

SwiftHub项目简介

1.1 SwiftHub项目UI

UI页面1
SwiftHub UI2

SwiftHub UI3
SwiftHub UI4

1.2 SwiftHub项目代码结构

SwiftHub项目代码结构

2. SwiftHub项目编译,用到的第三方库简介

2.1 SwiftHub项目编译

下载源码后,进入SwiftHub-master主目录,先要下载安装第三方库,如果你cd SwiftHub-master/ 就直接执行pod install的话一般都会报错:

pod install的报错

分析报错原因不难看出,已经提示我们需要先pod repo update 一下更新你本地的cocos pod库。

pod repo update一下

可能有的小伙伴网速不太好,pod install一直更新不了,这里提供了一份我编译好的源码:链接:https://pan.baidu.com/s/1qwkjY_ZrgV9Y5yudiyVJdQ 密码:60t7

2.2 SwiftHub项目用到的第三方框架

  • 我只能惊叹,哇塞,怎么用了这么多第三方框架啊,我个人观点是不太主张用太多第三方框架,能自己实现都自己实现,除非要实现的功能必须要用第三方框架。因为第三方框架会大大增加我们ipa包的大小,对于ipa大小有要求的是个灾难,例如之前我们有一个项目使用Realm 作为DB框架,但是发现这个框架实在是太占内存了足足有将近90MB,而我只是想里面一个小小的数据库存储相关的代码,后面改成WCDB.swift框架,这个框架只有2MB左右。

  • 下面我们先来看一下SwiftHub 项目用到的第三方框架吧:

    SwiftHub用到的第三方库

# Uncomment the next line to define a global platform for your project
platform :ios, '11.0'

use_frameworks!
inhibit_all_warnings!

target 'SwiftHub' do
    # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
    # Pods for SwiftHub

    # Networking
    pod 'Moya/RxSwift', '14.0.0-beta.2'  # https://github.com/Moya/Moya
    pod 'Apollo', '0.19.0'  # https://github.com/apollographql/apollo-ios

    # Rx Extensions
    pod 'RxDataSources', '~> 4.0'  # https://github.com/RxSwiftCommunity/RxDataSources
    pod 'RxSwiftExt', '~> 5.0'  # https://github.com/RxSwiftCommunity/RxSwiftExt
    pod 'NSObject+Rx', '~> 5.0'  # https://github.com/RxSwiftCommunity/NSObject-Rx
    pod 'RxViewController', '~> 1.0'  # https://github.com/devxoul/RxViewController
    pod 'RxGesture', '~> 3.0'  # https://github.com/RxSwiftCommunity/RxGesture
    pod 'RxOptional', '~> 4.0'  # https://github.com/RxSwiftCommunity/RxOptional
    pod 'RxTheme', '~> 4.0'  # https://github.com/RxSwiftCommunity/RxTheme
    #pod 'RxAnimated', '~> 0.4'  # https://github.com/RxSwiftCommunity/RxAnimated

    # JSON Mapping
    #pod 'ObjectMapper', :git => 'https://github.com/kajensen/ObjectMapper.git'  # https://github.com/Hearst-DD/ObjectMapper
    pod 'Moya-ObjectMapper/RxSwift', :git => 'https://github.com/khoren93/Moya-ObjectMapper.git', :branch => 'moya14'  # https://github.com/ivanbruel/Moya-ObjectMapper

    # Image
    pod 'Kingfisher', '~> 5.0'  # https://github.com/onevcat/Kingfisher

    # Date
    pod 'DateToolsSwift', '~> 4.0'  # https://github.com/MatthewYork/DateTools
    pod 'SwiftDate', '~> 6.0'  # https://github.com/malcommac/SwiftDate

    # Tools
    pod 'R.swift', '~> 5.0'  # https://github.com/mac-cain13/R.swift
    pod 'SwiftLint', '0.37.0'  # https://github.com/realm/SwiftLint

    # Keychain
    pod 'KeychainAccess', '~> 4.0'  # https://github.com/kishikawakatsumi/KeychainAccess

    # Fabric
    pod 'Fabric'
    pod 'Crashlytics'

    # UI
    pod 'NVActivityIndicatorView', '~> 4.0'  # https://github.com/ninjaprox/NVActivityIndicatorView
    pod 'ImageSlideshow/Kingfisher', '~> 1.8'  # https://github.com/zvonicek/ImageSlideshow
    pod 'DZNEmptyDataSet', '~> 1.0'  # https://github.com/dzenbot/DZNEmptyDataSet
    pod 'Hero', '~> 1.5.0'  # https://github.com/lkzhao/Hero
    pod 'Localize-Swift', '~> 3.0'  # https://github.com/marmelroy/Localize-Swift
    pod 'RAMAnimatedTabBarController', '~> 5.0'  # https://github.com/Ramotion/animated-tab-bar
    pod 'AcknowList', '~> 1.8'  # https://github.com/vtourraine/AcknowList
    pod 'KafkaRefresh', '~> 1.0'  # https://github.com/OpenFeyn/KafkaRefresh
    pod 'WhatsNewKit', '~> 1.0'  # https://github.com/SvenTiigi/WhatsNewKit
    pod 'Highlightr', '~> 2.0'  # https://github.com/raspu/Highlightr
    pod 'DropDown', '~> 2.0'  # https://github.com/AssistoLab/DropDown
    pod 'Toast-Swift', '~> 5.0'  # https://github.com/scalessec/Toast-Swift
    pod 'HMSegmentedControl', '~> 1.0'  # https://github.com/HeshamMegid/HMSegmentedControl
    pod 'FloatingPanel', '~> 1.0'  # https://github.com/SCENEE/FloatingPanel
    pod 'MessageKit', '~> 3.0'  # https://github.com/MessageKit/MessageKit
    pod 'MultiProgressView', '~> 1.0'  # https://github.com/mac-gallagher/MultiProgressView

    # Keyboard
    pod 'IQKeyboardManagerSwift', '~> 6.0'  # https://github.com/hackiftekhar/IQKeyboardManager

    # Auto Layout
    pod 'SnapKit', '~> 5.0'  # https://github.com/SnapKit/SnapKit

    # Code Quality
    pod 'FLEX', :git => 'https://github.com/khoren93/FLEX.git', :branch => 'remove_private_api'  # https://github.com/Flipboard/FLEX
    pod 'SwifterSwift', '~> 5.0'  # https://github.com/SwifterSwift/SwifterSwift
    pod 'BonMot', '~> 5.0'  # https://github.com/Rightpoint/BonMot

    # Logging
    pod 'CocoaLumberjack/Swift', '~> 3.0'  # https://github.com/CocoaLumberjack/CocoaLumberjack

    # Analytics
    # https://github.com/devxoul/Umbrella
    pod 'Umbrella/Mixpanel', '~> 0.8'
    pod 'Umbrella/Firebase'
    pod 'Mixpanel', '~> 3.0'  # https://github.com/mixpanel/mixpanel-iphone
    pod 'Firebase/Analytics'

    # Ads
    pod 'Firebase/AdMob'
    pod 'Google-Mobile-Ads-SDK', '7.52.0'
    
    target 'SwiftHubTests' do
        inherit! :search_paths
        # Pods for testing
        pod 'Quick', '~> 2.0'  # https://github.com/Quick/Quick
        pod 'Nimble', '~> 8.0'  # https://github.com/Quick/Nimble
        #pod 'RxNimble', '~> 4.0'  # https://github.com/RxSwiftCommunity/RxNimble
        pod 'RxAtomic', :modular_headers => true
        pod 'RxBlocking'  # https://github.com/ReactiveX/RxSwift
        pod 'Firebase'
    end
end

target 'SwiftHubUITests' do
    inherit! :search_paths
    # Pods for testing
end


post_install do |installer|
    # Cocoapods optimization, always clean project after pod updating
    Dir.glob(installer.sandbox.target_support_files_root + "Pods-*/*.sh").each do |script|
        flag_name = File.basename(script, ".sh") + "-Installation-Flag"
        folder = "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
        file = File.join(folder, flag_name)
        content = File.read(script)
        content.gsub!(/set -e/, "set -e\nKG_FILE=\"#{file}\"\nif [ -f \"$KG_FILE\" ]; then exit 0; fi\nmkdir -p \"#{folder}\"\ntouch \"$KG_FILE\"")
        File.write(script, content)
    end
    
    # Enable tracing resources
    installer.pods_project.targets.each do |target|
      if target.name == 'RxSwift'
        target.build_configurations.each do |config|
          if config.name == 'Debug'
            config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES']
          end
        end
      end
    end
end

  • 接下来,我们来分析这些第三方库都是用来干什么,说不定哪天你的项目也可以用到呢。

2.2.1 网络库

用到的网络库
2.2.1.1 Alamofire

AlamofireAFNetwork 是一对兄弟,是出自同一个公司的产品, 它是一个很好的Swift编写的网络框架库,提供了HTTP相关接口,能轻松实现链式请求和响应,能实现文件上传,下载,断点续传,后台下载等功能。

安装方式:

  • CocoaPods安装:pod 'Alamofire', '~> 5.1'
  • Carthage 安装:github "Alamofire/Alamofire" ~> 5.1

安装环境要求:

iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
Xcode 11+
Swift 5.1+

提供的功能特性:

  1. 可链请求/响应方法
  2. URL / JSON参数编码
  3. 上传文件/数据/流/ MultipartFormData
  4. 使用请求或简历数据下载文件
  5. 身份验证与URLCredential
  6. HTTP响应验证
  7. 上传和下载进度闭包与进度
  8. cURL命令输出
  9. 动态调整和重试请求
  10. TLS证书和公钥固定
  11. 网络可达性
  12. 全面的单元和集成测试覆盖
  13. 完整的文档

为了让 Alamofire更专注于处理网络相关的事情,Alamofire软件基金会已经创建了额外的组件库来为Alamofire生态系统带来额外的功能。如:AlamofireImage库和 AlamofireNetworkActivityIndicator

  1. AlamofireImage: 一个图像库,包括图像响应序列化器,UIImage和UIImageView扩展,自定义图像过滤器,一个自动清除内存缓存和一个基于优先级的图像下载系统。
  2. AlamofireNetworkActivityIndicator : 使用Alamofire控制iOS上的网络活动指示器的可见性。它包含可配置的延迟计时器,以帮助减少闪烁,并支持不受Alamofire管理的URLSession实例。
  • Alamofire框架结构图:

    Alamofire框架结构图

  • 关于Alamofire的使用可以参考我的一些博客:

Alamofire学习(一)网络基础

Alamofire(二)URLSession

Alamofire(三)后台下载原理

  • 网络请求步骤:
  1. 设置请求url
  2. 设置URLRequest对象,配置请求相关信息
  3. 创建会话配置URLSessionConfiguration
  4. 创建会话URLSession
  5. 创建任务和设置请求回调,并发起请求

简单请求代码:

func responseData() {
    let url = "http://onapp.kongyulu.top/public/?s=api/test/list"
    Alamofire.request(url).responseJSON {
        (response) in
        switch response.result{
        case .success(let json):
            print("json:\(json)")
            let dict = json as! Dictionary<String, Any>
            let list = dict["data"] as! Array<AnyObject>
            guard let result = [UserModel1].deserialize(from: list) else{return}
            self.observable.onNext(result as [Any])
            break
        case .failure(let error):
            print("error:\(error)")
            break
        }
    }
}

其中URLSessionConfiguration提供了框架的相关配置:

主要提供了以下3中方式:

  1. default:默认模式,常用模式,在该模式下系统会创建持久化缓存,并在用户的钥匙串中保存证书
  2. ephemeral:不支持持久性存储,所有内容的会随着session的生命周期结束而释放
    background:与default模式类似,在该模式下会创建一个独立线程来传输网络请求数据,可以在后台乃至APP关闭的时候也可以进行数据传输
  • 创建会话:
let configuration = URLSessionConfiguration.background(withIdentifier: "request_id")
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
session.dataTask(with: request) { (data, response, error) in
    do {
        let list =  try JSONSerialization.jsonObject(with: data!, options: .allowFragments)
        print(list)
    }catch{
        print(error)
    }
}.resume()

此外还提供了很多属性来按需配置

  • 常规属性:
  1. identifier:配置对象的后台会话标识符
  2. httpAdditionalHeaders:与请求一起发送的附加头文件字典
  3. networkServiceType:网络服务的类型
  4. allowsCellularAccess:一个布尔值,用于是否应通过蜂窝网络进行连接
  5. timeoutIntervalForRequest:等待附加数据的超时时间
  6. timeoutIntervalForResource:资源请求允许的最大时间范围
  7. sharedContainerIdentifier:应将后台URL会话中的文件下载到的共享容器的标识符
  8. waitsForConnectivity:一个布尔值,指示会话是否应等待连接变为可用还是立即失败
  • 设置Cookie策略:
  1. httpCookieAcceptPolicy:决定何时接受cookie的策略常量
  2. httpShouldSetCookies:一个布尔值,确定请求是否包含来自cookie存储区的cookie
  3. httpCookieStorage:用于会话中存储cookie的cookie存储区
  4. HTTPCookie:该对象为不可变对象,从包含cookie属性的字典初始化,支持两个不同的cookie版本,v0、v1
  • 设置安全策略:
  1. TLS协议:用于在两个通信应用程序之间提供保密性和数据完整性
  2. tlsMaximumSupportedProtocol:在此会话中建立连接时客户端应请求的最大TLS协议版本
  3. tlsMinimumSupportedProtocol:协议协商期间应接受的最小TLS协议
  4. urlCredentialStorage:为身份验证提供凭据的凭据存储区
  • 设置缓存策略:
  1. urlCache:用于为会话中的请求提供缓存响应的URL缓存
  2. requestCachePolicy:决定何时从缓存中返回响应的预定义常量
  • 支持后台模式:
  1. sessionSendsLaunchEvents:一个布尔值,指示当传输完成时,应用程序应在后台恢复还是启动
  2. isDiscretionary:一个布尔值,用于确定后台任务是否可以由系统自行安排已获得最佳性能
  3. shouldUseExtendedBackgroundIdleMode:一个布尔值,指示当应用程序转移到后台时是否应保持TCP连接打开
  • 支持自定义协议
  1. protocolClasses:在会话中处理请求的额外协议子类的数组
  2. URLProtocol:该对象用来处理加载协议特定URL数据
  • 支持多路径TCP:
  1. multipathServiceType:指定用于通过Wi-Fi和蜂窝接口传输数据的多路径TCP连接策略的服务类型
  • 设置HTTP策略和代理属性:
  1. httpMaximumConnectionsPerHost:同时连接到给定主机的最大数量
  2. httpShouldUsePipelining:一个布尔值,用于确定会话是否使用HTTP流水线
  3. connectionProxyDictionary:包含相关要在此会话中使用的代理信息的字典
  • 支持连接更改:
  1. waitsForConnectivity:一个布尔值,指示会话应等待连接可用还是立即失败
2.2.1.2 Rxswift

Rxswift家族提供非常好的函数响应式编程框架,使用Rxswift编写代码可以让代码变得非常简洁,逻辑清晰,如果配合Moya + Rxswift + MVVM架构,真的是很完美,这个开源项目SwiftHub就是这样的一个完美的项目。

ReactiveX(简写:Rx)是一个可以帮助我们简化异步编程的框架。而 RxSwift 是 Rx 的 Swift 版本。除了 RxSwift,还有 RxJava、RxJS、Rx.Net 等,对应的OC 版本则是 RAC(ReactiveCocoa),这里是 RxSwift 的 github 地址 ,已经有了将近 18.2K 颗星了。

Rxswift简洁图
Rxswift构成图

RxSwift: RxSwift的核心,提供由ReactiveX(主要)定义的Rx标准。它没有其他依赖项。
RxCocoa: 为一般的iOS/macOS/watchOS & tvOS应用程序开发提供特定于cocoa的功能,如绑定、特性等。它同时依赖于RxSwift和RxRelay。
RxRelay: 提供发布中继和行为中继,这两个简单的主题包装器。这取决于RxSwift。
RxTest and RxBlocking: 为基于rx的系统提供测试功能。这取决于RxSwift。

  • 关于Rxswift的使用可以参考我的一些博客:

Rxswift(一)函数响应式编程思想

RxSwift (二)序列核心逻辑分析

RxSwift (三)Observable的创建,订阅,销毁

RxSwift(四)高阶函数

RxSwift(五)(Rxswift对比swift,oc用法)

Rxswift (六)销毁者Dispose源码分析

RxSwift(七)Rxswift对比swift用法

RxSwift (十) 基础使用篇 1- 序列,订阅,销毁

RxSwift学习之十二 (基础使用篇 3- UI控件扩展)

Rxswift一些简单使用如下:

  • button点击事件:
//MARK: - RxSwift应用-button响应
func setupButton() {
    // 传统UI事件
    self.button.addTarget(self, action: #selector(didClickButton), for: .touchUpInside)

    // 这样的操作 - 不行啊!代码逻辑与事件逻辑分层
    self.button.rx.tap
        .subscribe(onNext: { [weak self] in
            print("点了,小鸡炖蘑菇")
            self?.view.backgroundColor = UIColor.orange   
        })
        .disposed(by: disposeBag) 
}
  • textfiled文本响应
//MARK: - RxSwift应用-textfiled
func setupTextFiled() {
    // 我们如果要对输入的文本进行操作 - 比如输入的的内容 然后我们获取里面的偶数
    // self.textFiled.delegate = self
    // 感觉是不是特别恶心
    // 下面我们来看看Rx
    self.textFiled.rx.text.orEmpty.changed.subscribe(onNext: { (text) in
        print("监听到了 - \(text)")
    }).disposed(by: disposeBag)

    self.textFiled.rx.text.bind(to: self.button.rx.title()).disposed(by: disposeBag)
}
  • scrollView使用
//MARK: - RxSwift应用-scrollView
func setupScrollerView() {
    scrollView.rx.contentOffset.subscribe(onNext: { [weak self] (content) in
        self?.view.backgroundColor = UIColor.init(red: content.y/255.0*0.8, green: content.y/255.0*0.3, blue: content.y/255.0*0.6, alpha: 1);
        print(content.y)
    }).disposed(by: disposeBag)
}
  • KVO
 //MARK: - RxSwift应用-KVO
func setupKVO() {
    // 系统KVO 还是比较麻烦的
    // person.addObserver(self, forKeyPath: "name", options: .new, context: nil)
    person.rx.observeWeakly(String.self, "name").subscribe(onNext: { (change) in
        print(change ?? "helloword")
    }).disposed(by: disposeBag)
}
  • 通知
//MARK: - 通知
func setupNotification(){
    NotificationCenter.default.rx
        .notification(UIResponder.keyboardWillShowNotification)
        .subscribe { (event) in
            print(event)
    }.disposed(by: disposeBag)
}
  • 手势
//MARK: - 手势
func setupGestureRecognizer(){
    let tap = UITapGestureRecognizer()
    self.label.addGestureRecognizer(tap)
    self.label.isUserInteractionEnabled = true
    tap.rx.event.subscribe { (event) in
        print("点了label")
    }.disposed(by: disposeBag)  
}
  • 网络请求
//MARK: - RxSwift应用-网络请求
func setupNextwork() {
    let url = URL(string: "https://www.baidu.com")
    URLSession.shared.rx.response(request: URLRequest(url: url!))
        .subscribe(onNext: { (response, data) in
            print("response ==== \(response)")
            print("data ===== \(data)")
        }, onError: { (error) in
            print("error ===== \(error)")
        }).disposed(by: disposeBag)
}
  • 定时器
//MARK: - RxSwift应用-timer定时器
func setupTimer() {
    timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
    timer.subscribe(onNext: { (num) in
        print("hello word \(num)")
    }).disposed(by: disposeBag)
}
2.2.1.3 Moya
  • 源码下载:Moya

Moya是一个网络抽象层,它在底层将Alamofire进行封装,对外提供更简洁的接口供开发者调用。在Objective-C中,大部分开发者会使用AFNetwork进行网络请求,当业务复杂一些时,会对AFNetwork进行二次封装,编写一个适用于自己项目的网络抽象层。在Objective-C中,有著名的YTKNetwork,它将AFNetworking封装成抽象父类,然后根据每一种不同的网络请求,都编写不同的子类,子类继承父类,来实现请求业务。Moya在项目层次中的地位,有点类似于YTKNetwork。可以看下图对比


Moya 扮演的角色

如果单纯把Moya等同于swift版的YTKNetwork,那就是比较错误的想法了。Moya的设计思路和YTKNetwork差距非常大。上面我在介绍YTKNetwork时在强调子类和父类,继承,是因为YTKNetwork是比较经典的利用OOP思想(面向对象)设计的产物。基于swift的Moya虽然也有使用到继承,但是它的整体上是以POP思想(Protocol Oriented Programming,面向协议编程)为主导的。

  • Moya的模块组成:
Moya的模块组成
  1. Providerprovider是一个提供网络请求服务的提供者。通过一些初始化配置之后,在外部可以直接用provider来发起request。
  2. Request:在使用Moya进行网络请求时,第一步需要进行配置,来生成一个Request。首先按照官方文档,创建一个枚举,遵守TargetType协议,并实现协议所规定的属性。为什么要创建枚举来遵守协议,枚举结合switch语句,使得API管理起来比较方便。
  3. 根据创建了一个遵守TargetType协议的名为Myservice的枚举,我们完成了如下几个变量的设置。
    baseURL
    path
    method
    sampleData
    task
    headers
  • Moya使用

import UIKit
import Moya
import RxCocoa
import Result
import SwiftyJSON

//初始rovider
let KApiProvider = MoyaProvider<KNetworkAPI>(plugins: [RequestLoadingPlugin()])

let K_Search_Base = "http://www.baid.com/search"

/** 请求的endpoints)**/
//请求分类
enum KNetworkAPI {
    case shareNavList:
    case shareList(pageSize: Int, pageNum: Int):
}
//请求配置
extension KNetworkAPI: TargetType {
    //服务器地址
    public var baseURL: URL {
        switch self {
        default:
            return URL(string: K_Search_Base)!
        }
    }
    
    //各个请求的具体路径
    public var path: String {
        switch self {
        case .shareNavList:
            return "manage/navigation/getNavigationList"
        default:
            return "default/list"
        }
    }
    
    //请求类型
    public var method: Moya.Method {
        switch self {
       
        default:
            return .get
        }
    }
    
    //请求任务事件(这里附带上参数)
    public var task: Task {
        switch self {
        case .shareNavList:
            return .requestPlain
       case .shareList(let pageSize, let pageNum):
            var params: [String: Any] = [:]
            params["pageSize"] = pageSize
            params["pageNum"] = pageNum
            return .requestParameters(parameters: params, encoding: URLEncoding.default)
        }
    }
    //是否执行Alamofire验证
    public var validate: Bool {
        return false
    }
    //这个就是做单元测试模拟的数据,
//    只会在单元测试文件中有作用
    public var sampleData: Data {
        return "{}".data(using: String.Encoding.utf8)!
    }

    //请求头
    public var headers: [String: String]? {
        switch self {
        default:
            return ["Content-type": "application/json"]
        }
    }
}


RequestLoadingPlugin 插件用来显示UI相关,捕获网络异常等操作,给出提示,代码如下:
```swift


import UIKit
import Foundation
import MBProgressHUD
import Moya
import Result

class RequestLoadingPlugin: PluginType {
    
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        print("prepare")
        var mRequest = request
        mRequest.timeoutInterval = 20
        return mRequest
    }
    func willSend(_ request: RequestType, target: TargetType) {
        print("开始请求")
        if SwiftIsShowHud == true {
            let keyViewController = UIApplication.shared.keyWindow?.rootViewController
            if (keyViewController != nil) {
                MBProgressHUD.showAdded(to: keyViewController!.view, animated: true)
            }
        }
        
    }
    
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        print("结束请求")
        let keyViewController = UIApplication.shared.keyWindow?.rootViewController
        if (keyViewController != nil) {
            MBProgressHUD.hide(for: keyViewController!.view, animated: true)
            //            MBProgressHUD.
        }
        
        
       guard case Result.failure(_) = result
        else {
            let respons = result.value
            let dic: Dictionary<String, Any>? =
                try? JSONSerialization.jsonObject(with: respons!.data, options: .mutableContainers) as! Dictionary<String, Any>
            
            if dic != nil {
                if dic?.keys.contains("status") == true {
                    if dic?["status"] as! Int == 11 || dic?["status"] as! Int == 12 {
                        print("Token 失效")
                    }
                }
                
                if dic?.keys.contains("code") == true {
                    if dic?["code"] as! Int == 11 || dic?["code"] as! Int == 12 {
                        print("Token 失效")
                    }
                }
            }
            return
        }
        let errorReason: String = (result.error?.errorDescription)!
        print("请求失败:\(errorReason)")
        var tip = ""
        if errorReason.contains("The Internet connection appears to be offline") {
            tip = "网络不给力,请检查您的网络"
        }else if errorReason.contains("Could not connect to the server") {
            tip = "无法连接服务器"
        }else {
           tip = "请求失败"
        }
        /// 使用tip文字 进行提示
    }
}


  • 调用代码如下:
import RxSwift
import RxCocoa
import ObjectMapper

KApiProvider.rx.request(input.category)
                .mapObject(KBaseModel<T>.self)
                .subscribe(onSuccess: { (baseModel) in
                    print("请求成功 返回数据如下")
                    if baseModel.status != 0 {
                        return
                    }
                }, onError: {error in
                    print("Error:请求错误")
                }).disposed(by: self.disposeBag)
            }, onError: { (error) in
                
        }, onCompleted: {
            
        }) {
            }.disposed(by: disposeBag)


2.2.1.4 Moya HTTPS 证书信任,自签名证书信任

Moya HTTPS 证书信任,自签名证书信任

  • 开发中,我们可能服务器用的是HTTPS的方式,这个时候如果服务器端是使用的证书颁发机构的证书,我们使用Moya的时候不需要做什么处理,如果是自签名证书的话,需要做一下证书信任,不然请求直接被拒绝。

可能有些朋友不太熟悉HTTPS握手的过程,要理解证书认证机制,有必要理解一下HTTPS握手过程:

发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下:

  1. 客户端发起握手请求,携带随机数、支持算法列表等参数。
  2. 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
  3. 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥加密。
  4. 服务端通过私钥获取随机数信息。
  5. 双方根据以上交互的信息生成session ticket,用作该连接后续数据传输的加密密钥。

第3步中,客户端需要验证服务端下发的证书,验证过程有以下两个要点:

  1. 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
  2. 客户端需要检查证书的domain域和扩展域,看是否包含本次请求的host。
    如果上述两点都校验通过,就证明当前的服务端是可信任的,否则就是不可信任,应当中断当前连接。

当客户端直接使用IP地址发起请求时,请求URL中的host会被替换成HTTP DNS解析出来的IP,所以在证书验证的第2步,会出现domain不匹配的情况,导致SSL/TLS握手不成功。

更多详情请参考我之前写的一篇关于HTTPS自签名证书上传下载文件的博客:

IOS 网络协议(一) 自签名证书HTTPS文件上传下载(上)

IOS音视频(四十五)HTTPS 自签名证书 实现边下边播

  • HTTPS SSL加密建立连接过程

如下图:



过程详解:

  1. ①客户端的浏览器向服务器发送请求,并传送客户端SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。
  2. ②服务器向客户端传送SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。
  3. ③客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。
  4. ④用户端随机产生一个用于通讯的“对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。
  5. ⑤如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的“预主密码”一起传给服务器。
  6. ⑥如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥能否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。
  7. ⑦服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通讯的加解密通讯。同时在SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。
  8. ⑧客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤. ⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
  9. ⑨服务器向客户端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
  10. ⑩SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。

要解决 Moya HTTPS 证书信任,自签名证书信任,请求被拒绝的问题,有以下几种方式:

  • 方法一: 简单粗暴的方法,关闭HTTPS证书认证(不推荐)。

这种方式相当于将HTTPS还原变成了HTTP了,非常不安全,数据容易被窃取。

/// 关闭https认证

let serverTrustPolicies: [String: ServerTrustPolicy] = [
“172.16.88.106”: .disableEvaluation
]

let manager = Manager(
configuration: URLSessionConfiguration.default,
serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

let provider = MoyaProvider(manager: manager, plugins: [NetworkLoggerPlugin(verbose: true)])


方法一是一种变通实现的方法,它直接关闭了Https的Domain验证,虽然可以请求正常进行,但是如果在客户端和服务器之间增加代理,请求发送时代理替换证书,那么代理就可以轻易拿到请求的数据,出于安全考虑并不推荐这种做法。

  • 方法二:验证自签名证书,CN 是否合法(推荐):

ServerTrustPolicy枚举中使用pinCertificates

case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)

具体实现代码如下:

//
//  JPNetworkProvider.swift
//  JimuPro
//
//  Created by yulu kong on 2019/10/24.
//  Copyright © 2019 UBTech. All rights reserved.
//

import Moya
import RxSwift
import Alamofire

typealias FileNetworking = JPNetworkProvider<FileManagerAPI>

let APIFileManager = FileNetworking(plugins: [NetworkLoggerPlugin(verbose: true)], isHttps: true)

final class JPNetworkProvider<Target: TargetType>: MoyaProvider<Target> {
    
    init(plugins: [PluginType] = [LoadingPlugin()], isHttps:Bool = true) {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders
        configuration.timeoutIntervalForRequest = kTimeoutIntervalForRequest
        var manager = Manager(configuration: configuration)
        if isHttps {
            let policies: [String: ServerTrustPolicy] = [
                 getFileTransportIP(): .pinPublicKeys(publicKeys: ServerTrustPolicy.publicKeys(),
                 validateCertificateChain: false,
                 validateHost: true)
             ]
            //debugPrint("JPNetworkProvider----https--证书:\(policies)")
            manager = Manager(configuration: configuration,serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
        }
        manager.startRequestsImmediately = false
        
        super.init(endpointClosure:JPNetworkProvider.endpointMapping ,manager: manager, plugins: plugins)
     }
    
    

        func requestWithProgress(
            _ target: Target,
            _ callbackQueue: DispatchQueue? = nil,
            _ isCache: Bool = false,
            file: StaticString = #file,
            function: StaticString = #function,
            line: UInt = #line
            ) -> Observable<ProgressResponse> {

            return self.rx.requestWithProgress(target, callbackQueue: callbackQueue).do(onNext: { (progressResponse) in
                
            })
        }
        
        
    func request(
        _ target: Target,
        _ isCache: Bool = false,
        file: StaticString = #file,
        function: StaticString = #function,
        line: UInt = #line
        ) -> Single<Response> {
        let requestString = "\(target.method) \(target.path)"
        
        return self.rx.request(target)
            .filterSuccessfulStatusCodes()
            .do(onSuccess: { (value) in
                let message = "*** SUCCESS: \(requestString) (\(value.statusCode))"
                logger.debug(message, file: file, function: function, line: line)
                
            }, onError: {(error) in
            
                //NotificationCenter.post(customNotification: .netError)
                
                if let response = (error as? MoyaError)?.response {
                if let jsonObject = try? response.mapJSON(failsOnEmptyData: false) {
                let message = "*** FAILURE: \(requestString) (\(response.statusCode))\n\(jsonObject)"
                logger.warning(message, file: file, function: function, line: line)
                } else if let rawString = String(data: response.data, encoding: .utf8) {
                let message = "*** FAILURE: \(requestString) (\(response.statusCode))\n\(rawString)"
                logger.warning(message, file: file, function: function, line: line)
                } else {
                let message = "*** FAILURE: \(requestString) (\(response.statusCode))"
                logger.warning(message, file: file, function: function, line: line)
                }
                } else {
                let message = "*** FAILURE: \(requestString)\n\(error)"
                logger.warning(message, file: file, function: function, line: line)
                }
            }, onSubscribed: {
                let message = "*** REQUEST: \(requestString)"
                logger.debug(message, file: file, function: function, line: line)
            })
    }
    
    private static func endpointMapping<Target: TargetType>(target: Target) -> Endpoint {
    
        var param: [String:Any] = [:]
        switch target.task {
        case let .requestParameters(parameters, _):
            param = parameters
        default:break
        }
    
        var url = "\(target.baseURL)\(target.path)?"
        
        if target.method == .get {
            
            
            let s = param.map { (key,value) -> String in
                
                return "\(key)=\(value)&"
            }
            
            for p in s {
                url += p
            }
            
            url.remove(at: String.Index(encodedOffset: url.count - 1))
            
            //logger.info("kyl请求链接:\(url) \n 请求方法:\(target.method)")
            
        }else{
            //logger.info("kyl请求链接:\(url) \n 参数:\(param) \n 请求方法:\(target.method)")
            
        }
    
        return MoyaProvider.defaultEndpointMapping(for: target)
    }
    
}



认证代码如下:

//
//  HTTPSManager.swift
//  JimuPro
//
//  Created by yulu kong on 2019/10/28.
//  Copyright © 2019 UBTech. All rights reserved.
//

import UIKit
import Alamofire
import Kingfisher

class HTTPSManager: NSObject {
    
    // MARK: - sll证书处理
   static func setKingfisherHTTPS() {
        //取出downloader单例
        let downloader = KingfisherManager.shared.downloader
        //信任Server的ip
        downloader.trustedHosts = Set([getFileTransportIP()])
    }
    
   static func setAlamofireHttps() {
        
        SessionManager.default.delegate.sessionDidReceiveChallenge = { (session: URLSession, challenge: URLAuthenticationChallenge) in
            
            let method = challenge.protectionSpace.authenticationMethod
            if method == NSURLAuthenticationMethodServerTrust {
                //验证服务器,直接信任或者验证证书二选一,推荐验证证书,更安全
                return HTTPSManager.trustServerWithCer(challenge: challenge)
//                return HTTPSManager.trustServer(challenge: challenge)
                
            } else if method == NSURLAuthenticationMethodClientCertificate {
                //认证客户端证书
                return HTTPSManager.sendClientCer()
                
            } else {
                //其他情况,不通过验证
                return (.cancelAuthenticationChallenge, nil)
            }
        }
    }
    
    //不做任何验证,直接信任服务器
    static private func trustServer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        
        let disposition = URLSession.AuthChallengeDisposition.useCredential
        let credential = URLCredential.init(trust: challenge.protectionSpace.serverTrust!)
        return (disposition, credential)
        
    }
    
    //验证服务器证书
    static  func trustServerWithCer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        
        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
        var credential: URLCredential?
        
        //获取服务器发送过来的证书
        let serverTrust:SecTrust = challenge.protectionSpace.serverTrust!
        let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)!
        let remoteCertificateData = CFBridgingRetain(SecCertificateCopyData(certificate))!
        
        //加载本地CA证书
//        let cerPath = Bundle.main.path(forResource: "oooo", ofType: "cer")!
//        let cerUrl = URL(fileURLWithPath:cerPath)
        
        let cerUrl = Bundle.main.url(forResource: "server", withExtension: "cer")!
        let localCertificateData = try! Data(contentsOf: cerUrl)
        
        if (remoteCertificateData.isEqual(localCertificateData) == true) {
            //服务器证书验证通过
            disposition = URLSession.AuthChallengeDisposition.useCredential
            credential = URLCredential(trust: serverTrust)
            
        } else {
            //服务器证书验证失败
            //disposition = URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge
            disposition = URLSession.AuthChallengeDisposition.useCredential
            credential = URLCredential(trust: serverTrust)
        }
        
        return (disposition, credential)
        
    }
    
    //发送客户端证书交由服务器验证
    static  func sendClientCer() -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        
        let disposition = URLSession.AuthChallengeDisposition.useCredential
        var credential: URLCredential?
        
        //获取项目中P12证书文件的路径
        let path: String = Bundle.main.path(forResource: "clientp12", ofType: "p12")!
        let PKCS12Data = NSData(contentsOfFile:path)!
        let key : NSString = kSecImportExportPassphrase as NSString
        let options : NSDictionary = [key : "123456"] //客户端证书密码
        
        var items: CFArray?
        let error = SecPKCS12Import(PKCS12Data, options, &items)
        
        if error == errSecSuccess {
            
            let itemArr = items! as Array
            let item = itemArr.first!
            
            let identityPointer = item["identity"];
            let secIdentityRef = identityPointer as! SecIdentity
            
            let chainPointer = item["chain"]
            let chainRef = chainPointer as? [Any]
            
            credential = URLCredential.init(identity: secIdentityRef, certificates: chainRef, persistence: URLCredential.Persistence.forSession)
        }
        
        return (disposition, credential)
        
    }
    
}


2.2.2 数据解析库

2.2.2.1 ObjectMapper

ObjectMapper 是一个使用 Swift 语言编写的数据模型转换框架,我们可以方便的将模型对象转换为JSON,或者JSON生成相应的模型类。

有如下特点:

  1. 将JSON映射到对象
  2. 将对象映射到JSON
  3. 支持嵌套对象(在数组或字典中单独使用)
  4. 支持映射过程中自定义转换
  5. 支持结构体
  6. 支持Immutable
  • 模型类定义:

创建模型类需要实现Mappable接口,包括init?(map: Map)func mapping(map: Map)两个方法
ObjectMapper使用<-特殊运算符表示JSON与模型属性之间的映射关系

实例代码如下:

class User: Mappable {
    var username: String?
    var age: Int?
    var weight: Double!
    var array: [Any]?
    var dictionary: [String : Any] = [:]
    var bestFriend: User?                       // Nested User object
    var friends: [User]?                        // Array of Users
    var birthday: Date?

//对象序列号之前验证JSON合法性,不符合条件返回nil阻止映射发生
    required init?(map: Map) {
      // 检查JSON是否有name字段
      if map.JSON["name"] == nil {
        return nil
      }
    }

    // Mappable
    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
        weight      <- map["weight"]
        array       <- map["arr"]
        dictionary  <- map["dict"]
        bestFriend  <- map["best_friend"]
        friends     <- map["friends"]
        birthday    <- (map["birthday"], DateTransform())
    }
}

struct Temperature: Mappable {
    var celsius: Double?
    var fahrenheit: Double?
    init?(map: Map) {
    }
    mutating func mapping(map: Map) {
        celsius     <- map["celsius"]
        fahrenheit  <- map["fahrenheit"]
    }
}
  • JSON字符串转模型类:
let user = User(JSONString: JSONString)
//使用Mapper
let user = Mapper<User>().map(JSONString: JSONString)
//使用Mapper转模型数组
let users: [User] = Mapper<User>().mapArray(JSONString: JSONString)
  • 模型类转JSON字符串:
//prettyPrint参数是为了打印可读性json
let JSONString = user.toJSONString(prettyPrint: true)
//使用Mapper
let JSONString = Mapper().toJSONString(users, prettyPrint: true)
  • 支持的类型:
Int
Bool
Double
Float
String
RawRepresentable (Enums)
Array<Any>
Dictionary<String, Any>
Object<T: Mappable>
Array<T: Mappable>
Array<Array<T: Mappable>>
Set<T: Mappable>
Dictionary<String, T: Mappable>
Dictionary<String, Array<T: Mappable>>
Optionals of all the above //上述的可选类型
Implicitly Unwrapped Optionals of the above //上述的隐式解析可选类型

  • 嵌套对象的简单映射:
  import ObjectMapper

class UserInfo: Mappable {
    var username: String?
    var age: Int?
    var weight: Double!
    var dictionary: UserInfo?
    var value: String?


    required init?(map: Map) {
    }
    
    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
        weight      <- map["weight"]
        dictionary  <- map["dictionary"]
        value  <- map["dictionary.username"]
    }
}
  • 自定义转换: ObjectMapper提供了一些类型转换如DateTransformDataTransformHexColorTransform,但是没有提供的就需要我们自定义,下面举例实现NSURLTransform

说到json解析字典转模型,我们可能经常用到有 SwiftyJSON 或者 HandyJSON 这些框架。
SwiftyJSON 只是把JSON 字符串转换为字典,需要我们自己去按key 去或者对应值。这样比较灵活,但是麻烦,适合于自定定义模型里面含有计算型属性的模型。
HandyJSON 会自动帮我把JSON字符串通过反射转换为我们定义的那个模型,这个模型需要继承 HandyJSON ,如下代码:

///接收到一个蓝牙命令
struct BluetoothInfo: HandyJSON {
    
    var action: String?
    var content: String?//json序列化字符串
}

而ObjectMapper呢是面向协议编程的,代码没有 HandyJSON 的强入侵性,又解决了SwiftyJSON的痛点,应该是结合两种所长,此外ObjectMapper还可以定义计算型属性,这样你可以使用同一个模型提供给上层使用,不必要将模型转换几次。

import UIKit
import ObjectMapper

class NSURLTransform: TransformType {
    typealias Object = NSURL
    typealias JSON = String
    
    func transformFromJSON(_ value: Any?) -> NSURL? {
        guard let string = value as? String else{
            return nil
        }
        return NSURL.init(string: string)
    }
    
    func transformToJSON(_ value: NSURL?) -> String? {
        guard let url = value else{
            return nil
        }
        return url.absoluteString
    }

}

此外,还有一个比较好用的框架AlamofireObjectMapper

该框架可以结合 AlamofireObjectMapper 使用, 为Alamofire的Request类扩展出了responseObjectresponseArray 方法, 更方便的将网络通信返回的JSON数据转换成对象


下面是它的样列代码:

let URL = "..."
Alamofire.request(.GET, URL).responseObject { (response: DataResponse<WeatherResponse>) in

    let weatherResponse = response.result.value

    if let threeDayForecast = weatherResponse?.threeDayForecast {
        for forecast in threeDayForecast {
            print(forecast.day)
            print(forecast.temperature)           
        }
    }
}
2.2.2.2 Moya-ObjectMapper/Swift

安装方式:

pod 'Moya-ObjectMapper'
#The subspec if you want to use the bindings over RxSwift.

pod 'Moya-ObjectMapper/RxSwift'
#The subspec if you want to use the bindings over ReactiveSwift.

pod 'Moya-ObjectMapper/ReactiveSwift'
  • 使用:
    先创建一个模型:
import Foundation
import ObjectMapper

// MARK: Initializer and Properties
struct Repository: Mappable {

  var identifier: Int!
  var language: String?
  var url: String!

  // MARK: JSON
  init?(map: Map) { }

  mutating func mapping(map: Map) {
    identifier <- map["id"]
    language <- map["language"]
    url <- map["url"]
  }
}

没有RxswiftReactiveSwift 的使用方法:

GitHubProvider.request(.userRepositories(username), completion: { result in

    var success = true
    var message = "Unable to fetch from GitHub"

    switch result {
    case let .success(response):
        do {
            if let repos = try response.mapArray(Repository) {
              self.repos = repos
            } else {
              success = false
            }
        } catch {
            success = false
        }
        self.tableView.reloadData()
    case let .failure(error):
        guard let error = error as? CustomStringConvertible else {
            break
        }
        message = error.description
        success = false
    }
})

Rxswift的使用方式:

GitHubProvider.request(.userRepositories(username))
  .mapArray(Repository.self)
  .subscribe { event -> Void in
    switch event {
    case .next(let repos):
      self.repos = repos
    case .error(let error):
      print(error)
    default: break
    }
  }.addDisposableTo(disposeBag)

ReactiveSwift的使用方式:

GitHubProvider.request(.userRepositories(username))
  .mapArray(Repository.self)
  .start { event in
    switch event {
    case .value(let repos):
      self.repos = repos
    case .failed(let error):
      print(error)
    default: break
    }
  }

ReactiveSwift简介:

ReactiveSwift提供了可组合的、声明性的和灵活的原语,这些原语是围绕着随时间流逝的价值流的宏大概念构建的。
这些原语可以用来统一地表示常见的Cocoa和泛型编程模式,它们本质上是一种观察行为,例如委托模式、回调闭包、通知、控制操作、响应链事件和键值观察(KVO)。
因为所有这些不同的机制都可以用相同的方式表示,所以很容易以声明的方式将它们组合在一起,用更少的意大利面条代码和状态来弥补差距。

2.2.3 Rxswift 框架和相关扩展

2.2.3.1 RxDataSources
  1. O(N)计算差异的算法:
    该算法假设所有的部分和项都是唯一的,因此没有歧义。
    如果有歧义,回退自动对非动画刷新。
  2. 它应用额外的启发式方法,向分段视图发送最少数量的命令:
    尽管运行时间是线性的,但发送命令的首选数量通常比线性少得多
    最好(也可能)将更改的数量限制在较小的范围内,如果更改的数量增长为线性,则只需进行正常的重新加载
  3. 支持扩展项目和节结构:
    用IdentifiableType和Equatable扩展你的项目,用AnimatableSectionModelType扩展你的部分
  4. 支持两个层次动画的所有组合的节和项目:
    节动画:插入,删除,移动
    项目动画:插入、删除、移动、重载(如果旧值不等于新值)
  5. 可配置的动画类型插入,重载和删除(自动,淡出,…)
  6. 示例应用程序
  7. 随机压力测试(示例app)
  8. 支持开箱即用的编辑(示例应用程序)
  9. 适用于UITableView和UICollectionView

安装:

CocoaPods

Podfile

pod 'RxDataSources', '~> 4.0'

Carthage

Cartfile

github "RxSwiftCommunity/RxDataSources" ~> 4.0
  • 使用:
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Int>>(configureCell: configureCell)
Observable.just([SectionModel(model: "title", items: [1, 2, 3])])
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)
2.2.3.2 RxSwiftExt

如果您正在使用Rxswift,您可能会遇到内置操作符不能提供所需功能的情况。为了避免膨胀,Rxswift内核被设计得尽可能紧凑。这个存储库的目的是提供额外的方便操作符和反应性扩展。

安装:
RxSwiftExt的这个分支以Swift 5为目标。x和Rxswift 5.0.0或更高版本。

如果您正在寻找RxSwiftExt的Swift 4版本,请使用该框架的3.4.0版本。

CocoaPods

Add to your Podfile:

pod 'RxSwiftExt', '~> 5'

这将同时安装RxSwift和RxCocoa扩展。如果您只想安装RxSwift扩展,而不想安装RxCocoa扩展,只需使用:

pod 'RxSwiftExt/Core'

Using Swift 4:

pod 'RxSwiftExt', '~> 3'

Carthage

github "RxSwiftCommunity/RxSwiftExt"

RxSwiftExt扩展了如下操作:

  • unwrap
    打开选项并过滤掉空值。
Observable.of(1,2,nil,Int?(4))
    .unwrap()
    .subscribe { print($0) }

结果:

next(1)
next(2)
next(4)
  • ignore:忽略特定元素。
Observable.from(["One","Two","Three"])
    .ignore("Two")
    .subscribe { print($0) }

结果:

next(One)
next(Three)
completed

  • ignoreWhen:根据闭包忽略元素。
 Observable<Int>
    .of(1,2,3,4,5,6)
    .ignoreWhen { $0 > 2 && $0 < 6 }
    .subscribe { print($0) }

结果:

next(1)
next(2)
next(6)
completed
  • once:将下一个元素精确地发送一次到接收它的第一个订阅服务器。进一步的订阅者将得到一个空序列。
let obs = Observable.once("Hello world")
  print("First")
  obs.subscribe { print($0) }
  print("Second")
  obs.subscribe { print($0) }

结果:

First
next(Hello world)
completed
Second
completed
  • distinct:只有在序列中从未出现过元素时,才将它们传递过去。
Observable.of("a","b","a","c","b","a","d")
    .distinct()
    .subscribe { print($0) }

结果:

next(a)
next(b)
next(c)
next(d)
completed
  • mapTo:用提供的值替换每个元素。
Observable.of(1,2,3)
    .mapTo("Nope.")
    .subscribe { print($0) }

结果:

next(Nope.)
next(Nope.)
next(Nope.)
completed
  • mapAt:将每个元素转换为提供的键路径上的值。
struct Person {
    let name: String
}

Observable
    .of(
        Person(name: "Bart"),
        Person(name: "Lisa"),
        Person(name: "Maggie")
    )
    .mapAt(\.name)
    .subscribe { print($0) }

结果:

next(Bart)
next(Lisa)
next(Maggie)
completed
  • not:否定的布尔值。
Observable.just(false)
    .not()
    .subscribe { print($0) }

结果:

next(true)
completed
  • and:验证发出的每个值都为真
Observable.of(true, true)
    .and()
    .subscribe { print($0) }

Observable.of(true, false)
    .and()
    .subscribe { print($0) }

Observable<Bool>.empty()
    .and()
    .subscribe { print($0) }

结果:

success(true)
success(false)
completed
  • cascade:顺序级联通过一系列可观察对象,当一个可观察对象在列表的更下方开始发射元素时,立即放弃之前的订阅。
let a = PublishSubject<String>()
let b = PublishSubject<String>()
let c = PublishSubject<String>()
Observable.cascade([a,b,c])
    .subscribe { print($0) }
a.onNext("a:1")
a.onNext("a:2")
b.onNext("b:1")
a.onNext("a:3")
c.onNext("c:1")
a.onNext("a:4")
b.onNext("b:4")
c.onNext("c:2")

结果:

next(a:1)
next(a:2)
next(b:1)
next(c:1)
next(c:2)
  • pairwise:将一个可观察对象发出的元素分组成数组,其中每个数组由最后两个连续的项组成;类似于滑动窗口。
Observable.from([1, 2, 3, 4, 5, 6])
    .pairwise()
    .subscribe { print($0) }

结果:

next((1, 2))
next((2, 3))
next((3, 4))
next((4, 5))
next((5, 6))
completed
  • nwise:将一个可观察对象发出的元素分组成数组,其中每个数组由最后的N个连续项组成;类似于滑动窗口。
Observable.from([1, 2, 3, 4, 5, 6])
    .nwise(3)
    .subscribe { print($0) }

结果:

next([1, 2, 3])
next([2, 3, 4])
next([3, 4, 5])
next([4, 5, 6])
completed
  • retry:在发生错误或成功终止之前,使用给定的行为重复源观察到的序列。有四种具有不同谓词和延迟选项的行为:immediate、delayed、exponentialDelayed和customTimerDelayed。
// in case of an error initial delay will be 1 second,
// every next delay will be doubled
// delay formula is: initial * pow(1 + multiplier, Double(currentAttempt - 1)), so multiplier 1.0 means, delay will doubled
_ = sampleObservable.retry(.exponentialDelayed(maxCount: 3, initial: 1.0, multiplier: 1.0), scheduler: delayScheduler)
    .subscribe(onNext: { event in
        print("Receive event: \(event)")
    }, onError: { error in
        print("Receive error: \(error)")
    })

结果:

Receive event: First
Receive event: Second
Receive event: First
Receive event: Second
Receive event: First
Receive event: Second
Receive error: fatalError
  • repeatWithBehavior:当源观察序列完成时,使用给定的行为重复它。此操作符接受与重试操作符相同的参数。有四种具有不同谓词和延迟选项的行为:immediate、delayed、exponentialDelayed和customTimerDelayed。
// when the sequence completes initial delay will be 1 second,
// every next delay will be doubled
// delay formula is: initial * pow(1 + multiplier, Double(currentAttempt - 1)), so multiplier 1.0 means, delay will doubled
_ = completingObservable.repeatWithBehavior(.exponentialDelayed(maxCount: 3, initial: 1.0, multiplier: 1.2), scheduler: delayScheduler)
    .subscribe(onNext: { event in
        print("Receive event: \(event)")
})

结果:

Receive event: First
Receive event: Second
Receive event: First
Receive event: Second
Receive event: First
Receive event: Second
  • catchErrorJustComplete:当发生错误时,取消错误条件,完成一个序列
let _ = sampleObservable
    .do(onError: { print("Source observable emitted error \($0), ignoring it") })
    .catchErrorJustComplete()
    .subscribe {
        print ("\($0)")
}

结果:

next(First)
next(Second)
Source observable emitted error fatalError, ignoring it
completed
  • pausable:暂停源观察序列的元素,除非来自第二个观察序列的最新元素为真。
let observable = Observable<Int>.interval(1, scheduler: MainScheduler.instance)

let trueAtThreeSeconds = Observable<Int>.timer(3, scheduler: MainScheduler.instance).map { _ in true }
let falseAtFiveSeconds = Observable<Int>.timer(5, scheduler: MainScheduler.instance).map { _ in false }
let pauser = Observable.of(trueAtThreeSeconds, falseAtFiveSeconds).merge()

let pausedObservable = observable.pausable(pauser)

let _ = pausedObservable
    .subscribe { print($0) }

结果:

next(2)
next(3)
  • apply:Apply为在可观察的序列上应用转换提供了一种统一的机制,而不必扩展ObservableType或重复您的转换。更多的理由见github上的讨论
// An ordinary function that applies some operators to its argument, and returns the resulting Observable
func requestPolicy(_ request: Observable<Void>) -> Observable<Response> {
    return request.retry(maxAttempts)
        .do(onNext: sideEffect)
        .map { Response.success }
        .catchError { error in Observable.just(parseRequestError(error: error)) }

// We can apply the function in the apply operator, which preserves the chaining style of invoking Rx operators
let resilientRequest = request.apply(requestPolicy)
  • filterMap:Rx中的一个常见模式是过滤掉一些值,然后将其余的值映射到其他值。filterMap允许你一步完成:
// keep only even numbers and double them
Observable.of(1,2,3,4,5,6)
    .filterMap { number in
        (number % 2 == 0) ? .ignore : .map(number * 2)
    }

上面的序列保持偶数2、4、6,并产生序列4、8、12。

  • errors, elements:这些操作符只适用于使用materialize()操作符(来自RxSwift core)物化的可观察序列。错误返回一个经过过滤的错误事件序列,即抛出的元素。元素返回一个经过过滤的元素事件序列,抛出错误。
let imageResult = _chooseImageButtonPressed.asObservable()
    .flatMap { imageReceiver.image.materialize() }
    .share()

let image = imageResult
    .elements()
    .asDriver(onErrorDriveWith: .never())

let errorMessage = imageResult
    .errors()
    .map(mapErrorMessages)
    .unwrap()
    .asDriver(onErrorDriveWith: .never())
  • fromAsync:将简单的异步完成处理程序转换为可观察的序列。适合与仅使用一个参数调用完成处理程序的现有异步服务一起使用。发出由完成处理程序生成的结果,然后完成。
func someAsynchronousService(arg1: String, arg2: Int, completionHandler:(String) -> Void) {
    // a service that asynchronously calls
    // the given completionHandler
}

let observableService = Observable
    .fromAsync(someAsynchronousService)

observableService("Foo", 0)
    .subscribe(onNext: { (result) in
        print(result)
    })
    .disposed(by: disposeBag)
  • zip(with:):便利版的Observable.zip(_:)。将指定的可观察序列合并为一个可观察序列,只要所有的可观察序列在相应的索引处产生一个元素,就使用选择器函数。
let first = Observable.from(numbers)
let second = Observable.from(strings)

first.zip(with: second) { i, s in
        s + String(i)
    }.subscribe(onNext: { (result) in
        print(result)
    })

结果:

next("a1")
next("b2")
next("c3")
  • merge(with:):便利版的Observable.merge(_:)。将可观察序列中的元素与不同的可观察序列中的元素合并为一个可观察序列。
let oddStream = Observable.of(1, 3, 5)
let evenStream = Observable.of(2, 4, 6)
let otherStream = Observable.of(1, 5, 6)

oddStream.merge(with: evenStream, otherStream)
    .subscribe(onNext: { result in
        print(result)
    })

结果:

1 2 1 3 4 5 5 6 6
  • ofType:ofType操作符过滤可观察序列的元素(如果它是提供的类型的实例)。
Observable.of(NSNumber(value: 1),
                  NSDecimalNumber(string: "2"),
                  NSNumber(value: 3),
                  NSNumber(value: 4),
                  NSDecimalNumber(string: "5"),
                  NSNumber(value: 6))
        .ofType(NSDecimalNumber.self)
        .subscribe { print($0) }

结果:

next(2)
next(5)
completed
  • withUnretained:withunretain (_:resultSelector:)操作符提供了一个未保留的、可以安全使用(即不隐式取消包装)的对象引用,以及序列发出的事件。如果提供的对象不能成功保留,则seqeunce将完成
class TestClass: CustomStringConvertible {
    var description: String { return "Test Class" }
}

Observable
    .of(1, 2, 3, 5, 8, 13, 18, 21, 23)
    .withUnretained(testClass)
    .do(onNext: { _, value in
        if value == 13 {
            // When testClass becomes nil, the next emission of the original
            // sequence will try to retain it and fail. As soon as it fails,
            // the sequence will complete.
            testClass = nil
        }
    })
    .subscribe()

结果:

next((Test Class, 1))
next((Test Class, 2))
next((Test Class, 3))
next((Test Class, 5))
next((Test Class, 8))
next((Test Class, 13))
completed
  • count:在一个可观察对象终止且没有错误时发出的项数。如果给定一个谓词,则只计算与谓词匹配的元素。
Observable.from([1, 2, 3, 4, 5, 6])
    .count { $0 % 2 == 0 }
    .subscribe()

结果:

next(3)
completed
  • partition:将一个流划分为两个单独的元素流,这两个元素流与提供的谓词匹配或不匹配。
let numbers = Observable
        .of(1, 2, 3, 4, 5, 6)

    let (evens, odds) = numbers.partition { $0 % 2 == 0 }

    _ = evens.debug("even").subscribe() // emits 2, 4, 6
    _ = odds.debug("odds").subscribe() // emits 1, 3, 5
  • bufferWithTrigger:收集源可观察到的元素,并在触发器发出时将它们作为数组发出。
let observable = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
let signalAtThreeSeconds = Observable<Int>.timer(3, scheduler: MainScheduler.instance).map { _ in () }
let signalAtFiveSeconds = Observable<Int>.timer(5, scheduler: MainScheduler.instance).map { _ in () }
let trigger = Observable.of(signalAtThreeSeconds, signalAtFiveSeconds).merge()
let buffered = observable.bufferWithTrigger(trigger)
buffered.subscribe { print($0) }
// prints next([0, 1, 2]) @ 3, next([3, 4]) @ 5
2.2.3.3 NSObject+Rx

如果你用Rxswift一般你经常需要这样子let disposeBag = DisposeBag()定义一个垃圾袋对象,用来销毁回收序列的资源。每个类中都要去定义这样一个东东是很麻烦的。而NSObject+Rx帮你简化了这部操作,你可以不需要定义let disposeBag = DisposeBag()这样的代码了,直接ob.rx.disposeBag就可以了,例如:

thing
  .bind(to: otherThing)
  .disposed(by: rx.disposeBag)
  • 安装方式:
    CocoaPods

Add to your Podfile:

pod 'NSObject+Rx'

Carthage

Add to Cartfile:

github "RxSwiftCommunity/NSObject-Rx"
2.2.3.4 RxViewController

RxViewController是用于UIViewController和NSViewController的RxSwift包装器。

有了RxViewController的包装后,你可以这样在VC中调用viewDidLoad方法:

self.rx.viewDidLoad
  .subscribe(onNext: {
    print("viewDidLoad 🎉")
  })

此外RxViewController还提供了以下这些API:

extension Reactive where Base: UIViewController {
  var viewDidLoad: ControlEvent<Void>

  var viewWillAppear: ControlEvent<Bool>
  var viewDidAppear: ControlEvent<Bool>

  var viewWillDisappear: ControlEvent<Bool>
  var viewDidDisappear: ControlEvent<Bool>

  var viewWillLayoutSubviews: ControlEvent<Void>
  var viewDidLayoutSubviews: ControlEvent<Void>

  var willMoveToParentViewController: ControlEvent<UIViewController?>
  var didMoveToParentViewController: ControlEvent<UIViewController?>

  var didReceiveMemoryWarning: ControlEvent<Void>
}
2.2.3.5 RxGesture


RxGesture可以让你轻松地将任何视图变成可移动或可滑动的控件,就像这样:

view.rx
  .tapGesture()
  .when(.recognized)
  .subscribe(onNext: { _ in
    //react to taps
  })
  .disposed(by: stepBag)

你也可以对多种手势做出反应。例如,当用户点击或上下滑动照片预览时,你可能想要关闭它:

view.rx
  .anyGesture(.tap(), .swipe([.up, .down]))
  .when(.recognized)
  .subscribe(onNext: { _ in
    //dismiss presented photo
  })
  .disposed(by: stepBag)

rx.gesture被定义为Observable<G>其中G是手势识别器的实际类型所以它发出的是手势识别器本身(如果想调用asLocation(in view:)asTranslation(in view:)这样的方法很方便)

RxGesture支持如下手势:

view.rx.tapGesture()           -> ControlEvent<UITapGestureRecognizer>
view.rx.pinchGesture()         -> ControlEvent<UIPinchGestureRecognizer>
view.rx.swipeGesture(.left)    -> ControlEvent<UISwipeGestureRecognizer>
view.rx.panGesture()           -> ControlEvent<UIPanGestureRecognizer>
view.rx.longPressGesture()     -> ControlEvent<UILongPressGestureRecognizer>
view.rx.rotationGesture()      -> ControlEvent<UIRotationGestureRecognizer>
view.rx.screenEdgePanGesture() -> ControlEvent<UIScreenEdgePanGestureRecognizer>

view.rx.anyGesture(.tap(), ...)           -> ControlEvent<UIGestureRecognizer>
view.rx.anyGesture(.pinch(), ...)         -> ControlEvent<UIGestureRecognizer>
view.rx.anyGesture(.swipe(.left), ...)    -> ControlEvent<UIGestureRecognizer>
view.rx.anyGesture(.pan(), ...)           -> ControlEvent<UIGestureRecognizer>
view.rx.anyGesture(.longPress(), ...)     -> ControlEvent<UIGestureRecognizer>
view.rx.anyGesture(.rotation(), ...)      -> ControlEvent<UIGestureRecognizer>
view.rx.anyGesture(.screenEdgePan(), ...) -> ControlEvent<UIGestureRecognizer>

如果您单独使用手势识别器,请选择view.rx.fooGesture()语法而不是view.rx.anyGesture(.foo()),因为它返回具体的UIGestureRecognizer子类,并避免您将其转换为subscribe()

  • RxGesture
    手势过滤:
    默认情况下,手势识别器的状态没有过滤器。这意味着您将始终接收到带有手势识别器初始状态的第一个事件(几乎总是.possible)。

默认情况下,手势识别器的状态没有过滤器。这意味着,这里有可以用于各种手势(iOS和macOS)的首选状态:



通常使用.when()操作符过滤状态:

view.rx.tapGesture().when(.recognized)
view.rx.panGesture().when(.began, .changed, .ended)

如果你同时观察多个手势,你可以使用when()操作符,如果你想过滤所有手势识别器的相同状态,或者使用tuple语法进行单独的过滤:

view.rx
  .anyGesture(.tap(), .swipe([.up, .down]))
  .when(.recognized)
  .subscribe(onNext: { gesture in
    // Called whenever a tap, a swipe-up or a swipe-down is recognized (state == .recognized)
  })
  .disposed(by: bag)

view.rx
  .anyGesture(
    (.tap(), when: .recognized),
    (.pan(), when: .ended)
  )
  .subscribe(onNext: { gesture in
    // Called whenever:
    // - a tap is recognized (state == .recognized)
    // - or a pan is ended (state == .ended)
  })
  .disposed(by: bag)

这里有一个官方的演示应用程序包括所有识别器的例子: ➡️ iOS, macOS.

每个手势识别器都有一个默认的RxGestureRecognizerDelegate。它允许你使用一个策略自定义每个委托方法:

  1. .always : 对应的委托方法是否返回true
  2. .never : 将返回false到相应的委托方法
  3. .custom : 获取将执行的关联闭包,以将值返回给相应的委托方法

以下是可用的策略及其相应的委托方法:

beginPolicy                   -> gestureRecognizerShouldBegin(:_)
touchReceptionPolicy          -> gestureRecognizer(_:shouldReceive:)
selfFailureRequirementPolicy  -> gestureRecognizer(_:shouldBeRequiredToFailBy:)
otherFailureRequirementPolicy -> gestureRecognizer(_:shouldRequireFailureOf:)
simultaneousRecognitionPolicy -> gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
eventRecognitionAttemptPolicy -> gestureRecognizer(_:shouldAttemptToRecognizeWith:) // macOS only
pressReceptionPolicy          -> gestureRecognizer(_:shouldReceive:) // iOS only

这个委托可以在配置包中定制:

view.rx.tapGesture(configuration: { gestureRecognizer, delegate in
  delegate.simultaneousRecognitionPolicy = .always // (default value)
  // or
  delegate.simultaneousRecognitionPolicy = .never
  // or
  delegate.simultaneousRecognitionPolicy = .custom { gestureRecognizer, otherGestureRecognizer in
    return otherGestureRecognizer is UIPanGestureRecognizer
  }
  delegate.otherFailureRequirementPolicy = .custom { gestureRecognizer, otherGestureRecognizer in
    return otherGestureRecognizer is UILongPressGestureRecognizer
  }
})

默认值可以在RxGestureRecognizerDelegate.swift中找到。

  • RxGesture
    次外还支持完全自定义方式:

您还可以用自己的委托替换默认委托,或者删除它。代码如下:

view.rx.tapGesture { [unowned self] gestureRecognizer, delegate in
  gestureRecognizer.delegate = nil
  // or
  gestureRecognizer.delegate = self
}
  • 安装方式:
    CocoaPods

Add this to Podfile

pod "RxGesture"

$ pod install
Carthage

Add this to Cartfile

github "RxSwiftCommunity/RxGesture" ~> 3.0

$ carthage update

2.2.3.6 RxOptional
  • 源码下载: RxOptional

    RxOptional适用于Swift选项和“可占用”类型的RxSwift扩展。

除另有说明外,所有操作符也可用于驱动程序和信号。

  • 可选操作:
    filterNil的用法:
Observable<String?>
    .of("One", nil, "Three")
    .filterNil()
    // Type is now Observable<String>
    .subscribe { print($0) }

结果打印:

next(One)
next(Three)
completed

replaceNilWith 的用法:

Observable<String?>
    .of("One", nil, "Three")
    .replaceNilWith("Two")
    // Type is now Observable<String>
    .subscribe { print($0) }

打印结果:

next(One)
next(Two)
next(Three)
completed

errorOnNil 的用法:

注意:在驱动程序上不可用,因为驱动程序不能出错。
默认情况下,rxoptionalerror . foundnilwhile eunwrappingoptional有错误。

Observable<String?>
    .of("One", nil, "Three")
    .errorOnNil()
    // Type is now Observable<String>
    .subscribe { print($0) }

结果打印:

next(One)
error(Found nil while trying to unwrap type <Optional<String>>)

catchOnNil 的用法:

Observable<String?>
    .of("One", nil, "Three")
    .catchOnNil {
        return Observable<String>.just("A String from a new Observable")
    }
    // Type is now Observable<String>
    .subscribe { print($0) }

打印结果:

next(One)
next(A String from a new Observable)
next(Three)
completed

distinctUntilChanged 的用法:

Observable<Int?>
    .of(5, 6, 6, nil, nil, 3)
    .distinctUntilChanged()
    .subscribe { print($0) }

打印结果:

next(Optional(5))
next(Optional(6))
next(nil)
next(Optional(3))
completed
  • 占位操作主要有:
  1. String
  2. Array
  3. Dictionary
  4. Set

目前在Swift协议中不能扩展到符合其他协议。目前,上面列出的类型符合Occupiable。您还可以使自定义类型符合Occupiable。

filterEmpty 的用法:

Observable<[String]>
    .of(["Single Element"], [], ["Two", "Elements"])
    .filterEmpty()
    .subscribe { print($0) }

打印结果:

next(["Single Element"])
next(["Two", "Elements"])
completed

errorOnEmpty的用法:

在驱动程序上不可用,因为驱动程序不能出错。

默认情况下,RxOptionalError.emptyOccupiable会出现错误。

Observable<[String]>
    .of(["Single Element"], [], ["Two", "Elements"])
    .errorOnEmpty()
    .subscribe { print($0) }

打印结果:

next(["Single Element"])
error(Empty occupiable of type <Array<String>>)

catchOnEmpty 的用法:

Observable<[String]>
    .of(["Single Element"], [], ["Two", "Elements"])
    .catchOnEmpty {
        return Observable<[String]>.just(["Not Empty"])
    }
    .subscribe { print($0) }

打印结果:

next(["Single Element"])
next(["Not Empty"])
next(["Two", "Elements"])
completed
  • 安装方式:

CocoaPods

RxOptional可以通过CocoaPods获得。要安装它,只需将以下行添加到您的Podfile中:

pod 'RxOptional'

Carthage

将此添加到Cartfile

github "RxSwiftCommunity/RxOptional" ~> 4.1.0

$ carthage update

2.2.3.7 RxTheme

RxTheme基于Rx的主题管理扩展框架

  • 安装方式:
    Cocoapods
pod 'RxTheme', '~> 4.0'

Carthage

github "RxSwiftCommunity/RxTheme" ~> 4.0.0

通过RxTheme 你可以这样定义app 的主题服务:

import RxTheme

protocol Theme {
    var backgroundColor: UIColor { get }
    var textColor: UIColor { get }
}

struct LightTheme: Theme {
    let backgroundColor = .white
    let textColor = .black
}

struct DarkTheme: Theme {
    let backgroundColor = .black
    let textColor = .white
}

enum ThemeType: ThemeProvider {
    case light, dark
    var associatedObject: Theme {
        switch self {
        case .light:
            return LightTheme()
        case .dark:
            return DarkTheme()
        }
    }
}

let themeService = ThemeType.service(initial: .light)
  • 将主题应用到UI
// Bind stream to a single attribute
// In the way, RxTheme would automatically manage the lifecycle of the binded stream 
view.theme.backgroundColor = themeService.attrStream { $0.backgroundColor }

// Or bind a bunch of attributes, add them to a disposeBag
themeService.rx
    .bind({ $0.textColor }, to: label1.rx.textColor, label2.rx.textColor)
    .bind({ $0.backgroundColor }, to: view.rx.backgroundColor)
    .disposed(by: disposeBag)

所有由ThemeService生成的流都是共享的(1)

  • 你可以很轻松的实现换肤,切换主题的功能,只需要一行代码搞定:
themeService.switch(.dark)
// When this is triggered by some signal, you can use:
someSignal.bind(to: themeService.switcher)

此外RxTheme还提供了下面的一些API:

// Current theme type
themeService.type
// Current theme attributes
themeService.attrs
// Theme type stream
themeService.typeStream
// Theme attributes stream
themeService.attrsStream
  • 已经实现预设的绑定器有:
    CALayer

backgroundColor
borderWidth
borderColor
shadowColor

CAShapeLayer:

strokeColor
fillColor

UIActivityIndicatorView

style

UIBarButtonItem

tintColor

UIButton

titleColor

UILabel

font
textColor
highlightedTextColor
shadowColor

UINavigationBar

barStyle
barTintColor
titleTextAttributes

UIPageControl

pageIndicatorTintColor
currentPageIndicatorTintColor

UIProgressView

progressTintColor
trackTintColor

UISearchBar

barStyle
barTintColor
keyboardAppearance

UISlider

thumbTintColor
minimumTrackTintColor
maximumTrackTintColor

UISwitch

onTintColor
thumbTintColor

UITabBar

barStyle
barTintColor

UITableView

separatorColor

UITAbleViewCell

selectionStyle

UITextField

font
textColor
keyboardAppearance

UITextView

font
textColor
keyboardAppearance

UIToolbar

barStyle

barTintColor

UIView

tintColor

  • 你还可以选择自己扩展代码库中的绑定:
    因为RxTheme使用来自RxCocoaBinder<T>,所以RxCocoa中定义的任何Binder都可以在这里使用。
    这也使得库超级容易在你的代码库中扩展,下面是一个例子:
extension Reactive where Base: UIView {
    var borderColor: Binder<UIColor?> {
        return Binder(self.base) { view, color in
            view.layer.borderColor = color?.cgColor
        }
    }
}

如果您还想使用sugar view.theme。边界颜色,你必须写另一个扩展:

extension ThemeProxy where Base: UIView {
    var borderColor: Observable<UIColor?> {
        get { return .empty() }
        set {
            let disposable = newValue
                .takeUntil(base.rx.deallocating)
                .observeOn(MainScheduler.instance)
                .bind(to: base.rx.borderColor)
            hold(disposable, for: "borderColor")
        }
    }
}
2.2.3.8 RxAnimated

2.2.4 图像处理库

2.2.4.1 Kingfisher

2.2.5 资源文件管理库

2.2.5.1 R.swift
2.2.5.2 SwiftLint

2.2.6 秘钥管理库

2.2.6.1 KeychainAccess

2.2.7 自动布局库

2.2.7.1 SnapKit

2.2.8 UI相关库

2.2.8.1 NVActivityIndicatorView
2.2.8.2 ImageSlidershow/Kingfisher
2.2.8.3 DZNEmptyDataSet
2.2.8.4 Hero
  • 源码下载:Hero
2.2.8.5 Localize-Swift
2.2.8.6 RAMAnimatedTabBarController
2.2.8.7 AcknowList
2.2.8.8 KafkaRefresh
2.2.8.9 WhatsNewKit
2.2.8.10 Highlightr
2.2.8.11 DropDown
2.2.8.12 Toast-Swift
2.2.8.13 HMSegmentedControl
2.2.8.14 FloatingPanel
2.2.8.15 MessageKit
2.2.8.16 MultiProgressView
2.2.8.17 IQKeyboardManagerSwift

2.2.9 日志管理库

2.2.9.1 CocoaLumberjack/Swift

2.2.10 数据埋点库

2.2.10.1 Umbrella
2.2.10.2 Umbrella/Mixpanel
2.2.10.3 Umbrella/Firebase
2.2.10.4 Mixpanel
2.2.10.5 Firebace/Analytics

2.2.11 广告工具点库

2.2.11.1 Firebase/AdMob
2.2.11.2 Google-Mobile-Ads-SDK

2.2.12 性能优化相关库

2.2.12.1 Fabric
2.2.12.2 Crashlytics

2.2.13 其他工具类库

2.2.13.1 FLEX
  • 源码下载: FLEX
2.2.13.2 SwifterSwift
2.2.13.3 BonMot
2.2.13.4 DateToolsSwift
2.2.13.5 SwiftDate

3. SwiftHub项目采用的架构分析

参考:https://www.jianshu.com/p/fb63ca356463

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容