RxSwift+Moya網絡請求之項目實戰

RxSwift+Moya之項目實戰

RxSwift相關基本介紹和用法可參考:html

一. 下面將將進行實戰項目

  • 1.登陸註冊功能
    • 輸入用戶名要大於6個字符,否則密碼不能輸入
    • 密碼必須大於6個字符,否則重複密碼不能輸入
    • 重複密碼輸入必須和密碼同樣,否則註冊按鈕不能點擊
    • 根據輸入的字符是否合法,按鈕動態的改變顏色
  • 2.UITableView和搜索SertchBar的應用
    • searchBar根據輸入的字體展現包含該字體的cell列表
    • RxSwift實現tableView列表展現
  • 3.Moya+RxSwift實現網絡請求

二. Demo地址

下面簡單看一下demo的界面

1. 登陸註冊

登陸註冊

2. UITableView和SearchBar

UITableView和SearchBar

3. UICollectionView和Moya

UICollectionView和Moya

三. 項目結構和框架

1. 結構

demo是使用的純MVVM模式,由於RxSwift就是爲MVVM而生。不懂MVVM的猿友可參考MVVM模式快速入門ios

項目結構

2. 項目框架

// Swift三方庫
    // Rx
    pod 'RxSwift'  //RxSwift的必備庫
    pod 'RxCocoa'  //對 UIKit Foundation 進行 Rx 化
    pod 'RxDataSources'   // 幫助咱們優雅的使用tableView的數據源方法

    // 網絡請求
    pod 'Moya/RxSwift'  // 爲RxSwift專用提供,對Alamofire進行封裝的一個網絡請求庫

    // 圖片處理
    pod 'Kingfisher'  //圖片處理庫

    // 數據解析
    pod 'ObjectMapper'  //json轉模型


    
// OC庫
    // MJRefresh
    pod 'MJRefresh'   //MJ上拉下拉刷新
    pod 'SVProgressHUD'  //HUD

複製代碼

四. 註冊界面

  • 這裏主要使用了Observable的相關知識,不瞭解的童鞋可參考RxSwift的使用詳解01,瞭解Observable的操做
  • 註冊和登陸並無保存已註冊的帳號和密碼, 故登陸功能並不完善,後期會在完善,望知曉
  • 下面將針對註冊用戶名作簡單介紹:

1. 首先在model裏處理輸入字符串的語法法則和字符個數是否符合規範

extension InputValidator {
    //判斷字符串是否符合語法法則
    class func isValidEmail(_ email: String) -> Bool {
        let regular = try? NSRegularExpression(pattern: "^\\S+@\\S+\\.\\S+$", options: [])
        if let re = regular {
            let range = NSRange(location: 0, length: email.lengthOfBytes(using: .utf8))
            let result = re.matches(in: email, options: [], range: range)
            return result.count > 0
        }
        return false
    }
    
    //判斷密碼字符個數>8
    class func isValidPassword(_ password: String) -> Bool {
        return password.characters.count >= 8
    }
    
    //判斷用戶名
    class func validateUserName(_ username: String) -> Result {
        //判斷字符個數是否正確
        if username.characters.count < 6 {
            return Result.failure(message: "輸入的字符個數不能少於6個字符")
        }
        
        //帳號可用
        return Result.success(message: "帳號可用")
    }
}
複製代碼

其中Result是一個返回是否成功的枚舉值,可傳入字符串變量git

enum Result {
    case success(message: String)
    case failure(message: String)
}
複製代碼

2. 根據輸入的用戶名判斷該用戶名是否可用

var usernameObserable: Observable<Result>
    var passwordObserable: Observable<Result>
    var repeatPassObserable: Observable<Result>
    var registerBtnObserable: Observable<Bool>
    
    
    init(){
        //檢測帳號
        usernameObserable = username.asObservable().map({ (username) -> Result in
            return InputValidator.validateUserName(username)
        })
    }    

複製代碼
  • 該返回參數Result,控制器將根據該Result是否成功來改變輸入框是不是可編輯狀態
  • 初始化方法中,咱們對傳入的序列進行處理和轉換成相對應的Result序列

3. controller邏輯,根據用戶名輸入改變各控件狀態

