Complete re-think on model, presentation. Pretty much everything

This commit is contained in:
Casey Fleser 2022-06-23 07:45:12 -05:00
parent afcb810a4f
commit 9bf4d8d62e
44 changed files with 1182 additions and 740 deletions

View file

@ -7,15 +7,16 @@
objects = {
/* Begin PBXBuildFile section */
C90BCE442861D3C500C2EF35 /* DeviceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE432861D3C500C2EF35 /* DeviceContent.swift */; };
C90BCE462861D57100C2EF35 /* DeviceHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE452861D57100C2EF35 /* DeviceHeader.swift */; };
C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE472861D70500C2EF35 /* ErrorView.swift */; };
C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */; };
C90BCE4C2861E37900C2EF35 /* AppHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4B2861E37900C2EF35 /* AppHeader.swift */; };
C90BCE4E2861E4E400C2EF35 /* AppContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4D2861E4E400C2EF35 /* AppContent.swift */; };
C90BCE502861E9D000C2EF35 /* SourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4F2861E9D000C2EF35 /* SourceState.swift */; };
C90BCE522861EDBF00C2EF35 /* SourceFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */; };
C927A0D92846414900533D66 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0D82846414900533D66 /* Helpers.swift */; };
C927A0DB2846502300533D66 /* PathActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0DA2846502300533D66 /* PathActions.swift */; };
C94C52C72844E80A00E2129E /* SimItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94C52C62844E80A00E2129E /* SimItemRow.swift */; };
C94C52C92844E99B00E2129E /* SimItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94C52C82844E99B00E2129E /* SimItemContent.swift */; };
C94C52CB2844EAAC00E2129E /* RuntimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94C52CA2844EAAC00E2129E /* RuntimeView.swift */; };
C95E5AB6284B6DDE00A2124E /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95E5AB5284B6DDE00A2124E /* AppView.swift */; };
C977973C284F58A900706DFB /* PresentationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C977973B284F58A900706DFB /* PresentationState.swift */; };
C977973E284F5AE100706DFB /* SimItemNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C977973D284F5AE100706DFB /* SimItemNavLink.swift */; };
C9779740284F5CBB00706DFB /* SimItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C977973F284F5CBB00706DFB /* SimItemGroup.swift */; };
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9779741284F6DE000706DFB /* ToolbarMenu.swift */; };
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F858283B9F9000D491F4 /* SimDirsApp.swift */; };
C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F85A283B9F9000D491F4 /* ContentView.swift */; };
@ -28,23 +29,31 @@
C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F876283D020C00D491F4 /* SimProductFamily.swift */; };
C982F879283D042E00D491F4 /* SimModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F878283D042E00D491F4 /* SimModel.swift */; };
C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F87A283E40C800D491F4 /* SimCtl.swift */; };
C982F880283E57E600D491F4 /* PresentationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F87F283E57E600D491F4 /* PresentationItem.swift */; };
C982F883283E813F00D491F4 /* DeviceTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F882283E813F00D491F4 /* DeviceTypeView.swift */; };
C9D729F128478AB00064152D /* DeviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D729F028478AB00064152D /* DeviceView.swift */; };
C9D73C25285C8C0C0044A279 /* SourceItemData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D73C24285C8C0C0044A279 /* SourceItemData.swift */; };
C9D73C29285C8C4B0044A279 /* SourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D73C28285C8C4B0044A279 /* SourceItem.swift */; };
C9DD54C32860936D00D46AB3 /* SourceItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */; };
C9DD54C52860938C00D46AB3 /* SourceItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C42860938C00D46AB3 /* SourceItemLink.swift */; };
C9DD54C7286093A500D46AB3 /* SourceItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C6286093A500D46AB3 /* SourceItemContent.swift */; };
C9DD54C9286093C100D46AB3 /* SourceItemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C8286093C100D46AB3 /* SourceItemImage.swift */; };
C9DD54CB2860948600D46AB3 /* SourceItemLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CA2860948600D46AB3 /* SourceItemLabel.swift */; };
C9DD54CE2860A0AF00D46AB3 /* DeviceTypeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */; };
C9DD54D02860A1A500D46AB3 /* DeviceTypeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */; };
C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */; };
C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EE0CD128478FDB00E9B97A /* PathRow.swift */; };
C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EE0CD32847B79E00E9B97A /* SimApp.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
C90BCE432861D3C500C2EF35 /* DeviceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceContent.swift; sourceTree = "<group>"; };
C90BCE452861D57100C2EF35 /* DeviceHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHeader.swift; sourceTree = "<group>"; };
C90BCE472861D70500C2EF35 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeHeader.swift; sourceTree = "<group>"; };
C90BCE4B2861E37900C2EF35 /* AppHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHeader.swift; sourceTree = "<group>"; };
C90BCE4D2861E4E400C2EF35 /* AppContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContent.swift; sourceTree = "<group>"; };
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceState.swift; sourceTree = "<group>"; };
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFilter.swift; sourceTree = "<group>"; };
C927A0D82846414900533D66 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
C927A0DA2846502300533D66 /* PathActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathActions.swift; sourceTree = "<group>"; };
C94C52C62844E80A00E2129E /* SimItemRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimItemRow.swift; sourceTree = "<group>"; };
C94C52C82844E99B00E2129E /* SimItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimItemContent.swift; sourceTree = "<group>"; };
C94C52CA2844EAAC00E2129E /* RuntimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeView.swift; sourceTree = "<group>"; };
C95E5AB5284B6DDE00A2124E /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
C977973B284F58A900706DFB /* PresentationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationState.swift; sourceTree = "<group>"; };
C977973D284F5AE100706DFB /* SimItemNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimItemNavLink.swift; sourceTree = "<group>"; };
C977973F284F5CBB00706DFB /* SimItemGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimItemGroup.swift; sourceTree = "<group>"; };
C9779741284F6DE000706DFB /* ToolbarMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarMenu.swift; sourceTree = "<group>"; };
C982F855283B9F9000D491F4 /* SimDirs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimDirs.app; sourceTree = BUILT_PRODUCTS_DIR; };
C982F858283B9F9000D491F4 /* SimDirsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDirsApp.swift; sourceTree = "<group>"; };
@ -59,9 +68,16 @@
C982F876283D020C00D491F4 /* SimProductFamily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimProductFamily.swift; sourceTree = "<group>"; };
C982F878283D042E00D491F4 /* SimModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimModel.swift; sourceTree = "<group>"; };
C982F87A283E40C800D491F4 /* SimCtl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimCtl.swift; sourceTree = "<group>"; };
C982F87F283E57E600D491F4 /* PresentationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationItem.swift; sourceTree = "<group>"; };
C982F882283E813F00D491F4 /* DeviceTypeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeView.swift; sourceTree = "<group>"; };
C9D729F028478AB00064152D /* DeviceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceView.swift; sourceTree = "<group>"; };
C9D73C24285C8C0C0044A279 /* SourceItemData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemData.swift; sourceTree = "<group>"; };
C9D73C28285C8C4B0044A279 /* SourceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItem.swift; sourceTree = "<group>"; };
C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemGroup.swift; sourceTree = "<group>"; };
C9DD54C42860938C00D46AB3 /* SourceItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemLink.swift; sourceTree = "<group>"; };
C9DD54C6286093A500D46AB3 /* SourceItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemContent.swift; sourceTree = "<group>"; };
C9DD54C8286093C100D46AB3 /* SourceItemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemImage.swift; sourceTree = "<group>"; };
C9DD54CA2860948600D46AB3 /* SourceItemLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemLabel.swift; sourceTree = "<group>"; };
C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeContent.swift; sourceTree = "<group>"; };
C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeHeader.swift; sourceTree = "<group>"; };
C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeContent.swift; sourceTree = "<group>"; };
C9EE0CD128478FDB00E9B97A /* PathRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathRow.swift; sourceTree = "<group>"; };
C9EE0CD32847B79E00E9B97A /* SimApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimApp.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -100,6 +116,7 @@
C982F85A283B9F9000D491F4 /* ContentView.swift */,
C927A0D82846414900533D66 /* Helpers.swift */,
C982F867283BA09B00D491F4 /* Model */,
C9D73C23285C8B3B0044A279 /* Presentation */,
C982F881283E7F0400D491F4 /* Views */,
C982F85C283B9F9200D491F4 /* Assets.xcassets */,
C982F861283B9F9200D491F4 /* SimDirs.entitlements */,
@ -119,7 +136,6 @@
C982F867283BA09B00D491F4 /* Model */ = {
isa = PBXGroup;
children = (
C982F87C283E579900D491F4 /* Presentation */,
C982F87A283E40C800D491F4 /* SimCtl.swift */,
C982F878283D042E00D491F4 /* SimModel.swift */,
C9EE0CD32847B79E00E9B97A /* SimApp.swift */,
@ -132,31 +148,55 @@
path = Model;
sourceTree = "<group>";
};
C982F87C283E579900D491F4 /* Presentation */ = {
C982F881283E7F0400D491F4 /* Views */ = {
isa = PBXGroup;
children = (
C982F87F283E57E600D491F4 /* PresentationItem.swift */,
C977973B284F58A900706DFB /* PresentationState.swift */,
C9DD54C12860935300D46AB3 /* SourceItem Views */,
C9DD54CC2860992200D46AB3 /* Model Views */,
C90BCE472861D70500C2EF35 /* ErrorView.swift */,
C927A0DA2846502300533D66 /* PathActions.swift */,
C9EE0CD128478FDB00E9B97A /* PathRow.swift */,
C9779741284F6DE000706DFB /* ToolbarMenu.swift */,
);
path = Views;
sourceTree = "<group>";
};
C9D73C23285C8B3B0044A279 /* Presentation */ = {
isa = PBXGroup;
children = (
C9D73C28285C8C4B0044A279 /* SourceItem.swift */,
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */,
C9D73C24285C8C0C0044A279 /* SourceItemData.swift */,
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */,
);
path = Presentation;
sourceTree = "<group>";
};
C982F881283E7F0400D491F4 /* Views */ = {
C9DD54C12860935300D46AB3 /* SourceItem Views */ = {
isa = PBXGroup;
children = (
C95E5AB5284B6DDE00A2124E /* AppView.swift */,
C982F882283E813F00D491F4 /* DeviceTypeView.swift */,
C9D729F028478AB00064152D /* DeviceView.swift */,
C927A0DA2846502300533D66 /* PathActions.swift */,
C9EE0CD128478FDB00E9B97A /* PathRow.swift */,
C94C52CA2844EAAC00E2129E /* RuntimeView.swift */,
C94C52C82844E99B00E2129E /* SimItemContent.swift */,
C977973F284F5CBB00706DFB /* SimItemGroup.swift */,
C977973D284F5AE100706DFB /* SimItemNavLink.swift */,
C94C52C62844E80A00E2129E /* SimItemRow.swift */,
C9779741284F6DE000706DFB /* ToolbarMenu.swift */,
C9DD54C6286093A500D46AB3 /* SourceItemContent.swift */,
C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */,
C9DD54C8286093C100D46AB3 /* SourceItemImage.swift */,
C9DD54CA2860948600D46AB3 /* SourceItemLabel.swift */,
C9DD54C42860938C00D46AB3 /* SourceItemLink.swift */,
);
path = Views;
path = "SourceItem Views";
sourceTree = "<group>";
};
C9DD54CC2860992200D46AB3 /* Model Views */ = {
isa = PBXGroup;
children = (
C90BCE4D2861E4E400C2EF35 /* AppContent.swift */,
C90BCE4B2861E37900C2EF35 /* AppHeader.swift */,
C90BCE432861D3C500C2EF35 /* DeviceContent.swift */,
C90BCE452861D57100C2EF35 /* DeviceHeader.swift */,
C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */,
C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */,
C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */,
C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */,
);
path = "Model Views";
sourceTree = "<group>";
};
/* End PBXGroup section */
@ -229,29 +269,37 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C982F883283E813F00D491F4 /* DeviceTypeView.swift in Sources */,
C94C52C92844E99B00E2129E /* SimItemContent.swift in Sources */,
C9DD54C9286093C100D46AB3 /* SourceItemImage.swift in Sources */,
C9D73C25285C8C0C0044A279 /* SourceItemData.swift in Sources */,
C90BCE4E2861E4E400C2EF35 /* AppContent.swift in Sources */,
C927A0DB2846502300533D66 /* PathActions.swift in Sources */,
C90BCE442861D3C500C2EF35 /* DeviceContent.swift in Sources */,
C90BCE502861E9D000C2EF35 /* SourceState.swift in Sources */,
C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */,
C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */,
C9DD54C7286093A500D46AB3 /* SourceItemContent.swift in Sources */,
C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */,
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */,
C9779740284F5CBB00706DFB /* SimItemGroup.swift in Sources */,
C95E5AB6284B6DDE00A2124E /* AppView.swift in Sources */,
C9DD54CE2860A0AF00D46AB3 /* DeviceTypeContent.swift in Sources */,
C90BCE4C2861E37900C2EF35 /* AppHeader.swift in Sources */,
C9DD54D02860A1A500D46AB3 /* DeviceTypeHeader.swift in Sources */,
C90BCE522861EDBF00C2EF35 /* SourceFilter.swift in Sources */,
C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */,
C982F875283CEEBB00D491F4 /* SimDevice.swift in Sources */,
C977973C284F58A900706DFB /* PresentationState.swift in Sources */,
C9DD54C52860938C00D46AB3 /* SourceItemLink.swift in Sources */,
C982F873283CE9AD00D491F4 /* SimDeviceType.swift in Sources */,
C977973E284F5AE100706DFB /* SimItemNavLink.swift in Sources */,
C9DD54CB2860948600D46AB3 /* SourceItemLabel.swift in Sources */,
C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */,
C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */,
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */,
C982F871283CE7B800D491F4 /* SimRuntime.swift in Sources */,
C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */,
C94C52C72844E80A00E2129E /* SimItemRow.swift in Sources */,
C982F86B283BA22100D491F4 /* SimPlatform.swift in Sources */,
C927A0D92846414900533D66 /* Helpers.swift in Sources */,
C94C52CB2844EAAC00E2129E /* RuntimeView.swift in Sources */,
C982F880283E57E600D491F4 /* PresentationItem.swift in Sources */,
C90BCE462861D57100C2EF35 /* DeviceHeader.swift in Sources */,
C9DD54C32860936D00D46AB3 /* SourceItemGroup.swift in Sources */,
C9D73C29285C8C4B0044A279 /* SourceItem.swift in Sources */,
C982F879283D042E00D491F4 /* SimModel.swift in Sources */,
C9D729F128478AB00064152D /* DeviceView.swift in Sources */,
C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.900",
"green" : "0.900",
"red" : "0.900"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.200",
"green" : "0.200",
"red" : "0.200"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -8,33 +8,52 @@
import SwiftUI
struct ContentView: View {
@ObservedObject var model : SimModel
@State private var state = PresentationState(filter: [])
@ObservedObject var state : SourceState
init(model: SimModel) {
state = SourceState(model: model)
}
var rootItems : [PresentationItem] { state.presentationItems(from: model) }
var body: some View {
NavigationView {
List {
ForEach(rootItems) { item in
SimItemGroup(item: item, state: $state)
VStack {
NavigationView {
List {
Divider()
switch state.filteredRoot {
case .placeholder:
Text("Placeholder")
case let .device(_, root):
ForEach(root.items) {
SourceItemGroup(selection: $state.selection, item: $0)
}
case let .runtime(_, root):
ForEach(root.items) {
SourceItemGroup(selection: $state.selection, item: $0)
}
}
}
.padding(.leading, 2.0)
.toolbar {
ToolbarItem { ToolbarMenu(state: state) }
}
.frame(minWidth: 200)
Image("Icon-256") // Initial View
}
.frame(minWidth: 200)
.toolbar {
ToolbarItem { ToolbarMenu(state: $state) }
}
Image("Icon-256")
.searchable(text: $state.filter.searchTerm, placement: .sidebar)
}
.searchable(text: $state.searchTerm, placement: .sidebar)
}
}
struct ContentView_Previews: PreviewProvider {
static var simModel = SimModel()
static var model = SimModel()
static var previews: some View {
ContentView(model: simModel)
ContentView(model: model)
.preferredColorScheme(.dark)
ContentView(model: model)
.preferredColorScheme(.light)
}
}

View file

@ -5,7 +5,7 @@
// Created by Casey Fleser on 5/31/22.
//
import AppKit
import SwiftUI
extension NSPasteboard {
static func copy(text: String) {
@ -23,12 +23,21 @@ extension NSWorkspace {
}
extension OptionSet where Self == Self.Element {
func settingBool(_ value: Bool, options: Self) -> Self {
if value { return union(options) }
else { return subtracting (options) }
}
mutating func booleanSet(_ value: Bool, options: Self) {
if value { update(with: options) }
else { subtract(options) }
}
}
extension ProcessInfo {
var isPreviewing : Bool { environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" }
}
extension PropertyListSerialization {
class func propertyList(from url: URL) -> [String : AnyObject]? {
guard let plistData = try? Data(contentsOf: url) else { return nil }
@ -36,3 +45,15 @@ extension PropertyListSerialization {
return try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String : AnyObject]
}
}
extension View {
@ViewBuilder
func evalIf<V: View>(_ test: Bool, then transform: (Self) -> V) -> some View {
if test {
transform(self)
}
else {
self
}
}
}

View file

@ -1,95 +0,0 @@
//
// PresentationItem.swift
// SimDirs
//
// Created by Casey Fleser on 5/25/22.
//
import SwiftUI
protocol PresentableItem {
var title : String { get }
var id : String { get }
var icon : NSImage? { get }
var imageName : String { get }
var imageColor : Color? { get }
var contentView : AnyView? { get }
}
extension PresentableItem {
var imageColor : Color? { return nil }
var icon : NSImage? { return nil }
var contentView : AnyView? { return nil }
}
struct PresentationItem: Identifiable {
let underlying : PresentableItem
var children : [PresentationItem]?
var id : String
var customImage : String?
var title : String { return underlying.title }
var navTitle : String { return "\(typeName): \(underlying.title)" }
var icon : NSImage? { return underlying.icon }
var imageName : String { return customImage ?? underlying.imageName }
var imageColor : Color { return underlying.imageColor ?? .white }
var contentView : AnyView { return underlying.contentView ?? AnyView(Text(title)) }
var flattened : [PresentationItem] { return [self] + (self.children?.flatMap { $0.flattened } ?? []) }
var typeName : String {
switch underlying {
case is SimPlatform: return "Platform"
case is SimProductFamily: return "Product Family"
case is SimRuntime: return "Runtime"
case is SimDeviceType: return "Device Type"
case is SimDevice: return "Device"
case is SimApp: return "App"
default: return "Item"
}
}
init(_ presentable: PresentableItem, image: String? = nil, identifier: String? = nil) {
underlying = presentable
id = identifier ?? underlying.id
customImage = image
}
func titlesContain(_ searchTerm: String) -> Bool {
return title.contains(searchTerm) || children?.contains(where: { $0.titlesContain(searchTerm)}) ?? false
}
func containsType<T>(_ type: T.Type) -> Bool {
return underlying is T || children?.contains(where: { $0.containsType(type)}) ?? false
}
}
extension Array where Element == PresentationItem {
var flatItems : [PresentationItem] { self.flatMap { $0.flattened } }
func itemsOf<T> (type: T.Type) -> [T] {
return flatItems.compactMap { $0.underlying as? T }
}
func validateItems() {
let allIDs = flatItems.map { $0.id }
var idSet = Set<String>()
print("Validating \(allIDs.count) items")
for id in allIDs {
if !idSet.insert(id).inserted {
print("Duplicate PresentationItem.id: \(id)")
}
}
}
func dumpPresentation(level: Int = 0) {
let ident = Array<String>(repeating: "\t", count: level).joined()
for item in self {
print("\(ident)\(item.title) [\(item.id)]")
if let children = item.children {
children.dumpPresentation(level: level + 1)
}
}
}
}

View file

@ -1,118 +0,0 @@
//
// PresentationState.swift
// SimDirs
//
// Created by Casey Fleser on 6/7/22.
//
import Foundation
struct PresentationState {
enum Organization: String, CaseIterable, Identifiable {
case byDevice = "By Device"
case byRuntime = "By Runtime"
var id: Organization { self }
}
struct Filter: OptionSet, CaseIterable {
let rawValue: Int
static let withApps = Filter(rawValue: 1 << 0)
static let runtimeInstalled = Filter(rawValue: 1 << 1)
static var allCases : [Filter] = [.withApps, .runtimeInstalled]
}
var organization = Organization.byRuntime
var filter = Filter()
var searchTerm = ""
static func testItemsOf<T>(type: T.Type) -> [T] {
let flatItems = PresentationState().presentationItems(from: SimModel()).flatItems
return flatItems.itemsOf(type: type)
}
func presentationItems(from model: SimModel) -> [PresentationItem] {
switch organization {
case .byDevice: return itemsForDeviceStyle(from: model)
case .byRuntime: return itemsForRuntimeStyle(from: model)
}
}
func itemsForDeviceStyle(from model: SimModel) -> [PresentationItem] {
return SimProductFamily.presentation.map{ family in
var familyItem = PresentationItem(family)
familyItem.children = model.deviceTypes.filter({ $0.supports(productFamily: family) }).map { deviceType in
var deviceTypeItem = PresentationItem(deviceType, identifier: deviceType.id)
let deviceTypeChildren : [PresentationItem] = model.runtimes.filter({ $0.supports(deviceType: deviceType) }).map { runtime in
var runtimeItem = PresentationItem(runtime, identifier: "\(deviceType.id) - \(runtime.id)")
let runtimeItemChildren = runtime.devices.filter({ $0.isDeviceOfType(deviceType) }).map { device -> PresentationItem in
var deviceItem = PresentationItem(device, image: family.imageName)
let deviceItemChildren = device.apps.map { PresentationItem($0) }
if !deviceItemChildren.isEmpty {
deviceItem.children = deviceItemChildren
}
return deviceItem
}
if !runtimeItemChildren.isEmpty {
runtimeItem.children = runtimeItemChildren
}
return runtimeItem
}
if !deviceTypeChildren.isEmpty {
deviceTypeItem.children = deviceTypeChildren
}
return deviceTypeItem
}
return familyItem
}
}
func itemsForRuntimeStyle(from model: SimModel) -> [PresentationItem] {
return SimPlatform.presentation.map{ platform in
var platformItem = PresentationItem(platform)
platformItem.children = model.runtimes.filter({ $0.supports(platform: platform) }).map { runtime in
var runtimeItem = PresentationItem(runtime)
let runtimeItemChildren : [PresentationItem] = model.deviceTypes.filter({ $0.supports(runtime: runtime) }).map { deviceType in
var deviceTypeItem = PresentationItem(deviceType, identifier: "\(runtime.id) - \(deviceType.id)")
let deviceTypeChildren = runtime.devices.filter({ $0.isDeviceOfType(deviceType) }).map { device -> PresentationItem in
var deviceItem = PresentationItem(device, image: deviceType.imageName)
let deviceItemChildren = device.apps.map { PresentationItem($0) }
if !deviceItemChildren.isEmpty {
deviceItem.children = deviceItemChildren
}
return deviceItem
}
if !deviceTypeChildren.isEmpty {
deviceTypeItem.children = deviceTypeChildren
}
return deviceTypeItem
}
if !runtimeItemChildren.isEmpty {
runtimeItem.children = runtimeItemChildren
}
return runtimeItem
}
return platformItem
}
}
}

View file

@ -71,10 +71,10 @@ struct SimApp: Equatable {
}
}
extension SimApp: PresentableItem, Identifiable {
var title : String { return displayName }
var id : String { return identifier }
extension SimApp: SourceItemData {
var title : String { return displayName }
var headerTitle : String { "App: \(title)" }
var imageDesc : SourceImageDesc { nsIcon.map { .icon(nsImage: $0) } ?? .symbol(systemName: "questionmark.app.dashed") }
var imageName : String { return "questionmark.app.dashed" }
var icon : NSImage? { return nsIcon }
var optionTrait : SourceFilter.Options { .withApps }
}

View file

@ -105,11 +105,14 @@ struct SimDevice: Decodable, Equatable {
}
}
extension SimDevice: PresentableItem, Identifiable {
extension SimDevice: SourceItemData {
var title : String { return name }
var id : String { return udid }
var imageName : String { return "shippingbox" }
var imageColor : Color? { return isAvailable ? .green : .red }
var headerTitle : String { "Device: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: "questionmark.circle", color: isAvailable ? .green : .red) }
}
extension Array where Element == SimDevice {
func of(deviceType: SimDeviceType) -> Self {
filter { $0.isDeviceOfType(deviceType) }
}
}

View file

@ -39,8 +39,19 @@ struct SimDeviceType: Decodable {
}
}
extension SimDeviceType: PresentableItem, Identifiable {
extension SimDeviceType: SourceItemData {
var title : String { return name }
var id : String { return identifier }
var imageName : String { return productFamily.imageName }
var headerTitle : String { "Device Type: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: productFamily.symbolName) }
}
extension Array where Element == SimDeviceType {
func supporting(productFamily: SimProductFamily) -> Self {
filter { $0.supports(productFamily: productFamily) }
}
func supporting(runtime: SimRuntime) -> Self {
filter { $0.supports(runtime: runtime) }
}
}

View file

@ -15,9 +15,12 @@ enum SimError: Error {
class SimModel: ObservableObject {
var deviceTypes : [SimDeviceType]
@Published var runtimes : [SimRuntime]
let timer = DispatchSource.makeTimerSource()
var monitorSource : DispatchSourceTimer?
let updateInterval = 1.0
var devices : [SimDevice] { runtimes.flatMap { $0.devices } }
var apps : [SimApp] { devices.flatMap { $0.apps } }
init() {
let simctl = SimCtl()
@ -38,7 +41,9 @@ class SimModel: ObservableObject {
}
}
runtimes.sort()
beginMonitor()
if !ProcessInfo.processInfo.isPreviewing {
beginMonitor()
}
}
catch {
fatalError("Failed to initialize data model:\n\(error)")
@ -46,6 +51,8 @@ class SimModel: ObservableObject {
}
func beginMonitor() {
let timer = DispatchSource.makeTimerSource()
timer.setEventHandler {
guard let runtimeDevs : [String : [SimDevice]] = try? SimCtl().readAllRuntimeDevices() else { return }
@ -78,5 +85,6 @@ class SimModel: ObservableObject {
}
timer.schedule(deadline: DispatchTime.now(), repeating: updateInterval)
timer.resume()
monitorSource = timer
}
}

View file

@ -13,12 +13,8 @@ enum SimPlatform: String, Decodable {
case watchOS
static let presentation : [SimPlatform] = [.iOS, .watchOS, .tvOS]
}
extension SimPlatform: PresentableItem {
var title : String { return self.rawValue }
var id : String { return self.rawValue }
var imageName : String {
var symbolName : String {
switch self {
case .iOS: return "iphone"
case .tvOS: return "appletv"
@ -26,3 +22,9 @@ extension SimPlatform: PresentableItem {
}
}
}
extension SimPlatform: SourceItemData {
var title : String { self.rawValue }
var headerTitle : String { "Platform: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: symbolName) }
}

View file

@ -14,13 +14,8 @@ enum SimProductFamily: String, Decodable {
case iPhone
static let presentation : [SimProductFamily] = [.iPhone, .iPad, .appleWatch, .appleTV]
}
extension SimProductFamily: PresentableItem {
var title : String { return self.rawValue }
var id : String { return self.rawValue }
var imageName : String {
var symbolName : String {
switch self {
case .iPad: return "ipad"
case .iPhone: return "iphone"
@ -29,3 +24,9 @@ extension SimProductFamily: PresentableItem {
}
}
}
extension SimProductFamily: SourceItemData {
var title : String { self.rawValue }
var headerTitle : String { "Product Family: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: symbolName) }
}

View file

@ -127,11 +127,12 @@ struct SimRuntime: Comparable, Decodable {
}
}
extension SimRuntime: PresentableItem, Identifiable {
var title : String { return name }
var id : String { return identifier }
var imageName : String { return "v.circle" }
var imageColor : Color? { return isAvailable ? .green : .red }
extension SimRuntime: SourceItemData {
var title : String { return name }
var headerTitle : String { "Runtime: \(title)" }
var imageDesc : SourceImageDesc { .symbol(systemName: "shippingbox", color: isAvailable ? .green : .red) }
var optionTrait : SourceFilter.Options { isAvailable ? .runtimeInstalled : [] }
}
extension Array where Element == SimRuntime {
@ -142,4 +143,12 @@ extension Array where Element == SimRuntime {
return self.endIndex - 1
}()
}
func supporting(deviceType: SimDeviceType) -> Self {
filter { $0.supports(deviceType: deviceType) }
}
func supporting(platform: SimPlatform) -> Self {
filter { $0.supports(platform: platform) }
}
}

View file

@ -0,0 +1,89 @@
//
// SourceFilter.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import Foundation
struct SourceFilter {
struct Options: OptionSet, CaseIterable {
let rawValue: Int
static let withApps = Options(rawValue: 1 << 0)
static let runtimeInstalled = Options(rawValue: 1 << 1)
static var allCases : [Options] = [.withApps, .runtimeInstalled]
func search<T: SourceItem>(item: T, progress: Options) -> Self {
var foundOptions = progress.union(item.data.optionTrait)
if let items = item.children, !subtracting(foundOptions).isEmpty {
for child in items {
foundOptions = search(item: child, progress: foundOptions)
if isSubset(of: foundOptions) {
break
}
}
}
return foundOptions
}
}
var searchTerm = ""
var options = Options()
func filtered<T>(root: SourceRoot<T>) -> SourceRoot<T> {
if !searchTerm.isEmpty || !options.isEmpty {
var fRoot = root
fRoot.items = root.items.compactMap { item in
let result = filtered(item: item)
return result.match ? result.fItem : nil
}
return fRoot
}
else {
return root
}
}
func filtered<T: SourceItem>(item: T, inheritedOptions: Options = []) -> (fItem: T, match: Bool) {
var fItem = item
var match = true
var childMatch = false
let optProgress = inheritedOptions.union(fItem.data.optionTrait) // options inherited by children
// If there are options to match then do that first passing inherited options along
// and consider a match fulfilled if any child contains all the desired options.
if !options.isEmpty {
var foundOptions = optProgress
if !options.isSubset(of: foundOptions) {
foundOptions = options.search(item: fItem, progress: foundOptions)
}
match = options.isSubset(of: foundOptions)
}
if !searchTerm.isEmpty && match {
match = fItem.title.uppercased().contains(searchTerm.uppercased())
}
if let srcChildren = fItem.children {
fItem.children = srcChildren.compactMap { child -> T.Child? in
let result = filtered(item: child, inheritedOptions: optProgress)
return result.match ? result.fItem : nil
}
childMatch = fItem.children?.isEmpty == false
}
return (fItem, match || childMatch)
}
}

View file

@ -0,0 +1,44 @@
//
// SourceItem.swift
// SimDirs
//
// Created by Casey Fleser on 6/14/22.
//
import SwiftUI
protocol SourceItem: Identifiable {
associatedtype Model : SourceItemData
associatedtype Child : SourceItem
var id : UUID { get }
var data : Model { get }
var children : [Child]? { get set }
var customImgDesc : SourceImageDesc? { get }
}
extension SourceItem {
var title : String { data.title }
var headerTitle : String { data.headerTitle }
var header : some View { data.header }
var content : some View { data.content }
var imageDesc : SourceImageDesc { customImgDesc ?? data.imageDesc }
var customImgDesc : SourceImageDesc? { nil }
}
struct SourceItemVal<Model: SourceItemData, Child: SourceItem>: SourceItem {
var id = UUID()
var data : Model
var children : [Child]?
var customImgDesc : SourceImageDesc?
}
struct SourceRoot<Item: SourceItem> {
var items : [Item]
}
extension Never: SourceItem {
public var id : UUID { fatalError() }
public var data : Never { fatalError() }
public var children : [Never]? { get { fatalError() } set { } }
}

View file

@ -0,0 +1,51 @@
//
// SourceItemData.swift
// SimDirs
//
// Created by Casey Fleser on 6/15/22.
//
import SwiftUI
protocol SourceItemData {
associatedtype Content : View
associatedtype Header : View
@ViewBuilder var header : Self.Header { get }
@ViewBuilder var content : Self.Content { get }
var title : String { get }
var headerTitle : String { get }
var imageDesc : SourceImageDesc { get }
var optionTrait : SourceFilter.Options { get }
}
extension SourceItemData {
var headerTitle : String { title }
var imageDesc : SourceImageDesc { .symbol() }
var header : some View { get { EmptyView() } }
var content : some View { get { EmptyView() } }
var optionTrait : SourceFilter.Options { [] }
}
enum SourceImageDesc {
case icon(nsImage: NSImage)
case symbol(systemName: String = "questionmark.circle", color: Color = .primary)
func withColor(_ color: Color) -> Self {
switch self {
case .icon: return self
case let .symbol(name, _): return .symbol(systemName: name, color: color)
}
}
}
extension Never: SourceItemData {
var headerTitle : String { fatalError () }
var imageDesc : SourceImageDesc { SourceImageDesc.symbol(systemName: "exclamationmark.octagon", color: .red) }
var header : some View { Text("Error: Never SourceItemData type cannot provide header") }
var content : some View { Text("Error: Never SourceItemData type cannot provide content") }
}

View file

@ -0,0 +1,140 @@
//
// SourceState.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import Foundation
class SourceState: ObservableObject {
typealias ProductFamily = SourceItemVal<SimProductFamily, DeviceType_DS>
typealias Platform = SourceItemVal<SimPlatform, Runtime_RT>
typealias DeviceType_DS = SourceItemVal<SimDeviceType, Runtime_DS>
typealias DeviceType_RT = SourceItemVal<SimDeviceType, Device>
typealias Runtime_DS = SourceItemVal<SimRuntime, Device>
typealias Runtime_RT = SourceItemVal<SimRuntime, DeviceType_RT>
typealias Device = SourceItemVal<SimDevice, App>
typealias App = SourceItemVal<SimApp, Never>
enum Root: Identifiable {
case placeholder(id: UUID = UUID())
case device(id: UUID = UUID(), SourceRoot<ProductFamily>)
case runtime(id: UUID = UUID(), SourceRoot<Platform>)
var id : UUID {
switch self {
case let .placeholder(id): return id
case let .device(id, _): return id
case let .runtime(id, _): return id
}
}
}
enum Style: Int, CaseIterable, Identifiable {
case placeholder
case byDevice
case byRuntime
var id : Int { rawValue }
var title : String {
switch self {
case .placeholder: return "Placeholder"
case .byDevice: return "By Device"
case .byRuntime: return "By Runtime"
}
}
var visible : Bool {
switch self {
case .placeholder: return false
default: return true
}
}
}
@Published var style = Style.placeholder { didSet { baseRoot = rootFor(style: style) } }
@Published var filter = SourceFilter()
@Published var selection : UUID?
var model : SimModel
var baseRoot = Root.placeholder()
var filteredRoot : Root {
switch baseRoot {
case .placeholder: return baseRoot
case let .device(_, root): return .device(id: baseRoot.id, filter.filtered(root: root))
case let .runtime(_, root): return .runtime(id: baseRoot.id, filter.filtered(root: root))
}
}
var filterApps : Bool {
get { filter.options.contains(.withApps) }
set { filter.options.booleanSet(newValue, options: .withApps) }
}
var filterRuntimes : Bool {
get { filter.options.contains(.runtimeInstalled) }
set { filter.options.booleanSet(newValue, options: .runtimeInstalled) }
}
func filtering<T: SourceItem>(_ items: [T]) -> [T] {
return items
}
init(model: SimModel) {
self.model = model
style = Style.byDevice
}
func rootFor(style: Style) -> Root {
switch style {
case .placeholder: return .placeholder()
case .byDevice: return .device(SourceRoot(items: deviceStyleItems()))
case .byRuntime: return .runtime(SourceRoot(items: runtimeStyleItems()))
}
}
func deviceStyleItems() -> [ProductFamily] {
SimProductFamily.presentation.map { family in
ProductFamily(data: family, children: model.deviceTypes.supporting(productFamily: family).map { devType in
DeviceType_DS(data: devType, children: model.runtimes.supporting(deviceType: devType).map { runtime in
Runtime_DS(data: runtime, children: runtime.devices.of(deviceType: devType).map { device in
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
return Device(data: device, children: device.apps.map { app in App(data: app) }, customImgDesc: imageDesc)
})
})
})
}
}
func runtimeStyleItems() -> [Platform] {
SimPlatform.presentation.map { platform in
Platform(data: platform, children: model.runtimes.supporting(platform: platform).map { runtime in
Runtime_RT(data: runtime, children: model.deviceTypes.supporting(runtime: runtime).map { devType in
DeviceType_RT(data: devType, children: runtime.devices.of(deviceType: devType).map { device in
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
return Device(data: device, children: device.apps.map { app in App(data: app) }, customImgDesc: imageDesc)
})
})
})
}
}
#if OLDWAY
static func testItemsOf<T>(type: T.Type) -> [T] {
return PresentationState(model: SimModel()).allUnderlyingOf(type: type)
}
func allItemsOf<T>(type: T.Type) -> [PresentationItem] {
return presentationItems().flatItems.itemsOf(type: type)
}
func allUnderlyingOf<T>(type: T.Type) -> [T] {
return presentationItems().flatItems.underlyingOf(type: type)
}
#endif
}

View file

@ -15,6 +15,7 @@ struct SimDirsApp: App {
WindowGroup {
ContentView(model: simModel)
}
.windowStyle(.hiddenTitleBar)
.commands {
SimCommands()
}

View file

@ -1,68 +0,0 @@
//
// AppView.swift
// SimDirs
//
// Created by Casey Fleser on 6/4/22.
//
import SwiftUI
extension SimApp {
var contentView : AnyView? { return AnyView(AppView(app: self)) }
}
struct AppView: View {
var app : SimApp
var icon : Image { app.nsIcon.map({ Image(nsImage: $0) }) ??
Image(systemName: "questionmark.app.dashed")
}
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
Group {
HStack(alignment: .top) {
VStack(alignment: .leading) {
Text("Display Name: \(app.displayName)")
Text("Bundle Name: \(app.bundleName)")
Text("Bundle ID: \(app.bundleID)")
Text("Version: \(app.version)")
Text("Minimum OS Version: \(app.minOSVersion)")
}
Spacer()
icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 72.0, maxHeight: 72.0)
.cornerRadius(4.0)
}
Divider()
.padding([.top, .bottom], 4.0)
PathRow(title: "Bundle Path", path: app.bundlePath)
if let sandboxPath = app.sandboxPath {
PathRow(title: "Sandbox Path", path: sandboxPath)
}
else {
Text("Sandbox Path: <unknown>")
}
}
.font(.subheadline)
.textSelection(.enabled)
.lineLimit(1)
}
}
}
struct AppView_Previews: PreviewProvider {
static var previews: some View {
let apps = PresentationState.testItemsOf(type: SimApp.self)
if apps.isEmpty {
Text("No SimApp present in model data")
}
else {
ForEach(apps[0...2]) {
AppView(app: $0)
}
}
}
}

View file

@ -1,47 +0,0 @@
//
// DeviceTypeView.swift
// SimDirs
//
// Created by Casey Fleser on 5/25/22.
//
import SwiftUI
extension SimDeviceType {
var contentView : AnyView? { return AnyView(DeviceTypeView(deviceType: self)) }
}
struct DeviceTypeView: View {
var deviceType : SimDeviceType
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
Group {
Text("Product Family: \(deviceType.productFamily.title)")
Text("Model ID: \(deviceType.modelIdentifier)")
Text("Min Runtime: \(deviceType.minRuntimeVersionString)")
Text("Max Runtime: \(UInt32.max == deviceType.maxRuntimeVersion ? "-" : deviceType.maxRuntimeVersionString)")
Text("Identifier: \(deviceType.identifier)")
PathRow(title: "Bundle Path", path: deviceType.bundlePath)
}
.font(.subheadline)
.textSelection(.enabled)
.lineLimit(1)
}
}
}
struct DeviceTypeView_Previews: PreviewProvider {
static var previews: some View {
let deviceTypes = PresentationState.testItemsOf(type: SimDeviceType.self)
if deviceTypes.isEmpty {
Text("No SimDeviceType present in model data")
}
else {
ForEach(deviceTypes[0...2]) {
DeviceTypeView(deviceType: $0)
}
}
}
}

View file

@ -1,57 +0,0 @@
//
// DeviceView.swift
// SimDirs
//
// Created by Casey Fleser on 6/1/22.
//
import SwiftUI
extension SimDevice {
var contentView : AnyView? { return AnyView(DeviceView(device: self)) }
}
struct DeviceView: View {
var device : SimDevice
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
Group {
Text(device.isAvailable ? "Available" : "Unavailable")
.foregroundColor(device.isAvailable ? .green : .red)
if !device.isAvailable {
let errText = device.availabilityError ?? "Unknown Error"
Text(errText)
.foregroundColor(.red)
.padding(.leading)
}
Text("State: \(device.state.rawValue)")
Text("UDID: \(device.udid)")
PathRow(title: "Data Path", path: device.dataPath)
PathRow(title: "Log Path", path: device.logPath)
}
.font(.subheadline)
.textSelection(.enabled)
.lineLimit(1)
}
}
}
struct DeviceView_Previews: PreviewProvider {
static var previews: some View {
let devices = PresentationState.testItemsOf(type: SimDevice.self)
if devices.isEmpty {
Text("No SimDevice present in model data")
}
else {
if let available = devices.first(where: { $0.isAvailable }) {
DeviceView(device: available)
}
if let unavailable = devices.first(where: { !$0.isAvailable }) {
DeviceView(device: unavailable)
}
}
}
}

View file

@ -0,0 +1,35 @@
//
// ErrorView.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import SwiftUI
struct ErrorView: View {
let title : String
let description : String
var body: some View {
HStack(alignment: .top) {
Image(systemName: "xmark.octagon.fill")
.symbolRenderingMode(.multicolor)
VStack(alignment: .leading) {
Text(title)
.fontWeight(.semibold)
Text(description)
.foregroundColor(.secondary)
}
}
.padding(.bottom, 8.0)
}
}
struct ErrorView_Previews: PreviewProvider {
static var previews: some View {
ErrorView(
title: "Something bad",
description: "Did you try turning it off and back on again?")
}
}

View file

@ -0,0 +1,43 @@
//
// AppContent.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import SwiftUI
extension SimApp {
public var content : some View { AppContent(app: self) }
}
struct AppContent: View {
var app : SimApp
var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
Text("PATHS")
.fontWeight(.semibold)
.foregroundColor(.secondary)
PathRow(title: "Bundle Path", path: app.bundlePath)
if let sandboxPath = app.sandboxPath {
PathRow(title: "Sandbox Path", path: sandboxPath)
}
else {
Text("Sandbox Path: <unknown>")
}
}
.font(.subheadline)
.lineLimit(1)
}
}
struct AppContent_Previews: PreviewProvider {
static var apps = SimModel().apps
static var previews: some View {
AppContent(app: apps[0])
AppContent(app: apps.randomElement() ?? apps[1])
}
}

View file

@ -0,0 +1,36 @@
//
// AppHeader.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import SwiftUI
extension SimApp {
public var header : some View { AppHeader(app: self) }
}
struct AppHeader: View {
var app : SimApp
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
Text("Display Name: \(app.displayName)")
Text("Bundle Name: \(app.bundleName)")
Text("Bundle ID: \(app.bundleID)")
Text("Version: \(app.version)")
Text("Minimum OS Version: \(app.minOSVersion)")
}
.font(.subheadline)
}
}
struct AppHeader_Previews: PreviewProvider {
static var apps = SimModel().apps
static var previews: some View {
AppHeader(app: apps[0])
AppHeader(app: apps.randomElement() ?? apps[1])
}
}

View file

@ -0,0 +1,46 @@
//
// DeviceContent.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import SwiftUI
extension SimDevice {
public var content : some View { DeviceContent(device: self) }
}
struct DeviceContent: View {
var device : SimDevice
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
Group {
if !device.isAvailable {
ErrorView(
title: "\(device.name) is unavailable",
description: device.availabilityError ?? "Unknown Error")
}
Text("PATHS")
.fontWeight(.semibold)
.foregroundColor(.secondary)
PathRow(title: "Data Path", path: device.dataPath)
PathRow(title: "Log Path", path: device.logPath)
}
.font(.subheadline)
.textSelection(.enabled)
.lineLimit(1)
}
}
}
struct DeviceContent_Previews: PreviewProvider {
static var devices = SimModel().devices
static var previews: some View {
DeviceContent(device: devices[0])
DeviceContent(device: devices.randomElement() ?? devices[1])
}
}

View file

@ -0,0 +1,34 @@
//
// DeviceHeader.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import SwiftUI
extension SimDevice {
public var header : some View { DeviceHeader(device: self) }
}
struct DeviceHeader: View {
var device : SimDevice
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
Text("State: \(device.state.rawValue)")
Text("UDID: \(device.udid)")
}
.font(.subheadline)
.textSelection(.enabled)
}
}
struct DeviceHeader_Previews: PreviewProvider {
static var devices = SimModel().devices
static var previews: some View {
DeviceHeader(device: devices[0])
DeviceHeader(device: devices.randomElement() ?? devices[1])
}
}

View file

@ -0,0 +1,32 @@
//
// DeviceTypeContent.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
extension SimDeviceType {
public var content : some View { DeviceTypeContent(deviceType: self) }
}
struct DeviceTypeContent: View {
var deviceType : SimDeviceType
var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
PathRow(title: "Bundle Path", path: deviceType.bundlePath)
}
.font(.subheadline)
.lineLimit(1)
}
}
struct DeviceTypeContent_Previews: PreviewProvider {
static var deviceTypes = SimModel().deviceTypes
static var previews: some View {
DeviceTypeContent(deviceType: deviceTypes[0])
}
}

View file

@ -0,0 +1,36 @@
//
// DeviceTypeHeader.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
extension SimDeviceType {
public var header : some View { DeviceTypeHeader(deviceType: self) }
}
struct DeviceTypeHeader: View {
var deviceType : SimDeviceType
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
Text("Product Family: \(deviceType.productFamily.title)")
Text("Model ID: \(deviceType.modelIdentifier)")
Text("Min Runtime: \(deviceType.minRuntimeVersionString)")
Text("Max Runtime: \(UInt32.max == deviceType.maxRuntimeVersion ? "-" : deviceType.maxRuntimeVersionString)")
Text("Identifier: \(deviceType.identifier)")
}
.font(.subheadline)
.textSelection(.enabled)
}
}
struct DeviceTypeHeader_Previews: PreviewProvider {
static var deviceTypes = SimModel().deviceTypes
static var previews: some View {
DeviceTypeHeader(deviceType: deviceTypes[0])
}
}

