Sitemap
Press enter or click to view image in full size

Swift is a powerful and modern programming language that has gained a lot of popularity in recent years. One of the key features of Swift is its support for protocols, which allow developers to define a set of rules that types must adopt in order to work together.

In this article, we’ll provide an introduction to protocols in Swift, starting with the basics of what they are and how they work. We’ll explore the key features of protocols, including properties, methods, and associated types, and show how they can be used to write generic code that works with different types.

We’ll also discuss some real-world use cases for protocols, such as defining interfaces for network requests, implementing delegate patterns, and creating reusable UI components. By the end of the article, you should have a solid understanding of protocols in Swift and be ready to start using them in your own projects.

Whether you’re a new developer just starting out with Swift or an experienced developer looking to improve your understanding of this powerful language feature, this article will provide a comprehensive introduction to protocols in Swift.

What is protocol?

In Swift, a protocol is a set of rules that defines a blueprint of methods, properties, and other requirements that a class, struct, or enum can adopt. It’s like a contract that specifies what a conforming type must implement in order to conform to the protocol.

Protocol Syntax and Property Requirements

You define protocols in a very similar way to classes, structures, and enumerations:

protocol SomeProtocol {
// protocol definition goes here
var mustBeSettable: Int { get set }
}

One of the key features of protocols is the ability to define property requirements. This means that a protocol can require that conforming types implement certain properties with specific names and types.

A protocol can make a type have a specific instance or type property with a certain name and type. The protocol doesn’t say whether the property should be stored or computed, but it does say if the property should be readable, writable or both.

If the protocol requires the property to be both readable and writable, it can’t be a constant stored property or a read-only computed property. If the protocol only requires the property to be readable, it can be any type of property and can be writable as well.

Property requirements in protocols are always declared as variable properties with the “var” keyword. If a property is both readable and writable, it’s written as “{ get set }” after the type declaration, and if it’s only readable, it’s written as “{ get }”.

protocol MyProtocol {
var myProperty: Int { get set } // Property requirement
}

Let’s take a real-world example to help illustrate this concept. Imagine you are building an e-commerce app that allows users to buy products online. You need to create a protocol called Product that defines the necessary properties for a product.

protocol Product {
var name: String { get }
var price: Double { get }
var description: String { get }
}

The Product protocol defines three property requirements: name, price, and description. These properties are all read-only, meaning they can only be get, but not set. This is indicated by the { get } keyword after each property name.

Now, let’s create a conforming type that implements the Product protocol:

struct Book: Product {
let name: String
let price: Double
let description: String
let author: String
}

Here, the Book struct conforms to the Product protocol by implementing its required properties. In addition to the required properties, the Book struct has an additional property author.

Now, in your e-commerce app, you can create a function that takes an argument of type Product, and call it with any type that conforms to the protocol:

func buyProduct(_ product: Product) {
print("Buying \(product.name) for \(product.price)")
}

let book = Book(name: "The Catcher in the Rye", price: 9.99,
description: "A novel by J.D. Salinger",
author: "J.D. Salinger")
buyProduct(book)

The buyProduct function takes an argument of type Product, which means it can accept any type that conforms to the protocol. When we call this function with the Book struct, which conforms to Product, it works as expected.

Property requirements in Swift protocols allow you to define a set of rules that types must adopt in order to work together. They provide a way to ensure that conforming types have certain properties with specific names and types, which helps to make your code more flexible and reusable.

Protocol methods

Protocol can require that conforming types implement specific instance methods and type methods. These methods are defined as part of the protocol, just like normal instance and type methods, but without curly braces or a method body.

You can use variadic parameters in protocol method requirements, but you can’t specify default values for method parameters within the protocol’s definition.

Essentially, protocol method requirements allow you to define a set of rules that conforming types must follow to ensure that they have certain methods with specific parameters and return types. This helps to make your code more flexible and reusable.

protocol MyProtocol {
func myMethod(arg1: Int, arg2: String) -> Double
func anotherMethod() // Method requirement with no arguments
func yetAnotherMethod(args: Int...) // Method requirement with variadic arguments
}

Mutating method Requirements

Protocol can define methods that modify the properties of an instance. These methods are called mutating methods, and they are marked with the mutating keyword in the protocol definition. Mutating methods can only be called on instances that are declared as var instead of let. By using mutating methods, we can define protocols that allow for more complex behaviors and interactions between different types of instances.

Protocol that defines a mutating method as fallows:

protocol Vehicle {
var speed: Double { get set }
mutating func accelerate(by speedIncrease: Double)
}

The Vehicle protocol defines a property requirement for speed and a method requirement for accelerate. The accelerate method is marked as mutating, which means that it can modify the properties of the instance that adopts the protocol.

