From e9e963344e701babbbd674ea0189ba35b284851f Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 4 Apr 2025 20:17:29 +0200 Subject: [PATCH 1/6] Introduced MethodKind --- .../Deprecated/NSObject+Deprecated.swift | 2 +- Sources/InterposeKit/Hooks/Hook.swift | 19 ++++++++++----- .../ClassHookStrategy/ClassHookStrategy.swift | 5 +++- Sources/InterposeKit/Interpose.swift | 5 +++- Sources/InterposeKit/MethodKind.swift | 24 +++++++++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 Sources/InterposeKit/MethodKind.swift diff --git a/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift b/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift index d1ec95d..1c0a114 100644 --- a/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift +++ b/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift @@ -35,7 +35,7 @@ extension NSObject { _ build: @escaping HookBuilder ) throws -> Hook { let hook = try Hook( - target: .class(self), + target: .class(self, .instance), selector: selector, build: build ) diff --git a/Sources/InterposeKit/Hooks/Hook.swift b/Sources/InterposeKit/Hooks/Hook.swift index bce4c9f..974b1a0 100644 --- a/Sources/InterposeKit/Hooks/Hook.swift +++ b/Sources/InterposeKit/Hooks/Hook.swift @@ -71,13 +71,14 @@ public final class Hook { } switch target { - case .class(let `class`): + case let .class(`class`, methodKind): return ClassHookStrategy( class: `class`, + methodKind: methodKind, selector: selector, makeHookIMP: makeHookIMP ) - case .object(let object): + case let .object(object): return ObjectHookStrategy( object: object, selector: selector, @@ -238,10 +239,10 @@ extension Hook: CustomDebugStringConvertible { public enum HookScope { - /// The scope that targets all instances of the class. - case `class` + /// The scope that targets a method on a class type (instance or class method). + case `class`(MethodKind) - /// The scope that targets a specific instance of the class. + /// The scope that targets a specific object instance. case object(NSObject) } @@ -259,7 +260,13 @@ public enum HookState: Equatable { } +/// Represents the target of a hook operation—either a class type or a specific object instance. internal enum HookTarget { - case `class`(AnyClass) + + /// A hook targeting a method defined on a class, either an instance method or a class method. + case `class`(AnyClass, MethodKind) + + /// A hook targeting a method on a specific object instance. case object(NSObject) + } diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift b/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift index 8acb4a8..d9427c3 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift @@ -8,11 +8,13 @@ internal final class ClassHookStrategy: HookStrategy { internal init( `class`: AnyClass, + methodKind: MethodKind, selector: Selector, makeHookIMP: @escaping () -> IMP ) { self.class = `class` self.selector = selector + self.methodKind = methodKind self.makeHookIMP = makeHookIMP } @@ -21,9 +23,10 @@ internal final class ClassHookStrategy: HookStrategy { // ============================================================================ // internal let `class`: AnyClass - internal var scope: HookScope { .class } + internal var scope: HookScope { .class(self.methodKind) } internal let selector: Selector + private let methodKind: MethodKind private let makeHookIMP: () -> IMP // ============================================================================ // diff --git a/Sources/InterposeKit/Interpose.swift b/Sources/InterposeKit/Interpose.swift index a2f7c3d..0c5bcef 100644 --- a/Sources/InterposeKit/Interpose.swift +++ b/Sources/InterposeKit/Interpose.swift @@ -14,12 +14,13 @@ public enum Interpose { public static func prepareHook( on `class`: AnyClass, for selector: Selector, + methodKind: MethodKind = .instance, methodSignature: MethodSignature.Type, hookSignature: HookSignature.Type, build: @escaping HookBuilder ) throws -> Hook { try Hook( - target: .class(`class`), + target: .class(`class`, methodKind), selector: selector, build: build ) @@ -29,6 +30,7 @@ public enum Interpose { public static func applyHook( on `class`: AnyClass, for selector: Selector, + methodKind: MethodKind = .instance, methodSignature: MethodSignature.Type, hookSignature: HookSignature.Type, build: @escaping HookBuilder @@ -36,6 +38,7 @@ public enum Interpose { let hook = try prepareHook( on: `class`, for: selector, + methodKind: methodKind, methodSignature: methodSignature, hookSignature: hookSignature, build: build diff --git a/Sources/InterposeKit/MethodKind.swift b/Sources/InterposeKit/MethodKind.swift new file mode 100644 index 0000000..d35dbaa --- /dev/null +++ b/Sources/InterposeKit/MethodKind.swift @@ -0,0 +1,24 @@ +public enum MethodKind: Equatable { + + /// An instance method, e.g. `-[MyClass doSomething]`. + case instance + + /// A class method, e.g. `+[MyClass doSomething]`. + case `class` + +} + +extension MethodKind { + + /// Returns the Objective-C method prefix symbol for this kind, `-` for instance methods + /// and `+` for class methods. + internal var symbolPrefix: String { + switch self { + case .instance: + return "-" + case .class: + return "+" + } + } + +} From 3d1f7664c8ae7b36f3508de4fa5ddc741b4be0bf Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 4 Apr 2025 20:50:35 +0200 Subject: [PATCH 2/6] Implemented support for hooking class methods --- Sources/InterposeKit/Hooks/Hook.swift | 13 +--- Sources/InterposeKit/Hooks/HookScope.swift | 25 ++++++++ .../ClassHookStrategy/ClassHookStrategy.swift | 46 ++++++++++++--- .../ObjectHookStrategy.swift | 7 +++ Sources/InterposeKit/InterposeError.swift | 59 +++++++++++-------- Tests/InterposeKitTests/ClassHookTests.swift | 56 +++++++++++++++--- 6 files changed, 154 insertions(+), 52 deletions(-) create mode 100644 Sources/InterposeKit/Hooks/HookScope.swift diff --git a/Sources/InterposeKit/Hooks/Hook.swift b/Sources/InterposeKit/Hooks/Hook.swift index 974b1a0..fb9a31e 100644 --- a/Sources/InterposeKit/Hooks/Hook.swift +++ b/Sources/InterposeKit/Hooks/Hook.swift @@ -223,7 +223,8 @@ extension Hook: CustomDebugStringConvertible { case .failed: description += "Failed" } - description.append(" hook for -[\(self.class) \(self.selector)]") + let symbolPrefix = self.scope.methodKind.symbolPrefix + description.append(" hook for \(symbolPrefix)[\(self.class) \(self.selector)]") if case .object(let object) = self.scope { description.append(" on \(Unmanaged.passUnretained(object).toOpaque())") @@ -237,16 +238,6 @@ extension Hook: CustomDebugStringConvertible { } } -public enum HookScope { - - /// The scope that targets a method on a class type (instance or class method). - case `class`(MethodKind) - - /// The scope that targets a specific object instance. - case object(NSObject) - -} - public enum HookState: Equatable { /// The hook is ready to be applied. diff --git a/Sources/InterposeKit/Hooks/HookScope.swift b/Sources/InterposeKit/Hooks/HookScope.swift new file mode 100644 index 0000000..8beb6c6 --- /dev/null +++ b/Sources/InterposeKit/Hooks/HookScope.swift @@ -0,0 +1,25 @@ +import ObjectiveC + +public enum HookScope { + + /// The scope that targets a method on a class type (instance or class method). + case `class`(MethodKind) + + /// The scope that targets a specific object instance. + case object(NSObject) + +} + +extension HookScope { + + /// Returns the kind of the method targeted by the hook scope. + public var methodKind: MethodKind { + switch self { + case .class(let methodKind): + return methodKind + case .object: + return .instance + } + } + +} diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift b/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift index d9427c3..ceddf18 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift @@ -36,23 +36,41 @@ internal final class ClassHookStrategy: HookStrategy { private(set) internal var appliedHookIMP: IMP? private(set) internal var storedOriginalIMP: IMP? + // ============================================================================ // + // MARK: Target Class + // ============================================================================ // + + /// The target class resolved for the configured method kind. + /// + /// This is the class itself for instance methods, or the metaclass for class methods. + private lazy var targetClass: AnyClass = { + switch self.methodKind { + case .instance: + return self.class + case .class: + return object_getClass(self.class) + } + }() + // ============================================================================ // // MARK: Validation // ============================================================================ // internal func validate() throws { // Ensure that the method exists. - guard class_getInstanceMethod(self.class, self.selector) != nil else { + guard class_getInstanceMethod(self.targetClass, self.selector) != nil else { throw InterposeError.methodNotFound( class: self.class, + kind: self.methodKind, selector: self.selector ) } // Ensure that the class directly implements the method. - guard class_implementsInstanceMethod(self.class, self.selector) else { + guard class_implementsInstanceMethod(self.targetClass, self.selector) else { throw InterposeError.methodNotDirectlyImplemented( class: self.class, + kind: self.methodKind, selector: self.selector ) } @@ -65,17 +83,18 @@ internal final class ClassHookStrategy: HookStrategy { internal func replaceImplementation() throws { let hookIMP = self.makeHookIMP() - guard let method = class_getInstanceMethod(self.class, self.selector) else { + guard let method = class_getInstanceMethod(self.targetClass, self.selector) else { // This should not happen under normal circumstances, as we perform validation upon // creating the hook strategy, which itself checks for the presence of the method. throw InterposeError.methodNotFound( class: self.class, + kind: self.methodKind, selector: self.selector ) } guard let originalIMP = class_replaceMethod( - self.class, + self.targetClass, self.selector, hookIMP, method_getTypeEncoding(method) @@ -83,7 +102,8 @@ internal final class ClassHookStrategy: HookStrategy { // This should not happen under normal circumstances, as we perform validation upon // creating the hook strategy, which checks if the class directly implements the method. throw InterposeError.implementationNotFound( - class: self.class, + class: self.targetClass, + kind: self.methodKind, selector: self.selector ) } @@ -91,7 +111,10 @@ internal final class ClassHookStrategy: HookStrategy { self.appliedHookIMP = hookIMP self.storedOriginalIMP = originalIMP - Interpose.log("Replaced implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP) -> \(hookIMP)") + Interpose.log({ + let selector = "\(self.methodKind.symbolPrefix)[\(self.class) \(self.selector)]" + return "Replaced implementation for \(selector) IMP: \(originalIMP) -> \(hookIMP)" + }()) } internal func restoreImplementation() throws { @@ -104,15 +127,16 @@ internal final class ClassHookStrategy: HookStrategy { self.storedOriginalIMP = nil } - guard let method = class_getInstanceMethod(self.class, self.selector) else { + guard let method = class_getInstanceMethod(self.targetClass, self.selector) else { throw InterposeError.methodNotFound( class: self.class, + kind: self.methodKind, selector: self.selector ) } let previousIMP = class_replaceMethod( - self.class, + self.targetClass, self.selector, originalIMP, method_getTypeEncoding(method) @@ -121,12 +145,16 @@ internal final class ClassHookStrategy: HookStrategy { guard previousIMP == hookIMP else { throw InterposeError.revertCorrupted( class: self.class, + kind: self.methodKind, selector: self.selector, imp: previousIMP ) } - Interpose.log("Restored implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP)") + Interpose.log({ + let selector = "\(self.methodKind.symbolPrefix)[\(self.class) \(self.selector)]" + return "Restored implementation for \(selector) IMP: \(originalIMP)" + }()) } } diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift index 0c294fb..576f976 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift @@ -50,6 +50,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard class_getInstanceMethod(self.class, self.selector) != nil else { throw InterposeError.methodNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -58,6 +59,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard self.lookUpIMP() != nil else { throw InterposeError.implementationNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -92,6 +94,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard let method = class_getInstanceMethod(self.class, self.selector) else { throw InterposeError.methodNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -137,6 +140,7 @@ internal final class ObjectHookStrategy: HookStrategy { // or an existing hook. throw InterposeError.implementationNotFound( class: subclass, + kind: .instance, selector: self.selector ) } @@ -171,6 +175,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard let method = class_getInstanceMethod(self.class, self.selector) else { throw InterposeError.methodNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -178,6 +183,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard let currentIMP = class_getMethodImplementation(dynamicSubclass, self.selector) else { throw InterposeError.implementationNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -194,6 +200,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard previousIMP == hookIMP else { throw InterposeError.revertCorrupted( class: dynamicSubclass, + kind: .instance, selector: self.selector, imp: previousIMP ) diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index 0211b67..ca3486b 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -5,35 +5,38 @@ public enum InterposeError: Error, @unchecked Sendable { /// A hook operation failed and the hook is no longer usable. case hookInFailedState - /// No instance method found for the selector on the specified class. + /// No method of the given kind found for the selector on the specified class. /// - /// This typically occurs when mistyping a stringified selector or attempting to interpose - /// a class method, which is not supported. + /// This typically occurs when mistyping a stringified selector or attempting to hook + /// a method that does not exist on the class. case methodNotFound( class: AnyClass, + kind: MethodKind, selector: Selector ) - - /// The method for the selector is not directly implemented on the specified class - /// but inherited from a superclass. + + /// The method for the selector is inherited from a superclass rather than directly implemented + /// by the specified class. /// - /// Class-based interposing only supports instance methods implemented directly by the class - /// itself. This restriction ensures safe reverting via `revert()`, which cannot remove - /// dynamically added methods. + /// Class-based interposing only supports methods directly implemented by the class itself. + /// This restriction ensures safe reverting via `revert()`, which cannot remove dynamically + /// added methods. /// /// To interpose this method, consider hooking the superclass that provides the implementation, /// or use object-based hooking on a specific instance instead. case methodNotDirectlyImplemented( class: AnyClass, + kind: MethodKind, selector: Selector ) - - /// No implementation found for the method matching the specified selector on the class. + + /// No implementation found for a method of the given kind matching the selector on the class. /// /// This should not occur under normal conditions and may indicate an invalid or misconfigured /// runtime state. case implementationNotFound( class: AnyClass, + kind: MethodKind, selector: Selector ) @@ -44,6 +47,7 @@ public enum InterposeError: Error, @unchecked Sendable { /// In such cases, `Hook.revert()` is unsafe and should be avoided. case revertCorrupted( class: AnyClass, + kind: MethodKind, selector: Selector, imp: IMP? ) @@ -102,34 +106,43 @@ public enum InterposeError: Error, @unchecked Sendable { extension InterposeError: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { switch lhs { - case let .methodNotFound(lhsClass, lhsSelector): + case let .methodNotFound(lhsClass, lhsKind, lhsSelector): switch rhs { - case let .methodNotFound(rhsClass, rhsSelector): - return lhsClass == rhsClass && lhsSelector == rhsSelector + case let .methodNotFound(rhsClass, rhsKind, rhsSelector): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector default: return false } - case let .methodNotDirectlyImplemented(lhsClass, lhsSelector): + case let .methodNotDirectlyImplemented(lhsClass, lhsKind, lhsSelector): switch rhs { - case let .methodNotDirectlyImplemented(rhsClass, rhsSelector): - return lhsClass == rhsClass && lhsSelector == rhsSelector + case let .methodNotDirectlyImplemented(rhsClass, rhsKind, rhsSelector): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector default: return false } - case let .implementationNotFound(lhsClass, lhsSelector): + case let .implementationNotFound(lhsClass, lhsKind, lhsSelector): switch rhs { - case let .implementationNotFound(rhsClass, rhsSelector): - return lhsClass == rhsClass && lhsSelector == rhsSelector + case let .implementationNotFound(rhsClass, rhsKind, rhsSelector): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector default: return false } - case let .revertCorrupted(lhsClass, lhsSelector, lhsIMP): + case let .revertCorrupted(lhsClass, lhsKind, lhsSelector, lhsIMP): switch rhs { - case let .revertCorrupted(rhsClass, rhsSelector, rhsIMP): - return lhsClass == rhsClass && lhsSelector == rhsSelector && lhsIMP == rhsIMP + case let .revertCorrupted(rhsClass, rhsKind, rhsSelector, rhsIMP): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector + && lhsIMP == rhsIMP default: return false } diff --git a/Tests/InterposeKitTests/ClassHookTests.swift b/Tests/InterposeKitTests/ClassHookTests.swift index f00d3a8..3c2fafd 100644 --- a/Tests/InterposeKitTests/ClassHookTests.swift +++ b/Tests/InterposeKitTests/ClassHookTests.swift @@ -3,6 +3,7 @@ import XCTest fileprivate class ExampleClass: NSObject { @objc static dynamic func doSomethingStatic() {} + @objc static dynamic let intValueStatic = 1 @objc dynamic func doSomething() {} @objc dynamic var intValue = 1 @objc dynamic var arrayValue: [String] { ["base"] } @@ -200,6 +201,38 @@ final class ClassHookTests: XCTestCase { XCTAssertEqual(object.arrayValue, ["base"]) } + func testClassMethod() throws { + let hook = try Interpose.prepareHook( + on: ExampleClass.self, + for: #selector(getter: ExampleClass.intValueStatic), + methodKind: .class, + methodSignature: (@convention(c) (ExampleClass, Selector) -> Int).self, + hookSignature: (@convention(block) (ExampleClass) -> Int).self + ) { hook in + return { `self` in 2 } + } + + XCTAssertEqual(ExampleClass.intValueStatic, 1) + XCTAssertMatchesRegex( + hook.debugDescription, + #"^Pending hook for \+\[ExampleClass intValueStatic\]$"# + ) + + try hook.apply() + XCTAssertEqual(ExampleClass.intValueStatic, 2) + XCTAssertMatchesRegex( + hook.debugDescription, + #"^Active hook for \+\[ExampleClass intValueStatic\] \(originalIMP: 0x[0-9a-fA-F]+\)$"# + ) + + try hook.revert() + XCTAssertEqual(ExampleClass.intValueStatic, 1) + XCTAssertMatchesRegex( + hook.debugDescription, + #"^Pending hook for \+\[ExampleClass intValueStatic\]$"# + ) + } + func testValidationFailure_methodNotFound_nonExistent() throws { XCTAssertThrowsError( try Interpose.prepareHook( @@ -212,33 +245,36 @@ final class ClassHookTests: XCTestCase { }, expected: InterposeError.methodNotFound( class: ExampleClass.self, + kind: .instance, selector: Selector(("doSomethingNotFound")) ) ) } - func testValidationFailure_methodNotFound_classMethod() throws { + func testValidationFailure_methodNotDirectlyImplemented_instanceMethod() throws { XCTAssertThrowsError( try Interpose.prepareHook( - on: ExampleClass.self, - for: #selector(ExampleClass.doSomethingStatic), + on: ExampleSubclass.self, + for: #selector(ExampleClass.doSomething), methodSignature: (@convention(c) (NSObject, Selector) -> Void).self, hookSignature: (@convention(block) (NSObject) -> Void).self ) { hook in return { `self` in } }, - expected: InterposeError.methodNotFound( - class: ExampleClass.self, - selector: #selector(ExampleClass.doSomethingStatic) + expected: InterposeError.methodNotDirectlyImplemented( + class: ExampleSubclass.self, + kind: .instance, + selector: #selector(ExampleClass.doSomething) ) ) } - func testValidationFailure_methodNotDirectlyImplemented() throws { + func testValidationFailure_methodNotDirectlyImplemented_classMethod() throws { XCTAssertThrowsError( try Interpose.prepareHook( on: ExampleSubclass.self, - for: #selector(ExampleClass.doSomething), + for: #selector(ExampleClass.doSomethingStatic), + methodKind: .class, methodSignature: (@convention(c) (NSObject, Selector) -> Void).self, hookSignature: (@convention(block) (NSObject) -> Void).self ) { hook in @@ -246,7 +282,8 @@ final class ClassHookTests: XCTestCase { }, expected: InterposeError.methodNotDirectlyImplemented( class: ExampleSubclass.self, - selector: #selector(ExampleClass.doSomething) + kind: .class, + selector: #selector(ExampleClass.doSomethingStatic) ) ) } @@ -283,6 +320,7 @@ final class ClassHookTests: XCTestCase { try hook.revert(), expected: InterposeError.revertCorrupted( class: ExampleClass.self, + kind: .instance, selector: #selector(ExampleClass.doSomething), imp: externalIMP ) From 9bbe39a549ae0352a36022bbae7e0f34b0ace083 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 5 Apr 2025 08:26:09 +0200 Subject: [PATCH 3/6] Added example for class method --- .../InterposeKitExample/Sources/AppDelegate.swift | 14 ++++++++++++-- .../InterposeKitExample/Sources/ContentView.swift | 15 ++++++--------- .../InterposeKitExample/Sources/HookExample.swift | 12 ++++++------ 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Example/InterposeKitExample/Sources/AppDelegate.swift b/Example/InterposeKitExample/Sources/AppDelegate.swift index f5cbd63..681d2aa 100644 --- a/Example/InterposeKitExample/Sources/AppDelegate.swift +++ b/Example/InterposeKitExample/Sources/AppDelegate.swift @@ -99,8 +99,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { return "## \(title) ##" } } - case .NSColor_controlAccentColor: - fatalError("Not implemented") + case .NSColor_labelColor: + return try Interpose.prepareHook( + on: NSColor.self, + for: #selector(getter: NSColor.labelColor), + methodKind: .class, + methodSignature: (@convention(c) (NSColor.Type, Selector) -> NSColor).self, + hookSignature: (@convention(block) (NSColor.Type) -> NSColor).self + ) { hook in + return { `self` in + return self.systemPink + } + } } } catch { fatalError("\(error)") diff --git a/Example/InterposeKitExample/Sources/ContentView.swift b/Example/InterposeKitExample/Sources/ContentView.swift index 6f84998..7278179 100644 --- a/Example/InterposeKitExample/Sources/ContentView.swift +++ b/Example/InterposeKitExample/Sources/ContentView.swift @@ -44,16 +44,13 @@ struct ContentView: View { .labelsHidden() .padding(.leading, 20) } label: { - Group { - Text(example.selector) - .monospaced() - - Text(example.description) - .font(.subheadline) - } - .opacity(example == .NSColor_controlAccentColor ? 0.5 : 1) + Text(example.selector) + .monospaced() + .foregroundStyle(Color(NSColor.labelColor)) + + Text(example.description) + .font(.subheadline) } - .disabled(example == .NSColor_controlAccentColor) } } } diff --git a/Example/InterposeKitExample/Sources/HookExample.swift b/Example/InterposeKitExample/Sources/HookExample.swift index 167b57c..cd805f7 100644 --- a/Example/InterposeKitExample/Sources/HookExample.swift +++ b/Example/InterposeKitExample/Sources/HookExample.swift @@ -2,7 +2,7 @@ enum HookExample: CaseIterable { case NSApplication_sendEvent case NSWindow_setTitle case NSMenuItem_title - case NSColor_controlAccentColor + case NSColor_labelColor } extension HookExample { @@ -14,8 +14,8 @@ extension HookExample { return "-[NSWindow setTitle:]" case .NSMenuItem_title: return "-[NSMenuItem title]" - case .NSColor_controlAccentColor: - return "+[NSColor controlAccentColor]" + case .NSColor_labelColor: + return "+[NSColor labelColor]" } } @@ -36,10 +36,10 @@ extension HookExample { A class hook on NSMenuItem that wraps all menu item titles with decorative markers, \ visible in the main menu and the text field’s context menu. """ - case .NSColor_controlAccentColor: + case .NSColor_labelColor: return """ - A class hook that overrides the system accent color by hooking the corresponding \ - class method on NSColor. (Not implemented.) + A class hook that overrides the standard label color by hooking the corresponding \ + class method on NSColor. Affects text in this window and menus. """ } } From 4833ed3433a7b24531d7c6d383294f8585eaddb6 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 5 Apr 2025 08:26:17 +0200 Subject: [PATCH 4/6] Minor fix in tests --- Tests/InterposeKitTests/ClassHookTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/InterposeKitTests/ClassHookTests.swift b/Tests/InterposeKitTests/ClassHookTests.swift index 3c2fafd..3dbb668 100644 --- a/Tests/InterposeKitTests/ClassHookTests.swift +++ b/Tests/InterposeKitTests/ClassHookTests.swift @@ -206,8 +206,8 @@ final class ClassHookTests: XCTestCase { on: ExampleClass.self, for: #selector(getter: ExampleClass.intValueStatic), methodKind: .class, - methodSignature: (@convention(c) (ExampleClass, Selector) -> Int).self, - hookSignature: (@convention(block) (ExampleClass) -> Int).self + methodSignature: (@convention(c) (ExampleClass.Type, Selector) -> Int).self, + hookSignature: (@convention(block) (ExampleClass.Type) -> Int).self ) { hook in return { `self` in 2 } } From cb4e42041e54ddc4f6bbc2fc479d1abf917a56b4 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 5 Apr 2025 08:28:22 +0200 Subject: [PATCH 5/6] Changed hook for NSApplication.sendEvent(_:) from object to class This caused issues with KVO when attempting to revert the hook --- .../Sources/AppDelegate.swift | 22 +++++++++---------- .../Sources/HookExample.swift | 15 ++++++------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Example/InterposeKitExample/Sources/AppDelegate.swift b/Example/InterposeKitExample/Sources/AppDelegate.swift index 681d2aa..9da7a43 100644 --- a/Example/InterposeKitExample/Sources/AppDelegate.swift +++ b/Example/InterposeKitExample/Sources/AppDelegate.swift @@ -64,6 +64,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { ) -> Hook { do { switch example { + case .NSWindow_setTitle: + return try Interpose.prepareHook( + on: self.window, + for: #selector(setter: NSWindow.title), + methodSignature: (@convention(c) (NSWindow, Selector, String) -> Void).self, + hookSignature: (@convention(block) (NSWindow, String) -> Void).self + ) { hook in + return { `self`, title in + hook.original(self, hook.selector, "## \(title.uppercased()) ##") + } + } case .NSApplication_sendEvent: return try Interpose.prepareHook( on: NSApplication.shared, @@ -76,17 +87,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { hook.original(self, hook.selector, event) } } - case .NSWindow_setTitle: - return try Interpose.prepareHook( - on: self.window, - for: #selector(setter: NSWindow.title), - methodSignature: (@convention(c) (NSWindow, Selector, String) -> Void).self, - hookSignature: (@convention(block) (NSWindow, String) -> Void).self - ) { hook in - return { `self`, title in - hook.original(self, hook.selector, "## \(title.uppercased()) ##") - } - } case .NSMenuItem_title: return try Interpose.prepareHook( on: NSMenuItem.self, diff --git a/Example/InterposeKitExample/Sources/HookExample.swift b/Example/InterposeKitExample/Sources/HookExample.swift index cd805f7..b461b5e 100644 --- a/Example/InterposeKitExample/Sources/HookExample.swift +++ b/Example/InterposeKitExample/Sources/HookExample.swift @@ -1,6 +1,6 @@ enum HookExample: CaseIterable { - case NSApplication_sendEvent case NSWindow_setTitle + case NSApplication_sendEvent case NSMenuItem_title case NSColor_labelColor } @@ -8,10 +8,10 @@ enum HookExample: CaseIterable { extension HookExample { var selector: String { switch self { - case .NSApplication_sendEvent: - return "-[NSApplication sendEvent:]" case .NSWindow_setTitle: return "-[NSWindow setTitle:]" + case .NSApplication_sendEvent: + return "-[NSApplication sendEvent:]" case .NSMenuItem_title: return "-[NSMenuItem title]" case .NSColor_labelColor: @@ -21,16 +21,15 @@ extension HookExample { var description: String { switch self { - case .NSApplication_sendEvent: - return """ - An object hook on the shared NSApplication instance that logs all events passed \ - through sendEvent(_:). - """ case .NSWindow_setTitle: return """ An object hook on the main NSWindow that uppercases the title and wraps it with \ decorative markers whenever it’s set. This can be tested using the text field below. """ + case .NSApplication_sendEvent: + return """ + A class hook on NSApplication that logs all events passed through sendEvent(_:). + """ case .NSMenuItem_title: return """ A class hook on NSMenuItem that wraps all menu item titles with decorative markers, \ From d0910759ce2a9e79328e6564e7cb98ee2e0aa594 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 5 Apr 2025 08:50:31 +0200 Subject: [PATCH 6/6] Added example hook for -[NSWindow miniaturize:] --- .../InterposeKitExample/Sources/AppDelegate.swift | 14 ++++++++++++++ .../InterposeKitExample/Sources/HookExample.swift | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/Example/InterposeKitExample/Sources/AppDelegate.swift b/Example/InterposeKitExample/Sources/AppDelegate.swift index 9da7a43..2fe098f 100644 --- a/Example/InterposeKitExample/Sources/AppDelegate.swift +++ b/Example/InterposeKitExample/Sources/AppDelegate.swift @@ -75,6 +75,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { hook.original(self, hook.selector, "## \(title.uppercased()) ##") } } + case .NSWindow_miniaturize: + return try Interpose.prepareHook( + on: self.window, + for: #selector(NSWindow.miniaturize(_:)), + methodSignature: (@convention(c) (NSWindow, Selector, Any?) -> Void).self, + hookSignature: (@convention(block) (NSWindow, Any?) -> Void).self + ) { hook in + return { `self`, sender in + let alert = NSAlert() + alert.messageText = "Miniaturization Intercepted" + alert.informativeText = "This window refused to minimize because a hook was applied to\n -[NSWindow miniaturize:]." + alert.runModal() + } + } case .NSApplication_sendEvent: return try Interpose.prepareHook( on: NSApplication.shared, diff --git a/Example/InterposeKitExample/Sources/HookExample.swift b/Example/InterposeKitExample/Sources/HookExample.swift index b461b5e..048dd8c 100644 --- a/Example/InterposeKitExample/Sources/HookExample.swift +++ b/Example/InterposeKitExample/Sources/HookExample.swift @@ -1,5 +1,6 @@ enum HookExample: CaseIterable { case NSWindow_setTitle + case NSWindow_miniaturize case NSApplication_sendEvent case NSMenuItem_title case NSColor_labelColor @@ -10,6 +11,8 @@ extension HookExample { switch self { case .NSWindow_setTitle: return "-[NSWindow setTitle:]" + case .NSWindow_miniaturize: + return "-[NSWindow miniaturize:]" case .NSApplication_sendEvent: return "-[NSApplication sendEvent:]" case .NSMenuItem_title: @@ -26,6 +29,11 @@ extension HookExample { An object hook on the main NSWindow that uppercases the title and wraps it with \ decorative markers whenever it’s set. This can be tested using the text field below. """ + case .NSWindow_miniaturize: + return """ + An object hook on the main NSWindow that intercepts miniaturization and shows \ + an alert instead of minimizing the window. + """ case .NSApplication_sendEvent: return """ A class hook on NSApplication that logs all events passed through sendEvent(_:).