//1. 帳號判斷邏輯
        //1-1. 檢測帳號
        usernameTextField.rx.text
            .orEmpty // 將String? 類型轉爲String型
            .bindTo(registerVM.username)
            .addDisposableTo(bag)
        
        //1-2. 根據帳號監聽提示字體的狀態
        registerVM.usernameObserable
            .bindTo(usernameHintLabel.rx.validationResult)
            .addDisposableTo(bag)
        
        //1-3. 根據帳號監聽密碼輸入框的狀態
        registerVM.usernameObserable
            .bindTo(passwordTextField.rx.enableResult)
            .addDisposableTo(bag)
            
複製代碼
  • 檢測輸入用戶名是否符合規範
  • 根據帳號監聽提示字體的狀態
  • 根據帳號監聽密碼輸入框的狀態
  • 根據帳號監聽註冊按鈕的狀態

五. UITableView和SearchBar

  • 該UITableView展現界面並未涉及網絡請求
  • 數據來源plist文件
  • 圖片爲本地圖片,可下載demo,在demo中查找圖片
  • 選用自定義UITableViewCell,故cell不作介紹
  • model小編這裏也很少作介紹,詳情可下載demo看具體代碼

1. viewModel中的代碼邏輯

1-1. 讀取plist文件,獲取模型數組

fileprivate func getHeroData() -> [HeroModel]{
    // 1.獲取路徑
    let path = Bundle.main.path(forResource: "heros.plist", ofType: nil)!
        
    // 2.讀取文件內容
    let dictArray = NSArray(contentsOfFile: path) as! [[String : Any]]
        
    // 3.遍歷全部的字典而且轉成模型對象
    return dictArray.map({ HeroModel(dict: $0) }).reversed()
}
複製代碼

1-2. seachBar

lazy var heroVariable: Variable<[HeroModel]> = {
        return Variable(self.getHeroData())
    }()
    
    var searchText: Observable<String>
    init(searchText: Observable<String>) {
        self.searchText = searchText
        
        self.searchText.subscribe(onNext: { (str: String) in
            let heros = self.getHeroData().filter({ (hero: HeroModel) -> Bool in
                //過濾
                if str.isEmpty { return true }
                //model是否包含搜索字符串
                return hero.name.contains(str)
            })
            self.heroVariable.value = heros
        }).addDisposableTo(bag)
    }

複製代碼
  • 其中heroVariable是一個數組模型的包裝箱,在controller內調用使用前須要asObservable或者asDriver解包裝;詳細用法可參考:RxSwift的使用詳解01
  • searchText搜索框輸入的關鍵字,根據該關鍵字從數組中過濾出全部包含該關鍵字的model
  • 對heroVariable從新賦值,發出事件

1-3. RxTableViewController.swift主要代碼

1-3-1. searchBar搜索框,輸入字符後間隔0.5秒開始搜索

var searchText: Observable<String> {
    //輸入後間隔0.5秒搜索,在主線程運行
    return searchBar.rx.text.orEmpty.throttle(0.5, scheduler: MainScheduler.instance)
}

複製代碼

1-3-2. UITableView的設置

//2.給tableView綁定數據
    //注意: 三個參數:row, model, cell三個順序不能夠搞錯, 不須要的可省略 
    heroVM.heroVariable.asDriver().drive(rxTableView.rx.items(cellIdentifier: kCellID, cellType: RxTableViewCell.self)) { (_, hero, cell) in
        cell.heroModel = hero
    }.addDisposableTo(bag)
        
    // 3.監聽UITableView的點擊
    rxTableView.rx.modelSelected(HeroModel.self).subscribe { (event: Event<HeroModel>) in
        print(event.element?.name ?? "")
    }.addDisposableTo(bag)

複製代碼
  • 將viewModel中的heroVariable進行解包裝,若是是Driver序列,咱們這裏不使用bingTo,而是使用的Driver,用法和bingTo如出一轍。
  • Deriver的監聽必定發生在主線程,因此很適合咱們更新UI的操做
  • 如需設置delegate的代理
rxTableView.rx.setDelegate(self).addDisposableTo(bag)
複製代碼

而後在實現相應的代理方法便可,如:github

extension RxTableViewController: UITableViewDelegate{
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}
複製代碼