View file

@ -0,0 +1,61 @@
//
// RuntimeContent.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
extension SimRuntime {
public var content : some View { RuntimeContent(runtime: self) }
}
struct RuntimeContent: View {
struct SupportedItem: Identifiable {
let name : String
var id : String { return name }
}
var runtime : SimRuntime
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
let items = runtime.supportedDeviceTypes.map { SupportedItem(name: $0.name) }
Group {
if !runtime.isAvailable {
ErrorView(
title: "\(runtime.name) is unavailable",
description: runtime.availabilityError ?? "Unknown Error")
}
Text("PATHS")
.fontWeight(.semibold)
.foregroundColor(.secondary)
if !runtime.bundlePath.isEmpty {
PathRow(title: "Bundle Path", path: runtime.bundlePath)
}
Text("SUPPORTED DEVICES \(runtime.isPlaceholder ? "(partial list)" : "")")
.fontWeight(.semibold)
.foregroundColor(.secondary)
.padding(.top, 8.0)
ForEach(items) { item in
Text("\(item.name)")
}
.padding(.leading)
}
.font(.subheadline)
.textSelection(.enabled)
}
}
}
struct RuntimeContent_Previews: PreviewProvider {
static var runtimes = SimModel().runtimes
static var previews: some View {
RuntimeContent(runtime: runtimes[0])
}
}

