Chapters

Hide chapters

Swift Internals

First Edition · iOS 26 · Swift 6.2 · Xcode 26

2. Protocols, Dispatch, Existential Types
Written by Aaqib Hussain

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

A protocol is one of the most effective ways to achieve polymorphism in Swift. Protocols work flawlessly with both classes and structs. Structs don’t support inheritance mainly because of their value semantics; however, protocols deliver similar functionality through abstraction and conformance.

When you use protocols, you often end up applying the SOLID principles unintentionally. And that’s a good thing. Your code becomes more maintainable, robust, loosely coupled, testable, and exactly what you want in a healthy architecture.

However, protocols aren’t all sunshine and rainbows. There are performance trade-offs, especially related to method dispatch. Not all dispatch types are costly; some are lightning-fast.

And what about Generics and Existentials? What’s their purpose? How do they differ? When should you choose one over the other? You’ll explore all of that in this chapter.

Now, put on your helmet and tighten your seatbelt; you’re about to go on an adventure through Swift’s protocol system.

Protocol Me This

A protocol is like a promise; if you agree to it, you must fulfill it. In Swift, you call this conformance. You conform to a protocol by implementing the required methods or properties.

Here’s a simple example:

protocol DataRepository {
  func fetch()
}

struct RemoteDataRepository: DataRepository {
  func fetch() {
    // fetch some data
  }
}

The cool thing about protocols? You can provide default implementations.

protocol Decoder {
  func decode()
}

extension Decoder {
  func decode() {
    print("Some Default Decoding")
  }
}

struct DefaultDecoder: Decoder {}

Here, DefaultDecoder conforms to the Decoder protocol, but it doesn’t need to implement decode() because the protocol extension provides a default. So if you do:

let defaultDecoder = DefaultDecoder()
defaultDecoder.decode() // Some Default Decoding

Default implementations help reduce boilerplate code, like in the example above, and allow you to simulate optional behavior in protocols. But be mindful when using them, as they may obscure intent and break the Interface Segregation Principle. It’s always best to keep protocols small and targeted, and only conform to types that require the behavior.

Conditional Conformance

Swift also supports conditional conformance, meaning a type only conforms to a protocol under specific conditions. For example:

protocol Summable { // 1
  func sum() -> Int
}

extension Array: Summable where Element == Int { // 2
  func sum() -> Int {
    return self.reduce(0, +)
  }
}

let numbers: [Int] = [1, 2, 3, 4, 5] // 3 

let total = numbers.sum() // 4

Dispatch

Now the million-dollar question: how does Swift decide which implementation to call? That’s what dispatch is all about. In short, dispatch is how Swift decides which concrete function body to run when you call a method. Swift uses three primary dispatch mechanisms:

Static Dispatch

It’s one of the fastest method call mechanisms Swift offers. The compiler determines the exact function to call at compile time, allowing it to eliminate runtime lookups and often inline the method entirely.

struct Size {
  var width, height: Double
  func area() -> Double {
    return width * height
  }
}

let size = Size(width: 10, height: 10)
size.area() // Static Dispatch
final class Size {
  var width, height: Double
  func area() -> Double {
    return width * height
  }
}

// OR

class Size {
  var width, height: Double
  final func area() -> Double {
    return width * height
  }
}

let size = Size(width: 10, height: 10)
size.area() // Static Dispatch
protocol Animal {}

extension Animal {
  func sleep() {
    print("Sleeping soundly.")
  }
}

struct Dog: Animal {}

// OR 

class Dog: Animal {}

let animal: Animal = Dog()
animal.sleep() // "Sleeping soundly."

Dynamic Dispatch

A call resolution technique in which the exact implementation is determined at runtime via indirection.

Virtual Table Dispatch

Virtual dispatch occurs for all members of a class or actor unless specified otherwise.

class Animal {
  func sleep() {
    print("Sleeping...")
  }
}

class Dog: Animal {
  override func sleep() {
    print("Dog is sleeping.")
  }
}

let animal: Animal = Dog()
animal.sleep() // V-Table Dispatch

Protocol Witness Table

Whenever you use a protocol with required methods and call them through a protocol type variable, Swift performs a Protocol Witness Table dispatch. At compile time, Swift creates a witness table that contains pointers to the actual implementations of the methods that the concrete type provides to fulfill the protocol.

protocol Animal {
  func sleep()
}

class Dog: Animal {
  func sleep() {
    print("Dog is sleeping.")
  }
}

let animal: Animal = Dog() 
animal.sleep() // PW-Table Dispatch
let animal: Dog = Dog()
animal.sleep() // V-Table Dispatch

Message Dispatch

