主要是練習Moya的熟練使用,全文涉及到CYLTabBarController搭建簡單易用的框架、Swift和OC互相調用、FLEX顯示界面層級UI的屬性、ObjectMapper解析數據、Kingfisher加載網絡圖片、MBProgressHUD融合到請求裏自動顯示與隱藏請求等待、MJRefresh作爲刷新簡單寫了一個類別、SDCycleScrollView顯示輪播圖、Then的使用,最終實現了一個簡單的界面...更加深入技術還在探究中,先放上本文的Demo
示例圖片
既然是介紹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(_,_,_):
case
.getDantangList,.Banner:
case
.Login:
default
:
}
}
/// 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.synchronize()
let tableView = UITableView().then {
$0.backgroundColor = .clear
$0.separatorStyle = .none
$0.register(MyCell.self, forCellReuseIdentifier:
"myCell"
)
}
}
|
如果佈局這樣還不簡單那再看下邊用Then和SnapKit一起使用的方式
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
|