6 Common Mistakes with Protocols in Swift and How to Avoid Them

6 Common Mistakes with Protocols in Swift and How to Avoid Them

Protocols, an integral part of Swift, hold a significant place in Swift development. They help us define a blueprint of methods, properties, and other requirements for particular tasks or functionalities.

As wonderful as protocols are, it's easy to make mistakes while using them, particularly when you are just starting with Swift. This post will outline some common mistakes developers make with protocols in Swift and how to avoid them. We will illustrate this with practical code examples.

1. Confusion Between Protocol and Concrete Types

One common mistake is confusing protocols with concrete types. Protocols define a blueprint, they cannot be instantiated directly. Let's see an example:

protocol Vehicle {
    var numberOfWheels: Int { get }
}

struct Car: Vehicle {
    var numberOfWheels: Int
}

let myVehicle: Vehicle = Vehicle()  // WRONG

In the above example, we're trying to create an instance of a protocol which isn't valid. Instead, we should instantiate a struct or class that conforms to that protocol:

let myVehicle: Vehicle = Car(numberOfWheels: 4)  // RIGHT

2. Not Implementing Required Properties or Methods

When a type promises to conform to a protocol, it must implement all the required properties and methods of that protocol. Forgetting to do so will lead to a compile-time error:

protocol Drawable {
    func draw()
}

struct Circle: Drawable {  // ERROR: Type 'Circle' does not conform to protocol 'Drawable'
}

The Circle struct says it conforms to Drawable, but doesn't implement the draw() method. The correct implementation would be:

struct Circle: Drawable {
    func draw() {
        print("Drawing a circle.")
    }
}

3. Misunderstanding of Protocol Inheritance

Protocols can inherit from other protocols, and this can sometimes lead to confusion. A common mistake is forgetting that a type conforming to a protocol that inherits from another protocol must satisfy the requirements of all protocols in its inheritance chain.

protocol Vehicle {
    var numberOfWheels: Int { get }
}

protocol MotorVehicle: Vehicle {
    var engineType: String { get }
}

struct Car: MotorVehicle {  // ERROR: Type 'Car' does not conform to protocol 'Vehicle'
    var engineType: String
}

In this example, Car conforms to MotorVehicle but does not provide the numberOfWheels property required by the Vehicle protocol. To fix this, we must provide all the required properties:

struct Car: MotorVehicle {
    var numberOfWheels: Int
    var engineType: String
}

4. Expecting Protocols to Store State

Swift protocols cannot store state. They can only declare properties, which must be implemented by the conforming types. This often confuses developers who come from class-based or object-oriented backgrounds.

protocol Drawable {
    var color: String { get set }  // Error: Property in protocol must have explicit { get } or { get set }
}

To avoid this, always remember that protocols are used to define a blueprint for methods and computed properties, not to store the state.

5. Misuse of Self in Protocols

When defining a protocol, Self is used to represent the current type. This can lead to problems if you try to use it in a way that doesn't make sense:

protocol Copyable {
    func copy() -> Self
}

struct Note: Copyable {
    var text: String

    func copy() -> Note {  // ERROR: Method 'copy()' in protocol 'Copyable' requires the return type to be 'Self'
        return Note(text: self.text)
    }
}

In the above example, we've defined a Copyable protocol with a copy() method that should return an instance of the conforming type. However, when we attempt to implement this in the Note struct, we specify Note as the return type instead of Self. This is incorrect because Self can refer to any type that implements the protocol (including any subclasses, in the case of classes), not just Note. The correct implementation would be:

struct Note: Copyable {
    var text: String

    func copy() -> Self {
        return Self(text: self.text)
    }
}

6. Ignoring Associated Types in Protocols

Another common mistake when using protocols is ignoring associated types. Protocols in Swift can contain associated type placeholders, which are replaced with a real type when the protocol is adopted. Here's an example of where this goes wrong:

protocol Container {
    associatedtype Item
    mutating func add(_ item: Item)
    var count: Int { get }
}

struct Stack: Container {  // ERROR: Type 'Stack' does not conform to protocol 'Container'
    var items = [Int]()

    mutating func add(_ item: Int) {
        items.append(item)
    }

    var count: Int {
        return items.count
    }
}

In the above example, Stack doesn't define what Item is, which leads to a compilation error. The correct way to implement the protocol would be:

struct Stack: Container {
    typealias Item = Int
    var items = [Item]()

    mutating func add(_ item: Item) {
        items.append(item)
    }

    var count: Int {
        return items.count
    }
}

By understanding these common mistakes and how to avoid them, you can start to harness the full power of protocols in your Swift applications. Remember, practice makes perfect. The more you use protocols, the more familiar you will become with their rules and nuances.

I hope you enjoyed this article, and if you have any questions, comments, or feedback, then feel free to comment here or reach out via Twitter.

Thanks for reading!