You often use it daily when working with Swift, but it may never have crossed your mind. Message dispatch can behave differently depending on the situation through #selector, Swizzling, and KVO. Although not officially classified as distinct types, they function quite differently at runtime.

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let button = UIButton()
    //...
    button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
  }
  
  @objc func buttonAction() {
    // some action
  }
}
extension UIViewController {
  @objc func track_viewDidLoad() {
    print("View Did Load: \(String(describing: type(of: self)))")
    self.track_viewDidLoad()
  }
  
  static func swizzleViewDidLoad() {
    let originalSelector = #selector(viewDidLoad)
    let swizzledSelector = #selector(track_viewDidLoad)
    
    guard let originalMethod = class_getInstanceMethod(self, originalSelector),
          let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else {
      return
    }
    
    method_exchangeImplementations(originalMethod, swizzledMethod)
  }
}
class ViewModelObserver {
  let viewModel: ViewModel
  private var valueObservation: NSKeyValueObservation?
  
  init(viewModel: ViewModel) {
    self.viewModel = viewModel
    
    self.valueObservation = viewModel.observe(\.value, options: [.old, .new]) { (viewModel, change) in
      print("--- KVO Triggered for 'value' ---")
      if let oldValue = change.oldValue, let newValue = change.newValue {
        print("The ViewModel's value changed from '\(oldValue)' to '\(newValue)'.")
      }
    }
  }
}

final class ViewModel: NSObject {
  @objc dynamic var value: String // 1
  
  init(value: String) {
    self.value = value
    super.init()
  }
}

Flowchart for Dispatch

Here’s a simple flowchart to visualize how Swift decides which dispatch mechanism to use based on the context of the method call:

Eg zhu wadz ar i didoi ndbi op yizid/nbaran/xwonogo yiytuf? Ij fmu vobg as o hjeqozuj-wxcon cabeufro gez i zareuviw gibrek? Iq kcu fobj om oy @axxt ol mfjutob cowxaw? Xinvug Vowf ul Btehq Zdobov Desqahyw Bazceto Cavravvg Kcusores Vemfumt Zokce H-Tohjo Rolqesym Vex Cor Zo Jo Wiw Ne
Bdob khesb msigicm gerwibnp owsiq u tenham sinn

Dispatch Types: Comparison Table

Here is a small table showing the main differences between the dispatch types:

Bvoum Jbru Watt B-Sitze Varyuzql Qaxyupj Bruzaw Wikvavky Lisuaq Xxanehah Yoczotd Wagta Zforeby Puhzato Rovsogfh Azfebusivne, egojzejopna fyelv miqdajt Ruxio xvyuv, zoruc/fevomguzapi/yjemeqi/hfowup mibpirc Ynamowikh wahh zoneawoq tajbeyv , YZI, , Osx-Y icsavof @ipzy#dahernez Jopxov Oza Yayip
O neutc nalbisadip conkuir hiwkolgk mzdup

Existential a.k.a Boxed Type Protocol

An existential is when you use any Protocol as a type in Swift. It’s a form of type erasure; you hide the concrete type behind the protocol. In modern Swift, any makes this usage explicit; if your protocol has no associatedtype or Self requirements, you can still omit it in type annotations, and the code will compile.

protocol Logger {
  func log(_ message: String)
}

struct ConsoleLogger: Logger {
  func log(_ message: String) {
    print("Writing to console: \(message)")
  }
}

struct FileLogger: Logger {
  func log(_ message: String) {
    print("Writing to file: \(message)")
  }
}
func runLogger(_ logger: any Logger) { // 'any' is optional here
  logger.log("Hello from an existential Logger!")
}

let logger: any Logger = ConsoleLogger()
runLogger(logger) // "Hello from an existential Logger!"
protocol Logger {
  associatedtype Message
  func log(_ message: Message)
}
let logger: Logger = ConsoleLogger() // Use of protocol 'Logger' as a type must be written 'any Logger'
let logger: any Logger = ConsoleLogger() 
func runLogger(_ logger: any Logger) {
  logger.log("Hello from an existential Logger!") // Member 'log' cannot be used on value of type 'any Logger'; consider using a generic constraint instead
}
protocol Logger {
  associatedtype Message
  func placeHolder() -> Message
}

struct ConsoleLogger: Logger {
  // ...
  func placeHolder() -> String {
    "Writing to console:"
  }
}

func printLoggerPlaceHolder(_ logger: any Logger) {
  print(logger.placeHolder()) // Compiles, without error
}

When to Use Existential Types?

Here are some of the most common cases where existential types shine:

protocol Tracker {
  func track(_ key: String, parameters: [String: Any])
}

