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閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370