Moya+Realm+RxSwift+SwiftyJSON優(yōu)雅的網(wǎng)絡(luò)請(qǐng)求方式(擴(kuò)展離線緩存)

Moya + RxSwift + SwiftyJSON + Realm 封裝網(wǎng)絡(luò)請(qǐng)求

先看一個(gè)例子,這段代碼是請(qǐng)求數(shù)據(jù)然后展示在Label上

viewModel.loadData(IPModel.self)
            .map { $0?.city }
            .bindTo(self.showResult.rx.text)
            .addDisposableTo(disposeBag)

看起來是不是很優(yōu)雅,接下來一步一步來詳細(xì)解釋是怎么實(shí)現(xiàn)的

先簡(jiǎn)單介紹一下用到的第三方庫(kù)(具體用法請(qǐng)自己搜索)

  • Moya 一個(gè)網(wǎng)絡(luò)抽象層庫(kù)
  • Realm 一款支持運(yùn)行在手機(jī)铐刘、平板和可穿戴設(shè)備上的嵌入式數(shù)據(jù)庫(kù)(旨在取代CoreData和Sqlite)
  • RxSwift 響應(yīng)式編程里超級(jí)優(yōu)雅的框架
  • SwiftyJSON 強(qiáng)大的JSON轉(zhuǎn)換庫(kù)
  • RxCoCoa RxSwift對(duì)Cocoa的擴(kuò)展

建立Moya的Target

/// 建立請(qǐng)求
enum ApiManager {
    case github
}

// MARK: - 實(shí)現(xiàn)Moya基本參數(shù)
extension ApiManager: TargetType {
    
    /// 基類API
    var baseURL: URL {
        return URL.init(string: "http://ditu.amap.com")!
    }
    
    /// 拼接請(qǐng)求路徑
    var path: String {
        switch self {
        case .github:
            return "/service/regeo"
        }
    }
    
    /// 設(shè)置請(qǐng)求方式
    var method: Moya.Method {
        switch self {
        case .github:
            return .get
        }
    }
    
    /// 設(shè)置傳參
    var parameters: [String: Any]? {
        switch self {
        case .github:
            return ["longitude" : "121.04925573429551", "latitude" : "31.315590522490712"]
        }
    }
    
    /// 設(shè)置編碼方式
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    /// 這個(gè)用于測(cè)試,對(duì)此不太熟悉!
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
    
    /// 設(shè)置任務(wù)的請(qǐng)求方式(可以改成上傳upload晨抡、下載download)
    var task: Task {
        return .request
    }
    
    /// Alamofire中的驗(yàn)證设捐,默認(rèn)是false
    var validate: Bool {
        return false
    }
    
}

Extension+Observable

// MARK: - 擴(kuò)展Map
extension Observable {
    /// 數(shù)據(jù)轉(zhuǎn)JSON
    fileprivate func resultToJSON<T: Mapable>(_ jsonData: JSON, ModelType: T.Type) -> T? {
        return T(jsonData: jsonData)
    }
    