六. UICollectionView+Moya+ObjectMapper網絡請求和數據處理

  • 與上述UITableView不一樣的是,這部分將以RxDataSources處理數據源
  • model數組以sections組集合處理
  • 結合Moya進行網絡請求
  • 使用ObjectMapper進行json數據轉模型

1. 配合ObjectMapper

這裏再介紹一下ObjectMapperjson

class AnchorModel: Mappable {

    var name = ""    //名字
    var pic51 = ""   //頭像
    var pic74 = ""   //大圖
    var live = 0
    var push = 0
    var focus = 0    //關注量
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        name  <- map["name"]
        pic51 <- map["pic51"]
        pic74 <- map["pic74"]
        live  <- map["live"]
        push  <- map["push"]
        focus <- map["focus"]
    }
}

複製代碼
  • 使用 ObjectMapper ,須要讓本身的 Model 類使用 Mappable 協議,這個協議包括兩個方法:
required init?(map: Map) {}
 
func mapping(map: Map) {}
複製代碼
  • 在 mapping 方法中,用 <- 操做符來處理和映射你的 JSON數據
  • 詳細的 ObjectMapper 教程能夠查看它的 Github 主頁,我在這裏只作簡單的介紹。

2. Moya的使用

  • Moya是基於Alamofire的網絡請求庫,這裏我使用了Moya/Swift,它在Moya的基礎上添加了對RxSwift的接口支持。
  • Github上的官方介紹羅列了Moya的一些特色:
    • 編譯時檢查正確的API端點訪問.
    • 使你定義不一樣端點枚舉值對應相應的用途更加明晰.
    • 提升測試地位從而使單元測試更加容易.
  • 接下來咱們來講下Moya的使用

2-1. 建立一個枚舉API

//請求枚舉類型
enum JunNetworkTool {
    
    case getNewList
    case getHomeList(page: Int)
}
複製代碼

2-2. 爲枚舉添加擴展

  • 需遵循協議 TargetType
  • 這個協議的Moya這個庫規定的協議,能夠單擊進入相應的文件進行查看
  • 這個協議內的每個參數(除了validate可不重寫)都必須重寫,不然會報錯
//請求參數
extension JunNetworkTool: TargetType {
    
    //統一基本的url
    var baseURL: URL {
        return (URL(string: "http://qf.56.com/home/v4/moreAnchor.ios"))!
    }
    
    //path字段會追加至baseURL後面
    var path: String {
        return ""
    }
    
    //請求的方式
    var method: Moya.Method {
        return .get
    }
    
    //參數編碼方式(這裏使用URL的默認方式)
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    //用於單元測試
    var sampleData: Data {
        return "getList".data(using: .utf8)!
    }
    
    //將要被執行的任務(請求:request 下載:upload 上傳:download)
    var task: Task {
        return .request
    }
    
    //請求參數(會在請求時進行編碼)
    var parameters: [String: Any]? {
        switch self {
        case .getHomeList(let index):
            return ["index": index]
        default:
            return ["index": 1]
        }
    }
    
    //是否執行Alamofire驗證,默認值爲false
    var validate: Bool {
        return false
    }
}

複製代碼

2-3. 定義一個全局變量用於整個項目的網絡請求

let junNetworkTool = RxMoyaProvider<JunNetworkTool>()
複製代碼

至此,咱們就可使用這個全局變量來請求數據了swift

3. RxDataSources

  • RxDataSources是以section來作爲數據結構來傳輸,這點很重要,好比:在傳統的數據源實現的方法中有一個numberOfSection,咱們在不少狀況下只須要一個section,因此這個方法可實現,也能夠不實現,默認返回的就是1,這給咱們帶來的一個迷惑點:【tableView是由row來組成的】,不知道在坐的各位中有沒有是這麼想的呢??有的話那從今天開始就要認清楚這一點,【tableView實際上是由section組成的】,因此在使用RxDataSources的過程當中,即便你的setion只有一個,那你也得返回一個section的數組出去!!!
  • 傳統方式適用於簡單的數據集,但不處理須要將複雜數據集與多個部分進行綁定的狀況,或者在添加/修改/刪除項目時須要執行動畫時。而使用RxDataSources時,它很容易寫
  • 想了解更多關於RxDataSources的用法,請參考其GitHub主頁

3-1. Sections自定義

  • 在咱們自定義的Model中建立一個AnchorSection的結構體
  • 並遵循SectionModelType協議,實現相應的協議方法
