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:
- A protocol says what must exist, not how it must be implemented.
- Multiple different types can conform to the same protocol.
- Structs, classes, and enums can all adopt protocols.
- Protocols help you write flexible code that works with behavior instead of one exact type.
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:
- You can write functions that accept any type that matches a required behavior.
- You can avoid large class hierarchies when types only need to share capabilities.
- You can swap one implementation for another more easily.
- Protocols support Swift’s protocol-oriented programming style.
- They make APIs clearer by showing what a value must be able to do.
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:
- Defining shared behavior for multiple models, such as anything that can be saved, printed, or validated.
- Writing flexible functions that accept any type matching a capability instead of one concrete type.
- Separating app logic from implementation details so code is easier to test.
- Replacing deep inheritance trees with smaller, composable behaviors.
- Creating APIs where the caller only needs to know what a type can do, not what it is internally.
- Modeling standard capabilities already used by Swift, such as Equatable, Comparable, Hashable, and CustomStringConvertible.
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 protocol only defines requirements. It does not automatically store data.
- Not every protocol can be used in every context exactly like a concrete type.
- Protocol requirements must match correctly in name, type, and mutability expectations.
- Using any with protocol types makes existential usage explicit in modern Swift.
- Some protocol-based designs become harder to understand if you create too many tiny protocols without a clear purpose.
- If you need shared stored properties, a protocol alone cannot provide them. That often requires concrete types or other patterns.
- When a method changes a struct or enum, protocol requirements may need mutating, which beginners often forget.
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
- A Swift protocol is a blueprint of required properties, methods, or initializers.
- Structs, classes, and enums can all conform to protocols.
- Protocols define what a type must do, not how it must do it.
- Protocols help you write flexible code that depends on behavior instead of one exact type.
- A type must implement every required member to conform successfully.
- Protocols are often a better fit than inheritance when unrelated types share the same capability.
- Protocol extensions can provide default implementations, but the protocol itself does not automatically do that.
11. Practice Exercise
Create a protocol named Discountable with one method named discountedPrice() that returns a Double. Then make two structs conform to it:
- Book with a price and a 10% discount
- Course with a price and a 20% discount
- Write a function that accepts any Discountable value and prints the discounted price
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.