IOS框架使用:Moya

原創:知識點總結性文章
創作不易,請珍惜,之后會持續更新,不斷完善
個人比較喜歡做筆記和寫總結,畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長歷程,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉,大家可通過command + F 輸入目錄標題后迅速尋找到你所需要的內容

目錄

  • 一、POP面向協議編程
    • 1、POP 面向協議編程相比面向對象編程的優勢
    • 2、使用POP進行網絡請求
    • 3、總結與解惑
  • 二、初識Moya
    • 1、Moya的簡介
    • 2、豆瓣范例
    • 3、訂單范例
    • 4、登錄范例
  • Demo
  • 參考文獻

一、POP面向協議編程

1、POP 面向協議編程相比面向對象編程的優勢

a、橫切關注點問題

指的是我們很難在不同繼承關系的類里共用代碼。想要解決這個問題,我們有幾個方案。

Copy & Paste

這是一個比較糟糕的解決方案,但是演講現場還是有不少朋友選擇了這個方案,特別是在工期很緊,無暇優化的情況下。這誠然可以理解,但是這也是壞代碼的開頭。我們應該盡量避免這種做法。

引入 BaseViewController

在一個繼承自 UIViewControllerBaseViewController 上添加需要共享的代碼,或者干脆在 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];
}

我們如果在 ViewControllerAnotherViewController 中都實現了 myMethod 的話,這段代碼是沒有問題的。myMethod 將會被動態發送給 array 中的 v1v2。但是,要是我們有一個沒有實現 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,但是通過 AnimalProtoclnameTeacher 依然可以遵守 PersonProtocl

extension AnimalProtocl
{
    var name: String { return "another default name" }
}

struct Teacher: PersonProtocl, AnimalProtocl
{
    // let name: String 
}

不過,當 PersonProtoclAnimalProtocl 都有 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> 這個泛型方式以外,我們還將 hostRequestable 移動到了 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 方法,接下去我們需要在 RequestableResponse 關聯類型中為它加上這個限制,這樣我們可以保證所有的 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 協議的一部分存在,這是很不合理的,因為這意味著我們無法在不更改請求的情況下更新發送請求的方式,它們被耦合在了一起。這樣的結構讓測試變得異常困難,我們可能需要通過 stubmock 的方式對請求攔截,然后返回構造的數據,這會用到 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 里的網絡請求的例子就是從實際項目中抽出來的,我們覺得這樣的請求寫起來非常輕松,因為代碼很簡單,新人進來交接也十分愜意。除了模型層之外,我們在 viewview controller 層也用了一些 POP 的代碼,比如支持分頁請求 tableview controllerNextPageLoadable,空列表時顯示頁面的 EmptyPage 等等。因為時間有限,不可能展開一一說明,所以這里我只挑選了一個具有代表性,又不是很復雜的網絡的例子。其實每個協議都讓我們的代碼,特別是 View Controller 變短,而且使測試變為可能。可以說,我們的項目從 POP 受益良多,而且我們應該會繼續使用下去。


二、初識Moya

1、Moya的簡介

我們知道在 iOS 開發中,可以使用 URLSession 進行網絡請求。但為了方便起見,我通常會選擇使用 Alamofire 這樣的第三方庫。這些庫本質上也是基于 URLSession 的,但其封裝了許多細節,可以讓我們網絡請求相關代碼(如獲取數據,提交數據,上傳文件,下載文件等)更加簡潔易用。Moya 又是一個基于 Alamofire 的更高層網絡請求封裝抽象層。Moya 也就可以看做我們的網絡管理層,用來封裝 URL、參數等請求所需要的一些基本信息。使用后我們的客戶端代碼會直接操作 Moya,然后 Moya 去管理請求,而不用跟 Alamofire 進行直接接觸。

在我們項目的 ServiceView、或者 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

參考文獻

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容