Moya+Realm+RxSwift+SwiftyJSON优雅的网络请求方式(扩展离线缓存)

Moya + RxSwift + SwiftyJSON + Realm 封装网络请求

先看一个例子,这段代码是请求数据然后展示在Label上

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

看起来是不是很优雅,接下来一步一步来详细解释是怎么实现的

先简单介绍一下用到的第三方库(具体用法请自己搜索)

  • Moya 一个网络抽象层库
  • Realm 一款支持运行在手机、平板和可穿戴设备上的嵌入式数据库(旨在取代CoreData和Sqlite)
  • RxSwift 响应式编程里超级优雅的框架
  • SwiftyJSON 强大的JSON转换库
  • RxCoCoa RxSwift对Cocoa的扩展

建立Moya的Target

/// 建立请求
enum ApiManager {
    case github
}

// MARK: - 实现Moya基本参数
extension ApiManager: TargetType {
    
    /// 基类API
    var baseURL: URL {
        return URL.init(string: "http://ditu.amap.com")!
    }
    
    /// 拼接请求路径
    var path: String {
        switch self {
        case .github:
            return "/service/regeo"
        }
    }
    
    /// 设置请求方式
    var method: Moya.Method {
        switch self {
        case .github:
            return .get
        }
    }
    
    /// 设置传参
    var parameters: [String: Any]? {
        switch self {
        case .github:
            return ["longitude" : "121.04925573429551", "latitude" : "31.315590522490712"]
        }
    }
    
    /// 设置编码方式
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    /// 这个用于测试,对此不太熟悉!
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
    
    /// 设置任务的请求方式(可以改成上传upload、下载download)
    var task: Task {
        return .request
    }
    
    /// Alamofire中的验证,默认是false
    var validate: Bool {
        return false
    }
    
}

Extension+Observable

// MARK: - 扩展Map
extension Observable {
    /// 数据转JSON
    fileprivate func resultToJSON<T: Mapable>(_ jsonData: JSON, ModelType: T.Type) -> T? {
        return T(jsonData: jsonData)
    }
    
    /// 数据是JSON使用这个转
    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
            }
            
            //检查是否是一次成功的响应
            guard ((200...209) ~= response.statusCode) else {
                throw XHError.XHFailureHTTP
            }
            
            //将data转为JSON
            let json = JSON.init(data: response.data)
            
            //判断是否有状态码
            if let code = json[RESULT_CODE].string {
                
                //判断返回的状态码是否与成功状态码一致
                if code == XHStatus.XHSuccess.rawValue {
                    //将数据结构中的数据包字段转为JSON传出
                    return self.resultToJSON(json[RESULT_DATA], ModelType: type)
                }else {
                    //状态码与成功状态码不一致的时候,返回提示信息
                    throw XHError.XHMsgError(statusCode: json[RESULT_CODE].string, errorMsg: json[RESULT_MESSAGE].string)
                }
                
            }else {
                //报错非对象
                throw XHError.XHNotMakeObjectError
            }
            
        }
    }

Extension+RxMoyaProvider

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

            //设置过滤条件
            let pre = NSPredicate(format: "key = %@",key)
            
            //过滤出来的数据(为数组)
            let ewresponse = realm.objects(ResultModel.self).filter(pre)
            
            //先看有无缓存的话,如果有数据,数组即不为0
            if ewresponse.count != 0 {
                //因为设置了过滤条件,只会出现一个数据,直接取
                let filterResult = ewresponse[0]
                //重新创建成Response发送出去
                let creatResponse = Response(statusCode: filterResult.statuCode, data: filterResult.data!)
                observer.onNext(creatResponse)
            }
            
            
            //进行正常的网络请求
            let cancellableToken = self?.request(token) { result in
                switch result {
                case let .success(response):
                    observer.onNext(response)
                    observer.onCompleted()
                    //建立数据库模型并赋值
                    let model = ResultModel()
                    model.data = response.data
                    model.key = key
                    model.statuCode = response.statusCode
                    //写入数据库(注意:update参数,如果在设置模型的时候没有设置主键的话,这里是不能使用update参数的,update参数可以保证如果有相同主键的数据就直接更新数据而不是新建)
                    try! realm.write {
                        realm.add(model, update: true)
                    }
                case let .failure(error):
                    observer.onError(error)
                }
                
            }
            return Disposables.create {
                cancellableToken?.cancel()
            }
            
        })
    }
    
    
    /// 字典转JSON字符串(用于设置数据库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
        
    }
}

这里我存在数据库中使用的key是(baseurl + path + 参数的json字符串),这样可以保证每个请求地址的唯一性,避免同一接口不同参数的数据混淆,由于Moya的Response被final修饰了,不能继承,所以我是把Response中的属性单独提出来保存到数据库,需要的时候再从数据库中获取出来重新组成Response发送出去


这里我使用的是Realm默认创建的数据库,我有写一个方法来创建自定义名字的数据库,需要修改的才调用,Realm会自动建立默认数据库

  /// 配置数据库(如果不需要修改默认数据库的,就不调用这个方法)
  func creatDataBase() {
        //获取当前配置
        var config = Realm.Configuration()
        
        // 使用默认的目录,替换默认数据库
        config.fileURL = config.fileURL!.deletingLastPathComponent()
            .appendingPathComponent(cacheDatabaseName)
        
        // 将这个配置应用到默认的 Realm 数据库当中
        Realm.Configuration.defaultConfiguration = config
    } 

建立数据模型

这是服务器返回的数据建立的模型(我只写了一个字段做测试)

  • 先创建协议
/// 定义数据转JSON协议
public protocol Mapable {
    init?(jsonData:JSON)
}
  • 建立数据模型
struct IPModel: Mapable {
    let city: String?
    
    init?(jsonData: JSON) {
        self.city = jsonData["city"].string
    }  
}

建立ViewModel

这里将RxMoya默认的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
                                 //捕获错误,不然离线访问会导致Binding error to UI,可以再此显示HUD等操作
                                       print(error.localizedDescription)
                                       return Observable.empty()
                                   })
                                  .mapResponseToObj(T.self)
                 }
}

最终在ViewControl里调用

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

写这个小封装看了不少大神的博客资料等,特别感谢前人们的博客资料!
因为没存地址,这里就不贴链接了,以后找到了贴上来!
第一次分享,希望大家指点其中的错误以及不足!万分感谢!

最后贴上完整Demo地址

觉得不错的求给个Star,欢迎提Issues

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容