InterposeKit is a modern library for hooking Objective-C methods in Swift, also known as method swizzling. It supports both class-based and object-based hooks, and it provides a clean, block-based, Swift-friendly API.
This is a continuation and modernization of Peter Steinberger’s original implementation. If you’re migrating, check out what’s changed.
- Swift-friendly, modern, and minimal API.
- Block-based hooks using direct
Method
implementation replacement under the hood rather than less-safe selector-based swizzling. - Ability to target both classes and individual objects.
- Support for both instance and class methods.
- Object hooks are safely isolated using runtime subclassing, similar to the KVO mechanism.
- Hooks get access to the original method implementation via a proxy.
- Hooks can be applied immediately or prepared and applied later, and safely reverted at any time1.
- Typed signatures must be explicitly provided for both the original method and the hook block. This adds some boilerplate but ensures a clean API and better performance compared to using
NSInvocation
2. - Written almost entirely in Swift on top of the Objective-C runtime3.
- Swift 5.9 or later
- Xcode 15 or later
- Apple platforms only (macOS, iOS, tvOS, watchOS)
arm64
orx86_64
architectures
You can add InterposeKit to your project using the Swift Package Manager.
In Xcode, open your project settings, select the Package Dependencies tab, click the + button, and enter the URL https://github.com/structuredpath/InterposeKit
. Then select the latest version and add the package to your desired target.
If you’re adding InterposeKit using a Package.swift
manifest, include it in your dependencies
like this:
dependencies: [
.package(url: "https://github.com/structuredpath/InterposeKit", from: "1.0.0")
]
Then add the product to any target that needs it:
.target(
name: "YourTarget",
dependencies: [
.product(name: "InterposeKit", package: "InterposeKit")
]
)
class MyClass: NSObject {
@objc dynamic func getValue() -> Int {
return 42
}
}
let object = MyClass()
print(object.getValue()) // => 42
let hook = try Interpose.applyHook(
on: MyClass.self,
for: #selector(MyClass.getValue),
methodSignature: (@convention(c) (MyClass, Selector) -> Int).self,
hookSignature: (@convention(block) (MyClass) -> Int).self
) { hook in
return { `self` in
// Retrieve the original result and add 1 to it. This can be skipped.
return hook.original(`self`, hook.selector) + 1
}
}
print(object.getValue()) // => 43
try hook.revert()
print(object.getValue()) // => 42
class MyClass: NSObject {
@objc dynamic class func getStaticValue() -> Int {
return 42
}
}
print(MyClass.getStaticValue()) // => 42
let hook = try Interpose.applyHook(
on: MyClass.self,
for: #selector(MyClass.getStaticValue),
methodKind: .class,
methodSignature: (@convention(c) (MyClass.Type, Selector) -> Int).self,
hookSignature: (@convention(block) (MyClass.Type) -> Int).self
) { hook in
return { `class` in
// Retrieve the original result and add 1 to it. This can be skipped.
return hook.original(`class`, hook.selector) + 1
}
}
print(MyClass.getStaticValue()) // => 43
try hook.revert()
print(MyClass.getStaticValue()) // => 42
class MyClass: NSObject {
@objc dynamic func getValue() -> Int {
return 42
}
}
let object1 = MyClass()
let object2 = MyClass()
print(object1.getValue()) // => 42
print(object2.getValue()) // => 42
let hook = try Interpose.applyHook(
on: object1,
for: #selector(MyClass.getValue),
methodSignature: (@convention(c) (MyClass, Selector) -> Int).self,
hookSignature: (@convention(block) (MyClass) -> Int).self
) { hook in
return { `self` in
// Retrieve the original result and add 1 to it. This can be skipped.
return hook.original(`self`, hook.selector) + 1
}
}
print(object1.getValue()) // => 43
print(object2.getValue()) // => 42
try hook.revert()
print(object1.getValue()) // => 42
print(object2.getValue()) // => 42
Important
If the object is already being observed via KVO when you apply or revert the hook, the operation will fail safely by throwing InterposeError.kvoDetected(object:)
. Using KVO after the hook is installed is fully supported.
You can check out the extensive test suite to see more advanced examples. The repository also comes with an example Xcode project, which showcases more real-life examples of tweaking AppKit classes.
Compared to the original implementation, this fork introduces several API and internal changes. Here is a summary of key differences with migration hints.
- Switched the library to Swift Package Manager only. Carthage and CocoaPods support was removed.
- Raised minimum Swift version to 5.9.
- Limited to Apple platforms with
arm64
andx86_64
architectures. Support for Linux was removed.
- Class hooks now support class methods.
- Removed the builder-based API
Interpose(…) { … }
for applying hooks in batches. Each hook must now be individually prepared, applied, and reverted. - Signature types must now be specified via parameters, improving clarity and streamlining the API.
- Renamed
hook(…)
methods toapplyHook(…)
. - Removed fluent-style
Hook
API, meaning that methods no longer returnself
. - Introduced
HookProxy
, which is passed into the hook builder. It still provides access to the original method implementation and selector, but hides other irrelevant APIs likerevert()
. - Hook types now use composition instead of inheritance. The public
Hook
class delegates to internal strategies conforming toHookStrategy
. - Object hooks now use a global counter instead of UUIDs for dynamic subclass names.
- Dynamic subclasses created at runtime are now cleaned up when the last hook is reverted on an object.
- Class hooks must now target the exact class that actually implements the method to ensure the revert functionality works correctly.
- Added initial Swift 6 support with basic concurrency checks. Should be thread-safe but most usage is still expected to be single-threaded.
- Removed support for delayed hooking via
Interpose.whenAvailable(…)
to keep the library laser-focused. - …and heavily refactored the Swift part of the codebase: cleaner use of Objective-C runtime APIs, a revamped
InterposeError
enum, and new supporting types likeHookScope
orHookState
.
- Fixed a crash where
IKTAddSuperImplementationToClass
was stripped in release builds per steipete/InterposeKit#29 by using the fix from steipete/InterposeKit#30 submitted by @Thomvis, which replaces a call via dynamic library with a direct Swift call toIKTSuperBuilder.addSuperInstanceMethod(to:selector:)
. - Fixed floating-point register handling on arm64 using the patch from steipete/InterposeKit#37 submitted by @ishutinvv, which resolves an issue affecting swizzled methods with
CGFloat
parameters or structs likeCGPoint
andCGRect
due to floating-point registers not being restored in the correct order after the trampoline call.
Peter originally wanted to go with Interpose, but Swift had (and still has) a bug where using the same name for a module and a type can break things in certain scenarios.
UIKit, AppKit, and other system frameworks written in Objective-C won’t go away and sometimes you still need to swizzle to fix bugs or tweak internal behavior. InterposeKit is meant as a rarely needed tool for these cases, providing a simple, Swift-friendly API.
This version of InterposeKit started as a fork of Peter Steinberger’s original library but has since evolved into a significantly reworked and modernized version. The core ideas and underlying runtime techniques remain, but large parts of the Swift codebase were restructured and rewritten.
No. Peter had plans to experiment with Swift method hooking, Swift dynamic function replacement, and hooking C functions via dyld_dynamic_interpose
, but none of these made it into InterposeKit, and frankly, they wouldn’t really fit the scope of this library anyway.
Modifying the internal behavior of system frameworks always carries risks. You should know what you’re doing, use defensive programming techniques, and assume that future OS updates might break your hooks.
That said, InterposeKit is designed to be safe for production use. It includes guardrails that verify the method state before applying or reverting hooks, helping catch unexpected conditions early. The focus is on simplicity and predictability, avoiding clever tricks that could introduce hidden side effects.
- Support for hooking KVO-enabled objects (#19)
- Correct super lookup when injecting a trampoline into classes with overridden methods (#21)
- Signature type checking at hook construction (#20)
- A way for retrieving all hooks on an object/class
- Support for reverting multiple hooks on a class in an arbitrary order (#12)
- Peter’s original implementation
- Peter’s introductory blog post
- Peter’s blog post on swizzling in Swift
- Peter’s blog post on calling super at runtime
- Aspects - Objective-C predecessor to InterposeKit
- NSHipster on method swizzling
- Stack Overflow: How do I remove instance methods at runtime in Objective-C 2.0?
This library is released under the MIT license. See LICENSE for details.
Footnotes
-
Both applying and reverting a hook include safety checks. InterposeKit detects if the method was modified externally—such as by KVO or other swizzling—and prevents the operation if it would lead to inconsistent behavior. ↩
-
There’s currently no runtime type checking. If the specified types don’t match, it will cause a runtime crash. ↩
-
The most advanced part of this library is
ITKSuperBuilder
, a component for constructing method implementations that simply callsuper
, which is surprisingly hard to do. It’s written in Objective-C and assembly, lives in its own SPM target, and is invoked from Swift. All credit goes to Peter, who originally came up with this masterpiece! ↩