View file

@ -0,0 +1,34 @@
//
// RuntimeHeader.swift
// SimDirs
//
// Created by Casey Fleser on 6/21/22.
//
import SwiftUI
extension SimRuntime {
public var header : some View { RuntimeHeader(runtime: self) }
}
struct RuntimeHeader: View {
var runtime : SimRuntime
var body: some View {
VStack(alignment: .leading, spacing: 3.0) {
if !runtime.buildversion.isEmpty {
Text("Build Version: \(runtime.buildversion)")
}
}
.font(.subheadline)
.textSelection(.enabled)
}
}
struct RuntimeHeader_Previews: PreviewProvider {
static var runtimes = SimModel().runtimes
static var previews: some View {
RuntimeContent(runtime: runtimes[0])
}
}

View file

@ -27,8 +27,6 @@ struct PathActions: View {
.stroke(.white.opacity(0.4), lineWidth: 1.0))
.background(.black.opacity(0.4))
.cornerRadius(6.0)
// based on scenePhase?
// .shadow(color: .black.opacity(0.4), radius: 8.0, x: 4.0, y: 4.0)
}
}

View file

@ -15,6 +15,7 @@ struct PathRow: View {
HStack {
Text("\(title): \(path)")
.truncationMode(/*@START_MENU_TOKEN@*/.middle/*@END_MENU_TOKEN@*/)
Spacer()
PathActions(path: path)
}
}

