---
title: "Swift Protocols: Protocol-Oriented Design"
slug: "swift-protocols"
category: "coding"
tags: ["swift", "protocols", "generics", "protocol-oriented", "associated-types"]
status: "stable"
last_updated: 2026-05-14
summary: "Protocol-oriented design, associated types, protocol composition, existentials with any, and generics vs type erasure in Swift."
related: ["[[coding/swift]]", "[[coding/swift-value-types]]", "[[coding/swift-error-handling]]", "[[ios/swiftui]]", "[[ios/core-data]]"]
---

## Overview

Protocol-oriented programming in Swift separates capability from implementation. Protocols define what a type can do; structs, enums, and classes define how. This page covers protocol design, associated types, composition, and the `any`/`some` existential distinction introduced in Swift 5.7 and tightened in Swift 6. Read [[coding/swift]] for the foundational rules, and [[coding/swift-value-types]] for why value types pair well with protocols.

## Define protocols around capabilities, not roles

A protocol should describe a single capability a caller depends on, not a complete role that a type plays. Small protocols compose into larger ones cleanly; large monolithic protocols force conforming types to implement irrelevant methods.

```swift
// Bad: one protocol doing too much
protocol DataSource {
    func fetch() async throws -> [Item]
    func save(_ item: Item) async throws
    func delete(id: UUID) async throws
    func count() -> Int
}

// Better: split by caller need
protocol ItemReader {
    func fetch() async throws -> [Item]
    func count() -> Int
}

protocol ItemWriter {
    func save(_ item: Item) async throws
    func delete(id: UUID) async throws
}
```

A view model that only reads can depend on `ItemReader` alone. Tests substitute a simple stub that only implements the methods the subject under test actually calls.

## Use protocol extensions for default implementations

Add behavior to a protocol via extensions so conforming types get it for free. Override the default in a specific conformer only when the type's implementation is meaningfully different.

```swift
protocol Timestamped {
    var createdAt: Date { get }
    var updatedAt: Date { get }
}

extension Timestamped {
    var age: TimeInterval { Date().timeIntervalSince(createdAt) }
    var isStale: Bool { Date().timeIntervalSince(updatedAt) > 3600 }
}
```

Protocol extensions dispatch statically, not dynamically. If a conforming class overrides `isStale` and you hold it as the protocol type `any Timestamped`, the override is not called. This is expected behavior, not a bug, but it is a reason to favor static dispatch in performance-sensitive paths.

## Use associated types for type-safe generic protocols

An associated type makes a protocol generic over a conformer-supplied type. Use it when the protocol's interface depends on a type that varies per conformance.

```swift
protocol Repository {
    associatedtype Entity: Identifiable
    func find(id: Entity.ID) async throws -> Entity?
    func save(_ entity: Entity) async throws
    func delete(id: Entity.ID) async throws
}

struct InMemoryRepository<E: Identifiable>: Repository {
    typealias Entity = E
    private var store: [E.ID: E] = [:]

    func find(id: E.ID) -> E? { store[id] }
    mutating func save(_ entity: E) { store[entity.id] = entity }
    mutating func delete(id: E.ID) { store.removeValue(forKey: id) }
}
```

Protocols with associated types cannot be used directly as existentials without wrapping in `any`. Prefer generic constraints when the concrete type is known at the call site; use `any` when the type must be erased.

## Prefer `some` over `any` when the concrete type is statically known

`some Protocol` is an opaque type: the compiler knows the concrete type and dispatches statically. `any Protocol` is an existential box: the concrete type is hidden at runtime and dispatch goes through a witness table with overhead.

```swift
// some: caller does not know the type, but the compiler does; no boxing
func makeReader() -> some ItemReader {
    InMemoryRepository<Item>()
}

// any: caller receives a type-erased value; boxing overhead
func register(reader: any ItemReader) {
    self.reader = reader
}
```

Return `some` from factory functions and computed properties. Accept `any` at injection boundaries where the concrete type genuinely varies at runtime, such as the environment injection pattern in [[ios/swiftui]].

## Compose protocols for precise constraints

Combine multiple protocols with `&` to express a constraint that a type must satisfy several capabilities at once.

```swift
typealias ReadableStore = ItemReader & Sendable
typealias FullStore = ItemReader & ItemWriter & Sendable

func sync<S: FullStore>(store: S) async throws {
    let local = try await store.fetch()
    let remote = try await remoteClient.fetch()
    let diff = computeDiff(local: local, remote: remote)
    for item in diff.toSave { try await store.save(item) }
    for id in diff.toDelete { try await store.delete(id: id) }
}
```

Protocol composition is cleaner than a single large protocol and more expressive than a class hierarchy. It pairs naturally with [[coding/swift-value-types]]: a struct can conform to multiple protocols without any inheritance.

## Use conditional conformance for targeted behavior

Extend a generic type to conform to a protocol only when its type parameters meet a condition.

```swift
extension Array: Timestamped where Element: Timestamped {
    var createdAt: Date { self.map(\.createdAt).min() ?? .distantPast }
    var updatedAt: Date { self.map(\.updatedAt).max() ?? .distantPast }
}
```

Conditional conformances express capability that emerges from the element type, not from the container. They are retroactive additions; use them judiciously to avoid surprising behavior.

## Related

- [[coding/swift]]
- [[coding/swift-value-types]]
- [[coding/swift-actors]]
- [[coding/swift-error-handling]]
- [[coding/swift-modules]]
- [[ios/swiftui]]
- [[ios/core-data]]
