RuntimeKit is a Swift wrapper around the Objective-C runtime. It provides an intuitive Swift API for inspecting and manipulating Objective-C classes and protocols dynamically.
RuntimeKit does not add any reflection to Swift-only types.
It only operates on types that are already visible to the runtime,
namely classes and protocols defined in Objective-C or exposed from Swift using @objc
.
Here's how you would normally get all the property names of an Objective-C class in Swift without RuntimeKit:
let cls = ExampleClass.self
var count: UInt32 = 0
guard let props = class_copyPropertyList(cls, &count) else {
return []
}
var names: [String] = []
for n in 0..<count {
let prop = props[Int(n)]
let nameCStr = property_getName(prop)
let name = String(cString: nameCStr)
names.append(name)
}
free(props)
return names
With RuntimeKit this becomes as simple as:
ObjCClass(ExampleClass.self)
.properties
.map(\.name)
Each of the main Objective-C runtime types are wrapped by a corresponding RuntimeKit type:
Just wrap an AnyClass
or Protocol
to start introspecting it:
let cls = ObjCClass(NSMeasurement.self)
print(cls.superclass) // NSObject
print(cls.isRootClass) // false
print(cls.protocols) // NSCopying, NSSecureCoding
print(cls.properties) // unit, doubleValue
print(cls.ivars) // _unit, _doubleValue
print(cls.methods) // canBeConvertedToUnit:, measurementByAddingMeasurement:, hash, isEqual: ...
print(cls.classMethods) // supportsSecureCoding
Properties, methods, ivars and protocols can be iterated using standard for-in
loop syntax:
for prop in cls.properties {
print(prop.name, prop.attributes.encoding)
}
// unit @"NSUnit"
// doubleValue d
Or accessed by name using subscripts:
let ivar = cls.ivars["_unit"]!
print(ivar.offset) // 8
Every class and protocol known to the runtime can also be iterated:
for cls in ObjCClass.allClasses {
// ...
}
for proto in ObjCProtocol.allProtocols {
// ...
}
Normally only the direct members of the wrapped class are returned.
To access the members inherited from superclasses, use the upTo
function:
let cls = ObjCClass(NSMutableArray.self)
// Only methods directly on NSMutableArray:
print(cls.methods) // 122 methods
// Include methods on superclasses up to but excluding NSObject.
// i.e. NSMutableArray + NSArray:
print(cls.methods.upTo(NSObject.self)) // 552 methods
// Include methods on all superclasses.
// i.e. NSMutableArray + NSArray + NSObject:
print(cls.methods.upTo(nil)) // 998 methods
Properties, methods and protocols can be attached to any class at runtime:
let cls = ObjCClass(NSObject.self)
let obj = NSObject()
// obj.value(forKey: "foo")
// -- would crash with: class is not key value coding-compliant for the key foo.
// Create a method body
let body: @convention(block) (AnyClass) -> Int = { _ in
return 123
}
// Dynamically add it to the class
cls.methods.add(with: "foo", types: "q@:", block: body)
// Add a corresponding property (not strictly necessary)
cls.properties.add("foo", attributes: .init(
nonAtomic: true,
readOnly: true,
encoding: "q"
))
// Now this will succeed
print(obj.value(forKey: "foo")) // 123
Exchange the implementations of two methods:
cls.methods.swizzle("foo", with: "bar")
// or:
let m1 = cls.methods["foo"]!
let m2 = cls.methods["bar"]!
m1.swizzle(with: m2)
// or replace an implementation directly:
m1.implementationBlock = { _ in
return 999
} as @convention(block) (AnyClass) -> Int
Entirely new classes can be defined at runtime:
let newCls = ObjCClass.create(
"MyClass",
superclass: cls,
ivars: [
.init(name: "_foo", encoding: "i"),
.init(name: "_bar", encoding: "c"),
]
)
// add any properties, methods, protocols...
Swift wrappers for NSInvocation
and NSMethodSignature
are also included, with strongly typed accessors for arguments and return types.
RuntimeKit is designed to have almost no overhead compared to calling the Objective-C runtime functions directly.
- The wrapper types (
ObjCClass
etc) are single-member structs with zero cost to create. - Most functions are
@inlinable
thus have zero additional cost to call.
An exception is where C strings are converted to String
but to minimise this, methods are referenced by Selector
instead of name.
Class and protocol members (properties, methods, etc) are returned as Sequence
notArray
so no heap allocations are required.
The performance of iterating these using for-in
loops is close to that of using the runtime functions directly.
However if maximum performance is needed (e.g. iterating every property of every class), use the alternate forEach
method:
// Fast:
for prop in cls.properties {
// ...
}
// Fastest:
cls.properties.forEach { prop in
// ...
}
Avoid using count
on these member iterators if you plan to loop through them anyway, as it requires fetching all the items from the runtime.
See my post on iterator performance for all the details.
Add RuntimeKit to your Package.swift
:
Package(
// ...
dependencies: [
.package(url: "https://github.com/nicked/RuntimeKit.git", from: "0.1.0"),
],
targets: [
.target(
// ...
dependencies: ["RuntimeKit"]
),
]
)