• MoyaSwift开发中起着重要的网络交互作用,但是还有不如之处,比如网络不可用时,返回的 Response 为 nil,这时还得去解析相应的 Error Codable 可以帮助我们快速的解析数据,但是一旦声明的属性类型与json中的不...
    • Moya 在Swift开发中起着重要的网络交互作用,但是还有不如之处,比如网络不可用时,返回的 Responsenil,这时还得去解析相应的 Error
    • Codable 可以帮助我们快速的解析数据,但是一旦声明的属性类型与json中的不一致,将无法正常解析; 而且对于模型中自定义属性名的处理也十分繁琐

    解决的方案有很多,不过我比较习惯使用 MoyaMapper ,不仅可以解决上述问题,还提供了多种模型转换数据互转多种数据类型任意存储的便捷方法。掌控Moya的网络请求、数据解析与缓存简直易如反掌。

    MoyaMapper是基于Moya和SwiftyJSON封装的工具,以Moya的plugin的方式来实现间接解析,支持RxSwift

    GitHub: MoyaMapper

    ? 详细的使用请查看手册 https://MoyaMapper.github.io

    特点

    • 支持jsonModel 自动映射 与 自定义映射
    • 无视 json 中值的类型,Model 中属性声明的是什么类型,它就是什么类型
    • 支持 json字符串Model
    • 插件方式,全方位保障Moya.Response,拒绝各种网络问题导致 Responsenil,将各式各样的原因导致的数据加载失败进行统一处理,开发者只需要关注 Response
    • 可选 - 支持数据随意缓存( JSONNumberStringBoolMoya.Response )
    • 可选 - 支持网络请求缓存

    数据解析

    一、插件注入

    附:插件 MoyaMapperPlugin 的详细使用

    1、定义适用于项目接口的 ModelableParameterType

    // statusCodeKey、tipStrKey、 modelKey 可以任意指定级别的路径,如: "error>used"
    struct NetParameter : ModelableParameterType {
        var successValue = "000"
        var statusCodeKey = "retStatus"
        var tipStrKey = "retMsg"
        var modelKey = "retBody"
    }
    

    2、在 MoyaProvider 中使用 MoyaMapperPlugin 插件,并指定 ModelableParameterType

    let lxfNetTool = MoyaProvider<LXFNetworkTool>(plugins: [MoyaMapperPlugin(NetParameter())])
    

    ❗ 使用 MoyaMapperPlugin 插件是整个 MoyaMapper 的核心所在!

    二、Model声明

    Model 需遵守 Modelable 协议

    • MoyaMapper 支持模型自动映射 和 自定义映射
    • 不需要考虑源json数据的真实类型,这里统一按 Model 中属性声明的类型进行转换

    1、一般情况下如下写法即可

    struct CompanyModel: Modelable {
        
        var name : String = ""
        var catchPhrase : String = ""
        
        init() { }
    }
    

    2、如果自定义映射,则可以实现方法 mutating func mapping(_ json: JSON)

    struct CompanyModel: Modelable {
        
        var name : String = ""
        var catchPhrase : String = ""
        
        init() { }
        mutating func mapping(_ json: JSON) {
            self.name = json["nickname"].stringValue
        }
    }
    

    3、支持模型嵌套

    struct UserModel: Modelable {
        
        var id : String = ""
        var name : String = ""
        var company : CompanyModel = CompanyModel()
        
        init() { }
    }
    
    三、Response 解析

    1、以下示例皆使用了 MoyaMapperPlugin ,所以不需要指定 解析路径

    2、如果没有使用 MoyaMapperPlugin 则需要指定 解析路径,否则无法正常解析

    ps: 解析路径 可以使用 a>b 这种形式来解决多级路径的问题

    解析方法如下列表所示

    方法 描述 (支持RxSwift)
    toJSON Response 转 JSON ( toJSON | rx.toJSON)
    fetchString 获取指定路径的字符串( fetchString | rx.fetchString)
    fetchJSONString 获取指定路径的原始json字符串 ( fetchJSONString | rx.fetchJSONString )
    mapResult Response -> MoyaMapperResult (Bool, String) ( mapResult | rx.mapResult )
    mapObject Response -> Model ( mapObject | rx.mapObject)
    mapObjResult Response -> (MoyaMapperResult, Model) ( mapObjResult | rx.mapObjResult)
    mapArray Response -> [Model]( mapArray | rx.mapArray)
    mapArrayResult Response -> (MoyaMapperResult, [Model]) ( mapArrayResult | rx.mapArrayResult)

    ❗除了 fetchJSONString 的默认解析路径是根路径之外,其它方法的默认解析路径为插件对象中的 modelKey

    如果接口请求后 json 的数据结构与下图类似,则使用 MoyaMapper 是最合适不过了

    // Normal
    let model = response.mapObject(MMModel.self)
    print("name -- \(model.name)")
    print("github -- \(model.github)")
    
    // 打印json
    print(response.fetchJSONString())
    
    // Rx
    rxRequest.mapObject(MMModel.self)
        .subscribe(onSuccess: { (model) in
            print("name -- \(model.name)")
            print("github -- \(model.github)")
        }).disposed(by: disposeBag)
    

    附: fetchJSONString的详细使用

    // Normal
    let models = response.mapArray(MMModel.self)
    let name = models[0].name
    print("count -- \(models.count)")
    print("name -- \(name)")
    
    // 打印 json 模型数组中第一个的name
    print(response.fetchString(keys: [0, "name"]))
    
    // Rx
    rxRequest.mapArray(MMModel.self)
        .subscribe(onSuccess: { models in
            let name = models[0].name
            print("count -- \(models.count)")
            print("name -- \(name)")
        }).disposed(by: disposeBag)
    

    附:mapArray的详细使用说明

    // Normal
    let (isSuccess, tipStr) = response.mapResult()
    print("isSuccess -- \(isSuccess)")
    print("tipStr -- \(tipStr)")
    
    // Rx
    rxRequest.mapResult()
        .subscribe(onSuccess: { (isSuccess, tipStr) in
            print("isSuccess -- \(isSuccess)") // 是否为 "000"
            print("retMsg -- \(retMsg)") // "缺少必要参数"
        }).disposed(by: disposeBag)
    

    附:mapResult的详细使用说明

    统一处理网络请求结果

    在APP的实际使用过程中,会遇到各种各样的网络请求结果,如:服务器挂了、手机无网络,此时 Moya 返回的 Response 为 nil,这样我们就不得不去判断 Error。但是使用 MoyaMapperPlugin 就可以让我们只关注 Response

    // MoyaMapperPlugin 的初始化方法
    public init<T: ModelableParameterType>(
        _ type: T,
        transformError: Bool = true
    )
    
    type : ModelableParameterType  用于定义字段路径,做为全局解析数据的依据
    transformError : Bool  是否当网络请求失败时,自动转换请求结果,默认为 true
    
    • 当请求失败的时候,此时的 result.responsenil,根据transformError是否为true 判断是否创建一个自定义的 response 并返回出去。

    ➡ 本来可以请求到的数据内容

    ➡ 现在关闭网络,再请求数据

    • 正常情况下,即不做任何不处理的时候, Responsenil

    • 经过 MoyaMapperPlugin 处理的后可得到转换后的 Response ,如图

    这里将请求失败进行了统一处理,无论是服务器还是自身网络的问题,retStatus 都为 MMStatusCode.loadFail,但是 errorDescription 会保持原来的样子并赋值给 retMsg

    • retStatus 值会从枚举 MMStatusCode 中取 loadFail.rawValue ,即 700
    • 取 类型为 ModelableParameterTypetypestatusCodeKey 所指定的值 为键名,retMsg也同理

    ps: 这个时候可以通过判断 retStatusresponse.statusCode 是否与 MMStatusCode.loadFail.rawValue 相同来判断是否显示加载失败的空白页占位图

    enum MMStatusCode: Int {
        case cache = 230
        case loadFail = 700
    }
    

    枚举 MMStatusCode 中除了 loadFail ,还有 cache,我们已经知道 loadFail 在数据加载失败的时候会出现,那 cache 是在什么时候出来呢?不急,看下一节就知道了。

    数据缓存

    一、基本使用
    // 缓存
    @discardableResult
    MMCache.shared.cache`XXX`(value : XXX, key: String, cacheContainer: MMCache.CacheContainer = .RAM)  -> Bool
    // 取舍
    MMCache.shared.fetch`XXX`Cache(key: String, cacheContainer: MMCache.CacheContainer = .RAM)
    

    缓存成功会返回一个 Bool 值,这里可不接收

    XXX 所支持类型
    Bool -
    Float -
    Double -
    String -
    JSON -
    Modelable [Modelable]
    Moya.Response -
    Int UInt
    Int8 UInt8
    Int16 UInt16
    Int32 UInt32
    Int64 UInt64

    其中,除了 Moya.Response 之外,其它类型皆是通过 JSON 来实现缓存

    所以,如果你想清除这些类型的缓存,只需要调用如下方法即可

    @discardableResult
    func removeJSONCache(_ key: String, cacheContainer: MMCache.CacheContainer = .RAM) -> Bool
    
    @discardableResult
    func removeAllJSONCache(cacheContainer: MMCache.CacheContainer = .RAM) -> Bool
    

    清除 Moya.Response 则使用如下两个方法

    @discardableResult
    func removeResponseCache(_ key: String) -> Bool
    
    @discardableResult
    func removeAllResponseCache() -> Bool
    

    再来看看MMCache.CacheContainer

    enum CacheContainer {
        case RAM 	// 只缓存于内存的容器
        case hybrid // 缓存于内存与磁盘的容器
    }
    

    这两种容器互不相通,即 即使key相同,使用 hybrid 来缓存后,再通过 RAM 取值是取不到的。

    • RAM : 仅缓存于内存之中,缓存的数据在APP使用期间一直存在
    • hybrid :缓存于内存与磁盘中,APP重启后也可以获取到数据
    二、缓存网络请求

    内部缓存过程:

    1. APP首次启动并进行网络请求,网络数据将缓存起来
    2. APP再次启动并进行网络请求时,会先返回缓存的数据,等请求成功后再返回网络数据
    3. 其它情况只会加载网络数据
    4. 每次成功请求到数据后,都会对缓存的数据进行更新
    // Normal
    func cacheRequest(
        _ target: Target, 
        cacheType: MMCache.CacheKeyType = .default, 
        callbackQueue: DispatchQueue? = nil, 
        progress: Moya.ProgressBlock? = nil, 
        completion: @escaping Moya.Completion
    ) -> Cancellable
    
    // Rx
    func cacheRequest(
        _ target: Base.Target, 
        callbackQueue: DispatchQueue? = nil, 
        cacheType: MMCache.CacheKeyType = .default
    ) -> Observable<Response>
    

    实际上是对 Moya 请求后的 Response 进行缓存。 其实与 Moya 自带的方法相比较只多了一个参数 cacheType: MMCache.CacheKeyType ,定义着缓存中的 key ,默认为 default

    下面是 MMCache.CacheKeyType 的定义

    /**
     let cacheKey = [method]baseURL/path
     
     - default : cacheKey + "?" + parameters
     - base : cacheKey
     - custom : cacheKey + "?" + customKey
     */
    public enum CacheKeyType {
        case `default`
        case base
        case custom(String)
    }
    

    如果你想缓存多页列表数据的最新一页数据,此时使用 default 是不合适的,因为 default 使用的 key 包含了 pageIndex,这样就达不到只缓存 最新一页数据 的目的, 所以这里应该使用 base 或者 custom(String)

    我们可以来试一下带缓存的请求

    /*
     * APP第一次启动并进行网络请求,网络数据将缓存起来
     * APP再次启动并进行网络请求时,会先加载缓存,再加载网络数据
     * 其它情况只会加载网络数据
     * 每次成功请求到数据都会进行数据更新
     */
    lxfNetTool.rx.cacheRequest(.data(type: .all, size: 10, index: 1))
        .subscribe(onNext: { response in
            log.debug("statusCode -- \(response.statusCode)")
        }).disposed(by: disposeBag)
    
    // 传统方式
    /*
    let _ = lxfNetTool.cacheRequest(.data(type: .all, size: 10, index: 1)) { result in
        guard let resp = result.value else { return }
        log.debug("statusCode -- \(resp.statusCode)")
    }
    */
    

    打印结果

    // 首次使用APP
    statusCode -- 200
    
    // 关闭并重新打开APP,再请求一下
    statusCode -- 230
    statusCode -- 200
    
    // 然后再请求一下
    statusCode -- 200
    

    这里的 230 就是 MMStatusCode.cache.rawValue

    CocoaPods

    • 默认安装

    MoyaMapper默认只安装Core下的文件

    pod 'MoyaMapper'
    
    • RxSwift拓展
    pod 'MoyaMapper/Rx'
    
    • 缓存拓展
    pod 'MoyaMapper/MMCache'
    
    • Rx缓存
    pod 'MoyaMapper/RxCache'
    
    展开全文
  • Moya使用demoMoya面向协议Moya的模块组成代码demo Moya Moya是一个网络抽象层,它在底层将Alamofire进行封装,对外提供更简洁的接口供开发者调用。在Objective-C中,大部分开发者会使用AFNetwork进行网络请求,当...

    Moya

    Moya是一个网络抽象层,它在底层将Alamofire进行封装,对外提供更简洁的接口供开发者调用。在Objective-C中,大部分开发者会使用AFNetwork进行网络请求,当业务复杂一些时,会对AFNetwork进行二次封装,编写一个适用于自己项目的网络抽象层。在Objective-C中,有著名的YTKNetwork,它将AFNetworking封装成抽象父类,然后根据每一种不同的网络请求,都编写不同的子类,子类继承父类,来实现请求业务。Moya在项目层次中的地位,有点类似于YTKNetwork。可以看下图对比
    网络请求层级
    如果单纯把Moya等同于swift版的YTKNetwork,那就是比较错误的想法了。Moya的设计思路和YTKNetwork差距非常大。上面我在介绍YTKNetwork时在强调子类和父类,继承,是因为YTKNetwork是比较经典的利用OOP思想(面向对象)设计的产物。基于swiftMoya虽然也有使用到继承,但是它的整体上是以POP思想(Protocol Oriented Programming,面向协议编程)为主导的。

    面向协议

    请看我的其中一篇文章有介绍到

    Moya的模块组成

    Moya层级

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

    代码demo

    //
    //  YShareApI.swift
    //  
    //
    //  Created by bruce yao on 2019/4/10.
    //  Copyright © 2019 bruce yao. All rights reserved.
    //
    
    import UIKit
    import Moya
    import RxCocoa
    import Result
    import SwiftyJSON
    
    //初始rovider
    let YShareApiProvider = MoyaProvider<YShareAPI>(plugins: [RequestLoadingPlugin()])
    
    /** 请求的endpoints)**/
    //请求分类
    enum YShareAPI {
        case shareNavList:
        case shareList(pageSize: Int, pageNum: Int):
    }
    //请求配置
    extension YShareAPI: TargetType {
        //服务器地址
        public var baseURL: URL {
            switch self {
            default:
                return URL(string: HD_Search_Base)!
            }
        }
        
        //各个请求的具体路径
        public var path: String {
            switch self {
            case .shareNavList:
                return "manage/navigation/getNavigationList"
            default:
                return "ddddd/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.swift
    //  
    //
    //  Created by bruce yao on 2019/1/25.
    //  Copyright . All rights reserved.
    //
    
    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文字 进行提示
        }
    }
    

    Controller中使用

    代码片段

    import RxSwift
    import RxCocoa
    import ObjectMapper
    
    YShareApiProvider
                    .rx.request(input.category)
                    .mapObject(YBaseModel<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)
    
    展开全文
  • ///获取APP缓存 func getCacheSize()-&gt; String { // 取出cache文件夹目录 let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first // 取出文件夹下...

    ///获取APP缓存
    func getCacheSize()-> String {

        // 取出cache文件夹目录
        let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first
    
        // 取出文件夹下所有文件数组
        let fileArr = FileManager.default.subpaths(atPath: cachePath!)
    
        //快速枚举出所有文件名 计算文件大小
        var size = 0
        for file in fileArr! {
    
            // 把文件名拼接到路径中
            let path = cachePath! + ("/\(file)")
            // 取出文件属性
            let floder = try! FileManager.default.attributesOfItem(atPath: path)
            // 用元组取出文件大小属性
            for (key, fileSize) in floder {
                // 累加文件大小
                if key == FileAttributeKey.size {
                    size += (fileSize as AnyObject).integerValue
                }
            }
        }
    
        let totalCache = Double(size) / 1024.00 / 1024.00
        return String(format: "%.2f", totalCache)
    }
    

    ///删除APP缓存
    func clearCache() {
    SVProgressHUD.show()
    // 取出cache文件夹目录
    let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first

        let fileArr = FileManager.default.subpaths(atPath: cachePath!)
    
        // 遍历删除
    
        for file in fileArr! {
    
            let path = (cachePath! as NSString).appending("/\(file)")
    
            if FileManager.default.fileExists(atPath: path) {
    
                do {
    
                    try FileManager.default.removeItem(atPath: path)
    
                } catch {
    
    
    
                }
    
            }
    
        }
        SVProgressHUD.dismiss()
        self.getCacheSize()
    }
    
    展开全文
  • 网络请求是 App 中最常用的更能之一,除了 Apple 提供的 URLSession 之外,还有对其进行封装,功能更加强的的 Alamofire等强大的工具,尽管这样,我们依然会在自己的 App 中封装一套网络请求工具,以达到做网络请求时,代码...

    网络请求是 App 中最常用的更能之一,除了 Apple 提供的 URLSession 之外,还有对其进行封装,功能更加强的的 Alamofire等强大的工具,尽管这样,我们依然会在自己的 App 中封装一套网络请求工具,以达到做网络请求时,代码简洁高效.

    封装目的:

    做网络请求的时候尽量的简单,只需要少量的代码即可处理返回的数据

    封装过程:

    1.实现网络请求单例,提供可修改 baseURL和 get post 请求方法.

    enum RequestMethod {
        case post
        case get
    }
    ///请求对象 
    struct Requset {
        init(method: RequestMethod = .post, baseURL: String = "", path: String, parameters: [String : Any]?){
            self.method = method
            self.baseURL = baseURL
            self.path = path
            self.parameters = parameters ?? [:]
        }
        var method: RequestMethod
        var baseURL: String
        var path: String
        var parameters: [String: Any]
    }
    ///网络工具
    class NetworkManager {
        static let shared = NetworkManager("")
        init(_ baseURL: String = "") {
            self.baseUrl = baseURL
        }
        var baseUrl = ""
        func post(path: String,params: [String: Any]?,result: @escaping ((Result<Data,Error>)->())){
            let request = Requset(baseURL:baseUrl, path: path, parameters: params)
            let target = Target(request: request)
            self.request(target: target, result: result)
        }
        func get(path: String,params: [String: Any]?,result: @escaping ((Result<Data,Error>)->())){
            let request = Requset(baseURL:baseUrl, path: path, parameters: params)
            let target = Target(request: request)
            self.request(target: target, result: result)
        }
        private func request(target: Target,result: @escaping ((Result<Data,Error>)->())) {
            MoyaProvider<Target>().request(target) { (res) in
                switch res {
                case .success(let a):
                    result(.success(a.data))
                case .failure:
                    result(.failure(res.error!))
                }
            }
        }
    }
    复制代码

    现在已经可以做网络请求了,比如:把百度首页数据请求下来

    NetworkManager("https://www.baidu.com").get(path: "", params: nil) { (res) in
        let data = try! res.get()
        print(String(data: data, encoding: .utf8))
    }
    复制代码

    Optional("\r\n\r\n\r\n\r\n <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">\r\n <meta http-equiv="content-type" content="text/html;charset=utf-8">\r\n <meta content="always" name="referrer">\r\n <script src="ss1.bdstatic.com/5eN1bjq8AAU…">\r\n

    页面不存在_百度搜索\r\n ....

    2. 二次封装

    仅仅这样封装很明显不能够达到精简的目的,这样其实和直接使用 Alamofire 没啥区别.

    一般后台返回的数据都有固定的格式,比如:

    {
        "msg": "请求成功",
        "code": 1001,
        "data": {...}
    }
    或者:
    {
        "msg": "请求成功",
        "code": 1001,
        "data": [...]
    }
    复制代码

    将其转换成对应的模型:

    public struct DataResponse<T> {
        public init (){}
        public var code: Int = -1
        public var msg: String = ""
        public var data: T?
    }
    public struct ListResponse<T> {
        public init (){}
        public var code: Int = -1
        public var msg: String = ""
        public var data: [T] = []
    }
    复制代码

    我们实现一个 Protocol 继承自 HandyJSON (HandyJSON本身也是协议),然后是我们的 Response 遵守这个协议.

    public protocol RequestProtocol: HandyJSON {
        static func request(api: API, params: [String: Any]?, result: ((ResponseResult<Self>)->())?)
    }
    
    public extension RequestProtocol where Self: HandyJSON {
        static func request(api: API, params: [String: Any]?, result: ((ResponseResult<Self>)->())?) {
            let completionHandle: ((Result<Data,Error>)->()) = { res in
                switch res {
                case .success(let data):
                    let jsonStr = String(data: data, encoding: .utf8)
                    #if DEBUG
                    print("url: \(api.path)")
                    print("response:")
                    print(jsonStr ?? "")
                    #endif
                    ///不是 json 数据,抛出 json 解析失败错误
                    guard let jsonObj = self.self.deserialize(from: jsonStr) else {
                        result?(.failure(.deserializeFailed))
                        return
                    }
                    result?(.success(jsonObj))
                case .failure(_):
                    ///处理错误 抛出去
                    result?(.failure(.requestFailed))
                }
            }
            if api.method == .post {
                NetworkManager.shared.post(path: api.path, params: params, result: completionHandle)
            }else{
                NetworkManager.shared.get(path: api.path, params: params, result: completionHandle)
            }
        }
    }
    复制代码

    然后久可以如下优雅的做请求:

    NetworkManager.shared.baseUrl = "https://api.douban.com"
    BookResponse.request(api: .getBookDetail, params: nil) { (res) in
        guard res.isSuccess else { return }
        print(res.value?.toJSON())
    }
    复制代码

    {"msg":"invalid_apikey","code":104,"request":"POST /v2/book/1220562"}

    API 是这么样的结构体

    public struct API {
        var path: String
        var method: RequestMethod
        init(path: String, method: RequestMethod = .post) {
            self.path = path
            self.method = method
        }
    }
    ///可以通过这种方式 减少硬编码可能会带来的错误
    extension API {
        static let getBookDetail = API(path: "/v2/book/1220562")
    }
    复制代码

    Demo地址

    转载于:https://juejin.im/post/5cbd2475e51d456e2500050a

    展开全文
  • 文章目录开源项目分析(SwiftHub)架构分析(一)1. SwiftHub项目简介2. SwiftHub项目采用的架构 开源项目分析(SwiftHub)架构分析(一) 1. SwiftHub项目简介 2. SwiftHub项目采用的架构 ...

    文章目录

    开源项目分析(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:一个布尔值,指示会话应等待连接可用还是立即失败
    • 基本使用:
    Alamofire.request("https://www.baid.com").responseJSON { (response) in
        
                // original url request
                print("Request: \(String(describing: response.request))")
                // http url response
                print("Response: \(String(describing: response.response))")
                // response serialization result
                print("Result: \(response.result)")
                
                if let json = response.result.value {
                    print("JSON:\(json)") // serialized json response
                }
                
                if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
                    print("Data: \(utf8Text)") // original server data as UTF8 string
                }
            }
    
    • 链式响应
    Alamofire.request("https://www.baidu.com").responseString { (response) in
                print("Response String: \(String(describing: response.result.value))")
                }.responseJSON { (response) in
                    print("Response JSON:\(String(describing: response.result.value)")
            }
    
    • 添加Headersparameter
    let parameters: Parameters = [
                "foo": "bar",
                "baz": ["a", 1],
                "qux": [
                    "x": 1,
                    "y": 2,
                    "z": 3
                ]
            ]
            let headers: HTTPHeaders = [
                "Accept" : "application/json",
                "Authorization" : "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
            ]
            Alamofire.request("https://www.baid.com/post", method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
    
    • 状态码过滤
    Alamofire.request("https://www.baid.com/get")
            .validate(statusCode: 200..<300)
            .validate(contentType: ["application/json"])
                .responseData { (response) in
                    switch response.result {
                    case .success:
                        print("successful")
                    case .failure:
                        print("error")
                    }
    
    • 上传文件
            let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")!
            Alamofire.upload(fileURL, to: "https://www.baid.com/post")
                .uploadProgress { progress in // main queue by default
                    print("Upload Progress: \(progress.fractionCompleted)")
                }
                .downloadProgress { progress in // main queue by default
                    print("Download Progress: \(progress.fractionCompleted)")
                }
                .responseJSON { response in
                    debugPrint(response)
            }
    
    • 上传Data数据
    let imageData = UIImagePNGRepresentation(image)!
          
            Alamofire.upload(imageData, to: "https://www.baid.com/post").responseJSON { response in
                debugPrint(response)
            }
    
    • 文件下载
     Alamofire.download("https://www.baid.com/image/png")
                .downloadProgress { progress in
                    print("Download Progress: \(progress.fractionCompleted)")
                }
                .responseData { response in
                    if let data = response.result.value {
                        let image = UIImage(data: data)
                    }
            }
    
    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)
    
    
    

    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加密建立连接过程

    如下图:
    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数据转换成对象
    AlamofireObjectMapper 简介
    下面是它的样列代码:

    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

    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简介

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

    2.2.3 Rxswift 框架和相关扩展

    2.2.3.1 RxDataSources

    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星星
    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星星
      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

    展开全文
  • 项目第一版网络框架用的是siesta,它的缓存与自动刷新确实很好用而且代码很简洁,但是在文件的上传与下载以及对返回类型需要精确匹配要求这方面就很不友好,所以在第二版的我选择了Moya,它是一个网络抽象层,它在...
  • 封装了moya,链式,类AFN式请求 SwiftHttpRequest Github地址 简书地址 Demo说明 Demo主要介绍Swift的网络部分,代码已更新到swift4 –网络部分 请求均采用 Alamofire 请求封装方式分为: * 1.Moya...
  • 封装了个网络请求库、但是想要手动解析、调出了方法、需要处理的是response中的data数据了 NetWorking是自己封装的类的名字 let ApiProvider = MoyaProvider<NetWorking>(requestClosure:timeoutClosure) ...
  • 之前写过Alamofire的简单使用,但是一般开发中都会对这些第三库封装,然后使用,之前自己封装的demo也是借鉴了一些Moya的设计思路。今天就介绍一下Moya一个帮助你处理网络层的第三方框架。 介绍 Moya Moya是一个...
  • Rx 是微软出品的一个 Funtional Reactive Programming 框架,RxSwift 是它的一个 Swift 版本的实现。 RxSwift 的主要目的是能简单的处理多个异步操作的组合,和事件/数据流。 利用 RxSwift,我们可以把本来要分散...
  • 主要是练习Moya的熟练使用,全文涉及到CYLTabBarController搭建简单易用的框架、Swift和OC互相调用、FLEX显示界面层级UI的属性、ObjectMapper解析数据、Kingfisher加载网络图片、MBProgressHUD融合到请求里自动显示...
  • 我从2009年开始做Android开发,开始接触Swift是在2014年底,当时组里曾经做过一个Demo App,感觉技术还不够成熟没有正式发布。2016年初我们正式使用swift开发上线了销售助手App产品,积累了比较丰富的swift项目实战...
  • swift+moya URLCahe

    2019-09-23 20:28:13
    1、定义获取缓存策略的接口 import Foundation protocol CachePolicyGettable {  var cachePolicy: URLRequest.CachePolicy {get} } 2、TargetType 实现该接口 extension MoyaAPI: TargetType, ...
  • Moya 是一个网络抽象层,默认是基于 Alamofire 的。网上已经有一些不错的原理分析及源码分析的文章,大家可以参考。在这我们从数据流的角度来粗略的描述一下 Moya 的基本实现。我们在此避开各种错误导致的分支流程,...
  • Moya:这是一个基于Alamofire的更高层网络请求封装抽象层。 Reachability.swift:用来检查应用当前的网络连接状况。 综合 Perfect:swift的服务器端开发框架(针对于移动后端开发、网站和web应用程序开发)。 ...
  • Swift 开源项目汇总

    2019-09-16 16:07:05
    Swift 开源项目汇总
  • Swift常用第三方库

    2019-02-16 14:50:16
    Moya:这是一个基于Alamofire的更高层网络请求封装抽象层。 Reachability.swift:用来检查应用当前的网络连接状况。 综合 Perfect:swift的服务器端开发框架(针对于移动后端开发、网站和web应用程序开发)。 ...
  • 之前做了OC的整理,这一篇是Swift的第三方资源库整理。同样的,有好的项目我就会加进来,并把更新说明写在文末。 更直观更方便的内容形式,请访问本文的GitBook地址:GitHub第三方资源库整理(Swift篇)。 或者...
1 2 3 4 5 ... 7
收藏数 133
精华内容 53
热门标签