    /// 數(shù)據(jù)是JSON使用這個(gè)轉(zhuǎn)
    func mapResponseToObj<T: Mapable>(_ type: T.Type) -> Observable<T?> {
        return map { representor in
            
            //檢查是否是Moya.Response
            guard let response = representor as? Moya.Response else {
                throw XHError.XHNoMoyaResponse
            }
            
            //檢查是否是一次成功的響應(yīng)
            guard ((200...209) ~= response.statusCode) else {
                throw XHError.XHFailureHTTP
            }
            
            //將data轉(zhuǎn)為JSON
            let json = JSON.init(data: response.data)
            
            //判斷是否有狀態(tài)碼
            if let code = json[RESULT_CODE].string {
                
                //判斷返回的狀態(tài)碼是否與成功狀態(tài)碼一致
                if code == XHStatus.XHSuccess.rawValue {
                    //將數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)包字段轉(zhuǎn)為JSON傳出
                    return self.resultToJSON(json[RESULT_DATA], ModelType: type)
                }else {
                    //狀態(tài)碼與成功狀態(tài)碼不一致的時(shí)候期揪,返回提示信息
                    throw XHError.XHMsgError(statusCode: json[RESULT_CODE].string, errorMsg: json[RESULT_MESSAGE].string)
                }
                
            }else {
                //報(bào)錯(cuò)非對(duì)象
                throw XHError.XHNotMakeObjectError
            }
            
        }
    }

Extension+RxMoyaProvider

extension RxMoyaProvider {
    func XHOffLineCacheRequest(token: Target) -> Observable<Moya.Response> {
        return Observable.create({[weak self] (observer) -> Disposable in
            //拼接成為數(shù)據(jù)庫(kù)的key
            let key = token.baseURL.absoluteString + token.path + (self?.toJSONString(dict: token.parameters))!
            
            //建立Realm
            let realm = try! Realm()

            //設(shè)置過濾條件
            let pre = NSPredicate(format: "key = %@",key)
            
            //過濾出來的數(shù)據(jù)(為數(shù)組)
            let ewresponse = realm.objects(ResultModel.self).filter(pre)
            
            //先看有無緩存的話,如果有數(shù)據(jù),數(shù)組即不為0
            if ewresponse.count != 0 {
                //因?yàn)樵O(shè)置了過濾條件狰晚,只會(huì)出現(xiàn)一個(gè)數(shù)據(jù),直接取
                let filterResult = ewresponse[0]
                //重新創(chuàng)建成Response發(fā)送出去
                let creatResponse = Response(statusCode: filterResult.statuCode, data: filterResult.data!)
                observer.onNext(creatResponse)
            }
            
            
            //進(jìn)行正常的網(wǎng)絡(luò)請(qǐng)求
            let cancellableToken = self?.request(token) { result in
                switch result {
                case let .success(response):
                    observer.onNext(response)
                    observer.onCompleted()
                    //建立數(shù)據(jù)庫(kù)模型并賦值
                    let model = ResultModel()
                    model.data = response.data
                    model.key = key
                    model.statuCode = response.statusCode
                    //寫入數(shù)據(jù)庫(kù)(注意:update參數(shù),如果在設(shè)置模型的時(shí)候沒有設(shè)置主鍵的話,這里是不能使用update參數(shù)的,update參數(shù)可以保證如果有相同主鍵的數(shù)據(jù)就直接更新數(shù)據(jù)而不是新建)
                    try! realm.write {
                        realm.add(model, update: true)
                    }
                case let .failure(error):
                    observer.onError(error)
                }
                
            }
            return Disposables.create {
                cancellableToken?.cancel()
            }
            
        })
    }
    
    
    /// 字典轉(zhuǎn)JSON字符串(用于設(shè)置數(shù)據(jù)庫(kù)key的唯一性)
    func toJSONString(dict:Dictionary<String, Any>?)->String{
        
        let data = try? JSONSerialization.data(withJSONObject: dict!, options: JSONSerialization.WritingOptions.prettyPrinted)
        
        let strJson = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
        
        return strJson! as String
        
    }
}

這里我存在數(shù)據(jù)庫(kù)中使用的key是(baseurl + path + 參數(shù)的json字符串)堂污,這樣可以保證每個(gè)請(qǐng)求地址的唯一性家肯,避免同一接口不同參數(shù)的數(shù)據(jù)混淆,由于Moya的Response被final修飾了盟猖,不能繼承讨衣,所以我是把Response中的屬性單獨(dú)提出來保存到數(shù)據(jù)庫(kù),需要的時(shí)候再?gòu)臄?shù)據(jù)庫(kù)中獲取出來重新組成Response發(fā)送出去


這里我使用的是Realm默認(rèn)創(chuàng)建的數(shù)據(jù)庫(kù),我有寫一個(gè)方法來創(chuàng)建自定義名字的數(shù)據(jù)庫(kù)式镐,需要修改的才調(diào)用反镇,Realm會(huì)自動(dòng)建立默認(rèn)數(shù)據(jù)庫(kù)

  /// 配置數(shù)據(jù)庫(kù)(如果不需要修改默認(rèn)數(shù)據(jù)庫(kù)的,就不調(diào)用這個(gè)方法)
  func creatDataBase() {
        //獲取當(dāng)前配置
        var config = Realm.Configuration()
        
        // 使用默認(rèn)的目錄娘汞,替換默認(rèn)數(shù)據(jù)庫(kù)
        config.fileURL = config.fileURL!.deletingLastPathComponent()
            .appendingPathComponent(cacheDatabaseName)
        
        // 將這個(gè)配置應(yīng)用到默認(rèn)的 Realm 數(shù)據(jù)庫(kù)當(dāng)中
        Realm.Configuration.defaultConfiguration = config
    } 

