系統性學習Moya+Alamofire+RxSwift+ObjectMapper的配合使用

主要是練習Moya的熟練使用,全文涉及到CYLTabBarController搭建簡單易用的框架、Swift和OC互相調用、FLEX顯示界面層級UI的屬性、ObjectMapper解析數據、Kingfisher加載網絡圖片、MBProgressHUD融合到請求裏自動顯示與隱藏請求等待、MJRefresh作爲刷新簡單寫了一個類別、SDCycleScrollView顯示輪播圖、Then的使用,最終實現了一個簡單的界面...更加深入技術還在探究中,先放上本文的Demo

1315706-25e6e2eae85abbdd (1).png

示例圖片

既然是介紹Moya的就主要先來介紹它吧,Moya是對 Alamofire的進一步封裝,簡化了網絡請求,方便維護,方便單元測試,使用Moya項目中網絡請求類的部分可能長這樣,所有的請求集中放在一起,集體化管理很方便

點擊查看官方教程

Moya發送簡單的網絡請求

枚舉類型需滿足TargetType協議

1
2
3
4
5
6
7
public protocol TargetType {
var  baseURL: NSURL { get }
var  path: String { get }
var  method: Moya.Method { get }
var  parameters: [String: AnyObject]? { get }
var  sampleData: NSData { get }
}

實現一個枚舉代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import Foundation
import Moya
 
enum ApiManager {
case  getDantangList(String)
case  getNewsList
case  getMoreNews(String)
case  getThemeList
case  getThemeDesc(Int)
case  getNewsDesc(Int)
case  Create(title: String, body: String, userId: Int)
case  Login(phone:String,password:String)
case  Banner(String)
}
 
extension ApiManager: TargetType {
/// The target's base `URL`.
var  baseURL: URL {
switch  self {
case  .Create(_,_,_):
return  URL.init(string:  "http://jsonplaceholder.typicode.com/" )!
case  .getDantangList,.Banner:
return  URL.init(string:  "http://api.dantangapp.com/" )!
case  .Login:
return  URL.init(string:  "https://api.grtstar.cn" )!
default :
return  URL.init(string:  "http://news-at.zhihu.com/api/" )!
}
}
 
/// The path to be appended to `baseURL` to form the full `URL`.
var  path: String {
switch  self {
case  .getDantangList(let page):
return  "v1/channels/\(page)/items"
case  .getNewsList:
return  "4/news/latest"
case  .getMoreNews(let date):
return  "4/news/before/"  + date
case  .getThemeList:
return  "4/themes"
case  .getThemeDesc(let id):
return  "4/theme/\(id)"
case  .getNewsDesc(let id):
return  "4/news/\(id)"
case  .Create(_, _, _):
return  "posts"
case  .Login:
return  "/rest/user/certificate"
case  .Banner:
return  "v1/banners"
 
}
}
 
/// The HTTP method used in the request.
var  method: Moya.Method {
switch  self {
 
case  .Create(_, _, _):
return  .post
case  .Login:
return  .post
default :
return  .get
}
 
}
 
/// The parameters to be incoded in the request.
var  parameters: [String: Any]? {
switch  self {
case  .Create(let title, let body, let userId):
return  [ "title" : title,  "body" : body,  "userId" : userId]
 
case  .Login(let number, let passwords):
return  [ "mobile"  : number,  "password"  :  passwords, "deviceId" "12121312323" ]
case  .Banner(let strin):
return  [ "channel"  :strin]
 
default :
return  nil
 
}
}
 
/// The method used for parameter encoding.
var  parameterEncoding: ParameterEncoding {
return  URLEncoding. default
}
 
/// Provides stub data for use in testing.
var  sampleData: Data {
 
switch  self {
case  .Create(_, _, _):
return  "Create post successfully" .data(using: String.Encoding.utf8)!
default :
return  "" .data(using: String.Encoding.utf8)!
 
}
}
 
var  task: Task {
return  .request
}
 
/// Whether or not to perform Alamofire validation. Defaults to `false`.
var  validate: Bool {
return  false
}
}

現在就可以發送簡單的網絡請求了:

1.定義一個全局變量MoyaProvider

1
let ApiManagerProvider = MoyaProvider

2.發送網絡請求

