├── HandleWindow.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── HandleWindow ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── HandleWindow.entitlements ├── HandleWindowApp.swift ├── Info.plist ├── ManagedWindow.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SceneConfiguration.swift ├── SettingsView.swift ├── UserDefaults-removeSettings.swift ├── WindowAccessor.swift ├── WindowManager.swift ├── WindowMonitor.swift ├── WindowState.swift └── WindowTracker.swift └── README.md /HandleWindow.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1717AA842874395200B83820 /* UserDefaults-removeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717AA832874395200B83820 /* UserDefaults-removeSettings.swift */; }; 11 | 171DB94A2860CCF000351CE3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171DB9492860CCF000351CE3 /* SettingsView.swift */; }; 12 | 17341734285E1C6C008E0D72 /* HandleWindowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17341733285E1C6C008E0D72 /* HandleWindowApp.swift */; }; 13 | 17341736285E1C6C008E0D72 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17341735285E1C6C008E0D72 /* ContentView.swift */; }; 14 | 17341738285E1C6C008E0D72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17341737285E1C6C008E0D72 /* Assets.xcassets */; }; 15 | 1734173B285E1C6C008E0D72 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1734173A285E1C6C008E0D72 /* Preview Assets.xcassets */; }; 16 | 17341743285E1C94008E0D72 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17341742285E1C94008E0D72 /* WindowAccessor.swift */; }; 17 | 17488A96290AE79200EDEFC2 /* SceneConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17488A95290AE79200EDEFC2 /* SceneConfiguration.swift */; }; 18 | 175070D6286C8127006DB200 /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175070D5286C8127006DB200 /* WindowManager.swift */; }; 19 | 175070D8286C8153006DB200 /* ManagedWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175070D7286C8153006DB200 /* ManagedWindow.swift */; }; 20 | 1766607A2902B12500A10D17 /* WindowMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176660792902B11800A10D17 /* WindowMonitor.swift */; }; 21 | 17FFE4BE2902980D00388AC5 /* WindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FFE4BD2902980D00388AC5 /* WindowState.swift */; }; 22 | 17FFE4C02902984000388AC5 /* WindowTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FFE4BF2902984000388AC5 /* WindowTracker.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 1717AA832874395200B83820 /* UserDefaults-removeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults-removeSettings.swift"; sourceTree = ""; }; 27 | 171DB9482860C9FD00351CE3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 28 | 171DB9492860CCF000351CE3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 29 | 171DB96628621E2200351CE3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 30 | 17341730285E1C6C008E0D72 /* HandleWindow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HandleWindow.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 17341733285E1C6C008E0D72 /* HandleWindowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleWindowApp.swift; sourceTree = ""; }; 32 | 17341735285E1C6C008E0D72 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 33 | 17341737285E1C6C008E0D72 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | 1734173A285E1C6C008E0D72 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 35 | 1734173C285E1C6C008E0D72 /* HandleWindow.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HandleWindow.entitlements; sourceTree = ""; }; 36 | 17341742285E1C94008E0D72 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; 37 | 17488A95290AE79200EDEFC2 /* SceneConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneConfiguration.swift; sourceTree = ""; }; 38 | 175070D5286C8127006DB200 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 39 | 175070D7286C8153006DB200 /* ManagedWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedWindow.swift; sourceTree = ""; }; 40 | 176660792902B11800A10D17 /* WindowMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowMonitor.swift; sourceTree = ""; }; 41 | 17FFE4BD2902980D00388AC5 /* WindowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowState.swift; sourceTree = ""; }; 42 | 17FFE4BF2902984000388AC5 /* WindowTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTracker.swift; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 1734172D285E1C6C008E0D72 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 17341727285E1C6C008E0D72 = { 57 | isa = PBXGroup; 58 | children = ( 59 | 171DB96628621E2200351CE3 /* README.md */, 60 | 17341732285E1C6C008E0D72 /* HandleWindow */, 61 | 17341731285E1C6C008E0D72 /* Products */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 17341731285E1C6C008E0D72 /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 17341730285E1C6C008E0D72 /* HandleWindow.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 17341732285E1C6C008E0D72 /* HandleWindow */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 17341733285E1C6C008E0D72 /* HandleWindowApp.swift */, 77 | 17341735285E1C6C008E0D72 /* ContentView.swift */, 78 | 175070D5286C8127006DB200 /* WindowManager.swift */, 79 | 175070D7286C8153006DB200 /* ManagedWindow.swift */, 80 | 17488A95290AE79200EDEFC2 /* SceneConfiguration.swift */, 81 | 171DB9492860CCF000351CE3 /* SettingsView.swift */, 82 | 1717AA832874395200B83820 /* UserDefaults-removeSettings.swift */, 83 | 17341742285E1C94008E0D72 /* WindowAccessor.swift */, 84 | 176660792902B11800A10D17 /* WindowMonitor.swift */, 85 | 17FFE4BD2902980D00388AC5 /* WindowState.swift */, 86 | 17FFE4BF2902984000388AC5 /* WindowTracker.swift */, 87 | 17341737285E1C6C008E0D72 /* Assets.xcassets */, 88 | 1734173C285E1C6C008E0D72 /* HandleWindow.entitlements */, 89 | 171DB9482860C9FD00351CE3 /* Info.plist */, 90 | 17341739285E1C6C008E0D72 /* Preview Content */, 91 | ); 92 | path = HandleWindow; 93 | sourceTree = ""; 94 | }; 95 | 17341739285E1C6C008E0D72 /* Preview Content */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 1734173A285E1C6C008E0D72 /* Preview Assets.xcassets */, 99 | ); 100 | path = "Preview Content"; 101 | sourceTree = ""; 102 | }; 103 | /* End PBXGroup section */ 104 | 105 | /* Begin PBXNativeTarget section */ 106 | 1734172F285E1C6C008E0D72 /* HandleWindow */ = { 107 | isa = PBXNativeTarget; 108 | buildConfigurationList = 1734173F285E1C6C008E0D72 /* Build configuration list for PBXNativeTarget "HandleWindow" */; 109 | buildPhases = ( 110 | 1734172C285E1C6C008E0D72 /* Sources */, 111 | 1734172D285E1C6C008E0D72 /* Frameworks */, 112 | 1734172E285E1C6C008E0D72 /* Resources */, 113 | ); 114 | buildRules = ( 115 | ); 116 | dependencies = ( 117 | ); 118 | name = HandleWindow; 119 | productName = HandleWindow; 120 | productReference = 17341730285E1C6C008E0D72 /* HandleWindow.app */; 121 | productType = "com.apple.product-type.application"; 122 | }; 123 | /* End PBXNativeTarget section */ 124 | 125 | /* Begin PBXProject section */ 126 | 17341728285E1C6C008E0D72 /* Project object */ = { 127 | isa = PBXProject; 128 | attributes = { 129 | BuildIndependentTargetsInParallel = 1; 130 | LastSwiftUpdateCheck = 1400; 131 | LastUpgradeCheck = 1400; 132 | TargetAttributes = { 133 | 1734172F285E1C6C008E0D72 = { 134 | CreatedOnToolsVersion = 14.0; 135 | }; 136 | }; 137 | }; 138 | buildConfigurationList = 1734172B285E1C6C008E0D72 /* Build configuration list for PBXProject "HandleWindow" */; 139 | compatibilityVersion = "Xcode 14.0"; 140 | developmentRegion = en; 141 | hasScannedForEncodings = 0; 142 | knownRegions = ( 143 | en, 144 | Base, 145 | ); 146 | mainGroup = 17341727285E1C6C008E0D72; 147 | productRefGroup = 17341731285E1C6C008E0D72 /* Products */; 148 | projectDirPath = ""; 149 | projectRoot = ""; 150 | targets = ( 151 | 1734172F285E1C6C008E0D72 /* HandleWindow */, 152 | ); 153 | }; 154 | /* End PBXProject section */ 155 | 156 | /* Begin PBXResourcesBuildPhase section */ 157 | 1734172E285E1C6C008E0D72 /* Resources */ = { 158 | isa = PBXResourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 1734173B285E1C6C008E0D72 /* Preview Assets.xcassets in Resources */, 162 | 17341738285E1C6C008E0D72 /* Assets.xcassets in Resources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXSourcesBuildPhase section */ 169 | 1734172C285E1C6C008E0D72 /* Sources */ = { 170 | isa = PBXSourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | 17FFE4C02902984000388AC5 /* WindowTracker.swift in Sources */, 174 | 17341743285E1C94008E0D72 /* WindowAccessor.swift in Sources */, 175 | 171DB94A2860CCF000351CE3 /* SettingsView.swift in Sources */, 176 | 17341736285E1C6C008E0D72 /* ContentView.swift in Sources */, 177 | 17488A96290AE79200EDEFC2 /* SceneConfiguration.swift in Sources */, 178 | 17341734285E1C6C008E0D72 /* HandleWindowApp.swift in Sources */, 179 | 1717AA842874395200B83820 /* UserDefaults-removeSettings.swift in Sources */, 180 | 1766607A2902B12500A10D17 /* WindowMonitor.swift in Sources */, 181 | 17FFE4BE2902980D00388AC5 /* WindowState.swift in Sources */, 182 | 175070D6286C8127006DB200 /* WindowManager.swift in Sources */, 183 | 175070D8286C8153006DB200 /* ManagedWindow.swift in Sources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXSourcesBuildPhase section */ 188 | 189 | /* Begin XCBuildConfiguration section */ 190 | 1734173D285E1C6C008E0D72 /* Debug */ = { 191 | isa = XCBuildConfiguration; 192 | buildSettings = { 193 | ALWAYS_SEARCH_USER_PATHS = NO; 194 | CLANG_ANALYZER_NONNULL = YES; 195 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 196 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 197 | CLANG_ENABLE_MODULES = YES; 198 | CLANG_ENABLE_OBJC_ARC = YES; 199 | CLANG_ENABLE_OBJC_WEAK = YES; 200 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 201 | CLANG_WARN_BOOL_CONVERSION = YES; 202 | CLANG_WARN_COMMA = YES; 203 | CLANG_WARN_CONSTANT_CONVERSION = YES; 204 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 205 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 206 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 207 | CLANG_WARN_EMPTY_BODY = YES; 208 | CLANG_WARN_ENUM_CONVERSION = YES; 209 | CLANG_WARN_INFINITE_RECURSION = YES; 210 | CLANG_WARN_INT_CONVERSION = YES; 211 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 212 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 213 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 214 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 215 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 216 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 217 | CLANG_WARN_STRICT_PROTOTYPES = YES; 218 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 219 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 220 | CLANG_WARN_UNREACHABLE_CODE = YES; 221 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 222 | COPY_PHASE_STRIP = NO; 223 | DEBUG_INFORMATION_FORMAT = dwarf; 224 | ENABLE_STRICT_OBJC_MSGSEND = YES; 225 | ENABLE_TESTABILITY = YES; 226 | GCC_C_LANGUAGE_STANDARD = gnu11; 227 | GCC_DYNAMIC_NO_PIC = NO; 228 | GCC_NO_COMMON_BLOCKS = YES; 229 | GCC_OPTIMIZATION_LEVEL = 0; 230 | GCC_PREPROCESSOR_DEFINITIONS = ( 231 | "DEBUG=1", 232 | "$(inherited)", 233 | ); 234 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 235 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 236 | GCC_WARN_UNDECLARED_SELECTOR = YES; 237 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 238 | GCC_WARN_UNUSED_FUNCTION = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | ONLY_ACTIVE_ARCH = YES; 241 | SDKROOT = macosx; 242 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 243 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 244 | }; 245 | name = Debug; 246 | }; 247 | 1734173E285E1C6C008E0D72 /* Release */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ALWAYS_SEARCH_USER_PATHS = NO; 251 | CLANG_ANALYZER_NONNULL = YES; 252 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 253 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 254 | CLANG_ENABLE_MODULES = YES; 255 | CLANG_ENABLE_OBJC_ARC = YES; 256 | CLANG_ENABLE_OBJC_WEAK = YES; 257 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 258 | CLANG_WARN_BOOL_CONVERSION = YES; 259 | CLANG_WARN_COMMA = YES; 260 | CLANG_WARN_CONSTANT_CONVERSION = YES; 261 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 263 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 264 | CLANG_WARN_EMPTY_BODY = YES; 265 | CLANG_WARN_ENUM_CONVERSION = YES; 266 | CLANG_WARN_INFINITE_RECURSION = YES; 267 | CLANG_WARN_INT_CONVERSION = YES; 268 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 269 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 270 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 271 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 272 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 273 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 274 | CLANG_WARN_STRICT_PROTOTYPES = YES; 275 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 276 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 277 | CLANG_WARN_UNREACHABLE_CODE = YES; 278 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 279 | COPY_PHASE_STRIP = NO; 280 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 281 | ENABLE_NS_ASSERTIONS = NO; 282 | ENABLE_STRICT_OBJC_MSGSEND = YES; 283 | GCC_C_LANGUAGE_STANDARD = gnu11; 284 | GCC_NO_COMMON_BLOCKS = YES; 285 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 286 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 287 | GCC_WARN_UNDECLARED_SELECTOR = YES; 288 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 289 | GCC_WARN_UNUSED_FUNCTION = YES; 290 | GCC_WARN_UNUSED_VARIABLE = YES; 291 | SDKROOT = macosx; 292 | SWIFT_COMPILATION_MODE = wholemodule; 293 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 294 | }; 295 | name = Release; 296 | }; 297 | 17341740285E1C6C008E0D72 /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 301 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 302 | CODE_SIGN_ENTITLEMENTS = HandleWindow/HandleWindow.entitlements; 303 | CODE_SIGN_STYLE = Automatic; 304 | COMBINE_HIDPI_IMAGES = YES; 305 | CURRENT_PROJECT_VERSION = 1; 306 | DEVELOPMENT_ASSET_PATHS = "\"HandleWindow/Preview Content\""; 307 | DEVELOPMENT_TEAM = P8CC95REUG; 308 | ENABLE_HARDENED_RUNTIME = YES; 309 | ENABLE_PREVIEWS = YES; 310 | GENERATE_INFOPLIST_FILE = YES; 311 | INFOPLIST_FILE = HandleWindow/Info.plist; 312 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 313 | LD_RUNPATH_SEARCH_PATHS = ( 314 | "$(inherited)", 315 | "@executable_path/../Frameworks", 316 | ); 317 | MARKETING_VERSION = 1.0; 318 | PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.HandleWindow; 319 | PRODUCT_NAME = "$(TARGET_NAME)"; 320 | SWIFT_EMIT_LOC_STRINGS = YES; 321 | SWIFT_VERSION = 5.0; 322 | }; 323 | name = Debug; 324 | }; 325 | 17341741285E1C6C008E0D72 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 329 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 330 | CODE_SIGN_ENTITLEMENTS = HandleWindow/HandleWindow.entitlements; 331 | CODE_SIGN_STYLE = Automatic; 332 | COMBINE_HIDPI_IMAGES = YES; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_ASSET_PATHS = "\"HandleWindow/Preview Content\""; 335 | DEVELOPMENT_TEAM = P8CC95REUG; 336 | ENABLE_HARDENED_RUNTIME = YES; 337 | ENABLE_PREVIEWS = YES; 338 | GENERATE_INFOPLIST_FILE = YES; 339 | INFOPLIST_FILE = HandleWindow/Info.plist; 340 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 341 | LD_RUNPATH_SEARCH_PATHS = ( 342 | "$(inherited)", 343 | "@executable_path/../Frameworks", 344 | ); 345 | MARKETING_VERSION = 1.0; 346 | PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.HandleWindow; 347 | PRODUCT_NAME = "$(TARGET_NAME)"; 348 | SWIFT_EMIT_LOC_STRINGS = YES; 349 | SWIFT_VERSION = 5.0; 350 | }; 351 | name = Release; 352 | }; 353 | /* End XCBuildConfiguration section */ 354 | 355 | /* Begin XCConfigurationList section */ 356 | 1734172B285E1C6C008E0D72 /* Build configuration list for PBXProject "HandleWindow" */ = { 357 | isa = XCConfigurationList; 358 | buildConfigurations = ( 359 | 1734173D285E1C6C008E0D72 /* Debug */, 360 | 1734173E285E1C6C008E0D72 /* Release */, 361 | ); 362 | defaultConfigurationIsVisible = 0; 363 | defaultConfigurationName = Release; 364 | }; 365 | 1734173F285E1C6C008E0D72 /* Build configuration list for PBXNativeTarget "HandleWindow" */ = { 366 | isa = XCConfigurationList; 367 | buildConfigurations = ( 368 | 17341740285E1C6C008E0D72 /* Debug */, 369 | 17341741285E1C6C008E0D72 /* Release */, 370 | ); 371 | defaultConfigurationIsVisible = 0; 372 | defaultConfigurationName = Release; 373 | }; 374 | /* End XCConfigurationList section */ 375 | }; 376 | rootObject = 17341728285E1C6C008E0D72 /* Project object */; 377 | } 378 | -------------------------------------------------------------------------------- /HandleWindow.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HandleWindow.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /HandleWindow/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 | -------------------------------------------------------------------------------- /HandleWindow/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /HandleWindow/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HandleWindow/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 18.06.22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct ContentView: View { 12 | @Environment(\.sceneID) private var sceneID 13 | @Environment(\.window) private var window 14 | @Environment(\.openURL) private var openURL 15 | 16 | @State private var hasChanges = false 17 | 18 | var body: some View { 19 | let _ = print("\(Self.self): body executed for ", window.windowGroupID, window.windowGroupInstance) 20 | let _ = Self._printChanges() 21 | VStack(spacing: 20) { 22 | Text("it finally works!") 23 | .font(.largeTitle) 24 | 25 | if let window { 26 | VStack { 27 | HStack { 28 | Text("Scene ID:") 29 | .bold() 30 | .frame(maxWidth: .infinity, alignment: .trailing) 31 | Text(sceneID) 32 | .fixedSize() 33 | .frame(maxWidth: .infinity, alignment: .leading) 34 | } 35 | HStack { 36 | Text("Window Identifier:") 37 | .bold() 38 | .frame(maxWidth: .infinity, alignment: .trailing) 39 | Text(window.windowIdentifier) 40 | .fixedSize() 41 | .frame(maxWidth: .infinity, alignment: .leading) 42 | } 43 | HStack { 44 | Text("Window Group ID:") 45 | .bold() 46 | .frame(maxWidth: .infinity, alignment: .trailing) 47 | Text(window.windowGroupID) 48 | .fixedSize() 49 | .frame(maxWidth: .infinity, alignment: .leading) 50 | } 51 | HStack { 52 | Text("Window Instance:") 53 | .bold() 54 | .frame(maxWidth: .infinity, alignment: .trailing) 55 | Text(window.windowGroupInstance, format: .number) 56 | .fixedSize() 57 | .frame(maxWidth: .infinity, alignment: .leading) 58 | } 59 | HStack { 60 | Text("frameAutosaveName:") 61 | .bold() 62 | .frame(maxWidth: .infinity, alignment: .trailing) 63 | Text(window.underlyingWindow.frameAutosaveName) 64 | .fixedSize() 65 | .frame(maxWidth: .infinity, alignment: .leading) 66 | } 67 | } 68 | 69 | HStack { 70 | Toggle("Has changes", isOn: $hasChanges) 71 | Button("Try close") { 72 | window.close() 73 | } 74 | 75 | Button("Dump window state") { 76 | dump(window) 77 | } 78 | } 79 | 80 | HStack { 81 | Button("Main") { 82 | openURL(URL(string: "handleWindow://main")!) 83 | } 84 | .disabled(sceneID == "main") 85 | 86 | Button("Secondary") { 87 | openURL(URL(string: "handleWindow://secondary")!) 88 | } 89 | .disabled(sceneID == "secondary") 90 | 91 | Button("Tertiary") { 92 | openURL(URL(string: "handleWindow://tertiary")!) 93 | } 94 | .disabled(sceneID == "tertiary") 95 | } 96 | } 97 | } 98 | .onChange(of: window.isVisible, perform: { isVisible in 99 | if isVisible { 100 | window.registerShouldClose(callback: { 101 | hasChanges == false 102 | }) 103 | } 104 | }) 105 | .frame(minWidth: 300) 106 | .fixedSize(horizontal: true, vertical: false) 107 | .padding(20) 108 | .frame(maxWidth: window.screenSize?.width, maxHeight: window.screenSize?.height) 109 | } 110 | } 111 | 112 | struct ContentView_Previews: PreviewProvider { 113 | static var previews: some View { 114 | ContentView() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /HandleWindow/HandleWindow.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 | -------------------------------------------------------------------------------- /HandleWindow/HandleWindowApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandleWindowApp.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 18.06.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct HandleWindowApp: App { 12 | @Environment(\.windowManager) var windowManager 13 | 14 | var body: some Scene { 15 | ManagedWindow("Main", id: "main") { 16 | ContentView() 17 | } 18 | .defaultPosition(.topLeading) 19 | 20 | ManagedWindowGroup("Secondary", id: "secondary") { 21 | ContentView() 22 | } 23 | .defaultPosition(.center) 24 | .defaultSize(CGSize(width: 400, height: 200)) 25 | 26 | ManagedWindow(id: "tertiary") { 27 | ContentView() 28 | } 29 | .defaultPosition(.topTrailing) 30 | .defaultSize(CGSize(width: 600, height: 600)) 31 | 32 | Settings { 33 | SettingsView() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /HandleWindow/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | handleWindow 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /HandleWindow/ManagedWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedWindow.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 29.06.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | private struct SceneIDEnvironmentKey: EnvironmentKey { 12 | static var defaultValue: SceneID = "" 13 | } 14 | 15 | extension EnvironmentValues { 16 | var sceneID: SceneID { 17 | get { self[SceneIDEnvironmentKey.self] } 18 | set { self[SceneIDEnvironmentKey.self] = newValue } 19 | } 20 | } 21 | 22 | struct ManagedWindowGroup: Scene { 23 | 24 | @Environment(\.windowManager) private var windowManager 25 | 26 | private let title: String? 27 | fileprivate let id: SceneID 28 | fileprivate let content: Content 29 | fileprivate let isSingleWindow: Bool 30 | 31 | public init(_ title: String, id: String, @ViewBuilder content: () -> Content) { 32 | self.init(title: title, id: id, isSingleWindow: false, content: content) 33 | } 34 | 35 | public init(_ title: String, @ViewBuilder content: () -> Content) { 36 | self.init(title: title, id: nil, isSingleWindow: false, content: content) 37 | } 38 | 39 | public init(id: String, @ViewBuilder content: () -> Content) { 40 | self.init(title: nil, id: id, isSingleWindow: false, content: content) 41 | } 42 | 43 | public init(@ViewBuilder content: () -> Content) { 44 | self.init(title: nil, id: nil, isSingleWindow: false, content: content) 45 | } 46 | 47 | fileprivate init(title: String?, id: String?, isSingleWindow: Bool, @ViewBuilder content: () -> Content) { 48 | self.title = title 49 | self.isSingleWindow = isSingleWindow 50 | self.content = content() 51 | self.id = SceneConfiguration.register(id: id ?? String(describing: Content.self), title: title, contentType: Content.self, isSingleWindow: isSingleWindow) 52 | } 53 | 54 | private func wrappedContent() -> some View { 55 | WrappedContent(sceneID: id, content: content) 56 | .trackUnderlyingWindow { windowState, isConnecting in 57 | print("onConnect", windowState.windowIdentifier, isConnecting) 58 | if isConnecting { 59 | windowManager.registerWindow(for: id, window: windowState.underlyingWindow) 60 | } else { 61 | windowManager.unregisterWindow(for: id, window: windowState.underlyingWindow) 62 | } 63 | } 64 | .environment(\.openURL, OpenURLAction(handler: windowManager.openURLHandler)) 65 | .environment(\.sceneID, id) 66 | } 67 | 68 | private var windowGroup: some Scene { 69 | if let title { 70 | return WindowGroup(title, id: id, content: wrappedContent) 71 | } else { 72 | return WindowGroup(id: id, content: wrappedContent) 73 | } 74 | } 75 | 76 | var body: some Scene { 77 | windowGroup 78 | .handlesExternalEvents(matching: Set([id])) 79 | .commands { 80 | windowManager.commands() 81 | } 82 | } 83 | 84 | private struct WrappedContent: View { 85 | @Environment(\.window) private var windowState 86 | @Environment(\.windowManager) private var windowManager 87 | 88 | let sceneID: String 89 | let content: Content 90 | 91 | var body: some View { 92 | content 93 | .onChange(of: windowState.isVisible) { isVisible in 94 | if isVisible { 95 | windowManager.setInitialFrame(to: windowState.underlyingWindow, for: sceneID) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | 103 | /// `ManagedWindow` is a special variant of `ManagedWindowGroup` which tries to avoid multiple windows of the same type 104 | struct ManagedWindow: Scene { 105 | 106 | fileprivate let id: SceneID 107 | private let content: ManagedWindowGroup 108 | 109 | public init(_ title: String, id: String, @ViewBuilder content: () -> Content) { 110 | self.id = id 111 | self.content = ManagedWindowGroup(title: title, id: id, isSingleWindow: true, content: content) 112 | } 113 | 114 | public init(id: String, @ViewBuilder content: () -> Content) { 115 | self.id = id 116 | self.content = ManagedWindowGroup(title: nil, id: id, isSingleWindow: true, content: content) 117 | } 118 | 119 | public init(@ViewBuilder content: () -> Content) { 120 | let content = ManagedWindowGroup(title: nil, id: nil, isSingleWindow: true, content: content) 121 | self.id = content.id 122 | self.content = content 123 | } 124 | 125 | var body: some Scene { 126 | content 127 | } 128 | } 129 | 130 | extension ManagedWindowGroup { 131 | func defaultPosition(_ position: UnitPoint) -> Self { 132 | SceneConfiguration.update(sceneID: id) { scene in 133 | scene.defaultPosition = position 134 | } 135 | return self 136 | } 137 | 138 | func defaultSize(_ size: CGSize) -> Self { 139 | SceneConfiguration.update(sceneID: id) { scene in 140 | scene.defaultSize = size 141 | } 142 | return self 143 | } 144 | } 145 | 146 | extension ManagedWindow { 147 | func defaultPosition(_ position: UnitPoint) -> Self { 148 | SceneConfiguration.update(sceneID: id) { scene in 149 | scene.defaultPosition = position 150 | } 151 | return self 152 | } 153 | 154 | func defaultSize(_ size: CGSize) -> Self { 155 | SceneConfiguration.update(sceneID: id) { scene in 156 | scene.defaultSize = size 157 | } 158 | return self 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /HandleWindow/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /HandleWindow/SceneConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneConfiguration.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 27.10.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | typealias SceneID = String 11 | 12 | struct SceneConfiguration: Identifiable { 13 | let id: SceneID 14 | let isMain: Bool 15 | let title: String? 16 | let orderBy: Int 17 | var defaultPosition: UnitPoint? 18 | var defaultSize: CGSize? 19 | var contentType: Any.Type 20 | var isSingleWindow: Bool 21 | 22 | var sceneFrameDescriptor: String? 23 | 24 | var keyboardShortcut: KeyboardShortcut? { 25 | isMain ? KeyboardShortcut("N", modifiers: .command) : nil 26 | } 27 | 28 | var commandName: String { 29 | if let title { 30 | return "New \(title) Window" 31 | } else { 32 | return "New Window" 33 | } 34 | } 35 | } 36 | 37 | extension SceneConfiguration: Comparable { 38 | static func < (lhs: SceneConfiguration, rhs: SceneConfiguration) -> Bool { 39 | lhs.orderBy < rhs.orderBy 40 | } 41 | 42 | static func == (lhs: SceneConfiguration, rhs: SceneConfiguration) -> Bool { 43 | lhs.id == rhs.id 44 | } 45 | } 46 | 47 | // MARK: - Scene configuration management 48 | 49 | extension SceneConfiguration { 50 | static var allScenes = [SceneID: SceneConfiguration]() 51 | 52 | static func register(id: SceneID, title: String?, contentType: Any.Type, isSingleWindow: Bool) -> SceneID { 53 | print("🟣 registering scene \(id) for \(contentType), \(type(of: contentType))") 54 | 55 | var id = id 56 | if allScenes[id] != nil { 57 | var counter = 0 58 | print(" duplicate scene ID \(id)") 59 | while allScenes[id] != nil { 60 | counter += 1 61 | let newID = "\(id)-\(counter)" 62 | print(" trying \(newID)...") 63 | if allScenes[newID] == nil { 64 | id = newID 65 | } 66 | } 67 | print(" using \(id)") 68 | } 69 | 70 | let sceneConfig = SceneConfiguration( 71 | id: id, 72 | isMain: allScenes.isEmpty, 73 | title: title, 74 | orderBy: allScenes.count, 75 | contentType: contentType, 76 | isSingleWindow: isSingleWindow 77 | ) 78 | allScenes[id] = sceneConfig 79 | 80 | print("🟣 registered scene \(id) as \(sceneConfig)") 81 | return sceneConfig.id 82 | } 83 | 84 | static func configuration(for sceneID: SceneID) -> SceneConfiguration? { 85 | allScenes[sceneID] 86 | } 87 | 88 | static func exists(withID id: SceneID) -> Bool { 89 | configuration(for: id) != nil 90 | } 91 | 92 | static func update(sceneID: SceneID, action: (inout SceneConfiguration) -> Void) { 93 | guard var config = allScenes[sceneID] else { 94 | fatalError("No scene for \(sceneID) found") 95 | } 96 | action(&config) 97 | allScenes[sceneID] = config 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /HandleWindow/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 20.06.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | var body: some View { 12 | Form { 13 | GroupBox { 14 | Button("Reset to defaults & quit", role: .destructive, action: resetToDefaults) 15 | .frame(maxWidth: .infinity) 16 | .padding() 17 | } 18 | .padding() 19 | } 20 | .frame(maxWidth: 400) 21 | } 22 | 23 | private func resetToDefaults() { 24 | UserDefaults.standard.removeAppSettings() 25 | NSApplication.shared.stop(nil) 26 | } 27 | } 28 | 29 | 30 | struct SettingsView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | SettingsView() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /HandleWindow/UserDefaults-removeSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults-removeSettings.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 05.07.22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension UserDefaults { 11 | func removeAppSettings() { 12 | guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } 13 | self.removePersistentDomain(forName: bundleIdentifier) 14 | self.synchronize() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /HandleWindow/WindowAccessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowAccessor.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 18.06.22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /// This view will add a `NSView` to the hierarchy and track its `window` property to 12 | /// get a handle to the `NSWindow`. 13 | /// The coordinator object is responsible for this KVO observation, triggering the relevant callbacks and updating `WindowState` 14 | struct WindowAccessor: NSViewRepresentable { 15 | let onConnect: (NSWindow, WindowMonitor) -> Void 16 | let onWillClose: () -> Void 17 | 18 | func makeNSView(context: Context) -> NSView { 19 | let view = NSView() 20 | context.coordinator.monitorView(view) 21 | return view 22 | } 23 | 24 | func updateNSView(_ view: NSView, context: Context) { 25 | } 26 | 27 | func makeCoordinator() -> WindowMonitor { 28 | WindowMonitor(onConnect, onWillClose: onWillClose) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /HandleWindow/WindowManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowManager.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 29.06.22. 6 | // 7 | 8 | import AppKit 9 | import Combine 10 | import SwiftUI 11 | 12 | /// `WindowManager` is tracking and collecting information about installed scenes (`ManagedWindow` and `ManagedWindowGroup`) and 13 | /// will automatically handle URL open request based on the IDs of the window scenes. 14 | /// It will also makes sure that single windows cannot be opened more than once. (This needs an override of the `New` command!) 15 | /// In the future it should also handle `defaultPosition()` and `defaultSize()` for windows. 16 | /// 17 | class WindowManager: ObservableObject { 18 | static let shared = WindowManager() 19 | 20 | private var scheme: String 21 | private var windows = [SceneID: [NSWindow]]() 22 | 23 | private init() { 24 | // Ugly way to extract primary App URL scheme from Info.plist 25 | guard let infoDictionary = Bundle.main.infoDictionary, 26 | let urlTypes = infoDictionary["CFBundleURLTypes"] as? [[String: Any]], 27 | let firstURLType = urlTypes.first, 28 | let urlSchemes = firstURLType["CFBundleURLSchemes"] as? [String], 29 | let primaryURLScheme = urlSchemes.first 30 | else { 31 | fatalError("No URL scheme defined") 32 | } 33 | 34 | self.scheme = primaryURLScheme 35 | } 36 | 37 | func registerWindow(for sceneID: SceneID, window: NSWindow) { 38 | guard let config = SceneConfiguration.configuration(for: sceneID) else { 39 | fatalError("No window group with ID \(sceneID)") 40 | } 41 | 42 | guard windows[sceneID, default: []].contains(window) == false else { 43 | return 44 | } 45 | 46 | // Restore frame descriptor from UserDefaults 47 | if let restorableFrameDescriptor = sceneFrameFromUserDefaults(sceneID), 48 | config.sceneFrameDescriptor == nil { 49 | SceneConfiguration.update(sceneID: sceneID) { scene in 50 | scene.sceneFrameDescriptor = restorableFrameDescriptor 51 | } 52 | } 53 | 54 | print("🟣 registered new window for \(sceneID)") 55 | windows[sceneID, default: []].append(window) 56 | } 57 | 58 | func unregisterWindow(for sceneID: SceneID, window: NSWindow) { 59 | guard SceneConfiguration.exists(withID: sceneID) else { 60 | fatalError("No window group with ID \(sceneID)") 61 | } 62 | guard let index = windows[sceneID, default: []].firstIndex(of: window) else { 63 | print("🔴 window \(window.identifier?.rawValue ?? "-") not found!") 64 | return 65 | } 66 | print("🟣 removing window \(window.identifier?.rawValue ?? "-") for \(sceneID)") 67 | windows[sceneID]?.remove(at: index) 68 | if windows[sceneID]?.count == 0 { 69 | let frameDescriptor = window.frameDescriptor 70 | print("🟣 saving position of last window to UserDefaults: \(frameDescriptor)") 71 | SceneConfiguration.update(sceneID: sceneID) { scene in 72 | scene.sceneFrameDescriptor = frameDescriptor 73 | } 74 | saveSceneFrameToUserDefaults(sceneID, frameDescriptor: frameDescriptor) 75 | } 76 | } 77 | 78 | func openWindow(id: SceneID) { 79 | print("🟣 ", #function, id) 80 | guard let scene = SceneConfiguration.configuration(for: id) else { 81 | fatalError("No WindowGroup registered with ID \(id)") 82 | } 83 | 84 | guard let host = id.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 85 | let url = URL(string: "\(scheme)://\(host)") 86 | else { 87 | fatalError("Unable to produce a valid url with \(id) and \(scheme)") 88 | } 89 | 90 | if scene.isSingleWindow, let window = windows[id]?.first { 91 | print("🟣 ", #function, "is single window") 92 | window.makeKeyAndOrderFront(nil) 93 | } else { 94 | NSWorkspace.shared.open(url) 95 | } 96 | } 97 | 98 | // This handler is used to implement the "single window" behaviour: if a request to open a new window 99 | // is received we might bring the already existing window to the front. 100 | func openURLHandler(_ url: URL) -> OpenURLAction.Result { 101 | print("🟣", #function, url) 102 | if url.scheme == scheme { 103 | if let sceneID = url.host, let scene = SceneConfiguration.configuration(for: sceneID) { 104 | if scene.isSingleWindow, let window = windows[sceneID]?.first { 105 | print("🟣 ", #function, "reopening single window") 106 | window.makeKeyAndOrderFront(nil) 107 | return .handled 108 | } 109 | } 110 | } 111 | return .systemAction 112 | } 113 | 114 | @CommandsBuilder 115 | func commands() -> some Commands { 116 | CommandGroup(replacing: .newItem) { 117 | Menu("New") { 118 | ForEach(SceneConfiguration.allScenes.values.sorted() 119 | .filter({ $0.title != nil || $0.isMain }) 120 | ) { scene in 121 | Button(LocalizedStringKey(scene.commandName), action: { [weak self] in self?.openWindow(id: scene.id) }) 122 | .keyboardShortcut(scene.keyboardShortcut) 123 | } 124 | } 125 | } 126 | } 127 | 128 | // MARK: - Handling default positioning of window 129 | 130 | private func sceneFrameAutosaveNameInUserDefaults(_ sceneID: SceneID) -> String { 131 | "NSWindow Frame \(sceneID)-AppWindow-1" 132 | } 133 | 134 | private func sceneFrameFromUserDefaults(_ sceneID: SceneID) -> String? { 135 | let key = sceneFrameAutosaveNameInUserDefaults(sceneID) 136 | let value = UserDefaults.standard.string(forKey: key) 137 | print(" Reading \(key): \(String(describing: value))") 138 | return value 139 | } 140 | 141 | private func saveSceneFrameToUserDefaults(_ sceneID: SceneID, frameDescriptor: String) { 142 | let key = sceneFrameAutosaveNameInUserDefaults(sceneID) 143 | print(" Writing \(key): \(frameDescriptor)") 144 | UserDefaults.standard.set(frameDescriptor, forKey: sceneFrameAutosaveNameInUserDefaults(sceneID)) 145 | } 146 | 147 | func setInitialFrame(to window: NSWindow, for id: SceneID) { 148 | print("🟣 ", #function, " window is currently at: \(window.frame)") 149 | 150 | // Position relative to last opened window 151 | if let lastWindow = windows[id]?.last(where: { $0 != window }) { 152 | 153 | let lastFrame = lastWindow.frame 154 | window.setFrame(lastFrame, display: false) 155 | window.setFrameTopLeftPoint( 156 | window.cascadeTopLeft(from: CGPoint(x: lastFrame.minX, 157 | y: lastFrame.maxY)) 158 | ) 159 | print(" placed new window relative to last window at", window.frame) 160 | 161 | } else { 162 | let scene = SceneConfiguration.configuration(for: id) 163 | 164 | // Place window where last window was located on closing 165 | if let savedFrameDescriptor = scene?.sceneFrameDescriptor { 166 | print(" placing at last saved position: ", savedFrameDescriptor) 167 | window.setFrame(from: savedFrameDescriptor) 168 | 169 | } else { 170 | let defaultPosition = scene?.defaultPosition 171 | let defaultSize = scene?.defaultSize 172 | 173 | if let defaultPosition { 174 | // Place at default location (if window is opened for the first time) 175 | print(" placing at UnitPoint position", defaultPosition) 176 | let frameOrigin = frameOriginForUnitPointPosition(window, position: defaultPosition, size: defaultSize) 177 | 178 | if let defaultSize { 179 | print(" sizing ", defaultSize) 180 | window.setFrame(CGRect(origin: frameOrigin, size: defaultSize), display: false) 181 | } else { 182 | window.setFrameOrigin(frameOrigin) 183 | } 184 | } else if let defaultSize { 185 | print(" sizing ", defaultSize) 186 | window.setFrame(CGRect(origin: window.frame.origin, size: defaultSize), display: false) 187 | } 188 | } 189 | } 190 | print(" window frame: \(window.frame)") 191 | } 192 | 193 | private func frameOriginForUnitPointPosition(_ window: NSWindow, position: UnitPoint, size: CGSize?) -> CGPoint { 194 | guard let screen = window.screen else { 195 | print("🔴 window is not attached to a screen") 196 | return .zero 197 | } 198 | 199 | let visibleFrame = screen.visibleFrame 200 | let windowSize = size ?? window.frame.size 201 | let screenSize = CGSize(width: visibleFrame.width-windowSize.width, height: visibleFrame.height-windowSize.height) 202 | 203 | let projectedPoint = CGPoint( 204 | x: visibleFrame.origin.x + min(max(0, position.x * screenSize.width), visibleFrame.width), 205 | y: visibleFrame.origin.y + max(0, (1-position.y) * screenSize.height) 206 | ) 207 | 208 | return projectedPoint 209 | } 210 | } 211 | 212 | private extension NSWindow { 213 | /// Returns scene ID based on window identifier (=first part) 214 | var sceneID: SceneID? { 215 | guard let parts = identifier?.rawValue.split(separator: "-"), 216 | parts.count == 3 && parts[1] == "AppWindow", 217 | let sceneIdentifier = parts.first 218 | else { 219 | return nil 220 | } 221 | return String(sceneIdentifier) 222 | } 223 | } 224 | 225 | private struct WindowManagerEnvironmentKey: EnvironmentKey { 226 | static var defaultValue = WindowManager.shared 227 | } 228 | 229 | extension EnvironmentValues { 230 | var windowManager: WindowManager { 231 | self[WindowManagerEnvironmentKey.self] 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /HandleWindow/WindowMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowMonitor.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 21.10.22. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | class WindowMonitor: NSObject { 12 | fileprivate var cancellables = Set() 13 | 14 | private let onConnect: (NSWindow, WindowMonitor) -> Void 15 | private let onWillClose: () -> Void 16 | private weak var window: NSWindow? 17 | 18 | init(_ onChange: @escaping (NSWindow, WindowMonitor) -> Void, onWillClose: @escaping () -> Void) { 19 | print("🟡 Coordinator", #function) 20 | self.onConnect = onChange 21 | self.onWillClose = onWillClose 22 | } 23 | 24 | deinit { 25 | print("🟡 Coordinator", #function) 26 | } 27 | 28 | private func dismantle() { 29 | print("🟡 Coordinator", #function) 30 | window = nil 31 | shouldCloseWindowSubscription = nil 32 | cancellables.forEach(self.remove) 33 | } 34 | 35 | func store(cancellable: AnyCancellable) { 36 | cancellables.insert(cancellable) 37 | } 38 | 39 | func remove(cancellable: AnyCancellable) { 40 | cancellable.cancel() 41 | cancellables.remove(cancellable) 42 | } 43 | 44 | /// This function uses KVO to observe the `window` property of `view` and calls `onConnect()` 45 | /// and starts observing window visibility and closing. 46 | func monitorView(_ view: NSView) { 47 | view.publisher(for: \.window, options: .new) 48 | .compactMap({ $0 }) 49 | .first() 50 | .sink { [weak self] newWindow in 51 | guard let self else { return } 52 | self.window = newWindow 53 | self.monitorClosing(of: newWindow) 54 | self.onConnect(newWindow, self) 55 | } 56 | .store(bindTo: self) 57 | } 58 | 59 | /// This function uses notifications to track closing of our views underlying `window` 60 | private func monitorClosing(of window: NSWindow) { 61 | NotificationCenter.default 62 | .publisher(for: NSWindow.willCloseNotification, object: window) 63 | .sink { [weak self] notification in 64 | guard let self = self else { return } 65 | DispatchQueue.main.async { 66 | self.onWillClose() 67 | self.dismantle() 68 | } 69 | } 70 | .store(bindTo: self) 71 | } 72 | 73 | // Handle a "should window be closed" check by using a subject 74 | private lazy var shouldCloseWindowSubject = PassthroughSubject() 75 | private var shouldCloseWindowSubscription: AnyCancellable? 76 | 77 | fileprivate func interceptCloseAction(_ handler: @escaping () -> Bool) { 78 | print(#function) 79 | guard let window else { return } 80 | if let closeButton = window.standardWindowButton(.closeButton) { 81 | closeButton.target = self 82 | closeButton.action = #selector(Self.checkAndClose(_:)) 83 | } 84 | 85 | shouldCloseWindowSubscription = shouldCloseWindowSubject 86 | .map(handler) 87 | .sink(receiveValue: { [weak window] shouldClose in 88 | if shouldClose { 89 | window?.close() 90 | } else { 91 | print("Handler rejected closing request") 92 | } 93 | }) 94 | } 95 | 96 | @objc 97 | private func checkAndClose(_ sender: Any) { 98 | print(#function) 99 | shouldCloseWindowSubject.send() 100 | } 101 | } 102 | 103 | extension AnyCancellable { 104 | func store(bindTo monitor: WindowMonitor) { 105 | store(in: &monitor.cancellables) 106 | } 107 | } 108 | 109 | extension WindowState { 110 | func registerShouldClose(callback: @escaping () -> Bool) { 111 | monitor?.interceptCloseAction(callback) 112 | } 113 | 114 | func close() { 115 | underlyingWindow.performClose(self) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /HandleWindow/WindowState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowState.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 21.10.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Storage for window related state and helpers 11 | struct WindowState { 12 | var monitor: WindowMonitor? 13 | var underlyingWindow = NSWindow() 14 | var isVisible: Bool = false 15 | 16 | var windowIdentifier: String { 17 | underlyingWindow.identifier?.rawValue ?? "" 18 | } 19 | 20 | var windowGroupID: String { 21 | windowIdentifier.split(separator: "-").first.map(String.init) ?? "" 22 | } 23 | 24 | var windowGroupInstance: Int { 25 | Int(windowIdentifier.split(separator: "-").last.map({ String($0) }) ?? "") ?? 0 26 | } 27 | 28 | var screenSize: CGSize? { 29 | underlyingWindow.screen?.frame.size 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /HandleWindow/WindowTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowTracker.swift 3 | // HandleWindow 4 | // 5 | // Created by Philipp on 21.10.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct WindowStateEnvironmentKey: EnvironmentKey { 11 | static var defaultValue = WindowState() 12 | } 13 | 14 | extension EnvironmentValues { 15 | var window: WindowState { 16 | get { self[WindowStateEnvironmentKey.self] } 17 | set { self[WindowStateEnvironmentKey.self] = newValue } 18 | } 19 | } 20 | 21 | /// This view modifier is holding and initialising `WindowState`, installs the `WindowAccessor` view in the views background to track the window and 22 | /// publishes state changes to the environment as `\.window` key. 23 | struct WindowTracker: ViewModifier { 24 | 25 | @State private var state = WindowState() 26 | 27 | let onConnect: (WindowState, Bool) -> Void 28 | 29 | func body(content: Content) -> some View { 30 | print(Self.self, #function, state) 31 | return content 32 | .background(WindowAccessor(onConnect: connectToWindow, onWillClose: windowWillClose)) 33 | .environment(\.window, state) 34 | } 35 | 36 | private func connectToWindow(_ window: NSWindow, _ monitor: WindowMonitor) { 37 | state = WindowState(monitor: monitor, underlyingWindow: window) 38 | onConnect(state, true) 39 | 40 | // Setup visibility tracking 41 | window.publisher(for: \.isVisible, options: .new) 42 | .filter({ $0 }) 43 | .first() 44 | .sink(receiveValue: { isVisible in 45 | print("updating visibility state", isVisible) 46 | state.isVisible = isVisible 47 | }) 48 | .store(bindTo: monitor) 49 | } 50 | 51 | private func windowWillClose() { 52 | onConnect(state, false) 53 | state = WindowState() 54 | } 55 | } 56 | 57 | extension View { 58 | func trackUnderlyingWindow(onConnect: @escaping (WindowState, Bool) -> Void) -> some View { 59 | return self.modifier(WindowTracker(onConnect: onConnect)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macOS SwiftUI window playground 2 | 3 | This is my personal playground where I'm learning and testing SwiftUI functionality related to multi-window support 4 | in macOS apps. 5 | 6 | So far, I've written a custom `WindowAccessor` implementation (inspired by the ones found on StackOverflow) which allows 7 | me to not only access `NSWindow` instance, but also being called-back when the view is inserted into the window and 8 | whenever the window becomes visible/invisible. This allows configuring and positioning a view on screen without flicker! 9 | 10 | The project contains currently the following major components: 11 | 12 | - `WindowAccessor` used to track the addition of a SwiftUI view to a window (in a background view). 13 | It uses the embedded `WindowMonitor` class as a coordinator and to notify callbacks when view has been added or removed 14 | from the window. The monitor automatically releases itself when `willCloseNotification` for the given window is received. 15 | - `WindowTracker` is a SwiftUI view modifier to embed the `WindowAccessor` and inject a `WindowState` environment variable 16 | which can be used by the child views to get access to the `underlyingWindow` or the `WindowMonitor` to track additional 17 | `NSWindow` attributes 18 | - `ManagedWindow` and `ManagedWindowGroup` are SwiftUI scenes (replacing `WindowGroup`) which can be used to easily create 19 | multi-window apps (without upgrading to macOS Ventura!). `ManagedWindow` is a single-window scene. It is impossible to 20 | open more than one at time. `ManagedWindowGroup` is a group of similar windows. 21 | - `WindowManager` is the manager of the new window scenes. He makes sure only one instance of a `ManagedWindow` can be 22 | opened and also ensures the last window position (per kind) is persisted. 23 | - [`HandleWindowApp.swift`](HandleWindow/HandleWindowApp.swift) is an example how to use it. 24 | 25 | There is still a lot which can be improved... 26 | 27 | --- 28 | 29 | The knowledge I gained so far: 30 | 31 | - The `id` parameter of a `WindowGroup` plays a crucial role in persisting the window position, because it will be used 32 | to derive the underlying `NSWindow.identifier`. 33 | 34 | If you do not set an ID it will be automatically derived from views type! For example: 35 | 36 | HandleWindow.ContentView-1-AppWindow-1 37 | 38 | By fixing the `id` parameter to "main", the identifier for the first window becomes "main-AppWindow-1" and 39 | "main-AppWindow-2" for the second, "main-AppWindow-3" for the third. 40 | 41 | - The `NSWindow.identifier` (derived from the `WindowGroup.id`) is used to define the windows `frameAutosaveName` 42 | (i.e. it has by default the same value). This "frame autosave name", is used to persist the windows frame in your apps 43 | standard settings (`UserDefaults.standard`). The key used is derived: 44 | 45 | NSWindow Frame -AppWindow-1 46 | 47 | I.e. in our example using "main", the window will persist the position under the key: 48 | 49 | NSWindow Frame main-AppWindow-1 50 | 51 | The value stored under this location consists of the windows frame and the screens dimension in String representation 52 | called `NSWindow.PersistableFrameDescriptor`. For example 53 | 54 | 1450 883 108 80 0 0 3008 1228 55 | 56 | The first 4 values represent the origin (x, y) and the size (width, height) of the window. The next 4 values represent 57 | the screen frame: origin and size. 58 | 59 | - SwiftUI does a poor job on remembering window positions! 60 | 61 | 1. It persists the position of a single window of an entire group. Even though you can have multiple windows open. 62 | Only one will be reopened: the one which was frontmost when the app closed. 63 | 2. After app launch, when moving the opened window, closing it and reopening it again (CMD + N), does not reopen it 64 | near the position where we closed it! 65 | 66 | It seems that SwiftUI treats each window as a unique entity and therefore gives it a unique identifier (with 67 | increasing sequence numbers at the end). Worst of all: in the standard preferences you will always see only one 68 | single entry per `WindowGroup` which corresponds to the last "topped" window. 69 | This is very annoying if you display the same `ContentView` in the same window (for example a Welcome screen). Closing 70 | and reopening it will make it jump location on the screen. 71 | --------------------------------------------------------------------------------