iosNudge

What is protocol-oriented programming in Swift?

MediumAsked oftenSwiftProtocols 2 min read

TL;DR

Protocol-oriented programming (POP) means you design with protocols first, not base classes. You describe small abilities as protocols. You give them default code with protocol extensions. Any type can adopt them, including structs and enums. The result: composition instead of deep inheritance, and easy testing with fakes.

Inheritance vs protocols

With classes, you get one parent. Behavior piles up in a big base class.

class BaseViewController: UIViewController {
    func showLoading() { /* ... */ }
    func showError(_ message: String) { /* ... */ }
    func trackScreen() { /* ... */ }
}

Every screen gets everything, even what it does not need.

With protocols, you mix only what you need:

protocol LoadingShowing { func showLoading() }
protocol ErrorShowing { func showError(_ message: String) }

final class ProfileViewController: UIViewController, LoadingShowing, ErrorShowing {
    func showLoading() { /* ... */ }
    func showError(_ message: String) { /* ... */ }
}

Default implementations with extensions

A protocol extension can provide code. Types get it for free.

protocol Describable {
    var name: String { get }
}

extension Describable {
    func describe() -> String { "This is \(name)" }
}

struct Dog: Describable { let name: String }
struct Car: Describable { let name: String }

print(Dog(name: "Rex").describe())   // "This is Rex"

Dog and Car share behavior without a common base class. They are structs, too.

Constrained extensions

You can add code only for some conforming types.

extension Collection where Element: Numeric {
    func total() -> Element { reduce(0, +) }
}

[1, 2, 3].total()      // 6
[1.5, 2.5].total()     // 4.0

Testing with protocols

POP makes dependency injection simple. Depend on a protocol, not a concrete type.

protocol UserService {
    func fetchUser(id: Int) async throws -> User
}

struct LiveUserService: UserService {
    func fetchUser(id: Int) async throws -> User { /* network */ }
}

struct MockUserService: UserService {
    var user: User
    func fetchUser(id: Int) async throws -> User { user }
}

@MainActor
final class ProfileViewModel {
    private let service: any UserService
    init(service: any UserService) { self.service = service }
}

In tests you pass MockUserService. No network needed.

The classic trap: requirement vs extension-only method

A method declared in the protocol uses dynamic dispatch. A method that exists only in the extension uses static dispatch.

protocol Greeter {
    func hello() -> String        // requirement
}

extension Greeter {
    func hello() -> String { "Hello" }
    func bye() -> String { "Bye" } // NOT a requirement
}

struct French: Greeter {
    func hello() -> String { "Bonjour" }
    func bye() -> String { "Au revoir" }
}

let g: any Greeter = French()
print(g.hello())   // "Bonjour"  (dynamic)
print(g.bye())     // "Bye"      (static, uses the extension)

If you want types to override it, declare it in the protocol.

When not to use POP

  • When you need shared mutable state. A class or actor is clearer.
  • When a protocol has only one conforming type and no test double. It adds noise.
  • When you need to subclass a framework class like UIView.

POP is a tool, not a rule. Interviewers like a balanced answer.

Common follow-up questions

  • Can protocols have stored properties? No. They can require properties, but the conforming type stores them.
  • What is retroactive conformance? Making a type you do not own conform to a protocol with an extension. In Swift 6, conforming another module's type to another module's protocol gives a warning; mark it @retroactive if you mean it.
  • What is the cost of any Protocol? It uses an existential box and dynamic dispatch. Generics or some can be faster.
  • Can a protocol be limited to classes? Yes: protocol Delegate: AnyObject. This lets you hold it as weak.
  • Is POP only a Swift idea? Similar ideas exist elsewhere (traits, interfaces with default methods). Apple named and promoted it at WWDC 2015.