├── .gitattributes ├── .gitignore ├── Assets └── SimBuddy.acorn ├── LICENSE ├── README.md ├── Screenshot.png ├── SimBuddy.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── SimBuddy.xcscheme └── SimBuddy ├── .gitattributes ├── .gitignore ├── AccentView.swift ├── AppDelegate.swift ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ ├── AppIcon128x128.png │ ├── AppIcon128x128@2x.png │ ├── AppIcon16x16.png │ ├── AppIcon16x16@2x.png │ ├── AppIcon256x256.png │ ├── AppIcon256x256@2x.png │ ├── AppIcon32x32.png │ ├── AppIcon32x32@2x.png │ ├── AppIcon512x512.png │ ├── AppIcon512x512@2x.png │ └── Contents.json └── Contents.json ├── Base.lproj └── Main.storyboard ├── Credits.rtf ├── Debug.swift ├── LICENSE ├── SimBuddy.entitlements ├── Simulator.swift └── ViewController.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .DS_Store 93 | 94 | -------------------------------------------------------------------------------- /Assets/SimBuddy.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/Assets/SimBuddy.acorn -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Craig Hockenberry 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 | # SimBuddy – Your Simulator’s BFF 2 | 3 | Have you ever added code like this to your app? 4 | 5 | ``` 6 | print(Bundle.main.resourcePath!) 7 | print(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path) 8 | ``` 9 | 10 | Or maybe you’ve been frustrated that you can't add that code because you’re in the middle of debugging? 11 | 12 | Yeah, me too. Many times. 13 | 14 | The locations show above, and many others, are available from Xcode using the `xcrun simctl` command. Every application on every device on every platform can be queried. But these lookups are difficult for developers because the information is structured around automatically generated GUIDs. The GUID you’re looking for changes every time a new OS is available, a device is added, or an application is installed. And we do that a lot! 15 | 16 | There are other tools available to help you navigate the _Simulator_, but they all do much more than I really need and take up space in my menu bar even though they are used infrequently. Additionally, none of these tools help find the "On My iPhone/iPad" container used by the Files app: a folder that I use whenever I’m testing import and export code. 17 | 18 | By now, you probably know where this is going: yes, I wrote my own utility and call it _SimBuddy_. It’s a [FREE download](https://files.iconfactory.net/software/SimBuddy.zip) from the [Iconfactory](https://iconfactory.com). 19 | 20 | ![Screenshot of SimBuddy window](./Screenshot.png) 21 | 22 | _SimBuddy_ uses two popup menus for navigation: the top one shows which devices are running in the _Simulator_ and the one below it shows all the applications installed on that device (your apps are listed first). Once you make a choice with those popups, you can use the buttons at the bottom of the window to navigate in the Finder. If you are using app group containers for sharing information between an extension/widget and your main app, you open those folders by selecting the ID and using "Open". 23 | 24 | If the _Terminal_ is more your thing, you can hold down the option key while clicking a button and a path to the folder is put on the clipboard. Paste that into a command line and away you go! 25 | 26 | It’s not a complicated app, as you can see from the [source code](https://github.com/chockenberry/SimBuddy), but it’s one that I’m very happy to have in my developer toolbox now. I hope you enjoy it, too! 27 | 28 | P.S. I love putting Easter eggs in apps. This time it’s in the app [icon](https://github.com/chockenberry/SimBuddy/blob/main/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon512x512%402x.png). 29 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/Screenshot.png -------------------------------------------------------------------------------- /SimBuddy.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4421AE752943F07400F43940 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4421AE742943F07400F43940 /* AppDelegate.swift */; }; 11 | 4421AE772943F07400F43940 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4421AE762943F07400F43940 /* ViewController.swift */; }; 12 | 4421AE792943F07500F43940 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4421AE782943F07500F43940 /* Assets.xcassets */; }; 13 | 4421AE7C2943F07500F43940 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4421AE7A2943F07500F43940 /* Main.storyboard */; }; 14 | 4421AE842944157400F43940 /* Simulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4421AE832944157400F43940 /* Simulator.swift */; }; 15 | 4421AE8629441DAE00F43940 /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4421AE8529441DAE00F43940 /* Debug.swift */; }; 16 | 443F81822947FEB200B24930 /* AccentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443F81812947FEB200B24930 /* AccentView.swift */; }; 17 | 443F8184294FB6ED00B24930 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 443F8183294FB6ED00B24930 /* Credits.rtf */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 4421AE712943F07400F43940 /* SimBuddy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimBuddy.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 4421AE742943F07400F43940 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 4421AE762943F07400F43940 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 24 | 4421AE782943F07500F43940 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 4421AE7B2943F07500F43940 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 4421AE7D2943F07500F43940 /* SimBuddy.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SimBuddy.entitlements; sourceTree = ""; }; 27 | 4421AE832944157400F43940 /* Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulator.swift; sourceTree = ""; }; 28 | 4421AE8529441DAE00F43940 /* Debug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = ""; }; 29 | 443F81812947FEB200B24930 /* AccentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentView.swift; sourceTree = ""; }; 30 | 443F8183294FB6ED00B24930 /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 31 | 447540E4294D35FA002CA9A3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | 4421AE6E2943F07400F43940 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 4421AE682943F07400F43940 = { 46 | isa = PBXGroup; 47 | children = ( 48 | 447540E4294D35FA002CA9A3 /* README.md */, 49 | 4421AE732943F07400F43940 /* SimBuddy */, 50 | 4421AE722943F07400F43940 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 4421AE722943F07400F43940 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 4421AE712943F07400F43940 /* SimBuddy.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 4421AE732943F07400F43940 /* SimBuddy */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 4421AE742943F07400F43940 /* AppDelegate.swift */, 66 | 4421AE762943F07400F43940 /* ViewController.swift */, 67 | 443F81812947FEB200B24930 /* AccentView.swift */, 68 | 4421AE832944157400F43940 /* Simulator.swift */, 69 | 4421AE8529441DAE00F43940 /* Debug.swift */, 70 | 4421AE782943F07500F43940 /* Assets.xcassets */, 71 | 4421AE7A2943F07500F43940 /* Main.storyboard */, 72 | 443F8183294FB6ED00B24930 /* Credits.rtf */, 73 | 4421AE7D2943F07500F43940 /* SimBuddy.entitlements */, 74 | ); 75 | path = SimBuddy; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | 4421AE702943F07400F43940 /* SimBuddy */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = 4421AE802943F07500F43940 /* Build configuration list for PBXNativeTarget "SimBuddy" */; 84 | buildPhases = ( 85 | 4421AE6D2943F07400F43940 /* Sources */, 86 | 4421AE6E2943F07400F43940 /* Frameworks */, 87 | 4421AE6F2943F07400F43940 /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = SimBuddy; 94 | productName = SimBuddy; 95 | productReference = 4421AE712943F07400F43940 /* SimBuddy.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | 4421AE692943F07400F43940 /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | BuildIndependentTargetsInParallel = 1; 105 | LastSwiftUpdateCheck = 1410; 106 | LastUpgradeCheck = 1410; 107 | TargetAttributes = { 108 | 4421AE702943F07400F43940 = { 109 | CreatedOnToolsVersion = 14.1; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = 4421AE6C2943F07400F43940 /* Build configuration list for PBXProject "SimBuddy" */; 114 | compatibilityVersion = "Xcode 14.0"; 115 | developmentRegion = en; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | Base, 120 | ); 121 | mainGroup = 4421AE682943F07400F43940; 122 | productRefGroup = 4421AE722943F07400F43940 /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | 4421AE702943F07400F43940 /* SimBuddy */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXResourcesBuildPhase section */ 132 | 4421AE6F2943F07400F43940 /* Resources */ = { 133 | isa = PBXResourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 4421AE792943F07500F43940 /* Assets.xcassets in Resources */, 137 | 443F8184294FB6ED00B24930 /* Credits.rtf in Resources */, 138 | 4421AE7C2943F07500F43940 /* Main.storyboard in Resources */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXResourcesBuildPhase section */ 143 | 144 | /* Begin PBXSourcesBuildPhase section */ 145 | 4421AE6D2943F07400F43940 /* Sources */ = { 146 | isa = PBXSourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 4421AE772943F07400F43940 /* ViewController.swift in Sources */, 150 | 443F81822947FEB200B24930 /* AccentView.swift in Sources */, 151 | 4421AE8629441DAE00F43940 /* Debug.swift in Sources */, 152 | 4421AE752943F07400F43940 /* AppDelegate.swift in Sources */, 153 | 4421AE842944157400F43940 /* Simulator.swift in Sources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXSourcesBuildPhase section */ 158 | 159 | /* Begin PBXVariantGroup section */ 160 | 4421AE7A2943F07500F43940 /* Main.storyboard */ = { 161 | isa = PBXVariantGroup; 162 | children = ( 163 | 4421AE7B2943F07500F43940 /* Base */, 164 | ); 165 | name = Main.storyboard; 166 | sourceTree = ""; 167 | }; 168 | /* End PBXVariantGroup section */ 169 | 170 | /* Begin XCBuildConfiguration section */ 171 | 4421AE7E2943F07500F43940 /* Debug */ = { 172 | isa = XCBuildConfiguration; 173 | buildSettings = { 174 | ALWAYS_SEARCH_USER_PATHS = NO; 175 | CLANG_ANALYZER_NONNULL = YES; 176 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 177 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 178 | CLANG_ENABLE_MODULES = YES; 179 | CLANG_ENABLE_OBJC_ARC = YES; 180 | CLANG_ENABLE_OBJC_WEAK = YES; 181 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 182 | CLANG_WARN_BOOL_CONVERSION = YES; 183 | CLANG_WARN_COMMA = YES; 184 | CLANG_WARN_CONSTANT_CONVERSION = YES; 185 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 186 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 187 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 188 | CLANG_WARN_EMPTY_BODY = YES; 189 | CLANG_WARN_ENUM_CONVERSION = YES; 190 | CLANG_WARN_INFINITE_RECURSION = YES; 191 | CLANG_WARN_INT_CONVERSION = YES; 192 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 193 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 194 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 195 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 196 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 197 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 198 | CLANG_WARN_STRICT_PROTOTYPES = YES; 199 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 200 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 201 | CLANG_WARN_UNREACHABLE_CODE = YES; 202 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 203 | COPY_PHASE_STRIP = NO; 204 | DEBUG_INFORMATION_FORMAT = dwarf; 205 | ENABLE_STRICT_OBJC_MSGSEND = YES; 206 | ENABLE_TESTABILITY = YES; 207 | GCC_C_LANGUAGE_STANDARD = gnu11; 208 | GCC_DYNAMIC_NO_PIC = NO; 209 | GCC_NO_COMMON_BLOCKS = YES; 210 | GCC_OPTIMIZATION_LEVEL = 0; 211 | GCC_PREPROCESSOR_DEFINITIONS = ( 212 | "DEBUG=1", 213 | "$(inherited)", 214 | ); 215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 217 | GCC_WARN_UNDECLARED_SELECTOR = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 219 | GCC_WARN_UNUSED_FUNCTION = YES; 220 | GCC_WARN_UNUSED_VARIABLE = YES; 221 | MACOSX_DEPLOYMENT_TARGET = 12.6; 222 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 223 | MTL_FAST_MATH = YES; 224 | ONLY_ACTIVE_ARCH = YES; 225 | SDKROOT = macosx; 226 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 227 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 228 | }; 229 | name = Debug; 230 | }; 231 | 4421AE7F2943F07500F43940 /* Release */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | ALWAYS_SEARCH_USER_PATHS = NO; 235 | CLANG_ANALYZER_NONNULL = YES; 236 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu11; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | MACOSX_DEPLOYMENT_TARGET = 12.6; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | MTL_FAST_MATH = YES; 278 | SDKROOT = macosx; 279 | SWIFT_COMPILATION_MODE = wholemodule; 280 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 281 | }; 282 | name = Release; 283 | }; 284 | 4421AE812943F07500F43940 /* Debug */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 289 | CODE_SIGN_ENTITLEMENTS = SimBuddy/SimBuddy.entitlements; 290 | CODE_SIGN_STYLE = Automatic; 291 | COMBINE_HIDPI_IMAGES = YES; 292 | CURRENT_PROJECT_VERSION = 3; 293 | DEVELOPMENT_TEAM = RYQWBTQRPT; 294 | ENABLE_HARDENED_RUNTIME = YES; 295 | GENERATE_INFOPLIST_FILE = YES; 296 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 297 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 The Iconfactory. All rights reserved."; 298 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 299 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 300 | LD_RUNPATH_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "@executable_path/../Frameworks", 303 | ); 304 | MARKETING_VERSION = 1.0; 305 | PRODUCT_BUNDLE_IDENTIFIER = com.iconfactory.SimBuddy; 306 | PRODUCT_NAME = "$(TARGET_NAME)"; 307 | SWIFT_EMIT_LOC_STRINGS = YES; 308 | SWIFT_VERSION = 5.0; 309 | }; 310 | name = Debug; 311 | }; 312 | 4421AE822943F07500F43940 /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 317 | CODE_SIGN_ENTITLEMENTS = SimBuddy/SimBuddy.entitlements; 318 | CODE_SIGN_STYLE = Automatic; 319 | COMBINE_HIDPI_IMAGES = YES; 320 | CURRENT_PROJECT_VERSION = 3; 321 | DEVELOPMENT_TEAM = RYQWBTQRPT; 322 | ENABLE_HARDENED_RUNTIME = YES; 323 | GENERATE_INFOPLIST_FILE = YES; 324 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 325 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 The Iconfactory. All rights reserved."; 326 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 327 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 328 | LD_RUNPATH_SEARCH_PATHS = ( 329 | "$(inherited)", 330 | "@executable_path/../Frameworks", 331 | ); 332 | MARKETING_VERSION = 1.0; 333 | PRODUCT_BUNDLE_IDENTIFIER = com.iconfactory.SimBuddy; 334 | PRODUCT_NAME = "$(TARGET_NAME)"; 335 | SWIFT_EMIT_LOC_STRINGS = YES; 336 | SWIFT_VERSION = 5.0; 337 | }; 338 | name = Release; 339 | }; 340 | /* End XCBuildConfiguration section */ 341 | 342 | /* Begin XCConfigurationList section */ 343 | 4421AE6C2943F07400F43940 /* Build configuration list for PBXProject "SimBuddy" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 4421AE7E2943F07500F43940 /* Debug */, 347 | 4421AE7F2943F07500F43940 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | 4421AE802943F07500F43940 /* Build configuration list for PBXNativeTarget "SimBuddy" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | 4421AE812943F07500F43940 /* Debug */, 356 | 4421AE822943F07500F43940 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | /* End XCConfigurationList section */ 362 | }; 363 | rootObject = 4421AE692943F07400F43940 /* Project object */; 364 | } 365 | -------------------------------------------------------------------------------- /SimBuddy.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SimBuddy.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SimBuddy.xcodeproj/xcshareddata/xcschemes/SimBuddy.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 | -------------------------------------------------------------------------------- /SimBuddy/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /SimBuddy/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .DS_Store 93 | 94 | -------------------------------------------------------------------------------- /SimBuddy/AccentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccentView.swift 3 | // SimBuddy 4 | // 5 | // Created by Craig Hockenberry on 12/12/22. 6 | // 7 | 8 | import Cocoa 9 | 10 | class AccentView: NSView { 11 | 12 | override func draw(_ dirtyRect: CGRect) { 13 | if let accentColor = NSColor(named: "AccentColor") { 14 | accentColor.withAlphaComponent(0.05).set() 15 | dirtyRect.fill() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SimBuddy/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SimBuddy 4 | // 5 | // Created by Craig Hockenberry on 12/9/22. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | var windowController: NSWindowController? 14 | 15 | let hasRunKey = "hasRun" 16 | 17 | func applicationDidFinishLaunching(_ aNotification: Notification) { 18 | let hasRun = UserDefaults.standard.bool(forKey: hasRunKey) 19 | if !hasRun { 20 | showHelpWindow(nil) 21 | UserDefaults.standard.set(true, forKey: hasRunKey) 22 | } 23 | } 24 | 25 | func applicationWillTerminate(_ aNotification: Notification) { 26 | // Insert code here to tear down your application 27 | } 28 | 29 | func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool { 30 | return true 31 | } 32 | 33 | @IBAction 34 | func showHelpWindow(_ sender: Any?) { 35 | if windowController == nil { 36 | if let newWindowController = NSStoryboard.main?.instantiateController(withIdentifier: "helpWindowController") as? NSWindowController { 37 | newWindowController.loadWindow() 38 | windowController = newWindowController 39 | } 40 | } 41 | if let windowController { 42 | windowController.window?.makeKeyAndOrderFront(nil) 43 | } 44 | } 45 | 46 | @IBAction 47 | func openIconfactory(_ sender: Any?) { 48 | NSWorkspace.shared.open(URL(string: "https://iconfactory.com")!) 49 | } 50 | 51 | @IBAction 52 | func openIconfactoryApps(_ sender: Any?) { 53 | NSWorkspace.shared.open(URL(string: "https://iconfactoryapps.com")!) 54 | } 55 | 56 | @IBAction 57 | func openGitHubProject(_ sender: Any?) { 58 | NSWorkspace.shared.open(URL(string: "https://github.com/chockenberry/SimBuddy")!) 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.227", 9 | "green" : "0.300", 10 | "red" : "0.919" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon128x128@2x.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon16x16@2x.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon256x256@2x.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon32x32@2x.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chockenberry/SimBuddy/6735deb9d6f4c00b2c280d26a45d6ac9a21ac5ea/SimBuddy/Assets.xcassets/AppIcon.appiconset/AppIcon512x512@2x.png -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "AppIcon16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "AppIcon32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "AppIcon32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "AppIcon128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "AppIcon128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "AppIcon256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "AppIcon256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "AppIcon512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SimBuddy/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SimBuddy/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | Then select an application from the popup menu: your applications will be listed first. The buttons at the bottom of the window will then open folders for that application in the Finder. 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 499 | 511 | 523 | 535 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | -------------------------------------------------------------------------------- /SimBuddy/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2639 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset0 HelveticaNeue-Bold;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \deftab720 7 | \pard\pardeftab720\pardirnatural\qc\partightenfactor0 8 | 9 | \f0\fs22 \cf0 \ 10 | 11 | \f1\b Made by\ 12 | 13 | \f0\b0 \ 14 | Craig Hockenberry \ 15 | \ 16 | \ 17 | 18 | \f1\b In memory of\ 19 | 20 | \f0\b0 \ 21 | Mary Jay Dunham Hockenberry\ 22 | } -------------------------------------------------------------------------------- /SimBuddy/Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug.swift 3 | // 4 | // Created by Craig Hockenberry on 10/4/19. 5 | 6 | 7 | // Usage: 8 | // 9 | // SplineReticulationManager.swift: 10 | // 11 | // func reticulationFunction() -> Float { 12 | // debugLog() // prints "2019-10-04 11:52:28 SplineReticulationManager: reticulationFunction() called" 13 | // 14 | // let splineCount = Int.random(in: 0...1000) 15 | // debugLog("reticulating \(splineCount) splines") // prints "2019-10-04 11:52:28 SplineReticulationManager: reticulationFunction() reticulating 123 splines" 16 | // 17 | // return debugResult(Float.random(in: 0...1)) // prints "2019-10-04 11:52:28 SplineReticulationManager: reticulationFunction() returned: 0.12345" 18 | // } 19 | 20 | 21 | import Foundation 22 | 23 | func releaseLog(_ message: String = "called", file: String = #file, function: String = #function) { 24 | let timestamp = ISO8601DateFormatter.string(from: Date(), timeZone: TimeZone.current, formatOptions: [.withYear, .withMonth, .withDay, .withDashSeparatorInDate, .withTime, .withColonSeparatorInTime, .withSpaceBetweenDateAndTime]) 25 | print("\(timestamp) \(URL(fileURLWithPath: file, isDirectory: false).deletingPathExtension().lastPathComponent): \(function) \(message)") 26 | } 27 | 28 | func debugLog(_ message: String = "called", file: String = #file, function: String = #function) { 29 | #if DEBUG 30 | let timestamp = ISO8601DateFormatter.string(from: Date(), timeZone: TimeZone.current, formatOptions: [.withYear, .withMonth, .withDay, .withDashSeparatorInDate, .withTime, .withColonSeparatorInTime, .withSpaceBetweenDateAndTime]) 31 | print("\(timestamp) \(URL(fileURLWithPath: file, isDirectory: false).deletingPathExtension().lastPathComponent): \(function) \(message)") 32 | #endif 33 | } 34 | 35 | @discardableResult 36 | func debugResult(_ result: T, file: String = #file, function: String = #function) -> T { 37 | debugLog("returned: \(result)", file: file, function: function) 38 | return result 39 | } 40 | 41 | #if true 42 | 43 | // weed out NSLog usage 44 | @available(iOS, deprecated: 1.0, message: "Convert to debugLog") 45 | public func NSLog(_ format: String, _ args: CVarArg...) 46 | { 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /SimBuddy/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Craig Hockenberry 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 | -------------------------------------------------------------------------------- /SimBuddy/SimBuddy.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SimBuddy/Simulator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Simulator.swift 3 | // SimBuddy 4 | // 5 | // Created by Craig Hockenberry on 12/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /* 11 | /Applications/Xcode.app/Contents/Developer/usr/bin/simctl list devices -j 12 | 13 | { 14 | "devices" : { 15 | ... 16 | "com.apple.CoreSimulator.SimRuntime.iOS-16-1" : [ 17 | { 18 | "lastBootedAt" : "2022-12-07T23:42:38Z", 19 | "dataPath" : "\/Users\/CHOCK\/Library\/Developer\/CoreSimulator\/Devices\/573D4A53-D1EC-48CA-A554-8DF7A94EFFBD\/data", 20 | "dataPathSize" : 4834729984, 21 | "logPath" : "\/Users\/CHOCK\/Library\/Logs\/CoreSimulator\/573D4A53-D1EC-48CA-A554-8DF7A94EFFBD", 22 | "udid" : "573D4A53-D1EC-48CA-A554-8DF7A94EFFBD", 23 | "isAvailable" : true, 24 | "logPathSize" : 1110016, 25 | "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-14", 26 | "state" : "Booted", 27 | "name" : "iPhone 14" 28 | }, 29 | { 30 | "dataPath" : "\/Users\/CHOCK\/Library\/Developer\/CoreSimulator\/Devices\/CB23E7F5-27DA-4290-9C1E-C641F0592BAF\/data", 31 | "dataPathSize" : 13316096, 32 | "logPath" : "\/Users\/CHOCK\/Library\/Logs\/CoreSimulator\/CB23E7F5-27DA-4290-9C1E-C641F0592BAF", 33 | "udid" : "CB23E7F5-27DA-4290-9C1E-C641F0592BAF", 34 | "isAvailable" : true, 35 | "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Plus", 36 | "state" : "Shutdown", 37 | "name" : "iPhone 14 Plus" 38 | } 39 | ... 40 | ] 41 | 42 | */ 43 | 44 | struct DeviceInfo { 45 | let name: String // name 46 | let dataPath: String // dataPath 47 | let isBooted: Bool // state == "Booted" 48 | let udid: String // udid 49 | 50 | var uniqueIdentifier: String { 51 | get { 52 | udid 53 | } 54 | } 55 | } 56 | 57 | /* 58 | /Applications/Xcode.app/Contents/Developer/usr/bin/simctl runtime list -j 59 | 60 | { 61 | "2B043A77-27EC-46F5-9E71-926519FB1DF8" : { 62 | "build" : "20K67", 63 | "deletable" : true, 64 | "identifier" : "2B043A77-27EC-46F5-9E71-926519FB1DF8", 65 | "kind" : "Disk Image", 66 | "mountPath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/tvOS_20K67", 67 | "path" : "\/Library\/Developer\/CoreSimulator\/Images\/2B043A77-27EC-46F5-9E71-926519FB1DF8.dmg", 68 | "platformIdentifier" : "com.apple.platform.appletvsimulator", 69 | "runtimeBundlePath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/tvOS_20K67\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/tvOS 16.1.simruntime 70 | ", 71 | "runtimeIdentifier" : "com.apple.CoreSimulator.SimRuntime.tvOS-16-1", 72 | "signatureState" : "Verified", 73 | "sizeBytes" : 3363890340, 74 | "state" : "Ready", 75 | "version" : "16.1" 76 | }, 77 | "03F43412-C224-4BE7-82FB-D7EF8384F91A" : { 78 | "build" : "20B72", 79 | "deletable" : false, 80 | "identifier" : "03F43412-C224-4BE7-82FB-D7EF8384F91A", 81 | "kind" : "Bundled with Xcode", 82 | "path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime", 83 | "platformIdentifier" : "com.apple.platform.iphonesimulator", 84 | "runtimeBundlePath" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime", 85 | "runtimeIdentifier" : "com.apple.CoreSimulator.SimRuntime.iOS-16-1", 86 | "signatureState" : "Unknown", 87 | "sizeBytes" : 3883925504, 88 | "state" : "Ready", 89 | "version" : "16.1" 90 | }, 91 | 92 | */ 93 | 94 | /* 95 | xcrun simctl listapps A42D2B4A-F65D-4E73-A1D7-3B9D20FA6FA0 > /tmp/output 96 | plutil -convert json /tmp/output -e json 97 | jsonify /tmp/output.json 98 | 99 | "com.iconfactory.Graphite" : { 100 | "ApplicationType" : "User", 101 | "Bundle" : "file:///Users/CHOCK/Library/Developer/CoreSimulator/Devices/A42D2B4A-F65D-4E73-A1D7-3B9D20FA6FA0/data/Containers/Bundle/Application/BC037B58-1CA1-4CEA-8908-05D468DDEC0C/Linea%20Sketch.app/", 102 | "BundleContainer" : "file:///Users/CHOCK/Library/Developer/CoreSimulator/Devices/A42D2B4A-F65D-4E73-A1D7-3B9D20FA6FA0/data/Containers/Bundle/Application/BC037B58-1CA1-4CEA-8908-05D468DDEC0C/", 103 | "CFBundleDisplayName" : "Linea Sketch", 104 | "CFBundleExecutable" : "Linea Sketch", 105 | "CFBundleIdentifier" : "com.iconfactory.Graphite", 106 | "CFBundleName" : "Linea Sketch", 107 | "CFBundleVersion" : "756", 108 | "DataContainer" : "file:///Users/CHOCK/Library/Developer/CoreSimulator/Devices/A42D2B4A-F65D-4E73-A1D7-3B9D20FA6FA0/data/Containers/Data/Application/713638FE-022B-444D-83A8-AF50E2F032A3/", 109 | "GroupContainers" : { 110 | "group.com.iconfactory.Graphite" : "file:///Users/CHOCK/Library/Developer/CoreSimulator/Devices/A42D2B4A-F65D-4E73-A1D7-3B9D20FA6FA0/data/Containers/Shared/AppGroup/DE8B3264-DCC6-488B-9DC2-EB76D8B46985/" 111 | }, 112 | "Path" : "/Users/CHOCK/Library/Developer/CoreSimulator/Devices/A42D2B4A-F65D-4E73-A1D7-3B9D20FA6FA0/data/Containers/Bundle/Application/BC037B58-1CA1-4CEA-8908-05D468DDEC0C/Linea Sketch.app", 113 | "SBAppTags" : [] 114 | }, 115 | 116 | */ 117 | 118 | struct ApplicationInfo { 119 | let name: String // CFBundleDisplayName 120 | let type: String // ApplicationType 121 | let bundleURL: URL // Bundle 122 | let bundleIdentifier: String // CFBundleIdentifier 123 | let bundleName: String // CFBundleName 124 | let dataURL: URL // DataContainer 125 | 126 | let groupContainers: [GroupContainerInfo] 127 | 128 | var uniqueIdentifier: String { 129 | get { 130 | bundleIdentifier 131 | } 132 | } 133 | } 134 | 135 | struct GroupContainerInfo { 136 | let identifier: String // key 137 | let containerURL: URL // value 138 | 139 | var uniqueIdentifier: String { 140 | get { 141 | identifier 142 | } 143 | } 144 | } 145 | 146 | class Simulator { 147 | 148 | static let plutilURL = URL(fileURLWithPath: "/usr/bin/plutil") 149 | static let xcrunURL = URL(fileURLWithPath: "/usr/bin/xcrun") 150 | 151 | static func applications(for udid: String) async -> [ApplicationInfo] { 152 | var result: [ApplicationInfo] = [] 153 | 154 | do { 155 | let executableURL = xcrunURL 156 | let arguments = ["simctl", "listapps", udid] 157 | let applicationData = try await Process.launch(executableURL: executableURL, arguments: arguments) 158 | do { 159 | let executableURL = plutilURL 160 | let arguments = ["-convert", "json", "-o", "-", "--", "-"] // do you like a dash of dash with your arguments? 161 | let data = try await Process.launch(executableURL: executableURL, arguments: arguments, input: applicationData) 162 | let object = try? JSONSerialization.jsonObject(with: data) 163 | if let root = object as? Dictionary { 164 | for item in root { 165 | if let application = item.value as? Dictionary { 166 | if let name = application["CFBundleDisplayName"] as? String, 167 | let type = application["ApplicationType"] as? String, 168 | let bundlePath = application["Bundle"] as? String, 169 | let bundleIdentifier = application["CFBundleIdentifier"] as? String, 170 | let bundleName = application["CFBundleName"] as? String, 171 | let dataPath = application["DataContainer"] as? String 172 | { 173 | if let bundleURL = URL(string: bundlePath), 174 | let dataURL = URL(string: dataPath) 175 | { 176 | var groupContainers: [GroupContainerInfo] = [] 177 | if let containers = application["GroupContainers"] as? Dictionary { 178 | for container in containers { 179 | let identifier = container.key 180 | if let containerPath = container.value as? String, 181 | let containerURL = URL(string: containerPath) 182 | { 183 | let groupContainerInfo = GroupContainerInfo(identifier: identifier, containerURL: containerURL) 184 | groupContainers.append(groupContainerInfo) 185 | } 186 | } 187 | } 188 | let applicationInfo = ApplicationInfo(name: name, type: type, bundleURL: bundleURL, bundleIdentifier: bundleIdentifier, bundleName: bundleName, dataURL: dataURL, groupContainers: groupContainers) 189 | result.append(applicationInfo) 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | } 197 | } 198 | catch { 199 | debugLog("error: \(error.localizedDescription)") 200 | } 201 | 202 | return result 203 | } 204 | 205 | static func devices() async -> [DeviceInfo] { 206 | var result: [DeviceInfo] = [] 207 | 208 | do { 209 | let executableURL = xcrunURL 210 | let arguments = ["simctl", "list", "devices", "-j"] 211 | let data = try await Process.launch(executableURL: executableURL, arguments: arguments) 212 | 213 | let object = try? JSONSerialization.jsonObject(with: data) 214 | if let root = object as? Dictionary { 215 | if let simulators = root["devices"] as? Dictionary> { 216 | for simulator in simulators { 217 | for device in simulator.value { 218 | if let device = device as? Dictionary { 219 | if let name = device["name"] as? String, 220 | let dataPath = device["dataPath"] as? String, 221 | let state = device["state"] as? String, 222 | let udid = device["udid"] as? String 223 | { 224 | let deviceInfo = DeviceInfo(name: name, dataPath: dataPath, isBooted: state == "Booted", udid: udid) 225 | result.append(deviceInfo) 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | catch { 234 | debugLog("error: \(error.localizedDescription)") 235 | } 236 | 237 | return result 238 | 239 | /* This could work in a sandbox, but would require getting a security-scoped bookmark for Xcode and tolerating a bunch of console spew: 240 | 241 | xcodeURL.startAccessingSecurityScopedResource() 242 | 243 | let executableURL = simctlURL 244 | let arguments = ["list", "devices", "-j"] 245 | let data = try await Process.launch(executableURL: executableURL, arguments: arguments) 246 | 247 | xcodeURL.stopAccessingSecurityScopedResource() 248 | */ 249 | } 250 | 251 | } 252 | 253 | extension Process { 254 | 255 | /// Quinn's code with async/await support 256 | /// 257 | /// - Parameters: 258 | /// - executableURL: The tool to run. 259 | /// - arguments: The command-line arguments to pass to that tool; defaults to the empty array. 260 | /// - input: Data to pass to the tool’s `stdin`; defaults to empty. 261 | /// - Returns: 262 | /// - Data from `stdout` 263 | 264 | // Adapted from: https://wwdcbysundell.com/2021/wrapping-completion-handlers-into-async-apis/ 265 | 266 | @MainActor 267 | static func launch(executableURL: URL, arguments: [String] = [], input: Data = Data()) async throws -> Data { 268 | return try await withCheckedThrowingContinuation { continuation in 269 | launch(executableURL: executableURL, arguments: arguments, input: input) { result, output in 270 | switch result { 271 | case .success(_): 272 | continuation.resume(returning: output) 273 | case .failure(let error): 274 | continuation.resume(throwing: error) 275 | } 276 | } 277 | } 278 | } 279 | 280 | 281 | // The following code by Quinn is from the Developer Forums: https://developer.apple.com/forums/thread/690310 282 | 283 | /// Runs the specified tool as a child process, supplying `stdin` and capturing `stdout`. 284 | /// 285 | /// - important: Must be run on the main queue. 286 | /// 287 | /// - Parameters: 288 | /// - executableURL: The tool to run. 289 | /// - arguments: The command-line arguments to pass to that tool; defaults to the empty array. 290 | /// - input: Data to pass to the tool’s `stdin`; defaults to empty. 291 | /// - completionHandler: Called on the main queue when the tool has terminated. 292 | 293 | static func launch(executableURL: URL, arguments: [String] = [], input: Data = Data(), completionHandler: @escaping CompletionHandler) { 294 | // This precondition is important; read the comment near the `run()` call to 295 | // understand why. 296 | dispatchPrecondition(condition: .onQueue(.main)) 297 | 298 | let group = DispatchGroup() 299 | let inputPipe = Pipe() 300 | let outputPipe = Pipe() 301 | 302 | var errorQ: Error? = nil 303 | var output = Data() 304 | 305 | let proc = Process() 306 | proc.executableURL = executableURL 307 | proc.arguments = arguments 308 | proc.standardInput = inputPipe 309 | proc.standardOutput = outputPipe 310 | group.enter() 311 | proc.terminationHandler = { _ in 312 | // This bounce to the main queue is important; read the comment near the 313 | // `run()` call to understand why. 314 | DispatchQueue.main.async { 315 | group.leave() 316 | } 317 | } 318 | 319 | // This runs the supplied block when all three events have completed (task 320 | // termination and the end of both I/O channels). 321 | // 322 | // - important: If the process was never launched, requesting its 323 | // termination status raises an Objective-C exception (ouch!). So, we only 324 | // read `terminationStatus` if `errorQ` is `nil`. 325 | 326 | group.notify(queue: .main) { 327 | if let error = errorQ { 328 | completionHandler(.failure(error), output) 329 | } else { 330 | completionHandler(.success(proc.terminationStatus), output) 331 | } 332 | } 333 | 334 | do { 335 | func posixErr(_ error: Int32) -> Error { NSError(domain: NSPOSIXErrorDomain, code: Int(error), userInfo: nil) } 336 | 337 | // If you write to a pipe whose remote end has closed, the OS raises a 338 | // `SIGPIPE` signal whose default disposition is to terminate your 339 | // process. Helpful! `F_SETNOSIGPIPE` disables that feature, causing 340 | // the write to fail with `EPIPE` instead. 341 | 342 | let fcntlResult = fcntl(inputPipe.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1) 343 | guard fcntlResult >= 0 else { throw posixErr(errno) } 344 | 345 | // Actually run the process. 346 | 347 | try proc.run() 348 | 349 | // At this point the termination handler could run and leave the group 350 | // before we have a chance to enter the group for each of the I/O 351 | // handlers. I avoid this problem by having the termination handler 352 | // dispatch to the main thread. We are running on the main thread, so 353 | // the termination handler can’t run until we return, at which point we 354 | // have already entered the group for each of the I/O handlers. 355 | // 356 | // An alternative design would be to enter the group at the top of this 357 | // block and then leave it in the error hander. I decided on this 358 | // design because it has the added benefit of all my code running on the 359 | // main queue and thus I can access shared mutable state, like `errorQ`, 360 | // without worrying about thread safety. 361 | 362 | // Enter the group and then set up a Dispatch I/O channel to write our 363 | // data to the child’s `stdin`. When that’s done, record any error and 364 | // leave the group. 365 | // 366 | // Note that we ignore the residual value passed to the 367 | // `write(offset:data:queue:ioHandler:)` completion handler. Earlier 368 | // versions of this code passed it along to our completion handler but 369 | // the reality is that it’s not very useful. The pipe buffer is big 370 | // enough that it usually soaks up all our data, so the residual is a 371 | // very poor indication of how much data was actually read by the 372 | // client. 373 | 374 | group.enter() 375 | let writeIO = DispatchIO(type: .stream, fileDescriptor: inputPipe.fileHandleForWriting.fileDescriptor, queue: .main) { _ in 376 | // `FileHandle` will automatically close the underlying file 377 | // descriptor when you release the last reference to it. By holidng 378 | // on to `inputPipe` until here, we ensure that doesn’t happen. And 379 | // as we have to hold a reference anyway, we might as well close it 380 | // explicitly. 381 | // 382 | // We apply the same logic to `readIO` below. 383 | try! inputPipe.fileHandleForWriting.close() 384 | } 385 | let inputDD = input.withUnsafeBytes { DispatchData(bytes: $0) } 386 | writeIO.write(offset: 0, data: inputDD, queue: .main) { isDone, _, error in 387 | if isDone || error != 0 { 388 | writeIO.close() 389 | if errorQ == nil && error != 0 { errorQ = posixErr(error) } 390 | group.leave() 391 | } 392 | } 393 | 394 | // Enter the group and then set up a Dispatch I/O channel to read data 395 | // from the child’s `stdin`. When that’s done, record any error and 396 | // leave the group. 397 | 398 | group.enter() 399 | let readIO = DispatchIO(type: .stream, fileDescriptor: outputPipe.fileHandleForReading.fileDescriptor, queue: .main) { _ in 400 | try! outputPipe.fileHandleForReading.close() 401 | } 402 | readIO.read(offset: 0, length: .max, queue: .main) { isDone, chunkQ, error in 403 | output.append(contentsOf: chunkQ ?? .empty) 404 | if isDone || error != 0 { 405 | readIO.close() 406 | if errorQ == nil && error != 0 { errorQ = posixErr(error) } 407 | group.leave() 408 | } 409 | } 410 | } catch { 411 | // If either the `fcntl` or the `run()` call threw, we set the error 412 | // and manually call the termination handler. Note that we’ve only 413 | // entered the group once at this point, so the single leave done by the 414 | // termination handler is enough to run the notify block and call the 415 | // client’s completion handler. 416 | errorQ = error 417 | proc.terminationHandler!(proc) 418 | } 419 | } 420 | 421 | /// Called when the tool has terminated. 422 | /// 423 | /// This must be run on the main queue. 424 | /// 425 | /// - Parameters: 426 | /// - result: Either the tool’s termination status or, if something went 427 | /// wrong, an error indicating what that was. 428 | /// - output: Data captured from the tool’s `stdout`. 429 | 430 | typealias CompletionHandler = (_ result: Result, _ output: Data) -> Void 431 | 432 | } 433 | -------------------------------------------------------------------------------- /SimBuddy/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SimBuddy 4 | // 5 | // Created by Craig Hockenberry on 12/9/22. 6 | // 7 | 8 | import Cocoa 9 | 10 | class ViewController: NSViewController { 11 | 12 | @IBOutlet var devicePopUpButton: NSPopUpButton! 13 | @IBOutlet var applicationPopUpButton: NSPopUpButton! 14 | @IBOutlet var groupContainersPopUpButton: NSPopUpButton! 15 | @IBOutlet var openGroupContainerButton: NSButton! 16 | 17 | @IBOutlet var deviceUDIDTextField: NSTextField! 18 | @IBOutlet var bundleNameTextField: NSTextField! 19 | @IBOutlet var bundleIdentifierTextField: NSTextField! 20 | 21 | @IBOutlet var openBundleButton: NSButton! 22 | @IBOutlet var openDataButton: NSButton! 23 | @IBOutlet var openDocumentsButton: NSButton! 24 | @IBOutlet var openPreferencesButton: NSButton! 25 | @IBOutlet var openLocalFilesButton: NSButton! 26 | 27 | let selectedDeviceIdentifierKey = "selectedDeviceIdentifier" 28 | let selectedApplicationIdentifierKey = "selectedApplicationIdentifier" 29 | let selectedGroupContainerIdentifierKey = "selectedGroupContainerIdentifier" 30 | 31 | var devices: [DeviceInfo] = [] 32 | var applications: [ApplicationInfo] = [] 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | let notificationCenter = NotificationCenter.default 38 | notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil) 39 | } 40 | 41 | override var representedObject: Any? { 42 | didSet { 43 | // Update the view, if already loaded. 44 | updateView() 45 | } 46 | } 47 | 48 | // MARK: - 49 | 50 | func loadDevices() { 51 | Task { 52 | devices = await Simulator.devices().filter({deviceInfo in 53 | deviceInfo.isBooted 54 | }).sorted(by: { firstDevice, secondDevice in 55 | firstDevice.name < secondDevice.name 56 | }) 57 | 58 | loadApplications() 59 | 60 | updateView() 61 | } 62 | } 63 | 64 | func loadApplications() { 65 | if haveDevices { 66 | Task { 67 | let deviceIdentifier = devices[selectedDeviceIndex].uniqueIdentifier 68 | applications = await Simulator.applications(for: deviceIdentifier).sorted(by: { firstApplication, secondApplication in 69 | firstApplication.name < secondApplication.name 70 | }) 71 | updateView() 72 | } 73 | } 74 | else { 75 | applications = [] 76 | updateView() 77 | } 78 | } 79 | 80 | // MARK: - 81 | 82 | var haveDevices: Bool { 83 | return devices.count > 0 84 | } 85 | 86 | var selectedDeviceIndex: Int { 87 | if let selectedDeviceIdentifier = UserDefaults.standard.string(forKey: selectedDeviceIdentifierKey) { 88 | if let deviceIndex = devices.firstIndex(where: { deviceInfo in 89 | deviceInfo.uniqueIdentifier == selectedDeviceIdentifier 90 | }) 91 | { 92 | return deviceIndex 93 | } 94 | } 95 | return 0 96 | } 97 | 98 | var selectedDevice: DeviceInfo { 99 | return devices[selectedDeviceIndex] 100 | } 101 | 102 | // MARK: - 103 | 104 | var haveApplications: Bool { 105 | return applications.count > 0 106 | } 107 | 108 | var selectedApplicationIndex: Int { 109 | // if an application has been selected previously, use that if possible 110 | if let selectedApplicationIdentifier = UserDefaults.standard.string(forKey: selectedApplicationIdentifierKey) { 111 | if let applicationIndex = applications.firstIndex(where: { applicationInfo in 112 | applicationInfo.uniqueIdentifier == selectedApplicationIdentifier 113 | }) { 114 | return applicationIndex 115 | } 116 | } 117 | 118 | // if not, try to use the first user application, and if that fails use the first one 119 | if let applicationIndex = applications.firstIndex(where: { applicationInfo in 120 | applicationInfo.type == "User" 121 | }) { 122 | return applicationIndex 123 | } 124 | else { 125 | return 0 126 | } 127 | } 128 | 129 | var selectedApplication: ApplicationInfo { 130 | return applications[selectedApplicationIndex] 131 | } 132 | 133 | // MARK: - 134 | 135 | var haveGroupContainers: Bool { 136 | if haveApplications { 137 | return selectedApplication.groupContainers.count > 0 138 | } 139 | return false 140 | } 141 | 142 | var selectedGroupContainerIndex: Int { 143 | if let selectedGroupContainerIdentifier = UserDefaults.standard.string(forKey: selectedGroupContainerIdentifierKey) { 144 | if let groupContainerIndex = selectedApplication.groupContainers.firstIndex(where: { groupContainerInfo in 145 | groupContainerInfo.uniqueIdentifier == selectedGroupContainerIdentifier 146 | }) { 147 | return groupContainerIndex 148 | } 149 | } 150 | return 0 151 | } 152 | 153 | var selectedGroupContainer: GroupContainerInfo { 154 | return selectedApplication.groupContainers[selectedGroupContainerIndex] 155 | } 156 | 157 | // MARK: - 158 | 159 | func updateView() { 160 | debugLog() 161 | 162 | // update devices popup and menu 163 | do { 164 | let menu = NSMenu(title: "Devices") 165 | if haveDevices { 166 | for (index, device) in devices.enumerated() { 167 | let menuItem = NSMenuItem(title: device.name, action: #selector(selectDevice), keyEquivalent: "") 168 | menuItem.tag = index 169 | menu.addItem(menuItem) 170 | } 171 | 172 | devicePopUpButton.menu = menu 173 | devicePopUpButton.isEnabled = true 174 | devicePopUpButton.selectItem(at: selectedDeviceIndex) 175 | } 176 | else { 177 | let menuItem = NSMenuItem(title: "No Devices", action: nil, keyEquivalent: "") 178 | menuItem.isEnabled = false 179 | menu.addItem(menuItem) 180 | 181 | devicePopUpButton.menu = menu 182 | devicePopUpButton.isEnabled = false 183 | devicePopUpButton.selectItem(at: 0) 184 | } 185 | } 186 | 187 | // update applications popup and menu 188 | do { 189 | let menu = NSMenu(title: "Applications") 190 | if haveApplications { 191 | // add user applications, a separator, then system applications 192 | for (index, application) in applications.enumerated() { 193 | if application.type == "User" { 194 | let menuItem = NSMenuItem(title: application.name, action: #selector(selectApplication), keyEquivalent: "") 195 | menuItem.tag = index 196 | menu.addItem(menuItem) 197 | } 198 | } 199 | do { 200 | let menuItem = NSMenuItem.separator() 201 | menuItem.tag = -1 202 | menu.addItem(menuItem) 203 | } 204 | for (index, application) in applications.enumerated() { 205 | if application.type != "User" { 206 | let menuItem = NSMenuItem(title: application.name, action: #selector(selectApplication), keyEquivalent: "") 207 | menuItem.tag = index 208 | menu.addItem(menuItem) 209 | } 210 | } 211 | 212 | applicationPopUpButton.menu = menu 213 | applicationPopUpButton.isEnabled = true 214 | applicationPopUpButton.selectItem(withTag: selectedApplicationIndex) 215 | } 216 | else { 217 | let menuItem = NSMenuItem(title: "No Applications", action: nil, keyEquivalent: "") 218 | menuItem.isEnabled = false 219 | menu.addItem(menuItem) 220 | 221 | applicationPopUpButton.menu = menu 222 | applicationPopUpButton.isEnabled = false 223 | applicationPopUpButton.selectItem(at: 0) 224 | } 225 | } 226 | 227 | // update information and controls 228 | do { 229 | if haveDevices { 230 | deviceUDIDTextField.stringValue = selectedDevice.uniqueIdentifier 231 | } 232 | else { 233 | deviceUDIDTextField.stringValue = "No Device – Start one in the Simulator" 234 | } 235 | 236 | if haveApplications { 237 | bundleNameTextField.stringValue = selectedApplication.bundleName 238 | bundleIdentifierTextField.stringValue = selectedApplication.bundleIdentifier 239 | } 240 | else { 241 | bundleNameTextField.stringValue = "" 242 | bundleIdentifierTextField.stringValue = "" 243 | } 244 | 245 | let isEnabled = haveApplications 246 | openBundleButton.isEnabled = isEnabled 247 | openDataButton.isEnabled = isEnabled 248 | openDocumentsButton.isEnabled = isEnabled 249 | openPreferencesButton.isEnabled = isEnabled 250 | openLocalFilesButton.isEnabled = isEnabled 251 | } 252 | 253 | // update group containers popup and menu 254 | do { 255 | if haveApplications { 256 | let menu = NSMenu(title: "GroupContainers") 257 | if haveGroupContainers { 258 | for (index, groupContainer) in selectedApplication.groupContainers.enumerated() { 259 | let menuItem = NSMenuItem(title: groupContainer.identifier, action: #selector(selectGroupContainer), keyEquivalent: "") 260 | menuItem.tag = index 261 | menu.addItem(menuItem) 262 | } 263 | groupContainersPopUpButton.menu = menu 264 | groupContainersPopUpButton.isEnabled = true 265 | groupContainersPopUpButton.selectItem(at: selectedGroupContainerIndex) 266 | openGroupContainerButton.isEnabled = true 267 | } 268 | else { 269 | let menuItem = NSMenuItem(title: "No Group Containers", action: nil, keyEquivalent: "") 270 | menuItem.isEnabled = false 271 | menu.addItem(menuItem) 272 | groupContainersPopUpButton.menu = menu 273 | groupContainersPopUpButton.isEnabled = false 274 | groupContainersPopUpButton.selectItem(at: 0) 275 | openGroupContainerButton.isEnabled = false 276 | } 277 | } 278 | else { 279 | let menu = NSMenu(title: "GroupContainers") 280 | groupContainersPopUpButton.menu = menu 281 | groupContainersPopUpButton.isEnabled = false 282 | openGroupContainerButton.isEnabled = false 283 | } 284 | } 285 | } 286 | 287 | // MARK: - 288 | 289 | @objc 290 | func selectDevice(_ sender: Any) { 291 | debugLog("sender = \(sender)") 292 | if haveDevices { 293 | if let menuItem = sender as? NSMenuItem { 294 | let index = menuItem.tag 295 | let device = devices[index] 296 | debugLog("index = \(index), udid = \(device.udid)") 297 | UserDefaults.standard.set(device.uniqueIdentifier, forKey: selectedDeviceIdentifierKey) 298 | loadApplications() 299 | } 300 | } 301 | } 302 | 303 | @objc 304 | func selectApplication(_ sender: Any) { 305 | debugLog("sender = \(sender)") 306 | if haveApplications { 307 | if let menuItem = sender as? NSMenuItem { 308 | let index = menuItem.tag 309 | let application = applications[index] 310 | //debugLog("index = \(index), uniqueIdentifier = \(application.uniqueIdentifier)") 311 | UserDefaults.standard.set(application.uniqueIdentifier, forKey: selectedApplicationIdentifierKey) 312 | updateView() 313 | } 314 | } 315 | } 316 | 317 | @IBAction 318 | func selectGroupContainer(_ sender: Any) { 319 | debugLog("sender = \(sender)") 320 | if haveApplications { 321 | if let menuItem = sender as? NSMenuItem { 322 | let index = menuItem.tag 323 | let groupContainer = selectedApplication.groupContainers[index] 324 | //debugLog("index = \(index), containerURL = \(groupContainer.containerURL)") 325 | UserDefaults.standard.set(groupContainer.uniqueIdentifier, forKey: selectedGroupContainerIdentifierKey) 326 | } 327 | } 328 | } 329 | 330 | @IBAction 331 | func openBundle(_ sender: Any) { 332 | debugLog("sender = \(sender)") 333 | if haveApplications { 334 | if NSEvent.modifierFlags == [.option] { 335 | NSPasteboard.general.clearContents() 336 | NSPasteboard.general.setString(selectedApplication.bundleURL.path, forType: .string) 337 | } 338 | else { 339 | let selectedURL: URL 340 | if #available(macOS 13.0, *) { 341 | selectedURL = selectedApplication.bundleURL.appending(path: "Info.plist", directoryHint: .notDirectory) 342 | } else { 343 | selectedURL = selectedApplication.bundleURL.appendingPathComponent("Info.plist") 344 | } 345 | NSWorkspace.shared.activateFileViewerSelecting([selectedURL]) 346 | } 347 | } 348 | } 349 | 350 | @IBAction 351 | func openData(_ sender: Any) { 352 | debugLog("sender = \(sender)") 353 | if NSEvent.modifierFlags == [.option] { 354 | NSPasteboard.general.clearContents() 355 | NSPasteboard.general.setString(selectedApplication.dataURL.path, forType: .string) 356 | } 357 | else { 358 | NSWorkspace.shared.open(selectedApplication.dataURL) 359 | } 360 | } 361 | 362 | @IBAction 363 | func openDocuments(_ sender: Any) { 364 | debugLog("sender = \(sender)") 365 | if haveApplications { 366 | let documentsURL: URL 367 | if #available(macOS 13.0, *) { 368 | documentsURL = selectedApplication.dataURL.appending(path: "Documents", directoryHint: .isDirectory) 369 | } else { 370 | documentsURL = selectedApplication.dataURL.appendingPathComponent("Documents") 371 | } 372 | if NSEvent.modifierFlags == [.option] { 373 | NSPasteboard.general.clearContents() 374 | NSPasteboard.general.setString(documentsURL.path, forType: .string) 375 | } 376 | else { 377 | NSWorkspace.shared.open(documentsURL) 378 | } 379 | } 380 | } 381 | 382 | @IBAction 383 | func openPreferences(_ sender: Any) { 384 | debugLog("sender = \(sender)") 385 | if haveApplications { 386 | let preferencesURL: URL 387 | if #available(macOS 13.0, *) { 388 | preferencesURL = selectedApplication.dataURL.appending(path: "Library/Preferences", directoryHint: .isDirectory) 389 | } else { 390 | preferencesURL = selectedApplication.dataURL.appendingPathComponent("Library/Preferences") 391 | } 392 | if NSEvent.modifierFlags == [.option] { 393 | NSPasteboard.general.clearContents() 394 | NSPasteboard.general.setString(preferencesURL.path, forType: .string) 395 | } 396 | else { 397 | NSWorkspace.shared.open(preferencesURL) 398 | } 399 | } 400 | } 401 | 402 | @IBAction 403 | func openLocalFiles(_ sender: Any) { 404 | debugLog("sender = \(sender)") 405 | 406 | let filesApplicationIdentifier = "com.apple.DocumentsApp" 407 | if let applicationIndex = applications.firstIndex(where: { applicationInfo in 408 | applicationInfo.uniqueIdentifier == filesApplicationIdentifier 409 | }) { 410 | let filesApplication = applications[applicationIndex] 411 | let localStorageGroupContainerIdentifier = "group.com.apple.FileProvider.LocalStorage" 412 | if let groupContainerIndex = filesApplication.groupContainers.firstIndex(where: { groupContainerInfo in 413 | groupContainerInfo.uniqueIdentifier == localStorageGroupContainerIdentifier 414 | }) { 415 | let groupContainer = filesApplication.groupContainers[groupContainerIndex] 416 | let localFilesURL: URL 417 | if #available(macOS 13.0, *) { 418 | localFilesURL = groupContainer.containerURL.appending(path: "File Provider Storage", directoryHint: .isDirectory) 419 | } else { 420 | localFilesURL = groupContainer.containerURL.appendingPathComponent("File Provider Storage") 421 | } 422 | if NSEvent.modifierFlags == [.option] { 423 | NSPasteboard.general.clearContents() 424 | NSPasteboard.general.setString(localFilesURL.path, forType: .string) 425 | } 426 | else { 427 | NSWorkspace.shared.open(localFilesURL) 428 | } 429 | } 430 | } 431 | } 432 | 433 | @IBAction 434 | func openGroupContainer(_ sender: Any) { 435 | debugLog("sender = \(sender)") 436 | if haveApplications && haveGroupContainers { 437 | if NSEvent.modifierFlags == [.option] { 438 | NSPasteboard.general.clearContents() 439 | NSPasteboard.general.setString(selectedGroupContainer.containerURL.path, forType: .string) 440 | } 441 | else { 442 | NSWorkspace.shared.open(selectedGroupContainer.containerURL) 443 | } 444 | } 445 | } 446 | 447 | @objc 448 | func applicationDidBecomeActive(_ notification: NSNotification) { 449 | debugLog("notification = \(notification)") 450 | loadDevices() 451 | updateView() 452 | } 453 | 454 | } 455 | 456 | --------------------------------------------------------------------------------