Skip to content

Commit 807d7bf

Browse files
committed
Initial ContentView to view classes and class headers
1 parent 5280dc6 commit 807d7bf

File tree

5 files changed

+269
-12
lines changed

5 files changed

+269
-12
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "ClassDumpRuntime"]
2+
path = ClassDumpRuntime
3+
url = [email protected]:leptos-null/ClassDumpRuntime.git

ClassDumpRuntime

Submodule ClassDumpRuntime added at 61618a8

HeaderViewer.xcodeproj/project.pbxproj

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,64 @@
1010
FAD661F82B81D1210000D2A6 /* HeaderViewerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD661F72B81D1210000D2A6 /* HeaderViewerApp.swift */; };
1111
FAD661FA2B81D1210000D2A6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD661F92B81D1210000D2A6 /* ContentView.swift */; };
1212
FAD661FC2B81D1220000D2A6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAD661FB2B81D1220000D2A6 /* Assets.xcassets */; };
13+
FAE3C1712B81F7B700242A99 /* ClassDump.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAD6620C2B81D2990000D2A6 /* ClassDump.framework */; };
14+
FAE3C1722B81F7B700242A99 /* ClassDump.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FAD6620C2B81D2990000D2A6 /* ClassDump.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1315
/* End PBXBuildFile section */
1416

17+
/* Begin PBXContainerItemProxy section */
18+
FAD6620B2B81D2990000D2A6 /* PBXContainerItemProxy */ = {
19+
isa = PBXContainerItemProxy;
20+
containerPortal = FAD662062B81D2990000D2A6 /* ClassDump.xcodeproj */;
21+
proxyType = 2;
22+
remoteGlobalIDString = FA2A00DC23ADD19D00B52F1D;
23+
remoteInfo = ClassDump;
24+
};
25+
FAD6620D2B81D2990000D2A6 /* PBXContainerItemProxy */ = {
26+
isa = PBXContainerItemProxy;
27+
containerPortal = FAD662062B81D2990000D2A6 /* ClassDump.xcodeproj */;
28+
proxyType = 2;
29+
remoteGlobalIDString = FA2A011923AEB15700B52F1D;
30+
remoteInfo = ClassDumpTests;
31+
};
32+
FAE3C16F2B81F7AC00242A99 /* PBXContainerItemProxy */ = {
33+
isa = PBXContainerItemProxy;
34+
containerPortal = FAD662062B81D2990000D2A6 /* ClassDump.xcodeproj */;
35+
proxyType = 1;
36+
remoteGlobalIDString = FA2A00DB23ADD19D00B52F1D;
37+
remoteInfo = ClassDump;
38+
};
39+
/* End PBXContainerItemProxy section */
40+
41+
/* Begin PBXCopyFilesBuildPhase section */
42+
FAE3C1732B81F7B700242A99 /* Embed Frameworks */ = {
43+
isa = PBXCopyFilesBuildPhase;
44+
buildActionMask = 2147483647;
45+
dstPath = "";
46+
dstSubfolderSpec = 10;
47+
files = (
48+
FAE3C1722B81F7B700242A99 /* ClassDump.framework in Embed Frameworks */,
49+
);
50+
name = "Embed Frameworks";
51+
runOnlyForDeploymentPostprocessing = 0;
52+
};
53+
/* End PBXCopyFilesBuildPhase section */
54+
1555
/* Begin PBXFileReference section */
56+
FA9687E72B82E37E00123476 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
1657
FAD661F42B81D1210000D2A6 /* HeaderViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HeaderViewer.app; sourceTree = BUILT_PRODUCTS_DIR; };
1758
FAD661F72B81D1210000D2A6 /* HeaderViewerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderViewerApp.swift; sourceTree = "<group>"; };
1859
FAD661F92B81D1210000D2A6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
1960
FAD661FB2B81D1220000D2A6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2061
FAD661FD2B81D1220000D2A6 /* HeaderViewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HeaderViewer.entitlements; sourceTree = "<group>"; };
62+
FAD662062B81D2990000D2A6 /* ClassDump.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ClassDump.xcodeproj; path = ClassDumpRuntime/ClassDump.xcodeproj; sourceTree = "<group>"; };
2163
/* End PBXFileReference section */
2264

