Method dispatch is one of those low-level Swift concepts that quietly influences performance, architecture, and API design. Although most apps function perfectly without thinking about dispatch, understanding how Swift decides which method implementation to call helps you write more predictable, efficient, and maintainable code—especially when building frameworks, reusable components, or performance-sensitive features.
This article explains:
The goal is not to optimize prematurely, but to build a correct mental model of how Swift executes your code.
In Swift, dispatch refers to how the compiler and runtime determine which function implementation to execute when a method is called.
Consider this example:
protocol Greeter {
func greet() -> String
}
struct EnglishGreeter: Greeter {
func greet() -> String {
"Hello"
}
}
let greeter: Greeter = EnglishGreeter()
print(greeter.greet())At the call site (greeter.greet()), Swift must decide which concrete greet() implementation to run. That decision can be made in two ways:
Understanding when each happens is key to understanding Swift’s performance and abstraction model.
Static dispatch means the compiler knows the exact method implementation at compile time and generates a direct call to that function. There is no runtime lookup.
struct Counter {
func increment(_ value: Int) -> Int {
value + 1
}
}
let counter = Counter()
print(counter.increment(10))Because Counter is a value type and cannot be subclassed, Swift knows exactly which increment(_:) method to call. The compiler can inline and optimize the call aggressively.
This is one reason Swift encourages the use of structs and enums for modeling data and behavior.
final Classesfinal class Logger {
func log(_ message: String) {
print(message)
}
}
let logger = Logger()
logger.log("Hello")Marking a class as final tells the compiler that the method cannot be overridden. This allows Swift to use static dispatch even for class methods.
Static dispatch enables the compiler to:
In many cases, the call disappears entirely after optimization.
Dynamic dispatch means the method implementation is chosen at runtime based on the actual type of the instance. This is required when Swift cannot know the concrete implementation at compile time.
Dynamic dispatch enables polymorphism but introduces runtime overhead.
class Animal {
func speak() -> String {
"..."
}
}
class Dog: Animal {
override func speak() -> String {
"Woof"
}
}
let animal: Animal = Dog()
print(animal.speak())Here:
AnimalDogSwift must determine the correct method implementation at runtime.
This uses Swift’s class dispatch mechanism (virtual method tables).
protocol Storage {
func save()
}
struct DiskStorage: Storage {
func save() {
print("Saved to disk")
}
}
let storage: Storage = DiskStorage()
storage.save()When a protocol is used as a type, Swift erases the concrete type information.
The method call is resolved at runtime using a lookup mechanism.
This flexibility allows you to write abstraction-oriented code, but comes with a small runtime cost.
Protocols in Swift can participate in either static or dynamic dispatch depending on how they are used.
protocol Greeter {
func greet() -> String
}
struct EnglishGreeter: Greeter {
func greet() -> String { "Hello" }
}
func greet<G: Greeter>(_ greeter: G) {
print(greeter.greet())
}Because the compiler specializes this function for each concrete type, method calls are statically dispatched. This allows the compiler to inline and optimize aggressively.
func greet(_ greeter: Greeter) {
print(greeter.greet())
}Here, the concrete type is erased. Swift must perform runtime lookup to determine which implementation to call.
@objc and dynamic Change DispatchWhen you mark methods with @objc or dynamic, Swift opts into Objective-C runtime dispatch:
class Player: NSObject {
@objc dynamic func play() {
print("Playing")
}
}
let player = Player()
player.play()This uses:
This is slower than Swift v-table dispatch and should be used only when you need:
| Pattern | Dispatch Type |
|---|---|
Generic constraint (<T: P>) |
Static dispatch |
Protocol as a type (P) |
Dynamic dispatch |
| Struct / enum methods | Static dispatch |
| Final class methods | Static dispatch |
| Overridable class methods | Dynamic dispatch |
Swift follows a few general rules:
Swift is designed to favor static dispatch whenever it is safe to do so.
Prefer:
final classes when subclassing is not intendedDynamic dispatch improves flexibility and architecture clarity.
Swift prefers static dispatch, but falls back to dynamic dispatch when runtime polymorphism is required.
Dynamic dispatch is slightly slower than static dispatch, but the cost is usually negligible in real applications. Architecture clarity often matters more than micro-optimizations.
Protocols are only dynamically dispatched when used as types.
Protocols used with generics are statically dispatched and highly optimized.
Dispatch is a foundational concept in Swift’s performance and abstraction model.
Understanding how and when Swift chooses static versus dynamic dispatch helps you:
You don’t need to optimize every call site—but knowing how dispatch works lets you make intentional design choices instead of accidental ones.
Thank you for reading. If you have any questions, feel free to follow me on X and send me a DM. If you enjoyed this article and would like to support my work, Buy me a coffee ☕️