Swift Protocols Explained: What Protocols Are and How to Use Them

Swift protocols are one of the language’s most important building blocks. They let you describe what a type can do without forcing every type to share the same parent class. In this article, you’ll learn what protocols are, how protocol requirements work, how structs, classes, and enums conform to protocols, and how to avoid common mistakes when using them in real Swift code.

Quick answer: A Swift protocol is a blueprint of requirements such as properties, methods, and initializers that a type must provide. Types do not inherit implementation from a protocol by default, but they promise to match its required interface.

Difficulty: Beginner

Helpful to know first: You’ll understand this better if you know basic Swift syntax, how structs and classes are declared, and how functions and properties work.

1. What Is a Protocol?

A protocol in Swift defines a set of rules that a type can agree to follow. Those rules usually describe required properties, required methods, or required initializers.

Think of a protocol as a contract:

For example, if several types can be displayed as text, they can all conform to one protocol even if they store data in completely different ways.

protocol Describable {
var description: String { get }
}

struct Book: Describable {
let title: String

var description: String {
"Book: \(title)"
}
}

struct Movie: Describable {
let name: String

var description: String {
"Movie: \(name)"
}
}

let book = Book(title: "Swift Essentials")
let movie = Movie(name: "Code Runner")

print(book.description)
print(movie.description)

Both types satisfy the same protocol, but each type provides its own implementation.

Protocols are often compared with classes and inheritance. A class hierarchy shares implementation through parent-child relationships, while a protocol focuses on shared capabilities. In modern Swift, protocols are often preferred when you want flexibility without forcing unrelated types into the same inheritance chain.

2. Why Protocols Matters

Protocols matter because they help you design code around behavior instead of concrete types. This makes your code easier to reuse, test, and extend.

Here are some practical reasons protocols are important:

Without protocols, you often end up tightly coupling code to one exact struct or class. That makes changes harder later.

protocol Payable {
func calculatePay() -> Double
}

struct Employee: Payable {
let hours: Double
let rate: Double

func calculatePay() -> Double {
hours * rate
}
}

struct Contractor: Payable {
let projectsCompleted: Int
let paymentPerProject: Double

func calculatePay() -> Double {
Double(projectsCompleted) * paymentPerProject
}
}

Now any code that needs something payable can work with both types, even though they calculate pay differently.

3. Basic Syntax or Core Idea

The basic syntax for a protocol starts with the protocol keyword, followed by the protocol name and its requirements.

Declaring a simple protocol

This protocol requires one property and one method.

protocol Greetable {
var name: String { get }
func greet()
}

The property requirement says conforming types must provide a readable name. The method requirement says they must also define greet().

Making a type conform to a protocol

You conform to a protocol by adding its name after a colon in the type declaration.

struct Person: Greetable {
let name: String

func greet() {
print("Hello, my name is \(name).")
}
}

This works because Person provides everything Greetable requires.

Using a protocol as a type

Once a type conforms, you can write code that refers to the protocol instead of the concrete type.

func introduce(_ item: any Greetable) {
item.greet()
}

let person = Person(name: "Maya")
introduce(person)

In Swift 5+, using any makes it clear that you are working with a protocol type value.

4. Step-by-Step Examples

These examples show how protocols work in increasingly practical situations.

Example 1: Requiring a method

This is the simplest use case: a protocol requires a behavior.

protocol Drivable {
func start()
}

struct Car: Drivable {
func start() {
print("Car started")
}
}

let car = Car()
car.start()

The protocol does not care what kind of vehicle this is. It only requires a start() method.

Example 2: Requiring a property

Protocols can require properties, including read-only or read-write properties.

protocol IdentifiableItem {
var id: Int { get }
}

struct Order: IdentifiableItem {
let id: Int
}

let order = Order(id: 101)
print(order.id)

Because the protocol only requires get, a constant stored property is enough to satisfy the requirement.

Example 3: Conforming with a class

Classes can conform to protocols just like structs and enums.

protocol Resettable {
func reset()
}

class GameSession: Resettable {
var score = 10

func reset() {
score = 0
}
}

The protocol gives a common shape, while the class chooses the implementation details.

Example 4: One protocol, many types

Protocols become especially useful when one function should support many types.

protocol Printable {
func printDetails()
}

struct User: Printable {
let username: String

func printDetails() {
print("User: \(username)")
}
}

struct Product: Printable {
let title: String

func printDetails() {
print("Product: \(title)")
}
}

func showItem(_ item: any Printable) {
item.printDetails()
}

showItem(User(username: "sam_dev"))
showItem(Product(title: "Mechanical Keyboard"))

This makes the function reusable across unrelated types.

5. Practical Use Cases

Protocols appear in many real Swift projects. Common uses include:

If you have ever wanted one function to work with several different types in a clean way, a protocol is often the right tool.

6. Common Mistakes

Beginners often understand the idea of protocols quickly but run into syntax and conformance errors. These are some of the most common ones.

Mistake 1: Forgetting to implement all required members

When a type claims to conform to a protocol, it must provide every required property, method, and initializer that the protocol declares.

Problem: This type says it conforms to the protocol, but it does not implement the required method, so Swift reports a conformance error such as Type 'Robot' does not conform to protocol 'Speakable'.

protocol Speakable {
func speak()
}

struct Robot: Speakable {
let name: String
}

Fix: Add every missing requirement exactly as the protocol describes it.

protocol Speakable {
func speak()
}