2365
/* Begin PBXFrameworksBuildPhase section */
2466
FAD661F12B81D1210000D2A6 /* Frameworks */ = {
2567
isa = PBXFrameworksBuildPhase;
2668
buildActionMask = 2147483647;
2769
files = (
70+
FAE3C1712B81F7B700242A99 /* ClassDump.framework in Frameworks */,
2871
);
2972
runOnlyForDeploymentPostprocessing = 0;
3073
};
@@ -34,8 +77,10 @@
3477
FAD661EB2B81D1210000D2A6 = {
3578
isa = PBXGroup;
3679
children = (
80+
FAD662062B81D2990000D2A6 /* ClassDump.xcodeproj */,
3781
FAD661F62B81D1210000D2A6 /* HeaderViewer */,
3882
FAD661F52B81D1210000D2A6 /* Products */,
83+
FAD6620F2B81D2C90000D2A6 /* Frameworks */,
3984
);
4085
sourceTree = "<group>";
4186
};
@@ -52,12 +97,29 @@
5297
children = (
5398
FAD661F72B81D1210000D2A6 /* HeaderViewerApp.swift */,
5499
FAD661F92B81D1210000D2A6 /* ContentView.swift */,
100+
FA9687E72B82E37E00123476 /* Info.plist */,
55101
FAD661FB2B81D1220000D2A6 /* Assets.xcassets */,
56102
FAD661FD2B81D1220000D2A6 /* HeaderViewer.entitlements */,
57103
);
58104
path = HeaderViewer;
59105
sourceTree = "<group>";
60106
};
107+
FAD662072B81D2990000D2A6 /* Products */ = {
108+
isa = PBXGroup;
109+
children = (
110+
FAD6620C2B81D2990000D2A6 /* ClassDump.framework */,
111+
FAD6620E2B81D2990000D2A6 /* ClassDumpTests.xctest */,
112+
);
113+
name = Products;
114+
sourceTree = "<group>";
115+
};
116+
FAD6620F2B81D2C90000D2A6 /* Frameworks */ = {
117+
isa = PBXGroup;
118+
children = (
119+
);
120+
name = Frameworks;
121+
sourceTree = "<group>";
122+
};
61123
/* End PBXGroup section */
62124

63125
/* Begin PBXNativeTarget section */
@@ -68,10 +130,12 @@
68130
FAD661F02B81D1210000D2A6 /* Sources */,
69131
FAD661F12B81D1210000D2A6 /* Frameworks */,
70132
FAD661F22B81D1210000D2A6 /* Resources */,
133+
FAE3C1732B81F7B700242A99 /* Embed Frameworks */,
71134
);
72135
buildRules = (
73136
);
74137
dependencies = (
138+
FAE3C1702B81F7AC00242A99 /* PBXTargetDependency */,
75139
);
76140
name = HeaderViewer;
77141
productName = HeaderViewer;
@@ -104,13 +168,36 @@
104168
mainGroup = FAD661EB2B81D1210000D2A6;
105169
productRefGroup = FAD661F52B81D1210000D2A6 /* Products */;
106170
projectDirPath = "";
171+
projectReferences = (
172+
{
173+
ProductGroup = FAD662072B81D2990000D2A6 /* Products */;
174+
ProjectRef = FAD662062B81D2990000D2A6 /* ClassDump.xcodeproj */;
175+
},
176+
);
107177
projectRoot = "";
108178
targets = (
109179
FAD661F32B81D1210000D2A6 /* HeaderViewer */,
110180
);
111181
};
112182
/* End PBXProject section */
113183

184+
/* Begin PBXReferenceProxy section */
185+
FAD6620C2B81D2990000D2A6 /* ClassDump.framework */ = {
186+
isa = PBXReferenceProxy;
187+
fileType = wrapper.framework;
188+
path = ClassDump.framework;
189+
remoteRef = FAD6620B2B81D2990000D2A6 /* PBXContainerItemProxy */;
190+
sourceTree = BUILT_PRODUCTS_DIR;
191+
};
192+
FAD6620E2B81D2990000D2A6 /* ClassDumpTests.xctest */ = {
193+
isa = PBXReferenceProxy;
194+
fileType = wrapper.cfbundle;
195+
path = ClassDumpTests.xctest;
196+
remoteRef = FAD6620D2B81D2990000D2A6 /* PBXContainerItemProxy */;
197+
sourceTree = BUILT_PRODUCTS_DIR;
198+
};
199+
/* End PBXReferenceProxy section */
200+
114201
/* Begin PBXResourcesBuildPhase section */
115202
FAD661F22B81D1210000D2A6 /* Resources */ = {
116203
isa = PBXResourcesBuildPhase;
@@ -134,6 +221,15 @@
134221
};
135222
/* End PBXSourcesBuildPhase section */
136223