View file

@ -1,78 +0,0 @@
//
// RuntimeView.swift
// SimDirs
//
// Created by Casey Fleser on 5/30/22.
//
import SwiftUI
extension SimRuntime {
var contentView : AnyView? { return AnyView(RuntimeView(runtime: self)) }
}
struct RuntimeView: View {
struct SupportedItem: Identifiable {
let name : String
var id : String { return name }
}
@Environment(\.scenePhase) private var scenePhase
var runtime : SimRuntime
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
let items = runtime.supportedDeviceTypes.map({ SupportedItem(name: $0.name) })
Group {
Text(runtime.isAvailable ? "Available" : "Unavailable")
.foregroundColor(runtime.isAvailable ? .green : .red)
if !runtime.isAvailable {
let errText = runtime.availabilityError ?? "Unknown Error"
Text(errText)
.foregroundColor(.red)
.padding(.leading)
}
if !runtime.buildversion.isEmpty {
Text("Build Version: \(runtime.buildversion)")
}
if !runtime.bundlePath.isEmpty {
PathRow(title: "Bundle Path", path: runtime.bundlePath)
}
Divider()
.padding(.vertical, 4.0)
Text("Supported Devices\(runtime.isPlaceholder ? " (partial list)" : "")")
ForEach(items) { item in
Text("\(item.name)")
}
.padding(.leading)
}
.font(.subheadline)
.textSelection(.enabled)
}
}
}
struct RuntimeView_Previews: PreviewProvider {
static var previews: some View {
let runtimes = PresentationState.testItemsOf(type: SimRuntime.self)
if runtimes.isEmpty {
Text("No SimRuntime present in model data")
}
else {
if let available = runtimes.first(where: { $0.isAvailable }) {
RuntimeView(runtime: available)
.previewLayout(.sizeThatFits)
}
if let unavailable = runtimes.first(where: { !$0.isAvailable }) {
RuntimeView(runtime: unavailable)
}
}
}
}