//MARK: SectionModel
struct AnchorSection {
    // items就是rows
    var items: [Item]
    
    // 你也能夠這裏加你須要的東西,好比 headerView 的 title
}

extension AnchorSection: SectionModelType {
    // 重定義 Item 的類型爲
    typealias Item = AnchorModel
    init(original: AnchorSection, items: [AnchorSection.Item]) {
        self = original
        self.items = items
    }
}
複製代碼

4. ViewModel

4-1. 自定義協議BaseViewModel

咱們知道MVVM思想就是將本來在ViewController的視圖顯示邏輯、驗證邏輯、網絡請求等代碼存放於ViewModel中,讓咱們的ViewController瘦身。這些邏輯由ViewModel負責,外界不須要關心,外界只須要結果,ViewModel也只須要將結果給到外界,基於此,咱們定義了一個協議數組

protocol JunViewModelType {
    //associatedtype: 關聯類型爲協議中的某個類型提供了一個佔位名(或者說別名),其表明的實際類型在協議被採納時纔會被指定
    associatedtype Input
    associatedtype Output
    
    //咱們經過 transform 方法將input攜帶的數據進行處理,生成了一個Output
    func transform(input: Input) -> Output
}

複製代碼

4-2. 自定義用於網絡請求的刷新狀態

  • 根據枚舉值的判斷,改變collection的刷新狀態
//刷新的狀態
enum JunRefreshStatus {
    case none
    case beingHeaderRefresh
    case endHeaderRefresh
    case beingFooterRefresh
    case endFooterRefresh
    case noMoreData
}

複製代碼

4-3. 自定義用於繼承的BaseViewModel

  • 定義請求數據的頁數index
  • 定義input和output的結構體
class BaseViewModel: NSObject {
    // 記錄當前的索引值
    var index: Int = 1
    
    struct JunInput {
        // 網絡請求類型
        let category: JunNetworkTool
        
        init(category: JunNetworkTool) {
            self.category = category
        }
    }
    
    struct JunOutput {
        // tableView的sections數據
        let sections: Driver<[AnchorSection]>
        // 外界經過該屬性告訴viewModel加載數據(傳入的值是爲了標誌是否從新加載)
        let requestCommond = PublishSubject<Bool>()
        // 告訴外界的tableView當前的刷新狀態
        let refreshStatus = Variable<JunRefreshStatus>(.none)
        
        //初始化時,section的數據
        init(sections: Driver<[AnchorSection]>) {
            self.sections = sections
        }
    }
}

複製代碼

4-4. 自定義AnchorViewModel

    1. 繼承BaseViewModel
class AnchorViewModel : BaseViewModel{
    // 存放着解析完成的模型數組
    let anchorArr = Variable<[AnchorModel]>([])

}
複製代碼
    1. 遵循JunViewModelType協議
extension AnchorViewModel: JunViewModelType {
    typealias Input = JunInput
    typealias Output = JunOutput

    func transform(input: AnchorViewModel.JunInput) -> AnchorViewModel.JunOutput {
        let sectionArr = anchorArr.asDriver().map { (models) -> [AnchorSection] in
            // 當models的值被改變時會調用
            return [AnchorSection(items: models)]
        }.asDriver(onErrorJustReturn: [])
        
        let output = JunOutput(sections: sectionArr)
        
        output.requestCommond.subscribe(onNext: { (isReloadData) in
            self.index = isReloadData ? 1 : self.index + 1
            //開始請求數據
            junNetworkTool.request(JunNetworkTool.getHomeList(page: self.index))
                .mapObjectArray(AnchorModel.self)
                .subscribe({ (event) in
                    switch event {
                    case let .next(modelArr):
                        self.anchorArr.value = isReloadData ? modelArr : (self.anchorArr.value) + modelArr
                        SVProgressHUD.showSuccess(withStatus: "加載成功")
                    case let .error(error):
                        SVProgressHUD.showError(withStatus: error.localizedDescription)
                    case .completed:
                        output.refreshStatus.value = isReloadData ? .endHeaderRefresh : .endFooterRefresh
                    }
            }).addDisposableTo(bag)
        }).addDisposableTo(bag)
        
        return output
    }
}
複製代碼
  • sectionArr是將model數組按照section分別存儲
  • 當請求回來的anchorArr數據改變的時候, sectionArr隨之會發生改變
  • isReloadData用於區分是下拉刷新(true時), 仍是上拉加載更多(false時)

