開源項目分析(SwiftHub)Rxswift + MVVM + Moya 架構分析(一)第三方框架使用

開源項目分析(SwiftHub)Rxswift + MVVM + Moya 架構分析(一)第三方框架使用

1. SwiftHub項目簡介

SwiftHub 是大神Khoren Markosyan 寫的一個完全采用Rxswift + MVVM + Moya 的架構的項目钝侠,代碼很精簡酬凳,想學習MVVM架構的認真去研究這個項目的設計,對你以后的編程思想和習慣都會有很大的幫助子檀。(點擊這里下載:SwiftHub源碼

SwiftHub項目簡介

1.1 SwiftHub項目UI

UI頁面1
SwiftHub UI2

SwiftHub UI3
SwiftHub UI4

1.2 SwiftHub項目代碼結構

SwiftHub項目代碼結構

2. SwiftHub項目編譯,用到的第三方庫簡介

2.1 SwiftHub項目編譯

下載源碼后致燥,進入SwiftHub-master主目錄配喳,先要下載安裝第三方庫蹂安,如果你cd SwiftHub-master/ 就直接執(zhí)行pod install的話一般都會報錯:

pod install的報錯

分析報錯原因不難看出,已經(jīng)提示我們需要先pod repo update 一下更新你本地的cocos pod庫彰阴。

pod repo update一下

可能有的小伙伴網(wǎng)速不太好瘾敢,pod install一直更新不了,這里提供了一份我編譯好的源碼:鏈接:https://pan.baidu.com/s/1qwkjY_ZrgV9Y5yudiyVJdQ 密碼:60t7

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

  • 我只能驚嘆尿这,哇塞簇抵,怎么用了這么多第三方框架啊,我個人觀點是不太主張用太多第三方框架射众,能自己實現(xiàn)都自己實現(xiàn)碟摆,除非要實現(xiàn)的功能必須要用第三方框架。因為第三方框架會大大增加我們ipa包的大小叨橱,對于ipa大小有要求的是個災難典蜕,例如之前我們有一個項目使用Realm 作為DB框架,但是發(fā)現(xiàn)這個框架實在是太占內存了足足有將近90MB罗洗,而我只是想里面一個小小的數(shù)據(jù)庫存儲相關的代碼愉舔,后面改成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 網(wǎng)絡庫

用到的網(wǎng)絡庫
2.2.1.1 Alamofire

AlamofireAFNetwork 是一對兄弟火的,是出自同一個公司的產(chǎn)品, 它是一個很好的Swift編寫的網(wǎng)絡框架庫壶愤,提供了HTTP相關接口,能輕松實現(xiàn)鏈式請求和響應馏鹤,能實現(xiàn)文件上傳征椒,下載,斷點續(xù)傳假瞬,后臺下載等功能陕靠。

安裝方式:

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

安裝環(huán)境要求:

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

提供的功能特性:

  1. 可鏈請求/響應方法
  2. URL / JSON參數(shù)編碼
  3. 上傳文件/數(shù)據(jù)/流/ MultipartFormData
  4. 使用請求或簡歷數(shù)據(jù)下載文件
  5. 身份驗證與URLCredential
  6. HTTP響應驗證
  7. 上傳和下載進度閉包與進度
  8. cURL命令輸出
  9. 動態(tài)調整和重試請求
  10. TLS證書和公鑰固定
  11. 網(wǎng)絡可達性
  12. 全面的單元和集成測試覆蓋
  13. 完整的文檔

為了讓 Alamofire更專注于處理網(wǎng)絡相關的事情,Alamofire軟件基金會已經(jīng)創(chuàng)建了額外的組件庫來為Alamofire生態(tài)系統(tǒng)帶來額外的功能脱茉。如:AlamofireImage庫和 AlamofireNetworkActivityIndicator

  1. AlamofireImage: 一個圖像庫剪芥,包括圖像響應序列化器,UIImage和UIImageView擴展琴许,自定義圖像過濾器税肪,一個自動清除內存緩存和一個基于優(yōu)先級的圖像下載系統(tǒng)。
  2. AlamofireNetworkActivityIndicator : 使用Alamofire控制iOS上的網(wǎng)絡活動指示器的可見性榜田。它包含可配置的延遲計時器益兄,以幫助減少閃爍,并支持不受Alamofire管理的URLSession實例箭券。
  • Alamofire框架結構圖:

    Alamofire框架結構圖

  • 關于Alamofire的使用可以參考我的一些博客:

Alamofire學習(一)網(wǎng)絡基礎

Alamofire(二)URLSession

Alamofire(三)后臺下載原理

  • 網(wǎng)絡請求步驟:
  1. 設置請求url
  2. 設置URLRequest對象净捅,配置請求相關信息
  3. 創(chuàng)建會話配置URLSessionConfiguration
  4. 創(chuàng)建會話URLSession
  5. 創(chuàng)建任務和設置請求回調,并發(fā)起請求

簡單請求代碼:

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:默認模式辩块,常用模式蛔六,在該模式下系統(tǒng)會創(chuàng)建持久化緩存,并在用戶的鑰匙串中保存證書
  2. ephemeral:不支持持久性存儲废亭,所有內容的會隨著session的生命周期結束而釋放
    background:與default模式類似国章,在該模式下會創(chuàng)建一個獨立線程來傳輸網(wǎng)絡請求數(shù)據(jù),可以在后臺乃至APP關閉的時候也可以進行數(shù)據(jù)傳輸
  • 創(chuàng)建會話:
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()

此外還提供了很多屬性來按需配置

  • 常規(guī)屬性:
  1. identifier:配置對象的后臺會話標識符
  2. httpAdditionalHeaders:與請求一起發(fā)送的附加頭文件字典
  3. networkServiceType:網(wǎng)絡服務的類型
  4. allowsCellularAccess:一個布爾值豆村,用于是否應通過蜂窩網(wǎng)絡進行連接
  5. timeoutIntervalForRequest:等待附加數(shù)據(jù)的超時時間
  6. timeoutIntervalForResource:資源請求允許的最大時間范圍
  7. sharedContainerIdentifier:應將后臺URL會話中的文件下載到的共享容器的標識符
  8. waitsForConnectivity:一個布爾值液兽,指示會話是否應等待連接變?yōu)榭捎眠€是立即失敗
  • 設置Cookie策略:
  1. httpCookieAcceptPolicy:決定何時接受cookie的策略常量
  2. httpShouldSetCookies:一個布爾值,確定請求是否包含來自cookie存儲區(qū)的cookie
  3. httpCookieStorage:用于會話中存儲cookie的cookie存儲區(qū)
  4. HTTPCookie:該對象為不可變對象掌动,從包含cookie屬性的字典初始化四啰,支持兩個不同的cookie版本,v0坏匪、v1
  • 設置安全策略:
  1. TLS協(xié)議:用于在兩個通信應用程序之間提供保密性和數(shù)據(jù)完整性
  2. tlsMaximumSupportedProtocol:在此會話中建立連接時客戶端應請求的最大TLS協(xié)議版本
  3. tlsMinimumSupportedProtocol:協(xié)議協(xié)商期間應接受的最小TLS協(xié)議
  4. urlCredentialStorage:為身份驗證提供憑據(jù)的憑據(jù)存儲區(qū)
  • 設置緩存策略:
  1. urlCache:用于為會話中的請求提供緩存響應的URL緩存
  2. requestCachePolicy:決定何時從緩存中返回響應的預定義常量
  • 支持后臺模式:
  1. sessionSendsLaunchEvents:一個布爾值拟逮,指示當傳輸完成時,應用程序應在后臺恢復還是啟動
  2. isDiscretionary:一個布爾值适滓,用于確定后臺任務是否可以由系統(tǒng)自行安排已獲得最佳性能
  3. shouldUseExtendedBackgroundIdleMode:一個布爾值敦迄,指示當應用程序轉移到后臺時是否應保持TCP連接打開
  • 支持自定義協(xié)議
  1. protocolClasses:在會話中處理請求的額外協(xié)議子類的數(shù)組
  2. URLProtocol:該對象用來處理加載協(xié)議特定URL數(shù)據(jù)
  • 支持多路徑TCP:
  1. multipathServiceType:指定用于通過Wi-Fi和蜂窩接口傳輸數(shù)據(jù)的多路徑TCP連接策略的服務類型
  • 設置HTTP策略和代理屬性:
  1. httpMaximumConnectionsPerHost:同時連接到給定主機的最大數(shù)量
  2. httpShouldUsePipelining:一個布爾值,用于確定會話是否使用HTTP流水線
  3. connectionProxyDictionary:包含相關要在此會話中使用的代理信息的字典
  • 支持連接更改:
  1. waitsForConnectivity:一個布爾值,指示會話應等待連接可用還是立即失敗