View file

@ -1,34 +0,0 @@
//
// SimItemContent.swift
// SimDirs
//
// Created by Casey Fleser on 5/30/22.
//
import SwiftUI
struct SimItemContent: View {
var item : PresentationItem
var body: some View {
ScrollView {
VStack {
HStack {
item.contentView
Spacer()
}
Spacer()
}
.padding(.all)
.navigationTitle(item.navTitle)
}
}
}
struct SimItemContent_Previews: PreviewProvider {
static var previews: some View {
let testItem = PresentationState().presentationItems(from: SimModel())[0]
SimItemContent(item: testItem.children![0])
}
}

View file

@ -1,62 +0,0 @@
//
// SimItemGroup.swift
// SimDirs
//
// Created by Casey Fleser on 6/7/22.
//
import SwiftUI
struct SimItemGroup: View {
let item : PresentationItem
@Binding var state : PresentationState
@State private var isExpanded = false
var children : [PresentationItem]? {
guard var items = item.children else { return nil }
if state.filter.contains(.withApps) {
items = items.filter { $0.containsType(SimApp.self) }
}
if state.filter.contains(.runtimeInstalled) {
items = items.filter {
guard let runtime = $0.underlying as? SimRuntime else { return true }
return runtime.isAvailable
}
}
if !state.searchTerm.isEmpty {
items = items.filter { $0.titlesContain(state.searchTerm) }
}
return items.isEmpty ? nil : items
}
var body: some View {
if let childItems = children {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
ForEach(childItems) { childItem in
SimItemGroup(item: childItem, state: $state)
}
},
label: { SimItemNavLink(item: item) }
)
}
else {
SimItemNavLink(item: item)
}
}
}
struct SimItemGroup_Previews: PreviewProvider {
static var simModel = SimModel()
@State static var state = PresentationState()
static var previews: some View {
let testItem = state.presentationItems(from: simModel)[0]
SimItemGroup(item: testItem, state: $state)
}
}