224+
/* Begin PBXTargetDependency section */
225+
FAE3C1702B81F7AC00242A99 /* PBXTargetDependency */ = {
226+
isa = PBXTargetDependency;
227+
name = ClassDump;
228+
platformFilter = ios;
229+
targetProxy = FAE3C16F2B81F7AC00242A99 /* PBXContainerItemProxy */;
230+
};
231+
/* End PBXTargetDependency section */
232+
137233
/* Begin XCBuildConfiguration section */
138234
FAD662012B81D1220000D2A6 /* Debug */ = {
139235
isa = XCBuildConfiguration;
@@ -261,6 +357,7 @@
261357
ENABLE_HARDENED_RUNTIME = YES;
262358
ENABLE_PREVIEWS = YES;
263359
GENERATE_INFOPLIST_FILE = YES;
360+
INFOPLIST_FILE = HeaderViewer/Info.plist;
264361
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
265362
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
266363
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
@@ -280,10 +377,11 @@
280377
PRODUCT_BUNDLE_IDENTIFIER = null.leptos.HeaderViewer;
281378
PRODUCT_NAME = "$(TARGET_NAME)";
282379
SDKROOT = auto;
283-
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
380+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
381+
SUPPORTS_MACCATALYST = YES;
284382
SWIFT_EMIT_LOC_STRINGS = YES;
285383
SWIFT_VERSION = 5.0;
286-
TARGETED_DEVICE_FAMILY = "1,2";
384+
TARGETED_DEVICE_FAMILY = "1,2,6";
287385
};
288386
name = Debug;
289387
};
@@ -299,6 +397,7 @@
299397
ENABLE_HARDENED_RUNTIME = YES;
300398
ENABLE_PREVIEWS = YES;
301399
GENERATE_INFOPLIST_FILE = YES;
400+
INFOPLIST_FILE = HeaderViewer/Info.plist;
302401
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
303402
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
304403
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
@@ -318,10 +417,11 @@
318417
PRODUCT_BUNDLE_IDENTIFIER = null.leptos.HeaderViewer;
319418
PRODUCT_NAME = "$(TARGET_NAME)";
320419
SDKROOT = auto;
321-
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
420+
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
421+
SUPPORTS_MACCATALYST = YES;
322422
SWIFT_EMIT_LOC_STRINGS = YES;
323423
SWIFT_VERSION = 5.0;
324-
TARGETED_DEVICE_FAMILY = "1,2";
424+
TARGETED_DEVICE_FAMILY = "1,2,6";
325425
};
326426
name = Release;
327427
};

HeaderViewer/ContentView.swift

Lines changed: 150 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,161 @@
66
//
77

88
import SwiftUI
9+
import MachO.dyld
10+
import ObjectiveC.runtime
11+
import ClassDump
12+
import Combine
13+
14+
extension CDUtilities {
15+
class func imageNames() -> [String] {
16+
var imageCount: UInt32 = 0
17+
let imageNames = objc_copyImageNames(&imageCount)
18+
19+
let names = sequence(first: imageNames) { $0.successor() }
20+
.prefix(Int(imageCount))
21+
.map { String(cString: $0.pointee) }
22+
23+
imageNames.deallocate()
24+
25+
return names
26+
}
27+
28+
class func protocolNames() -> [String] {
29+
var protocolCount: UInt32 = 0
30+
guard let protocolList = objc_copyProtocolList(&protocolCount) else { return [] }
31+
32+
let names = sequence(first: protocolList) { $0.successor() }
33+
.prefix(Int(protocolCount))
34+
.map { NSStringFromProtocol($0.pointee) }
35+
36+
return names
37+
}
38+
}
39+
40+
final class ObjcRuntime: ObservableObject {
41+
static let shared = ObjcRuntime()
42+
private static var sharedIfExists: ObjcRuntime?
43+
44+
@Published private(set) var classList: [String]
45+
@Published private(set) var protocolList: [String]
46+
@Published private(set) var imageList: [String]
47+
48+
private let shouldReloadClassList = PassthroughSubject<Void, Never>()
49+
50+
private var subscriptions: Set<AnyCancellable> = []
51+
52+
private init() {
53+
self.classList = CDUtilities.safeClassNames()
54+
self.protocolList = CDUtilities.protocolNames()
55+
self.imageList = CDUtilities.imageNames()
56+
57+
ObjcRuntime.sharedIfExists = self
58+
59+
shouldReloadClassList
60+
.debounce(for: .milliseconds(15), scheduler: DispatchQueue.main)
61+
.sink { [weak self] in
62+
guard let self else { return }
63+
self.classList = CDUtilities.safeClassNames()
64+
self.protocolList = CDUtilities.protocolNames()
65+
self.imageList = CDUtilities.imageNames()
66+
}
67+
.store(in: &subscriptions)
68+
69+
_dyld_register_func_for_add_image { _, _ in
70+
ObjcRuntime.sharedIfExists?.shouldReloadClassList.send()
71+
}
72+
73+
_dyld_register_func_for_remove_image { _, _ in
74+
ObjcRuntime.sharedIfExists?.shouldReloadClassList.send()
75+
}
76+
}
77+
}
978

