Mastering Swift’s `dynamic` and `@_dynamicReplacement(for:)` for Dynamic Behaviour

👍
· Jan 23, 2025 · 6 mins read
Mastering Swift’s `dynamic` and `@_dynamicReplacement(for:)` for Dynamic Behaviour

Swift is primarily a statically dispatched language, but it does offer dynamic dispatch capabilities for times when flexibility trumps compile-time optimisation. In this article, we’ll dive into Swift’s **dynamic** keyword and the underscored **@_dynamicReplacement(for:)** attribute, which together enable a form of method swizzling in Swift.

Understanding the dynamic Keyword in Swift

In Swift, marking a method or property as dynamic tells the compiler to always use dynamic dispatch, which defers the decision of which implementation to call until runtime and enable implementation replacement.

Swift’s dynamic dispatch was tied to the Objective-C runtime before, but with latest versions, the Swift compiler no longer restricts dynamic to only @objc class members. We can mark methods, properties, subscripts, and even initializers as dynamic on classes, structs, enums, and extensions without needing Objective-C​. Under the hood, a dynamic declaration in Swift compiles to an indirect call via a function pointer (or dispatch table) that can be updated at runtime​.

Key points about **dynamic**:

  • It forces dynamic (late-bound) dispatch. If the entity is @objc, it uses Objective-C message dispatch; otherwise it uses Swift’s own dynamic dispatch table.
  • Marking something dynamic prevents certain optimizations (like inlining) and slightly impacts performance due to the indirection, so use it only when needed.
  • Cannot use dynamic on things the runtime can’t dispatch (e.g. top-level constants, or generic functions in some cases). It’s mostly for functions, computed properties, subscripts, and initializers that we intend to swap out or observe.

In short, dynamic in Swift is a promise that calls will be routed through a runtime lookup, which lays the groundwork for Swift’s own form of method replacement.

What is @_dynamicReplacement(for:)?

With the dynamic keyword making dynamic dispatch possible in pure Swift, the next piece is the @_dynamicReplacement(for:) attribute. This enables dynamic method replacement – essentially, a Swift-native equivalent of method swizzling.

Using @_dynamicReplacement, we can declare an alternative implementation for a function or computed property that will replace the original implementation at runtime. Unlike Objective-C’s method_exchangeImplementations (which can be called at any time), Swift’s dynamic replacement happens automatically at program launch or when a dynamic library is loaded, not arbitrarily in the middle of execution​.

How it works: When we mark a function as a replacement for another, the Swift runtime will swap out the implementation pointers before any code runs. All calls to the original (through dynamic dispatch) will then land in the new replacement. This is done in a type-safe manner – the compiler ensures the new replacement has the exact same signature as the original, and it routes calls appropriately.

Please note that if the new replacement calls the original function, it will invoke the original implementation (not recurse into itself)​. This allows the new replacement to either completely override or wrap the original behaviour.

Syntax and Requirements for @_dynamicReplacement

To use dynamic replacement, we need to follow a specific pattern. Both the original function and the replacement must obey certain rules:

  • Mark the original as **dynamic**: A function or property must be declared with dynamic for it to be replaceable. If we try to replace something not marked dynamic, nothing will happen.
  • Declare the replacement with **@_dynamicReplacement(for:)**: The replacement is typically declared in an extension of the same type (or at the global scope if replacing a global function). The replacement function itself should match the original’s signature exactly – same parameter types, return type, throws or async specifiers, mutating for structs, etc. Any mismatch will result in a compiler error​.
struct MyStruct {  
    dynamic var foo: String {  
        "foo"  
    }  
}  
  
extension MyStruct {  
    @_dynamicReplacement(for: foo)  
    var bar: String {  
        "bar"  
    }  
}  
  
print(MyStruct().foo) // will print out bar

In this example, bar will run instead of foo at runtime. Please don’t use this in Playground and this feature wouldn’t work there.

Both original and replacement must have adequate access levels so that the replacement can see the original declaration. Simply put, we can only replace the dynamic properties/methods that are accessible at the location where we define the replacements.

The Power of this Dynamic replacement

Swift’s @_dynamicReplacement(for:) (combined with the dynamic keyword) unlocks an incredibly powerful mechanism: compile-time swizzling for Swift-only code, without unsafe Objective-C hacks.

1. Seamless Implementation Swapping

We can completely change the behaviour of any dynamic method or property without touching the original source code — no branching, no subclassing.

dynamic func process() -> String {  
    "Original"  
}  
  
@_dynamicReplacement(for: process())  
func replacedProcess() -> String {  
    // process() call original if needed  
    return "Modified!"  
}

All existing call sites now route to replacedProcess()automatically. This can be used in many cases like Testing, Debugging etc.

2. Cross-File and Cross-Target Support

Replacement functions can live in extensions, other modules, or frameworks, as long as:

  • The original declaration is marked dynamic
  • Both modules see the same declaration at compile time (e.g., via a shared CoreModule)

This makes it perfect for layered app architectures and plugin systems.

3. Dynamically Loadable from .dylib

You can define a replacement in a .dylib and load it at runtime using dlopen().

let handle = dlopen("/path/to/libPatch.dylib", RTLD_NOW)

With one line of code above, magic happens instantly like below:

This works even if the dylib was not statically linked at build time, enabling:

  • Feature hot-swapping
  • White-label behaviour overrides
  • Live debugging or diagnostics injection
  • Patchable runtime bug fixes
  • Scripting/DSL engine integration

4. Safer than Objective-C Swizzling

  • Type-checked by the compiler
  • Works with structs, classes, properties, and even global functions
  • No need to expose things to the Objective-C runtime
  • Clear declaration of what’s being replaced

5. Only One Callsite, Many Behaviours

No need of changing caller side code but can inject different behaviours:

  • In Debug vs Release
  • At compile-time via flags
  • At runtime via dlopen
  • Per environment (e.g., staging vs production)

Conclusion

Swift’s dynamic and @_dynamicReplacement(for:) give us a powerful mechanism to alter program behavior at runtime in a controlled, type-safe manner.

At this moment, this is still a private attribute. As Swift continues to evolve, we may see @_dynamicReplacement become an official feature. Until then, it’s a sort of power tool: extremely useful in the right situation, but one to use with care.

Comments

Sharing is caring!