Protocols in Swift: Defining Contracts and Enabling Polymorphism


Imagine Pixar is hiring for a new movie. The job posting doesn’t care if you’re a toy, a monster, a fish, or a robot — it just lists the requirements: “must be able to animate, must have a name, must deliver a catchphrase.” Any character who meets those requirements gets the role. That’s exactly how protocols work in Swift.

In this guide, you’ll learn how to define protocols, conform types to them, add default behavior with protocol extensions, and understand why Swift favors protocol-oriented programming. We won’t cover generics or associated types — those have their own dedicated posts.

What You’ll Learn

What Is a Protocol?

A protocol is a contract — a list of requirements that any conforming type must fulfill. It defines what a type must do, but not how it does it. Protocols don’t provide implementation; they just set the rules.

Think of a protocol as that Pixar job posting: “Any character applying for this role must be able to animate() and provide a name.” Whether you’re Woody (a struct), Sulley (a class), or Lightning McQueen (an enum), it doesn’t matter — as long as you meet the listed requirements, you qualify.

Apple Docs: Protocol — The Swift Programming Language

Defining and Conforming

You define a protocol with the protocol keyword. It looks similar to a struct or class, but it only declares what is required — no implementation.

protocol Animatable {
    func animate()
}

Any type can conform to a protocol by listing it after a colon and implementing all its requirements:

struct Woody: Animatable {
    func animate() {
        print("Woody pulls his string and waves!")
    }
}

let woody = Woody()
woody.animate()
Woody pulls his string and waves!

Classes conform the same way:

class Sulley: Animatable {
    func animate() {
        print("Sulley roars and scares!")
    }
}

let sulley = Sulley()
sulley.animate()
Sulley roars and scares!

The compiler enforces conformance — if you forget to implement animate(), your code won’t compile. This guarantee is what makes protocols powerful: you can trust that any Animatable type has the required behavior.

Note: A struct, class, or enum can conform to as many protocols as it needs. Just separate them with commas: struct Buzz: Animatable, CustomStringConvertible.

Protocol Properties and Methods

Protocols can require both properties and methods. For properties, you must specify whether they’re gettable ({ get }) or gettable and settable ({ get set }).

protocol Character {
    var name: String { get }
    var catchphrase: String { get }
    func introduce()
}

A { get } property means conforming types must provide at least a getter — they can also make it settable. A { get set } property means the conformer must allow both reading and writing.

struct ToyStoryCharacter: Character {
    let name: String
    let catchphrase: String

    func introduce() {
        print("\(name) says: \"\(catchphrase)\"")
    }
}

let buzz = ToyStoryCharacter(
    name: "Buzz Lightyear",
    catchphrase: "To infinity and beyond!"
)
buzz.introduce()
Buzz Lightyear says: "To infinity and beyond!"

Protocols can also require mutating methods for value types. If a method needs to modify self on a struct or enum, mark it mutating in the protocol:

protocol Healable {
    var health: Int { get set }
    mutating func heal(amount: Int)
}

Tip: Use { get } by default for protocol properties. Only require { get set } when conformers genuinely need to allow external mutation.

Protocol Extensions

A protocol extension adds default implementations to a protocol. Any type that conforms gets the behavior for free — no extra code needed.

protocol Performable {
    var movieTitle: String { get }
}

extension Performable {
    func showCredits() {
        print("🎬 \(movieTitle) — A Pixar Production")
    }
}

Now any conforming type automatically has showCredits():

struct Film: Performable {
    var movieTitle: String
}

let coco = Film(movieTitle: "Coco")
coco.showCredits()
🎬 Coco — A Pixar Production

Conformers can also override the default if they need custom behavior:

struct SpecialFilm: Performable {
    var movieTitle: String

    func showCredits() {
        print("✨ \(movieTitle) — A Special Pixar Presentation ✨")
    }
}

let inside = SpecialFilm(movieTitle: "Inside Out 2")
inside.showCredits()
✨ Inside Out 2 — A Special Pixar Presentation ✨

Note: Protocol extensions are one of Swift’s most distinctive features. They let you share behavior across unrelated types without using inheritance.

Protocol as a Type

You can use a protocol as a type to hold any value that conforms to it. In Swift 6, prefix it with any to make this explicit:

let characters: [any Animatable] = [Woody(), Sulley()]

for character in characters {
    character.animate()
}
Woody pulls his string and waves!
Sulley roars and scares!

This is called polymorphism — a single variable or collection holding different concrete types, all unified by a shared protocol. You don’t need to know the exact type; you just know it can animate().

func performShow(with actor: any Animatable) {
    actor.animate()
}

performShow(with: Woody())
performShow(with: Sulley())
Woody pulls his string and waves!
Sulley roars and scares!

Tip: Use any Protocol when you need to store mixed types. Use some Protocol when a function returns a single (but opaque) conforming type — this gives the compiler more optimization room.

Protocol-Oriented Programming

In many languages, you share code by building deep inheritance hierarchies — a base class with layers of subclasses. Swift takes a different approach: compose behavior from multiple protocols instead.

protocol Drawable {
    func draw()
}

protocol Soundable {
    func playSound()
}

struct PixarCharacter: Drawable, Soundable {
    let name: String

    func draw() {
        print("Drawing \(name) on screen")
    }

    func playSound() {
        print("\(name) plays their theme music")
    }
}

let nemo = PixarCharacter(name: "Nemo")
nemo.draw()
nemo.playSound()
Drawing Nemo on screen
Nemo plays their theme music

With protocols, a type can adopt exactly the capabilities it needs. There’s no forced inheritance of things it doesn’t want. This is protocol-oriented programming — Swift’s preferred design philosophy.

Warning: Deep class inheritance can become rigid and hard to change. Protocols give you flexibility — a struct can conform to Drawable without being locked into a class hierarchy.

Common Mistakes

Missing { get } or { get set } on Property Requirements

Protocol properties must specify their access:

// ❌ Compile error: property in protocol must have
// explicit { get } or { get set }
protocol BadProtocol {
    var title: String
}
// ✅ Always specify { get } or { get set }
protocol GoodProtocol {
    var title: String { get }
}

Confusing any and some with Protocol Types

When you try to store a protocol-typed value, you need the any keyword in Swift 6:

// ❌ In Swift 6, this produces a warning/error
// var character: Animatable = Woody()
// ✅ Use `any` for existential types
var character: any Animatable = Woody()
character.animate()
Woody pulls his string and waves!

Use some in return types when the function always returns the same concrete type — the compiler can optimize this better. Use any when you need to hold different conforming types.

What’s Next?

  • A protocol defines a contract — a list of requirements any conforming type must fulfill.
  • Types conform by implementing all required properties and methods.
  • Protocol extensions add default implementations that conformers get for free.
  • Use any Protocol to hold mixed conforming types in variables and collections.
  • Protocol-oriented programming composes behavior from multiple protocols instead of deep inheritance.

Extensions let you add new capabilities to existing types — including adding protocol conformances after the fact. Head to Extensions in Swift to learn how to extend any type without modifying its source code.