View file

@ -1,27 +0,0 @@
//
// SimItemNavLink.swift
// SimDirs
//
// Created by Casey Fleser on 6/7/22.
//
import SwiftUI
struct SimItemNavLink: View {
let item : PresentationItem
var body: some View {
NavigationLink {
SimItemContent(item: item) } label: {
SimItemRow(item: item)
}
}
}
struct SimItemNavLink_Previews: PreviewProvider {
static var previews: some View {
let testItem = PresentationState().presentationItems(from: SimModel())[0]
SimItemNavLink(item: testItem)
}
}

View file

@ -1,37 +0,0 @@
//
// SimItemRow.swift
// SimDirs
//
// Created by Casey Fleser on 5/30/22.
//
import SwiftUI
struct SimItemRow: View {
var item : PresentationItem
var body: some View {
Label {
Text(item.title)
} icon: {
if let icon = item.icon {
Image(nsImage: icon)
.resizable()
.frame(maxWidth: 20.0, maxHeight: 20.0)
.cornerRadius(4.0)
}
else {
Image(systemName: item.imageName)
.foregroundColor(item.imageColor)
.symbolRenderingMode(.hierarchical)
}
}
}
}
struct SimItemRow_Previews: PreviewProvider {
static var previews: some View {
let testItem = PresentationState().presentationItems(from: SimModel())[0]
SimItemRow(item: testItem)
}
}