1
2
3
4
5
6
7
ApiManagerProvider.request(.getNewsList) { (result) -> ()  in
case  let .success(response):
break
case  let .failure(error):
break  
 
}

MoyaProvider的初始化

我們觀察下MoyaProvider的初始化方法. MoyaProvider初始化都是有默認值的

1
2
3
4
5
6
public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
manager: Manager = MoyaProvider.defaultAlamofireManager(),
plugins: [PluginType] = [],
trackInflights: Bool =  false )

這些可選參數就是Moya的強大之處了 ,文章主要也是介紹如何使用這些插件的。

參數說明:

  • EndpointClosure

可以對請求參數做進一步的修改,如可以修改endpointByAddingParameters endpointByAddingHTTPHeaderFields等

  • RequestClosure 你可以在發送請求前,做點手腳. 如修改超時時間,打印一些數據等等

  • StubClosure可以設置請求的延遲時間,可以當做模擬慢速網絡

  • Manager 請求網絡請求的方式。默認是Alamofire

  • [PluginType]一些插件。回調的位置在發送請求後,接受服務器返回之前

稍後詳細介紹這部分內容。

RxSwift

Moya也有自己的RxSwift的擴展,不懂RxSwift的童鞋可以看下我們博客中的關於RxSwift庫介紹的文章。Moya使用RxSwift很簡單,如下所示我們只需要對請求結果進行監聽就行了

使用RxSwift可以這樣來請求

1
2
3
4
5
6
7
8
9
let provider = RxMoyaProvider() //要使用RxMoyaProvider創建provider,暫時不攜帶任何參數
provider.request(.getNewsList).subscribe { event  in
switch  event {
case  .next(let response):
// do something with the data
case  .error(let error):
// handle the error
}
}

我們還可以對Observable進行擴展,自定義一些自己流水線操作,比如自動實現json轉化Model,定義如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func mapObject(type: T.Type) -> Observable {
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
}
guard (dict[ "code" ] as?Int) != nil  else {
throw  RxSwiftMoyaError.ParseJSONError
}
 
if  let error = self.parseError(response: dict) {
throw  error
}
 
 
return  Mapper().map(JSON: dict)!
}
}

下邊的方法就需要根據服務器返回數據進行判斷了,我常用的邏輯是數據請求成功了才返回再就行界面賦值刷新操作,如果是狀態碼不成功就直接攔截拋出錯誤(後臺返回的message),比如是登錄密碼錯誤提示之類的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fileprivate func parseError(response: [String: Any]?) -> NSError? {
var  error: NSError?
if  let value = response {
var  code:Int?
 
//後臺的數據每次會返回code只有是200纔會表示邏輯正常執行
if  let codes = value[ "code" ] as?Int
{
code = codes
 
}
if   code != 200 {
var  msg =  ""
if  let message = value[ "message" ] as? String {
msg = message
}
error = NSError(domain:  "Network" , code: code!, userInfo: [NSLocalizedDescriptionKey: msg])
}
}
return  error
}

那麼就可以定義一個請求方法了

1
2
3
4
5
6
7
func login(phone: String, password:String) -> Observable {
return  provider.request(.Login(phone: phone, password: password))
.mapJSON()
.debug()  // 打印請求發送中的調試信息
 
.mapObject(type: UserModel.self)
}

如下代碼就完成了一次請求

1
2
3
4
5
6
7
8
let viewModel  = ViewModel(self)
viewModel.login(phone:  "156178...."  , password:  "11111" )
.subscribe(onNext: { (userModel: UserModel)  in
//do something with posts
print(userModel.user?.nickName ??  "" )
 
})
.addDisposableTo(dispose)

Moya也爲我們提供了很多Observable的擴展,讓我們能更輕鬆的處理MoyaResponse,常用的如下:

  • filter(statusCodes:) 過濾response狀態碼

  • filterSuccessfulStatusCodes() 過濾狀態碼爲請求成功的

  • mapJSON() 將請求response轉化爲JSON格式

  • mapString() 將請求response轉化爲String格式

具體可以參考官方文檔

下邊就說說RxMoyaProvider參數吧

EndpointClosure

沒寫什麼就打印下參數,請求方法,路徑..可以覈對

1
2
3
4
5
6
private func endpointMapping(target: Target) -> Endpoint {
print( "請求連接:\(target.baseURL)\(target.path) \n方法:\(target.method)\n參數:\(String(describing: target.parameters)) " )
4
4
return  MoyaProvider.defaultEndpointMapping( for : target)
}

