RxSwift + Moya + ObjectMapper + MVVM 的网络请求

2016年底的时候, 项目经理决定把我们的项目以MVVM架构方式进行重构; 当时Swift已经是3.0版本了, 也是时候转战Swift了. 之所以选择使用RxSwift实现MVVM, 而没有使用 RAC, 是因为我觉得RxSwift更符合Swift的编程思想.


首先送上 Demo 地址

Demo地址


MVVM浅析

| M| Model|负责数据层| |:-:|:-:|:-:| | V| ViewController|负责View| | VM| ViewModel|负责业务逻辑|

  • ViewModel 负责网络请求和数据解析, 可以再抽出一层网络请求层APIService层;
  • ViewModel负责把数据解析为对应的Model;
  • ViewControllerViewModel中读取数据;
  • ViewControllerModel 之间是不接触的, 相当于ViewModel是它们之间的桥梁.

构建 Service

  1. 新建一个enum 遵循 TargetType协议, 枚举的case值, 为每一个接口
    enum AppService: TargetType {
     case login(username: String, pwd: String)
     case video
    }
    
  2. 在枚举的扩展中定义一个网络请求的必要参数
    extension AppService {
     var baseURL: URL {
         return URL(string: API_PRO)!
     }
        
     var path: String {
         switch self {
         case .login(username: _, pwd: _):
             return "/login"
         case .video:
             return "/video"
         }
     }
        
     var method: Moya.Method {
         switch self {
         case .login(username: _, pwd: _):
             return .get
         case .video:
             return .post
         }
     }
        
     var parameters: [String: Any]? {
         switch self {
         case .login(username: let username, pwd: let pwd):
             return ["username": username, "pwd": pwd]
         case .video:
             return ["type": "JSON"]
         }
     }
        
     var parameterEncoding: ParameterEncoding {
         return URLEncoding.default
     }
        
     var sampleData: Data {
         return "".data(using: String.Encoding.utf8)!
     }
        
     var task: Task {
         return .request
     }
    }
    
  3. 可以把baseUrl , 请求头, 公共参数, 定义在一个单独的文件中;
    let API_PRO = "http://120.25.226.186:32812"
    let headerFields: [String: String] = ["system": "iOS","sys_ver": String(UIDevice.version())]
    let publicParameters: [String: String] = ["language": "_zh_CN"]
    
  4. 创建RxMoyaProvider用于发送网络请求, 可以在创建的时候传入请求头和公共参数
    let appServiceProvider = RxMoyaProvider<AppService>.init()
    

构建 Model

使用ObjectMapper库转模型

  1. 登录接口的Model
    class LoginModel: Mappable {
     var error: String?
     var success: String?
        
     required init?(map: Map) {
     }
        
     func mapping(map: Map) {
         error <- map["error"]
         success <- map["success"]
     }
    }
    
  2. 保存视频信息的Model;
    class VideoModel: Mappable {
     var videos: [Video]?
        
     required init?(map: Map) {
     }
        
     func mapping(map: Map) {
         videos <- map["videos"]
     }
    }
    
  class Video: Mappable {
    var id: String?
    var length: Float?
    var name: String?
    var url: String?
    
    required init?(map: Map) {
    }
    
    func mapping(map: Map) {
        id <- map["id"]
        length <- map["length"]
        name <- map["name"]
        url <- map["url"]
    }
}

构建 ViewModel

ViewModel层发送网络请求, 获取数据, 获取到的数据保存在Model中, 通过回调刷新UI, 并且返回一个可观察者.

class ViewModel {
    
    func login(username: String, pwd: String) -> Observable<LoginModel> {
        return appServiceProvider.request(.login(username: username, pwd: pwd))
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .showAPIErrorToast()
            .mapObject(type: LoginModel.self)
    }
    
    func video() -> Observable<VideoModel> {
        return appServiceProvider.request(.video)
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .showAPIErrorToast()
            .mapObject(type: VideoModel.self)
    }
}

函数中的showAPIErrorToast()是自己定义的一个Observable的扩展函数, 用于在网络请求错误的时候需要做的一些操作

extension Observable {
    func showAPIErrorToast() -> Observable<Element> {
        return self.do(onNext: { (event) in
        }, onError: { (error) in
            // TODO: 可以在此处做一些网络错误的时候的提示信息
            print("\(error.localizedDescription)")
        }, onCompleted: {
        }, onSubscribe: {
        }, onDispose: {
        })
    }
}

函数中的 mapObject(type:) 是自定义的Observable扩展函数, 用于json 转模型, 需要自己根据实际项目中的json格式, 做相应的解析.

extension Observable {
    func mapObject<T: Mappable>(type: T.Type) -> Observable<T> {
        return self.map { response in
            //if response is a dictionary, then use ObjectMapper to map the dictionary
            //if not throw an error
            guard let dict = response as? [String: Any] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            return Mapper<T>().map(JSON: dict)!
        }
    }
    
    func mapArray<T: Mappable>(type: T.Type) -> Observable<[T]> {
        return self.map { response in
            //if response is an array of dictionaries, then use ObjectMapper to map the dictionary
            //if not, throw an error
            guard let array = response as? [[String: Any]] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            return Mapper<T>().mapArray(JSONArray: array)!
        }
    }
}