View file

@ -0,0 +1,67 @@
//
// SourceItemContent.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
import SwiftUI
struct SourceItemContent<Item: SourceItem>: View {
var item : Item
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0.0) {
// --- Header section ---
VStack(alignment: .leading) {
Text(item.headerTitle)
.font(.system(size: 20))
.padding(.top, 12.0)
.padding(.bottom, 8.0)
item.header
.padding(.trailing, 136.0)
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity, maxHeight: 144.0, alignment: .topLeading)
Rectangle().frame(height: 1.0).foregroundColor(Color("HeaderEdge"))
// --- Content section ---
ScrollView {
HStack {
item.content
.padding(.top, 4.0)
.padding(.trailing)
Spacer()
}
}
.frame(maxWidth: .infinity)
.padding([.leading, .top])
.background(.background)
}
.overlay(
SourceItemImage(imageDesc: item.imageDesc, isLabelImage: false)
.padding([.top, .trailing], 24.0),
alignment: .topTrailing
)
.padding(.top, -geometry.frame(in: .global).origin.y)
}
.navigationTitle(item.title)
}
}
struct SourceItemContent_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var sampleItems = state.deviceStyleItems()[0...1]
static var previews: some View {
ForEach(sampleItems) { item in
SourceItemContent(item: item)
.preferredColorScheme(.dark)
SourceItemContent(item: item)
.preferredColorScheme(.light)
}
}
}