建立數(shù)據(jù)模型

這是服務(wù)器返回的數(shù)據(jù)建立的模型(我只寫了一個(gè)字段做測(cè)試)

  • 先創(chuàng)建協(xié)議
/// 定義數(shù)據(jù)轉(zhuǎn)JSON協(xié)議
public protocol Mapable {
    init?(jsonData:JSON)
}
  • 建立數(shù)據(jù)模型
struct IPModel: Mapable {
    let city: String?
    
    init?(jsonData: JSON) {
        self.city = jsonData["city"].string
    }  
}

建立ViewModel

這里將RxMoya默認(rèn)的Request改成剛剛我自己寫的Extension的方法

class ViewModel {
    private let provider = RxMoyaProvider<ApiManager>()
    
  

    func loadData<T: Mapable>(_ model: T.Type) -> Observable<T?> {
          return provider.XHOffLineCacheRequest(token: .github)
                                 .debug()
                                 .subscribeOn(ConcurrentDispatchQueueScheduler.init(qos: 
                                 .default))
                                 .observeOn(MainScheduler.instance)
                                 .distinctUntilChanged()
                                 .catchError({ (error) -> Observable<Response> in
                                 //捕獲錯(cuò)誤歹茶,不然離線訪問會(huì)導(dǎo)致Binding error to UI,可以再此顯示HUD等操作
                                       print(error.localizedDescription)
                                       return Observable.empty()
                                   })
                                  .mapResponseToObj(T.self)
                 }
}

最終在ViewControl里調(diào)用

viewModel.loadData(IPModel.self)
            .map { $0?.city }
            .bindTo(self.showResult.rx.text)
            .addDisposableTo(disposeBag)

寫這個(gè)小封裝看了不少大神的博客資料等你弦,特別感謝前人們的博客資料!
因?yàn)闆]存地址惊豺,這里就不貼鏈接了,以后找到了貼上來禽作!
第一次分享尸昧,希望大家指點(diǎn)其中的錯(cuò)誤以及不足!萬分感謝旷偿!

最后貼上完整Demo地址

覺得不錯(cuò)的求給個(gè)Star烹俗,歡迎提Issues

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市萍程,隨后出現(xiàn)的幾起案子幢妄,更是在濱河造成了極大的恐慌,老刑警劉巖茫负,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蕉鸳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡朽褪,警方通過查閱死者的電腦和手機(jī)置吓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缔赠,“玉大人,你說我怎么就攤上這事友题∴脱撸” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)踢匣。 經(jīng)常有香客問我告匠,道長(zhǎng),這世上最難降的妖魔是什么离唬? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任后专,我火速辦了婚禮,結(jié)果婚禮上输莺,老公的妹妹穿的比我還像新娘戚哎。我一直安慰自己,他們只是感情好嫂用,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布型凳。 她就那樣靜靜地躺著,像睡著了一般嘱函。 火紅的嫁衣襯著肌膚如雪甘畅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天往弓,我揣著相機(jī)與錄音疏唾,去河邊找鬼。 笑死函似,一個(gè)胖子當(dāng)著我的面吹牛槐脏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缴淋,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼准给,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了重抖?” 一聲冷哼從身側(cè)響起露氮,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钟沛,沒想到半個(gè)月后畔规,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恨统,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年叁扫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畜埋。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡莫绣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悠鞍,到底是詐尸還是另有隱情对室,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站掩宜,受9級(jí)特大地震影響蔫骂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牺汤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一辽旋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧檐迟,春花似錦补胚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至怔匣,卻和暖如春握联,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背每瞒。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工金闽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剿骨。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓代芜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親浓利。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挤庇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容