2.2.1.2 Rxswift

Rxswift家族提供非常好的函數(shù)響應式編程框架罚屋,使用Rxswift編寫代碼可以讓代碼變得非常簡潔苦囱,邏輯清晰,如果配合Moya + Rxswift + MVVM架構脾猛,真的是很完美撕彤,這個開源項目SwiftHub就是這樣的一個完美的項目。

ReactiveX(簡寫:Rx)是一個可以幫助我們簡化異步編程的框架猛拴。而 RxSwift 是 Rx 的 Swift 版本羹铅。除了 RxSwift,還有 RxJava愉昆、RxJS职员、Rx.Net 等,對應的OC 版本則是 RAC(ReactiveCocoa)跛溉,這里是 RxSwift 的 github 地址 焊切,已經(jīng)有了將近 18.2K 顆星了。

Rxswift簡潔圖
Rxswift構成圖

RxSwift: RxSwift的核心芳室,提供由ReactiveX(主要)定義的Rx標準专肪。它沒有其他依賴項。
RxCocoa: 為一般的iOS/macOS/watchOS & tvOS應用程序開發(fā)提供特定于cocoa的功能堪侯,如綁定嚎尤、特性等。它同時依賴于RxSwift和RxRelay伍宦。
RxRelay: 提供發(fā)布中繼和行為中繼诺苹,這兩個簡單的主題包裝器。這取決于RxSwift雹拄。
RxTest and RxBlocking: 為基于rx的系統(tǒng)提供測試功能。這取決于RxSwift掌呜。

  • 關于Rxswift的使用可以參考我的一些博客:

Rxswift(一)函數(shù)響應式編程思想

RxSwift (二)序列核心邏輯分析

RxSwift (三)Observable的創(chuàng)建滓玖,訂閱,銷毀

RxSwift(四)高階函數(shù)

RxSwift(五)(Rxswift對比swift质蕉,oc用法)

Rxswift (六)銷毀者Dispose源碼分析

RxSwift(七)Rxswift對比swift用法

RxSwift (十) 基礎使用篇 1- 序列势篡,訂閱,銷毀

RxSwift學習之十二 (基礎使用篇 3- UI控件擴展)

Rxswift一些簡單使用如下:

  • button點擊事件:
//MARK: - RxSwift應用-button響應
func setupButton() {
    // 傳統(tǒng)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() {
    // 我們如果要對輸入的文本進行操作 - 比如輸入的的內容 然后我們獲取里面的偶數(shù)
    // self.textFiled.delegate = self
    // 感覺是不是特別惡心
    // 下面我們來看看Rx
    self.textFiled.rx.text.orEmpty.changed.subscribe(onNext: { (text) in
        print("監(jiān)聽到了 - \(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() {
    // 系統(tǒng)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)  
}
  • 網(wǎng)絡請求
//MARK: - RxSwift應用-網(wǎng)絡請求
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是一個網(wǎng)絡抽象層模暗,它在底層將Alamofire進行封裝禁悠,對外提供更簡潔的接口供開發(fā)者調用。在Objective-C中兑宇,大部分開發(fā)者會使用AFNetwork進行網(wǎng)絡請求碍侦,當業(yè)務復雜一些時叫潦,會對AFNetwork進行二次封裝途戒,編寫一個適用于自己項目的網(wǎng)絡抽象層。在Objective-C中,有著名的YTKNetwork警检,它將AFNetworking封裝成抽象父類,然后根據(jù)每一種不同的網(wǎng)絡請求娜庇,都編寫不同的子類炫贤,子類繼承父類,來實現(xiàn)請求業(yè)務尔邓。Moya在項目層次中的地位晾剖,有點類似于YTKNetwork√菟裕可以看下圖對比


Moya 扮演的角色

如果單純把Moya等同于swift版的YTKNetwork齿尽,那就是比較錯誤的想法了。Moya的設計思路和YTKNetwork差距非常大慷荔。上面我在介紹YTKNetwork時在強調子類和父類雕什,繼承,是因為YTKNetwork是比較經(jīng)典的利用OOP思想(面向對象)設計的產(chǎn)物显晶〈叮基于swift的Moya雖然也有使用到繼承,但是它的整體上是以POP思想(Protocol Oriented Programming,面向協(xié)議編程)為主導的磷雇。

  • Moya的模塊組成:
Moya的模塊組成
  1. Providerprovider是一個提供網(wǎng)絡請求服務的提供者偿警。通過一些初始化配置之后,在外部可以直接用provider來發(fā)起request唯笙。
  2. Request:在使用Moya進行網(wǎng)絡請求時螟蒸,第一步需要進行配置,來生成一個Request崩掘。首先按照官方文檔七嫌,創(chuàng)建一個枚舉,遵守TargetType協(xié)議苞慢,并實現(xiàn)協(xié)議所規(guī)定的屬性诵原。為什么要創(chuàng)建枚舉來遵守協(xié)議,枚舉結合switch語句挽放,使得API管理起來比較方便绍赛。
  3. 根據(jù)創(chuàng)建了一個遵守TargetType協(xié)議的名為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
        }
    }
    
    //請求任務事件(這里附帶上參數(shù))
    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)
        }
    }
    //是否執(zhí)行Alamofire驗證
    public var validate: Bool {
        return false
    }
    //這個就是做單元測試模擬的數(shù)據(jù)吗蚌,
