原創:知識點總結性文章
創作不易,請珍惜,之后會持續更新,不斷完善
個人比較喜歡做筆記和寫總結,畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長歷程,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉,大家可通過command + F 輸入目錄標題后迅速尋找到你所需要的內容
目錄
- 一、POP面向協議編程
- 1、POP 面向協議編程相比面向對象編程的優勢
- 2、使用POP進行網絡請求
- 3、總結與解惑
- 二、初識Moya
- 1、Moya的簡介
- 2、豆瓣范例
- 3、訂單范例
- 4、登錄范例
- Demo
- 參考文獻
一、POP面向協議編程
1、POP 面向協議編程相比面向對象編程的優勢
a、橫切關注點問題
指的是我們很難在不同繼承關系的類里共用代碼。想要解決這個問題,我們有幾個方案。
Copy & Paste
這是一個比較糟糕的解決方案,但是演講現場還是有不少朋友選擇了這個方案,特別是在工期很緊,無暇優化的情況下。這誠然可以理解,但是這也是壞代碼的開頭。我們應該盡量避免這種做法。
引入 BaseViewController
在一個繼承自 UIViewController
的 BaseViewController
上添加需要共享的代碼,或者干脆在 UIViewController
上添加 extension
。看起來這是一個稍微靠譜的做法,但是如果不斷這么做,會讓所謂的 Base
很快變成垃圾堆。職責不明確,任何東西都能扔進 Base
,你完全不知道哪些類走了 Base
,而這個“超級類”對代碼的影響也會不可預估。
依賴注入
通過外界傳入一個帶有 myMethod
的對象,用新的類型來提供這個功能。這是一個稍好的方式,但是引入額外的依賴關系,可能也是我們不太愿意看到的。
面向協議
現在通過面向協議的方式,任何遵循協議的對象都可以使用協議中的方法和屬性,比如只有對象遵守了下面代碼中的PersonProtocl
協議就可以使用 name
屬性以及sayHello()
方法。
b、POP 解決橫切關注點
? 提供聲明
protocol PersonProtocl
{
// 協議屬性
var name: String {get}
// 協議方法
func sayHello()
}
通過結構體來實現協議
struct Teacher: PersonProtocl
{
var name: String
func sayHello()
{
print("同學們好,請把周末的作業交上來")
}
}
struct Student: PersonProtocl
{
var name: String
func sayHello()
{
print("老師你好,我作業放在家里忘帶了")
}
}
進行調用
override func viewDidLoad()
{
super.viewDidLoad()
let teacher = Teacher(name: "蔣紅")
let student = Student(name: "謝佳培")
teacher.sayHello()
student.sayHello()
}
輸出結果為
同學們好,請把周末的作業交上來
老師你好,我作業放在家里忘帶了
? 擴展實現
但是仍然存在一個很大的問題,那就是協議里的方法和屬性缺乏具體的實現。如果只是提供聲明,那意味著我們還需要在每一個類里面都實現一遍,那協議就顯得比較雞肋了,而且有很多時候這些方法是共有的,不需要太多的特定實現。這時候就需要對協議提供默認實現的協議擴展閃亮登場了。
extension PersonProtocl
{
func sayHello()
{
print("hello! boy")
}
}
對其進行調用
class UsePop: UIViewController, PersonProtocl
{
var name: String = ""
override func viewDidLoad()
{
super.viewDidLoad()
sayHello()
}
}
輸出結果為
hello! boy
c、POP 解決動態派發安全性
Objective-C
恰如其名,是一門典型的 OOP 語言,同時它繼承了 Small Talk
的消息發送機制。這套機制十分靈活,是 OC 的基礎思想,但是有時候相對危險。考慮下面的代碼:
ViewController *v1 = ...
[v1 myMethod];
AnotherViewController *v2 = ...
[v2 myMethod];
NSArray *array = @[v1, v2];
for (id obj in array)
{
[obj myMethod];
}
我們如果在 ViewController
和 AnotherViewController
中都實現了 myMethod
的話,這段代碼是沒有問題的。myMethod
將會被動態發送給 array
中的 v1
和 v2
。但是,要是我們有一個沒有實現 myMethod
的類型,會如何呢?
NSObject *v3 = [NSObject new]
// v3 沒有實現 `myMethod`
NSArray *array = @[v1, v2, v3];
for (id obj in array)
{
[obj myMethod];
}
編譯依然可以通過,但是顯然,程序將在運行時崩潰。Objective-C
是不安全的,編譯器默認你知道某個方法確實有實現,這是消息發送的靈活性所必須付出的代價。而在 app 開發看來,用可能的崩潰來換取靈活性,顯然這個代價太大了。雖然這不是OOP
范式的問題,但它確實在 Objective-C
時代給我們帶來了切膚之痛。
Runtime error: unrecognized selector sent to instance blabla
與之相對,對于沒有實現 Protocl
提供的屬性和方法的對象,編譯器將進行錯誤提示,因此更加安全。
Type 'Teacher' does not conform to protocol 'PersonProtocl' Do you want to add protocol stubs?
d、POP 解決菱形缺陷
繼承中存在的一個重要問題是菱形缺陷,也就是子類無法確定使用哪個父類的方法。在協議的對應方面,這個問題依然存在,因為多個協議可能存在相同的協議屬性、協議方法,遵循者也是無法確定使用的是哪個協議中的方法,所以我們在開發中一定要盡量規避多個協議中的同名問題。
protocol AnimalProtocl
{
// 協議屬性
var name: String {get}
// 協議方法
func sayHello()
func canNotThink()
}
遵守協議
struct Teacher: PersonProtocl, AnimalProtocl
{
var name: String
func sayHello()
{
print("同學們好,請把周末的作業交上來")
}
func canNotThink()
{
print("動物無法思考,僅僅憑借生存本能行動")
}
}
進行調用
func solveProblem()
{
let teacher = Teacher(name: "蔣紅")
let student = Student(name: "謝佳培")
teacher.sayHello()
student.sayHello()
teacher.canNotThink()
}
輸出結果
同學們好,請把周末的作業交上來
老師你好,我作業放在家里忘帶了
動物無法思考,僅僅憑借生存本能行動
如果我們為其中的某個協議進行了擴展,在其中提供了默認的 name
實現,這樣的編譯是可以通過的,雖然 Teacher
中沒有定義 name
,但是通過 AnimalProtocl
的 name
,Teacher
依然可以遵守 PersonProtocl
。
extension AnimalProtocl
{
var name: String { return "another default name" }
}
struct Teacher: PersonProtocl, AnimalProtocl
{
// let name: String
}
不過,當 PersonProtocl
和 AnimalProtocl
都有 name
的協議擴展的話,就無法編譯了。這種情況下,Teacher
無法確定要使用哪個協議擴展中 name
的定義。在同時實現兩個含有同名元素的協議,并且它們都提供了默認擴展時,我們需要在具體的類型中明確地提供實現。這里我們將 Teacher
中的 name
進行實現就可以了。
extension PersonProtocl
{
var name: String { return "default name" }
}
extension AnimalProtocl
{
var name: String { return "another default name" }
}
struct Teacher: PersonProtocl, AnimalProtocl
{
let name: String
}
let teacher = Teacher(name: "蔣紅")
2、使用POP進行網絡請求
a、直接在ViewController (代表應用層) 進行網絡請求
- 應用層與網絡層耦合在一起,但應用層其實根本不應該關心網絡請求的方法、接口、參數
- 到處嵌套,可復用性特別低
class StudentAndTeacher: UIViewController
{
AF.request("http://127.0.0.1:5000/pythonJson/")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData
{ response in
switch response.result
{
case .success:
print(response)
//let _ = LoginClient.json(data: response.data)
case .failure(let error):
print(error)
}
}
}
b、提供信息能力者
? 通過面向協議的方式給 PersonRequest 賦予網絡請求的能力(能夠提供網絡請求需要的各種屬性)
// 請求協議
protocol Requestable
{
// 請求路徑
var path: String { get }
// 請求方法
var method: HTTPMethod { get }
// 請求參數
var parameter: [String: Any] { get }
// 遵守解碼協議的關聯類型
// 通過在 Requestable 協議中添加一個關聯類型,我們可以將回調參數進行抽象
associatedtype Response: DecodableProtocol
}
? 遵守請求協議
struct PersonRequest: Requestable
{
// 相應地添加類型定義,以滿足協議,默認使用的數據模型是 Person
typealias Response = Person
// 未定義初始值的 name 屬性
let name: String
// 將 host 和 path 拼接起來可以得到我們需要請求的 API 地址
var path: String
{
return "/users/\(name)"
}
// 在我們的例子中只會使用到 GET 請求
let method: HTTPMethod = .GET
// 因為請求的參數用戶名 name 會通過 URL 進行傳遞,所以 parameter 是一個空字典就足夠了
let parameter: [String: Any] = [:]
}
c、網絡請求能力者
? HTTPMethod 提供本模塊 PersonRequest 需要的請求方法枚舉
enum HTTPMethod: String
{
case GET
case POST
}
? 客戶端協議:提供基地址屬性和發送請求方法
-
T
是遵守請求協議的范型,request
是請求,handler
是請求完成后的回調閉包,Response
是遵守解碼協議的關聯類型 - 定義了可逃逸的閉包
(T.Response?) -> Void
。在請求完成后,我們調用這個handler
方法來通知調用者請求是否完成,如果一切正常,則將一個數據模型Person
實例傳回,否則傳回nil
- 我們想要發送請求的
send
方法對于所有的Request
都通用,所以顯然回調的參數類型不能是數據模型Person
- 因為
Requestable
是含有關聯類型的協議,所以它并不能作為獨立的類型來使用,我們只能夠將它作為類型約束,來限制輸入參數request
- 除了使用
<T: Request>
這個泛型方式以外,我們還將host
從Requestable
移動到了Client
里,這是更適合它的地方
protocol ClientProtocol
{
// 基地址屬性
var host: String { get }
// 發送請求方法
func send<T: Requestable>(_ request: T, handler: @escaping (T.Response?) -> Void)
}
? 客戶端遵守客戶端協議
除了 URLSessionClient
以外,我們還可以使用任意的類型來滿足這個協議,并發送請求。這樣網絡層的具體實現和請求本身就不再相關了,我們之后在測試的時候會進一步看到這么做所帶來的好處。
class URLSessionClient: ClientProtocol
{
// 創建客戶端管理者
static let manager = URLSessionClient()
// 給基地址賦值
let host: String = "http://127.0.0.1:5000"
// 實現發送請求方法
func send<T>(_ request: T, handler: @escaping (T.Response?) -> Void) where T : Requestable
{
...
}
}
? 實現發送請求方法
func send<T>(_ request: T, handler: @escaping (T.Response?) -> Void) where T : Requestable
{
// 請求地址 = 基地址 + request的傳入路徑
let url = URL(string: host.appending(request.path))!
// 根據url創建URLRequest
var urlRequest = URLRequest(url: url)
// 設置請求方法
urlRequest.httpMethod = request.method.rawValue
// 根據request創建dataTask并將請求發送
let task = URLSession.shared.dataTask(with: urlRequest)
// 使用 Response 中的 parse 方法將回調中的 data 轉換為合適的對象類型,并調用 handler 通知外部調用者
{ (data, response, error) in
// 調用Response里面的解碼方法將請求到的數據解碼成model后從主線程傳遞出去
if let data = data, let model = T.Response.parse(data: data)
{
DispatchQueue.main.async { handler(model) }
}
else
{
DispatchQueue.main.async { handler(nil) }
}
}
task.resume()
}
d、序列化能力者
? 解碼協議提供解碼方法
請求不應該也不需要知道如何解析得到的數據,這項工作應該交給 Response
來做,而現在我們沒有對 Response
進行任何限定。接下來我們將新增一個協議,滿足這個協議的類型將知道如何將一個 data
轉換為實際的數據類型。
對于 Person
我們知道可以使用 Person.init(data:)
將json
數據進行轉化成數據模型,但是對于一般的 Response
,我們還不知道要如何將數據轉為模型。DecodableProtocol
要求滿足該協議的具體類型提供parse(data:)
方法合適的實現,這樣提供轉換方法的任務就被“下放”到了各數據模型中。
protocol DecodableProtocol
{
static func parse(data: Data) -> Self?
}
DecodableProtocol
定義了一個靜態的 parse
方法,接下去我們需要在 Requestable
的 Response
關聯類型中為它加上這個限制,這樣我們可以保證所有的 Response
都可以對數據進行解析。
protocol Requestable
{
var path: String { get }
var method: HTTPMethod { get }
var parameter: [String: Any] { get }
associatedtype Response: DecodableProtocol
}
? 遵守解碼協議實現解碼方法
extension Person: DecodableProtocol
{
static func parse(data: Data) -> Person?
{
// 傳入data獲取到Person,調用Person的初始化方法
return Person(data: data)
}
}
? 數據模型類Person
struct Person
{
// 屬性
let name: String
let age: String
let hobby: String
let petPhrase: String
// 初始化方法
init?(data: Data)
{
// [String: Any] 表示是字典類型
guard let person = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil }
// 獲取person中的數據
guard let name = person["name"] as? String else { return nil }
guard let age = person["age"] as? String else { return nil }
guard let hobby = person["hobby"] as? String else { return nil }
guard let petPhrase = person["petPhrase"] as? String else { return nil }
// 給Person結構體的屬性賦值
self.name = name
self.age = age
self.hobby = hobby
self.petPhrase = petPhrase
}
}
e、外界調用
當然,你也可以為 URLSessionClient
添加一個單例來減少請求時的創建開銷,或者為請求添加 Promise
的調用方式等等。在 POP
的組織下,這些改動都很自然,也不會牽扯到請求的其他部分。你可以用和 UserRequest
類型相似的方式,為網絡層添加其他的 API 請求,只需要定義請求所必要的內容,而不用擔心會觸及網絡方面的具體實現。
// 根據傳入的name創建request
let request = PersonRequest(name: "Xiejiapei")
// 客戶端發送request
URLSessionClient().send(request)
{ [weak self](person) in
// 根據服務端返回的數據更新UI
if let person = person
{
// 更新UI
print("\(person.hobby) from \(person.name)")
self?.updataUI(person: person)
}
}
f、進行模塊劃分而不是全部堆砌在Request協議中的原因
倘若不進行功能模塊劃分,而是將全部功能都放在Request
協議中,就會變成下面這樣。這里最大的問題在于,Request
協議管理了太多的東西。一個 Request
協議應該做的事情應該僅僅是定義請求入口和期望的響應類型,而現在 Request
協議不光定義了 host
的值,還對如何解析數據了如指掌。最后 send
方法被綁死在了 URLSession
的實現上,而且是作為 Request
協議的一部分存在,這是很不合理的,因為這意味著我們無法在不更改請求的情況下更新發送請求的方式,它們被耦合在了一起。這樣的結構讓測試變得異常困難,我們可能需要通過 stub
和 mock
的方式對請求攔截,然后返回構造的數據,這會用到 NSURLProtocol
的內容,或者是引入一些第三方的測試框架,大大增加了項目的復雜度。
protocol Request
{
var host: String { get }
var path: String { get }
var method: HTTPMethod { get }
var parameter: [String: Any] { get }
associatedtype Response
func parse(data: Data) -> Response?
}
extension Request
{
func send(handler: @escaping (Response?) -> Void)
{
...
}
}
g、網絡層測試
將 Client
聲明為協議給我們帶來了額外的好處,那就是我們不再局限于使用某種特定的技術 (比如這里的 URLSession
) 來實現網絡請求。利用 POP
,你只是定義了一個發送請求的協議,你可以很容易地使用像是 AFNetworking
或者 Alamofire
這樣的成熟的第三方框架來構建具體的數據并處理請求的底層實現。我們甚至可以提供一組“虛假”的對請求的響應,用來進行測試。這和傳統的 stub
& mock
的方式在概念上是接近的,但是實現起來要簡單得多,也明確得多。我們現在來看一看具體應該怎么做。
我們先準備一個文本文件,將它添加到項目的測試 target
中,作為網絡請求返回的內容
// 文件名:usersXiejiapei
{"name":"姓名:謝佳培", "age": "年齡:23", "hobby": "愛好:讀書", "petPhrase": "格言:求知若渴,虛心若愚"}
接下來,可以創建一個新的類型,讓它滿足 ClientProtocol
協議。但是與 URLSessionClient
不同,這個新類型的 send
方法并不會實際去創建請求,并發送給服務器。我們在測試時需要驗證的是一個請求發出后如果服務器正確響應,那么我們應該也可以得到正確的模型實例。所以這個新的 LocalFileClient
需要做的事情就是從本地文件中加載定義好的結果,然后驗證模型實例是否正確。
struct LocalFileClient: ClientProtocol
{
// 為了滿足 ClientProtocol 的要求,實際上我們不會發送請求
let host = ""
func send<T : Requestable>(_ request: T, handler: @escaping (T.Response?) -> Void)
{
switch request.path
{
case "/users/xiejiapei":
// 獲取fileURL
guard let fileURL = Bundle.main.url(forResource: "usersXiejiapei", withExtension: "") else { fatalError() }
// 根據fileURL獲取data
guard let data = try? Data(contentsOf: fileURL) else { fatalError() }
// 將data傳遞出去
handler(T.Response.parse(data: data))
default:
fatalError("Unknown path")
}
}
}
LocalFileClient
做的事情很簡單,它先檢查輸入請求的 path
屬性,如果是 /users/Xiejiapei
(也就是我們需要測試的請求),那么就從測試的 bundle
中讀取預先定義的文件,將其作為返回結果進行 parse
,然后調用 handler
。如果我們需要增加其他請求的測試,可以添加新的 case
項。
在 LocalFileClient
的幫助下,現在可以很容易地對 UserRequest
進行測試了。通過這種方法,我們沒有依賴任何第三方測試庫,也沒有使用 url
代理或者運行時消息轉發等等這些復雜的技術,就可以進行請求測試了。保持簡單的代碼和邏輯,對于項目維護和發展是至關重要的。
let client = LocalFileClient()
client.send(PersonRequest(name: "xiejiapei"))
{ [weak self](person) in
if let person = person
{
print("\(person.hobby) from \(person.name)")
self?.updataUI(person: person)
}
}
3、總結與解惑
a、總結
因為高度解耦,這種基于 POP
的實現為代碼的擴展提供了相對寬松的可能性。我們剛才已經說過,你不必自行去實現一個完整的 Client
,而可以依賴于現有的網絡請求框架,實現請求發送的方法即可。也就是說,你也可以很容易地將某個正在使用的請求方式替換為另外的方式,而不會影響到請求的定義和使用。類似地,在 Response
的處理上,現在我們定義了 Decodable
,用自己手寫的方式在解析模型。我們完全也可以使用任意的第三方 JSON
解析庫,來幫助我們迅速構建模型類型,這僅僅只需要實現一個將 Data
轉換為對應模型類型的方法即可。
通過面向協議的編程,我們可以從傳統的繼承上解放出來,用一種更靈活的方式,搭積木一樣對程序進行組裝。每個協議專注于自己的功能,特別得益于協議擴展,我們可以減少類和繼承帶來的共享狀態的風險,讓代碼更加清晰。高度的協議化有助于解耦、測試以及擴展,而結合泛型來使用協議,更可以讓我們免于動態調用和類型轉換的苦惱,保證了代碼的安全性。
b、解惑
? 范例都是直接先寫 protocol,而不是 struct 或者 class,是不是我們在實踐 POP 的時候都應該直接先定義協議?
我直接寫 protocol
是因為我已經對我要做什么有充分的了解。但是實際開發的時候你可能會無法一開始就寫出合適的協議定義。建議可以像我在 demo
中做的那樣,先“粗略”地進行定義,然后通過不斷重構來得到一個最終的版本。當然,你也可以先用紙筆勾勒一個輪廓,然后再去定義和實現協議。當然了,也沒人規定一定需要先定義協議,你完全也可以從普通類型開始寫起,然后等發現共通點或者遇到我們之前提到的困境時,再回頭看看是不是面向協議更加合適,這需要一定的 POP
經驗。
? 既然 POP 有這么多好處,那我們是不是不再需要面向對象,可以全面轉向面向協議了?
答案可能讓你失望。在我們的日常項目中,每天打交道的 Cocoa 其實還是一個帶有濃厚 OOP
色彩的框架。也就是說,可能一段時期內我們不可能拋棄 OOP
。不過 POP
其實可以和 OOP
“和諧共處”,我們也已經看到了不少使用 POP
改善代碼設計的例子。另外需要補充的是,POP
其實也并不是銀彈,它有不好的一面。最大的問題是協議會增加代碼的抽象層級 (這點上和類繼承是一樣的),特別是當你的協議又繼承了其他協議的時候,這個問題尤為嚴重。在經過若干層的繼承后,滿足末端的協議會變得困難,你也難以確定某個方法究竟滿足的是哪個協議的要求。這會讓代碼迅速變得復雜。如果一個協議并沒有能描述很多共通點,或者說能讓人很快理解的話,可能使用基本的類型還會更簡單一些。
? 想問一下你們在項目中使用 POP 的情況
我們在項目里用了很多 POP
的概念。上面 demo
里的網絡請求的例子就是從實際項目中抽出來的,我們覺得這樣的請求寫起來非常輕松,因為代碼很簡單,新人進來交接也十分愜意。除了模型層之外,我們在 view
和 view controller
層也用了一些 POP
的代碼,比如支持分頁請求 tableview controller
的 NextPageLoadable
,空列表時顯示頁面的 EmptyPage
等等。因為時間有限,不可能展開一一說明,所以這里我只挑選了一個具有代表性,又不是很復雜的網絡的例子。其實每個協議都讓我們的代碼,特別是 View Controller
變短,而且使測試變為可能。可以說,我們的項目從 POP
受益良多,而且我們應該會繼續使用下去。
二、初識Moya
1、Moya的簡介
我們知道在 iOS 開發中,可以使用 URLSession
進行網絡請求。但為了方便起見,我通常會選擇使用 Alamofire
這樣的第三方庫。這些庫本質上也是基于 URLSession
的,但其封裝了許多細節,可以讓我們網絡請求相關代碼(如獲取數據,提交數據,上傳文件,下載文件等)更加簡潔易用。Moya
又是一個基于 Alamofire
的更高層網絡請求封裝抽象層。Moya
也就可以看做我們的網絡管理層,用來封裝 URL
、參數等請求所需要的一些基本信息。使用后我們的客戶端代碼會直接操作 Moya
,然后 Moya
去管理請求,而不用跟 Alamofire
進行直接接觸。
在我們項目的 Service
、View
、或者 Model
文件中可能都會出現請求網絡數據的情況,如果直接使用 Alamofire
,不僅很繁瑣,而且還會使代碼變得很混亂。過去我們通常的做法是在項目中添加一個網絡請求層ViewModel
用來管理網絡請求,但這樣做可能會遇到一些問題。
2、豆瓣范例
a、創建provider:如果要發起網絡請求就使用這個 provider
// 初始化豆瓣FM請求的provider
let DouBanProvider = MoyaProvider<DouBan>()
b、請求類型
public enum DouBan
{
case channels //獲取頻道列表
case playlist(String) //獲取歌曲信息
}
c、配置請求信息
extension DouBan: TargetType
{
...
}
? 服務器地址
public var baseURL: URL
{
switch self
{
case .channels:
return URL(string: "https://www.douban.com")!
case .playlist(_):
return URL(string: "https://douban.fm")!
}
}
? 各個請求的具體路徑
public var path: String
{
switch self
{
case .channels:
return "/j/app/radio/channels"
case .playlist(_):
return "/j/mine/playlist"
}
}
? 請求方法類型
public var method: Moya.Method
{
return .get
}
? 請求任務事件(這里附帶上參數)
public var task: Task
{
switch self
{
case .playlist(let channel):
var params: [String: Any] = [:]
params["channel"] = channel
params["type"] = "n"
params["from"] = "mainsite"
return .requestParameters(parameters: params, encoding: URLEncoding.default)
default:
return .requestPlain
}
}
? 是否執行Alamofire驗證
public var validate: Bool
{
return false
}
? 下面這個是做單元測試模擬的數據,只會在單元測試文件中有作用
public var sampleData: Data
{
return "{}".data(using: String.Encoding.utf8)!
}
? 設置請求頭
public var headers: [String: String]?
{
return nil
}
d、使用我們的provider進行網絡請求(獲取頻道列表數據)
// 頻道列表數據
var channels:Array<JSON> = []
DouBanProvider.request(.channels)
{ result in
if case let .success(response) = result
{
// 解析數據
let data = try? response.mapJSON()
let json = JSON(data!)
self.channels = json["channels"].arrayValue
// 刷新表格數據
DispatchQueue.main.async
{
self.tableView.reloadData()
}
}
}
e、使用我們的provider進行網絡請求(根據頻道ID獲取下面的歌曲)
DouBanProvider.request(.playlist(channelId))
{ result in
if case let .success(response) = result
{
// 解析數據,獲取歌曲信息
let data = try? response.mapJSON()
let json = JSON(data!)
let music = json["song"].arrayValue[0]
let artist = music["artist"].stringValue
let title = music["title"].stringValue
let message = "歌手:\(artist)\n歌曲:\(title)"
// 將歌曲信息彈出顯示
self.showAlert(title: channelName, message: message)
}
}
3、訂單范例
a、準備工作
- 請求地址:http://127.0.0.1:8080
- 公共請求頭:devtype:iOS,devid
- 公共請求參數:token:"Gz1qYLXeBW8MZuUfDlr9wsAYuVS1cZFMJY9BbaF842L2gRps747o4w=="
API | 參數 | 說明 |
---|---|---|
order/list | pageNO:訂單列表開始頁碼,默認從1開始。 pageSize:每頁記錄數 | 訂單列表 |
order/findById | sn:訂單id | 根據id查詢訂單 |
b、配置請求信息
? 生成請求封裝類
let orderProvider = MoyaProvider<OrderApi>()
? 訂單相關api
enum OrderApi
{
case list(pageNO: Int = 1, pageSize: Int = 10) //很好的利用了枚舉綁定值這個特性
case findOne(sn: String)
}
c、實現TargetType協議
extension OrderApi: TargetType
{
...
}
? baseURL
var baseURL: URL
{
return URL(string: "http://127.0.0.1:8080/order")!
}
? 請求路徑
var path: String
{
switch self
{
case .list:
return "list"
case .findOne(_):
return "findById"
}
}
? 請求方式
var method: Moya.Method
{
return .post
}
? 解析格式
var sampleData: Data
{
return "{}".data(using: String.Encoding.utf8)!
}
? 創建請求任務
var task: Task
{
// 公共參數
var params: [String: Any] = ["token": "Gz1qYLXeBW8MZuUfDlr9wsAYuVS1cZFMJY9BbaF842L2gRps747o4w=="]
// 收集參數
switch self
{
case let .list(pageNO, pageSize):
params["pageNO"] = pageNO
params["pageSize"] = pageSize
case .findOne(let sn):
params["sn"] = sn
}
// 發起請求
return .requestParameters(parameters: params, encoding: URLEncoding.default)
}
? 公共請求頭
var headers: [String : String]?
{
return ["devtype": "iOS", "devid": UIDevice().identifierForVendor?.uuidString ?? "unknow"]
}
d、調用發送請求
orderProvider.request(OrderApi.findOne(sn: "DJKRE3248DFHJEW23"))
{ (result) in
print(result)
}
4、登錄范例
a、LoginAPI
類型
public enum LoginAPI
{
case login(String, String, String) // 登錄接口
case smscode(String) // 登錄,發送驗證碼
case otherRequest // 其他接口,沒有參數
}
服務器地址
public var baseURL: URL
{
return URL(string:"http://127.0.0.1:5000/")!
}
各個請求的具體路徑
public var path: String
{
switch self
{
case .login:
return "login/"
case .smscode:
return "login/smscode/"
case .otherRequest:
return "login/otherRequest/"
}
}
請求方式
public var method: Moya.Method
{
switch self
{
case .login:
return .post
case .smscode:
return .post
default:
return .get
}
}
請求任務事件(這里附帶上參數)
public var task: Task
{
var param:[String:Any] = [:]
switch self
{
case .login(let username,let password,let smscode):
param["username"] = username
param["password"] = password
param["smscode"] = smscode
case .smscode(let username):
param["username"] = username
default:
return .requestPlain
}
return .requestParameters(parameters: param, encoding: URLEncoding.default)
}
設置請求頭
public var headers: [String: String]?
{
return nil
}
b、LoginClient
static let manager = LoginClient()
發送驗證碼
func smscode(username:String,complete:@escaping ((String) -> Void))
{
let provide = MoyaProvider<LoginAPI>()
provide.request(.smscode(username))
{ (result) in
switch result
{
case let .success(response):
let dict = LoginClient.myJson(data: response.data)
complete(dict["smscode"] as! String)
case let .failure(error):
print(error)
complete("")
}
}
}
進行登錄
func login(username:String,password:String,smscode:String)
{
let provide = MoyaProvider<LoginAPI>()
provide.request(.login(username, password, smscode))
{ (result) in
switch result
{
case let .success(response):
let _ = LoginClient.myJson(data: response.data)
case let .failure(error):
print(error)
}
}
}
其他事件 - 比如注冊
func otherRequest()
{
let provide = MoyaProvider<LoginAPI>()
provide.request(.otherRequest)
{ (result) in
switch result
{
case let .success(response):
let _ = LoginClient.myJson(data: response.data)
case let .failure(error):
print(error)
}
}
}
序列化
static func myJson(data:Data?)->([String: Any])
{
guard let data = data else
{
print("data 為空")
return [:]
}
do
{
let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print("序列化字典: \(dict)")
return dict as! ([String : Any])
}
catch
{
print("序列化失敗")
return [:]
}
}
c、點擊登錄或者注冊
點擊發送驗證碼
@objc func didClickCodeButton()
{
guard let username = usernameTF.text else
{
print("賬戶不可為空")
return
}
LoginClient.manager.smscode(username: username)
{ [weak self](smscode) in
self?.smscodeTF.text = smscode
}
}
點擊登錄
@objc func didClickLoginButton()
{
LoginClient.manager.login(username:usernameTF.text!, password: passwordTF.text!, smscode: smscodeTF.text!)
}
Demo
Demo在我的Github上,歡迎下載。
UseFrameworkDemo