Skip to content

Commit d1948cd

Browse files
committed
Improved API of hook method on NSObject
1 parent 2f611e5 commit d1948cd

File tree

4 files changed

+156
-64
lines changed

4 files changed

+156
-64
lines changed

Sources/InterposeKit/InterposeKit.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,5 @@
11
import Foundation
22

3-
extension NSObject {
4-
/// Hook an `@objc dynamic` instance method via selector on the current object or class..
5-
@discardableResult public func hook<MethodSignature, HookSignature> (
6-
_ selector: Selector,
7-
methodSignature: MethodSignature.Type = MethodSignature.self,
8-
hookSignature: HookSignature.Type = HookSignature.self,
9-
_ implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?) throws -> AnyHook {
10-
11-
if let klass = self as? AnyClass {
12-
return try Interpose.ClassHook(class: klass, selector: selector, implementation: implementation).apply()
13-
} else {
14-
return try Interpose.ObjectHook(object: self, selector: selector, implementation: implementation).apply()
15-
}
16-
}
17-
18-
/// Hook an `@objc dynamic` instance method via selector on the current object or class..
19-
@discardableResult public class func hook<MethodSignature, HookSignature> (
20-
_ selector: Selector,
21-
methodSignature: MethodSignature.Type = MethodSignature.self,
22-
hookSignature: HookSignature.Type = HookSignature.self,
23-
_ implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?) throws -> AnyHook {
24-
return try Interpose.ClassHook(class: self as AnyClass,
25-
selector: selector, implementation: implementation).apply()
26-
}
27-
}
28-
293
/// Interpose is a modern library to swizzle elegantly in Swift.
304
///
315
/// Methods are hooked via replacing the implementation, instead of the usual exchange.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import ObjectiveC
2+
3+
extension NSObject {
4+
5+
/// Installs a hook for the specified Objective-C selector on this object instance.
6+
///
7+
/// This method replaces the implementation of an Objective-C instance method with
8+
/// a block-based implementation, while optionally allowing access to the original
9+
/// implementation.
10+
///
11+
/// To be hookable, the method must be exposed to the Objective-C runtime. When written
12+
/// in Swift, it must be marked `@objc dynamic`.
13+
///
14+
/// - Parameters:
15+
/// - selector: The selector of the instance method to hook.
16+
/// - methodSignature: The expected C function type of the original method implementation.
17+
/// - hookSignature: The type of the replacement block.
18+
/// - implementation: A closure that receives a `TypedHook` and returns the replacement
19+
/// implementation block.
20+
///
21+
/// - Returns: An `AnyHook` representing the installed hook, allowing to remove the hook later
22+
/// by calling `hook.revert()`.
23+
///
24+
/// - Throws: An error if the hook could not be applied, such as if the method does not exist
25+
/// or is not implemented in Objective-C.
26+
///
27+
/// ### Example
28+
///
29+
/// ```swift
30+
/// let hook = try object.addHook(
31+
/// for: #selector(MyClass.someMethod),
32+
/// methodSignature: (@convention(c) (NSObject, Selector, Int) -> Void).self,
33+
/// hookSignature: (@convention(block) (NSObject, Int) -> Void).self
34+
/// ) { hook in
35+
/// return { `self`, parameter in
36+
/// hook.original(self, hook.selector, parameter)
37+
/// }
38+
/// }
39+
///
40+
/// hook.revert()
41+
/// ```
42+
@discardableResult
43+
public func addHook<MethodSignature, HookSignature>(
44+
for selector: Selector,
45+
methodSignature: MethodSignature.Type,
46+
hookSignature: HookSignature.Type,
47+
implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?
48+
) throws -> AnyHook {
49+
try Interpose.ObjectHook(
50+
object: self,
51+
selector: selector,
52+
implementation: implementation
53+
).apply()
54+
}
55+
56+
}
57+
58+
extension NSObject {
59+
60+
@available(*, deprecated, renamed: "addHook(for:methodSignature:hookSignature:using:)")
61+
@discardableResult
62+
public func hook<MethodSignature, HookSignature> (
63+
_ selector: Selector,
64+
methodSignature: MethodSignature.Type = MethodSignature.self,
65+
hookSignature: HookSignature.Type = HookSignature.self,
66+
_ implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?
67+
) throws -> AnyHook {
68+
precondition(
69+
!(self is AnyClass),
70+
"There should not be a way to cast an NSObject to AnyClass."
71+
)
72+
73+
return try self.addHook(
74+
for: selector,
75+
methodSignature: methodSignature,
76+
hookSignature: hookSignature,
77+
implementation: implementation
78+
)
79+
}
80+
81+
@available(*, deprecated, message: """
82+
Deprecated to avoid confusion: this hooks instance methods on classes, but can be mistaken \
83+
for hooking class methods, which is not supported. Use `Interpose(Class.self)` with \
84+
`prepareHook(…)` to make the intent explicit.
85+
""")
86+
@discardableResult
87+
public class func hook<MethodSignature, HookSignature> (
88+
_ selector: Selector,
89+
methodSignature: MethodSignature.Type = MethodSignature.self,
90+
hookSignature: HookSignature.Type = HookSignature.self,
91+
_ implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?
92+
) throws -> AnyHook {
93+
return try Interpose.ClassHook(
94+
class: self as AnyClass,
95+
selector: selector,
96+
implementation: implementation
97+
).apply()
98+
}
99+
100+
}