struct AppsFlyerTracker: Tracker {
  func track(_ key: String, parameters: [String : Any]) {
    // some AppsFlyer tracking code.
  }
}

struct AdjustTracker: Tracker {
  func track(_ key: String, parameters: [String : Any]) {
    // some Adjust tracking code.
  }
}
let trackers: [any Tracker] = [AppsFlyerTracker(), AdjustTracker()]
for tracker in trackers {
  tracker.track("some_event", parameters: ["some_key": "some_value"])
}
func showRelevantView(_ hasNoUserData: Bool) -> any View {
  hasNoUserData ? UserDataView() : EmptyStateView()
}

Opaque Types

This is another way Swift hides the concrete type from the outside world. You can think of it as a form of type erasure, but it’s aimed at the compiler’s benefit rather than runtime flexibility.

protocol Logger {
  func log(_ message: String)
}

struct ConsoleLogger: Logger {
  func log(_ message: String) {
    print("[LOG]: \(message)")
  }
}

func makeLogger() -> some Logger {
  ConsoleLogger()
}

let logger = makeLogger()
logger.log("Hello from an opaque return type!")

some vs any

Take a look at the following comparison between some and any:

copi Suobato Goob ili ey ojekxatsiin bezlauqos — hawxodox hwebl gna hpyu. non Uyalhinzeuy zamwiusiw Licijh i prosizem cavmxidu fudawd dnbo dqod eq AGU wohmog (amdwrunhoop). Fcukanm Azi Qiwi Ifot tzo bwagazuk qitniht rosha bip pojaabitutd giyvt. JQM qab jhumumej diliizonuslm Wujamcrg wbequn vru kiyio ah binidimpi. Ithqa adloyewreey Eryobw osuj us uzowkexmeor nocraelaf (dxilow tumrolc sezma + yolai). Sduwagz dadxewodw tucpdoni qsfos wruk rofpiqb sa xfo kufi vkavavic el u vobewamuroiog xendivteam. Acmo amuq blu nfizeqog juqruzr pabwa. Agpagv ofcx od okdxe putof ew aljuxitwoed bntuafr agicmifkeew psuvuha. edt Maw ben-pugeogutugt oysadkiuf mofpecm (evsivz xcinad). Ylipad lansogqn Timuf awc gtuyy ij lijnamo xapa. Kademj hixiac Rep zuh-yeyiopumurr ewkecfioj fisvovx (uxxirj ndarey). Uvmbuph id voxtewe rodo.
tulu fq. ops

Uncommon Dispatch Scenarios

At this point, you might think you know everything about dispatch, but Swift can surprise you in different situations. Dispatch sometimes behaves the opposite of what you expect. These edge cases usually result from how the compiler perceives the type at the call site and whether it needs to resolve the method at compile time or runtime.

The dynamic Keyword

If you mark a method as dynamic even if you aren’t using it with Objective-C or any Objective-C runtime features, Swift defers its resolution until runtime. Check the following snippet:

class Example: NSObject {
  dynamic func greet() {
    print("Hello")
  }
}

Chained Dispatch

In some scenarios, you may encounter a situation where both an existential container and a PWT lookup occur. Example:

func callLog<T: Logger>(_ logger: T) where T: AnyObject { // 1
  let existential: any Logger = logger // 2
  existential.log("Hello") // 3
}

Static Dispatch Isn’t Always Inline

It’s incorrect to assume that the compiler always inlines static dispatch methods. That’s not always true. Inlining is a compiler optimization decision, not determined purely by the dispatch type. Therefore, the compiler might choose not to inline a large method.

SwiftUI and Dispatch

Since the release of SwiftUI, you can see how much static dispatch is happening behind the scenes — for one reason: performance. From generic views to opaque types to the heavy use of structs, all point to one thing: Swift aims to achieve maximum performance in UI code.

Class Methods vs. Static Methods

In classes, you can have both class and static type methods, but their dispatch behaviors differ.

class Vehicle {
  static func vehicleType() -> String {
    return "Generic Vehicle"
  }
  
  class func maxSpeed() -> Int {
    return 100
  }
}

class Car: Vehicle {
  // SUCCESS: Can override class method
  override class func maxSpeed() -> Int {
    return 250
  }
}

// Static Dispatch: The compiler calls Vehicle.vehicleType() directly.
print(Car.vehicleType()) // Prints: "Generic Vehicle"

// Dynamic Dispatch: The compiler does a v-table lookup to find Car's implementation.
print(Car.maxSpeed()) // Prints: "250"

Inheritance and Protocol Conformance

Have you considered what occurs when a subclass overrides a method that is also required by a protocol? Does Swift use the v-table or the PWT? The answer is: both, in a sense.

protocol Heatable {
  func heat()
}

