簡介
Moya是一個網絡抽象層的第三方Swift庫,它主要集成了Alamofire,并做了一個抽象層的接口類叫MoyaProvider,利用這個provider就可以進行一些request了。
Network abstraction layer written in Swift.
用法
官方使用文檔地址: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發起網絡請求了。
下面以豆瓣電臺為例簡單演示下具體用法;
- 定義一個enum為DoubanAPI,并定義網絡接口:
enum DoubanAPI {
case channels
case playList(channel: String)
}
- 讓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:
NetworkLoggerPlugin
和NetworkActivityPlugin
,牽著用于請求信息的log打印,后者用于請求的監聽,有2種狀態began
和ended
;用法(注意是配合請求的發起者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
如果對你有幫助,別忘了給個??或??,有問題歡迎在下面留言討論。