The beauty of Swift Language – protocol

👍
· Jan 26, 2024 · 13 mins read
The beauty of Swift Language – protocol

Swift promotes protocol oriented programming and the protocol in Swift is designed to provide rich functionalities to support this goal. Conceptually, it is like interface in Kotlin and C# but with many syntax and functionality differences. In this article, we are going to focus on the wonderful design of the protocol in Swift.

1. Define protocol requirement

As mentioned earlier, the protocol defines some requirements regarding properties and functions like below:

protocol SampleProtocol {  
    var sampleProperty: Int { get set } // Define a property requirement  
    func sampleFunction() // Define a method requirement  
} 

Property requirements

It always uses var to declare a property requirement. To further specify its readability and writability, use set /get to do so. Please note, the property should be always readable, which means we should always put get inside the {} and optionally add set depending on the needs.

It is possible to specify a static property requirement too. For this, we just need to add static before the var keyword.

Method requirements

To define a method requirement, just provide the method signature without method body like the sample above. Swift allows to specify various method requirements like mutating methods, static/class methods etc like below. In order to satisfy astatic method requirement, we can define either a static method or a class method.

protocol SampleProtocol {  
    mutating func sampleFunction() // Define a mutating method requirement  
    static func sampleFunction() // Define a static method requirement  
}

Similarly, we can also define Initializer Requirements like below:

protocol SampleProtocol {  
    init() // Define an Initializer Requirement  
    init?(a: Int) // Define a Failable Initializer Requirement  
}

Class-Only Protocols

If we want the protocol can only be applied to classes, which is common for delegate patterns, use AnyObject to mark it like below.

// AnyObject here marks the protocol can only be applied to classes  
protocol SampleProtocol: AnyObject { }

Optional requirements

If want to define some optional requirements, there are two ways to achieve this goal: using optional keyword and providing default implementation.

@objc protocol SampleProtocol {  
    @objc optional var property1: Int { get }  
    @objc optional func optionalFunc()  
}

The keyword optional can only be used with @objc attribute, which also marks protocol as class only protocol. The more swifty way of making requirement optional is to provide default implementation like below:

protocol SampleProtocol {  
    var property1: Int { get }  
    func optionalFunc()  
}  
  
extension SampleProtocol { // Providing default implementation  
    var property1: Int { 0 }  
    func optionalFunc() {}  
}  
  
// This is ok as all requirements in the SampleProtocol have default implementation  
struct SampleStruct: SampleProtocol {} 

Type requirements (Associate types)

Like generics, if want a protocol to be able to handle different types, use associated types. For example, we can define a Stack protocol and make IntStack class conform to it like below.

protocol Stack {  
    associatedtype Element  
    func push(e: Element)  
    func pop() -> Element?  
}  
  
class IntStack: Stack {  
    // Use typealias to specify the concrete type of this Element  
    typealias Element = Int  
    private var elements: [Int] = []  
  
    func push(e: Int) {  
        elements.insert(e, at: 0)  
    }  
  
    func pop() -> Int? {  
        elements.removeFirst()  
    }  
}  
  
let stack = IntStack()  
stack.push(e: 1)  
let e = stack.pop()

In order to conform to a protocol that with associated types, we use typealias in the conforming type to specify the concrete type of this associated type. Most of times, we can ignore this and leave the complier to infer the concrete type.

Since the Stack protocol only requires an associatedtype Element , like IntStack handles Int type Element, we can also create a type that support other types like below:

class StringStack: Stack {  
    private var elements: [String] = []  
  
    func push(e: String) {  
        elements.insert(e, at: 0)  
    }  
  
    func pop() -> String? {  
        elements.removeFirst()  
    }  
}

Or we can create a generic type to conform to this protocol like below:

class GenericStack<E: Any>: Stack {  
    private var elements: [E] = []  
  
    func push(e: E) {  
        elements.insert(e, at: 0)  
    }  
  
    func pop() -> E? {  
        elements.removeFirst()  
    }  
}

If needed, we can further specify requirement for the associated types like below, where we not only require the associated type Iterator to conform to IteratorProtocol , but its Element is as same as the associated Element type of this Sequence protocol.

protocol Sequence {  
  associatedtype Element  
  associatedtype Iterator: IteratorProtocol where Element == Iterator.Element  
}

Since Swift 5.7, it also allows us to specify primary associated types for protocols using a syntax like generic parameter like below:

protocol Stack<Element> {  
    associatedtype Element  
    func push(e: Element)  
    func pop() -> Element?  
}

With this new version of the definition of Stack protocol, we can use it the same way as the previous version. However, the new version enables the following new usage:

func isTopElementEqual(first: any Stack<Int>, second: any Stack<Int>) -> Bool {  
    first.pop() == second.pop()  
}  
  
// This is basically equal to the following method but more readable  
// func isTopElementEqual<S: Stack>(first: S, second: S) -> Bool where S.Element == Int {  
//    first.pop() == second.pop()  
//}

As mentioned in the proposal, primary associated types are intended to be used for associated types which are usually provided by the caller. It provides a more readable syntax for these use cases.

2. Object Orientated features of Protocols

To some extent, protocol is just an abstract type definitions that separates type interface from its implementation and this separation offers flexibility. Like other types in Swift, protocols also have object oriented features like inheritance, extension etc.

Protocol inheritance

A good example of Swift protocol inheritance is Error protocol. It inherits Sendable protocol. Similarly, we can also define our protocol inheritance as needed like below:

protocol Nameable: Identifiable {  
    var name: String { get }  
}  
  
protocol Animal: Nameable {  
    associatedtype Food  
    func eat(food: Food)  
}  
  
protocol Plant: Nameable {  
    associatedtype Food  
    func harvest() -> Food  
}

In this example, we have Nameable that inherit [Identifiable](https://developer.apple.com/documentation/swift/identifiable) protocol that requires a id property. The Nameable protocol define a name property requirement. Both Animal and Plant protocol inherit from Nameable , which mean, they both require id and name properties that defined in their parent protocols Nameable and Identifiable .

Protocol Extensions

Like type extensions, we can define protocol extensions. The declarations in the protocol extension are shared by all the types that conform to this protocol. For example:

protocol AProtocol {  
    var property: Int { get }  
    func sampleFunc()  
}  
  
extension AProtocol {  
    // Default implementation of property  
    var property: Int {  
        0  
    }  
    // Default implementation of function  
    func sampleFunc() {  
    }  
  
    // Define a new property(or a func) that is not part of protocol requirement  
    var isFromAProtocol: Bool {  
        true  
    }  
}

Conditional Extensions

If we want to limit the declarations in extension to part of types that conform to this protocol, we can use generic where clause to do so.

extension AProtocol where Self: UIViewController {  
    func alert(title: String? = nil, message: String) {  
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)  
        present(alert, animated: true)  
    }  
}

In the above example, we add alert function only to the types that conform to AProtocol and are subclasses of UIViewController . Since this func is only visible to subclasses of UIViewController , we can call present method of UIViewController in this method.

Protocol composition

In Swift, we can combine requirements of two or more protocols together using & . One example of this is Codable protocol, which is acutally a type alias of protocol composition of DeCodable and EnCodable . For example:

typealias Codable = Decodable & Encodable  
struct A: Codable {}  
// Or  
struct A: Encodable, Decodable {}  
// Or  
struct A: Encodable & Decodable {}  
  
func process(item: Codable) {}  
// Or  
func process(item: Encodable & Decodable) {}

3. Conforming to protocols

To make a type conform to a protocol, we need to implement all the requirements of the protocol for the type. It’s recommended to create a extension of that type and conform to the protocol in the extension like below:

protocol Tracing {  
    func log()  
}  
  
struct AStruct {  
    var property: Int  
}  
  
extension AStruct: Tracing {  
    func log() {  
        ...  
    }  
}

Conditionally Conforming to a Protocol

For generic types, we can make it conform to certain protocol under some conditions. For instance, we make Int Array to conform to Averageable protocol like below:

protocol Averageable {  
    var average: Float { get }  
}  
  
extension Array: Averageable where Element == Int {  
    var average: Float {  
        guard count > 0 else { return 0 }  
        return Float(reduce(0, +)) / Float(count)  
    }  
}

Declaring Protocol Adoption with an Extension

It’s a recommended style to conform a protocol using type extension because it helps group code into smaller pieces like below:

protocol Nameable {  
    var name: String { get }  
}  
  
struct Person {  
    let firstName: String  
    let lastName: String  
}  
  
extension Person: Nameable {  
    var name: String {  
        "\(firstName) \(lastName)"  
    }  
}

When all of the requirements of the protocol have their default implementations, or the type has met all the requirements, we can simply declare the protocol conformance by an empty extension.

