Moya+PromiseKit+RxSwift优雅的书写网络请求

前言

公司之前的项目是由其他同事搭建的,随着公司业务的拓展,网络请求随之增加。网络工具类内部的代码愈发庞大,最终难以管理。为此寻找一个可行的解决方案,顺便学习一下RxSwift的使用。不说那么多底层原理,直接咱就说怎么用、怎么写,通俗易懂。文章附demo源码,由于本人也在学习中,所以代码中难免存在疏漏,存在问题可以互相讨论。demo内api选自公司内部api,所以不过分使用,仅作实例而已。

环境配置

  • Xcode 8.3
  • Swift 3

cocoapods

  • Alamofire
  • ObjectMapper
  • Moya
  • RxSwift
  • RxCocoa
  • PromiseKit

其中,数据解析部分(ObjectMapper)我们根据各自公司的习惯,可以选择Argo,SwiftyJSON等等其他框架,这不影响本博客内容。

实例

Moya部分

Moya是一个网络抽象层库。它在Alamofire基础上提供了一系列简单的抽象接口,让客户端代码不用去直接调用Alamofire,同时提供了很多实用的功能。

创建请求

Moya的TargetType协议规定使用枚举来创建网络请求,我们可以通过枚举来区分不同业务的网络请求。例如:

enum UserAPI {
    case registerUser(name: String, password: String)
    case upload(avator: UIImage)
}

enum homepageAPI {
    case homepageData
    case homepageBannerData
}

这里我们使用demo里ApiExample的实例,分别表示一个get、post和一个上传图片的请求,下载的请求一般只有特殊的业务里才会用到,这里不做展示。

enum ApiExample {
    case frontpage
    case fetchMorePackageArts(count: Int, artStyleId: Int, artType: Int)
    case updateExample(image: UIImage, otherParameter: String)
}

定义请求枚举完成后,我们需要在枚举的拓展里实现Moya的TargetType协议。

extension ApiExample: TargetType {
    
    public var baseURL: URL {
        return URL(string: "http://zuzu.artally.com.cn/zuzuart/")!
    }
    
    public var path: String {
        switch self {
        case .frontpage:
            return "frontpage/index/list/"
        case .fetchMorePackageArts:
            return "work/banner/work/list10/"
        case .updateExample:
            return "这里不提供实例,使用伪代码"
        }
    }
    
    public var method: Moya.Method {
        switch self {
        case .frontpage:
            return .get
        case .fetchMorePackageArts, .updateExample:
            return .post
        }
    }
    
    public var parameters: [String: Any]? {
        switch self {
        case .frontpage:
            return nil
        case .fetchMorePackageArts(let count, let artStyleId, let artType):
            return ["count": count, "banner_style_id": artStyleId, "type": artType]
        case .updateExample(_, let otherParameter):
            // 这里返回除图片之外的所有参数
            return ["参数名":otherParameter]
        }
    }
    
    // Local data for unit test.use empty data temporarily.
    public var sampleData: Data {
        return "".data(using: .utf8)!
    }
    
    // Represents an HTTP task.
    public var task: Task {
        switch self {
        case .updateExample(let image, _):
            let data = UIImageJPEGRepresentation(image, 0.7)
            let img = MultipartFormData(provider: .data(data!), name: "参数名", fileName: "名称随便写.jpg", mimeType: "image/jpeg")
            return .upload(.multipart([img]))
        default:
            return .request
        }
    }
   
    public var parameterEncoding: ParameterEncoding {
        // Select type of parameter encoding based on requirements.Usually we use 'URLEncoding.default'.
        /*
        if self.method == .get || self.method == .head {
            return URLEncoding.default
        } else {
            return JSONEncoding.default
        }
        */
        return URLEncoding.default
    }

这里对几个特殊的参数做出解释。sampleData是单元测试等等需要的假数据,只在有测试的需求下才用得到,一般我们返回一个空的数据就可以。parameterEncoding是参数编码方式,通常我们使用URLEncoding就可以,根据不同的需求去选择,如实例中注释代码。

插件机制

如果我们想在请求前和请求后做一些操作,Moya提供自定义请求插件来实现这一需求。比如我们要做请求完成前显示菊花,请求完成或失败后隐藏菊花,我们可以这样做。自定义插件需要遵守PluginType协议,根据不同需求实现协议里的方法。实例:

public final class RequestLoadingPlugin: PluginType {
    
    public func willSend(_ request: RequestType, target: TargetType) {
        // show loading
    }
    
    public func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        // hide loading
    }
    
}

ObjectMapper解析数据

我们使用ObjectMapper解析数据映射为model,ObjectMapper的使用这里不作介绍,请看官方文档。
我们为RxSwift中Observable写一个拓展,用于解析数据。通常我们服务器后台返回的数据格式类似以下形式,有的直接是一个我们要用到的json数据,有的在数据外还包一层表示请求状态的无用数据(code,msg 等等):