RxSwiftMoyaError是自定义的一个错误类型枚举值, 可以返回一些错误信息, 用在showAPIErrorToast()中提示用户的信息

enum RxSwiftMoyaError : Swift.Error {
    case ParseJSONError
    case NoRepresentor
    case NotSuccessfulHTTP
    case NoData
    case CouldNotMakeObjectError
    case BizError(resultCode: String, resultMsg: String)
}

extension RxSwiftMoyaError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .ParseJSONError:
            return "数据解析失败"
        case .NoRepresentor:
            return "NoRepresentor."
        case .NotSuccessfulHTTP:
            return "NotSuccessfulHTTP."
        case .NoData:
            return "NoData."
        case .CouldNotMakeObjectError:
            return "CouldNotMakeObjectError."
        case .BizError(resultCode: let resultCode, resultMsg: let resultMsg):
            return "错误码: \(resultCode), 错误信息: \(resultMsg)"
        }
    }
}

ViewController 中就可以直接操作 viewModel 发送网络请求, 并且获取到模型数据

ViewController 中包含一个 ViewModel 对象, View需要变化的时候, 直接让这个对象调用自己的函数, 获取到Model数据, 刷新UI

let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let viewModel = ViewModel()
        viewModel.login(username: "520it", pwd: "520it").subscribe(onNext: { (loginModel) in
            print("---\(loginModel.success)")
        }).addDisposableTo(disposeBag)
        
        viewModel.video().subscribe(onNext: { (videoModel) in
            guard let videos = videoModel.videos else {
                return
            }
            for video in videos {
                print("----id:\(video.id)---length:\(video.length)---name:\(video.name)---url:\(video.url)")
            }
        }).addDisposableTo(disposeBag)
    }

结果如下所示:

---Optional("登录成功")
----id:nil---length:Optional(10.0)---name:Optional("小黄人 第01部")---url:Optional("resources/videos/minion_01.mp4")
----id:nil---length:Optional(12.0)---name:Optional("小黄人 第02部")---url:Optional("resources/videos/minion_02.mp4")
----id:nil---length:Optional(14.0)---name:Optional("小黄人 第03部")---url:Optional("resources/videos/minion_03.mp4")
----id:nil---length:Optional(16.0)---name:Optional("小黄人 第04部")---url:Optional("resources/videos/minion_04.mp4")
----id:nil---length:Optional(18.0)---name:Optional("小黄人 第05部")---url:Optional("resources/videos/minion_05.mp4")
----id:nil---length:Optional(20.0)---name:Optional("小黄人 第06部")---url:Optional("resources/videos/minion_06.mp4")
----id:nil---length:Optional(22.0)---name:Optional("小黄人 第07部")---url:Optional("resources/videos/minion_07.mp4")
----id:nil---length:Optional(24.0)---name:Optional("小黄人 第08部")---url:Optional("resources/videos/minion_08.mp4")
----id:nil---length:Optional(26.0)---name:Optional("小黄人 第09部")---url:Optional("resources/videos/minion_09.mp4")
----id:nil---length:Optional(28.0)---name:Optional("小黄人 第10部")---url:Optional("resources/videos/minion_10.mp4")
----id:nil---length:Optional(30.0)---name:Optional("小黄人 第11部")---url:Optional("resources/videos/minion_11.mp4")
----id:nil---length:Optional(32.0)---name:Optional("小黄人 第12部")---url:Optional("resources/videos/minion_12.mp4")
----id:nil---length:Optional(34.0)---name:Optional("小黄人 第13部")---url:Optional("resources/videos/minion_13.mp4")
----id:nil---length:Optional(36.0)---name:Optional("小黄人 第14部")---url:Optional("resources/videos/minion_14.mp4")
----id:nil---length:Optional(38.0)---name:Optional("小黄人 第15部")---url:Optional("resources/videos/minion_15.mp4")
----id:nil---length:Optional(40.0)---name:Optional("小黄人 第16部")---url:Optional("resources/videos/minion_16.mp4")

到此, 我们已经完成了使用RxSwift 进行 MVVM 架构, 以及使用Moya 封装网络请求层, 和使用 ObjectMapper 进行 json 转模型. 不知道小伙伴有没有感觉到 使用 Swift 编程, 的确是很优雅呢?


写在最后

Demo地址

自己写的博客, 内容略显粗浅, 大家可以看下面列出的博客.

RxSwift 中的 Subject 详解 (翻译二)

RxSwift 上手详解 —— 入门篇(翻译三)

RxSwift 中的 Units——一个富有哲学意味的概念(翻译四)

【iOS开发】RxSwift入坑解读-你所需要知道的各种概念

【iOS开发】RxSwift入坑解读-那些难以理解的细节

【iOS开发】RxSwift实战教程-核心用法

【iOS开发】Moya入坑记-用法解读篇

【iOS开发】使用RxSwift+Moya进行优雅的网络请求

Loading Disqus comments...
Table of Contents