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.
Sharing is caring!