|
8 | 8 | import SwiftUI |
9 | 9 | import ClassDump |
10 | 10 |
|
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 { |
12 | 19 | let namedNode: NamedNode |
13 | | - @Binding private var selection: RuntimeObjectType? |
14 | | - @State private var searchString: String = "" |
15 | | - @State private var loadError: Error? |
16 | 20 |
|
17 | | - @EnvironmentObject private var listings: RuntimeListings |
| 21 | + let imagePath: String |
| 22 | + let imageName: String |
18 | 23 |
|
19 | | - private var classNames: [String] { |
20 | | - CDUtilities.classNamesIn(image: namedNode.path) |
21 | | - } |
| 24 | + let runtimeListings: RuntimeListings = .shared |
22 | 25 |
|
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 |
27 | 31 |
|
28 | | - private var runtimeObjects: [RuntimeObjectType] { |
| 32 | + private static func runtimeObjectsFor(classNames: [String], searchString: String) -> [RuntimeObjectType] { |
29 | 33 | let ret: [RuntimeObjectType] = classNames.map { .class(named: $0) } |
30 | 34 | if searchString.isEmpty { return ret } |
31 | 35 | return ret.filter { $0.name.localizedCaseInsensitiveContains(searchString) } |
32 | 36 | } |
33 | 37 |
|
| 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 | + |
34 | 100 | var body: some View { |
35 | 101 | 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") |
48 | 111 | } |
| 112 | + .buttonStyle(.bordered) |
49 | 113 | } |
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 { |
53 | 119 | 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") |
55 | 121 | .padding(.top) |
56 | 122 | } |
57 | 123 | } else { |
| 124 | + let runtimeObjects = viewModel.runtimeObjects |
58 | 125 | ListView(runtimeObjects, selection: $selection) { runtimeObject in |
59 | 126 | RuntimeObjectRow(type: runtimeObject) |
60 | 127 | } |
61 | 128 | .id(runtimeObjects) // don't try to diff the List |
62 | | - .searchable(text: $searchString) |
| 129 | + .searchable(text: $viewModel.searchString) |
63 | 130 | } |
64 | | - } else { |
| 131 | + case .loadError(let error): |
65 | 132 | 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) |
76 | 141 | } |
77 | | - .buttonStyle(.bordered) |
78 | 142 | } |
79 | 143 | } |
80 | 144 | } |
81 | | - .navigationTitle(namedNode.name) |
| 145 | + .navigationTitle(viewModel.imageName) |
82 | 146 | } |
83 | 147 | } |
84 | 148 |
|
|
0 commit comments