Let’s say we have a Car struct that adopts the Vehicle protocol:

struct Car: Vehicle {
var speed: Double = 0

mutating func accelerate(by speedIncrease: Double) {
speed += speedIncrease
}
}

The Car struct adopts the Vehicle protocol and implements the speed and accelerate requirements. The accelerate method is marked as mutating, because it modifies the speed property of the instance.

Now, we can create an instance of Car and call the accelerate method to increase its speed:

var myCar = Car()
myCar.accelerate(by: 10.0)
print(myCar.speed) // Output: 10.0

We create a new instance of Car, and then call the accelerate method to increase its speed by 10.0. We can see that the speed property has been updated to 10.0 after the method call.

Initializer Requirements

Protocol can define initializer requirements, which are specific rules for how a conforming type must be initialized. This allows a protocol to define the required steps for initializing an instance, without specifying how those steps are implemented. By using initializer requirements, we can ensure that conforming types are initialized in a consistent way, even if the implementation details vary.

To define an initializer requirement in a protocol, we use the init keyword, followed by the parameters and return type for the initializer, just like we would for a regular initializer method.

protocol Food {
init(name: String, calories: Int)
var name: String { get }
var calories: Int { get }
}

The Food protocol defines an initializer requirement that takes two parameters, name and calories. The protocol also defines property requirements for name and calories.

Get Lingeswaran Kandasamy’s stories in your inbox

Join Medium for free to get updates from this writer.

A conforming type that adopts the Food protocol must implement the required initializer, as well as the property requirements. Let’s see an example of a struct that conforms to the Food protocol:

struct Fruit: Food {
var name: String
var calories: Int

init(name: String, calories: Int) {
self.name = name
self.calories = calories
}
}

The Fruit struct adopts the Food protocol and implements the name, calories, and init requirements. The implementation of the init requirement initializes the name and calories properties of the instance.

Now, we can create an instance of Fruit and pass in the required parameters to the init method:

let apple = Fruit(name: "Apple", calories: 95)

We create a new instance of Fruit and pass in the required name and calories parameters to the initializer. The name and calories properties of the apple instance are initialized with the values we passed in.

Class Implementations of Protocol Initializer Requirements

In Swift, a class that adopts a protocol with initializer requirements has some additional rules to follow compared to a struct or enum. This is because classes have more complex initialization requirements and need to ensure that their designated initializer properly initializes all of their stored properties.

When a class adopts a protocol with an initializer requirement, it must implement the required initializer(s) as a designated initializer. This means that the initializer must call a designated initializer of the class before it can initialize any of its own properties or call any instance methods.

Let’s see an example of a protocol with an initializer requirement, and a class that adopts the protocol:

protocol Vehicle {
init(name: String, wheels: Int)
var name: String { get }
var wheels: Int { get }
}

class Car: Vehicle {
var name: String
var wheels: Int

required init(name: String, wheels: Int) {
self.name = name
self.wheels = wheels
}

init(make: String, model: String) {
self.name = make + " " + model
self.wheels = 4
}
}

The Vehicle protocol defines an initializer requirement that takes two parameters, name and wheels. The Car class adopts the Vehicle protocol and provides two initializers: a required initializer that implements the initializer requirement, and a convenience initializer that takes a make and model parameter.

The required keyword before the init method means that any subclass of Car must also implement this initializer. This ensures that any subclass will properly initialize the properties required by the Vehicle protocol.

The implementation of the init method for the Car class initializes the name and wheels properties of the instance. The required initializer meets the requirement of the Vehicle protocol, while the convenience initializer provides a different way to create a new Car instance.

Now, we can create an instance of Car and pass in the required parameters to the required initializer.

let sedan = Car(name: "Sedan", wheels: 4)

We create a new instance of Car and pass in the required name and wheels parameters to the required initializer. The name and wheels properties of the sedan instance are initialized with the values we passed in.

Failable Initializer Requirements

A failable initializer is an initializer that can return nil if it fails to initialize an instance of a class or struct. Protocols can require failable initializers to be implemented by conforming types. This can be useful when defining protocols for classes or structs that have initialization requirements that may not always be possible to meet.

To define a failable initializer requirement in a protocol, you can use the init? syntax, which indicates that the initializer can return nil if it fails to initialize an instance.

Below, the User protocol requires a failable initializer that takes two parameters, username and password. The initializer returns an optional instance of the conforming type, allowing it to return nil if it fails to initialize the required properties.

protocol User {
init?(username: String, password: String)
var username: String { get }
var password: String { get }
}