manager

用的是Alamofire請求,這裏主要寫了一個忽略SSL驗證的方法,當然也可以在這裏修改請求頭等等

1
2
3
4
5
6
7
8
9
10
11
12
13
public func defaultAlamofireManager() -> Manager {
let configuration = URLSessionConfiguration. default
configuration.httpAdditionalHeaders = Alamofire.SessionManager.defaultHTTPHeaders
 
let policies: [String: ServerTrustPolicy] = [
 
"ap.dimain.cn" : .disableEvaluation
]
let manager = Alamofire.SessionManager(configuration: configuration,serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
 
manager.startRequestsImmediately =  false
return  manager
}

最有意思的還是插件了 ,可以自定義各種功能

plugins

plugins參數是一個數組的形式,遵循PluginType協議我們先看下PluginType的協議內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public protocol PluginType {
/// Called to modify a request before sending
//請求前可以修改一些request
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
 
/// Called immediately before a request is sent over the network (or stubbed).
//開始請求
func willSend(_ request: RequestType, target: TargetType)
 
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
//結束請求
func didReceive(_ result: Result, target: TargetType)
 
/// Called to modify a result before completion
func process(_ result: Result, target: TargetType) -> Result}

狀態條中的網絡加載提示,俗稱"菊花加載

networkActivityPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let networkActivityPlugin = NetworkActivityPlugin { (change) -> ()  in
 
 
switch (change){
 
case  .ended:
 
UIApplication.shared.isNetworkActivityIndicatorVisible =  false
 
case  .began:
 
UIApplication.shared.isNetworkActivityIndicatorVisible =  true
 
}
}

NetworkActivityPlugin是Moya提供的方法,還是根據PluginType的協議實現的

請求一般就需要loading了這裏用MBProgressHUD實現自動顯示隱藏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final class RequestLoadingPlugin: PluginType {
private let viewController: UIViewController
var  HUD:MBProgressHUD
var  hide:Bool
 
init(_ vc: UIViewController,_ hideView:Bool) {
self.viewController = vc
self.hide = hideView
HUD = MBProgressHUD.init()
guard self.hide  else  {
 
return
}
HUD = MBProgressHUD.showAdded(to: self.viewController.view, animated:  true )
 
}
 
public func willSend(_ request: RequestType, target: TargetType) {
print( "開始請求\(self.viewController)" )
 
if  self.hide  !=  false   {
 
HUD.mode = MBProgressHUDMode.indeterminate
HUD.label.text =  "加載中"
HUD.bezelView.color = UIColor.lightGray
 
HUD.removeFromSuperViewOnHide =  true
HUD.backgroundView.style = .solidColor  //或SolidColor
 
}
}
 
public func didReceive(_ result: Result, target: TargetType) {
print( "結束請求" )
HUD.hide(animated:  true )
 
}
 
}

修改請求頭想想不該放在插件了實現,應該是在manager裏實現,先放出來代碼吧

1
2
3
4
5
6
7
8
9
10
11
12
struct AuthPlugin: PluginType {
let token: String
 
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
var  request = request
request.timeoutInterval = 30
request.addValue(token, forHTTPHeaderField:  "token" )
request.addValue( "ios" , forHTTPHeaderField:  "platform" )
request.addValue( "version" , forHTTPHeaderField: Bundle.main.object(forInfoDictionaryKey:  "CFBundleShortVersionString" ) as! String)
return  request
}
}

請求時候遇到邏輯錯誤或者不滿足條件,參數錯誤等要提示這裏用的是Toast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//檢測token有效性
final class AccessTokenPlugin: PluginType {
private let viewController: UIViewController
 
init(_ vc: UIViewController) {
self.viewController = vc
}
 
public func willSend(_ request: RequestType, target: TargetType) {}
public func didReceive(_ result: Result, target: TargetType) {
switch  result {
case  .success(let response):
//請求狀態碼
guard  response.statusCode == 200    else  {
return
}
var  json:Dictionary? =  try ! JSONSerialization.jsonObject( with : response.data,options:.allowFragments) as! [String: Any]
print( "請求狀態碼\(json?[" status "] ?? " ")" )
guard (json?[ "message" ]) != nil   else  {
return
}
guard let codeString = json?[ "status" ] else  { return }
//請求狀態爲1時候立即返回不彈出任何提示 否則提示後臺返回的錯誤信息
guard codeString as! Int != 1  else { return }
self.viewController.view .makeToast( json?[ "message" ] as! String)
 
case  .failure(let error):
print( "出錯了\(error)" )
 
break
}
}
}

