Skip to main content

Swift Protocol Types

Introduction

In Swift, protocols define a blueprint of methods, properties, and other requirements that suit a particular task or functionality. However, not all protocols are created equal! Swift offers several variations of protocol types that serve different purposes and have unique constraints. Understanding these different protocol types is crucial for designing flexible, maintainable, and efficient Swift code.

In this guide, we'll explore the various protocol types available in Swift:

  • Regular protocols
  • Class-only protocols
  • Protocol composition
  • Protocol inheritance
  • Protocol extensions

Let's dive into each type and understand when and how to use them.

Regular Protocols

Regular protocols are the most common type you'll work with. They can be adopted by any type, including classes, structs, and enums.

Basic Structure

swift
protocol Describable {
var description: String { get }
func describe() -> String
}

// A struct conforming to the protocol
struct Person: Describable {
var name: String
var age: Int

var description: String {
return "Person named \(name), age \(age)"
}

func describe() -> String {
return description
}
}

// Using the protocol
let john = Person(name: "John", age: 30)
print(john.describe()) // Output: Person named John, age 30

Regular protocols are versatile and form the backbone of Swift's protocol-oriented programming paradigm. They enable you to define contracts that different types can fulfill, regardless of their underlying implementation.

Class-only Protocols

Sometimes, you might want to restrict a protocol so that only classes (and not structs or enums) can adopt it. This is useful when your protocol requires reference semantics or when it deals with functionality only relevant to classes.

Declaration and Usage

To create a class-only protocol, use the AnyObject keyword:

swift
protocol ClassOnlyProtocol: AnyObject {
var mutableProperty: Int { get set }
func someMethod()
}

// This works fine because UIViewController is a class
class MyViewController: UIViewController, ClassOnlyProtocol {
var mutableProperty: Int = 0

func someMethod() {
print("Method implemented in a class")
}
}

// This would cause a compiler error because structs can't adopt class-only protocols
// struct MyStruct: ClassOnlyProtocol { ... }

When to Use Class-only Protocols

  1. Weak References: When your protocol needs to use weak references to prevent retain cycles
  2. Class Inheritance: When the protocol is designed to work with class inheritance
  3. Objective-C Interoperability: When working with Objective-C code that expects classes
swift
protocol DataSourceDelegate: AnyObject {
func dataUpdated()
}

class DataManager {
// Using weak reference requires a class-only protocol
weak var delegate: DataSourceDelegate?

func updateData() {
// Update some data
delegate?.dataUpdated()
}
}

Protocol Composition

Swift allows you to combine multiple protocols into a single requirement using protocol composition. This is powerful when you need a type that conforms to several protocols simultaneously.

Syntax and Examples

Protocol composition uses the & operator:

swift
protocol Named {
var name: String { get }
}

protocol Aged {
var age: Int { get }
}

// Function that requires a type conforming to both protocols
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name)! You're now \(celebrator.age)!")
}

struct Employee: Named, Aged {
var name: String
var age: Int
}

let robert = Employee(name: "Robert", age: 35)
wishHappyBirthday(to: robert) // Output: Happy birthday, Robert! You're now 35!

Using Type Aliases for Composition

For complex compositions, you can use a type alias to make your code cleaner:

swift
protocol Payable {
func calculatePay() -> Double
}

protocol TimeTracking {
func recordHours(hours: Double)
}

typealias EmployeeRequirements = Payable & TimeTracking & Named & Aged

// Now we can use this composite type
func processStaff(employee: EmployeeRequirements) {
// Process an employee that conforms to all four protocols
}

Protocol Inheritance

Protocols can inherit from other protocols, requiring conforming types to fulfill the requirements of multiple protocols.

Basic Protocol Inheritance

swift
protocol Vehicle {
var numberOfWheels: Int { get }
func startEngine()
}

protocol ElectricVehicle: Vehicle {
var batteryLevel: Int { get }
func charge()
}

// A type conforming to ElectricVehicle must implement all requirements
// from both ElectricVehicle and Vehicle
struct Tesla: ElectricVehicle {
var numberOfWheels: Int = 4
var batteryLevel: Int = 80

func startEngine() {
print("Silent electric motor starting...")
}

func charge() {
print("Charging battery...")
}
}

let myTesla = Tesla()
myTesla.startEngine() // Output: Silent electric motor starting...
print("Wheels: \(myTesla.numberOfWheels), Battery: \(myTesla.batteryLevel)%")
// Output: Wheels: 4, Battery: 80%

Multiple Inheritance

Protocols can inherit from multiple other protocols:

swift
protocol Chargeable {
func charge()
}

protocol Drivable {
func drive()
}

protocol ElectricCar: Chargeable, Drivable {
var maxRange: Double { get }
}

struct NissanLeaf: ElectricCar {
var maxRange: Double = 240 // km

func charge() {
print("Charging Nissan Leaf...")
}

func drive() {
print("Driving silently...")
}
}

Protocol Extensions

Protocol extensions allow you to provide default implementations for methods and properties in your protocols. This powerful feature enables code reuse and is a cornerstone of protocol-oriented programming.