// success
{
code: 200,
msg: "success",
work_info: { }
}

or

{
work_comments: [ ]
}

// error
{
code: 500,
error: "token error",
}

所以在拓展里面提供两个方法去解析模型和数组,并可选的传入key值来解析返回的json数据中字典对应的key值。

func mapObject<T: Mappable>(type: T.Type, key: String? = nil) -> Observable<T> {
}
    
func mapArray<T: Mappable>(type: T.Type, key: String? = nil) -> Observable<[T]> {
}

另外我们可以自定义错误类型来处理数据解析中发生的错误。

enum RxSwiftMoyaError: String {
    case ParseJSONError
    case OtherError
    // you can define other error 
}


extension RxSwiftMoyaError: Swift.Error {}

有时候请求是成功的,但是请求内容是错误的,错误信息由我们的服务器返回,如前文提及。所以我们在这个拓展里提供了一个方法解析服务器返回的错误信息,若有错误则抛出,无则返回nil

fileprivate func parseError(response: [String: Any]?) -> NSError? {
        var error: NSError?
        if let value = response {
            if let code = value["code"] as? Int, code != 200 {
                var msg = ""
                if let message = value["error"] as? String {
                    msg = message
                }
                error = NSError(domain: "Network", code: code, userInfo: [NSLocalizedDescriptionKey: msg])
            }
        }
        return error
    }

异步编程 PromiseKit

现代编程语言都很好的支持了异步编程,因此在swift编程中,拥有功能强大且轻量级的异步编程工具的需求变得很强烈。

这是PromiseKit中提到的,所以我们不甘落后,也想来一发。
如官方文档中提到的异步链式调用,这很常见。详细使用请看官方文档,这里不做介绍。

login().then { json in
    //
}.catch { error in
    // handle error
}

例如,在本demo中我们使用MVVM模式。在viewModel中我们处理网络请求。在demo中ViewModelExample文件内,我们书写以下代码。首先,我们使用Moya的RxSwift拓展,来创建一个RxMoyaProvider,用于请求数据。在这里,我们创建Provider时可以自主选择插件,用于不同的需求。例如有些网络请求我们并不希望用户看到,所以不需要出现加载提示(如菊花,等等),我们在创建时就不需要加入插件。反之,我们可以在创建Provider时加入所需的插件以适应不同的需求。因此Moya变得更加灵活。实例:

let provider = RxMoyaProvider<ApiExample>(plugins: [RequestLoadingPlugin(),NetworkLogger()])

接着我们来创建一个Promise(异步任务)。

func getHomepagePageData() -> Promise<HomepageData> {
        return Promise(resolvers: { (result, error) in
            provider.request(.frontpage)
                .filterSuccessfulStatusCodes()
                .mapJSON()
                .mapObject(type: HomepageData.self)
                .subscribe(onNext: {
                    result($0)
                }, onError: {
                    error($0)
                })
                .addDisposableTo(disposeBag)
        })
    }

在这个方法内部,我们返回一个Promise,在实例化promise内部使用Moya请求数据。然后通过Observable拓展中的* .mapObject或者.mapArray方法来解析数据并返回,其中产生的错误通过.onError*回调进行处理。通过RxSwift链式调用,我们可以很简洁的书写网络请求的代码,是不是开始有些感觉了?

provider.request(.frontpage)
                .filterSuccessfulStatusCodes()
                .mapJSON()
                .mapObject(type: HomepageData.self)
                .subscribe(onNext: {
                    result($0)
                }, onError: {
                    error($0)
                })
                .addDisposableTo(disposeBag)

使用实例

我们在控制器里添加一个viewModel的实例,然后在网络请求的方法是实现viewModel中定义的相关请求方法即可。实例:

lazy var viewModel = ViewModelExample()

func getRequestExample() {
        viewModel.getHomepagePageData().then { data in
            // fetch data to refresh UI
            print(data.msg ?? "")
        }.always {
            // optional
            // always do something before request complete,such as cache data,etc.
            print("request complete")
        }.catch { (error) in
            // handle error
            print(error)
        }
    }


通过PromiseKit直接链式调用,我们很简洁的通过.then一步步的处理数据,通过.catch处理错误。通过Moya和RxSwift,我们发送请求和解析数据也变得灵活机动干净利落,不拖泥带水。同时因为我们通过枚举来管理不同业务的请求接口,代码逻辑也变得清晰。

总之,Moya+PromiseKit+Swift 所谓优雅的书写网络请求,非常值得尝试一下!

相关参考:
学习 Swift Moya(二)- Moya + SwiftyJSON + RxSwift
如何写出最简洁优雅的网络封装 Moya + RxSwift

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

推荐阅读更多精彩内容