AccessTokenPlugin這個名字有點問題哈,起初是想在這裏判斷token不正確就退出登錄用的由於沒有合適的api就實現了請求結果的狀態判斷,這就自動實現了邏輯錯誤的提示了 不用一個請求一個請求的判斷了,還是挺方便的

有了這些插件就可以這樣初始化RxMoyaProvider

1
2
3
4
5
6
let provider :RxMoyaProviderprovider = RxMoyaProvider(
endpointClosure: endpointMapping,
manager:defaultAlamofireManager(),
plugins:[RequestLoadingPlugin(self.viewController, true ),
AccessTokenPlugin( self.viewController), NetworkLoggerPlugin(verbose:  true ),
networkActivityPlugin,AuthPlugin(token:  "暫時爲空" )]

關於Moya的用法先介紹到這裏後續我會繼續探究更加靈活全面的用法。

下邊介紹下Then的語法棉花糖吧,看例子吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_ = UILabel.init(frame: CGRect.init(x: 0, y: 0, width: kScreenW, height: 50)).then({ (make)  in
 
make.text =  "Then的簡單用法超讚????"
make.font = .systemFont(ofSize: 20)
make.textColor = .red
make.textAlignment = .center
self.view.addSubview(make)
 
})
 
UserDefaults.standard. do  {
$0.set( "devxoul" , forKey:  "username" )
$0.set( "[email protected]" , forKey:  "email" )
$0.synchronize()
 
let tableView = UITableView().then {
$0.backgroundColor = .clear
$0.separatorStyle = .none
$0.register(MyCell.self, forCellReuseIdentifier:  "myCell" )
}
 
}

如果佈局這樣還不簡單那再看下邊用ThenSnapKit一起使用的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
_ = UILabel().then({ (make)  in
make.text =  "Then的簡單用法超讚????"
make.font = .systemFont(ofSize: 20)
make.textColor = .red
make.textAlignment = .center
self.view.addSubview(make)
make.snp.makeConstraints({ (make)  in
make.top.left.right.equalTo(0)
make.height.equalTo(50)
 
})
 
})

再不滿意只能用Xib佈局了....

在Swift中用SDCycleScrollView輪播圖

SDCycleScrollView之前一直在OC中使用覺得很簡單又熟悉了所以這次寫的Demo依舊搬了過來,但是呢SDCycleScrollView裏實現圖片下載用的是SDWebImage,而Swift版本提供了Kingfisher那不可能都用了,因爲也不想放棄SDCycleScrollView就不得已修改了裏邊圖片下載的方法,在Swift項目裏OC類直接調用Swift類是調用不到的,所以我就諮詢了下找到一個合適辦法,新建Swift裏繼承SDCycleScrollView然後用Kingfisher實現圖片下載,方法比較簡單就是給開發者提供一個參考方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import UIKit
import SDCycleScrollView
import Kingfisher
class CustomSDCycleScrollView: SDCycleScrollView  {
 
//因爲之前庫裏邊用的是SDWebImageView 緩存的圖片 現在 換了Swift版本的Kingfisher所以 無奈修改了原庫的方法 重寫了下
open override func imageView(_ imageView: UIImageView!, url: URL!) -> UIImageView! {
let imageView: UIImageView? = imageView
imageView?.kf.setImage( with : url,placeholder:UIImage.init(named:  "tab_5th_h" ))
return  imageView
}
//重寫oc代碼 刪除緩存
override class func clearImagesCache()
{
let cache = KingfisherManager.shared.cache
 
// 獲取硬盤緩存的大小
cache.calculateDiskCacheSize { (size) -> ()  in
print( "磁盤緩存大小: \(size) bytes " )
cache.clearDiskCache()
 
}
}
 
 
}

用的時候直接使用CustomSDCycleScrollView即可

項目使用MJRefresh實現刷新

給UIScrollView寫了一個類別比較簡單代碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import UIKit
相關文章
相關標籤/搜索