Adding Default Implementations

swift
protocol Identifiable {
var id: String { get }
func identify()
}

// Provide a default implementation for the identify() method
extension Identifiable {
func identify() {
print("My ID is \(id)")
}
}

// Types can use the default implementation
struct User: Identifiable {
var id: String

// No need to implement identify() as it will use the default
}

let user = User(id: "123ABC")
user.identify() // Output: My ID is 123ABC

// Or they can provide their own implementation
struct SpecialUser: Identifiable {
var id: String

func identify() {
print("Special user with ID: \(id)")
}
}

let specialUser = SpecialUser(id: "XYZ789")
specialUser.identify() // Output: Special user with ID: XYZ789

Conditional Conformance with Extensions

You can make types conform to protocols conditionally:

swift
// Make arrays of Equatable elements themselves Equatable
extension Array: Equatable where Element: Equatable {
// Swift standard library already implements this
}

// Now we can compare arrays of equatable items
let array1 = [1, 2, 3]
let array2 = [1, 2, 3]
let array3 = [3, 2, 1]

print(array1 == array2) // Output: true
print(array1 == array3) // Output: false

Real-world Example: Building a Media Player Framework

Let's see how different protocol types can work together in a practical example - a media player framework:

swift
// Base protocol for all media items
protocol MediaItem {
var title: String { get }
var duration: Double { get } // in seconds
}

// Class-only protocol for playable items that need state
protocol Playable: AnyObject, MediaItem {
var isPlaying: Bool { get }
func play()
func pause()
}

// Protocol for items that can be rated
protocol Rateable {
var rating: Int { get set } // 1-5 stars
func rate(stars: Int)
}

// Protocol for shareable content
protocol Shareable {
func share(to platform: String)
}

// Protocol extension to provide default implementation
extension Playable {
func togglePlayback() {
if isPlaying {
pause()
} else {
play()
}
}
}

// A music track conforming to multiple protocols
class MusicTrack: Playable, Rateable {
var title: String
var artist: String
var duration: Double
var isPlaying: Bool = false
var rating: Int = 0

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

func play() {
print("Playing '\(title)' by \(artist)...")
isPlaying = true
}

func pause() {
print("Paused '\(title)'")
isPlaying = false
}

func rate(stars: Int) {
let validRating = min(max(stars, 1), 5)
rating = validRating
print("Rated '\(title)' \(validRating)/5 stars")
}
}

// A video that conforms to all three protocols
class Video: Playable, Rateable, Shareable {
var title: String
var director: String
var duration: Double
var isPlaying: Bool = false
var rating: Int = 0

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

func play() {
print("Playing video: \(title)...")
isPlaying = true
}

func pause() {
print("Paused video: \(title)")
isPlaying = false
}

func rate(stars: Int) {
let validRating = min(max(stars, 1), 5)
rating = validRating
print("Rated '\(title)' \(validRating)/5 stars")
}

func share(to platform: String) {
print("Sharing '\(title)' to \(platform)")
}
}

// Function that accepts any Playable & Rateable item
func playAndRate(media: Playable & Rateable, rating: Int) {
media.play()
media.rate(stars: rating)
}

// Create and use our media items
let song = MusicTrack(title: "Imagine", artist: "John Lennon", duration: 183)
let movie = Video(title: "The Matrix", director: "Wachowski Sisters", duration: 8160)

playAndRate(media: song, rating: 5)
// Output:
// Playing 'Imagine' by John Lennon...
// Rated 'Imagine' 5/5 stars

playAndRate(media: movie, rating: 4)
// Output:
// Playing video: The Matrix...
// Rated 'The Matrix' 4/5 stars

// We can only share the video
movie.share(to: "YouTube")
// Output: Sharing 'The Matrix' to YouTube

// Using the default implementation from protocol extension
song.togglePlayback() // Will pause since it's currently playing
// Output: Paused 'Imagine'

This example demonstrates how different protocol types can be combined to create a flexible, modular system for a media player framework.

Summary

In this guide, we've explored the various types of protocols in Swift:

  • Regular protocols: The foundation of protocol-oriented programming, usable by any type
  • Class-only protocols: Restricted to class types, useful for reference semantics and weak references
  • Protocol composition: Combining multiple protocols into a single requirement
  • Protocol inheritance: Creating hierarchy and relationships between protocols
  • Protocol extensions: Providing default implementations to add functionality

Understanding these different protocol types allows you to design more flexible, reusable, and maintainable Swift code. Protocols are one of Swift's most powerful features, and mastering the different types will help you build better applications.

Additional Resources

Exercises

  1. Create a Printable protocol with a method printDetails() and provide a default implementation in a protocol extension.

  2. Design a Database protocol that is class-only and explain why you made it class-only.

  3. Create a protocol hierarchy for different vehicle types (land vehicles, water vehicles, air vehicles) with appropriate properties and methods.

  4. Implement a simple task management system using protocol composition, with protocols for Task, Deadline, Priority, and Assignable.

  5. Use conditional conformance to make an array of your custom type conform to a protocol only when the elements meet certain criteria.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)