Skip to content

Commit bd38145

Browse files
committed
Implemented example app with sample hooks
Closes #15
1 parent 6aa695d commit bd38145

File tree

4 files changed

+253
-14
lines changed

4 files changed

+253
-14
lines changed

Example/InterposeKitExample.xcodeproj/project.pbxproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
8078DF9F2DA016EE009A0B1A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8078DF962DA016EE009A0B1A /* Assets.xcassets */; };
1111
8078DFA02DA016EE009A0B1A /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8078DF982DA016EE009A0B1A /* MainMenu.xib */; };
1212
8078DFA12DA016EE009A0B1A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8078DF9A2DA016EE009A0B1A /* AppDelegate.swift */; };
13+
8078DFA52DA01B90009A0B1A /* InterposeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8078DFA42DA01B90009A0B1A /* InterposeKit */; };
14+
8078DFAA2DA04DAA009A0B1A /* HookExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8078DFA92DA04DAA009A0B1A /* HookExample.swift */; };
15+
8078DFAC2DA04DC6009A0B1A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8078DFAB2DA04DC6009A0B1A /* ContentView.swift */; };
1316
/* End PBXBuildFile section */
1417

1518
/* Begin PBXFileReference section */
@@ -18,13 +21,16 @@
1821
8078DF972DA016EE009A0B1A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
1922
8078DF9A2DA016EE009A0B1A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
2023
8078DF9C2DA016EE009A0B1A /* InterposeKitExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeKitExample.entitlements; sourceTree = "<group>"; };
24+
8078DFA92DA04DAA009A0B1A /* HookExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HookExample.swift; sourceTree = "<group>"; };
25+
8078DFAB2DA04DC6009A0B1A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
2126
/* End PBXFileReference section */
2227

2328
/* Begin PBXFrameworksBuildPhase section */
2429
8078DF802DA016B6009A0B1A /* Frameworks */ = {
2530
isa = PBXFrameworksBuildPhase;
2631
buildActionMask = 2147483647;
2732
files = (
33+
8078DFA52DA01B90009A0B1A /* InterposeKit in Frameworks */,
2834
);
2935
runOnlyForDeploymentPostprocessing = 0;
3036
};
@@ -60,6 +66,8 @@
6066
isa = PBXGroup;
6167
children = (
6268
8078DF9A2DA016EE009A0B1A /* AppDelegate.swift */,
69+
8078DFA92DA04DAA009A0B1A /* HookExample.swift */,
70+
8078DFAB2DA04DC6009A0B1A /* ContentView.swift */,
6371
);
6472
path = Sources;
6573
sourceTree = "<group>";
@@ -99,6 +107,7 @@
99107
);
100108
name = InterposeKitExample;
101109
packageProductDependencies = (
110+
8078DFA42DA01B90009A0B1A /* InterposeKit */,
102111
);
103112
productName = InterposeKitExample;
104113
productReference = 8078DF832DA016B6009A0B1A /* InterposeKitExample.app */;
@@ -128,6 +137,8 @@
128137
);
129138
mainGroup = 8078DF7A2DA016B6009A0B1A;
130139
minimizedProjectReferenceProxies = 1;
140+
packageReferences = (
141+
);
131142
preferredProjectObjectVersion = 77;
132143
productRefGroup = 8078DF842DA016B6009A0B1A /* Products */;
133144
projectDirPath = "";
@@ -155,7 +166,9 @@
155166
isa = PBXSourcesBuildPhase;
156167
buildActionMask = 2147483647;
157168
files = (
169+
8078DFAA2DA04DAA009A0B1A /* HookExample.swift in Sources */,
158170
8078DFA12DA016EE009A0B1A /* AppDelegate.swift in Sources */,
171+
8078DFAC2DA04DC6009A0B1A /* ContentView.swift in Sources */,
159172
);
160173
runOnlyForDeploymentPostprocessing = 0;
161174
};
@@ -308,6 +321,7 @@
308321
"$(inherited)",
309322
"@executable_path/../Frameworks",
310323
);
324+
MACOSX_DEPLOYMENT_TARGET = 13.5;
311325
MARKETING_VERSION = 1.0;
312326
PRODUCT_BUNDLE_IDENTIFIER = eu.structuredpath.InterposeKitExample;
313327
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -334,6 +348,7 @@
334348
"$(inherited)",
335349
"@executable_path/../Frameworks",
336350
);
351+
MACOSX_DEPLOYMENT_TARGET = 13.5;
337352
MARKETING_VERSION = 1.0;
338353
PRODUCT_BUNDLE_IDENTIFIER = eu.structuredpath.InterposeKitExample;
339354
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -365,6 +380,13 @@
365380
defaultConfigurationName = Release;
366381
};
367382
/* End XCConfigurationList section */
383+
384+
/* Begin XCSwiftPackageProductDependency section */
385+
8078DFA42DA01B90009A0B1A /* InterposeKit */ = {
386+
isa = XCSwiftPackageProductDependency;
387+
productName = InterposeKit;
388+
};
389+
/* End XCSwiftPackageProductDependency section */
368390
};
369391
rootObject = 8078DF7B2DA016B6009A0B1A /* Project object */;
370392
}

Example/InterposeKitExample/Sources/AppDelegate.swift

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import AppKit
2+
import InterposeKit
23
import SwiftUI
34

45
@main
56
class AppDelegate: NSObject, NSApplicationDelegate {
67

8+
// ============================================================================ //
9+
// MARK: Window & Content View
10+
// ============================================================================ //
11+
712
private lazy var window: NSWindow = {
813
let window = NSWindow(
914
contentRect: .zero,
@@ -12,17 +17,109 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1217
defer: false
1318
)
1419

15-
window.title = "InterposeKit Example"
16-
window.contentViewController = self.hostingController
17-
20+
let hostingController = NSHostingController(rootView: self.contentView)
21+
window.contentViewController = hostingController
1822
return window
1923
}()
2024

21-
private lazy var hostingController: NSViewController = {
22-
return NSHostingController(rootView: ContentView())
25+
private lazy var contentView: ContentView = {
26+
ContentView(
27+
onHookToggled: { [weak self] example, isEnabled in
28+
guard let self else { return }
29+
30+
let hook = self.hook(for: example)
31+
32+
do {
33+
if isEnabled {
34+
try hook.apply()
35+
} else {
36+
try hook.revert()
37+
}
38+
} catch {
39+
fatalError("\(error)")
40+
}
41+
},
42+
onWindowTitleChanged: { [weak self] title in
43+
self?.window.title = title
44+
}
45+
)
2346
}()
2447

48+
// ============================================================================ //
49+
// MARK: Hooks
50+
// ============================================================================ //
51+
52+
private func hook(
53+
for example: HookExample
54+
) -> Hook {
55+
if let hook = self._hooks[example] { return hook }
56+
57+
let hook = self._makeHook(for: example)
58+
self._hooks[example] = hook
59+
return hook
60+
}
61+
62+
private func _makeHook(
63+
for example: HookExample
64+
) -> Hook {
65+
do {
66+
switch example {
67+
case .NSApplication_sendEvent:
68+
return try Interpose.prepareHook(
69+
on: NSApplication.shared,
70+
for: #selector(NSApplication.sendEvent(_:)),
71+
methodSignature: (@convention(c) (NSApplication, Selector, NSEvent) -> Void).self,
72+
hookSignature: (@convention(block) (NSApplication, NSEvent) -> Void).self
73+
) { hook in
74+
return { `self`, event in
75+
print("NSApplication.sendEvent(_:) \(event)")
76+
hook.original(self, hook.selector, event)
77+
}
78+
}
79+
case .NSWindow_setTitle:
80+
return try Interpose.prepareHook(
81+
on: self.window,
82+
for: #selector(setter: NSWindow.title),
83+
methodSignature: (@convention(c) (NSWindow, Selector, String) -> Void).self,
84+
hookSignature: (@convention(block) (NSWindow, String) -> Void).self
85+
) { hook in
86+
return { `self`, title in
87+
hook.original(self, hook.selector, "## \(title.uppercased()) ##")
88+
}
89+
}
90+
case .NSMenuItem_title:
91+
return try Interpose.prepareHook(
92+
on: NSMenuItem.self,
93+
for: #selector(getter: NSMenuItem.title),
94+
methodSignature: (@convention(c) (NSMenuItem, Selector) -> String).self,
95+
hookSignature: (@convention(block) (NSMenuItem) -> String).self
96+
) { hook in
97+
return { `self` in
98+
let title = hook.original(`self`, hook.selector)
99+
return "## \(title) ##"
100+
}
101+
}
102+
case .NSColor_controlAccentColor:
103+
fatalError("Not implemented")
104+
}
105+
} catch {
106+
fatalError("\(error)")
107+
}
108+
}
109+
110+
private var _hooks = [HookExample: Hook]()
111+
112+
// ============================================================================ //
113+
// MARK: NSApplicationDelegate
114+
// ============================================================================ //
115+
116+
func applicationWillFinishLaunching(_ notification: Notification) {
117+
Interpose.isLoggingEnabled = true
118+
}
119+
25120
func applicationDidFinishLaunching(_ notification: Notification) {
121+
self.window.contentView?.layoutSubtreeIfNeeded()
122+
26123
Task { @MainActor in
27124
self.window.center()
28125
self.window.makeKeyAndOrderFront(nil)
@@ -36,12 +133,3 @@ class AppDelegate: NSObject, NSApplicationDelegate {
36133
}
37134

38135
}
39-
40-
fileprivate struct ContentView: View {
41-
fileprivate var body: some View {
42-
Text("Hello from InterposeKit!")
43-
.font(.title)
44-
.fixedSize()
45-
.padding(200)
46-
}
47-
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import SwiftUI
2+
3+
struct ContentView: View {
4+
5+
// ============================================================================ //
6+
// MARK: Configuration
7+
// ============================================================================ //
8+
9+
let onHookToggled: (HookExample, Bool) -> Void
10+
let onWindowTitleChanged: (String) -> Void
11+
12+
// ============================================================================ //
13+
// MARK: State
14+
// ============================================================================ //
15+
16+
@State
17+
private var hookStates = Dictionary(
18+
uniqueKeysWithValues: HookExample.allCases.map { ($0, false) }
19+
)
20+
21+
@State
22+
private var windowTitle = "InterposeKit Example"
23+
24+
// ============================================================================ //
25+
// MARK: View Body
26+
// ============================================================================ //
27+
28+
var body: some View {
29+
VStack {
30+
Form {
31+
Section("Hooks") {
32+
ForEach(HookExample.allCases, id: \.self) { example in
33+
let isOn = Binding(
34+
get: { self.hookStates[example] ?? false },
35+
set: { newValue in
36+
self.hookStates[example] = newValue
37+
self.onHookToggled(example, newValue)
38+
}
39+
)
40+
41+
LabeledContent {
42+
Toggle("", isOn: isOn)
43+
.toggleStyle(.switch)
44+
.labelsHidden()
45+
.padding(.leading, 20)
46+
} label: {
47+
Group {
48+
Text(example.selector)
49+
.monospaced()
50+
51+
Text(example.description)
52+
.font(.subheadline)
53+
}
54+
.opacity(example == .NSColor_controlAccentColor ? 0.5 : 1)
55+
}
56+
.disabled(example == .NSColor_controlAccentColor)
57+
}
58+
}
59+
}
60+
.formStyle(.grouped)
61+
62+
LabeledContent("Window Title:") {
63+
TextField("", text: self.$windowTitle)
64+
.onSubmit {
65+
self.onWindowTitleChanged(self.windowTitle)
66+
}
67+
68+
Button {
69+
self.onWindowTitleChanged(self.windowTitle)
70+
} label: {
71+
Text("Set").padding(.horizontal, 4)
72+
}
73+
}
74+
.padding(.horizontal, 20)
75+
.padding(.bottom, 28)
76+
}
77+
.fixedSize()
78+
.onAppear {
79+
self.onWindowTitleChanged(self.windowTitle)
80+
}
81+
}
82+
83+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
enum HookExample: CaseIterable {
2+
case NSApplication_sendEvent
3+
case NSWindow_setTitle
4+
case NSMenuItem_title
5+
case NSColor_controlAccentColor
6+
}
7+
8+
extension HookExample {
9+
var selector: String {
10+
switch self {
11+
case .NSApplication_sendEvent:
12+
return "-[NSApplication sendEvent:]"
13+
case .NSWindow_setTitle:
14+
return "-[NSWindow setTitle:]"
15+
case .NSMenuItem_title:
16+
return "-[NSMenuItem title]"
17+
case .NSColor_controlAccentColor:
18+
return "+[NSColor controlAccentColor]"
19+
}
20+
}
21+
22+
var description: String {
23+
switch self {
24+
case .NSApplication_sendEvent:
25+
return """
26+
An object hook on the shared NSApplication instance that logs all events passed \
27+
through sendEvent(_:).
28+
"""
29+
case .NSWindow_setTitle:
30+
return """
31+
An object hook on the main NSWindow that uppercases the title and wraps it with \
32+
decorative markers whenever it’s set. This can be tested using the text field below.
33+
"""
34+
case .NSMenuItem_title:
35+
return """
36+
A class hook on NSMenuItem that wraps all menu item titles with decorative markers, \
37+
visible in the main menu and the text field’s context menu.
38+
"""
39+
case .NSColor_controlAccentColor:
40+
return """
41+
A class hook that overrides the system accent color by hooking the corresponding \
42+
class method on NSColor. (Not implemented.)
43+
"""
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)