Protocol Oriented Programming (POP) is a Programming paradigm of Swift, which was proposed by Apple in WWDC in 2015. A lot of POP can be seen in Swift standard library.
At the same time, Swift is an Object Oriented Programming language (OOP), OOP and POP are complementary to each other in Swift development, neither party can replace the other party. POP makes up for some of OOP’s design shortcomings.
POP and OOP
OOP has three major features: encapsulation, inheritance, and polymorphism.
An example of the use of inheritance: When multiple classes (such as A, B, or C) have many commonalities, these commonalities can be extracted into A parent class (such as D), and finally A, B, or C inherit from D.
But there are some problems that OOP does not solve very well, for example: extracting the common method run from BVC and DVC.
class BVC: UIViewController {
func run(a) {
print("run")}}class DVC: UITableViewController {
func run(a) {
print("run")}}Copy the code
Let’s look at the relationship between BVC and DVC.
Here are some possible solutions based on OOP:
-
Put the run method into another object, A, and then BVC and DVC have object A attributes, but this adds some extra dependencies.
-
Add the run method to the UIViewController class, but UIViewController is getting bloated and may affect all of its other subclasses.
Obviously, taking an OOP approach to solving problems has some drawbacks and is not perfect. Let’s see how POP solves this problem.
protocol Runnable {
func run(a)
}
extension Runnable {
func run(a) {
print("run")}}class BVC: UIViewController.Runnable {}class DVC: UITableViewController.Runnable {}Copy the code
Extract the RUN method through the protocol, and then use the extension of the protocol to implement the run method. When a controller needs to use a run method, it simply follows the relevant protocol and has the run method, thus compensating for some of the shortcomings of OOP implementation.
If we encounter more complex inheritance relationships, the benefits of POP implementation will be more obvious.
Tips for using POP:
- Create the protocol first, not the parent (base) class.
- Value types (struct, enum) are given priority over reference types (class).
- Make use of protocol extensions.
- Do not use protocols for protocol oriented purposes.
Two, the use of protocol to achieve the prefix effect
1. OC prefixes methods
When we extend methods to a class in OC, we usually add the extended methods to the class classification, and to prevent conflicts with native methods or methods from third-party libraries, we usually prefix the methods with our own.
@interface NSObject (SHRun)
- (void)sh_run;
@end
@implementation NSObject (SHRun)
- (void)sh_run {
NSLog(@"run");
}
@end
Copy the code
NSObject *objc = [NSObject new];
[objc sh_run];
Copy the code
The prefix is usually written in the following format: < prefix >_< method >. However, it is not good to use this method in Swift. The feeling of writing in this way is no different from OC, and does not reflect the characteristics of Swift. So how do you prefix Swift?
2. Swift prefixes methods
Suppose I want to add a numberCount method to String to get the number of numbers in the String. The implementation of numberCount looks like this:
func numberCount(string: String) -> Int {
var count = 0
for c in string where ("0"."9").contains(c) {
count + = 1
}
return count
}
Copy the code
The drawback of this method is obvious. It is a global method and requires a String to be passed. Since it is a method unique to String, we can extract numberCount into String’s extension.
extension String {
func numberCount(a) -> Int {
var count = 0
for c in self where ("0"."9").contains(c) {
count + = 1
}
return count
}
}
Copy the code
It is possible that the names of the extended methods will conflict. There are two ways to resolve the conflict. Like OC, String is prefixed in the same format as OC: < prefix >_< method >.
Another way is to add a prefix type to String and call the method in the form of < prefix >.< method >. Since it is called as < prefix >.< method >, it is obvious that the prefix is an attribute.
struct SHBase {
var string: String
func numberCount(a) -> Int {
var count = 0
for c in string where ("0"."9").contains(c) {
count + = 1
}
return count
}
}
extension String {
var sh: SHBase { SH(string: self)}}Copy the code
let count = "123abc456".sh.numberCount()
Copy the code
This way you can call the method as < prefix >.< method >. If other types also want to call methods as: < prefix >.< method >, do you want to add an attribute to the type to be extended to SH? This can cause a lot of problems, which can be solved by using generics.
struct SHBase<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}
extension String {
var sh: SHBase<String> { SHBase(self)}}Copy the code
"123abc456".sh
Copy the code
The String instance can now be called as.sh, and the same applies to adding an sh prefix to Person as follows:
class Person {}
extension Person {
var sh: SHBase<Person> { SHBase(self)}}Copy the code
let person = Person()
person.sh
Copy the code
Now, how do you expand things. Attention! < prefix >.< method > is a prefix type of method, so we end up extending SHBase methods. For example, extend a numberCount method to String.
First, a prefix type attribute is required, so add a prefix type attribute to String’s extension:
extension String {
var sh: SHBase<String> { SHBase(self)}}Copy the code
You then add a numberCount method to the prefix type via Extension to determine in Extension whether the prefix type’s generic is a String.
extension SHBase where Base= =String {
func numberCount(a) -> Int {
var count = 0
for c in base where ("0"."9").contains(c) {
count + = 1
}
return count
}
}
Copy the code
let count = "123abc456".sh.numberCount()
Copy the code
At this point we can call the method with < prefix >.< method > without calling other types of methods, such as Person’s run method. If we add an extended method to Person, we can do the same:
extension Person {
var sh: SHBase<Person> { SHBase(self)}}extension SHBase where Base: Person {
func run(a) {
print("run")}}Copy the code
let person = Person()
person.sh.run()
Copy the code
< prefix >.< method > to call a method, there are three steps:
-
Declare a generic type prefixed by a type.
-
Add the type attribute as extension to the type to which you want to add extension methods.
-
Finally, the prefix type extension method is used to determine whether the generic type needs to be extended in the extension of the prefix type and implement the extension content.
This is not perfect, however, because every time you want to prefix a type, you have to add the prefix type attribute in Extension. At this point we can settle the matter by agreement. The code is as follows:
protocol SHCompatible {}
extension SHCompatible {
var sh: SHBase<Self> { SHBase(self)}}Copy the code
Here, two features of the protocol are also used: one is that the extension of the protocol can add calculation attributes and methods; One is that the protocol Self can restore the real type. All you need to do at this point is comply with the SHCompatible protocol for the type to which you want to add extension methods to have the prefix type attribute for sh.
extension String: SHCompatible {}
extension Person: SHCompatible {}
Copy the code
let count = "123abc456".sh.numberCount()
let person = Person()
person.sh.run()
Copy the code
Instead of adding a prefix type attribute to every type that needs a prefix type, you just have to comply with SHCompatible to have the prefix type attribute. The sh attribute added above is the computed attribute of the instance, so instance methods can only be called through sh. To call the class method at this point, add a static sh to the SHCompatible protocol.
The code is as follows:
extension SHCompatible {
var sh: SHBase<Self> { SHBase(self)}static var sh: SHBase<Self>.Type { SHBase<Self>.self}}extension SHBase where Base: Person {
static func run(a) {
print("run")}}Copy the code
Person.sh.run()
Copy the code
As a final detail, the type that complies with the SHCompatible protocol may be a value type, and when the memory of the value type needs to be manipulated in the extension implementation’s method, mutating is added, so that the sh attribute we define cannot call the mutating method of the value type. At this point we can change the sh property to be readable and writable.
The code is as follows:
extension SHCompatible {
var sh: SHBase<Self> {
get { SHBase(self)}set{}}static var sh: SHBase<Self>.Type {
get{ SHBase<Self>.self }
set{}}}Copy the code
struct Person {
var age = 10
}
extension Person: SHCompatible {}extension SHBase where Base= =Person {
mutating func setAge(a) {
base.age = base.age + 8}}Copy the code
var person = Person()
person.sh.setAge()
Copy the code
3. Summary
To conclude, there are four steps to prefix type calls in Swift:
- Declares a prefix type, which is usually a struct.
- A protocol that declares prefix attributes and implements instance prefix attributes and statically typed prefix attributes in the protocol.
- The type of custom extension content that needs to be called with a prefix complies with the prefix attribute protocol.
- Type extension content is implemented by prefix type extension.
The complete example is as follows:
// Prefix type
struct SHBase<Base> {
var base: Base
init(_ base: Base) {
self.base = base
}
}
// Protocol with prefix attributes
protocol SHCompatible {}
extension SHCompatible {
var sh: SHBase<Self> {
get { SHBase(self)}set{}}static var sh: SHBase<Self>.Type {
get{ SHBase<Self>.self }
set{}}}// Adhere to the type of protocol that has the prefix attribute
extension String: SHCompatible {}
// Use prefix type extension to extend the content of String
extension SHBase where Base= =String {
func numberCount(a) -> Int {
var count = 0
for c in base where ("0"."9").contains(c) {
count + = 1
}
return count
}
}
/ / call
let count = "123abc456".sh.numberCount()
print(count) / / 6
Copy the code
Third, Moya
Most of our development is done with the web, and most of iOS is done with third-party libraries: AFNetworking or Alamofire. Both encapsulate the official URLSession and avoid using the official API for development.
However, after using AFNetworking or Alamofire for a long time, you will find AFNetworking or Alamofire-related codes scattered everywhere in the App, which will cause inconvenience and unified management. In addition, we will find that many codes are duplicated, so we will create a Network Manager to manage the code related to Network requests.
When dealing with the Network, we only need to deal with the encapsulated Network Manager. The purpose of Network Manager is to isolate the third-party Network request library. When the third-party Network request library needs to be replaced, It just needs to be replaced inside the Network Manager, which has no impact on our upper-level business logic.
If the encapsulated Network Manager is not ideal, it may result in directly bypassing the Network Manager and dealing with the underlying Network request library. Let’s take a look at the diagram provided by Moya:
Therefore, the role of Moya is actually the Network Manager we mentioned, which is an abstraction of Network business logic. We only need to follow relevant protocols to initiate Network requests without the need for low-level details.
Moya document address: Chinese document; English document.
1. Basic use of Moya
Since Moya is an intermediate layer of Network request, its use is different from the Network Manager we usually write. Moya is more Swift, and Moya is built with the paradigm of protocol-oriented programming.
Moya has one protocol: TargetType; This protocol defines the basic network request data that is usually needed, and it is simpler to define. Take a look at what TargetType looks like.
If you want to add baseURL, Path, method, headers to your TargetType, you can create a new file myService. swift for your API, and define an enumeration: MyService.
enum MyService {
case zen
case showUser(id: Int)
case createUser(firstName: String, lastName: String)
case updateUser(id: Int, firstName: String, lastName: String)
case showAccounts
}
Copy the code
The enumeration is defined as a set of API entries, and the enumeration complies with the TargetType protocol to populate the API’s baseURL and path.
// MARK: - TargetType Protocol Implementation
extension MyService: TargetType {
var baseURL: URL { return URL(string: "https://api.myservice.com")! }
var path: String {
switch self {
case .zen:
return "/zen"
case .showUser(let id), .updateUser(let id, _._) :return "/users/\(id)"
case .createUser(_._) :return "/users"
case .showAccounts:
return "/accounts"}}var method: Moya.Method {
switch self {
case .zen, .showUser, .showAccounts:
return .get
case .createUser, .updateUser:
return .post
}
}
var task: Task {
switch self {
case .zen, .showUser, .showAccounts: // Send no parameters
return .requestPlain
case let .updateUser(_, firstName, lastName): // Always sends parameters in URL, regardless of which HTTP method is used
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: URLEncoding.queryString)
case let .createUser(firstName, lastName): // Always send parameters as JSON in request body
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
}
}
var sampleData: Data {
switch self {
case .zen:
return "Half measures are as bad as nothing at all.".utf8Encoded
case .showUser(let id):
return "{\"id\": \(id).\"first_name\": \"Harry\".\"last_name\": \"Potter\"}".utf8Encoded
case .createUser(let firstName, let lastName):
return "{\"id\": 100, \"first_name\": \"\(firstName)\".\"last_name\": \"\(lastName)\"}".utf8Encoded
case .updateUser(let id, let firstName, let lastName):
return "{\"id\": \(id).\"first_name\": \"\(firstName)\".\"last_name\": \"\(lastName)\"}".utf8Encoded
case .showAccounts:
// Provided you have a file named accounts.json in your bundle.
guard let url = Bundle.main.url(forResource: "accounts", withExtension: "json"),
let data = try? Data(contentsOf: url) else {
return Data()}return data
}
}
var headers: [String: String]? {
return ["Content-type": "application/json"]}}// MARK: - Helpers
private extension String {
var urlEscaped: String {
addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
}
var utf8Encoded: Data {
Data(self.utf8)
}
}
Copy the code
Using TargetType, you can define the urlString, parameter, and request method required by the request. At this point, you’ll notice that all the preparations for our request are already written in TargetType, and the next step is to initiate the network request.
When using Moya externally to make network requests, just send the request through the MoyaProvider object:
let provider = MoyaProvider<MyService>()
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
// do something with the result (read on for more details)
}
provider.request(.updateUser(id: 123, firstName: "Harry", lastName: "Potter")) { result in
// do something with the result (read on for more details)
}
Copy the code
We can also customize the Endpoint to do a few things:
let endpointClosure = { (target: MyService) - >Endpoint in
return Endpoint(url: URL(target: target).absoluteString, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task)
}
Copy the code
We’ll talk about Endpoint objects next, just to give you an idea, and finally, how do we get the data in the network request when we make it.
provider.request(.zen) { result in
// do something with `result`
}
Copy the code
The Request method is passed a MyService value (.zen) that contains all the necessary information to create an Endpoint, The Endpoint instance object is used to create an URLRequest (the heavy work is done by Alamofire), and the request is sent (also by -alamofire). Once Alamofire gets a response (or doesn’t get one), Moya will wrap success or failure with enumResult, which is either.success(moya.response) or.failure(MoyaError).
So, we can get the data we need from Moya.Response:
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
let data = moyaResponse.data // Data, your JSON response is probably in here!
let statusCode = moyaResponse.statusCode // Int - 200, 401, 500, etc
// do something in your app
case let .failure(error):
// TODO: handle the error == best. comment. ever.}}Copy the code
2. Construction of Moya
After looking at the source code of Moya, the summarized modules can be roughly divided into five modules:
The main data processing process of Moya can be represented by the following graph:
MoyaProvider
Moya first defines the basic configuration information of the network request through TargetType, and then the MoyaProvider object initiates the actual network request. MoyaProvider complies with one protocol: MoyaProviderType; MoyaProviderType is defined as follows:
As you can see, MoyaProviderType is basically a request method that defines a request method. The first parameter must comply with a generic parameter of the TargetType protocol type through the associated type. Take a look at how MoyaProvider implements the request method of the protocol:
In requestNormal, the type that complies with the TargetType protocol is first generated as an Endpoint type.
The Endpoint is a class that contains parts of the TargetType, such as URL, method, and so on.
Inside the requestNormal method, we will eventually call requestClosure, which is a closure expression that calls requestClosure as follows:
requestClosure(endpoint, performNetworking)
Copy the code
As you can see, requestClosure receives an endpoint and a performNetworking, which is obviously a network request operation. At the end, requestNormal returns a type of CancellableWrapper, which governs whether to cancel the current network request task.
What does this requestClosure do internally? Let’s see where the closure expression is assigned:
RequestClosure has an initial value by default when the MoyaProvider initializes, and endpointClosure has a default initial value (described below).
Can see requestClosure is essentially MoyaProvider defaultRequestMapping, let’s take a look at this function in the internal all did some what:
As you can see, the defaultRequestMapping method does only one thing, generating URLRequest through the Endpoint. In requestNormal, the Endpoint is generated by calling the Endpoint method. The internal implementation of the Endpoint is very simple, which calls the closure expression just mentioned: EndpointClosure.
EndpointClosure is essentially MoyaProvider defaultEndpointMapping, this method is used to generate the Endpoint, we look at:
As mentioned earlier, the Endpoint is used to generate URLRequest. How does the Endpoint generate URLRequest? The Endpoint generates different types of URlRequests based on the current task.
After the URLRequest is generated, the network request is initiated by calling the performRequest method in requestNormal’s performNetworking closure, which is the entry point for Moya to initiate the network request.
In performRequest, different requests are handled depending on the Endpoint task, but the sendAlamofireRequest method is ultimately called internally. SendAlamofireRequest initiates a network request through Moya to the method encapsulated by Alamofire.
Moya+Alamofire
Moya’s encapsulation of Alamofire is very simple. It extends Alamofire’s Request through the Requestable protocol. The Requestable only declares a response method, which returns a Self type. Self gets the actual type of the Request in Alamofire and calls the Resume method to start the current Request.
Let’s take a final look at what DataRequest and DownloadRequest are:
Moya’s main data processing process ends here, with the callback handling after the network request is done in the sendAlamofireRequest method.