1079
struct ContentView: View {
80+
@StateObject private var objc: ObjcRuntime = .shared
81+
@State private var selectedClass: String?
82+
1183
var body: some View {
12-
VStack {
13-
Image(systemName: "globe")
14-
.imageScale(.large)
15-
.foregroundStyle(.tint)
16-
Text("Hello, world!")
84+
NavigationSplitView {
85+
List(objc.classList, id: \.self, selection: $selectedClass) { className in
86+
HStack(alignment: .firstTextBaseline) {
87+
Image(systemName: "c.square.fill")
88+
.foregroundColor(.green)
89+
Text(className)
90+
Spacer()
91+
}
92+
}
93+
.id(objc.classList) // don't try to diff the List
94+
.navigationTitle("Header Viewer")
95+
.toolbar {
96+
ToolbarItem(placement: .topBarTrailing) {
97+
Button {
98+
// TODO
99+
} label: {
100+
Label("Browse", systemImage: "folder")
101+
}
102+
}
103+
}
104+
} detail: {
105+
if let selectedClass {
106+
GeometryReader { geomProxy in
107+
ScrollView([.horizontal, .vertical]) {
108+
CDClassModel(with: NSClassFromString(selectedClass))
109+
.semanticLines(withComments: false, synthesizeStrip: true)
110+
.swiftText()
111+
.textSelection(.enabled)
112+
.multilineTextAlignment(.leading)
113+
.scenePadding()
114+
.frame(
115+
minWidth: geomProxy.size.width, maxWidth: .infinity,
116+
minHeight: geomProxy.size.height, maxHeight: .infinity,
117+
alignment: .topLeading
118+
)
119+
}
120+
.animation(.snappy, value: geomProxy.size)
121+
}
122+
.navigationBarTitleDisplayMode(.inline)
123+
.navigationTitle(selectedClass)
124+
} else {
125+
Text("Select a class or protocol")
126+
.scenePadding()
127+
}
17128
}
18-
.padding()
19129
}
20130
}
21131

22-
#Preview {
23-
ContentView()
132+
extension CDSemanticString {
133+
func swiftText() -> Text {
134+
var ret = Text(verbatim: "")
135+
self.enumerateTypes { str, type in
136+
switch type {
137+
case .standard:
138+
ret = ret + Text(verbatim: str!)
139+
case .comment:
140+
ret = ret + Text(verbatim: str!)
141+
.foregroundColor(.gray)
142+
case .keyword:
143+
ret = ret + Text(verbatim: str!)
144+
.foregroundColor(.pink)
145+
case .variable:
146+
ret = ret + Text(verbatim: str!)
147+
case .recordName:
148+
ret = ret + Text(verbatim: str!)
149+
.foregroundColor(.cyan)
150+
case .class:
151+
ret = ret + Text(verbatim: str!)
152+
.foregroundColor(.orange)
153+
case .protocol:
154+
ret = ret + Text(verbatim: str!)
155+
.foregroundColor(.teal)
156+
case .numeric:
157+
ret = ret + Text(verbatim: str!)
158+
.foregroundColor(.purple)
159+
default:
160+
ret = ret + Text(verbatim: str!)
161+
}
162+
}
163+
return ret
164+
.font(.body.monospaced())
165+
}
24166
}

0 commit comments

Comments
 (0)