原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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