struct Robot: Speakable {
let name: String

func speak() {
print("Hello, I am \(name).")
}
}

The corrected version works because the type now satisfies the full protocol contract.

Mistake 2: Mismatching a property requirement

A protocol property requirement must be matched with compatible access and type details. A read-write requirement cannot be satisfied by a read-only property.

Problem: The protocol requires both reading and writing, but this type only exposes a read-only property, so the conformance fails.

protocol Nameable {
var name: String { get set }
}

struct Customer: Nameable {
let name: String
}

Fix: Use a writable property when the protocol requires both get and set.

protocol Nameable {
var name: String { get set }
}

struct Customer: Nameable {
var name: String
}

The corrected version works because the stored property now matches the protocol’s required access level.

Mistake 3: Assuming a protocol provides implementation automatically

A plain protocol only declares requirements. It does not automatically supply behavior unless a protocol extension adds a default implementation.

Problem: This code expects the protocol declaration itself to create working behavior, but no implementation exists, so the conforming type still fails to meet the requirement.

protocol Loggable {
func log()
}

struct ServerEvent: Loggable {
let message: String
}

Fix: Either implement the requirement in the type itself, or provide a default implementation in a protocol extension.

protocol Loggable {
var message: String { get }
func log()
}

extension Loggable {
func log() {
print("Log: \(message)")
}
}

struct ServerEvent: Loggable {
let message: String
}

The corrected version works because the protocol extension now supplies the missing method implementation.

7. Best Practices

Protocols are most helpful when they describe meaningful capabilities clearly and narrowly.

Practice 1: Define behavior, not concrete data models

A protocol should usually describe what a type can do. If it becomes a copy of one specific model, it loses flexibility.

Less flexible:

protocol UserData {
var firstName: String { get }
var lastName: String { get }
var streetAddress: String { get }
}

Preferred when the real need is display behavior:

protocol DisplayNameProviding {
var displayName: String { get }
}

This is better because the protocol describes the capability your code actually needs.

Practice 2: Keep protocols small and focused

Large protocols are harder to adopt and often force types to implement unrelated requirements.

Less preferred:

protocol ManagerTool {
func save()
func load()
func delete()
func share()
func archive()
}

Preferred:

protocol Savable {
func save()
}

protocol Loadable {
func load()
}

Smaller protocols are easier to mix together only where needed.

Practice 3: Use protocol types when behavior matters more than implementation

If a function only needs a capability, accept the protocol rather than a specific concrete type.

Less reusable:

struct EmailNotifier {
func send() {
print("Email sent")
}
}

func runNotification(_ notifier: EmailNotifier) {
notifier.send()
}

Preferred:

protocol Notifying {
func send()
}

struct EmailNotifier: Notifying {
func send() {
print("Email sent")
}
}

func runNotification(_ notifier: any Notifying) {
notifier.send()
}

This makes the function work with any future notifier type that conforms to the protocol.

8. Limitations and Edge Cases

Protocols are powerful, but they do have rules and tradeoffs that can surprise beginners.

A common “not working” situation is declaring a method in a protocol that should modify a struct, then forgetting to mark the requirement and implementation as mutating. Swift then reports errors because value types cannot change themselves through non-mutating methods.

9. Practical Mini Project

Let’s build a small but complete example that uses protocols to handle different kinds of notifications. The goal is to write one function that can send any notification type as long as it conforms to the same protocol.

protocol NotificationSender {
var recipient: String { get }
func send()
}

struct EmailSender: NotificationSender {
let recipient: String

func send() {
print("Sending email to \(recipient)")
}
}

struct SMSSender: NotificationSender {
let recipient: String

func send() {
print("Sending SMS to \(recipient)")
}
}

func sendWelcomeMessage(using sender: any NotificationSender) {
print("Preparing message for \(sender.recipient)")
sender.send()
}

let email = EmailSender(recipient: "[email protected]")
let sms = SMSSender(recipient: "555-1234")

sendWelcomeMessage(using: email)
sendWelcomeMessage(using: sms)

This example shows the core advantage of protocols. The sendWelcomeMessage function does not care whether the sender is an email sender or an SMS sender. It only cares that the sender matches the NotificationSender contract.

10. Key Points

11. Practice Exercise

Create a protocol named Discountable with one method named discountedPrice() that returns a Double. Then make two structs conform to it:

Expected output: The program should print the discounted price for both a book and a course.

Hint: Define the protocol first, then make each struct implement the method in its own way.

protocol Discountable {
func discountedPrice() -> Double
}

struct Book: Discountable {
let price: Double

func discountedPrice() -> Double {
price * 0.9
}
}

struct Course: Discountable {
let price: Double

func discountedPrice() -> Double {
price * 0.8
}
}

func printDiscount(for item: any Discountable) {
print("Discounted price: \(item.discountedPrice())")
}

let book = Book(price: 30.0)
let course = Course(price: 100.0)

printDiscount(for: book)
printDiscount(for: course)

12. Final Summary

Protocols are a core part of Swift because they let you define shared behavior without forcing different types into the same inheritance structure. Instead of asking, “What class is this?”, protocols let you ask, “What can this type do?” That shift leads to more flexible and reusable code.

In this article, you learned what protocols are, how to declare them, how types conform to them, where they are useful, and what common mistakes to avoid. Once you are comfortable with the basics, the best next step is to learn protocol extensions and see how default implementations make protocol-oriented programming even more powerful in Swift.