5. RxCollectionViewController控制器中

  • 建立數據源RxDataSources
  • 綁定cell
  • 初始化input和output請求
  • 綁定section數據
  • 設置刷新

5-1. 建立數據源RxDataSources

// 建立一個數據源屬性,類型爲自定義的Section類型
let dataSource = RxCollectionViewSectionedReloadDataSource<AnchorSection>()
複製代碼

5-2. 綁定cell(自定義的cell要提早註冊)

dataSource.configureCell = { dataSource, collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollecCellID, for: indexPath) as! RxCollectionViewCell
    cell.anchorModel = item
    return cell
}
複製代碼
  • 以上四個參數的順序分別爲:dataSource, collectionView(或者tableView), indexPath, model, 其對應類型不言而喻,很少作介紹

5-3. 初始化input和output請求

let vmInput = AnchorViewModel.JunInput(category: .getNewList)
let vmOutput = anchorVM.transform(input: vmInput)
複製代碼

5-4. 綁定section數據

//4-1. 經過dataSource和section的model數組綁定數據(demo的用法, 推薦)
vmOutput.sections
    .asDriver()
    .drive(collectionVIew.rx.items(dataSource: dataSource))
    .addDisposableTo(bag)
複製代碼

5-5. 設置刷新

5-5-0. 在controller中初始化刷新狀態

collectionVIew.mj_header = MJRefreshNormalHeader(refreshingBlock: {
    vmOutput.requestCommond.onNext(true)
})
collectionVIew.mj_header.beginRefreshing()
        
collectionVIew.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {
    vmOutput.requestCommond.onNext(false)
})
複製代碼

5-5-1. 添加刷新的序列

  • 在JunOutput的結構體中添加刷新序列
  • 咱們在進行網絡請求並獲得結果以後,修改refreshStatus的value爲相應的JunRefreshStatus項
  • MJRefre遍會根據該狀態作出相應的刷新事件
  • 默認狀態爲none
// 告訴外界的tableView當前的刷新狀態
let refreshStatus = Variable<JunRefreshStatus>(.none)
複製代碼

5-5-2. 外界訂閱output的refreshStatus

  • 外界訂閱output的refreshStatus,而且根據接收到的值進行相應的操做
  • refreshStatus每次改變都會觸發刷新事件
//5. 設置刷新狀態
vmOutput.refreshStatus.asObservable().subscribe(onNext: { (status) in
    switch status {
    case .beingHeaderRefresh:
        self.collectionVIew.mj_header.beginRefreshing()
    case .endHeaderRefresh:
        self.collectionVIew.mj_header.endRefreshing()
    case .beingFooterRefresh:
        self.collectionVIew.mj_footer.beginRefreshing()
    case .endFooterRefresh:
        self.collectionVIew.mj_footer.endRefreshing()
    case .noMoreData:                   
        self.collectionVIew.mj_footer.endRefreshingWithNoMoreData()
    default:
        break
    }
}).addDisposableTo(bag)
複製代碼

5-5-3. output提供一個requestCommond用於控制是否請求數據

  • PublishSubject 的特色:便可以做爲Observable,也能夠做爲Observer,說白了就是能夠發送信號,也能夠訂閱信號
  • 當你訂閱PublishSubject的時候,你只能接收到訂閱他以後發生的事件。subject.onNext()發出onNext事件,對應的還有onError()和onCompleted()事件
// 外界經過該屬性告訴viewModel加載數據(傳入的值是爲了標誌是否從新加載)
let requestCommond = PublishSubject<Bool>()
複製代碼

七. 總結

  • 爲了研究RxSwift相關知識, 工做之餘的時間,差很少一個月了
  • 學習的瓶頸大部分在於網絡請求和配合刷新這一模塊
  • 文中如出現self循環引用的問題,還望大神多多指正
  • 小編目前也還在初學階段,文中如出現小錯誤還望多多指正,若有更好的方法,也但願不吝分享
  • 若是喜歡,能夠收藏,也能夠在Github上star一下

最後再一次附上Demo地址

參考文獻:

相關文章
相關標籤/搜索