//    只會在單元測試文件中有作用
    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相關,捕獲網(wǎng)絡異常等操作纯出,給出提示蚯妇,代碼如下:
```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 = "網(wǎng)絡不給力,請檢查您的網(wǎng)絡"
        }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("請求成功 返回數(shù)據(jù)如下")
                    if baseModel.status != 0 {
                        return
                    }
                }, onError: {error in
                    print("Error:請求錯誤")
                }).disposed(by: self.disposeBag)
            }, onError: { (error) in
                
        }, onCompleted: {
            
        }) {
            }.disposed(by: disposeBag)


2.2.1.4 Moya HTTPS 證書信任侮措,自簽名證書信任

Moya HTTPS 證書信任懈叹,自簽名證書信任

  • 開發(fā)中,我們可能服務器用的是HTTPS的方式分扎,這個時候如果服務器端是使用的證書頒發(fā)機構的證書澄成,我們使用Moya的時候不需要做什么處理,如果是自簽名證書的話畏吓,需要做一下證書信任墨状,不然請求直接被拒絕。

可能有些朋友不太熟悉HTTPS握手的過程菲饼,要理解證書認證機制肾砂,有必要理解一下HTTPS握手過程:

發(fā)送HTTPS請求首先要進行SSL/TLS握手,握手過程大致如下:

  1. 客戶端發(fā)起握手請求宏悦,攜帶隨機數(shù)镐确、支持算法列表等參數(shù)。
  2. 服務端收到請求饼煞,選擇合適的算法源葫,下發(fā)公鑰證書和隨機數(shù)。
  3. 客戶端對服務端證書進行校驗砖瞧,并發(fā)送隨機數(shù)信息息堂,該信息使用公鑰加密。
  4. 服務端通過私鑰獲取隨機數(shù)信息块促。
  5. 雙方根據(jù)以上交互的信息生成session ticket荣堰,用作該連接后續(xù)數(shù)據(jù)傳輸?shù)募用苊荑€。

第3步中竭翠,客戶端需要驗證服務端下發(fā)的證書振坚,驗證過程有以下兩個要點:

  1. 客戶端用本地保存的根證書解開證書鏈,確認服務端下發(fā)的證書是由可信任的機構頒發(fā)的斋扰。
  2. 客戶端需要檢查證書的domain域和擴展域屡拨,看是否包含本次請求的host。
    如果上述兩點都校驗通過褥实,就證明當前的服務端是可信任的,否則就是不可信任裂允,應當中斷當前連接损离。

當客戶端直接使用IP地址發(fā)起請求時,請求URL中的host會被替換成HTTP DNS解析出來的IP绝编,所以在證書驗證的第2步僻澎,會出現(xiàn)domain不匹配的情況貌踏,導致SSL/TLS握手不成功。

更多詳情請參考我之前寫的一篇關于HTTPS自簽名證書上傳下載文件的博客:

IOS 網(wǎng)絡協(xié)議(一) 自簽名證書HTTPS文件上傳下載(上)

IOS音視頻(四十五)HTTPS 自簽名證書 實現(xiàn)邊下邊播

  • HTTPS SSL加密建立連接過程

如下圖:



過程詳解:

  1. ①客戶端的瀏覽器向服務器發(fā)送請求窟勃,并傳送客戶端SSL 協(xié)議的版本號祖乳,加密算法的種類,產(chǎn)生的隨機數(shù)秉氧,以及其他服務器和客戶端之間通訊所需要的各種信息眷昆。
  2. ②服務器向客戶端傳送SSL 協(xié)議的版本號,加密算法的種類汁咏,隨機數(shù)以及其他相關信息亚斋,同時服務器還將向客戶端傳送自己的證書。
  3. ③客戶端利用服務器傳過來的信息驗證服務器的合法性攘滩,服務器的合法性包括:證書是否過期帅刊,發(fā)行服務器證書的CA 是否可靠,發(fā)行者證書的公鑰能否正確解開服務器證書的“發(fā)行者的數(shù)字簽名”漂问,服務器證書上的域名是否和服務器的實際域名相匹配赖瞒。如果合法性驗證沒有通過,通訊將斷開蚤假;如果合法性驗證通過栏饮,將繼續(xù)進行第四步。
  4. ④用戶端隨機產(chǎn)生一個用于通訊的“對稱密碼”勤哗,然后用服務器的公鑰(服務器的公鑰從步驟②中的服務器的證書中獲得)對其加密抡爹,然后將加密后的“預主密碼”傳給服務器。
  5. ⑤如果服務器要求客戶的身份認證(在握手過程中為可選)芒划,用戶可以建立一個隨機數(shù)然后對其進行數(shù)據(jù)簽名冬竟,將這個含有簽名的隨機數(shù)和客戶自己的證書以及加密過的“預主密碼”一起傳給服務器。
  6. ⑥如果服務器要求客戶的身份認證民逼,服務器必須檢驗客戶證書和簽名隨機數(shù)的合法性泵殴,具體的合法性驗證過程包括:客戶的證書使用日期是否有效,為客戶提供證書的CA 是否可靠拼苍,發(fā)行CA 的公鑰能否正確解開客戶證書的發(fā)行CA 的數(shù)字簽名笑诅,檢查客戶的證書是否在證書廢止列表(CRL)中。檢驗如果沒有通過疮鲫,通訊立刻中斷吆你;如果驗證通過,服務器將用自己的私鑰解開加密的“預主密碼”俊犯,然后執(zhí)行一系列步驟來產(chǎn)生主通訊密碼(客戶端也將通過同樣的方法產(chǎn)生相同的主通訊密碼)妇多。
  7. ⑦服務器和客戶端用相同的主密碼即“通話密碼”,一個對稱密鑰用于SSL 協(xié)議的安全數(shù)據(jù)通訊的加解密通訊燕侠。同時在SSL 通訊過程中還要完成數(shù)據(jù)通訊的完整性者祖,防止數(shù)據(jù)通訊中的任何變化立莉。
  8. ⑧客戶端向服務器端發(fā)出信息,指明后面的數(shù)據(jù)通訊將使用的步驟. ⑦中的主密碼為對稱密鑰七问,同時通知服務器客戶端的握手過程結束蜓耻。
  9. ⑨服務器向客戶端發(fā)出信息,指明后面的數(shù)據(jù)通訊將使用的步驟⑦中的主密碼為對稱密鑰械巡,同時通知客戶端服務器端的握手過程結束刹淌。
  10. ⑩SSL 的握手部分結束,SSL 安全通道的數(shù)據(jù)通訊開始坟比,客戶和服務器開始使用相同的對稱密鑰進行數(shù)據(jù)通訊芦鳍,同時進行通訊完整性的檢驗。

要解決 Moya HTTPS 證書信任葛账,自簽名證書信任柠衅,請求被拒絕的問題,有以下幾種方式:

  • 方法一: 簡單粗暴的方法籍琳,關閉HTTPS證書認證(不推薦)菲宴。

這種方式相當于將HTTPS還原變成了HTTP了,非常不安全趋急,數(shù)據(jù)容易被竊取喝峦。

/// 關閉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)])


方法一是一種變通實現(xiàn)的方法,它直接關閉了Https的Domain驗證呜达,雖然可以請求正常進行谣蠢,但是如果在客戶端和服務器之間增加代理,請求發(fā)送時代理替換證書查近,那么代理就可以輕易拿到請求的數(shù)據(jù)眉踱,出于安全考慮并不推薦這種做法。

  • 方法二:驗證自簽名證書霜威,CN 是否合法(推薦):

ServerTrustPolicy枚舉中使用pinCertificates谈喳。

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

具體實現(xiàn)代碼如下:

//
//  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 參數(shù):\(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?
        
        //獲取服務器發(fā)送過來的證書
        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)
        
    }
    
    //發(fā)送客戶端證書交由服務器驗證
    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 數(shù)據(jù)解析庫

2.2.2.1 ObjectMapper

ObjectMapper 是一個使用 Swift 語言編寫的數(shù)據(jù)模型轉換框架扭倾,我們可以方便的將模型對象轉換為JSON,或者JSON生成相應的模型類挽绩。

有如下特點:

  1. 將JSON映射到對象
  2. 將對象映射到JSON
  3. 支持嵌套對象(在數(shù)組或字典中單獨使用)
  4. 支持映射過程中自定義轉換
  5. 支持結構體
  6. 支持Immutable
  • 模型類定義:

創(chuàng)建模型類需要實現(xiàn)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阻止映射發(fā)生
    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轉模型數(shù)組
let users: [User] = Mapper<User>().mapArray(JSONString: JSONString)
  • 模型類轉JSON字符串:
//prettyPrint參數(shù)是為了打印可讀性json
let JSONString = user.toJSONString(prettyPrint: true)
//使用Mapper
let JSONString = Mapper().toJSONString(users, prettyPrint: true)
  • 支持的類型:
Int
Bool
Double
Float
String
RawRepresentable (Enums)
Array<Any>
Dictionary<String, Any>
Object<T: Mappable>
Array<T: Mappable>
Array<Array<T: Mappable>>
Set<T: Mappable>
Dictionary<String, T: Mappable>
Dictionary<String, Array<T: Mappable>>
Optionals of all the above //上述的可選類型
Implicitly Unwrapped Optionals of the above //上述的隱式解析可選類型

  • 嵌套對象的簡單映射:
  import ObjectMapper

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


    required init?(map: Map) {
    }
    
    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
        weight      <- map["weight"]
        dictionary  <- map["dictionary"]
        value  <- map["dictionary.username"]
    }
}
  • 自定義轉換: ObjectMapper提供了一些類型轉換如DateTransform琼牧、DataTransform恢筝、HexColorTransform,但是沒有提供的就需要我們自定義巨坊,下面舉例實現(xiàn)NSURLTransform

說到json解析字典轉模型撬槽,我們可能經(jīng)常用到有 SwiftyJSON 或者 HandyJSON 這些框架。
SwiftyJSON 只是把JSON 字符串轉換為字典趾撵,需要我們自己去按key 去或者對應值侄柔。這樣比較靈活,但是麻煩占调,適合于自定定義模型里面含有計算型屬性的模型暂题。
HandyJSON 會自動幫我把JSON字符串通過反射轉換為我們定義的那個模型,這個模型需要繼承 HandyJSON ,如下代碼:

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

而ObjectMapper呢是面向協(xié)議編程的究珊,代碼沒有 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 方法, 更方便的將網(wǎng)絡通信返回的JSON數(shù)據(jù)轉換成對象


下面是它的樣列代碼:

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

    let weatherResponse = response.result.value

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

安裝方式:

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

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

pod 'Moya-ObjectMapper/ReactiveSwift'
  • 使用:
    先創(chuàng)建一個模型:
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提供了可組合的、聲明性的和靈活的原語瞬浓,這些原語是圍繞著隨時間流逝的價值流的宏大概念構建的初婆。
這些原語可以用來統(tǒng)一地表示常見的Cocoa和泛型編程模式,它們本質上是一種觀察行為猿棉,例如委托模式磅叛、回調閉包、通知铺根、控制操作宪躯、響應鏈事件和鍵值觀察(KVO)。
因為所有這些不同的機制都可以用相同的方式表示位迂,所以很容易以聲明的方式將它們組合在一起访雪,用更少的意大利面條代碼和狀態(tài)來彌補差距。

2.2.3 Rxswift 框架和相關擴展

2.2.3.1 RxDataSources
  1. O(N)計算差異的算法:
    該算法假設所有的部分和項都是唯一的掂林,因此沒有歧義臣缀。
    如果有歧義,回退自動對非動畫刷新泻帮。
  2. 它應用額外的啟發(fā)式方法精置,向分段視圖發(fā)送最少數(shù)量的命令:
    盡管運行時間是線性的,但發(fā)送命令的首選數(shù)量通常比線性少得多
    最好(也可能)將更改的數(shù)量限制在較小的范圍內锣杂,如果更改的數(shù)量增長為線性脂倦,則只需進行正常的重新加載
  3. 支持擴展項目和節(jié)結構:
    用IdentifiableType和Equatable擴展你的項目番宁,用AnimatableSectionModelType擴展你的部分
  4. 支持兩個層次動畫的所有組合的節(jié)和項目:
    節(jié)動畫:插入,刪除赖阻,移動
    項目動畫:插入蝶押、刪除、移動火欧、重載(如果舊值不等于新值)
  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:根據(jù)閉包忽略元素蹬叭。
 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:將下一個元素精確地發(fā)送一次到接收它的第一個訂閱服務器藕咏。進一步的訂閱者將得到一個空序列。
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:只有在序列中從未出現(xiàn)過元素時秽五,才將它們傳遞過去孽查。
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:驗證發(fā)出的每個值都為真
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:順序級聯(lián)通過一系列可觀察對象,當一個可觀察對象在列表的更下方開始發(fā)射元素時瓣铣,立即放棄之前的訂閱答朋。
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:將一個可觀察對象發(fā)出的元素分組成數(shù)組,其中每個數(shù)組由最后兩個連續(xù)的項組成;類似于滑動窗口棠笑。
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:將一個可觀察對象發(fā)出的元素分組成數(shù)組辰企,其中每個數(shù)組由最后的N個連續(xù)項組成;類似于滑動窗口硫椰。
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:在發(fā)生錯誤或成功終止之前猾骡,使用給定的行為重復源觀察到的序列。有四種具有不同謂詞和延遲選項的行為: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:當源觀察序列完成時库车,使用給定的行為重復它。此操作符接受與重試操作符相同的參數(shù)樱拴。有四種具有不同謂詞和延遲選項的行為: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:當發(fā)生錯誤時,取消錯誤條件牺勾,完成一個序列
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為在可觀察的序列上應用轉換提供了一種統(tǒng)一的機制驻民,而不必擴展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)
    }

上面的序列保持偶數(shù)2裆泳、4、6柠硕,并產(chǎn)生序列4工禾、8、12蝗柔。

  • errors, elements:這些操作符只適用于使用materialize()操作符(來自RxSwift core)物化的可觀察序列闻葵。錯誤返回一個經(jīng)過過濾的錯誤事件序列,即拋出的元素癣丧。元素返回一個經(jīng)過過濾的元素事件序列槽畔,拋出錯誤。
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:將簡單的異步完成處理程序轉換為可觀察的序列胁编。適合與僅使用一個參數(shù)調用完成處理程序的現(xiàn)有異步服務一起使用厢钧。發(fā)出由完成處理程序生成的結果,然后完成掏呼。
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(_:)坏快。將指定的可觀察序列合并為一個可觀察序列,只要所有的可觀察序列在相應的索引處產(chǎn)生一個元素憎夷,就使用選擇器函數(shù)莽鸿。
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:)操作符提供了一個未保留的兔沃、可以安全使用(即不隱式取消包裝)的對象引用,以及序列發(fā)出的事件级及。如果提供的對象不能成功保留乒疏,則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:在一個可觀察對象終止且沒有錯誤時發(fā)出的項數(shù)。如果給定一個謂詞饮焦,則只計算與謂詞匹配的元素怕吴。
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:收集源可觀察到的元素转绷,并在觸發(fā)器發(fā)出時將它們作為數(shù)組發(fā)出。
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一般你經(jīng)常需要這樣子let disposeBag = DisposeBag()定義一個垃圾袋對象硼啤,用來銷毀回收序列的資源议经。每個類中都要去定義這樣一個東東是很麻煩的。而NSObject+Rx幫你簡化了這部操作谴返,你可以不需要定義let disposeBag = DisposeBag()這樣的代碼了煞肾,直接ob.rx.disposeBag就可以了,例如:

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

Add to your Podfile:

pod 'NSObject+Rx'

Carthage

Add to Cartfile:

github "RxSwiftCommunity/NSObject-Rx"
2.2.3.4 RxViewController

RxViewController是用于UIViewController和NSViewController的RxSwift包裝器嗓袱。

有了RxViewController的包裝后籍救,你可以這樣在VC中調用viewDidLoad方法:

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

此外RxViewController還提供了以下這些API:

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

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

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

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

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

  var didReceiveMemoryWarning: ControlEvent<Void>
}
2.2.3.5 RxGesture


RxGesture可以讓你輕松地將任何視圖變成可移動或可滑動的控件,就像這樣:

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

你也可以對多種手勢做出反應渠抹。例如钧忽,當用戶點擊或上下滑動照片預覽時,你可能想要關閉它:

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

rx.gesture被定義為Observable<G>其中G是手勢識別器的實際類型所以它發(fā)出的是手勢識別器本身(如果想調用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
    手勢過濾:
    默認情況下大刊,手勢識別器的狀態(tài)沒有過濾器。這意味著您將始終接收到帶有手勢識別器初始狀態(tài)的第一個事件(幾乎總是.possible)三椿。

默認情況下缺菌,手勢識別器的狀態(tài)沒有過濾器。這意味著搜锰,這里有可以用于各種手勢(iOS和macOS)的首選狀態(tài):



通常使用.when()操作符過濾狀態(tài):

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

如果你同時觀察多個手勢伴郁,你可以使用when()操作符,如果你想過濾所有手勢識別器的相同狀態(tài)蛋叼,或者使用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 : 獲取將執(zhí)行的關聯(lián)閉包剂陡,以將值返回給相應的委托方法

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

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

這個委托可以在配置包中定制:

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

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

  • RxGesture
    次外還支持完全自定義方式:

您還可以用自己的委托替換默認委托狐胎,或者刪除它鸭栖。代碼如下:

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

Add this to Podfile

pod "RxGesture"

$ pod install
Carthage

Add this to Cartfile

github "RxSwiftCommunity/RxGesture" ~> 3.0

$ carthage update

2.2.3.6 RxOptional
  • 源碼下載: RxOptional

    RxOptional適用于Swift選項和“可占用”類型的RxSwift擴展。

除另有說明外握巢,所有操作符也可用于驅動程序和信號晕鹊。

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

結果打印:

next(One)
next(Three)
completed

replaceNilWith 的用法:

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

打印結果:

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

errorOnNil 的用法:

注意:在驅動程序上不可用暴浦,因為驅動程序不能出錯溅话。
默認情況下,rxoptionalerror . foundnilwhile eunwrappingoptional有錯誤歌焦。

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

結果打庸:

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

catchOnNil 的用法:

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

打印結果:

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

distinctUntilChanged 的用法:

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

打印結果:

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

目前在Swift協(xié)議中不能擴展到符合其他協(xié)議。目前同规,上面列出的類型符合Occupiable。您還可以使自定義類型符合Occupiable窟社。

filterEmpty 的用法:

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

打印結果:

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

errorOnEmpty的用法:

在驅動程序上不可用券勺,因為驅動程序不能出錯。

默認情況下灿里,RxOptionalError.emptyOccupiable會出現(xiàn)錯誤关炼。

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)

  • 你可以很輕松的實現(xiàn)換膚儒拂,切換主題的功能,只需要一行代碼搞定:
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
  • 已經(jīng)實現(xiàn)預設的綁定器有:
    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 數(shù)據(jù)埋點庫

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 性能優(yōu)化相關庫

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項目采用的架構分析

參考:http://www.reibang.com/p/fb63ca356463

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末蒜哀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吏砂,更是在濱河造成了極大的恐慌撵儿,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狐血,死亡現(xiàn)場離奇詭異淀歇,居然都是意外死亡,警方通過查閱死者的電腦和手機匈织,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門浪默,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事浴鸿【猓” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵岳链,是天一觀的道長花竞。 經(jīng)常有香客問我,道長掸哑,這世上最難降的妖魔是什么约急? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮苗分,結果婚禮上厌蔽,老公的妹妹穿的比我還像新娘。我一直安慰自己摔癣,他們只是感情好奴饮,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著择浊,像睡著了一般戴卜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琢岩,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天投剥,我揣著相機與錄音,去河邊找鬼担孔。 笑死江锨,一個胖子當著我的面吹牛,可吹牛的內容都是我干的糕篇。 我是一名探鬼主播啄育,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拌消!你這毒婦竟也來了灸撰?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拼坎,失蹤者是張志新(化名)和其女友劉穎浮毯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泰鸡,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡债蓝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了盛龄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饰迹。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡芳誓,死狀恐怖,靈堂內的尸體忽然破棺而出啊鸭,到底是詐尸還是另有隱情锹淌,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布赠制,位于F島的核電站赂摆,受9級特大地震影響,放射性物質發(fā)生泄漏钟些。R本人自食惡果不足惜烟号,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望政恍。 院中可真熱鬧汪拥,春花似錦、人聲如沸篙耗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宗弯。三九已至脯燃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間罕伯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工叽讳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留追他,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓岛蚤,卻偏偏與公主長得像邑狸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涤妒,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容