protocol Titled {  
    var title: String? { get }  
}  
  
class HomeViewController: UIViewController {}  
  
extension HomeViewController: Titled {}

Since there is a title of String? type in type UIViewController , we can just simply declare this Titled protocol conformance with an empty extension.

Using Synthesized Implementations

In particular, Swift compiler can generate conformance code to some types for protocols like Equatable, Hashable, Comparable Encodable and DeCodable etc. To use this feature, we can just simply declare protocol conformance with an empty extension like below.

enum Status {  
    case on  
    case off  
}  
  
extension Status: Equatable, Codable, Comparable, Hashable {}

Checking for Protocol Conformance

In runtime, if we want to check if a type conform to a protocol or not, use type casting syntax like below:

let a = Person(firstName: "T", lastName: "W")  
  
if a is Nameable {  
    print("A Nameable indeed")  
}  
  
if let nameable = a as? Nameable {  
    print(nameable.name)  
}

4. Using protocols as types

When it comes to protocols used as types, there are three major use cases.

As generic constraints

This is the most common way to use a protocol as a type. In this case, the concrete type is made clear on caller side. In the example below, even though in the method body of printNames , we are not sure about the what concrete types it handles, but on the caller side of this method, we call this method with concrete types.

protocol Thing: Identifiable {  
    var name: String { get }  
}  
  
extension Array where Element: Thing {  
    func printNames() {  
        forEach {  
            print($0.name)  
        }  
    }  
}  
  
struct Object {  
    let name: String  
    let id: UUID = UUID()  
}  
  
extension Object: Thing {}  
  
let objects: [Object] = [.init(name: "Laptop"), .init(name: "Camera")]  
objects.printNames()

Existential type — any

This is also called a boxed protocol type. In this case, protocols abstract away concrete type information, which, in many cases, has performance impact because the compiler needs to handle memory properly so that any concrete types that conform to this protocol can be handled correctly.

In order to explicitly express this existential usage of protocols, Swift introduces [any](https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md) keyword. For example, in the code below, things array need to be able to save values of any concrete types that conform to Thing protocol.

struct Values {  
    var things: [any Thing]  
  
    func printNames() {  
        things.forEach { print($0.name) }  
    }  
}

Opaque types — some

Swift offers Opaque types syntax some to hide information about concrete types. It can be used in Parameter Declarations and result types. When used in Parameter Declarations , it is a syntax sugar for generic constraints. For example:

func printNames(thing1: some Thing, thing2: some Thing) {  
    print("\(thing1.name) and \(thing2.name)")  
}  
  
// is the shorter form for the following method  
  
func printNames<T: Thing>(thing1: T, thing2: T) {  
    print("\(thing1.name) and \(thing2.name)")  
}

When it’s used in result types, it’s also called Reverse generics because unlike generics where the caller decides the concrete type of the generic type, in opaque return types, the callee decides the concrete type but return an opaque generic type for the purpose of hiding the information of the concrete types.

protocol Thing: Identifiable {  
    var name: String { get }  
}  
  
struct Object {  
    let name: String  
    let id: UUID = UUID()  
}  
  
extension Object: Thing {}  
  
func create() -> some Thing {  
    Object(name: "Laptop")  
}

In the above example, function create returns an instance of Object type as Thing type to hide the concrete type of Object .

We can explore the significant differences between any and some in the following example:

struct Object {  
    let name: String  
    let id: UUID = UUID()  
}  
  
struct Object2 {  
    let name: String  
    let id: UUID = UUID()  
}  
  
extension Object: Thing {}  
extension Object2: Thing {}  
  
struct Values {  
    // Works because we know the concrete type, the Opaque type only hides it  
    var things: [some Thing] = [Object(name: "Laptop")]  
  
    // Error because we do not know the concrete type  
    // var things: [some Thing]  
  
    // Error because Opaque type cannot have mixed concrete types  
    // var things: [some Thing] = [Object(name: "Laptop"), Object2(name: "Camera")]  
  
    func printNames() {  
        things.forEach { print($0.name) }  
    }  
}

any creates a boxed type that can dynamically handle different concrete types that conform to the protocol while some hides the information about concrete types, it can only bind to one concrete type.

Protocol in Swift is carefully designed and offers rich functionalities. We has covered the most important aspects of this beautiful Swift feature in this article. Hope it helps you write better code with fully understanding of this protocol oriented language.

Comments

Sharing is caring!