ObjCRuntimeTools is a Swift library that provides powerful tools for working with Objective-C runtime features, including property association and method swizzling. It leverages modern Swift macros to simplify and enhance the use of these runtime capabilities.
- Property Association: easily associate properties with Objective-C objects using the
@Associated
macro. - Swizzling: perform type-safe and boilerplate-free runtime swizzling with the
#swizzle
macro.
Add ObjCRuntimeTools
to your Swift Package Manager dependencies:
.package(url: "https://github.com/davdroman/objc-runtime-tools", from: "0.1.0"),
Then, add the dependency to your desired target:
.product(name: "ObjCRuntimeTools", package: "objc-runtime-tools"),
Use the @Associated
macro to associate properties with Objective-C objects:
class MyClass: NSObject {}
extension MyClass {
@Associated(.retain(.nonatomic))
var associatedProperty: String = "Default Value"
}
Use the #swizzle
macro to perform function, getter, or setter swizzling.
try #swizzle(UIViewController.viewDidLoad) { $self in
print("Before")
self.viewDidLoad()
print("After")
}
try #swizzle(UIViewController.viewDidAppear, param: Bool.self) { $self, animated in
print("Before")
self.viewDidAppear(animated)
print("After")
}
try #swizzle(UITextField.resignFirstResponder, returning: Bool.self) { $self in
print("Before")
let result = self.resignFirstResponder()
print("After")
return result
}
try #swizzle(
UIScrollView.touchesShouldBegin,
params: Set<UITouch>.self, UIEvent?.self, UIView.self,
returning: Bool.self
) { $self, touches, event, view in
print("Before")
let result = self.touchesShouldBegin(touches, with: event, in: view)
print("After")
return result
}
try #swizzle(getter: \UIView.isHidden, returning: Bool.self) { $self in
print("Before")
let isHidden = self.isHidden
print("After")
return isHidden
}
try #swizzle(setter: \UIView.isHidden, param: Bool.self) { $self, isHidden in
print("Before")
self.isHidden = isHidden
print("After")
}
It's up to you to ensure that the swizzling is done only once. You can use a static variable to track whether the swizzling has already been performed:
class MyClass: NSObject {
static var isSwizzled = false
static func swizzleOnce() throws {
guard !MyClass.isSwizzled else { return }
MyClass.isSwizzled = true
try #swizzle(MyClass.doSomething) { $self in
print("Before")
self.doSomething()
print("After")
}
}
}
However this is error prone, noisy and not very maintainable, especially if you have multiple swizzling points. Not to mention Swift 6's strict concurrency rules, which will make this approach even more difficult because static variables aren't thread-safe by default.
A better approach is to use a macro such as #once
which ensures any action is executed only once, regardless of how many times it is called over the lifetime of the app.
class MyClass: NSObject {
static func swizzleOnce() throws {
try #once {
try #swizzle(MyClass.doSomething) { $self in
print("Before")
self.doSomething()
print("After")
}
}
}
}
This library is inspired by and builds upon the following projects: