开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(一)第三方框架使用
1. SwiftHub项目简介
SwiftHub 是大神Khoren Markosyan 写的一个完全采用Rxswift + MVVM + Moya 的架构的项目,代码很精简,想学习MVVM架构的认真去研究这个项目的设计,对你以后的编程思想和习惯都会有很大的帮助。(点击这里下载:SwiftHub源码 )
1.1 SwiftHub项目UI
1.2 SwiftHub项目代码结构
2. SwiftHub项目编译,用到的第三方库简介
2.1 SwiftHub项目编译
下载源码后,进入SwiftHub-master主目录,先要下载安装第三方库,如果你cd SwiftHub-master/
就直接执行pod install
的话一般都会报错:
分析报错原因不难看出,已经提示我们需要先pod repo update
一下更新你本地的cocos pod库。
可能有的小伙伴网速不太好,pod install一直更新不了,这里提供了一份我编译好的源码:链接:https://pan.baidu.com/s/1qwkjY_ZrgV9Y5yudiyVJdQ 密码:60t7
2.2 SwiftHub项目用到的第三方框架
我只能惊叹,哇塞,怎么用了这么多第三方框架啊,我个人观点是不太主张用太多第三方框架,能自己实现都自己实现,除非要实现的功能必须要用第三方框架。因为第三方框架会大大增加我们ipa包的大小,对于ipa大小有要求的是个灾难,例如之前我们有一个项目使用
Realm
作为DB框架,但是发现这个框架实在是太占内存了足足有将近90MB,而我只是想里面一个小小的数据库存储相关的代码,后面改成WCDB.swift
框架,这个框架只有2MB左右。-
下面我们先来看一下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
Alamofire和AFNetwork 是一对兄弟,是出自同一个公司的产品, 它是一个很好的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+
提供的功能特性:
- 可链请求/响应方法
- URL / JSON参数编码
- 上传文件/数据/流/ MultipartFormData
- 使用请求或简历数据下载文件
- 身份验证与URLCredential
- HTTP响应验证
- 上传和下载进度闭包与进度
- cURL命令输出
- 动态调整和重试请求
- TLS证书和公钥固定
- 网络可达性
- 全面的单元和集成测试覆盖
- 完整的文档
为了让 Alamofire更专注于处理网络相关的事情,Alamofire软件基金会已经创建了额外的组件库来为Alamofire生态系统带来额外的功能。如:AlamofireImage库和 AlamofireNetworkActivityIndicator
- AlamofireImage: 一个图像库,包括图像响应序列化器,UIImage和UIImageView扩展,自定义图像过滤器,一个自动清除内存缓存和一个基于优先级的图像下载系统。
- AlamofireNetworkActivityIndicator : 使用Alamofire控制iOS上的网络活动指示器的可见性。它包含可配置的延迟计时器,以帮助减少闪烁,并支持不受Alamofire管理的URLSession实例。
-
Alamofire框架结构图:
关于Alamofire的使用可以参考我的一些博客:
- 网络请求步骤:
- 设置请求url
- 设置URLRequest对象,配置请求相关信息
- 创建会话配置URLSessionConfiguration
- 创建会话URLSession
- 创建任务和设置请求回调,并发起请求
简单请求代码:
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中方式:
default
:默认模式,常用模式,在该模式下系统会创建持久化缓存,并在用户的钥匙串中保存证书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()
此外还提供了很多属性来按需配置
- 常规属性:
identifier
:配置对象的后台会话标识符httpAdditionalHeaders
:与请求一起发送的附加头文件字典networkServiceType
:网络服务的类型allowsCellularAccess
:一个布尔值,用于是否应通过蜂窝网络进行连接timeoutIntervalForRequest
:等待附加数据的超时时间timeoutIntervalForResource
:资源请求允许的最大时间范围sharedContainerIdentifier
:应将后台URL会话中的文件下载到的共享容器的标识符waitsForConnectivity
:一个布尔值,指示会话是否应等待连接变为可用还是立即失败
- 设置Cookie策略:
httpCookieAcceptPolicy
:决定何时接受cookie的策略常量httpShouldSetCookies
:一个布尔值,确定请求是否包含来自cookie存储区的cookiehttpCookieStorage
:用于会话中存储cookie的cookie存储区HTTPCookie
:该对象为不可变对象,从包含cookie属性的字典初始化,支持两个不同的cookie版本,v0、v1
- 设置安全策略:
TLS协议
:用于在两个通信应用程序之间提供保密性和数据完整性tlsMaximumSupportedProtocol
:在此会话中建立连接时客户端应请求的最大TLS协议版本tlsMinimumSupportedProtocol
:协议协商期间应接受的最小TLS协议urlCredentialStorage
:为身份验证提供凭据的凭据存储区
- 设置缓存策略:
urlCache
:用于为会话中的请求提供缓存响应的URL缓存requestCachePolicy
:决定何时从缓存中返回响应的预定义常量
- 支持后台模式:
sessionSendsLaunchEvents
:一个布尔值,指示当传输完成时,应用程序应在后台恢复还是启动isDiscretionary
:一个布尔值,用于确定后台任务是否可以由系统自行安排已获得最佳性能shouldUseExtendedBackgroundIdleMode
:一个布尔值,指示当应用程序转移到后台时是否应保持TCP连接打开
- 支持自定义协议
protocolClasses
:在会话中处理请求的额外协议子类的数组URLProtocol
:该对象用来处理加载协议特定URL数据
- 支持多路径TCP:
multipathServiceType
:指定用于通过Wi-Fi和蜂窝接口传输数据的多路径TCP连接策略的服务类型
- 设置HTTP策略和代理属性:
httpMaximumConnectionsPerHost
:同时连接到给定主机的最大数量httpShouldUsePipelining
:一个布尔值,用于确定会话是否使用HTTP流水线connectionProxyDictionary
:包含相关要在此会话中使用的代理信息的字典
- 支持连接更改:
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的核心,提供由ReactiveX(主要)定义的Rx标准。它没有其他依赖项。
RxCocoa
: 为一般的iOS/macOS/watchOS & tvOS应用程序开发提供特定于cocoa的功能,如绑定、特性等。它同时依赖于RxSwift和RxRelay。
RxRelay
: 提供发布中继和行为中继,这两个简单的主题包装器。这取决于RxSwift。
RxTest and RxBlocking
: 为基于rx的系统提供测试功能。这取决于RxSwift。
- 关于Rxswift的使用可以参考我的一些博客:
RxSwift (三)Observable的创建,订阅,销毁
RxSwift(五)(Rxswift对比swift,oc用法)
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等同于swift版的YTKNetwork,那就是比较错误的想法了。Moya的设计思路和YTKNetwork差距非常大。上面我在介绍YTKNetwork时在强调子类和父类,继承,是因为YTKNetwork是比较经典的利用OOP思想(面向对象)设计的产物。基于swift的Moya虽然也有使用到继承,但是它的整体上是以POP思想(Protocol Oriented Programming,面向协议编程)为主导的。
- Moya的模块组成:
Provider
:provider
是一个提供网络请求服务的提供者。通过一些初始化配置之后,在外部可以直接用provider来发起request。Request
:在使用Moya
进行网络请求时,第一步需要进行配置,来生成一个Request。首先按照官方文档,创建一个枚举,遵守TargetType
协议,并实现协议所规定的属性。为什么要创建枚举来遵守协议,枚举结合switch
语句,使得API管理起来比较方便。- 根据创建了一个遵守
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握手,握手过程大致如下:
- 客户端发起握手请求,携带随机数、支持算法列表等参数。
- 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
- 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥加密。
- 服务端通过私钥获取随机数信息。
- 双方根据以上交互的信息生成session ticket,用作该连接后续数据传输的加密密钥。
第3步中,客户端需要验证服务端下发的证书,验证过程有以下两个要点:
- 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
- 客户端需要检查证书的domain域和扩展域,看是否包含本次请求的host。
如果上述两点都校验通过,就证明当前的服务端是可信任的,否则就是不可信任,应当中断当前连接。
当客户端直接使用IP地址发起请求时,请求URL
中的host
会被替换成HTTP
DNS
解析出来的IP,所以在证书验证的第2步,会出现domain
不匹配的情况,导致SSL/TLS握手不成功。
更多详情请参考我之前写的一篇关于HTTPS自签名证书上传下载文件的博客:
IOS 网络协议(一) 自签名证书HTTPS文件上传下载(上)
- HTTPS SSL加密建立连接过程
如下图:
过程详解:
- ①客户端的浏览器向服务器发送请求,并传送客户端SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。
- ②服务器向客户端传送SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。
- ③客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。
- ④用户端随机产生一个用于通讯的“对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。
- ⑤如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的“预主密码”一起传给服务器。
- ⑥如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥能否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。
- ⑦服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通讯的加解密通讯。同时在SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。
- ⑧客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤. ⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
- ⑨服务器向客户端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
- ⑩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
ObjectMapper 是一个使用 Swift 语言编写的数据模型转换框架,我们可以方便的将模型对象转换为JSON,或者JSON生成相应的模型类。
有如下特点:
- 将JSON映射到对象
- 将对象映射到JSON
- 支持嵌套对象(在数组或字典中单独使用)
- 支持映射过程中自定义转换
- 支持结构体
- 支持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提供了一些类型转换如
DateTransform
、DataTransform
、HexColorTransform
,但是没有提供的就需要我们自定义,下面举例实现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:
该框架可以结合 Alamofire 和 ObjectMapper 使用, 为Alamofire的Request类扩展出了
responseObject
和responseArray
方法, 更方便的将网络通信返回的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"]
}
}
没有Rxswift 和 ReactiveSwift 的使用方法:
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提供了可组合的、声明性的和灵活的原语,这些原语是围绕着随时间流逝的价值流的宏大概念构建的。
这些原语可以用来统一地表示常见的Cocoa
和泛型编程模式,它们本质上是一种观察行为,例如委托模式、回调闭包、通知、控制操作、响应链事件和键值观察(KVO
)。
因为所有这些不同的机制都可以用相同的方式表示,所以很容易以声明的方式将它们组合在一起,用更少的意大利面条代码和状态来弥补差距。
2.2.3 Rxswift 框架和相关扩展
2.2.3.1 RxDataSources
- 源码下载:RxDataSources
- O(N)计算差异的算法:
该算法假设所有的部分和项都是唯一的,因此没有歧义。
如果有歧义,回退自动对非动画刷新。- 它应用额外的启发式方法,向分段视图发送最少数量的命令:
尽管运行时间是线性的,但发送命令的首选数量通常比线性少得多
最好(也可能)将更改的数量限制在较小的范围内,如果更改的数量增长为线性,则只需进行正常的重新加载- 支持扩展项目和节结构:
用IdentifiableType和Equatable扩展你的项目,用AnimatableSectionModelType扩展你的部分- 支持两个层次动画的所有组合的节和项目:
节动画:插入,删除,移动
项目动画:插入、删除、移动、重载(如果旧值不等于新值)- 可配置的动画类型插入,重载和删除(自动,淡出,…)
- 示例应用程序
- 随机压力测试(示例app)
- 支持开箱即用的编辑(示例应用程序)
- 适用于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
- 源码下载: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
- 源码下载: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
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
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.
-
RxGesture
此外还支持委托定制:
每个手势识别器都有一个默认的RxGestureRecognizerDelegate。它允许你使用一个策略自定义每个委托方法:
.always
: 对应的委托方法是否返回true.never
: 将返回false到相应的委托方法.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
- 占位操作主要有:
String
Array
Dictionary
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
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
使用来自RxCocoa
的Binder<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
- 源码下载: RxAnimated
2.2.4 图像处理库
2.2.4.1 Kingfisher
2.2.5 资源文件管理库
2.2.5.1 R.swift
- 源码下载:R.swift
2.2.5.2 SwiftLint
- 源码下载: SwiftLint
2.2.6 秘钥管理库
2.2.6.1 KeychainAccess
- 源码下载: KeychainAccess
2.2.7 自动布局库
2.2.7.1 SnapKit
- 源码下载: SnapKit
2.2.8 UI相关库
2.2.8.1 NVActivityIndicatorView
2.2.8.2 ImageSlidershow/Kingfisher
2.2.8.3 DZNEmptyDataSet
- 源码下载: DZNEmptyDataSet
2.2.8.4 Hero
- 源码下载:Hero
2.2.8.5 Localize-Swift
- 源码下载: Localize-Swift
2.2.8.6 RAMAnimatedTabBarController
2.2.8.7 AcknowList
- 源码下载: AcknowList
2.2.8.8 KafkaRefresh
- 源码下载:KafkaRefresh
2.2.8.9 WhatsNewKit
- 源码下载: WhatsNewKit
2.2.8.10 Highlightr
- 源码下载: Highlightr
2.2.8.11 DropDown
- 源码下载:DropDown
2.2.8.12 Toast-Swift
- 源码下载:Toast-Swift
2.2.8.13 HMSegmentedControl
- 源码下载: HMSegmentedControl
2.2.8.14 FloatingPanel
- 源码下载: FloatingPanel
2.2.8.15 MessageKit
- 源码下载: MessageKit
2.2.8.16 MultiProgressView
- 源码下载: MultiProgressView
2.2.8.17 IQKeyboardManagerSwift
2.2.9 日志管理库
2.2.9.1 CocoaLumberjack/Swift
2.2.10 数据埋点库
2.2.10.1 Umbrella
- 源码下载: Umbrella
2.2.10.2 Umbrella/Mixpanel
2.2.10.3 Umbrella/Firebase
2.2.10.4 Mixpanel
- 源码下载: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
- 源码下载: SwifterSwift
2.2.13.3 BonMot
- 源码下载:BonMot
2.2.13.4 DateToolsSwift
- 源码下载:DateToolsSwift
2.2.13.5 SwiftDate
- 源码下载: SwiftDate