Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Adding Reusable Behavior with Protocols and Extensions

Recommended resources

Good to see you again. In the previous lesson, you used closures to pass behavior into APIs such as Button, filter, and ForEach. This lesson moves one level up: instead of passing a single piece of behavior, you will define a capability contract that multiple types can share.

Protocols and extensions are central to idiomatic iOS development. SwiftUI itself uses them constantly—every SwiftUI screen is a type that conforms to View. Later, protocols will also let you substitute real services with test doubles when testing your plant identifier. For now, focus on the foundation: define requirements, conform types to them, and use extensions to organize or share behavior.


Protocols describe capabilities

A protocol is a blueprint of requirements. It says what a type must be able to provide—specific properties, methods, or both—without dictating how that type stores data or performs its work.

If you have used C#, a Swift protocol is closest to an interface. One important Swift feature is that a protocol extension can supply useful default behavior, which we will reach shortly.

Imagine that several app models can provide a concise plant summary. They may be different types—a plant fetched from a service, a saved history item, or a manually entered plant—but the rest of the app only needs the shared capability: “give me a summary.”

protocol PlantSummaryProviding {
    var commonName: String { get }
    var scientificName: String { get }
    var wateringIntervalDays: Int { get }

    func summary() -> String
}

Read this as a contract:

  • A conforming type must expose a readable commonName, scientificName, and wateringIntervalDays.
  • It must implement a summary() method that returns a String.
  • The protocol does not say whether these values are stored properties, computed properties, or obtained some other way.

The { get } after each property means “code using this protocol can read this value.” A let stored property, a var stored property, or a read-only computed property can all satisfy that requirement.

If a protocol needs callers to modify a property, it uses { get set }:

protocol FavoriteMarking {
    var isFavorite: Bool { get set }
}

A type conforming to FavoriteMarking needs a writable isFavorite property. A constant let isFavorite would not be sufficient.

Watch Paul Hudson’s explanation before writing the examples below. It establishes the useful mental model: protocols describe the capabilities code depends on, rather than tying code to one concrete type.

How to create and use protocols – Swift for Complete Beginners

Watch “How to create and use protocols – Swift for Complete Beginners” by Paul Hudson for a compact walkthrough of protocol contracts, conformance, and property requirements.

Watch the contract idea for the problem protocols solve. Continue with definition and conformance, paying attention to the fact that a conforming method’s name, parameters, and return type must match exactly. Then skip to property requirements to see the practical difference between readable and writable protocol properties.



Conformance: a type fulfills the contract

A type adopts a protocol by placing the protocol name after a colon:

struct Plant: PlantSummaryProviding {
    let commonName: String
    let scientificName: String
    let wateringIntervalDays: Int

    func summary() -> String {
        "\(commonName) (\(scientificName)): water about every \(wateringIntervalDays) days."
    }
}

Plant now conforms to PlantSummaryProviding because it supplies every required property and method with compatible types.

The compiler checks this immediately. For example, this would fail:

struct IncompletePlant: PlantSummaryProviding {
    let commonName: String
    let scientificName: String
    let wateringIntervalDays: Int
}

Swift would report that IncompletePlant does not conform because summary() is missing. This compile-time guarantee is the value of the protocol: code that accepts a PlantSummaryProviding can safely use all of those requirements.

For example:

func printSummary(for plant: any PlantSummaryProviding) {
    print(plant.summary())
}

The any keyword means this parameter can hold a value whose concrete type is any type conforming to PlantSummaryProviding.

let snakePlant = Plant(
    commonName: "Snake plant",
    scientificName: "Dracaena trifasciata",
    wateringIntervalDays: 14
)

printSummary(for: snakePlant)

The function does not need to know that its argument is specifically a Plant. It only relies on the promised capability: the value can produce a summary.

The official Swift guide is worth reading here because it distinguishes property requirements, method requirements, adding conformance later, and supplying default implementations.

Protocols | Documentation

Read the relevant parts of the Swift Language Guide’s “Protocols” chapter to reinforce the precise rules that the compiler applies to protocol properties, methods, and extensions.

