iOS | Moya第三方网络抽象层Swift库

Moya

简介

Moya是一个网络抽象层的第三方Swift库,它主要集成了Alamofire,并做了一个抽象层的接口类叫MoyaProvider,利用这个provider就可以进行一些request了。
Network abstraction layer written in Swift.

Moya对比

用法

官方使用文档地址:https://moya.github.io

对比

  • 以往我们进行网络请求,一般是用系统的URLSession,然后新建一个Task进行请求;

  • 或者用Alamofire直接调用其基于URLSession封装的请求方法request(_:),但如果每个请求都使用相同的一堆代码,进行response处理代码的话,就有点冗余了;

  • 所以Moya做的事情就是把请求的具体实现封装到内部,然后定义一个协议TargetType,基于这个协议你可以指定每个请求的baseURL、path、method、parameters、parametersEncoding等,方便集中管理每个项目模块中用到的数据接口;

集成

  • 要手动集成Moya,你可以用CocoaPods也可以用Carthage,也支持Swift Package Manager,并且有Rx和ReactNative的版本,具体用法见https://moya.github.io

  • 个人推荐使用Carthage,对Swift支持得更好;

Target
  • 要想使用Moya,就得让所用的API接口遵守Moya.TargetType协议,然后创建一个Moya.Provider<Moya.TargetType>对象就可以针对你的Target发起网络请求了。

  • 下面以豆瓣电台为例简单演示下具体用法;

  1. 定义一个enum为DoubanAPI,并定义网络接口:
enum DoubanAPI {
    case channels
    case playList(channel: String)
}
  1. 让DoubanAPI遵守TargetType协议,并实现相应的属性:
var task: Task{
    return .request
}
  • 注意这里的Task一共有3种,可以针对不同的api接口用switch self指定各自的task类型:

public enum Task {
// 普通网络请求
case request
// 文件上传
case upload(Moya.UploadType)
// 文件下载
case download(Moya.DownloadType)
}

  • 接着实现协议中的其他属性
var baseURL: URL{
    switch self {
    case .channels:
        return URL(string: "https://www.douban.com")!
    case .playList(_):
        return URL(string: "https://douban.fm")!
    }
}

var path: String{
    switch self {
    case .channels:
        return "/j/app/radio/channels"
    case .playList(_):
        return "/j/mine/playlist"
    }
}

var method: Moya.Method{
    return .get
}
// 是否需要Alamofire校验url
var validate: Bool{
    return false
}
// 测试数据,单元测试时用
var sampleData: Data{
    return "{}".data(using: .utf8)!
}

var parameters: [String : Any]?{
    switch self {
    case .playList(let channel):
        return ["channel": channel, 
                "type": "n", 
                "from": "mainsite"]
    default:
        return nil
    }
}

var parameterEncoding: ParameterEncoding{
    return URLEncoding.default
}
Request
let provider = MoyaProvider<DoubanAPI>()
provider.request(target) {
    switch $0{
    case .success(let response):
        print("[Network Request] : \(response.request?.url?.absoluteString ?? "")")
        
        // 数据解析成JSON
        guard  let json: [String: Any] = response.json() else{
            failure(.jsonMapping(response))
            return
        }
        
        // 网络返回的错误提示信息:如用户名不存在等;
        guard let status = json["status"] as? Bool, status else{
            error(json["message"] as? String ?? "未知错误")
            return
        }
        
        // 网络请求成功
        success(json)
    case .failure(let error):
        // 服务器错误:如网络连接失败,请求超时等;
        failure(error)
    }
}
  • 注意上边的response.json()方法是对Moya.Response的扩展,用来将Data解析成JSON;
extension Moya.Response{
    func json<T>() -> T?{
        guard 
            let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? T else {
                return nil
        }
        return json
    }
}
  • 但是如果每个接口,都要新建一个MoyaProvider,再发起请求,未免有点太过麻烦,所以可以考虑再封装一层为Network;
import UIKit
import Moya

struct Network {
// 注意这里只是针对特定DoubanAPI的Provider这样有局限性
    static let defaultProvider = MoyaProvider<DoubanAPI>()
    
    static func request(_ target: DoubanAPI
                        success: @escaping (([String: Any]) -> Void), // 成功
                        error: @escaping ((String) -> Void),  // 服务器错误提示
                        failure: @escaping ((MoyaError) -> Void)){ // 网络请求失败
        defaultProvider.request(target) { /*进行一些处理,这里就和上边的一样了*/ 
                }
    }
}
  • 使用
