├── .gitignore ├── LICENSE.md ├── README.md ├── SimDirs.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── SimDirs.xcscheme ├── SimDirs ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128.png │ │ ├── icon_128@2x.png │ │ ├── icon_16.png │ │ ├── icon_16@2x.png │ │ ├── icon_256.png │ │ ├── icon_256@2x.png │ │ ├── icon_32.png │ │ ├── icon_32@2x.png │ │ ├── icon_512.png │ │ └── icon_512@2x.png │ ├── Colors │ │ ├── CircleSymbolBkgOff.colorset │ │ │ └── Contents.json │ │ ├── CircleSymbolOff.colorset │ │ │ └── Contents.json │ │ ├── ContentHeader.colorset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── HeaderEdge.colorset │ │ │ └── Contents.json │ │ ├── RoundedBackground.colorset │ │ │ └── Contents.json │ │ └── RoundedBorder.colorset │ │ │ └── Contents.json │ ├── Contents.json │ └── Icon-256.imageset │ │ ├── Contents.json │ │ ├── icon_256.png │ │ └── icon_256@2x.png ├── ContentView.swift ├── Helpers.swift ├── Model │ ├── SimApp.swift │ ├── SimCtl.swift │ ├── SimDevice.swift │ ├── SimDeviceType.swift │ ├── SimModel.swift │ ├── SimPlatform.swift │ ├── SimProductFamily.swift │ └── SimRuntime.swift ├── Node │ ├── Conforming │ │ ├── SimApp+Node.swift │ │ ├── SimDevice+Node.swift │ │ ├── SimDeviceType+Node.swift │ │ ├── SimPlatform+Node.swift │ │ ├── SimProductFamily+Node.swift │ │ └── SimRuntime+Node.swift │ ├── FilteredNode.swift │ ├── Node.swift │ ├── NodeAB.swift │ ├── NodeListBuilder.swift │ └── Views │ │ ├── FilteredNodeView.swift │ │ ├── NodeLabel.swift │ │ └── NodeView.swift ├── Presentation │ └── SourceFilter.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SimDirs.entitlements ├── SimDirsApp.swift └── Views │ ├── AppearancePicker.swift │ ├── ContentHeader.swift │ ├── DescriptiveToggle.swift │ ├── ErrorView.swift │ ├── Model Views │ ├── AppContent.swift │ ├── AppHeader.swift │ ├── DeviceContent.swift │ ├── DeviceHeader.swift │ ├── DeviceTypeContent.swift │ ├── DeviceTypeHeader.swift │ ├── RuntimeContent.swift │ └── RuntimeHeader.swift │ ├── PathActions.swift │ ├── PathRow.swift │ ├── Styles │ ├── AppearanceButtonStyle.swift │ ├── DescriptiveToggleStyle.swift │ └── SystemIconButtonStyle.swift │ └── ToolbarMenu.swift └── screens ├── app.png └── device.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | Resources 3 | xcuserdata/ 4 | *.xccheckout 5 | *.xcscmblueprint 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Casey Fleser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SimDirs 2 | ======= 3 | 4 | This started as a quick and dirty app to display the location of simulator devices and apps way back in Xcode 6. I didn't put a ton of thought into it at the time. I've recently (Spring 2022) started looking at it again. First just getting it to compile and then redesigning it to use SwiftUI and driven, mostly, by the output of `xcrun simctl`. 5 | 6 | It's a bit of a work in progress. Perhaps eventually It'll turn into a tool with a bit more functionality. If that happens I may even write some real documentation. 7 | 8 | That's it. 9 | 10 | #### Device View 11 | ![Screenshot](https://raw.githubusercontent.com/somegeekintn/SimDirs/main/screens/device.png) 12 | 13 | #### App View 14 | ![Screenshot](https://raw.githubusercontent.com/somegeekintn/SimDirs/main/screens/app.png) 15 | -------------------------------------------------------------------------------- /SimDirs.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C90BCE442861D3C500C2EF35 /* DeviceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE432861D3C500C2EF35 /* DeviceContent.swift */; }; 11 | C90BCE462861D57100C2EF35 /* DeviceHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE452861D57100C2EF35 /* DeviceHeader.swift */; }; 12 | C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE472861D70500C2EF35 /* ErrorView.swift */; }; 13 | C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */; }; 14 | C90BCE4C2861E37900C2EF35 /* AppHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4B2861E37900C2EF35 /* AppHeader.swift */; }; 15 | C90BCE4E2861E4E400C2EF35 /* AppContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4D2861E4E400C2EF35 /* AppContent.swift */; }; 16 | C90BCE522861EDBF00C2EF35 /* SourceFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */; }; 17 | C90DCC142896AAAA0072E403 /* ContentHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90DCC132896AAAA0072E403 /* ContentHeader.swift */; }; 18 | C90DCC162896B0370072E403 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90DCC152896B0370072E403 /* AppearancePicker.swift */; }; 19 | C927A0D92846414900533D66 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0D82846414900533D66 /* Helpers.swift */; }; 20 | C927A0DB2846502300533D66 /* PathActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C927A0DA2846502300533D66 /* PathActions.swift */; }; 21 | C95CC0F828B2411700928FAE /* AppearanceButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */; }; 22 | C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */; }; 23 | C966876C29B7641F007BB3F5 /* FilteredNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966875E29B7641F007BB3F5 /* FilteredNode.swift */; }; 24 | C966876D29B7641F007BB3F5 /* SimApp+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876029B7641F007BB3F5 /* SimApp+Node.swift */; }; 25 | C966876E29B7641F007BB3F5 /* SimProductFamily+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876129B7641F007BB3F5 /* SimProductFamily+Node.swift */; }; 26 | C966876F29B7641F007BB3F5 /* SimPlatform+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876229B7641F007BB3F5 /* SimPlatform+Node.swift */; }; 27 | C966877029B7641F007BB3F5 /* SimDeviceType+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876329B7641F007BB3F5 /* SimDeviceType+Node.swift */; }; 28 | C966877129B7641F007BB3F5 /* SimRuntime+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876429B7641F007BB3F5 /* SimRuntime+Node.swift */; }; 29 | C966877229B7641F007BB3F5 /* SimDevice+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876529B7641F007BB3F5 /* SimDevice+Node.swift */; }; 30 | C966877329B7641F007BB3F5 /* NodeListBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876629B7641F007BB3F5 /* NodeListBuilder.swift */; }; 31 | C966877429B7641F007BB3F5 /* NodeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876829B7641F007BB3F5 /* NodeLabel.swift */; }; 32 | C966877529B7641F007BB3F5 /* FilteredNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876929B7641F007BB3F5 /* FilteredNodeView.swift */; }; 33 | C966877629B7641F007BB3F5 /* NodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876A29B7641F007BB3F5 /* NodeView.swift */; }; 34 | C966877729B7641F007BB3F5 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876B29B7641F007BB3F5 /* Node.swift */; }; 35 | C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9779741284F6DE000706DFB /* ToolbarMenu.swift */; }; 36 | C98048FC29C5FC2A00DE0DBD /* NodeAB.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */; }; 37 | C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F858283B9F9000D491F4 /* SimDirsApp.swift */; }; 38 | C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F85A283B9F9000D491F4 /* ContentView.swift */; }; 39 | C982F85D283B9F9200D491F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C982F85C283B9F9200D491F4 /* Assets.xcassets */; }; 40 | C982F860283B9F9200D491F4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C982F85F283B9F9200D491F4 /* Preview Assets.xcassets */; }; 41 | C982F86B283BA22100D491F4 /* SimPlatform.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F86A283BA22100D491F4 /* SimPlatform.swift */; }; 42 | C982F871283CE7B800D491F4 /* SimRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F870283CE7B800D491F4 /* SimRuntime.swift */; }; 43 | C982F873283CE9AD00D491F4 /* SimDeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F872283CE9AD00D491F4 /* SimDeviceType.swift */; }; 44 | C982F875283CEEBB00D491F4 /* SimDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F874283CEEBB00D491F4 /* SimDevice.swift */; }; 45 | C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F876283D020C00D491F4 /* SimProductFamily.swift */; }; 46 | C982F879283D042E00D491F4 /* SimModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F878283D042E00D491F4 /* SimModel.swift */; }; 47 | C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F87A283E40C800D491F4 /* SimCtl.swift */; }; 48 | C9BF5232289FE95D00BDDC91 /* DescriptiveToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */; }; 49 | C9BF5234289FE99600BDDC91 /* DescriptiveToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */; }; 50 | C9DD54CE2860A0AF00D46AB3 /* DeviceTypeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */; }; 51 | C9DD54D02860A1A500D46AB3 /* DeviceTypeHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */; }; 52 | C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */; }; 53 | C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EE0CD128478FDB00E9B97A /* PathRow.swift */; }; 54 | C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EE0CD32847B79E00E9B97A /* SimApp.swift */; }; 55 | /* End PBXBuildFile section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | C90BCE432861D3C500C2EF35 /* DeviceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceContent.swift; sourceTree = ""; }; 59 | C90BCE452861D57100C2EF35 /* DeviceHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHeader.swift; sourceTree = ""; }; 60 | C90BCE472861D70500C2EF35 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 61 | C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeHeader.swift; sourceTree = ""; }; 62 | C90BCE4B2861E37900C2EF35 /* AppHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHeader.swift; sourceTree = ""; }; 63 | C90BCE4D2861E4E400C2EF35 /* AppContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContent.swift; sourceTree = ""; }; 64 | C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFilter.swift; sourceTree = ""; }; 65 | C90DCC132896AAAA0072E403 /* ContentHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentHeader.swift; sourceTree = ""; }; 66 | C90DCC152896B0370072E403 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = ""; }; 67 | C927A0D82846414900533D66 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 68 | C927A0DA2846502300533D66 /* PathActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathActions.swift; sourceTree = ""; }; 69 | C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceButtonStyle.swift; sourceTree = ""; }; 70 | C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemIconButtonStyle.swift; sourceTree = ""; }; 71 | C966875E29B7641F007BB3F5 /* FilteredNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilteredNode.swift; sourceTree = ""; }; 72 | C966876029B7641F007BB3F5 /* SimApp+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimApp+Node.swift"; sourceTree = ""; }; 73 | C966876129B7641F007BB3F5 /* SimProductFamily+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimProductFamily+Node.swift"; sourceTree = ""; }; 74 | C966876229B7641F007BB3F5 /* SimPlatform+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimPlatform+Node.swift"; sourceTree = ""; }; 75 | C966876329B7641F007BB3F5 /* SimDeviceType+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimDeviceType+Node.swift"; sourceTree = ""; }; 76 | C966876429B7641F007BB3F5 /* SimRuntime+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimRuntime+Node.swift"; sourceTree = ""; }; 77 | C966876529B7641F007BB3F5 /* SimDevice+Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimDevice+Node.swift"; sourceTree = ""; }; 78 | C966876629B7641F007BB3F5 /* NodeListBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeListBuilder.swift; sourceTree = ""; }; 79 | C966876829B7641F007BB3F5 /* NodeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeLabel.swift; sourceTree = ""; }; 80 | C966876929B7641F007BB3F5 /* FilteredNodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilteredNodeView.swift; sourceTree = ""; }; 81 | C966876A29B7641F007BB3F5 /* NodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeView.swift; sourceTree = ""; }; 82 | C966876B29B7641F007BB3F5 /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; 83 | C9779741284F6DE000706DFB /* ToolbarMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarMenu.swift; sourceTree = ""; }; 84 | C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAB.swift; sourceTree = ""; }; 85 | C982F855283B9F9000D491F4 /* SimDirs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimDirs.app; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | C982F858283B9F9000D491F4 /* SimDirsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDirsApp.swift; sourceTree = ""; }; 87 | C982F85A283B9F9000D491F4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 88 | C982F85C283B9F9200D491F4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 89 | C982F85F283B9F9200D491F4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 90 | C982F861283B9F9200D491F4 /* SimDirs.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SimDirs.entitlements; sourceTree = ""; }; 91 | C982F86A283BA22100D491F4 /* SimPlatform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimPlatform.swift; sourceTree = ""; }; 92 | C982F870283CE7B800D491F4 /* SimRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimRuntime.swift; sourceTree = ""; }; 93 | C982F872283CE9AD00D491F4 /* SimDeviceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDeviceType.swift; sourceTree = ""; }; 94 | C982F874283CEEBB00D491F4 /* SimDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDevice.swift; sourceTree = ""; }; 95 | C982F876283D020C00D491F4 /* SimProductFamily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimProductFamily.swift; sourceTree = ""; }; 96 | C982F878283D042E00D491F4 /* SimModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimModel.swift; sourceTree = ""; }; 97 | C982F87A283E40C800D491F4 /* SimCtl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimCtl.swift; sourceTree = ""; }; 98 | C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveToggle.swift; sourceTree = ""; }; 99 | C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveToggleStyle.swift; sourceTree = ""; }; 100 | C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeContent.swift; sourceTree = ""; }; 101 | C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTypeHeader.swift; sourceTree = ""; }; 102 | C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeContent.swift; sourceTree = ""; }; 103 | C9EE0CD128478FDB00E9B97A /* PathRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathRow.swift; sourceTree = ""; }; 104 | C9EE0CD32847B79E00E9B97A /* SimApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimApp.swift; sourceTree = ""; }; 105 | /* End PBXFileReference section */ 106 | 107 | /* Begin PBXFrameworksBuildPhase section */ 108 | C982F852283B9F9000D491F4 /* Frameworks */ = { 109 | isa = PBXFrameworksBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXFrameworksBuildPhase section */ 116 | 117 | /* Begin PBXGroup section */ 118 | C95CC0F628B240F500928FAE /* Styles */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | C95CC0F728B2411700928FAE /* AppearanceButtonStyle.swift */, 122 | C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */, 123 | C95CC0F928B2414900928FAE /* SystemIconButtonStyle.swift */, 124 | ); 125 | path = Styles; 126 | sourceTree = ""; 127 | }; 128 | C966875D29B7641F007BB3F5 /* Node */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | C966875F29B7641F007BB3F5 /* Conforming */, 132 | C966876729B7641F007BB3F5 /* Views */, 133 | C966875E29B7641F007BB3F5 /* FilteredNode.swift */, 134 | C966876629B7641F007BB3F5 /* NodeListBuilder.swift */, 135 | C966876B29B7641F007BB3F5 /* Node.swift */, 136 | C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */, 137 | ); 138 | path = Node; 139 | sourceTree = ""; 140 | }; 141 | C966875F29B7641F007BB3F5 /* Conforming */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | C966876029B7641F007BB3F5 /* SimApp+Node.swift */, 145 | C966876129B7641F007BB3F5 /* SimProductFamily+Node.swift */, 146 | C966876229B7641F007BB3F5 /* SimPlatform+Node.swift */, 147 | C966876329B7641F007BB3F5 /* SimDeviceType+Node.swift */, 148 | C966876429B7641F007BB3F5 /* SimRuntime+Node.swift */, 149 | C966876529B7641F007BB3F5 /* SimDevice+Node.swift */, 150 | ); 151 | path = Conforming; 152 | sourceTree = ""; 153 | }; 154 | C966876729B7641F007BB3F5 /* Views */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | C966876829B7641F007BB3F5 /* NodeLabel.swift */, 158 | C966876929B7641F007BB3F5 /* FilteredNodeView.swift */, 159 | C966876A29B7641F007BB3F5 /* NodeView.swift */, 160 | ); 161 | path = Views; 162 | sourceTree = ""; 163 | }; 164 | C982F84C283B9F9000D491F4 = { 165 | isa = PBXGroup; 166 | children = ( 167 | C982F857283B9F9000D491F4 /* SimDirs */, 168 | C982F856283B9F9000D491F4 /* Products */, 169 | ); 170 | indentWidth = 4; 171 | sourceTree = ""; 172 | tabWidth = 4; 173 | }; 174 | C982F856283B9F9000D491F4 /* Products */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | C982F855283B9F9000D491F4 /* SimDirs.app */, 178 | ); 179 | name = Products; 180 | sourceTree = ""; 181 | }; 182 | C982F857283B9F9000D491F4 /* SimDirs */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | C982F858283B9F9000D491F4 /* SimDirsApp.swift */, 186 | C982F85A283B9F9000D491F4 /* ContentView.swift */, 187 | C927A0D82846414900533D66 /* Helpers.swift */, 188 | C982F867283BA09B00D491F4 /* Model */, 189 | C966875D29B7641F007BB3F5 /* Node */, 190 | C9D73C23285C8B3B0044A279 /* Presentation */, 191 | C982F881283E7F0400D491F4 /* Views */, 192 | C982F85C283B9F9200D491F4 /* Assets.xcassets */, 193 | C982F861283B9F9200D491F4 /* SimDirs.entitlements */, 194 | C982F85E283B9F9200D491F4 /* Preview Content */, 195 | ); 196 | path = SimDirs; 197 | sourceTree = ""; 198 | }; 199 | C982F85E283B9F9200D491F4 /* Preview Content */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | C982F85F283B9F9200D491F4 /* Preview Assets.xcassets */, 203 | ); 204 | path = "Preview Content"; 205 | sourceTree = ""; 206 | }; 207 | C982F867283BA09B00D491F4 /* Model */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | C982F87A283E40C800D491F4 /* SimCtl.swift */, 211 | C982F878283D042E00D491F4 /* SimModel.swift */, 212 | C9EE0CD32847B79E00E9B97A /* SimApp.swift */, 213 | C982F874283CEEBB00D491F4 /* SimDevice.swift */, 214 | C982F872283CE9AD00D491F4 /* SimDeviceType.swift */, 215 | C982F86A283BA22100D491F4 /* SimPlatform.swift */, 216 | C982F876283D020C00D491F4 /* SimProductFamily.swift */, 217 | C982F870283CE7B800D491F4 /* SimRuntime.swift */, 218 | ); 219 | path = Model; 220 | sourceTree = ""; 221 | }; 222 | C982F881283E7F0400D491F4 /* Views */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | C95CC0F628B240F500928FAE /* Styles */, 226 | C9DD54CC2860992200D46AB3 /* Model Views */, 227 | C90DCC152896B0370072E403 /* AppearancePicker.swift */, 228 | C90DCC132896AAAA0072E403 /* ContentHeader.swift */, 229 | C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */, 230 | C90BCE472861D70500C2EF35 /* ErrorView.swift */, 231 | C927A0DA2846502300533D66 /* PathActions.swift */, 232 | C9EE0CD128478FDB00E9B97A /* PathRow.swift */, 233 | C9779741284F6DE000706DFB /* ToolbarMenu.swift */, 234 | ); 235 | path = Views; 236 | sourceTree = ""; 237 | }; 238 | C9D73C23285C8B3B0044A279 /* Presentation */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */, 242 | ); 243 | path = Presentation; 244 | sourceTree = ""; 245 | }; 246 | C9DD54CC2860992200D46AB3 /* Model Views */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | C90BCE4D2861E4E400C2EF35 /* AppContent.swift */, 250 | C90BCE4B2861E37900C2EF35 /* AppHeader.swift */, 251 | C90BCE432861D3C500C2EF35 /* DeviceContent.swift */, 252 | C90BCE452861D57100C2EF35 /* DeviceHeader.swift */, 253 | C9DD54CD2860A0AF00D46AB3 /* DeviceTypeContent.swift */, 254 | C9DD54CF2860A1A500D46AB3 /* DeviceTypeHeader.swift */, 255 | C9DD54D12860A24B00D46AB3 /* RuntimeContent.swift */, 256 | C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */, 257 | ); 258 | path = "Model Views"; 259 | sourceTree = ""; 260 | }; 261 | /* End PBXGroup section */ 262 | 263 | /* Begin PBXNativeTarget section */ 264 | C982F854283B9F9000D491F4 /* SimDirs */ = { 265 | isa = PBXNativeTarget; 266 | buildConfigurationList = C982F864283B9F9200D491F4 /* Build configuration list for PBXNativeTarget "SimDirs" */; 267 | buildPhases = ( 268 | C982F851283B9F9000D491F4 /* Sources */, 269 | C982F852283B9F9000D491F4 /* Frameworks */, 270 | C982F853283B9F9000D491F4 /* Resources */, 271 | ); 272 | buildRules = ( 273 | ); 274 | dependencies = ( 275 | ); 276 | name = SimDirs; 277 | productName = SimDirs; 278 | productReference = C982F855283B9F9000D491F4 /* SimDirs.app */; 279 | productType = "com.apple.product-type.application"; 280 | }; 281 | /* End PBXNativeTarget section */ 282 | 283 | /* Begin PBXProject section */ 284 | C982F84D283B9F9000D491F4 /* Project object */ = { 285 | isa = PBXProject; 286 | attributes = { 287 | BuildIndependentTargetsInParallel = 1; 288 | LastSwiftUpdateCheck = 1340; 289 | LastUpgradeCheck = 1340; 290 | TargetAttributes = { 291 | C982F854283B9F9000D491F4 = { 292 | CreatedOnToolsVersion = 13.4; 293 | }; 294 | }; 295 | }; 296 | buildConfigurationList = C982F850283B9F9000D491F4 /* Build configuration list for PBXProject "SimDirs" */; 297 | compatibilityVersion = "Xcode 13.0"; 298 | developmentRegion = en; 299 | hasScannedForEncodings = 0; 300 | knownRegions = ( 301 | en, 302 | Base, 303 | ); 304 | mainGroup = C982F84C283B9F9000D491F4; 305 | productRefGroup = C982F856283B9F9000D491F4 /* Products */; 306 | projectDirPath = ""; 307 | projectRoot = ""; 308 | targets = ( 309 | C982F854283B9F9000D491F4 /* SimDirs */, 310 | ); 311 | }; 312 | /* End PBXProject section */ 313 | 314 | /* Begin PBXResourcesBuildPhase section */ 315 | C982F853283B9F9000D491F4 /* Resources */ = { 316 | isa = PBXResourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | C982F860283B9F9200D491F4 /* Preview Assets.xcassets in Resources */, 320 | C982F85D283B9F9200D491F4 /* Assets.xcassets in Resources */, 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | /* End PBXResourcesBuildPhase section */ 325 | 326 | /* Begin PBXSourcesBuildPhase section */ 327 | C982F851283B9F9000D491F4 /* Sources */ = { 328 | isa = PBXSourcesBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | C966877729B7641F007BB3F5 /* Node.swift in Sources */, 332 | C966877529B7641F007BB3F5 /* FilteredNodeView.swift in Sources */, 333 | C966877229B7641F007BB3F5 /* SimDevice+Node.swift in Sources */, 334 | C966876C29B7641F007BB3F5 /* FilteredNode.swift in Sources */, 335 | C966877029B7641F007BB3F5 /* SimDeviceType+Node.swift in Sources */, 336 | C90BCE4E2861E4E400C2EF35 /* AppContent.swift in Sources */, 337 | C927A0DB2846502300533D66 /* PathActions.swift in Sources */, 338 | C90BCE442861D3C500C2EF35 /* DeviceContent.swift in Sources */, 339 | C966876F29B7641F007BB3F5 /* SimPlatform+Node.swift in Sources */, 340 | C90DCC142896AAAA0072E403 /* ContentHeader.swift in Sources */, 341 | C9BF5232289FE95D00BDDC91 /* DescriptiveToggle.swift in Sources */, 342 | C966877629B7641F007BB3F5 /* NodeView.swift in Sources */, 343 | C966877129B7641F007BB3F5 /* SimRuntime+Node.swift in Sources */, 344 | C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */, 345 | C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */, 346 | C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */, 347 | C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */, 348 | C9DD54CE2860A0AF00D46AB3 /* DeviceTypeContent.swift in Sources */, 349 | C90DCC162896B0370072E403 /* AppearancePicker.swift in Sources */, 350 | C90BCE4C2861E37900C2EF35 /* AppHeader.swift in Sources */, 351 | C9DD54D02860A1A500D46AB3 /* DeviceTypeHeader.swift in Sources */, 352 | C966877429B7641F007BB3F5 /* NodeLabel.swift in Sources */, 353 | C90BCE522861EDBF00C2EF35 /* SourceFilter.swift in Sources */, 354 | C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */, 355 | C982F875283CEEBB00D491F4 /* SimDevice.swift in Sources */, 356 | C9BF5234289FE99600BDDC91 /* DescriptiveToggleStyle.swift in Sources */, 357 | C966876D29B7641F007BB3F5 /* SimApp+Node.swift in Sources */, 358 | C982F873283CE9AD00D491F4 /* SimDeviceType.swift in Sources */, 359 | C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */, 360 | C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */, 361 | C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */, 362 | C98048FC29C5FC2A00DE0DBD /* NodeAB.swift in Sources */, 363 | C982F871283CE7B800D491F4 /* SimRuntime.swift in Sources */, 364 | C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */, 365 | C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */, 366 | C982F86B283BA22100D491F4 /* SimPlatform.swift in Sources */, 367 | C927A0D92846414900533D66 /* Helpers.swift in Sources */, 368 | C90BCE462861D57100C2EF35 /* DeviceHeader.swift in Sources */, 369 | C966877329B7641F007BB3F5 /* NodeListBuilder.swift in Sources */, 370 | C982F879283D042E00D491F4 /* SimModel.swift in Sources */, 371 | C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */, 372 | C966876E29B7641F007BB3F5 /* SimProductFamily+Node.swift in Sources */, 373 | C95CC0F828B2411700928FAE /* AppearanceButtonStyle.swift in Sources */, 374 | ); 375 | runOnlyForDeploymentPostprocessing = 0; 376 | }; 377 | /* End PBXSourcesBuildPhase section */ 378 | 379 | /* Begin XCBuildConfiguration section */ 380 | C982F862283B9F9200D491F4 /* Debug */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ALWAYS_SEARCH_USER_PATHS = NO; 384 | CLANG_ANALYZER_NONNULL = YES; 385 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 387 | CLANG_ENABLE_MODULES = YES; 388 | CLANG_ENABLE_OBJC_ARC = YES; 389 | CLANG_ENABLE_OBJC_WEAK = YES; 390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 391 | CLANG_WARN_BOOL_CONVERSION = YES; 392 | CLANG_WARN_COMMA = YES; 393 | CLANG_WARN_CONSTANT_CONVERSION = YES; 394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 396 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 397 | CLANG_WARN_EMPTY_BODY = YES; 398 | CLANG_WARN_ENUM_CONVERSION = YES; 399 | CLANG_WARN_INFINITE_RECURSION = YES; 400 | CLANG_WARN_INT_CONVERSION = YES; 401 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 403 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 404 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 405 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 407 | CLANG_WARN_STRICT_PROTOTYPES = YES; 408 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 409 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 410 | CLANG_WARN_UNREACHABLE_CODE = YES; 411 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 412 | COPY_PHASE_STRIP = NO; 413 | DEBUG_INFORMATION_FORMAT = dwarf; 414 | ENABLE_STRICT_OBJC_MSGSEND = YES; 415 | ENABLE_TESTABILITY = YES; 416 | GCC_C_LANGUAGE_STANDARD = gnu11; 417 | GCC_DYNAMIC_NO_PIC = NO; 418 | GCC_NO_COMMON_BLOCKS = YES; 419 | GCC_OPTIMIZATION_LEVEL = 0; 420 | GCC_PREPROCESSOR_DEFINITIONS = ( 421 | "DEBUG=1", 422 | "$(inherited)", 423 | ); 424 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 425 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 426 | GCC_WARN_UNDECLARED_SELECTOR = YES; 427 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 428 | GCC_WARN_UNUSED_FUNCTION = YES; 429 | GCC_WARN_UNUSED_VARIABLE = YES; 430 | MACOSX_DEPLOYMENT_TARGET = 12.3; 431 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 432 | MTL_FAST_MATH = YES; 433 | ONLY_ACTIVE_ARCH = YES; 434 | SDKROOT = macosx; 435 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 436 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 437 | }; 438 | name = Debug; 439 | }; 440 | C982F863283B9F9200D491F4 /* Release */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | ALWAYS_SEARCH_USER_PATHS = NO; 444 | CLANG_ANALYZER_NONNULL = YES; 445 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 446 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 447 | CLANG_ENABLE_MODULES = YES; 448 | CLANG_ENABLE_OBJC_ARC = YES; 449 | CLANG_ENABLE_OBJC_WEAK = YES; 450 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 451 | CLANG_WARN_BOOL_CONVERSION = YES; 452 | CLANG_WARN_COMMA = YES; 453 | CLANG_WARN_CONSTANT_CONVERSION = YES; 454 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 455 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 456 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 457 | CLANG_WARN_EMPTY_BODY = YES; 458 | CLANG_WARN_ENUM_CONVERSION = YES; 459 | CLANG_WARN_INFINITE_RECURSION = YES; 460 | CLANG_WARN_INT_CONVERSION = YES; 461 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 462 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 463 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 464 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 465 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 466 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 467 | CLANG_WARN_STRICT_PROTOTYPES = YES; 468 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 469 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 470 | CLANG_WARN_UNREACHABLE_CODE = YES; 471 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 472 | COPY_PHASE_STRIP = NO; 473 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 474 | ENABLE_NS_ASSERTIONS = NO; 475 | ENABLE_STRICT_OBJC_MSGSEND = YES; 476 | GCC_C_LANGUAGE_STANDARD = gnu11; 477 | GCC_NO_COMMON_BLOCKS = YES; 478 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 479 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 480 | GCC_WARN_UNDECLARED_SELECTOR = YES; 481 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 482 | GCC_WARN_UNUSED_FUNCTION = YES; 483 | GCC_WARN_UNUSED_VARIABLE = YES; 484 | MACOSX_DEPLOYMENT_TARGET = 12.3; 485 | MTL_ENABLE_DEBUG_INFO = NO; 486 | MTL_FAST_MATH = YES; 487 | SDKROOT = macosx; 488 | SWIFT_COMPILATION_MODE = wholemodule; 489 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 490 | }; 491 | name = Release; 492 | }; 493 | C982F865283B9F9200D491F4 /* Debug */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 497 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 498 | CODE_SIGN_ENTITLEMENTS = SimDirs/SimDirs.entitlements; 499 | CODE_SIGN_STYLE = Automatic; 500 | COMBINE_HIDPI_IMAGES = YES; 501 | CURRENT_PROJECT_VERSION = 1; 502 | DEVELOPMENT_ASSET_PATHS = "\"SimDirs/Preview Content\""; 503 | ENABLE_PREVIEWS = YES; 504 | GENERATE_INFOPLIST_FILE = YES; 505 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 506 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 507 | LD_RUNPATH_SEARCH_PATHS = ( 508 | "$(inherited)", 509 | "@executable_path/../Frameworks", 510 | ); 511 | MACOSX_DEPLOYMENT_TARGET = 12.0; 512 | MARKETING_VERSION = 0.7; 513 | PRODUCT_BUNDLE_IDENTIFIER = com.sgntn.SimDirs; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | SWIFT_EMIT_LOC_STRINGS = YES; 516 | SWIFT_VERSION = 5.0; 517 | }; 518 | name = Debug; 519 | }; 520 | C982F866283B9F9200D491F4 /* Release */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 524 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 525 | CODE_SIGN_ENTITLEMENTS = SimDirs/SimDirs.entitlements; 526 | CODE_SIGN_STYLE = Automatic; 527 | COMBINE_HIDPI_IMAGES = YES; 528 | CURRENT_PROJECT_VERSION = 1; 529 | DEVELOPMENT_ASSET_PATHS = "\"SimDirs/Preview Content\""; 530 | ENABLE_PREVIEWS = YES; 531 | GENERATE_INFOPLIST_FILE = YES; 532 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 533 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 534 | LD_RUNPATH_SEARCH_PATHS = ( 535 | "$(inherited)", 536 | "@executable_path/../Frameworks", 537 | ); 538 | MACOSX_DEPLOYMENT_TARGET = 12.0; 539 | MARKETING_VERSION = 0.7; 540 | PRODUCT_BUNDLE_IDENTIFIER = com.sgntn.SimDirs; 541 | PRODUCT_NAME = "$(TARGET_NAME)"; 542 | SWIFT_EMIT_LOC_STRINGS = YES; 543 | SWIFT_VERSION = 5.0; 544 | }; 545 | name = Release; 546 | }; 547 | /* End XCBuildConfiguration section */ 548 | 549 | /* Begin XCConfigurationList section */ 550 | C982F850283B9F9000D491F4 /* Build configuration list for PBXProject "SimDirs" */ = { 551 | isa = XCConfigurationList; 552 | buildConfigurations = ( 553 | C982F862283B9F9200D491F4 /* Debug */, 554 | C982F863283B9F9200D491F4 /* Release */, 555 | ); 556 | defaultConfigurationIsVisible = 0; 557 | defaultConfigurationName = Release; 558 | }; 559 | C982F864283B9F9200D491F4 /* Build configuration list for PBXNativeTarget "SimDirs" */ = { 560 | isa = XCConfigurationList; 561 | buildConfigurations = ( 562 | C982F865283B9F9200D491F4 /* Debug */, 563 | C982F866283B9F9200D491F4 /* Release */, 564 | ); 565 | defaultConfigurationIsVisible = 0; 566 | defaultConfigurationName = Release; 567 | }; 568 | /* End XCConfigurationList section */ 569 | }; 570 | rootObject = C982F84D283B9F9000D491F4 /* Project object */; 571 | } 572 | -------------------------------------------------------------------------------- /SimDirs.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SimDirs.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimDirs.xcodeproj/xcshareddata/xcschemes/SimDirs.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_128.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_16.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_256.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_32.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_512.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/CircleSymbolBkgOff.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.804", 9 | "green" : "0.804", 10 | "red" : "0.804" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.310", 27 | "green" : "0.310", 28 | "red" : "0.310" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/CircleSymbolOff.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.306", 9 | "green" : "0.306", 10 | "red" : "0.306" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.796", 27 | "green" : "0.796", 28 | "red" : "0.796" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/ContentHeader.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC0", 9 | "green" : "0xC0", 10 | "red" : "0xC0" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x56", 27 | "green" : "0x56", 28 | "red" : "0x56" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/HeaderEdge.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE5", 9 | "green" : "0xE5", 10 | "red" : "0xE5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x35", 27 | "green" : "0x34", 28 | "red" : "0x35" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/RoundedBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.902", 9 | "green" : "0.902", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.173", 27 | "green" : "0.173", 28 | "red" : "0.173" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Colors/RoundedBorder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.902", 9 | "green" : "0.902", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.294", 27 | "green" : "0.294", 28 | "red" : "0.294" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Icon-256.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_256.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon_256@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Icon-256.imageset/icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/Icon-256.imageset/icon_256.png -------------------------------------------------------------------------------- /SimDirs/Assets.xcassets/Icon-256.imageset/icon_256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/SimDirs/Assets.xcassets/Icon-256.imageset/icon_256@2x.png -------------------------------------------------------------------------------- /SimDirs/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/23/22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct ContentView: View { 12 | enum Style: Int, CaseIterable, Identifiable { 13 | case placeholder 14 | case byDevice 15 | case byRuntime 16 | 17 | var id : Int { rawValue } 18 | var visible : Bool { self != .placeholder } 19 | 20 | var title : String { 21 | switch self { 22 | case .placeholder: return "Placeholder" 23 | case .byDevice: return "By Device" 24 | case .byRuntime: return "By Runtime" 25 | } 26 | } 27 | } 28 | 29 | @State var filter = SourceFilter.restore() 30 | @State var viewID = UUID().uuidString 31 | @State var style = Style.byDevice 32 | let model : SimModel 33 | 34 | init(model: SimModel) { 35 | self.model = model 36 | } 37 | 38 | var body: some View { 39 | VStack { 40 | NavigationView { 41 | FilteredNodeView(filter: $filter) { items } 42 | .id(viewID) 43 | .toolbar { ToolbarItem { ToolbarMenu(style: $style, filter: $filter) } } 44 | .frame(minWidth: 200) 45 | 46 | Image("Icon-256") // Initial View 47 | } 48 | } 49 | .onChange(of: style) { _ in resetView() } 50 | .environment(\.deviceUpdates, model.deviceUpdates) 51 | } 52 | 53 | @NodeListBuilder 54 | var items: [some Node] { 55 | switch style { 56 | case .placeholder: [] as [LeafNode] 57 | case .byDevice: SimProductFamily.presentation.map { $0.linked(from: model) } 58 | case .byRuntime: SimPlatform.presentation.map { $0.linked(from: model) } 59 | } 60 | } 61 | 62 | func resetView() { 63 | viewID = UUID().uuidString 64 | } 65 | } 66 | 67 | struct ContentView_Previews: PreviewProvider { 68 | static var model = SimModel() 69 | 70 | static var previews: some View { 71 | ContentView(model: model) 72 | .preferredColorScheme(.dark) 73 | ContentView(model: model) 74 | .preferredColorScheme(.light) 75 | } 76 | } 77 | 78 | private struct DeviceUpdatesKey: EnvironmentKey { 79 | static let defaultValue = PassthroughSubject() 80 | } 81 | 82 | extension EnvironmentValues { 83 | var deviceUpdates: PassthroughSubject { 84 | get { self[DeviceUpdatesKey.self] } 85 | set { self[DeviceUpdatesKey.self] = newValue } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SimDirs/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/31/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension NSPasteboard { 11 | static func copy(text: String) { 12 | general.clearContents() 13 | general.setData(text.data(using: .utf8), forType: .string) 14 | } 15 | } 16 | 17 | extension NSWorkspace { 18 | static func reveal(filepath: String) { 19 | let filepathURL = URL(fileURLWithPath: filepath) 20 | 21 | shared.activateFileViewerSelecting([filepathURL]) 22 | } 23 | } 24 | 25 | extension OptionSet where Self == Self.Element { 26 | func settingBool(_ value: Bool, options: Self) -> Self { 27 | if value { return union(options) } 28 | else { return subtracting (options) } 29 | } 30 | 31 | mutating func booleanSet(_ value: Bool, options: Self) { 32 | if value { update(with: options) } 33 | else { subtract(options) } 34 | } 35 | } 36 | 37 | extension ProcessInfo { 38 | var isPreviewing : Bool { environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" } 39 | } 40 | 41 | extension PropertyListSerialization { 42 | class func propertyList(from url: URL) -> [String : AnyObject]? { 43 | guard let plistData = try? Data(contentsOf: url) else { return nil } 44 | 45 | return try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String : AnyObject] 46 | } 47 | } 48 | 49 | extension View { 50 | @ViewBuilder 51 | func evalIf(_ test: Bool, then transform: (Self) -> V) -> some View { 52 | if test { 53 | transform(self) 54 | } 55 | else { 56 | self 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SimDirs/Model/SimApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimApp.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class SimApp: ObservableObject { 11 | @Published var state = State.unknown 12 | @Published var pid : Int? 13 | 14 | weak var device : SimDevice? 15 | let identifier : String 16 | let bundleID : String 17 | let bundleName : String 18 | let displayName : String 19 | let version : String 20 | let minOSVersion : String 21 | let bundlePath : String 22 | let sandboxPath : String? 23 | let nsIcon : NSImage? 24 | 25 | init(bundlePath: URL, sandboxPaths: [String : URL], device: SimDevice) throws { 26 | guard let infoPList = PropertyListSerialization.propertyList(from: bundlePath.appendingPathComponent("Info.plist")) else { throw SimError.invalidApp } 27 | guard let bundleID = infoPList[kCFBundleIdentifierKey as String] as? String else { throw SimError.invalidApp } 28 | 29 | self.bundlePath = bundlePath.path 30 | self.bundleID = bundleID 31 | self.device = device 32 | bundleName = (infoPList[kCFBundleNameKey as String] as? String) ?? "" 33 | displayName = (infoPList["CFBundleDisplayName"] as? String) ?? bundleName 34 | version = (infoPList["CFBundleShortVersionString"] as? String) ?? "" 35 | minOSVersion = (infoPList["MinimumOSVersion"] as? String) ?? "" 36 | sandboxPath = sandboxPaths[bundleID]?.path 37 | identifier = bundlePath.deletingLastPathComponent().lastPathComponent 38 | 39 | // Currently using the biggest image we can find in pixels. Maybe there's a better way? 40 | nsIcon = (infoPList["CFBundleIcons"] as? [String : AnyObject]).flatMap { bundleIcons -> NSImage? in 41 | guard let iconFiles = bundleIcons["CFBundlePrimaryIcon"].map({ iconEntry -> [String] in 42 | switch iconEntry { 43 | case let primaryDict as [String : AnyObject]: return primaryDict["CFBundleIconFiles"] as? [String] ?? [] 44 | case let str as String: return [str] 45 | default: return [] 46 | } 47 | }) else { return nil } 48 | var icon = NSImage() 49 | var pixelWidth = 0 50 | var validIcon = false 51 | 52 | for iconFile in iconFiles { 53 | let iconPathComps : [String] = [iconFile, "\(iconFile)@2x", "\(iconFile)@3x"] 54 | 55 | for pathComp in iconPathComps { 56 | var iconURL = bundlePath.appendingPathComponent(pathComp) 57 | 58 | if iconURL.pathExtension.isEmpty { 59 | iconURL.appendPathExtension("png") 60 | } 61 | 62 | if let testIcon = NSImage(contentsOf: iconURL) { 63 | for imageRep in testIcon.representations { 64 | if pixelWidth < imageRep.pixelsWide { 65 | icon = testIcon 66 | pixelWidth = imageRep.pixelsWide 67 | validIcon = true 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | return validIcon ? icon : nil 75 | } 76 | } 77 | 78 | func discoverState() { 79 | Task { 80 | let result = try await SimCtl().getAppPID(self) 81 | 82 | await MainActor.run { 83 | pid = result 84 | state = pid != nil ? .launched : .terminated 85 | } 86 | } 87 | } 88 | 89 | func toggleLaunchState() { 90 | Task { 91 | switch state { 92 | case .launched: 93 | try SimCtl().terminate(self) 94 | 95 | await MainActor.run { 96 | pid = nil 97 | state = .terminated 98 | } 99 | 100 | default: 101 | let result = try await SimCtl().launch(self) 102 | 103 | await MainActor.run { 104 | pid = result 105 | state = pid != nil ? .launched : .terminated 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | extension SimApp { 113 | enum State: String { 114 | case terminated = "terminated" 115 | case launched = "launched" 116 | case unknown = "unknown" 117 | 118 | var isOn : Bool { self == .launched } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /SimDirs/Model/SimCtl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimCtl.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/23/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SimCtl { 11 | func run(args: [String], run: Bool = true) throws -> Process { 12 | let process = Process() 13 | 14 | process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") 15 | process.arguments = ["simctl"] + args 16 | process.standardError = nil 17 | if run { 18 | try process.run() 19 | } 20 | 21 | return process 22 | } 23 | 24 | func run(args: [String]) throws -> Data { 25 | let process : Process = try run(args: args, run: false) 26 | let pipe = Pipe() 27 | 28 | process.standardOutput = pipe 29 | try process.run() 30 | 31 | return pipe.fileHandleForReading.readDataToEndOfFile() 32 | } 33 | 34 | func run(args: [String]) throws -> String { 35 | return try String(data: run(args: args), encoding: .utf8) ?? "" 36 | } 37 | 38 | func runAsync(args: [String]) throws { 39 | Task(priority: nil, operation: { let _ : Data = try run(args: args) }) 40 | } 41 | 42 | func runAsync(args: [String]) async throws -> Data { 43 | return try await Task(priority: nil, operation: { try run(args: args) }).value 44 | } 45 | 46 | func runAsync(args: [String]) async throws -> String { 47 | return try await Task(priority: nil, operation: { try String(data: run(args: args), encoding: .utf8) ?? "" }).value 48 | } 49 | 50 | func readAllDeviceTypes() throws -> [SimDeviceType] { 51 | let json : Data = try run(args: ["list", "-j", "devicetypes"]) 52 | 53 | return try JSONDecoder().decode([String : [SimDeviceType]].self, from: json)["devicetypes"] ?? [] 54 | } 55 | 56 | func readAllRuntimes() throws -> [SimRuntime] { 57 | let json : Data = try run(args: ["list", "-j", "runtimes"]) 58 | 59 | return try JSONDecoder().decode([String : [SimRuntime]].self, from: json)["runtimes"] ?? [] 60 | } 61 | 62 | func readAllRuntimeDevices() throws -> [String : [SimDevice]] { 63 | let json : Data = try run(args: ["list", "-j", "devices"]) 64 | 65 | return try JSONDecoder().decode([String : [String : [SimDevice]]].self, from: json)["devices"] ?? [:] 66 | } 67 | 68 | func readDevice(_ device: SimDevice) throws -> SimDevice? { 69 | let json : Data = try run(args: ["list", "-j", "devices", device.udid]) 70 | let decoded = try JSONDecoder().decode([String : [String : [SimDevice]]].self, from: json)["devices"] ?? [:] 71 | var result : SimDevice? = nil 72 | 73 | for devices in decoded.values { 74 | if let match = devices.first(where: { $0.udid == device.udid }) { 75 | result = match 76 | break 77 | } 78 | } 79 | 80 | return result 81 | } 82 | 83 | func readAllDevices() throws -> [SimDevice] { 84 | return try readAllRuntimeDevices().flatMap { $1 } 85 | } 86 | 87 | func bootDevice(_ device: SimDevice, boot: Bool) async throws { 88 | try await withThrowingTaskGroup(of: Void.self) { group in 89 | group.addTask { let _ : Data = try run(args: [boot ? "boot" : "shutdown", device.udid]) } 90 | group.addTask { try await Task.sleep(nanoseconds: 1_000_000_000) } 91 | try await group.next() // wait for timeout or run to complate 92 | } 93 | 94 | if let refreshedDev = try readDevice(device) { 95 | await MainActor.run { () -> Void in device.updateDevice(from: refreshedDev) } 96 | } 97 | } 98 | 99 | func getDeviceAppearance(_ device: SimDevice) async throws -> SimDevice.Appearance { 100 | let appearance : String = try await runAsync(args: ["ui", device.udid, "appearance"]).trimmingCharacters(in: .whitespacesAndNewlines) 101 | 102 | return SimDevice.Appearance(rawValue: appearance) ?? .unknown 103 | } 104 | 105 | func setDeviceAppearance(_ device: SimDevice, appearance: SimDevice.Appearance) throws { 106 | try runAsync(args: ["ui", device.udid, "appearance", appearance.rawValue]) 107 | } 108 | 109 | func getDeviceContentSize(_ device: SimDevice) async throws -> SimDevice.ContentSize { 110 | let contentSize : String = try await runAsync(args: ["ui", device.udid, "content_size"]).trimmingCharacters(in: .whitespacesAndNewlines) 111 | 112 | return SimDevice.ContentSize(rawValue: contentSize) ?? .unknown 113 | } 114 | 115 | func setDeviceContentSize(_ device: SimDevice, contentSize: SimDevice.ContentSize) throws { 116 | try runAsync(args: ["ui", device.udid, "content_size", contentSize.rawValue]) 117 | } 118 | 119 | func getDeviceIncreaseContrast(_ device: SimDevice) async throws -> SimDevice.IncreaseContrast { 120 | let increaseContrast : String = try await runAsync(args: ["ui", device.udid, "increase_contrast"]).trimmingCharacters(in: .whitespacesAndNewlines) 121 | 122 | return SimDevice.IncreaseContrast(rawValue: increaseContrast) ?? .unknown 123 | } 124 | 125 | func setDeviceIncreaseContrast(_ device: SimDevice, increaseContrast: SimDevice.IncreaseContrast) throws { 126 | try runAsync(args: ["ui", device.udid, "increase_contrast", increaseContrast.rawValue]) 127 | } 128 | 129 | func sendPushNotification(_ device: SimDevice, payload: Data) throws { 130 | let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 131 | let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString) 132 | 133 | try payload.write(to: temporaryFileURL, options: .atomic) 134 | try runAsync(args: ["push", device.udid, temporaryFileURL.path]) 135 | } 136 | 137 | func saveScreen(_ device: SimDevice, url: URL) throws { 138 | try runAsync(args: ["io", device.udid, "screenshot", url.path]) 139 | } 140 | 141 | func saveVideo(_ device: SimDevice, url: URL) throws -> Process { 142 | return try run(args: ["io", device.udid, "recordVideo", "--force", url.path]) 143 | } 144 | 145 | func getAppPID(_ app: SimApp) async throws -> Int? { 146 | guard let device = app.device else { return nil } 147 | let list : String = try await runAsync(args: ["spawn", device.udid, "launchctl", "list"]) 148 | let regex = try NSRegularExpression(pattern: "(?[0-9]+).*\(app.bundleID)") 149 | let nsRange = NSRange(location: 0, length: (list as NSString).length) 150 | var pid : Int? = nil 151 | 152 | if let match = regex.firstMatch(in: list, range: nsRange) { 153 | let range = match.range(withName: "PID") 154 | 155 | if range.location != NSNotFound { 156 | pid = Int((list as NSString).substring(with: range)) 157 | } 158 | } 159 | 160 | return pid 161 | } 162 | 163 | func launch(_ app: SimApp) async throws -> Int? { 164 | guard let device = app.device else { return nil } 165 | let output : String = try await runAsync(args: ["launch", device.udid, app.bundleID]) 166 | let regex = try NSRegularExpression(pattern: ".*: (?[0-9]+)") 167 | let nsRange = NSRange(location: 0, length: (output as NSString).length) 168 | var pid : Int? = nil 169 | 170 | if let match = regex.firstMatch(in: output, range: nsRange) { 171 | let range = match.range(withName: "PID") 172 | 173 | if range.location != NSNotFound { 174 | pid = Int((output as NSString).substring(with: range)) 175 | } 176 | } 177 | 178 | return pid 179 | } 180 | 181 | func terminate(_ app: SimApp) throws { 182 | guard let device = app.device else { return } 183 | 184 | try runAsync(args: ["terminate", device.udid, app.bundleID]) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /SimDirs/Model/SimDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimDevice.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/24/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class SimDevice: ObservableObject, Decodable { 11 | enum CodingKeys: String, CodingKey { 12 | case availabilityError 13 | case dataPath 14 | case dataPathSize 15 | case deviceTypeIdentifier 16 | case isAvailable 17 | case logPath 18 | case name 19 | case state 20 | case udid 21 | } 22 | 23 | @Published var name : String 24 | @Published var state : State 25 | @Published var isAvailable : Bool 26 | @Published var availabilityError : String? 27 | @Published var appearance = Appearance.unknown 28 | @Published var contentSize = ContentSize.unknown 29 | @Published var increaseContrast = IncreaseContrast.unknown 30 | @Published var isRecording = false 31 | var recordingProcess : Process? 32 | var isTransitioning : Bool { state == .booting || state == .shuttingDown } 33 | var isBooted : Bool { 34 | get { state.showBooted == true } 35 | set { bootDevice(newValue) } 36 | } 37 | 38 | let udid : String 39 | let dataPath : String 40 | let dataPathSize : Int 41 | let logPath : String 42 | let deviceTypeIdentifier : String 43 | var deviceModel : String? 44 | var apps = [SimApp]() 45 | var dataURL : URL { URL(fileURLWithPath: dataPath) } 46 | var logURL : URL { URL(fileURLWithPath: logPath) } 47 | var bundleContainerURL : URL { dataURL.appendingPathComponent("Containers/Bundle/Application") } 48 | var dataContainerURL : URL { dataURL.appendingPathComponent("Containers/Data/Application") } 49 | 50 | required init(from decoder: Decoder) throws { 51 | let values = try decoder.container(keyedBy: CodingKeys.self) 52 | 53 | availabilityError = try values.decodeIfPresent(String.self, forKey: .availabilityError) 54 | dataPath = try values.decode(String.self, forKey: .dataPath) 55 | dataPathSize = try values.decode(Int.self, forKey: .dataPathSize) 56 | deviceTypeIdentifier = try values.decode(String.self, forKey: .deviceTypeIdentifier) 57 | isAvailable = try values.decode(Bool.self, forKey: .isAvailable) 58 | logPath = try values.decode(String.self, forKey: .logPath) 59 | name = try values.decode(String.self, forKey: .name) 60 | state = try values.decode(State.self, forKey: .state) 61 | udid = try values.decode(String.self, forKey: .udid) 62 | } 63 | 64 | func isDeviceOfType(_ deviceType: SimDeviceType) -> Bool { 65 | return deviceTypeIdentifier == deviceType.identifier 66 | } 67 | 68 | func completeSetup(with devTypes: [SimDeviceType]) { 69 | deviceModel = devTypes.first(where: { $0.identifier == deviceTypeIdentifier })?.name 70 | scanApplications() 71 | } 72 | 73 | func scanApplications() { 74 | let fileManager = FileManager.default 75 | var sandboxPaths = [String : URL]() 76 | 77 | if let dataDirs = try? fileManager.contentsOfDirectory(at: dataContainerURL, includingPropertiesForKeys: nil) { 78 | for dataDir in dataDirs { 79 | let metadataURL = dataDir.appendingPathComponent(".com.apple.mobile_container_manager.metadata.plist") 80 | guard let metadata = PropertyListSerialization.propertyList(from: metadataURL) else { continue } 81 | guard let bundleID = metadata["MCMMetadataIdentifier"] as? String else { continue } 82 | 83 | sandboxPaths[bundleID] = dataDir 84 | } 85 | } 86 | 87 | if let bundleDirs = try? fileManager.contentsOfDirectory(at: bundleContainerURL, includingPropertiesForKeys: nil) { 88 | apps.removeAll() 89 | for bundleDir in bundleDirs { 90 | guard let testDirs = try? fileManager.contentsOfDirectory(at: bundleDir, includingPropertiesForKeys: nil) else { continue } 91 | 92 | for testDir in testDirs { 93 | if NSWorkspace.shared.isFilePackage(atPath: testDir.path) { 94 | do { 95 | apps.append(try SimApp(bundlePath: testDir, sandboxPaths: sandboxPaths, device: self)) 96 | } 97 | catch { 98 | print("Failed to instantiate SimApp at \(testDir.path)") 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | func hasChanged(from other: SimDevice) -> Bool { 107 | return !(name == other.name && state == other.state && isAvailable == other.isAvailable && availabilityError == other.availabilityError) 108 | } 109 | 110 | @discardableResult func updateDevice(from other: SimDevice) -> Bool { 111 | guard hasChanged(from: other) else { return false } 112 | 113 | name = other.name 114 | state = other.state 115 | isAvailable = other.isAvailable 116 | availabilityError = other.availabilityError 117 | 118 | return true 119 | } 120 | 121 | func bootDevice(_ boot: Bool) { 122 | if boot && state == .shutdown || !boot && state == .booted { 123 | state = boot ? .booting : .shuttingDown 124 | Task { 125 | do { 126 | try await SimCtl().bootDevice(self, boot: boot) 127 | } catch { 128 | print("Failed to \(boot ? "boot" : "shutdown") device: \(error)") 129 | } 130 | } 131 | } 132 | } 133 | 134 | func discoverUI() { 135 | if appearance == .unknown { 136 | Task { 137 | let result = try await SimCtl().getDeviceAppearance(self) 138 | 139 | await MainActor.run { appearance = result } 140 | } 141 | } 142 | if contentSize == .unknown { 143 | Task { 144 | let result = try await SimCtl().getDeviceContentSize(self) 145 | 146 | await MainActor.run { contentSize = result } 147 | } 148 | } 149 | if increaseContrast == .unknown { 150 | Task { 151 | let result = try await SimCtl().getDeviceIncreaseContrast(self) 152 | 153 | await MainActor.run { increaseContrast = result } 154 | } 155 | } 156 | } 157 | 158 | func setAppearance(_ appearance: Appearance) { 159 | self.appearance = appearance // optimistic 160 | 161 | do { 162 | try SimCtl().setDeviceAppearance(self, appearance: appearance) 163 | } catch { 164 | print("Failed to set device appeaarnce: \(error)") 165 | } 166 | } 167 | 168 | func setContenSize(_ contentSize: ContentSize) { 169 | self.contentSize = contentSize // optimistic 170 | 171 | do { 172 | try SimCtl().setDeviceContentSize(self, contentSize: contentSize) 173 | } catch { 174 | print("Failed to set device content size: \(error)") 175 | } 176 | } 177 | 178 | func setIncreaseContrast(_ increaseContrast: IncreaseContrast) { 179 | self.increaseContrast = increaseContrast // optimistic 180 | 181 | do { 182 | try SimCtl().setDeviceIncreaseContrast(self, increaseContrast: increaseContrast) 183 | } catch { 184 | print("Failed to set device increase contrast: \(error)") 185 | } 186 | } 187 | 188 | func saveScreen(_ url: URL) { 189 | do { 190 | try SimCtl().saveScreen(self, url: url) 191 | } catch { 192 | print("Failed to save screen: \(error)") 193 | } 194 | } 195 | 196 | func saveVideo(_ url: URL) { 197 | do { 198 | recordingProcess = try SimCtl().saveVideo(self, url: url) 199 | if recordingProcess != nil { 200 | isRecording = true 201 | } 202 | } catch { 203 | print("Failed to save video: \(error)") 204 | } 205 | } 206 | 207 | func endRecording() { 208 | if let process = recordingProcess { 209 | process.interrupt() 210 | recordingProcess = nil 211 | isRecording = false 212 | } 213 | else { 214 | isRecording = false 215 | } 216 | } 217 | 218 | func sendPushNotification(payload: Data) { 219 | do { 220 | try SimCtl().sendPushNotification(self, payload: payload) 221 | } catch { 222 | print("Failed to send push notification: \(error)") 223 | } 224 | } 225 | } 226 | 227 | extension SimDevice { 228 | enum State: String, Decodable { 229 | case booting = "Booting" 230 | case booted = "Booted" 231 | case shuttingDown = "Shutting Down" 232 | case shutdown = "Shutdown" 233 | 234 | var showBooted : Bool { 235 | switch self { 236 | case .booted, .booting: return true 237 | default: return false 238 | } 239 | } 240 | } 241 | 242 | enum Appearance: String { 243 | case light = "light" 244 | case dark = "dark" 245 | case unsupported = "unsupported" 246 | case unknown = "unknown" 247 | } 248 | 249 | enum ContentSize: String { 250 | case XS = "extra-small" 251 | case S = "small" 252 | case M = "medium" 253 | case L = "large" 254 | case XL = "extra-large" 255 | case XXL = "extra-extra-large" 256 | case XXXL = "extra-extra-extra-large" 257 | case A12Y_M = "accessibility-medium" 258 | case A12Y_L = "accessibility-large" 259 | case A12Y_XL = "accessibility-extra-large" 260 | case A12Y_XXL = "accessibility-extra-extra-large" 261 | case A12Y_XXXL = "accessibility-extra-extra-extra-large" 262 | case unsupported = "unsupported" 263 | case unknown = "unknown" 264 | 265 | static var range : ClosedRange { Double(ContentSize.XS.intValue)...Double(ContentSize.A12Y_XXXL.intValue) } 266 | 267 | var intValue : Int { 268 | switch self { 269 | case .XS: return 0 270 | case .S: return 1 271 | case .M: return 2 272 | case .L: return 3 273 | case .XL: return 4 274 | case .XXL: return 5 275 | case .XXXL: return 6 276 | case .A12Y_M: return 7 277 | case .A12Y_L: return 8 278 | case .A12Y_XL: return 8 279 | case .A12Y_XXL: return 10 280 | case .A12Y_XXXL: return 11 281 | case .unsupported: return 12 282 | case .unknown: return 13 283 | } 284 | } 285 | 286 | init(intValue: Int) { 287 | switch intValue { 288 | case 0: self = .XS 289 | case 1: self = .S 290 | case 2: self = .M 291 | case 3: self = .L 292 | case 4: self = .XL 293 | case 5: self = .XXL 294 | case 6: self = .XXXL 295 | case 7: self = .A12Y_M 296 | case 8: self = .A12Y_L 297 | case 9: self = .A12Y_XL 298 | case 10: self = .A12Y_XXL 299 | case 11: self = .A12Y_XXXL 300 | case 12: self = .unsupported 301 | default: self = .unknown 302 | } 303 | } 304 | } 305 | 306 | enum IncreaseContrast: String { 307 | case enabled = "enabled" 308 | case disabled = "disabled" 309 | case unsupported = "unsupported" 310 | case unknown = "unknown" 311 | 312 | var isOn : Bool { self == .enabled } 313 | } 314 | } 315 | 316 | extension Array where Element == SimDevice { 317 | func of(deviceType: SimDeviceType) -> Self { 318 | filter { $0.isDeviceOfType(deviceType) } 319 | } 320 | 321 | func completeSetup(with devTypes: [SimDeviceType]) { 322 | for device in self { device.completeSetup(with: devTypes) } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /SimDirs/Model/SimDeviceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimDeviceType.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/24/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SimDeviceType: Decodable { 11 | // enum CodingKeys: String, CodingKey { 12 | // case bundlePath 13 | // case identifier 14 | // case maxRuntimeVersion 15 | // case maxRuntimeVersionString 16 | // case minRuntimeVersion 17 | // case minRuntimeVersionString 18 | // case modelIdentifier 19 | // case name 20 | // case productFamily 21 | // } 22 | // 23 | let name : String 24 | let identifier : String 25 | let productFamily : SimProductFamily 26 | let modelIdentifier : String 27 | let bundlePath : String 28 | let minRuntimeVersion : Int 29 | let maxRuntimeVersion : Int 30 | let minRuntimeVersionString : String 31 | let maxRuntimeVersionString : String 32 | 33 | func supports(productFamily: SimProductFamily) -> Bool { 34 | return productFamily == self.productFamily 35 | } 36 | 37 | func supports(runtime: SimRuntime) -> Bool { 38 | return runtime.supportedDeviceTypes.contains { $0.identifier == identifier } 39 | } 40 | } 41 | 42 | extension Array where Element == SimDeviceType { 43 | func supporting(productFamily: SimProductFamily) -> Self { 44 | filter { $0.supports(productFamily: productFamily) } 45 | } 46 | 47 | func supporting(runtime: SimRuntime) -> Self { 48 | filter { $0.supports(runtime: runtime) } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /SimDirs/Model/SimModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimModel.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/24/22. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | enum SimError: Error { 12 | case deviceParsingFailure 13 | case invalidApp 14 | } 15 | 16 | class SimModel { 17 | struct Update { 18 | let runtime : SimRuntime 19 | var additions : [SimDevice] 20 | var removals : [SimDevice] 21 | } 22 | 23 | var deviceTypes : [SimDeviceType] 24 | var runtimes : [SimRuntime] 25 | var monitor : Cancellable? 26 | let updateInterval = 2.0 27 | 28 | var devices : [SimDevice] { runtimes.flatMap { $0.devices } } 29 | var apps : [SimApp] { devices.flatMap { $0.apps } } 30 | 31 | var deviceUpdates = PassthroughSubject() 32 | 33 | init() { 34 | let simctl = SimCtl() 35 | 36 | do { 37 | let runtimeDevs : [String : [SimDevice]] 38 | 39 | deviceTypes = try simctl.readAllDeviceTypes() 40 | runtimes = try simctl.readAllRuntimes() 41 | runtimeDevs = try simctl.readAllRuntimeDevices() 42 | for (runtimeID, devices) in runtimeDevs { 43 | do { 44 | let runtimeIdx = try runtimes.indexOfMatchedOrCreated(identifier: runtimeID) 45 | 46 | runtimes[runtimeIdx].setDevices(devices, from: deviceTypes) 47 | } 48 | catch { 49 | print("Warning: Unable to create placeholder runtime from \(runtimeID)") 50 | } 51 | } 52 | runtimes.sort() 53 | devices.completeSetup(with: deviceTypes) 54 | 55 | if !ProcessInfo.processInfo.isPreviewing { 56 | beginMonitor() 57 | } 58 | } 59 | catch { 60 | fatalError("Failed to initialize data model:\n\(error)") 61 | } 62 | } 63 | 64 | func beginMonitor() { 65 | monitor = Timer.publish(every: updateInterval, on: .main, in: .default) 66 | .autoconnect() 67 | .receive(on: DispatchQueue.global(qos: .background)) 68 | .flatMap { _ in 69 | Just((try? SimCtl().readAllRuntimeDevices()) ?? [String : [SimDevice]]()) 70 | } 71 | .receive(on: DispatchQueue.main) 72 | .sink { [weak self] runtimeDevs in 73 | guard let this = self else { return } 74 | 75 | for (runtimeID, curDevices) in runtimeDevs { 76 | guard let runtime = this.runtimes.first(where: { $0.identifier == runtimeID }) else { print("missing runtime: \(runtimeID)"); continue } 77 | 78 | if let changes = runtime.reconcileDevices(curDevices, forTypes: this.deviceTypes) { 79 | this.deviceUpdates.send(changes) 80 | } 81 | 82 | for srcDevice in curDevices { 83 | guard let dstDevice = runtime.devices.first(where: { $0.udid == srcDevice.udid }) else { print("missing device: \(srcDevice.udid)"); continue } 84 | 85 | if dstDevice.updateDevice(from: srcDevice) { 86 | print("\(dstDevice.udid) updated: \(dstDevice.state)") 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SimDirs/Model/SimPlatform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimPlatform.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/23/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SimPlatform: String, Decodable { 11 | case iOS 12 | case tvOS 13 | case watchOS 14 | 15 | static let presentation : [SimPlatform] = [.iOS, .watchOS, .tvOS] 16 | 17 | var symbolName : String { 18 | switch self { 19 | case .iOS: return "iphone" 20 | case .tvOS: return "appletv" 21 | case .watchOS: return "applewatch" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SimDirs/Model/SimProductFamily.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimProductFamily.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/24/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SimProductFamily: String, Decodable { 11 | case appleTV = "Apple TV" 12 | case appleWatch = "Apple Watch" 13 | case iPad 14 | case iPhone 15 | 16 | static let presentation : [SimProductFamily] = [.iPhone, .iPad, .appleWatch, .appleTV] 17 | 18 | var symbolName : String { 19 | switch self { 20 | case .iPad: return "ipad" 21 | case .iPhone: return "iphone" 22 | case .appleTV: return "appletv" 23 | case .appleWatch: return "applewatch" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SimDirs/Model/SimRuntime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimRuntime.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/24/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class SimRuntime: ObservableObject, Comparable, Decodable { 11 | enum CodingKeys: String, CodingKey { 12 | case availabilityError 13 | case bundlePath 14 | case buildversion 15 | case identifier 16 | case isAvailable 17 | case isInternal 18 | case name 19 | case platform 20 | case runtimeRoot 21 | case supportedDeviceTypes 22 | case version 23 | } 24 | 25 | struct DeviceType: Decodable { 26 | let name : String 27 | let bundlePath : String 28 | let identifier : String 29 | let productFamily : SimProductFamily 30 | 31 | init(canonical: SimDeviceType) { 32 | name = canonical.name 33 | bundlePath = canonical.bundlePath 34 | identifier = canonical.identifier 35 | productFamily = canonical.productFamily 36 | } 37 | } 38 | 39 | @Published var devices = [SimDevice]() 40 | 41 | let name : String 42 | let version : String 43 | let identifier : String 44 | let platform : SimPlatform 45 | 46 | let bundlePath : String 47 | let buildversion : String 48 | let runtimeRoot : String 49 | let isInternal : Bool 50 | let isAvailable : Bool 51 | var supportedDeviceTypes : [DeviceType] 52 | let availabilityError : String? 53 | 54 | var isPlaceholder = false 55 | 56 | static func < (lhs: SimRuntime, rhs: SimRuntime) -> Bool { 57 | return lhs.name < rhs.name 58 | } 59 | 60 | static func == (lhs: SimRuntime, rhs: SimRuntime) -> Bool { 61 | return lhs.identifier == rhs.identifier 62 | } 63 | 64 | init(platformID: String) throws { 65 | guard let lastComponent = platformID.split(separator: ".").last else { throw SimError.deviceParsingFailure } 66 | let vComps = lastComponent.split(separator: "-") 67 | 68 | if vComps.count == 3 { 69 | guard let compPlatform = SimPlatform(rawValue: String(vComps[0])) else { throw SimError.deviceParsingFailure } 70 | guard let major = Int(vComps[1]) else { throw SimError.deviceParsingFailure } 71 | guard let minor = Int(vComps[2]) else { throw SimError.deviceParsingFailure } 72 | 73 | platform = compPlatform 74 | version = "\(major).\(minor)" 75 | name = "\(platform) \(version)" 76 | identifier = platformID 77 | 78 | bundlePath = "" 79 | buildversion = "" 80 | runtimeRoot = "" 81 | isInternal = false 82 | isAvailable = false 83 | supportedDeviceTypes = [] 84 | availabilityError = "Missing runtime" 85 | isPlaceholder = true 86 | } 87 | else { 88 | throw SimError.deviceParsingFailure 89 | } 90 | } 91 | 92 | func supports(deviceType: SimDeviceType) -> Bool { 93 | return supportedDeviceTypes.contains { $0.identifier == deviceType.identifier } 94 | } 95 | 96 | func supports(platform: SimPlatform) -> Bool { 97 | return self.platform == platform 98 | } 99 | 100 | func setDevices(_ devices: [SimDevice], from devTypes: [SimDeviceType]) { 101 | self.devices = devices 102 | 103 | // If this runtime is a placeholder it will be missing supported device types 104 | // create device type stubs based on the devices being added using supplied 105 | // fully described device types 106 | 107 | if isPlaceholder { 108 | let devTypeIDs = Set(devices.map({ $0.deviceTypeIdentifier })) 109 | 110 | self.supportedDeviceTypes = devTypeIDs.compactMap { devTypeID in 111 | devTypes.first(where: { $0.identifier == devTypeID }).map({ SimRuntime.DeviceType(canonical: $0) }) 112 | } 113 | } 114 | } 115 | 116 | func reconcileDevices(_ curDevices: [SimDevice], forTypes deviceTypes: [SimDeviceType]) -> SimModel.Update? { 117 | let curDevIDs = curDevices.map { $0.udid } 118 | let ourDevIDs = devices.map { $0.udid } 119 | let additions = curDevices.filter { !ourDevIDs.contains($0.udid) } 120 | let removals = devices.filter { !curDevIDs.contains($0.udid) } 121 | var result : SimModel.Update? = nil 122 | 123 | if !additions.isEmpty || !removals.isEmpty { 124 | let idsToRemove = removals.map { $0.udid } 125 | 126 | additions.completeSetup(with: deviceTypes) 127 | devices.removeAll(where: { idsToRemove.contains($0.udid) }) 128 | devices.append(contentsOf: additions) 129 | 130 | result = SimModel.Update(runtime: self, additions: additions, removals: removals) 131 | } 132 | 133 | return result 134 | } 135 | } 136 | 137 | extension Array where Element == SimRuntime { 138 | mutating func indexOfMatchedOrCreated(identifier: String) throws -> Index { 139 | return try firstIndex { $0.identifier == identifier } ?? { 140 | try self.append(SimRuntime(platformID: identifier)) 141 | 142 | return self.endIndex - 1 143 | }() 144 | } 145 | 146 | func supporting(deviceType: SimDeviceType) -> Self { 147 | filter { $0.supports(deviceType: deviceType) } 148 | } 149 | 150 | func supporting(platform: SimPlatform) -> Self { 151 | filter { $0.supports(platform: platform) } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /SimDirs/Node/Conforming/SimApp+Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimApp+Node.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SimApp: Node { 11 | var title : String { return displayName } 12 | var headerTitle : String { "App: \(title)" } 13 | 14 | var header : some View { AppHeader(app: self) } 15 | var content : some View { AppContent(app: self) } 16 | 17 | func icon(forHeader: Bool) -> some View { 18 | if let nsIcon = nsIcon { 19 | let iconSize : CGFloat = forHeader ? 128 : 20 20 | 21 | Image(nsImage: nsIcon) 22 | .resizable() 23 | .aspectRatio(contentMode: .fit) 24 | .cornerRadius(iconSize / 5.0) 25 | .shadow(radius: 4.0, x: 2.0, y: 2.0) 26 | .frame(maxWidth: iconSize, maxHeight: iconSize) 27 | } 28 | else { 29 | symbolIcon("questionmark.app.dashed", forHeader: forHeader) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /SimDirs/Node/Conforming/SimDevice+Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimDevice+Node.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // SimDevice requires a wrapper to simulate Node conformance because its 11 | // icon is provided by a SimDeviceType 12 | 13 | struct SimDeviceNode: Node { 14 | let device : SimDevice 15 | var iconName : String 16 | 17 | var title : String { device.name } 18 | var headerTitle : String { "Device: \(title)" } 19 | var header : some View { DeviceHeader(device) } 20 | var content : some View { DeviceContent(device) } 21 | var items : [SimApp]? { 22 | get { device.apps } 23 | set { device.apps = newValue ?? [] } 24 | } 25 | 26 | init(_ device: SimDevice, iconName: String) { 27 | self.device = device 28 | self.iconName = iconName 29 | } 30 | 31 | func icon(forHeader: Bool) -> some View { 32 | symbolIcon(iconName, color: device.isAvailable ? .green : .red, forHeader: forHeader) 33 | } 34 | 35 | func matchedFilterOptions() -> SourceFilter.Options { 36 | return !device.apps.isEmpty ? .withApps : [] 37 | } 38 | } 39 | 40 | extension Array where Element == SimDevice { 41 | func nodesFor(deviceType: SimDeviceType) -> [SimDeviceNode] { 42 | filter({ $0.isDeviceOfType(deviceType) }).map({ SimDeviceNode($0, iconName: deviceType.productFamily.symbolName) }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SimDirs/Node/Conforming/SimDeviceType+Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimDeviceType+Node.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SimDeviceType: Node { 11 | var title : String { return name } 12 | var headerTitle : String { "Device Type: \(title)" } 13 | 14 | var header : some View { DeviceTypeHeader(deviceType: self) } 15 | var content : some View { DeviceTypeContent(deviceType: self) } 16 | 17 | func icon(forHeader: Bool) -> some View { 18 | symbolIcon(productFamily.symbolName, forHeader: forHeader) 19 | } 20 | 21 | func linkedForDeviceStyle(from model: SimModel) -> some Node { 22 | NodeLink(self) { 23 | model.runtimes.supporting(deviceType: self).map { runtime in 24 | runtime.linkedForDeviceStyle(from: model, deviceType: self) 25 | } 26 | } 27 | } 28 | 29 | func linkedForRuntimeStyle(from model: SimModel, runtime: SimRuntime) -> some Node { 30 | var node = NodeLink(self, items: runtime.devices.nodesFor(deviceType: self)) 31 | 32 | return node.onUpdate { update in 33 | guard let runtime = model.runtimes.supporting(deviceType: self).first(where: { $0 == update.runtime }) else { return nil } 34 | let ourAdditions = update.additions.filter({ $0.deviceTypeIdentifier == identifier }) 35 | let ourRemovals = update.removals.filter({ $0.deviceTypeIdentifier == identifier }) 36 | 37 | if !ourAdditions.isEmpty || !ourRemovals.isEmpty { 38 | return runtime.devices.nodesFor(deviceType: self) 39 | } 40 | else { 41 | return nil 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SimDirs/Node/Conforming/SimPlatform+Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimPlatform+Node.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SimPlatform: Node { 11 | var title : String { self.rawValue } 12 | var headerTitle : String { "Platform: \(title)" } 13 | 14 | var header : some View { get { EmptyView() } } 15 | var content : some View { get { EmptyView() } } 16 | 17 | func icon(forHeader: Bool) -> some View { 18 | symbolIcon(symbolName, forHeader: forHeader) 19 | } 20 | 21 | func linked(from model: SimModel) -> some Node { 22 | NodeLink(self) { 23 | model.runtimes.supporting(platform: self).map { runtime in 24 | runtime.linkedForRuntimeStyle(from: model) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimDirs/Node/Conforming/SimProductFamily+Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimProductFamily+Node.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SimProductFamily: Node { 11 | var title : String { self.rawValue } 12 | var headerTitle : String { "Product Family: \(title)" } 13 | 14 | var header : some View { get { EmptyView() } } 15 | var content : some View { get { EmptyView() } } 16 | 17 | func icon(forHeader: Bool) -> some View { 18 | symbolIcon(symbolName, forHeader: forHeader) 19 | } 20 | 21 | func linked(from model: SimModel) -> some Node { 22 | NodeLink(self) { 23 | model.deviceTypes.supporting(productFamily: self).map { deviceType in 24 | deviceType.linkedForDeviceStyle(from: model) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimDirs/Node/Conforming/SimRuntime+Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimRuntime+Node.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SimRuntime: Node { 11 | var title : String { return name } 12 | var headerTitle : String { "Runtime: \(title)" } 13 | 14 | var header : some View { RuntimeHeader(runtime: self) } 15 | var content : some View { RuntimeContent(runtime: self) } 16 | 17 | func icon(forHeader: Bool) -> some View { 18 | symbolIcon("shippingbox", color: isAvailable ? .green : .red, forHeader: forHeader) 19 | } 20 | 21 | func matchedFilterOptions() -> SourceFilter.Options { 22 | return isAvailable ? .runtimeInstalled : [] 23 | } 24 | 25 | func linkedForDeviceStyle(from model: SimModel, deviceType: SimDeviceType) -> some Node { 26 | var node = NodeLink(self) { devices.nodesFor(deviceType: deviceType) } 27 | 28 | return node.onUpdate { [weak self] update in 29 | guard let this = self else { return nil } 30 | guard update.runtime == this else { return nil } 31 | let ourAdditions = update.additions.filter({ $0.deviceTypeIdentifier == deviceType.identifier }) 32 | let ourRemovals = update.removals.filter({ $0.deviceTypeIdentifier == deviceType.identifier }) 33 | 34 | if !ourAdditions.isEmpty || !ourRemovals.isEmpty { 35 | return this.devices.nodesFor(deviceType: deviceType) 36 | } 37 | else { 38 | return nil 39 | } 40 | } 41 | } 42 | 43 | func linkedForRuntimeStyle(from model: SimModel) -> some Node { 44 | NodeLink(self) { 45 | model.deviceTypes.supporting(runtime: self).map { devType in 46 | devType.linkedForRuntimeStyle(from: model, runtime: self) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SimDirs/Node/FilteredNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilteredNode.swift 3 | // NodeItems 4 | // 5 | // Created by Casey Fleser on 3/3/23. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | class FilteredNode: Node, ObservableObject { 12 | typealias FilteredList = [FilteredNode] 13 | 14 | @Published var filtered : Bool 15 | @Published var isExpanded = false 16 | @Published var items : FilteredList? 17 | 18 | var wrappedNode : T 19 | var title : String { wrappedNode.title } 20 | var headerTitle : String { wrappedNode.headerTitle } 21 | var header : some View { wrappedNode.header } 22 | var content : some View { wrappedNode.content } 23 | var children : FilteredList { items ?? [] } 24 | 25 | init(_ node: T) { 26 | self.wrappedNode = node 27 | self.filtered = false 28 | 29 | self.items = node.items?.asFilteredNodes() 30 | } 31 | 32 | func icon(forHeader: Bool) -> some View { 33 | wrappedNode.icon(forHeader: forHeader) 34 | } 35 | 36 | func toggleExpanded(_ expanded: Bool? = nil, deep: Bool) { 37 | isExpanded = expanded ?? !isExpanded 38 | 39 | if deep { 40 | for child in children { 41 | child.toggleExpanded(isExpanded, deep: true) 42 | } 43 | } 44 | } 45 | 46 | func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool { 47 | wrappedNode.matchesFilter(filter, inherited: options) 48 | } 49 | 50 | func matchedFilterOptions() -> SourceFilter.Options { 51 | return wrappedNode.matchedFilterOptions() 52 | } 53 | 54 | @discardableResult 55 | func applyFilter(_ filter: SourceFilter, inheriting options: SourceFilter.Options = []) -> Bool { 56 | let updatedOptions = options.union(matchedFilterOptions()) 57 | let childMatch = children.reduce(false) { result, node in 58 | node.applyFilter(filter, inheriting: updatedOptions) || result // deliberately not short circuiting here 59 | } 60 | let nodeMatch = childMatch || wrappedNode.matchesFilter(filter, inherited: updatedOptions) 61 | 62 | filtered = !nodeMatch 63 | 64 | return nodeMatch 65 | } 66 | 67 | @discardableResult 68 | func processUpdate(_ update: SimModel.Update) -> Bool { 69 | if wrappedNode.processUpdate(update) { 70 | items = wrappedNode.items?.asFilteredNodes() 71 | } 72 | 73 | if let items { 74 | for node in items { 75 | node.processUpdate(update) 76 | } 77 | } 78 | 79 | return false 80 | } 81 | } 82 | 83 | extension Array where Element: Node { 84 | func asFilteredNodes() -> [FilteredNode] { 85 | self.map { FilteredNode($0) } 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /SimDirs/Node/Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Node.swift 3 | // NodeItems 4 | // 5 | // Created by Casey Fleser on 3/2/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol Node { 11 | associatedtype Icon: View 12 | associatedtype Header: View 13 | associatedtype Content: View 14 | associatedtype Child: Node 15 | 16 | var items : [Child]? { get set } 17 | 18 | var title : String { get } 19 | var headerTitle : String { get } 20 | 21 | @ViewBuilder var header : Header { get } 22 | @ViewBuilder var content : Content { get } 23 | 24 | @ViewBuilder 25 | func icon(forHeader: Bool) -> Icon 26 | 27 | func matchedFilterOptions() -> SourceFilter.Options 28 | func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool 29 | @discardableResult 30 | mutating func processUpdate(_ update: SimModel.Update) -> Bool 31 | } 32 | 33 | extension Node { 34 | var items : [LeafNode]? { get { nil } set { } } 35 | 36 | @ViewBuilder 37 | func symbolIcon(_ systemName: String, color: Color? = nil, forHeader: Bool) -> some View { 38 | if forHeader { 39 | Image(systemName: systemName) 40 | .resizable() 41 | .aspectRatio(contentMode: .fit) 42 | .frame(maxWidth: 128, maxHeight: 128) 43 | .shadow(radius: 4.0, x: 2.0, y: 2.0) 44 | } 45 | else { 46 | Image(systemName: systemName) 47 | .foregroundColor(color ?? .primary) 48 | .symbolRenderingMode(.hierarchical) 49 | } 50 | } 51 | 52 | func callAsFunction(emptyIsNil: Bool = false, @NodeListBuilder items: () -> [Item]) -> NodeLink { 53 | link(emptyIsNil: emptyIsNil, to: items) 54 | } 55 | 56 | func link(emptyIsNil: Bool = false, @NodeListBuilder to items: () -> [Item]) -> NodeLink { 57 | NodeLink(self, emptyIsNil: emptyIsNil, items: items) 58 | } 59 | 60 | func matchedFilterOptions() -> SourceFilter.Options { 61 | return [] 62 | } 63 | 64 | func matchesTerm(_ term: String) -> Bool { 65 | term.isEmpty || title.uppercased().contains(term.uppercased()) 66 | } 67 | 68 | func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool { 69 | filter.options.isSubset(of: options) && matchesTerm(filter.searchTerm) 70 | } 71 | 72 | @discardableResult 73 | mutating func processUpdate(_ update: SimModel.Update) -> Bool { 74 | return false 75 | } 76 | } 77 | 78 | /// Defines the requirements of a collection that can serve as a `NodeList`. 79 | 80 | protocol NodeList: RandomAccessCollection where Self.Element: Node, Index: Hashable { } 81 | 82 | extension NodeList { 83 | @NodeListBuilder 84 | func linkEachTo(emptyIsNil: Bool = false, @NodeListBuilder items: (Element) -> [Item]) -> [some Node] { 85 | map { item in 86 | item.link(emptyIsNil: emptyIsNil, to: { items(item) }) 87 | } 88 | } 89 | } 90 | 91 | extension Array: NodeList where Element: Node { } 92 | 93 | // MARK: - Special Nodes - 94 | 95 | enum LeafNode: Node { 96 | var title : String { "impossible" } 97 | var headerTitle : String { title } 98 | 99 | var header: some View { Text("impossible") } 100 | var content: some View { Text("impossible") } 101 | 102 | func icon(forHeader: Bool) -> some View { 103 | Text("impossible") 104 | } 105 | } 106 | 107 | struct RootNode: Node { 108 | var items : [Item]? 109 | var title : String { "Root" } 110 | var headerTitle : String { title } 111 | var header : some View { Text("Root") } 112 | var content : some View { Text("Root") } 113 | 114 | init() { 115 | self.items = nil 116 | } 117 | 118 | init(@NodeListBuilder _ items: () -> [Item]) { 119 | self.items = items() 120 | } 121 | 122 | func icon(forHeader: Bool) -> some View { 123 | symbolIcon("tree", forHeader: forHeader) 124 | } 125 | } 126 | 127 | struct NodeLink: Node { 128 | typealias UpdateHandler = (SimModel.Update) -> [Item]?? 129 | 130 | var base : Base 131 | var items : [Item]? 132 | var title : String { base.title } 133 | var headerTitle : String { base.headerTitle } 134 | var header : Base.Header { base.header } 135 | var content : Base.Content { base.content } 136 | var updaterHandler : UpdateHandler? = nil 137 | 138 | init(_ base: Base, emptyIsNil: Bool = false, @NodeListBuilder items: () -> [Item]) { 139 | let list = items() 140 | 141 | self.base = base 142 | self.items = emptyIsNil ? (list.isEmpty ? nil : list) : list 143 | } 144 | 145 | init(_ base: Base, emptyIsNil: Bool = false, items: [Item]) { 146 | self.base = base 147 | self.items = emptyIsNil ? (items.isEmpty ? nil : items) : items 148 | } 149 | 150 | func icon(forHeader: Bool) -> some View { 151 | base.icon(forHeader: forHeader) 152 | } 153 | 154 | func matchedFilterOptions() -> SourceFilter.Options { 155 | return base.matchedFilterOptions() 156 | } 157 | 158 | mutating func onUpdate(_ handler: @escaping UpdateHandler) -> Self { 159 | updaterHandler = handler 160 | 161 | return self 162 | } 163 | 164 | @discardableResult 165 | mutating func processUpdate(_ update: SimModel.Update) -> Bool { 166 | guard let newItems = updaterHandler?(update) else { return false } 167 | 168 | self.items = newItems 169 | 170 | return true 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /SimDirs/Node/NodeAB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeAB.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/18/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum NodeAB: Node, CustomStringConvertible { 11 | case a(A) 12 | case b(B) 13 | 14 | var title : String { 15 | switch self { 16 | case .a(let node): return node.title 17 | case .b(let node): return node.title 18 | } 19 | } 20 | 21 | var headerTitle : String { 22 | switch self { 23 | case .a(let node): return node.headerTitle 24 | case .b(let node): return node.headerTitle 25 | } 26 | } 27 | 28 | @ViewBuilder 29 | var header : some View { 30 | switch self { 31 | case .a(let node): node.header 32 | case .b(let node): node.header 33 | } 34 | } 35 | 36 | @ViewBuilder 37 | var content : some View { 38 | switch self { 39 | case .a(let node): node.content 40 | case .b(let node): node.content 41 | } 42 | } 43 | 44 | var items : [NodeAB]? { 45 | get { 46 | switch self { 47 | case .a(let node): return node.items?.map { .a($0) } 48 | case .b(let node): return node.items?.map { .b($0) } 49 | } 50 | } 51 | set { 52 | switch self { 53 | case .a(var node): 54 | let items : [A.Child]? = newValue?.compactMap({ ab in 55 | guard case .a(let a) = ab else { return nil } 56 | 57 | return a 58 | }) 59 | 60 | node.items = items 61 | self = .a(node) 62 | 63 | case .b(var node): 64 | let items : [B.Child]? = newValue?.compactMap({ ab in 65 | guard case .b(let b) = ab else { return nil } 66 | 67 | return b 68 | }) 69 | 70 | node.items = items 71 | self = .b(node) 72 | } 73 | } 74 | } 75 | 76 | var description : String { 77 | let valueDesc : String 78 | 79 | switch self { 80 | case .a(let node): valueDesc = ".a: \(String(describing: node))" 81 | case .b(let node): valueDesc = ".b: \(String(describing: node))" 82 | } 83 | 84 | return "NodeAB: \(valueDesc)" 85 | } 86 | 87 | func icon(forHeader: Bool) -> some View { 88 | switch self { 89 | case .a(let node): node.icon(forHeader: forHeader) 90 | case .b(let node): node.icon(forHeader: forHeader) 91 | } 92 | } 93 | 94 | func matchedFilterOptions() -> SourceFilter.Options { 95 | switch self { 96 | case .a(let node): return node.matchedFilterOptions() 97 | case .b(let node): return node.matchedFilterOptions() 98 | } 99 | } 100 | 101 | @discardableResult 102 | mutating func processUpdate(_ update: SimModel.Update) -> Bool { 103 | switch self { 104 | case .a(var node): 105 | let result = node.processUpdate(update) 106 | 107 | self = .a(node) 108 | 109 | return result 110 | 111 | case .b(var node): 112 | let result = node.processUpdate(update) 113 | 114 | self = .b(node) 115 | 116 | return result 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /SimDirs/Node/NodeListBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeListBuilder.swift 3 | // NodeItems 4 | // 5 | // Created by Casey Fleser on 3/2/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @resultBuilder struct NodeListBuilder { 11 | typealias P = Node 12 | typealias OneOf = NodeAB 13 | 14 | static func buildPartialBlock(first c: [C]) -> [C] { 15 | c 16 | } 17 | 18 | // matching types 19 | static func buildPartialBlock(accumulated c0: [C], next c1: [C]) -> [C] { 20 | c0 + c1 21 | } 22 | 23 | // matches A of OneOf 24 | static func buildPartialBlock(accumulated ab: [OneOf], next a: [A]) -> [OneOf] { 25 | ab + a.map { .a($0) } 26 | } 27 | 28 | // matches B of OneOf 29 | static func buildPartialBlock(accumulated ab: [OneOf], next b: [B]) -> [OneOf] { 30 | ab + b.map { .b($0) } 31 | } 32 | 33 | // matches A of OneOf, C> 34 | static func buildPartialBlock(accumulated abc: [OneOf, C>], next a: [A]) -> [OneOf, C>] { 35 | buildPartialBlock(accumulated: [] as [OneOf], next: a).map { .a($0) } 36 | } 37 | 38 | // matches B of OneOf, C> 39 | static func buildPartialBlock(accumulated abc: [OneOf, C>], next b: [B]) -> [OneOf, C>] { 40 | buildPartialBlock(accumulated: [] as [OneOf], next: b).map { .a($0) } 41 | } 42 | 43 | // matches C of OneOf, C> 44 | static func buildPartialBlock(accumulated abc: [OneOf, C>], next c: [C]) -> [OneOf, C>] { 45 | abc + c.map { .b($0) } 46 | } 47 | 48 | // matches A of OneOf, OneOf> 49 | static func buildPartialBlock(accumulated abcd: [OneOf, OneOf>], next a: [A]) -> [OneOf, OneOf>] { 50 | buildPartialBlock(accumulated: [] as [OneOf], next: a).map { .a($0) } 51 | } 52 | 53 | // matches B of OneOf, OneOf> 54 | static func buildPartialBlock(accumulated abcd: [OneOf, OneOf>], next b: [B]) -> [OneOf, OneOf>] { 55 | buildPartialBlock(accumulated: [] as [OneOf], next: b).map { .a($0) } 56 | } 57 | 58 | // matches C of OneOf, OneOf> 59 | static func buildPartialBlock(accumulated abcd: [OneOf, OneOf>], next c: [C]) -> [OneOf, OneOf>] { 60 | buildPartialBlock(accumulated: [] as [OneOf], next: c).map { .b($0) } 61 | } 62 | 63 | // matches D of OneOf, OneOf> 64 | static func buildPartialBlock(accumulated abcd: [OneOf, OneOf>], next d: [D]) -> [OneOf, OneOf>] { 65 | buildPartialBlock(accumulated: [] as [OneOf], next: d).map { .b($0) } 66 | } 67 | 68 | // non-matching types 69 | static func buildPartialBlock(accumulated c0: [C0], next c1: [C1]) -> [OneOf] { 70 | c0.map({ OneOf.a($0) }) + c1.map({ OneOf.b($0) }) 71 | } 72 | 73 | // static func buildBlock(_ c: [C]...) -> [C] { 74 | // c.flatMap { $0 } 75 | // } 76 | // 77 | // Same type buildBlocks. This works but buildBlock(_ c: [C]...) -> [C] confuses the compiler 78 | 79 | static func buildBlock (_ c0: [C0], _ c1: [C0]) -> [C0] { 80 | [c0, c1].flatMap { $0 } 81 | } 82 | 83 | static func buildBlock (_ c0: [C0], _ c1: [C0], _ c2: [C0]) -> [C0] { 84 | [c0, c1, c2].flatMap { $0 } 85 | } 86 | 87 | static func buildEither(first c0: [C0]) -> [OneOf] { 88 | c0.map { OneOf.a($0) } 89 | } 90 | 91 | static func buildEither(second c1: [C1]) -> [OneOf] { 92 | c1.map { OneOf.b($0) } 93 | } 94 | 95 | static func buildOptional(_ c: [C]?) -> [C] { 96 | c ?? [] 97 | } 98 | 99 | static func buildArray(_ c: [[C]]) -> [C] { 100 | c.flatMap { $0 } 101 | } 102 | 103 | static func buildExpression(_ node: N) -> [N] { 104 | return [node] 105 | } 106 | 107 | static func buildExpression(_ nodeList: NL) -> [NL.Element] { 108 | return Array(nodeList) 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /SimDirs/Node/Views/FilteredNodeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilteredNodeView.swift 3 | // NodeItems 4 | // 5 | // Created by Casey Fleser on 3/3/23. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct FilteredNodeView: View { 12 | @Environment(\.deviceUpdates) var deviceUpdates 13 | @StateObject var node : FilteredNode 14 | @Binding var filter : SourceFilter 15 | 16 | init(_ node: T, filter: Binding) { 17 | self._node = StateObject(wrappedValue: FilteredNode(node)) 18 | self._filter = filter 19 | } 20 | 21 | init(filter: Binding, @NodeListBuilder items: () -> [Item]) where T == RootNode { 22 | self.init(RootNode(items), filter: filter) 23 | } 24 | 25 | var body: some View { 26 | Root(node: node) 27 | .searchable(text: $filter.searchTerm, placement: .sidebar) 28 | .onAppear { node.applyFilter(filter) } 29 | .onChange(of: filter) { node.applyFilter($0) } 30 | .onReceive(deviceUpdates) { update in 31 | node.processUpdate(update) 32 | node.applyFilter(filter) 33 | } 34 | } 35 | } 36 | 37 | extension FilteredNodeView { 38 | struct Root: View { 39 | @ObservedObject var node : FilteredNode 40 | 41 | var visibleItems : [FilteredNode] { node.items.map { $0.filter { !$0.filtered} } ?? [] } 42 | 43 | var body: some View { 44 | let items = visibleItems 45 | 46 | List { 47 | if !items.isEmpty { 48 | ForEach(items.indices, id: \.self) { index in 49 | Item(node: items[index]) 50 | } 51 | } 52 | else { 53 | Text("No Filter Results") 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct ItemList: View { 60 | var items : [FilteredNode] 61 | 62 | init(items: [FilteredNode]) { 63 | self.items = items 64 | } 65 | 66 | var body: some View { 67 | ForEach(items.indices, id: \.self) { index in 68 | Item(node: items[index]) 69 | } 70 | } 71 | } 72 | 73 | struct Item: View { 74 | @ObservedObject var node : FilteredNode 75 | 76 | var body: some View { 77 | if !node.filtered { 78 | NodeLabel(node) 79 | 80 | if let items = node.items, node.isExpanded { 81 | ItemList(items: items) 82 | .padding(.leading, 12.0) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | struct FilteredNodeView_Previews: PreviewProvider { 90 | @State static var filter = SourceFilter.restore() 91 | 92 | static var previews: some View { 93 | List { 94 | FilteredNodeView(filter: $filter) { 95 | SimPlatform.iOS 96 | SimPlatform.tvOS 97 | SimPlatform.watchOS 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /SimDirs/Node/Views/NodeLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeLabel.swift 3 | // NodeItems 4 | // 5 | // Created by Casey Fleser on 3/3/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NodeLabel: View { 11 | @ObservedObject var node : FilteredNode 12 | 13 | init(_ node: FilteredNode) { 14 | self.node = node 15 | } 16 | 17 | var body: some View { 18 | HStack(spacing: 0) { 19 | let button = 20 | Button( 21 | action: { 22 | let optionActive = NSApplication.shared.currentEvent?.modifierFlags.contains(.option) == true 23 | 24 | withAnimation(.easeInOut(duration: 0.2)) { 25 | node.toggleExpanded(deep: optionActive) 26 | } 27 | }, 28 | label: { 29 | Image(systemName: "chevron.right") 30 | .padding(.horizontal, 4.0) 31 | .contentShape(Rectangle()) 32 | .rotationEffect(.degrees(node.isExpanded ? 90.0 : 0.0)) 33 | } 34 | ) 35 | .buttonStyle(.plain) 36 | 37 | if node.items != nil { button } 38 | else { button.hidden() } 39 | 40 | NavigationLink( 41 | destination: { NodeView(node) }, 42 | label: { 43 | Label( 44 | title: { Text(node.title) }, 45 | icon: { node.icon(forHeader: false) } 46 | ) 47 | } 48 | ) 49 | } 50 | } 51 | } 52 | 53 | struct NodeLabel_Previews: PreviewProvider { 54 | @StateObject static var previewItem = FilteredNode(SimPlatform.iOS) 55 | 56 | static var previews: some View { 57 | VStack { 58 | NodeLabel(previewItem) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /SimDirs/Node/Views/NodeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeView.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 3/6/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NodeView: View { 11 | var node : Item 12 | 13 | init(_ node: Item) { 14 | self.node = node 15 | } 16 | 17 | var body: some View { 18 | GeometryReader { geometry in 19 | VStack(alignment: .leading, spacing: 0.0) { 20 | // --- Header section --- 21 | VStack(alignment: .leading) { 22 | Text(node.headerTitle) 23 | .font(.system(size: 20)) 24 | .padding(.top, 12.0) 25 | .padding(.bottom, 8.0) 26 | node.header 27 | .padding(.trailing, 136.0) 28 | } 29 | .padding([.leading, .trailing]) 30 | .frame(maxWidth: .infinity, maxHeight: 144.0, alignment: .topLeading) 31 | Rectangle().frame(height: 1.0).foregroundColor(Color("HeaderEdge")) 32 | 33 | // --- Content section --- 34 | ScrollView { 35 | HStack { 36 | node.content 37 | .padding(.top, 4.0) 38 | .padding(.trailing) 39 | Spacer() 40 | } 41 | } 42 | .frame(maxWidth: .infinity) 43 | .padding([.leading, .top]) 44 | .background(.background) 45 | } 46 | .overlay(alignment: .topTrailing) { 47 | node.icon(forHeader: true) 48 | .padding([.top, .trailing], 24.0) 49 | } 50 | .padding(.top, -geometry.frame(in: .global).origin.y) 51 | } 52 | .navigationTitle(node.title) 53 | } 54 | } 55 | 56 | struct NodeView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | NodeView(SimPlatform.iOS) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SimDirs/Presentation/SourceFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceFilter.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SourceFilter: Equatable { 11 | struct Options: OptionSet, CaseIterable { 12 | let rawValue: Int 13 | 14 | static let withApps = Options(rawValue: 1 << 0) 15 | static let runtimeInstalled = Options(rawValue: 1 << 1) 16 | 17 | static var allCases : [Options] = [.withApps, .runtimeInstalled] 18 | } 19 | 20 | var searchTerm = "" 21 | var options = Options() { didSet { UserDefaults.standard.set(options.rawValue, forKey: "FilterOptions") } } 22 | 23 | var filterApps : Bool { 24 | get { options.contains(.withApps) } 25 | set { options.booleanSet(newValue, options: .withApps) } 26 | } 27 | 28 | var filterRuntimes : Bool { 29 | get { options.contains(.runtimeInstalled) } 30 | set { options.booleanSet(newValue, options: .runtimeInstalled) } 31 | } 32 | 33 | static func restore() -> SourceFilter { 34 | var filter = SourceFilter() 35 | 36 | filter.options = SourceFilter.Options(rawValue: UserDefaults.standard.integer(forKey: "FilterOptions")) 37 | 38 | return filter 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SimDirs/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimDirs/SimDirs.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SimDirs/SimDirsApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimDirsApp.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/23/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SimDirsApp: App { 12 | private var simModel = SimModel() 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView(model: simModel) 17 | } 18 | .windowStyle(.hiddenTitleBar) 19 | .commands { 20 | SimCommands() 21 | } 22 | } 23 | } 24 | 25 | struct SimCommands: Commands { 26 | var body: some Commands { 27 | SidebarCommands() 28 | 29 | CommandMenu("Commands") { 30 | Button("Command") { 31 | print("Command") 32 | } 33 | .keyboardShortcut("f", modifiers: [.shift, .option]) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SimDirs/Views/AppearancePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppearancePicker.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 7/31/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppearancePicker: View { 11 | @Environment(\.isEnabled) var isEnabled 12 | @Binding var scheme : ColorScheme? 13 | 14 | var body: some View { 15 | HStack(spacing: 16.0) { 16 | Button("Light") { 17 | scheme = .light 18 | } 19 | .buttonStyle(.appearance(selected: scheme == .light, scheme: .light)) 20 | 21 | Button("Dark") { 22 | scheme = .dark 23 | } 24 | .buttonStyle(.appearance(selected: scheme == .dark, scheme: .dark)) 25 | } 26 | .opacity(isEnabled ? 1.0 : 0.5) 27 | } 28 | } 29 | 30 | struct AppearancePicker_Previews: PreviewProvider { 31 | @State static var scheme : ColorScheme? = .light 32 | 33 | static var previews: some View { 34 | AppearancePicker(scheme: $scheme) 35 | .disabled(false) 36 | AppearancePicker(scheme: $scheme) 37 | .disabled(true) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SimDirs/Views/ContentHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentHeader.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 7/31/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentHeader: View { 11 | let title : String 12 | 13 | init(_ title: String) { 14 | self.title = title.uppercased() 15 | } 16 | 17 | var body: some View { 18 | VStack(alignment: .leading, spacing: 4) { 19 | Text(title) 20 | .fontWeight(.semibold) 21 | .foregroundColor(Color("ContentHeader")) 22 | .padding(.top) 23 | Divider() 24 | } 25 | .padding(.bottom, 4) 26 | } 27 | } 28 | 29 | struct ContentHeader_Previews: PreviewProvider { 30 | static var previews: some View { 31 | ContentHeader("Content Header") 32 | .preferredColorScheme(.light) 33 | ContentHeader("Content Header") 34 | .preferredColorScheme(.dark) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SimDirs/Views/DescriptiveToggle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DescriptiveToggle.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 8/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol ToggleDescriptor { 11 | var isOn : Bool { get } 12 | var titleKey : LocalizedStringKey { get } 13 | var text : String { get } 14 | var image : Image { get } 15 | } 16 | 17 | extension ToggleDescriptor { 18 | var circleColor : Color { isOn ? .accentColor : Color("CircleSymbolBkgOff") } 19 | } 20 | 21 | struct DescriptiveToggle: View { 22 | @Binding var isOn : Bool 23 | var descriptor : T 24 | var subtitled : Bool 25 | 26 | init(_ descriptor: T, isOn: Binding, subtitled: Bool = true) { 27 | self._isOn = isOn 28 | self.descriptor = descriptor 29 | self.subtitled = subtitled 30 | } 31 | 32 | var body: some View { 33 | Toggle(descriptor.titleKey, isOn: _isOn) 34 | .toggleStyle(DescriptiveToggleStyle(descriptor, subtitled: subtitled)) 35 | } 36 | } 37 | 38 | struct DescriptiveToggle_Previews: PreviewProvider { 39 | struct DarkMode: ToggleDescriptor { 40 | var isOn : Bool = true 41 | var titleKey : LocalizedStringKey { "Dark Mode" } 42 | var text : String { isOn ? "On" : "Off" } 43 | var image : Image { Image(systemName: "circle.circle") } 44 | } 45 | 46 | @State static var toggle = DarkMode() 47 | 48 | static var previews: some View { 49 | DescriptiveToggle(DarkMode(), isOn: $toggle.isOn) 50 | .disabled(true) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SimDirs/Views/ErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorView.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ErrorView: View { 11 | let title : String 12 | let description : String 13 | 14 | var body: some View { 15 | HStack(alignment: .top) { 16 | Image(systemName: "xmark.octagon.fill") 17 | .symbolRenderingMode(.multicolor) 18 | VStack(alignment: .leading) { 19 | Text(title) 20 | .fontWeight(.semibold) 21 | Text(description) 22 | .foregroundColor(.secondary) 23 | } 24 | } 25 | .padding(.bottom, 8.0) 26 | } 27 | } 28 | 29 | struct ErrorView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | ErrorView( 32 | title: "Something bad", 33 | description: "Did you try turning it off and back on again?") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/AppContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContent.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension SimApp { 11 | var isLaunched : Bool { 12 | get { state.isOn } 13 | set { toggleLaunchState() } 14 | } 15 | } 16 | 17 | extension SimApp.State: ToggleDescriptor { 18 | var titleKey : LocalizedStringKey { isOn ? "Terminate" : "Launch" } 19 | var text : String { isOn ? "Launched" : "Terminated" } 20 | var image : Image { Image(systemName: "power.circle") } 21 | } 22 | 23 | struct AppContent: View { 24 | // https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html 25 | 26 | @ObservedObject var app : SimApp 27 | @State var validJSON = true 28 | @State var jsonErrMsg = "" 29 | @State var jsonText : String = """ 30 | { 31 | "aps" : { 32 | "alert": "Push from SimDirs", 33 | "sound": "chime", 34 | "badge": 1 35 | } 36 | } 37 | """ 38 | 39 | var body: some View { 40 | VStack(alignment: .leading, spacing: 0.0) { 41 | ContentHeader("Paths") 42 | Group { 43 | PathRow(title: "Bundle Path", path: app.bundlePath) 44 | if let sandboxPath = app.sandboxPath { 45 | PathRow(title: "Sandbox Path", path: sandboxPath) 46 | } 47 | else { 48 | Text("Sandbox Path: ") 49 | } 50 | } 51 | .font(.subheadline) 52 | .lineLimit(1) 53 | 54 | ContentHeader("Actions") 55 | VStack(alignment: .leading) { 56 | HStack(spacing: 16) { 57 | DescriptiveToggle(app.state, isOn: $app.isLaunched, subtitled: false) 58 | .frame(width: 58) 59 | } 60 | .environment(\.isEnabled, app.device?.isBooted == true) 61 | 62 | HStack { 63 | Button(action: pushJSON) { 64 | Text("Push") 65 | .fontWeight(.semibold) 66 | .font(.system(size: 11)) 67 | } 68 | .buttonStyle(.systemIcon("bell.badge")) 69 | .disabled(!validJSON) 70 | .frame(width: 58) 71 | 72 | VStack(alignment: .leading, spacing: 4) { 73 | HStack(spacing: 4) { 74 | Text("JSON payload:") 75 | .font(.subheadline) 76 | .padding(.leading, 8) 77 | Text(jsonErrMsg.isEmpty ? "Valid" : jsonErrMsg) 78 | .font(.subheadline) 79 | .foregroundColor(jsonErrMsg.isEmpty ? .green : .red) 80 | } 81 | TextEditor(text: $jsonText) 82 | .font(.system(size: 11, design: .monospaced)) 83 | .frame(height: 96) 84 | .border(.black) 85 | } 86 | } 87 | } 88 | } 89 | .onAppear { app.discoverState() } 90 | .onChange(of: jsonText, perform: validateJSON) 91 | } 92 | 93 | func pushJSON() { 94 | guard let jsonData = jsonText.data(using: .utf8), let device = app.device else { return } 95 | 96 | do { 97 | if var payload = try JSONSerialization.jsonObject(with: jsonData) as? [String : Any] { 98 | let jsonData : Data 99 | 100 | payload["Simulator Target Bundle"] = app.bundleID 101 | jsonData = try JSONSerialization.data(withJSONObject: payload) 102 | 103 | device.sendPushNotification(payload: jsonData) 104 | } 105 | } 106 | catch { // 107 | print("Error attempting to create push payload: \(error)") 108 | } 109 | } 110 | 111 | func validateJSON(_ json: String) { 112 | guard let jsonData = json.data(using: .utf8) else { return } 113 | 114 | do { 115 | let obj = try JSONSerialization.jsonObject(with: jsonData) 116 | 117 | if obj as? [String : Any] == nil { 118 | jsonErrMsg = "Root expected to be dictionary" 119 | validJSON = false 120 | } 121 | else { 122 | jsonErrMsg = "" 123 | validJSON = true 124 | } 125 | } 126 | catch { 127 | let nsError = (error as NSError) 128 | 129 | jsonErrMsg = nsError.userInfo[NSDebugDescriptionErrorKey] as? String ?? nsError.localizedDescription 130 | validJSON = false 131 | } 132 | } 133 | } 134 | 135 | extension NSTextView { 136 | // gross (see: https://stackoverflow.com/questions/66721935/swiftui-how-to-disable-the-smart-quotes-in-texteditor) 137 | open override var frame: CGRect { 138 | didSet { 139 | self.isAutomaticQuoteSubstitutionEnabled = false 140 | } 141 | } 142 | } 143 | 144 | struct AppContent_Previews: PreviewProvider { 145 | static var apps = SimModel().apps 146 | 147 | static var previews: some View { 148 | AppContent(app: apps[0]) 149 | AppContent(app: apps.randomElement() ?? apps[1]) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/AppHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppHeader.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppHeader: View { 11 | var app : SimApp 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 3.0) { 15 | Text("Display Name: \(app.displayName)") 16 | Text("Bundle Name: \(app.bundleName)") 17 | Text("Bundle ID: \(app.bundleID)") 18 | Text("Version: \(app.version)") 19 | Text("Minimum OS Version: \(app.minOSVersion)") 20 | } 21 | .font(.subheadline) 22 | .textSelection(.enabled) 23 | } 24 | } 25 | 26 | struct AppHeader_Previews: PreviewProvider { 27 | static var apps = SimModel().apps 28 | 29 | static var previews: some View { 30 | AppHeader(app: apps[0]) 31 | AppHeader(app: apps.randomElement() ?? apps[1]) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/DeviceContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceContent.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | 11 | extension SimDevice { 12 | var scheme : ColorScheme? { 13 | get { 14 | switch appearance { 15 | case .light: return .light 16 | case .dark: return .dark 17 | default: return nil 18 | } 19 | } 20 | set { 21 | switch newValue { 22 | case .light: setAppearance(.light) 23 | case .dark: setAppearance(.dark) 24 | default: break 25 | } 26 | } 27 | } 28 | 29 | var contentSizeVal : Double { 30 | get { Double(contentSize.intValue) } 31 | set { setContenSize(ContentSize(intValue: Int(newValue))) } 32 | } 33 | 34 | var isIncreaseContrast : Bool { 35 | get { increaseContrast.isOn } 36 | set { setIncreaseContrast(newValue ? .enabled : .disabled) } 37 | } 38 | } 39 | 40 | extension SimDevice.IncreaseContrast: ToggleDescriptor { 41 | var titleKey : LocalizedStringKey { "Increase Contrast" } 42 | var text : String { rawValue.capitalized } 43 | var image : Image { Image(systemName: "circle.lefthalf.filled") } 44 | } 45 | 46 | struct DeviceContent: View { 47 | enum SaveType { 48 | case image 49 | case video 50 | 51 | var allowedContentTypes : [UTType] { 52 | switch self { 53 | case .image: return [.png] 54 | case .video: return [.mpeg4Movie] 55 | } 56 | } 57 | 58 | var title : String { 59 | switch self { 60 | case .image: return "Save Screen" 61 | case .video: return "Save Recording" 62 | } 63 | } 64 | } 65 | 66 | @ObservedObject var device : SimDevice 67 | @State var isBooted : Bool 68 | 69 | var fileDateFormatter : DateFormatter { 70 | let formatter = DateFormatter() 71 | 72 | formatter.dateFormat = "yyyy.MM.dd'_'HH.mm.ss" 73 | 74 | return formatter 75 | } 76 | 77 | init(_ device: SimDevice) { 78 | self.device = device 79 | self.isBooted = device.isBooted 80 | } 81 | 82 | var body: some View { 83 | VStack(alignment: .leading, spacing: 3.0) { 84 | if !device.isAvailable { 85 | ErrorView( 86 | title: "\(device.name) is unavailable", 87 | description: device.availabilityError ?? "Unknown Error") 88 | } 89 | 90 | ContentHeader("Paths") 91 | Group { 92 | PathRow(title: "Data Path", path: device.dataPath) 93 | PathRow(title: "Log Path", path: device.logPath) 94 | } 95 | .font(.subheadline) 96 | .textSelection(.enabled) 97 | 98 | ContentHeader("Actions") 99 | HStack(spacing: 16) { 100 | Button(action: { saveScreen(.image) }) { 101 | Text("Save Screen") 102 | .fontWeight(.semibold) 103 | .font(.system(size: 11)) 104 | } 105 | .buttonStyle(.systemIcon("camera.on.rectangle")) 106 | 107 | Button(action: { device.isRecording ? device.endRecording() : saveScreen(.video) }) { 108 | Text(device.isRecording ? "End Recording" : "Record Screen") 109 | .fontWeight(.semibold) 110 | .font(.system(size: 11)) 111 | } 112 | .buttonStyle(.systemIcon("record.circle", active: device.isRecording)) 113 | } 114 | .environment(\.isEnabled, isBooted) 115 | 116 | ContentHeader("UI") 117 | HStack(spacing: 16) { 118 | if device.appearance != .unsupported { 119 | AppearancePicker(scheme: $device.scheme) 120 | } 121 | if device.appearance != .unsupported { 122 | DescriptiveToggle(device.increaseContrast, isOn: $device.isIncreaseContrast) 123 | } 124 | if device.contentSize != .unsupported { 125 | VStack { 126 | HStack { 127 | Image(systemName: "textformat.size") 128 | .imageScale(.small) 129 | Slider(value: $device.contentSizeVal, in: SimDevice.ContentSize.range, step: 1) 130 | Image(systemName: "textformat.size") 131 | .imageScale(.large) 132 | } 133 | Text("Content Size") 134 | } 135 | .opacity(isBooted ? 1.0 : 0.5) 136 | } 137 | } 138 | .environment(\.isEnabled, isBooted) 139 | } 140 | .onAppear { 141 | device.discoverUI() 142 | } 143 | .onChange(of: device.state) { state in 144 | let trulyBooted = state == .booted 145 | 146 | if isBooted != trulyBooted { 147 | isBooted = trulyBooted 148 | 149 | if isBooted { 150 | device.discoverUI() 151 | } 152 | } 153 | } 154 | } 155 | 156 | func saveScreen(_ type: SaveType = .image) { 157 | let savePanel = NSSavePanel() 158 | 159 | savePanel.allowedContentTypes = type.allowedContentTypes 160 | savePanel.canCreateDirectories = true 161 | savePanel.isExtensionHidden = false 162 | savePanel.title = type.title 163 | savePanel.message = "Select destination" 164 | savePanel.nameFieldLabel = "Filename:" 165 | savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))" 166 | 167 | if savePanel.runModal() == .OK { 168 | if let url = savePanel.url { 169 | switch type { 170 | case .image: device.saveScreen(url) 171 | case .video: device.saveVideo(url) 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | struct DeviceContent_Previews: PreviewProvider { 179 | static var devices = SimModel().devices 180 | 181 | static var previews: some View { 182 | if !devices.isEmpty { 183 | DeviceContent(devices[0]) 184 | .preferredColorScheme(.light) 185 | DeviceContent(devices.randomElement() ?? devices[0]) 186 | .preferredColorScheme(.dark) 187 | } 188 | else { 189 | Text("No devices") 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/DeviceHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceHeader.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeviceHeader: View { 11 | @ObservedObject var device : SimDevice 12 | 13 | init(_ device: SimDevice) { 14 | self.device = device 15 | } 16 | 17 | var body: some View { 18 | VStack(alignment: .leading, spacing: 3.0) { 19 | HStack(spacing: 8.0) { 20 | Toggle("Booted", isOn: $device.isBooted) 21 | .toggleStyle(.switch) 22 | .disabled(device.isTransitioning) 23 | 24 | if device.isTransitioning { 25 | ProgressView() 26 | .controlSize(.small) 27 | Text(device.state.rawValue) 28 | } 29 | } 30 | Text("Model: \(device.deviceModel ?? "- unknown - ")") 31 | Text("Identifier: \(device.udid)") 32 | } 33 | .font(.subheadline) 34 | .textSelection(.enabled) 35 | } 36 | } 37 | 38 | struct DeviceHeader_Previews: PreviewProvider { 39 | static var devices = SimModel().devices 40 | 41 | static var previews: some View { 42 | if !devices.isEmpty { 43 | DeviceHeader(devices[0]) 44 | DeviceHeader(devices.randomElement() ?? devices[1]) 45 | } 46 | else { 47 | Text("No devices") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/DeviceTypeContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceTypeContent.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/20/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeviceTypeContent: View { 11 | var deviceType : SimDeviceType 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 0.0) { 15 | ContentHeader("Paths") 16 | PathRow(title: "Bundle Path", path: deviceType.bundlePath) 17 | } 18 | .font(.subheadline) 19 | .lineLimit(1) 20 | } 21 | } 22 | 23 | struct DeviceTypeContent_Previews: PreviewProvider { 24 | static var deviceTypes = SimModel().deviceTypes 25 | 26 | static var previews: some View { 27 | DeviceTypeContent(deviceType: deviceTypes[0]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/DeviceTypeHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceTypeHeader.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/20/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeviceTypeHeader: View { 11 | var deviceType : SimDeviceType 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 3.0) { 15 | Text("Product Family: \(deviceType.productFamily.title)") 16 | Text("Model ID: \(deviceType.modelIdentifier)") 17 | Text("Min Runtime: \(deviceType.minRuntimeVersionString)") 18 | Text("Max Runtime: \(UInt32.max == deviceType.maxRuntimeVersion ? "-" : deviceType.maxRuntimeVersionString)") 19 | Text("Identifier: \(deviceType.identifier)") 20 | } 21 | .font(.subheadline) 22 | .textSelection(.enabled) 23 | } 24 | } 25 | 26 | struct DeviceTypeHeader_Previews: PreviewProvider { 27 | static var deviceTypes = SimModel().deviceTypes 28 | 29 | static var previews: some View { 30 | DeviceTypeHeader(deviceType: deviceTypes[0]) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/RuntimeContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuntimeContent.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/20/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RuntimeContent: View { 11 | struct SupportedItem: Identifiable { 12 | let name : String 13 | var id : String { return name } 14 | } 15 | 16 | var runtime : SimRuntime 17 | 18 | var body: some View { 19 | VStack(alignment: .leading, spacing: 3.0) { 20 | let items = runtime.supportedDeviceTypes.map { SupportedItem(name: $0.name) } 21 | 22 | Group { 23 | if !runtime.isAvailable { 24 | ErrorView( 25 | title: "\(runtime.name) is unavailable", 26 | description: runtime.availabilityError ?? "Unknown Error") 27 | } 28 | 29 | ContentHeader("Paths") 30 | if !runtime.bundlePath.isEmpty { 31 | PathRow(title: "Bundle Path", path: runtime.bundlePath) 32 | } 33 | 34 | ContentHeader("Supported devices\(runtime.isPlaceholder ? " (partial list)" : "")") 35 | ForEach(items) { item in 36 | Text("• \(item.name)") 37 | } 38 | .padding(.leading) 39 | } 40 | .font(.subheadline) 41 | .textSelection(.enabled) 42 | } 43 | } 44 | } 45 | 46 | struct RuntimeContent_Previews: PreviewProvider { 47 | static var runtimes = SimModel().runtimes 48 | 49 | static var previews: some View { 50 | RuntimeContent(runtime: runtimes[0]) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SimDirs/Views/Model Views/RuntimeHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuntimeHeader.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RuntimeHeader: View { 11 | var runtime : SimRuntime 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 3.0) { 15 | if !runtime.buildversion.isEmpty { 16 | Text("Build Version: \(runtime.buildversion)") 17 | } 18 | } 19 | .font(.subheadline) 20 | .textSelection(.enabled) 21 | } 22 | } 23 | 24 | struct RuntimeHeader_Previews: PreviewProvider { 25 | static var runtimes = SimModel().runtimes 26 | 27 | static var previews: some View { 28 | RuntimeContent(runtime: runtimes[0]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SimDirs/Views/PathActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathActions.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 5/31/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PathActions: View { 11 | var path : String 12 | 13 | var body: some View { 14 | // ControlGroup almost but not quite what we want 15 | HStack { 16 | Button(action: { NSPasteboard.copy(text: path) }) { 17 | Image(systemName: "doc.on.doc") } 18 | Divider() 19 | .frame(height: 16.0) 20 | Button(action: { NSWorkspace.reveal(filepath: path) }) { 21 | Image(systemName: "arrow.right.circle.fill") } 22 | } 23 | .buttonStyle(.borderless) 24 | .padding(.vertical, 4.0) 25 | .padding(.horizontal, 8.0) 26 | .overlay(RoundedRectangle(cornerRadius: 6.0) 27 | .stroke(.white.opacity(0.4), lineWidth: 1.0)) 28 | .background(.black.opacity(0.4)) 29 | .cornerRadius(6.0) 30 | } 31 | } 32 | 33 | struct PathActions_Previews: PreviewProvider { 34 | static var previews: some View { 35 | PathActions(path: "~/Desktop") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SimDirs/Views/PathRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathRow.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PathRow: View { 11 | var title : String 12 | var path : String 13 | 14 | var body: some View { 15 | HStack { 16 | Text("\(title): \(path)") 17 | .truncationMode(/*@START_MENU_TOKEN@*/.middle/*@END_MENU_TOKEN@*/) 18 | Spacer() 19 | PathActions(path: path) 20 | } 21 | } 22 | } 23 | 24 | struct PathRow_Previews: PreviewProvider { 25 | static var previews: some View { 26 | PathRow(title: "Desktop Path", path: "~/Desktop") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SimDirs/Views/Styles/AppearanceButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppearanceButtonStyle.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 8/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension ButtonStyle where Self == AppearanceButtonStyle { 11 | static func appearance(selected: Bool, scheme: ColorScheme) -> AppearanceButtonStyle { 12 | AppearanceButtonStyle(selected: selected, scheme: scheme) 13 | } 14 | } 15 | 16 | struct AppearanceButtonStyle: ButtonStyle { 17 | let selected : Bool 18 | let scheme : ColorScheme 19 | 20 | func makeBody(configuration: Configuration) -> some View { 21 | VStack { 22 | let bordered = selected != configuration.isPressed 23 | let color = scheme == .light ? Color.white : Color.black 24 | let content = color 25 | .frame(width: 48, height: 32) 26 | .cornerRadius(5.0) 27 | 28 | if bordered { 29 | content 30 | .overlay( 31 | RoundedRectangle(cornerRadius: 6.0) 32 | .stroke(Color.accentColor, lineWidth: 2.0) 33 | ) 34 | } 35 | else { 36 | content 37 | .shadow(color: .black.opacity(0.5), radius: 1.0, x: 0, y: 1.0) 38 | } 39 | 40 | configuration.label 41 | } 42 | .padding(1.0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SimDirs/Views/Styles/DescriptiveToggleStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DescriptiveToggleStyle.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 8/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DescriptiveToggleStyle: ToggleStyle { 11 | @Environment(\.isEnabled) var isEnabled 12 | @State var isFocused = false 13 | 14 | var descriptor : T 15 | var subtitled : Bool 16 | 17 | init(_ descriptor: T, subtitled: Bool = true) { 18 | self.descriptor = descriptor 19 | self.subtitled = subtitled 20 | } 21 | 22 | func makeBody(configuration: Configuration) -> some View { 23 | Button(action: { configuration.isOn.toggle() }) { 24 | VStack(spacing: 0) { 25 | ZStack { 26 | Circle() 27 | .foregroundColor(descriptor.circleColor.opacity(isFocused ? 1.0 : 0.9)) 28 | descriptor.image 29 | .resizable() 30 | .foregroundColor(descriptor.isOn ? .white : Color("CircleSymbolOff")) 31 | .aspectRatio(contentMode: .fit) 32 | .padding(9) 33 | } 34 | .frame(width: 36, height: 36) 35 | .padding(.bottom, 4) 36 | 37 | Group { 38 | Text(descriptor.titleKey) 39 | .fontWeight(.semibold) 40 | 41 | if subtitled { 42 | Text(descriptor.text) 43 | .foregroundColor(.secondary) 44 | } 45 | } 46 | .font(.system(size: 11)) 47 | .allowsTightening(true) 48 | .minimumScaleFactor(0.5) 49 | .multilineTextAlignment(.center) 50 | } 51 | .onHover { isFocused = $0 && isEnabled } 52 | } 53 | .buttonStyle(.plain) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SimDirs/Views/Styles/SystemIconButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemIconButtonStyle.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 8/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension ButtonStyle where Self == SystemIconButtonStyle { 11 | static func systemIcon(_ imageName: String, active: Bool = false) -> SystemIconButtonStyle { 12 | SystemIconButtonStyle(imageName, active: active) 13 | } 14 | } 15 | 16 | struct SystemIconButtonStyle: ButtonStyle { 17 | @Environment(\.isEnabled) var isEnabled 18 | @State var isFocused = false 19 | let isActive : Bool 20 | let imageName : String 21 | 22 | init(_ imageName: String, active: Bool = false) { 23 | self.imageName = imageName 24 | self.isActive = active 25 | } 26 | 27 | func makeBody(configuration: Configuration) -> some View { 28 | VStack { 29 | ZStack { 30 | backgroundColor(pressed: configuration.isPressed) 31 | .frame(width: 36, height: 36) 32 | .cornerRadius(5.0) 33 | 34 | Image(systemName: imageName) 35 | .resizable() 36 | .aspectRatio(contentMode: ContentMode.fit) 37 | .frame(width: 24, height: 24) 38 | .foregroundColor(isActive ? .accentColor : foregroundColor(pressed: configuration.isPressed)) 39 | } 40 | 41 | configuration.label 42 | .foregroundColor(foregroundColor(pressed: configuration.isPressed)) 43 | } 44 | .padding(1.0) 45 | .onHover { isFocused = $0 && isEnabled } 46 | } 47 | 48 | func foregroundColor(pressed: Bool) -> Color { 49 | return .primary.opacity(pressed ? 1.0 : (isEnabled ? 0.7 : 0.4)) 50 | } 51 | 52 | func backgroundColor(pressed: Bool) -> Color { 53 | return .primary.opacity((isEnabled ? 0.1 : 0.05) + (isFocused ? 0.1 : 0.0) + (pressed ? 0.1 : 0.0)) 54 | } 55 | } 56 | 57 | struct SystemIconButtonStyle_Previews: PreviewProvider { 58 | static var previews: some View { 59 | Button("Button") { print("do stuff") } 60 | .preferredColorScheme(.light) 61 | .buttonStyle(.systemIcon("camera.on.rectangle")) 62 | .padding(20) 63 | Button("Button") { print("do stuff") } 64 | .preferredColorScheme(.dark) 65 | .buttonStyle(.systemIcon("camera.on.rectangle")) 66 | .padding(20) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SimDirs/Views/ToolbarMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolbarMenu.swift 3 | // SimDirs 4 | // 5 | // Created by Casey Fleser on 6/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ToolbarMenu: View { 11 | @Binding var style : ContentView.Style 12 | @Binding var filter : SourceFilter 13 | 14 | var body: some View { 15 | Menu { 16 | Picker("Style", selection: $style) { 17 | ForEach(ContentView.Style.allCases) { style in 18 | if style.visible { 19 | Text(style.title).tag(style) 20 | } 21 | } 22 | } 23 | .pickerStyle(.inline) 24 | Toggle(isOn: $filter.filterApps) { Label("With Apps", systemImage: "app.fill") } 25 | Toggle(isOn: $filter.filterRuntimes) { Label("Installed Runtimes", systemImage: "cpu.fill") } 26 | } label: { 27 | Label("Filter", systemImage: "slider.horizontal.3") 28 | } 29 | } 30 | } 31 | 32 | struct ToolbarMenu_Previews: PreviewProvider { 33 | @State static var style = ContentView.Style.byDevice 34 | @State static var filter = SourceFilter.restore() 35 | 36 | static var previews: some View { 37 | ToolbarMenu(style: $style, filter: $filter) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /screens/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/screens/app.png -------------------------------------------------------------------------------- /screens/device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somegeekintn/SimDirs/baee85bcae985aecf3681eeab6d345ead34889a9/screens/device.png --------------------------------------------------------------------------------