In “Property Requirements” and “Method Requirements,” focus on how a protocol specifies what must exist without specifying stored versus computed implementation details. Then read “Adding Protocol Conformance with an Extension” to see why conformance can be separated from a type’s original declaration. In “Protocol Extensions,” read the opening explanation, then continue through “Providing Default Implementations.” Pay particular attention to the default implementation rule.



Extensions add behavior without changing the original declaration

An extension adds functionality to an existing type. It is useful both for organizing your own types and for adding capabilities to types whose original source code you do not control.

Here is a type extension that adds a computed property to Plant:

extension Plant {
    var displayName: String {
        "\(commonName) (\(scientificName))"
    }
}

Now every Plant value has displayName:

print(snakePlant.displayName)
// Snake plant (Dracaena trifasciata)

This behavior belongs naturally with Plant, so the dot syntax reads well and Xcode can discover it through code completion.

Extensions can add:

  • computed properties;
  • instance and type methods;
  • initializers;
  • protocol conformances.

They cannot add stored properties. This is invalid:

extension Plant {
    // var sourceURL: URL?   // Error: extensions cannot contain stored properties
}

A stored property changes what data every instance physically holds, so it must be declared in the original type definition. An extension can instead add a computed property derived from data the type already has.

Study the extension syntax and its restrictions in the Swift guide.

Extensions | Documentation

Read the Swift Language Guide’s “Extensions” chapter for the exact syntax and the key distinction between computed and stored properties.

In “Extension Syntax,” read the explanation of conformance and scope. Then read “Computed Properties,” including the note after the distance-unit example: extensions can add computed properties but not stored properties. Focus on the design reason rather than memorizing the unit-conversion code.



Add protocol conformance in an extension

You do not have to put conformance in the original type declaration. This is often useful when you want to keep a model’s stored data separate from a particular capability.

Start with a plain model:

struct Plant {
    let commonName: String
    let scientificName: String
    let wateringIntervalDays: Int
}

Then add its conformance elsewhere:

extension Plant: PlantSummaryProviding {
    func summary() -> String {
        "\(commonName) (\(scientificName)): water about every \(wateringIntervalDays) days."
    }
}

This means exactly the same thing as putting : PlantSummaryProviding next to struct Plant. The extension simply gives you a clean place to group the code involved in that conformance.

You can also use an extension to conform to a standard-library protocol. CustomStringConvertible requires a readable description property that print() can use:

extension Plant: CustomStringConvertible {
    var description: String {
        displayName
    }
}

Now this gives a useful result:

print(snakePlant)
// Snake plant (Dracaena trifasciata)

In a growing app, it is common to organize code with files such as:

Plant.swift
Plant+Summary.swift
Plant+Display.swift

The +Summary and +Display parts are naming conventions, not Swift syntax. They simply communicate that the files contain extensions of Plant.



Protocol extensions share default behavior

A type extension adds behavior to one particular type. A protocol extension adds behavior to every type that conforms to a protocol.

Return to the protocol:

protocol PlantSummaryProviding {
    var commonName: String { get }
    var scientificName: String { get }
    var wateringIntervalDays: Int { get }

    func summary() -> String
}

Both the protocol and every conforming type currently repeat the idea of a summary. Instead, write one default implementation:

extension PlantSummaryProviding {
    func summary() -> String {
        "\(commonName) (\(scientificName)): water about every \(wateringIntervalDays) days."
    }
}

Now Plant can conform with an empty extension:

extension Plant: PlantSummaryProviding {}

That works because:

  1. Plant already has the three required properties.
  2. The protocol extension supplies the required summary() method.

The protocol extension uses only the protocol’s required properties. It therefore works for any conforming type, not just Plant.

An Xcode Playground defines an `Employee` protocol, gives its required `doWork()` method a default implementation in an `extension Employee`, and shows that `OfficeDrone` receives that behavior simply by conforming to the protocol.

A type can still replace the default when it has a better domain-specific implementation:

struct SavedPlant: PlantSummaryProviding {
    let commonName: String
    let scientificName: String
    let wateringIntervalDays: Int

    func summary() -> String {
        "\(commonName) is saved in your history. Water about every \(wateringIntervalDays) days."
    }
}

Because summary() is explicitly listed in the protocol, Swift uses SavedPlant’s implementation when the value is a SavedPlant, and uses the default for types such as Plant that do not provide one.

A practical rule prevents a subtle class of mistakes:

If conforming types should be allowed to customize a shared method, declare that method in the protocol first, then provide its default in a protocol extension.

Here is the distinction to keep in mind:

Form Example Best use
Type extension extension Plant { ... } Add behavior specific to Plant.
Conformance extension extension Plant: PlantSummaryProviding { ... } Declare that an existing type fulfills a protocol.
Protocol extension extension PlantSummaryProviding { ... } Share default behavior across all conforming types.


Build a reusable summary feature

Create a new Blank Playground in Xcode and replace its contents with the following code. A Playground keeps this language-focused experiment separate from your SwiftUI app while you inspect the printed output.

protocol PlantSummaryProviding {
    var commonName: String { get }
    var scientificName: String { get }
    var wateringIntervalDays: Int { get }

    func summary() -> String
}

extension PlantSummaryProviding {
    func summary() -> String {
        "\(commonName) (\(scientificName)): water about every \(wateringIntervalDays) days."
    }
}

struct Plant {
    let commonName: String
    let scientificName: String
    let wateringIntervalDays: Int
}

extension Plant: PlantSummaryProviding {}

extension Plant {
    var displayName: String {
        "\(commonName) (\(scientificName))"
    }
}

extension Plant: CustomStringConvertible {
    var description: String {
        displayName
    }
}

struct SavedPlant: PlantSummaryProviding {
    let commonName: String
    let scientificName: String
    let wateringIntervalDays: Int

    func summary() -> String {
        "\(commonName) is saved in your history. Water about every \(wateringIntervalDays) days."
    }
}

func printSummary(for plant: any PlantSummaryProviding) {
    print(plant.summary())
}

let snakePlant = Plant(
    commonName: "Snake plant",
    scientificName: "Dracaena trifasciata",
    wateringIntervalDays: 14
)

let savedPothos = SavedPlant(
    commonName: "Golden pothos",
    scientificName: "Epipremnum aureum",
    wateringIntervalDays: 7
)

printSummary(for: snakePlant)
printSummary(for: savedPothos)
print(snakePlant)

Run the Playground and inspect the results:

Snake plant (Dracaena trifasciata): water about every 14 days.
Golden pothos is saved in your history. Water about every 7 days.
Snake plant (Dracaena trifasciata)

Notice the three distinct reuse mechanisms in this one example:

  • Plant gets summary() from the protocol extension.
  • SavedPlant supplies a more specialized version of summary().
  • Plant gets displayName from a type extension.
  • Plant adopts CustomStringConvertible in a separate conformance extension.

This is more than an exercise in syntax. It prevents duplicated formatting logic, keeps each type focused on its data, and gives later code a stable capability-based interface.



Choosing the right tool

When you write app code, decide based on the scope of the behavior:

  • Use a protocol when multiple types should guarantee the same capability.
  • Use a protocol extension when those conforming types can share a sensible default implementation.
  • Use a type extension when behavior belongs only to one existing type or when you want to organize related code outside its main declaration.
  • Use { get } for a read-only requirement and { get set } only when consumers of the protocol must be able to modify the property.

Avoid creating protocols for every small convenience. A protocol earns its place when other code genuinely benefits from depending on a capability rather than a concrete model. In this course, that will become especially valuable when a view model depends on a plant-information service and tests replace the real service with a controlled test double.



Takeaways

A protocol defines a set of required capabilities. A type conforms by providing every required property and method with matching types and signatures:

struct Plant: PlantSummaryProviding {
    // Requirements are implemented here.
}

An extension adds functionality to an existing type:

extension Plant {
    var displayName: String { ... }
}

It can also add conformance separately:

extension Plant: PlantSummaryProviding {
    // Protocol requirements can be implemented here.
}

Finally, a protocol extension provides shared behavior for all conforming types:

extension PlantSummaryProviding {
    func summary() -> String { ... }
}

You now have a key Swift design tool: types can share a common contract without sharing one parent class, and shared behavior can live in one well-chosen place. Next, you will begin the SwiftUI interface module by composing a reusable view from standard controls and custom subviews.