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

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容