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 namesWith 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) // supportsSecureCodingProperties, 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 dOr accessed by name using subscripts:
let ivar = cls.ivars["_unit"]!
print(ivar.offset) // 8Every 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 methodsProperties, 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: .getter(for: .longLong), block: body)
// Add a corresponding property (not strictly necessary)
cls.properties.add("foo", attributes: .init(
nonAtomic: true,
readOnly: true,
encoding: .longLong
))
// Now this will succeed
print(obj.value(forKey: "foo")) // 123Exchange 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) -> IntEntirely 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...The included TypeEncoding enum can fully parse even the most complex of Objective-C type encodings.
For example, the encoding ^{CGPoint="x"d"y"d} can be parsed into:
.pointer(.struct(name: "CGPoint", [("x", .double), ("y", .double)]))A parsed TypeEncoding can also be re-encoded into an encoding string which is identical to the original.
This lossless round-trip encoding has been tested on 660,000+ type encodings from all the classes and protocols in macOS.
If you find an encoding from the runtime which cannot be parsed successfully, please file a bug.
The related MethodTypeEncodings struct represents the types encoded in a method signature.
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 (
ObjCClassetc) are single-member structs with zero cost to create. - Most functions are
@inlinablethus 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"]
),
]
)