How do you create or retarget a class at run time, and why does Swift sometimes ignore it?
Predict first
A subclass overrides greet at run time and the instance is switched to it. The method is @objc dynamic. What does this print?
class Base: NSObject { @objc dynamic func greet() -> String { "base" } }
let sub: AnyClass = objc_allocateClassPair(Base.self, "RuntimeSub", 0)!
let block: @convention(block) (AnyObject) -> String = { _ in "runtime" }
class_addMethod(sub, #selector(Base.greet), imp_implementationWithBlock(block), "@@:")
objc_registerClassPair(sub)
let b = Base()
print(b.greet())
object_setClass(b, sub)
print(b.greet(), b.isKind(of: Base.self))
- base / base true
- base / runtime true
- base / runtime false
Predict first
The same program, but greet is only @objc, not dynamic. What does it print (the second line sends the message through perform)?
class Base: NSObject { @objc func greet() -> String { "base" } }
let sub: AnyClass = objc_allocateClassPair(Base.self, "RuntimeSub", 0)!
let block: @convention(block) (AnyObject) -> String = { _ in "runtime" }
class_addMethod(sub, #selector(Base.greet), imp_implementationWithBlock(block), "@@:")
objc_registerClassPair(sub)
let b = Base()
object_setClass(b, sub)
print(b.greet())
print(b.perform(#selector(Base.greet))!.takeUnretainedValue())
- runtime / runtime
- base / runtime
Mental model. object_setClass sends the same person back to a new job title. People who ask the HR directory see the new title; people with a memo from last year still treat them the old way.
The runtime builds classes in three steps. objc_allocateClassPair(super, name, 0) creates an unregistered subclass, class_addMethod (with a type encoding and an IMP, often from imp_implementationWithBlock) fills the method table, and objc_registerClassPair publishes it. After that object_setClass(obj, newClass) changes an existing instance’s isa, which is exactly what KVO does when you observe an object (it inserts a generated NSKVONotifying_… subclass), and what mock libraries and Core Data faults do. The new class must have the same instance layout as the old one, so a pair created with zero extra ivar bytes is safe.
Whether your new method runs depends on how the caller dispatches. Objective-C code and perform(_:) always look the method up through the isa, so they see the new class. A Swift call to a plain @objc method uses the class’s vtable or a direct call chosen at compile time and keeps running the old code. Only a dynamic member is guaranteed to go through objc_msgSend. This is the same rule as for swizzling, and it is the reason frameworks tell you to mark observed properties dynamic.
After the switch the table lookup finds the new method, and the object is still a kind of Base because the new class is its subclass.
The Swift call is bound at compile time and never looks at the table, so the override is ignored. perform(#selector(...)) would see it.
Common traps
- Registering the same class name twice.
objc_allocateClassPairreturns nil and force-unwrapping crashes. - Calling
objc_registerClassPairand then adding ivars. Ivars must be added before registration; methods can be added any time.
They might follow up with
- Why does KVO override
-classon its generated subclass? So that[obj class]still reports the original class, hiding the swap. - Is subclass creation at runtime safe for Swift classes? For
NSObjectsubclasses with the same layout, yes; pure Swift classes have no such support.
Related questions
- 4.1.4 What is isa, and why does your object’s runtime class sometimes differ from its Swift type?
- 4.6.1 How does method swizzling work, and why does it need dynamic in Swift?
- 4.10.2 What is a method type encoding, and why does a wrong signature crash instead of failing to compile?
← What is an IMP, and how can you call one without sending a message? Why can’t Swift catch an NSException, and how do you handle one… →