Tests/InterposeKitTests/MultipleInterposing.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ final class MultipleInterposingTests: InterposeKitTestCase {
2525
XCTAssertEqual(testObj.sayHi(), testClassHi + testString)
2626
XCTAssertEqual(testObj2.sayHi(), testClassHi)
2727

28-
try testObj.hook(
29-
#selector(TestClass.sayHi),
28+
try testObj.addHook(
29+
for: #selector(TestClass.sayHi),
3030
methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
31-
hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in
32-
return store.original(bSelf, store.selector) + testString2
33-
}
31+
hookSignature: (@convention(block) (AnyObject) -> String).self
32+
) { hook in
33+
return { `self` in
34+
return hook.original(self, hook.selector) + testString2
35+
}
3436
}
3537

3638
XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testString2)

Tests/InterposeKitTests/ObjectInterposeTests.swift

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ final class ObjectInterposeTests: InterposeKitTestCase {
1111
XCTAssertEqual(testObj.sayHi(), testClassHi)
1212
XCTAssertEqual(testObj2.sayHi(), testClassHi)
1313

14-
let hook = try testObj.hook(
15-
#selector(TestClass.sayHi),
14+
let hook = try testObj.addHook(
15+
for: #selector(TestClass.sayHi),
1616
methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
17-
hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in
18-
print("Before Interposing \(bSelf)")
19-
let string = store.original(bSelf, store.selector)
20-
print("After Interposing \(bSelf)")
17+
hookSignature: (@convention(block) (AnyObject) -> String).self
18+
) { hook in
19+
return { `self` in
20+
print("Before Interposing \(self)")
21+
let string = hook.original(self, hook.selector)
22+
print("After Interposing \(self)")
2123
return string + testString
22-
}
24+
}
2325
}
2426

2527
XCTAssertEqual(testObj.sayHi(), testClassHi + testString)
@@ -38,11 +40,14 @@ final class ObjectInterposeTests: InterposeKitTestCase {
3840
let returnIntOverrideOffset = 2
3941
XCTAssertEqual(testObj.returnInt(), returnIntDefault)
4042

41-
let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook
42-
<@convention(c) (AnyObject, Selector) -> Int,
43-
@convention(block) (AnyObject) -> Int>) in {
44-
let int = store.original($0, store.selector)
45-
return int + returnIntOverrideOffset
43+
let hook = try testObj.addHook(
44+
for: #selector(TestClass.returnInt),
45+
methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self,
46+
hookSignature: (@convention(block) (AnyObject) -> Int).self
47+
) { hook in
48+
return { `self` in
49+
let int = hook.original(self, hook.selector)
50+
return int + returnIntOverrideOffset
4651
}
4752
}
4853

@@ -66,11 +71,14 @@ final class ObjectInterposeTests: InterposeKitTestCase {
6671
XCTAssertEqual(testObj.returnInt(), returnIntDefault)
6772

6873
// Functions need to be `@objc dynamic` to be hookable.
69-
let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook
70-
<@convention(c) (AnyObject, Selector) -> Int,
71-
@convention(block) (AnyObject) -> Int>) in {
74+
let hook = try testObj.addHook(
75+
for: #selector(TestClass.returnInt),
76+
methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self,
77+
hookSignature: (@convention(block) (AnyObject) -> Int).self
78+
) { hook in
79+
return { `self` in
7280
// You're free to skip calling the original implementation.
73-
store.original($0, store.selector) + returnIntOverrideOffset
81+
hook.original(self, hook.selector) + returnIntOverrideOffset
7482
}
7583
}
7684
XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset)
@@ -102,11 +110,13 @@ final class ObjectInterposeTests: InterposeKitTestCase {
102110
XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3)
103111

104112
// Functions need to be `@objc dynamic` to be hookable.
105-
let hook = try testObj.hook(#selector(TestClass.calculate)) { (store: TypedHook
106-
<@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int,
107-
@convention(block) (AnyObject, Int, Int, Int) -> Int>) in {
108-
// You're free to skip calling the original implementation.
109-
let orig = store.original($0, store.selector, $1, $2, $3)
113+
let hook = try testObj.addHook(
114+
for: #selector(TestClass.calculate),
115+
methodSignature: (@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int).self,
116+
hookSignature: (@convention(block) (AnyObject, Int, Int, Int) -> Int).self
117+
) { hook in
118+
return {
119+
let orig = hook.original($0, hook.selector, $1, $2, $3)
110120
return orig + 1
111121
}
112122
}
@@ -121,11 +131,14 @@ final class ObjectInterposeTests: InterposeKitTestCase {
121131
var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6)
122132

123133
// Functions need to be `@objc dynamic` to be hookable.
124-
let hook = try testObj.hook(#selector(TestClass.calculate2)) { (store: TypedHook
125-
<@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int,
126-
@convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int>) in {
134+
let hook = try testObj.addHook(
135+
for: #selector(TestClass.calculate2),
136+
methodSignature: (@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int).self,
137+
hookSignature: (@convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int).self
138+
) { hook in
139+
return {
127140
// You're free to skip calling the original implementation.
128-
let orig = store.original($0, store.selector, $1, $2, $3, $4, $5, $6)
141+
let orig = hook.original($0, hook.selector, $1, $2, $3, $4, $5, $6)
129142
return orig + 1
130143
}
131144
}
@@ -140,10 +153,13 @@ final class ObjectInterposeTests: InterposeKitTestCase {
140153
XCTAssertEqual(testObj.doubleString(string: str), str + str)
141154

142155
// Functions need to be `@objc dynamic` to be hookable.
143-
let hook = try testObj.hook(#selector(TestClass.doubleString)) { (store: TypedHook
144-
<@convention(c) (AnyObject, Selector, String) -> String,
145-
@convention(block) (AnyObject, String) -> String>) in {
146-
store.original($0, store.selector, $1) + str
156+
let hook = try testObj.addHook(
157+
for: #selector(TestClass.doubleString),
158+
methodSignature: (@convention(c) (AnyObject, Selector, String) -> String).self,
159+
hookSignature: (@convention(block) (AnyObject, String) -> String).self
160+
) { hook in
161+
return { `self`, parameter in
162+
hook.original(self, hook.selector, parameter) + str
147163
}
148164
}
149165
XCTAssertEqual(testObj.doubleString(string: str), str + str + str)
@@ -155,8 +171,8 @@ final class ObjectInterposeTests: InterposeKitTestCase {
155171
let object = TestClass()
156172
XCTAssertEqual(object.getPoint(), CGPoint(x: -1, y: 1))
157173

158-
let hook = try object.hook(
159-
#selector(TestClass.getPoint),
174+
let hook = try object.addHook(
175+
for: #selector(TestClass.getPoint),
160176
methodSignature: (@convention(c) (NSObject, Selector) -> CGPoint).self,
161177
hookSignature: (@convention(block) (NSObject) -> CGPoint).self
162178
) { hook in
@@ -182,8 +198,8 @@ final class ObjectInterposeTests: InterposeKitTestCase {
182198
CGPoint(x: 1, y: 1)
183199
)
184200

185-
let hook = try object.hook(
186-
#selector(TestClass.passthroughPoint(_:)),
201+
let hook = try object.addHook(
202+
for: #selector(TestClass.passthroughPoint(_:)),
187203
methodSignature: (@convention(c) (NSObject, Selector, CGPoint) -> CGPoint).self,
188204
hookSignature: (@convention(block) (NSObject, CGPoint) -> CGPoint).self
189205
) { hook in

0 commit comments

Comments
 (0)