class Appliance: Heatable {
  func heat() {
    print("The appliance is heating up.")
  }
}

class Toaster: Appliance {
  override func heat() { // 1
    print("The toaster is toasting bread.")
  }
}

let heater: any Heatable = Toaster() // 2
heater.heat() // Prints: "The toaster is toasting bread."

Generics vs Existentials

It’s one of Swift’s most powerful features. It helps you achieve reusability, flexibility, and type safety. With this, you can write code that avoids duplication.

struct Queue<Element> {
  private var elements: [Element]
  
  init(elements: [Element]) {
    self.elements = elements
  }
  
  mutating func enqueue(_ element: Element) {
    self.elements.append(element)
  }
  
  mutating func dequeue() -> Element? {
    guard !elements.isEmpty else { return nil }
    return elements.removeFirst()
  }
}
var idsQueue = Queue<Int>(elements: []) // A queue of Ids
idsQueue.enqueue(1)
idsQueue.enqueue(2)
        
var peopleQueue = Queue<String>(elements: []) // A queue of People
peopleQueue.enqueue("Steve")
peopleQueue.enqueue("Jobs")
protocol Animal {
  func sleep()
}

class Dog: Animal {
  func sleep() {
    print("Dog is sleeping.")
  }
}

func makeItSleep<T: Animal>(_ animal: T) {
  animal.sleep()
}

Protocol Composition

Sometimes, you may want a type to conform to multiple protocols. Protocol composition lets you express this by using the & operator.

protocol Flyable {
  func fly()
}

protocol Swimmable {
  func swim()
}

struct Duck: Flyable, Swimmable {
  func fly() { print("Flapping wings!") }
  func swim() { print("Paddling in the pond!") }
}

func makeItMove(_ creature: Flyable & Swimmable) { // 1
  creature.fly() // 2
  creature.swim() // 3
}

makeItMove(Duck()) // 4

Common Pitfalls

Even if you’ve been writing Swift for many years, it’s easy to stumble into subtle traps that protocols and dispatch can cause. Some of these incorrect behaviors and reduced performance may leave you scratching your head, wondering why they behave a certain way, why the compiler won’t let you do something that seems perfectly reasonable.

Default Methods in Protocol Extensions

The default implementation using extensions is a powerful way to leverage Swift’s flexibility. However, extensions behave differently in certain scenarios.

protocol Greeter {
  func greet()
}

extension Greeter {
  func greet() { print("Hello from default!") }
}

struct Person: Greeter {
  func greet() { print("Hello from Person!") }
}

let john = Person()
john.greet() // Hello from Person!

let greeter: Greeter = Person()
greeter.greet() // Hello from Person! 
protocol Greeter {}

extension Greeter {
  func greet() { print("Hello from default!") }
}

struct Person: Greeter {
  func greet() { print("Hello from Person!") }
}

let greeter: Greeter = Person()
greeter.greet() // Hello from default! 

Existentials with associatedtype

Even though Swift now allows you to create existentials with protocols having associatedtype or Self, an existential removes the type information tied to the associated type, which means you cannot directly call methods that depend on it.

protocol Logger {
  associatedtype Message
  func log(_ message: Message)
}

func testLogger(_ logger: any Logger) {
  logger.log("Hello") // Error — Message type is erased
}
func testLogger<T: Logger>(_ logger: T, message: T.Message) {
  logger.log(message)
}

Guide your Compiler

Don’t let your compiler struggle with simple tasks; guide it where possible. If you’re not using existentials, opt for opaque types instead.

func makeLogger() -> any Logger {
  ConsoleLogger()
}
func makeLogger() -> some Logger {
  ConsoleLogger()
}

Key Points

Challenge

AnyLogger Wrapper: Existential & Generic Versions

Create a wrapper called AnyLogger that can accept any type conforming to the Logger protocol defined earlier in the chapter. Your task is to implement two versions: one using an Existential, and the other using Generics.

Requirement

  1. Your Logger protocol should include at least one method: log(_ message: String).
  2. AnyLogger should work with both ConsoleLogger and FileLogger without modifying their implementations.

Example Usage

let consoleLogger = ConsoleLogger()
let fileLogger = FileLogger()

// Existential version
let anyExistentialLogger: AnyLogger = AnyLogger(consoleLogger)
anyExistentialLogger.log("Hello Existential!")

// Generic version
let anyGenericLogger = AnyLogger(consoleLogger) // Generic<T: Logger>
anyGenericLogger.log("Hello Generic!")

Let the logger game begin!

Where to Go From Here?

Now that you’ve explored dispatch, existentials, opaque types, and generics, you should have a clear understanding of how they operate and behave in different scenarios.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now