Understanding Protocol in Swift.

MYH
6 min readFeb 15, 2023

--

It’s been so long since my last post. Kinda feel sorry to the people who follow me on medium. lol. I’ve improved so much since my last post. In hindsight, a lot of my code and concepts that I wrote back then look very rubbish haha. Anyway, now that I feel like I know so much more than before, I decide to share something that I learnt along the way. And hopefully it can be helpful to you.

In this article, I’m gonna talk about how to use protocol in Swift. Similar concepts might be adaptable to other OOP programming languages.

1. Conforming to protocol

Start with a basic one. Any object conforming to the protocol needs to implement its properties/methods.

protocol Example {
func A()
}

struct AObject: Example {
// Compiler will ask you to implement Exmample protocol's function
// Type 'AObject' does not conform to protcol 'Example'
func A() {}
}

2. Protocol is a blueprint!!!

Pretty self-explanatory. Protocol is an “abstraction”. It shows what object should look like. But the real implementation depends on the object itself.
Protocol also makes your system more flexible.

class HTTPService {
func sendRequest() {
// do API request
}
}

class ViewController {
let service: HTTPService
init(service: HTTPService) {
self.service = service
service.sendRequest()
}
}

Instead of injecting a class(struct, enum) dependency in an instance, passing a protocol to represent the object would make your system not that rigid. This is the basic step of dependency inversion to make your system achieve low coupling, high cohesion.

Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules; both should depend on abstractions.

protocol HTTPService {
func sendRequest()
}

class ProductionAPIService: HTTPService {
func sendRequest() {
// do API request
}
}

class StubTestingService: HTTPService {
func sendRequest() {
// For testing
}
}

class ViewController {
let service: HTTPService
init(service: HTTPService) {
self.service = service
service.sendRequest()
}
}

Now that we make it into a protocol, we have easy control over what get pass into ViewController. In this example, I can switch two instances easily, one is for production, the other one is for testing purpose.

3. Protocol is an interface!!!!

This is probably the most important concept, in my opinion. A lot of beginners are familiar with the previous concepts but don’t know what should be written in the protocol.

Imagine being a driver. The interface should be the break, the gas pedal, and the steering wheel (I only know how to drive automatic so don’t ask me about lever). This interface is the protocol. What happened under the hood (quite literal) is not important to the driver. The driver should only be exposed to the interface and nothing else.

protocol AutomaticCar {
func stepOnGas()
func hitTheBreak()
func steerTheWheel()
}

class Driver {
let car: AutomaticCar
init(car: AutomaticCar) {
self.car = car
}

func goDriving() {
// The driver should only be exposed to the interface and nothing else.
car.stepOnGas()
car.steerTheWheel()
car.hitTheBreak()
}
}

With that being said, when designing your system, designing your protocol, you should only put what matters to the “driver” (the client that is going to conform to the protocol.) For example, let’s say you want to load some data to your ViewController(VC). The VC (client) shouldn’t have the knowledge of other operations besides load. Thus, the Loader protocol should only expose load() function.

protocol Loader {
func load()
}

class RemoteSource: Loader {
func load() {
// Do remotely load
}
private func doOtherOperation() {}
}

class LocalSource: Loader {
func load() {
// Do locally load
}
private func doSomethingElse() {}
}

class ViewController: UIViewController {
let service: Loader
init(service: Loader) {
self.service = service
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
service.load()
}
}

4. Default for protocol methods.

This happened to me a lot at work. Our company’s legacy codebase has a lot of custom views which use delegation. It looks something like this.

protocol CustomViewDelegate {
func doA()
func doB()
func doC()
func doD()
}
// A customView with delegation that can be used to notify or pass data
class CustomView: UIView {

var delegate: CustomViewDelegate?
}
// VieweController has to conform to CustomViewDelegate becuase
// when we write "customView.delegate = self"
// we appoint the delegation to this ViewController
class ViewController: UIViewController, CustomViewDelegate {
let customView = CustomView()

init(){
super.init(nibName: nil, bundle: nil)
customView.delegate = self
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// And hence, we are forced to implement all these methods
func doA() {
<#code#>
}
func doB() {
<#code#>
}
func doC() {
<#code#>
}
func doD() {
<#code#>
}
}

In most of the cases, we won’t be needing all theses delegation. A great example is UITableViewDelegate. Conforming to UITableViewDelegate would give you the control such as:
func tableView(UITableView, didSelectRowAt: IndexPath)
func tableView(UITableView, willDisplay: UITableViewCell, forRowAt: IndexPath)

However, these are all optional. The compiler would not force you to implement these methods. To achieve this “optional protocol methods,” we can write extension for the protocol.

extension CustomViewDelegate {
func doA() {}
func doB() {}
}
// Now that doA & doB have a defualt implemntation in the extension, we no longer need to implement these two. (You still can if needed.)

5. Protocol should be concise.

When you have more than two methods(or properties) in your protocol. It’s a smell. You might be violating Liskov Substitution principle & Interface Segregation principle. Let’s look at an example.

protocol Developer {
func writeCode()
func beHappy()
}

struct FrontEndDev: Developer {
func writeCode() {}

func beHappy() {}
}

struct BackEndDev: Developer {
func writeCode() {}

func beHappy() {
fatalError("I'm not happy")
}
}

In this example, both FrontEndDev and BackEndDev conform to Developer principle. However, BackEndDev doesn’t have the ability to call function beHappy(). This violated the Interface Segregation principle.

Interface Segregation principle: No code should be forced to depend on methods it doesn’t use.

class Company {
let devs: Developer
init(devs: Developer) {
self.devs = devs
}
}

let companyA = Company(devs: BackEndDev())
companyA.devs.beHappy() // Leads to crash. The BackEndDev can't beHappy()

When we create a Company which requires a Developer. The company expects its developer can call beHappy() method. But in our case, the above code would lead to crash. This violated the Liskov Substitution principle.

Liskov Substitution principle: objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

With all that being said, make sure the method or property is necessary.

6. Protocol can conform to another protocol

protocol BaseProtocol {
func cancel()
}

protocol SpecialTask: BaseProtocol {
func stopOperation()
func pauseOperation()
func startOperation()
}

The above code shows that SpecialTask conforms to BaseProtocol. This means that whoever conforms to SpecialTask protocol must also implements the BaseProtocol.

struct SpecialForce: SpecialTask {
func stopOperation() {
<#code#>
}
func pauseOperation() {
<#code#>
}
func startOperation() {
<#code#>
}
func cancel() {
<#code#>
}
}

However, I do not recommend it. Like I mentioned in the previous point, a protocol with too much restrictions and settings might violate principles and lead to messy codebase and fragile system.

I’ve seen some implementations like this for API request where the requests should be cancellable. (In that case, instead of conforming your protocol to another protocol, I would recommend using Combine framework which gives you cancellable for free.)

7. Generic Protocol

As a developer, we always thrive to make our code reusable. Protocol is no exception. We can use associatedType to make protocol generic.

// The Loader protocol doesn't know about the type that load function is going to complete with.
protocol Loader {
associatedtype Item
func load(completion: @escaping (Item) -> Void)
}
// Specify the type in implementation
class Service: Loader {
typealias Item = String
func load(completion: @escaping (Item) -> Void) {
}
}
// Or, you can just specify the type in the implemented function
class ServiceA: Loader {
func load(completion: @escaping (String) -> Void) {
}
}

And that’s probably a wrap. Please let me know if I miss any important note. And if you like this kind of article where I demonstrate different usages of a certain concept or object, please let me know as well.

Hope all of y’all have a great day. Onward and Upward. Let’s go. 🚀🦾

--

--