Here’s an implementation of the User protocol by a struct that represents a user:

struct BasicUser: User {
var username: String
var password: String

init?(username: String, password: String) {
guard !username.isEmpty && !password.isEmpty else {
return nil
}

self.username = username
self.password = password
}
}

The BasicUser struct adopts the User protocol and provides a failable initializer that checks if the username and password parameters are empty, and returns nil if either of them is empty. If both parameters have values, the BasicUser instance is initialized with the provided values.

Now, we can create an instance of BasicUser and pass in valid values for username and password:

let user = BasicUser(username: "johndoe", password: "password")

We attempt to create a new instance of BasicUser with an empty value for username. The initializer returns nil, indicating that it failed to initialize the instance.

Failable initializer requirements in protocols can be used to ensure that conforming types meet certain initialization requirements, and provide a way to handle initialization failures when creating new instances of those types.

Protocols as Types

Protocols can be used as types, just like classes, structs, or enums. This means that a variable, parameter, or return value can be declared as a protocol type, allowing any conforming type to be used in that position.

Imagine you’re building a game that involves various vehicles, such as cars, trucks, and airplanes. Each vehicle has certain properties and behaviors that are important for the game. You could create a protocol called Vehicle that defines these properties and behaviors, like this:

protocol Vehicle {
var name: String { get }
var speed: Double { get set }
func move()
func stop()
}

Now, you can create separate structs or classes for each type of vehicle, and have them conform to the Vehicle protocol. For example:

struct Car: Vehicle {
var name: String
var speed: Double

func move() {
// code to move the car forward
}

func stop() {
// code to stop the car
}
}

class Truck: Vehicle {
var name: String
var speed: Double

init(name: String, speed: Double) {
self.name = name
self.speed = speed
}

func move() {
// code to move the truck forward
}

func stop() {
// code to stop the truck
}
}

class Airplane: Vehicle {
var name: String
var speed: Double

func move() {
// code to make the airplane take off and fly
}

func stop() {
// code to land the airplane and stop its engines
}
}

Now, you can create a variable or parameter of type Vehicle, and pass in any conforming type, like this:

func race(vehicle1: Vehicle, vehicle2: Vehicle) {
// code to make the vehicles race
}

let myCar = Car(name: "My Car", speed: 60.0)
let myTruck = Truck(name: "My Truck", speed: 40.0)

race(vehicle1: myCar, vehicle2: myTruck)

This allows for greater flexibility and reusability in your code, as you can easily add new types of vehicles without needing to modify existing code.

Associated Types

Associated types are a way to define a placeholder type that will be filled in by a conforming type. This allows a protocol to be flexible enough to work with a variety of types, while still enforcing type safety.

Imagine you’re building an app that involves displaying a list of items, such as books, movies, and songs. Each item has different properties, but they all share the same basic functionality of being able to be displayed and selected. You could create a protocol called Displayable that defines this functionality, like this:

protocol Displayable {
associatedtype ItemType
func display(item: ItemType)
func didSelect(item: ItemType)
}

Notice the associatedtype ItemType. This defines a placeholder type that will be filled in by the conforming type, allowing the protocol to work with any type of item.

Now, you can create separate classes or structs for each type of item, and have them conform to the Displayable protocol. For example:

class Book: Displayable {
typealias ItemType = Book
var title: String
var author: String

init(title: String, author: String) {
self.title = title
self.author = author
}

func display(item: Book) {
// code to display a book in the UI
}

func didSelect(item: Book) {
// code to handle when a book is selected
}
}

class Movie: Displayable {
typealias ItemType = Movie
var title: String
var director: String

init(title: String, director: String) {
self.title = title
self.director = director
}

func display(item: Movie) {
// code to display a movie in the UI
}

func didSelect(item: Movie) {
// code to handle when a movie is selected
}
}

class Song: Displayable {
typealias ItemType = Song
var title: String
var artist: String

init(title: String, artist: String) {
self.title = title
self.artist = artist
}

func display(item: Song) {
// code to display a song in the UI
}

func didSelect(item: Song) {
// code to handle when a song is selected
}
}

Now, you can create a variable or parameter of type Displayable, and pass in any conforming type, like this:

let myBook = Book(title: "The Great Gatsby", author: "F. Scott Fitzgerald")
let myMovie = Movie(title: "The Godfather", director: "Francis Ford Coppola")

func displayList(list: [Displayable]) {
for item in list {
item.display(item: item)
}
}

displayList(list: [myBook, myMovie])

This allows for greater flexibility and reusability in your code, as you can easily add new types of items without needing to modify existing code. The associatedtype allows the protocol to work with any type, while still enforcing type safety.

--

--