Network.request(.channels, viewController: self, success: { 
            guard 
                let array = $0["channels"] as? [[String: Any]] else{
                    print("数据解析失败")
                    return
            }
            self.data = array
            self.tableView.reloadData()
        }, error: { 
            self.showErrorAlert(title: "数据请求失败", message: $0)
        }) { 
            self.showErrorAlert(title: "网络错误", message: $0.localizedDescription)
        }
  • 错误提示
extension UIViewController{
    func showErrorAlert(title: String?, message: String){
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(.init(title: "OK", style: .cancel, handler: nil))
        
        present(alert, animated: true, completion: nil)
    }
}
  • 这样一来,就可以在任何地方简洁使用provider的request了;

  • 不过这里也有一个问题,虽然封装出来了,但上边的Network显然不能适配更灵活的请情况,比如我还有一个模块叫MovieAPI,那就不能用Network.request了,因为以上只是针对DoubanAPI的Target进行的请求;

  • 好在Moya提供了一个叫MultiTarget的enum,当然它是基于TargetType的,只是里边把一个单独的target给包裹起来,达到适配的目的;

  • 对Network的改造如下:

// 只是简单讲DoubanAPI改为通配的MultiTarget
static let defaultProvider = MoyaProvider<MultiTarget>()
  • 使用(只需基于target新建一个MultiTarget)public init(_ target: TargetType)
Network.request(MultiTarget(DoubanAPI.channels))...
Network.request(MultiTarget(MovieAPI.list))...
Download
  • 向DoubanAPI增加一个下载mp4的接口:case downloadMP4(String)
  • 指定下载的baseURL和path、task:
var task: Task{
        switch self {
        case .downloadMP4(_):
// 下载文件需要指定下载目录
            return .download(.request(DefaultDownloadDestination))
        default:
            return .request
        }
    }
var baseURL: URL{
        switch self {
        case .downloadMP4(let url):
            return URL(string: url)!
        }
    }
    
    var path: String{
        switch self {
        default:
            return ""
        }
    }
  • 默认的下载目录为Documents
let DefaultDownloadDestination: DownloadDestination = { temporaryURL, response in
    let directoryURLs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    
    if !directoryURLs.isEmpty {
        return (directoryURLs[0].appendingPathComponent(response.suggestedFilename!), [.removePreviousFile])
    }
    
    return (temporaryURL, [])
}
  • 在Network中封装统一的下载方法:
struct Network {
    typealias Success = (([String: Any]) -> Void)
    typealias Error = ((String) -> Void)
    typealias Failure = ((MoyaError) -> Void)
    typealias Progress = ((Double, Bool) -> Void)
}
static func download(_ target: MultiTarget, 
                         progress: @escaping Progress, 
                         failure: @escaping Failure){
        defaultProvider.request(target, queue: DispatchQueue.main, progress: { 
            progress($0.progress, $0.completed)
        }) { 
            switch $0{
                case .success:
                    progress(1, true)
                case .failure(let error):
                    failure(error)
            }
        }
    }
  • 使用
@IBAction func downloadMP4(_ sender: Any){
        self.downloadBtn.isEnabled = false
        Network.download(MultiTarget(API.downloadMP4(self.url ?? "")), progress: { (progress, isCompleted) in
            
            let title = isCompleted ? "已下载" : "\(progress * 100) %"
            self.downloadBtn.titleLabel?.text = title
            self.downloadBtn.setTitle(title, for: .normal)
            
        }) { 
            self.showErrorAlert(title: "下载失败", message: $0.errorDescription ?? "未知错误")
            self.downloadBtn.isEnabled = true
        }
    }
Upload
  • 增加API网络接口task:
var task: Task{
        switch self {
        case let .uploadGif(data):
            return .upload(.multipart([
                .init(provider: .data(data), name: "file")
            ]))
        }
    }
  • 指定baseURL、path和parameters、method等:
var baseURL: URL{
        switch self {
        case .uploadGif:
            return URL(string: "https://upload.giphy.com")!
        }
    }
    
    var path: String{
        switch self {
        case .uploadGif:
            return "/v1/gifs"
        }
    }
    
    var method: Moya.Method{
        switch self {
        case .uploadGif:
            return .post
              }
    }
  • 在Network中增加upload方法:
static func upload(_ target: MultiTarget, 
                         progress: @escaping Progress, 
                         failure: @escaping Failure, 
                         error: @escaping Error){
        defaultProvider.request(target, queue: DispatchQueue.main, progress: { 
                if let response = $0.response{ 
//服务器有可能会报错误,此时progress却为1
                response.statusCode == 200 
                    ? progress($0.progress, $0.completed)
                    : failure(MoyaError.statusCode(response))
            }
        }) { 
            switch $0{
            case let .success(response):
                if let json: JSONDictionary = response.json(),
                    let meta = json["meta"] as? JSONDictionary,
                    let status = meta["status"] as? Int, 
                    let msg = meta["msg"] as? String{
                    status == 200 && msg == "OK"
                        ? progress(1, true) 
                        : error(msg)
                }
                else{
                    error("未知原因")
                }
            case .failure(let error):
                failure(error)
            }
        }
    }
  • 使用:
@IBAction func uploadGif(_ sender: Any?) {
        uploadBtn.isUserInteractionEnabled = false
    Network.upload(MultiTarget.init(API.uploadGif(animatedBirdGifData())), progress: { 
            let title = ($0 >= 1 && $1) ? "上传完成" : "\(Int($0 * 100)) %"
            self.uploadBtn.titleLabel?.text = title
            self.uploadBtn.setTitle(title, for: .normal)
        }, failure: { 
            handleUploadError($0.localizedDescription)
        }){
            handleUploadError($0)
        }
        
        func handleUploadError(_ error: String){
            self.showErrorAlert(title: "上传Gif失败", message: error)
            self.uploadBtn.isUserInteractionEnabled = true
            self.uploadBtn.setTitle("重新上传", for: .normal)
        }
    }
Plugin
  • 在Moya中有一个协议叫PluginType,作用是在发起请求和请求结束时回调,进行一些信息处理和提示,如HUD提示,打印请求信息等;

  • Moya默认提供了2个plugin:NetworkLoggerPluginNetworkActivityPlugin,牵着用于请求信息的log打印,后者用于请求的监听,有2种状态beganended

  • 用法(注意是配合请求的发起者provider使用的):

static let defaultProvider = MoyaProvider<MultiTarget>(plugins:[
// verbose为true时,也会打印response的body数据
        NetworkLoggerPlugin(verbose: true),
        NetworkActivityPlugin(networkActivityClosure: { 
            print($0 == .began ? "正在加载..." : "加载完成")
        })
    ])
  • 自定义plugin(HUDLoading控件):
import UIKit
import Moya
import Result

final class RequestLoadingPlugin: PluginType {
    private let viewController: UIViewController
    private var spinner: UIActivityIndicatorView!
    
    init(viewController: UIViewController) {
        self.viewController = viewController
        
        let view = UIView(frame: viewController.view.bounds)
        view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        spinner = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
        spinner.center = view.center
        view.addSubview(spinner)
        viewController.view.addSubview(view)
    }
    //协议方法
// 在一个请求发起前,可以动态修改URLRequest里的内容,做一些调整,比如重设request的超时时间、缓存策略、Cookies设置、允许移动网络等;
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {

        print("[Network Request] : \(request.url?.absoluteString ?? "")")

        return request
    }
// 发起请求
    func willSend(_ request: RequestType, target: TargetType) {
        print("[Network Request Target] : \(target)")
    }
    
// 收到服务器响应
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        print("请求完成")
        spinner.superview?.removeFromSuperview()
        
        guard case let Result.failure(error) = result else { return }
        
        let alert = UIAlertController(title: "数据请求失败", message: error.errorDescription ?? "未知错误", preferredStyle: .alert)
        alert.addAction(.init(title: "好", style: .cancel, handler: nil))
        viewController.present(alert, animated: true, completion: nil)
    }
// 处理返回数据,可以对数据做一些操作    
    func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response, MoyaError> {

        print("数据处理")
        return result
    }

总结

个人觉得Moya很强大,能够适用于很多多模块项目的网络请求中,并且提供plugin,方便灵活,且内置了Alamofire第三库,在Swift项目中推荐使用。

Github

https://github.com/BackWorld/MoyaDemo

Demo效果

如果对你有帮助,别忘了给个👍或😍,有问题欢迎在下面留言讨论。

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