Always wanted to write something about Swift, but didn’t know where to start. Because there are so many things I want to write, and then everything gets jumbled up and nothing comes out. Take a look at some of the things you’ve shared with your group and think about putting them together for a blog post. I made a list of things I planned to write (these days I like to make a list of everything before I go to bed, which has helped my procrastination somewhat 🤪) :
- Protocol oriented programming
- Use value types instead of reference types
- Functional programming
- Unidirectional data flow
Protocol-oriented programming is one of the features that sets Swift apart from other languages, and one that is much more powerful than Objective-C (not unique to Swift, but much more powerful than OC’s protocol), so it’s fitting to start this Swift series with protocol-oriented programming.
The content of the article may be a little long, SO I listed the content to be talked about briefly. Students can jump to the corresponding summary to read according to their own situation. Here are the main points:
- Protocol oriented programming is not a new concept
- Protocol in Swift
- Start with a drawing application. By implementing a drawing application to explain the use of protocol in Swift
- A protocol with Self and association types
- The protocol with Self. Explain how to use Self? In the protocol by implementing a binary lookup
- A protocol with an association type. How to use association types in protocols by implementing a data loader with loading animation
- Protocol and function distribution. An example using polymorphism is used to illustrate how function distribution behaves in the protocol
- Use protocols to improve existing code design
Protocol oriented programming is not a new concept
Protocol oriented programming is not a new concept, it is in fact widely known as interface oriented programming. Protocol Oriented Programming is a Programming paradigm of Swift proposed by Apple at WWDC in 2015.
Many programmers understand object-oriented concepts like classes, objects, inheritance, and interfaces. But what is the difference between classes and interfaces? Why use an interface when you have a class? I believe many people have such a question. Interfaces (protocols are the same thing) define types, and implementing interfaces (subtyping) allows us to substitute one object for another. Class inheritance, on the other hand, is a mechanism for defining the implementation and type of an object by reusing the functionality of a parent class or simply sharing code and representation. Class inheritance allows us to quickly define new classes by inheriting most of the functionality we need from existing classes. So interfaces focus on types (the use of a type as if it were another type), while classes focus on reuse. Understanding this distinction will help you know when to use interfaces and when to use classes.
GoF, in his book Design Patterns, describes the principles of reusable object-oriented software design:
Program for interfaces, not implementations
Defining groups with the same interface is important because polymorphism is interface-based. Other object-oriented programming languages, such as Java, allow us to define “interface” types, which define a client directly into a “contract” with all other concrete classes. The equivalent in Objective-C and Swift is protocol. A protocol is also a contract between objects, but cannot be instantiated as an object itself. Implement protocols or inherit from abstract classes so that objects share the same interface. Therefore, any object of a subtype can respond to an interface of a protocol or abstract class.
Protocol in Swift
At WWDC2015, Apple announced Swift 2. The new version includes many new language features. Of the many changes, the most notable is the Protocol Extensions. In Swift’s first release, we could extend functionality to an existing class, struct, or enum with Extension. In Swift 2, we can also add an extension to Protocol. While this new feature may not seem like much at first, the Protocol Extensions are actually powerful enough to change some of Swift’s previous programming ideas. I’ll present a protocol Extension use case in a real project later.
In addition to protocol extension, there are several protocols in Swift that have other features, such as protocols with association types and protocols that include Self. There are some differences between these two protocols and normal protocols, and I will give specific examples later.
We are now ready to start writing code to master the techniques of using the Swift protocol in real development. The following drawing application and binary search examples are from this Session in WWDC2015. Before writing this article, THE author also thought about many examples, but always felt that the official example is not good. So my advice is: watch this Session at least once. After reviewing it, start writing your own implementation.
Start with a drawing application
Now we can learn how to use the protocol in Swift by completing a specific requirement.
Our requirement is to implement a drawing program that can draw complex graphics. We can first define a simple drawing process with a Render:
struct Renderer {
func move(to p: CGPoint) { print("move to (\(p.x).\(p.y))")}func line(to p: CGPoint) { print("line to (\(p.x).\(p.y))")}
func arc(at center: CGPoint, radius: CGFloat, starAngle: CGFloat, endAngle: CGFloat) {
print("arc at center: \(center), radius: \(radius), startAngel: \(starAngle), endAngle: \(endAngle)")}}Copy the code
You can then define a Drawable protocol to define a draw operation:
protocol Drawable {
func draw(with render: Renderer)
}
Copy the code
The Drawable protocol defines a draw operation that accepts a concrete drawing tool to draw. This separates the drawable content from the actual drawing operation for the purpose of separation of responsibilities, as you’ll see later.
If we want to draw a circle, we can simply draw a circle using the drawing tool implemented above, as follows:
struct Circle: Drawable {
let center: CGPoint
let radius: CGFloat
func draw(with render: Renderer) {
render.arc(at: center, radius: radius, starAngle: 0, endAngle: CGFloat.pi * 2)}}Copy the code
Now that we want to draw a polygon, the Drawable protocol makes it very easy:
struct Polygon: Drawable {
let corners: [CGPoint]
func draw(with render: Renderer) {
if corners.isEmpty { return }
render.move(to: corners.last!)
for p in corners { render.line(to: p) }
}
}
Copy the code
Now that the simple graphics have been drawn, we are ready to complete our drawing program:
struct Diagram: Drawable {
let elements: [Drawable]
func draw(with render: Renderer) {
for ele in elements { ele.draw(with: render) }
}
}
let render = Renderer(a)let circle = Circle(center: CGPoint(x: 100, y: 100), radius: 100)
let triangle = Polygon(corners: [
CGPoint(x: 100, y: 0),
CGPoint(x: 0, y: 150),
CGPoint(x: 200, y: 150)])
let client = Diagram(elements: [triangle, circle])
client.draw(with: render)
// Result:
// move to (200.0, 150.0)
// line to (100.0, 0.0)
// line to (0.0, 150.0)
// line to (200.0, 150.0)
Arc at Center: (100.0, 100.0), radius: 100.0, startAngel: 0.0, endAngle: 6.28318530717959
Copy the code
Through the above code is very easy to implement a simple drawing program. However, the current drawing program can only show the drawing process in the console. What if we want to draw it on the screen? It’s actually quite easy to draw content on the screen using the same protocol. We can change the Renderer structure to protocol:
protocol Renderer {
func move(to p: CGPoint)
func line(to p: CGPoint)
func arc(at center: CGPoint, radius: CGFloat, starAngle: CGFloat, endAngle: CGFloat)
}
Copy the code
Now that the Renderer is done, we can use CoreGraphics to draw graphics on the screen:
extension CGContext: Renderer {
func line(to p: CGPoint) {
addLine(to: p)
}
func arc(at center: CGPoint, radius: CGFloat, starAngle: CGFloat, endAngle: CGFloat) {
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: starAngle, endAngle: endAngle, clockwise: true)
addPath(path)
}
}
Copy the code
Rendering is done very simply by extending CGContext to comply with the Renderer protocol and then using the interface provided by CGContext. Here is the final result of the drawing program:
The key to completing the drawing program above is to separate the definition of graphics from the actual drawing operation, and to complete a highly extensible program by designing Drawable and Renderer protocols. To draw other shapes, just implement a new Drawable. For example, I want to draw something like this:
We can scale the original Diagram. The code is as follows:
let big = Diagram(elements: [triangle, circle])
diagram = Diagram(elements: [big, big.scaled(by: 0.2)])
Copy the code
By implementing the Renderer protocol, you can create console based drawing programs, CoreGraphics drawing programs, and even OpenGL drawing programs as easily as possible. This programming philosophy is very useful when writing cross-platform programs.
A protocol with Self and association types
As I pointed out in the previous section, there are some differences between a protocol with an association type and a normal protocol. This is also true for protocols that use the Self keyword in their protocols. In Swift 3, such protocols cannot be used as separate types. This limitation may be removed in the future when a full generic system is implemented, but until then, we will all have to deal with it.
The protocol with Self
We’ll still start with an example:
func binarySearch(_ keys: [Int], for key: Int) -> Int {
var lo = 0, hi = keys.count - 1
while lo <= hi {
let mid = lo + (hi - lo) / 2
if keys[mid] == key { return mid }
else if keys[mid] < key { lo = mid + 1 }
else { hi = mid - 1}}return -1
}
let position = binarySearch([Int] (1.10), for: 3)
// result: 2
Copy the code
The above code implements a simple binary lookup, but currently only supports finding data of type Int. If we wanted to support other types of data, we would have to modify the above code to use protocol. For example, I could add the following implementation:
protocol Ordered {
func precedes(other: Ordered) -> Bool
func equal(to other: Ordered) -> Bool
}
func binarySearch(_ keys: [Ordered], for key: Ordered) -> Int {
var lo = 0, hi = keys.count - 1
while lo <= hi {
let mid = lo + (hi - lo) / 2
if keys[mid].equal(to: key) { return mid }
else if keys[mid].precedes(other: key) { lo = mid + 1 }
else { hi = mid - 1}}return -1
}
Copy the code
To support finding data of Int type, we must make Int implement Oredered protocol:
Int and Oredered types cannot be compared with <, as can ==. To solve this problem, we can use Self in protocol:
protocol Ordered {
func precedes(other: Self) -> Bool
func equal(to other: Self) -> Bool
}
extension Int: Ordered {
func precedes(other: Int) -> Bool { return self < other }
func equal(to other: Int) -> Bool { return self == other }
}
Copy the code
After Self is used in Oredered, the compiler replaces Self with a concrete type in the implementation, just as Self is replaced with an Int in the code above. So we’ve solved the above problem. But a new problem arose:
As stated above, a protocol with Self cannot be used as a separate type. In this case, we can use generics to solve the problem:
func binarySearch<T: Ordered>(_ keys: [T], for key: T) -> Int{... }Copy the code
Binary lookup can also be used for String data:
extension String: Ordered {
func precedes(other: String) -> Bool { return self < other }
func equal(to other: String) -> Bool { return self == other }
}
let position = binarySearch(["a"."b"."c"."d"].for: "d")
// result: 3
Copy the code
Of course, if you are familiar with the protocols in the Swift standard library, you will find that the above implementation can be simplified to the following lines:
func binarySearch<T: Comparable>(_ keys: [T], for key: T) -> Int? {
var lo = 0, hi = keys.count - 1
while lo <= hi {
let mid = lo + (hi - lo) / 2
if keys[mid] == key { return mid }
else if keys[mid] < key { lo = mid + 1 }
else { hi = mid - 1}}return nil
}
Copy the code
Here we define the Ordered protocol only to demonstrate the process of using Self within the protocol. In practice, protocols provided in the standard library can be used flexibly. The Comparable protocol also uses Self in the standard library:
extension Comparable {
public static func > (lhs: Self, rhs: Self) -> Bool
}
Copy the code
The above demonstrates how to use the protocol with Self by implementing a binary lookup algorithm. Simply put, you can think of Self as a placeholder that can be replaced with the actual type later in the implementation of the concrete type.
A protocol with an association type
A protocol with an associated type cannot be used as a separate type. There are many such protocols in Swift, such as Collection, Sequence, IteratorProtocol, and so on. If you still want to use this protocol as a type, you can use a technique called type erasure. You can learn how to implement it here.
Again, here is an example of how to use a protocol with an association type in a project. This time we will implement a data loader with loading animation through the protocol and display the corresponding placeholder map in case of an error.
Here, we define a Loading protocol, which means that data can be loaded. However, to satisfy the Loading protocol, a loadingView must be provided, and the loadingView is an instance of the associated type in the protocol.
protocol Loading: class {
associatedtype LoadingView: UIView.LoadingViewType
var loadingView: LoadingView { get}}Copy the code
An association type in the Loading protocol has two requirements. First, it must be a subclass of UIView, and second, it must comply with the LoadingViewType protocol. LoadingViewType can be simply defined as follows:
protocol LoadingViewType: class {
var isAnimating: Bool { get set }
var isError: Bool { get set }
func startAnimating(a)
func stopAnimating(a)
}
Copy the code
In an extension of the Loading protocol, we can define some methods related to the Loading logic:
extension Loading where Self: UIViewController {
func startLoading(a) {
if! view.subviews.contains(loadingView) {
view.addSubview(loadingView)
loadingView.frame = view.bounds
}
view.bringSubview(toFront: loadingView)
loadingView.startAnimating()
}
func stopLoading(a) {
loadingView.stopAnimating()
}
}
Copy the code
We can continue to add logic to Loading with network data:
extension Loading where Self: UIViewController {
func loadData(with re: Resource, completion: @escaping (Result) -> Void) {
startLoading()
NetworkTool.shared.request(re) { result in
guard case .succeed = result else {
self.loadingView.isError = true // Display the error view, which can be displayed according to the error type
self.stopLoading()
return
}
completion(result)
self.loadingView.isError = false
self.stopLoading()
}
}
}
Copy the code
The above is the implementation of the Loading protocol. This is different from the previous example in that it mainly uses an extension of the protocol to implement the requirements. The reason for this is that all the loading logic is almost the same, with the possible difference being the animation being loaded. Therefore, the part responsible for animation is put in the LoadingViewType protocol, and the Loading logic is defined in the protocol extension. There are differences between the methods defined in the protocol declaration and those defined in the protocol extension, and an example will be given later to illustrate the differences.
To enable the ViewController to loadData, simply make the controller obey the Loading protocol, and then call the loadData method where appropriate:
class ViewController: UIViewController.Loading {
var loadingView = TestLoadingView(a)override func viewDidLoad(a) {
super.viewDidLoad()
loadData(with: Test.justEmpty) { print($0)}}}Copy the code
Here is the result:
As long as the controller complies with the Loading protocol, we can load data from the network with Loading animation, and display an error view if something goes wrong. There are certainly people here who would argue that you can do the same with inheritance. Of course, we can implement this requirement by putting all the load logic in the protocol into a base class. If you add refreshes and pagination later, that code will have to be placed in the base class, which will become more and more bloated as the project gets bigger, known as the God class. If we treat data loading, refreshing, and paging as separate protocols, and let the controller follow whatever it needs, then the controller won’t contain functionality it doesn’t need. This is like building blocks, giving the program the flexibility to add what it needs.
Protocol and function distribution
Function dispatch is the process by which a program selects instructions to execute when calling a method. This happens every time we call a method.
Compiled languages have three basic function dispatches: Direct Dispatch, Table Dispatch, and Message Dispatch. Most languages support one or two. Java uses function table distribution by default, and you can change it to direct distribution by using the final keyword. C++ default uses direct distribution, through the virtual keyword can be changed to function table distribution. Objective-c always uses message dispatch, but allows developers to use C direct dispatch for performance gains (such as direct calls to IMP). Swift is ahead of the curve, supporting all three delivery options. This approach is great, but it has caused a lot of headaches for Swift developers.
Here is a brief description of how function distribution behaves in Protocol. Look at the following example:
protocol Flyable {
func fly(a)
}
Copy the code
The Flyable agreement, defined above, represents the ability to fly. Complying with this protocol requires implementing the FLY () method. We can provide several implementations:
struct Eagle: Flyable {
func fly(a) { print("🦅 is flying")}}struct Plane: Flyable {
func fly(a) { print("✈ ️ is flying." ")}}Copy the code
Write a client program to test it:
let fls: [Flyable] = [Eagle(), Plane()]
for fl in fls {
fl.fly()
}
// result:
🦅 isFlying ✈ ️is flying
Copy the code
The above test results are exactly as we expected. The fly() method above is declared in the protocol definition, and now we declare it in the protocol extension, as follows:
extension Flyable {
func fly(a) { print("Something is flying")}}Copy the code
You can guess the result of the run before you run it.
Pause for 3 seconds…
Here is the result:
Something is flying
Something is flying
Copy the code
You see, we simply moved the method from the protocol definition to the protocol extension, and the results were completely different. Results like the above are also due to this line of code:
let fls: [Flyable] = [Eagle(), Plane()]
Copy the code
If you call directly with a concrete type, there is no problem, as follows:
Eagle().fly() / / 🦅 is flying
Plane().fly() / / ✈ ️ is flying
Copy the code
The above two completely different results are mainly because function distribution adopts different strategies according to the position of method declaration, which can be summarized as follows:
- Value types (struct, enum) are always distributed directly
- And protocols and classes
extension
Will use direct distribution - Methods in the protocol and normal Swift class declaration scope are distributed using the function table
- inheritance
NSObject
All methods in the class declaration scope are distributed using the function table - inheritance
NSObject
Of a classextension
, the use ofdynamic
The marked method uses message distribution
The diagram below clearly summarizes the way functions are distributed in Swift, but without the dynamic approach.
In the above example, although both Eagle and Plane implement the fly() method, the default implementation in the protocol extension is still called when polymorphic. Because the protocol extension declaration method, when called, uses direct distribution, direct distribution is always better than other distribution methods.
So understanding the distribution of functions in Swift is essential for writing well-structured, bug-free code. Of course, if you don’t use polymorphism and just use concrete types, you won’t have this problem. Since you’re starting to “program for interfaces, not implementations,” how can you not use polymorphism?
Use protocols to improve existing code design
As you can see from the above example, code sharing by protocol has several advantages over sharing by inheritance:
- We don’t need to be forced to use a parent class.
- We can make existing types satisfy the protocol (for example, we made CGContext satisfy the Renderer). Subclasses are less flexible, and if CGContext is a class, we cannot retroactively change its parent class.
- Protocols can be used with classes as well as structures and enumerations, while inheritance cannot be used with structures and enumerations.
- Protocols can simulate multiple inheritance.
- Finally, when dealing with protocols, we don’t have to worry about method overwriting or calling super at the right time.
With protocol-oriented programming, we can free ourselves from traditional inheritance and assemble programs in a more flexible way, like building blocks. Protocols, like classes, are designed to adhere to the “single responsibility” principle, allowing each protocol to focus on its own functionality. Thanks to protocol extensions, we can reduce the risk of shared state from inheritance and make the code cleaner.
Using protocol-oriented programming helps us write code that is low-coupling, easily extensible, and testable, while using protocols with generics frees us from the hassle of dynamic calls and type conversions and keeps our code safe.