View file

@ -0,0 +1,42 @@
//
// SourceItemGroup.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemGroup<Item: SourceItem>: View {
@State private var isExpanded = false
@Binding var selection: UUID?
var item : Item
var body: some View {
if let childItems = item.children, childItems.count > 0 {
DisclosureGroup(
isExpanded: $isExpanded) {
ForEach(childItems) { childItem in
SourceItemGroup<Item.Child>(selection: $selection, item: childItem)
}
} label: {
SourceItemLink(selection: $selection, item: item)
}
}
else {
SourceItemLink(selection: $selection, item: item)
}
}
}
struct SourceItemGroup_Previews: PreviewProvider {
@State static var selection : UUID?
static var state = SourceState(model: SimModel())
static var sampleItem = state.deviceStyleItems()[0]
static var previews: some View {
SourceItemGroup(selection: $selection, item: sampleItem)
}
}

View file

@ -0,0 +1,54 @@
//
// SourceItemImage.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemImage: View {
var imageDesc : SourceImageDesc
var isLabelImage = true
var imageSize : CGFloat?
var body: some View {
let size = imageSize ?? (isLabelImage ? 20.0 : 128.0)
switch imageDesc {
case let .icon(nsImage):
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: size, maxHeight: size)
.cornerRadius(size / 5.0)
.shadow(radius: 4.0, x: 2.0, y: 2.0)
case let .symbol(systemName, color):
if isLabelImage {
Image(systemName: systemName)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
}
else {
Image(systemName: systemName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: size, maxHeight: size)
.shadow(radius: 4.0, x: 2.0, y: 2.0)
}
}
}
}
struct SourceItemImage_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var sampleItems = state.deviceStyleItems()[0...1]
static var previews: some View {
ForEach(sampleItems) { item in
SourceItemImage(imageDesc: item.imageDesc)
SourceItemImage(imageDesc: item.imageDesc, isLabelImage: false)
}
}
}

View file

@ -0,0 +1,31 @@
//
// SourceItemLabel.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemLabel<Item: SourceItem>: View {
var item : Item
var body: some View {
Label(
title: { Text(item.title) },
icon: { SourceItemImage(imageDesc: item.imageDesc) }
)
}
}
struct SourceItemLabel_Previews: PreviewProvider {
static var state = SourceState(model: SimModel())
static var sampleItems = state.deviceStyleItems()[0...1]
static var previews: some View {
ForEach(sampleItems) { item in
SourceItemLabel(item: item)
SourceItemLabel(item: item)
}
}
}

View file

@ -0,0 +1,37 @@
//
// SourceItemLink.swift
// SimDirs
//
// Created by Casey Fleser on 6/20/22.
//
import SwiftUI
struct SourceItemLink<Item: SourceItem>: View {
@Binding var selection: UUID?
var item : Item
var body: some View {
NavigationLink(tag: item.id, selection: $selection,
destination: { SourceItemContent(item: item) },
label: {
SourceItemLabel(item: item)
.padding(.leading, 2.0)
}
)
}
}
struct SourceItemLink_Previews: PreviewProvider {
@State static var selection : UUID?
static var state = SourceState(model: SimModel())
static var sampleItem = state.deviceStyleItems()[0]
static var previews: some View {
SourceItemLink(selection: $selection, item: sampleItem)
.preferredColorScheme(.dark)
SourceItemLink(selection: $selection, item: sampleItem)
.preferredColorScheme(.light)
}
}

View file

@ -8,27 +8,20 @@
import SwiftUI
struct ToolbarMenu: View {
@Binding var state : PresentationState
var withApps : Binding<Bool> {
Binding(get: { state.filter.contains(.withApps) },
set: { state.filter.booleanSet($0, options: .withApps) })
}
var withRuntimes : Binding<Bool> {
Binding(get: { state.filter.contains(.runtimeInstalled) },
set: { state.filter.booleanSet($0, options: .runtimeInstalled) })
}
@ObservedObject var state : SourceState
var body: some View {
Menu {
Picker("Organization", selection: $state.organization) {
ForEach(PresentationState.Organization.allCases) { style in
Text(style.rawValue).tag(style)
Picker("Style", selection: $state.style) {
ForEach(SourceState.Style.allCases) { style in
if style.visible {
Text(style.title).tag(style)
}
}
}
.pickerStyle(.inline)
Toggle(isOn: withApps) { Label("With Apps", systemImage: "app.fill") }
Toggle(isOn: withRuntimes) { Label("Installed Runtimes", systemImage: "cpu.fill") }
Toggle(isOn: $state.filterApps) { Label("With Apps", systemImage: "app.fill") }
Toggle(isOn: $state.filterRuntimes) { Label("Installed Runtimes", systemImage: "cpu.fill") }
} label: {
Label("Filter", systemImage: "slider.horizontal.3")
}
@ -36,10 +29,10 @@ struct ToolbarMenu: View {
}
struct ToolbarMenu_Previews: PreviewProvider {
@State static var state = PresentationState(filter: [])
static var state = SourceState(model: SimModel())
static var previews: some View {
ToolbarMenu(state: $state)
ToolbarMenu(state: state)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

After

Width:  |  Height:  |  Size: 485 KiB