davdroman/objc-runtime-tools
Swift macros for Obj-C runtime property association and swizzling
Features
- Property Association: easily associate properties with Objective-C objects using the
@Associatedmacro. - Swizzling: perform type-safe and boilerplate-free runtime swizzling with the
#swizzlemacro.
Installation
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"),Usage
Property Association
Use the @Associated macro to associate properties with Objective-C objects:
class MyClass: NSObject {}
extension MyClass {
@Associated(.retain(.nonatomic))
var associatedProperty: String = "Default Value"
}Swizzling
Use the #swizzle macro to perform function, getter, or setter swizzling.
Function 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
}Getter and Setter Swizzling
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")
}⚠️ Important: how to swizzle only once ⚠️
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")
}
}
}
}Credits
This library is inspired by and builds upon the following projects:
Package Metadata
Repository: davdroman/objc-runtime-tools
Stars: 8
Forks: 1
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
README: README.md