Skip to content

Commit 13b652b

Browse files
committed
Support ImageClassPicker with a view model
1 parent 4e78d7b commit 13b652b

File tree

2 files changed

+107
-44
lines changed

2 files changed

+107
-44
lines changed

HeaderViewer/ContentView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ struct ContentRootView: View {
8686
.navigationDestination(for: NamedNode.self) { namedNode in
8787
if namedNode.isLeaf {
8888
ImageClassPicker(namedNode: namedNode, selection: $selectedObject)
89-
.environmentObject(listings)
9089
} else {
9190
NamedNodeView(node: namedNode)
9291
.environmentObject(listings)

HeaderViewer/ImageClassPicker.swift

Lines changed: 107 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,77 +8,141 @@
88
import SwiftUI
99
import ClassDump
1010

11-
struct ImageClassPicker: View {
11+
private enum ImageLoadState {
12+
case notLoaded
13+
case loading
14+
case loaded
15+
case loadError(Error)
16+
}
17+
18+
private class ImageClassPickerModel: ObservableObject {
1219
let namedNode: NamedNode
13-
@Binding private var selection: RuntimeObjectType?
14-
@State private var searchString: String = ""
15-
@State private var loadError: Error?
1620

17-
@EnvironmentObject private var listings: RuntimeListings
21+
let imagePath: String
22+
let imageName: String
1823

19-
private var classNames: [String] {
20-
CDUtilities.classNamesIn(image: namedNode.path)
21-
}
24+
let runtimeListings: RuntimeListings = .shared
2225

23-
init(namedNode: NamedNode, selection: Binding<RuntimeObjectType?>) {
24-
self.namedNode = namedNode
25-
_selection = selection
26-
}
26+
@Published var searchString: String
27+
28+
@Published private(set) var classNames: [String] // not filtered
29+
@Published private(set) var runtimeObjects: [RuntimeObjectType] // filtered based on search
30+
@Published private(set) var loadState: ImageLoadState
2731

28-
private var runtimeObjects: [RuntimeObjectType] {
32+
private static func runtimeObjectsFor(classNames: [String], searchString: String) -> [RuntimeObjectType] {
2933
let ret: [RuntimeObjectType] = classNames.map { .class(named: $0) }
3034
if searchString.isEmpty { return ret }
3135
return ret.filter { $0.name.localizedCaseInsensitiveContains(searchString) }
3236
}
3337

38+
init(namedNode: NamedNode) {
39+
self.namedNode = namedNode
40+
41+
let imagePath = namedNode.path
42+
self.imagePath = imagePath
43+
self.imageName = namedNode.name
44+
45+
let classNames = CDUtilities.classNamesIn(image: imagePath)
46+
self.classNames = classNames
47+
48+
let searchString = ""
49+
self.searchString = searchString
50+
51+
self.runtimeObjects = Self.runtimeObjectsFor(classNames: classNames, searchString: searchString)
52+
53+
self.loadState = runtimeListings.isImageLoaded(path: imagePath) ? .loaded : .notLoaded
54+
55+
runtimeListings.$classList
56+
.map { _ in
57+
CDUtilities.classNamesIn(image: imagePath)
58+
}
59+
.assign(to: &$classNames)
60+
61+
let debouncedSearch = $searchString
62+
.debounce(for: 0.08, scheduler: RunLoop.main)
63+
64+
$classNames.combineLatest(debouncedSearch) { classNames, searchString in
65+
Self.runtimeObjectsFor(classNames: classNames, searchString: searchString)
66+
}
67+
.assign(to: &$runtimeObjects)
68+
69+
runtimeListings.$imageList
70+
.map { imageList in
71+
imageList.contains(CDUtilities.patchImagePathForDyld(imagePath))
72+
}
73+
.filter { $0 } // only allow isLoaded to pass through; we don't want to erase an existing state
74+
.map { _ in
75+
ImageLoadState.loaded
76+
}
77+
.assign(to: &$loadState)
78+
}
79+
80+
func tryLoadImage() {
81+
do {
82+
loadState = .loading
83+
try CDUtilities.loadImage(at: imagePath)
84+
// we could set .loaded here, but there are already pipelines that will update the state
85+
} catch {
86+
loadState = .loadError(error)
87+
}
88+
}
89+
}
90+
91+
struct ImageClassPicker: View {
92+
@StateObject private var viewModel: ImageClassPickerModel
93+
@Binding private var selection: RuntimeObjectType?
94+
95+
init(namedNode: NamedNode, selection: Binding<RuntimeObjectType?>) {
96+
_viewModel = StateObject(wrappedValue: ImageClassPickerModel(namedNode: namedNode))
97+
_selection = selection
98+
}
99+
34100
var body: some View {
35101
Group {
36-
if let loadError {
37-
if let dlOpenError = loadError as? DlOpenError,
38-
let errorMessage = dlOpenError.message {
39-
StatusView {
40-
Text(errorMessage)
41-
.font(.callout.monospaced())
42-
.padding(.top)
43-
}
44-
} else {
45-
StatusView {
46-
Text("An unknown error occured trying to load '\(namedNode.path)'")
47-
.padding(.top)
102+
switch viewModel.loadState {
103+
case .notLoaded:
104+
StatusView {
105+
Text("\(viewModel.imageName) is not yet loaded")
106+
.padding(.top)
107+
Button {
108+
viewModel.tryLoadImage()
109+
} label: {
110+
Text("Load now")
48111
}
112+
.buttonStyle(.bordered)
49113
}
50-
} else if listings.isImageLoaded(path: namedNode.path) {
51-
let runtimeObjects = self.runtimeObjects
52-
if runtimeObjects.isEmpty {
114+
case .loading:
115+
ProgressView()
116+
.scenePadding()
117+
case .loaded:
118+
if viewModel.classNames.isEmpty {
53119
StatusView {
54-
Text("\(namedNode.name) is loaded however does not appear to contain any classes")
120+
Text("\(viewModel.imageName) is loaded however does not appear to contain any classes")
55121
.padding(.top)
56122
}
57123
} else {
124+
let runtimeObjects = viewModel.runtimeObjects
58125
ListView(runtimeObjects, selection: $selection) { runtimeObject in
59126
RuntimeObjectRow(type: runtimeObject)
60127
}
61128
.id(runtimeObjects) // don't try to diff the List
62-
.searchable(text: $searchString)
129+
.searchable(text: $viewModel.searchString)
63130
}
64-
} else {
131+
case .loadError(let error):
65132
StatusView {
66-
Text("\(namedNode.name) is not yet loaded")
67-
.padding(.top)
68-
Button {
69-
do {
70-
try CDUtilities.loadImage(at: namedNode.path)
71-
} catch {
72-
loadError = error
73-
}
74-
} label: {
75-
Text("Load now")
133+
if let dlOpenError = error as? DlOpenError,
134+
let errorMessage = dlOpenError.message {
135+
Text(errorMessage)
136+
.font(.callout.monospaced())
137+
.padding(.top)
138+
} else {
139+
Text("An unknown error occured trying to load '\(viewModel.imagePath)'")
140+
.padding(.top)
76141
}
77-
.buttonStyle(.bordered)
78142
}
79143
}
80144
}
81-
.navigationTitle(namedNode.name)
145+
.navigationTitle(viewModel.imageName)
82146
}
83147
}
84148

0 commit comments

Comments
 (0)