diff --git a/Demo/Demo/ContentView.swift b/Demo/Demo/ContentView.swift index 8951a93..e2ce6df 100644 --- a/Demo/Demo/ContentView.swift +++ b/Demo/Demo/ContentView.swift @@ -6,19 +6,25 @@ // import SwiftUI +import SettingsAccess struct ContentView: View { - @Environment(\.openSettings) private var openSettings + // NOTE: As of Xcode 16, Apple has made their previously-private `openSettings` method public. + // NOTE: This only works when compiling with Xcode 16+ and only supports macOS 14+. + // @Environment(\.openSettings) private var openSettings + + // OTHERWISE: If your app needs to support macOS 13 or older, use SettingsAccess' legacy method: + @Environment(\.openSettingsLegacy) private var openSettingsLegacy var body: some View { VStack(spacing: 20) { Button("Open Settings Programmatically") { - do { try openSettings() } catch { print(error) } + do { try openSettingsLegacy() } catch { print(error) } } if #available(macOS 14, *) { SettingsLink { - Text("Open Settings Using SettingsLink") + Text("Open Settings Using Native SettingsLink") } } diff --git a/Demo/Demo/DemoApp.swift b/Demo/Demo/DemoApp.swift index c8dbece..1e176f3 100644 --- a/Demo/Demo/DemoApp.swift +++ b/Demo/Demo/DemoApp.swift @@ -38,7 +38,11 @@ struct MenuBarExtraMenuView: View { Text("Settings...") } preAction: { // code to run before Settings opens - NSApp.activate(ignoringOtherApps: true) // this does nothing if running from Xcode, but running as a standalone app it will work + + // this does nothing if running from Xcode, but running as a standalone app it will work. + // the reason we want to activate the app is that the native SettingsLink does not bring + // the Settings window to the front in a dockless menubar app. + NSApp.activate(ignoringOtherApps: true) } postAction: { // code to run after Settings opens } diff --git a/README.md b/README.md index 857d456..3738487 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ As of macOS 14 Sonoma: ## Solution -- **SettingsAccess** provides a SwiftUI environment method called `openSettings()` that can be called anywhere in the view hierarchy to programmatically open the `Settings` scene. +- **SettingsAccess** provides a SwiftUI environment method called `openSettingsLegacy()` that can be called anywhere in the view hierarchy to programmatically open the `Settings` scene. - **SettingsAccess** also provides an initializer for `SettingsLink` which provides two closures allowing execution of arbitrary code before and/or after opening the `Settings` scene. - The library is backwards compatible with macOS 11 Big Sur and later. - No private API is used, so it is safe for the Mac App Store. @@ -30,7 +30,7 @@ See [Getting Started](#Getting-Started) below for example usage. ## Limitations - **SettingsAccess** will only work **within a SwiftUI context**. Which means it requires at least one SwiftUI view and for that view to be instanced and its body invoked. Which means it cannot simply be used globally. This is 100% an Apple-imposed limitation. -- Due to SwiftUI limitations, `openSettings()` is not usable within a `menu`-based `MenuBarExtra`. In that context, the custom `SettingsLink` initializer may be used to run code before/after opening the `Settings` scene. +- Due to SwiftUI limitations, `openSettingsLegacy()` is not usable within a `menu`-based `MenuBarExtra`. In that context, the custom `SettingsLink` initializer may be used to run code before/after opening the `Settings` scene. ## Using the Package @@ -60,7 +60,7 @@ import SettingsAccess ### 1. Open Settings Programmatically -- Attach the `openSettingsAccess` view modifier to the base view whose subviews needs access to the `openSettings` method. +- Attach the `openSettingsAccess` view modifier to the base view whose subviews needs access to the `openSettingsLegacy` method. ```swift @main @@ -80,17 +80,17 @@ import SettingsAccess ```swift struct ContentView: View { - @Environment(\.openSettings) private var openSettings + @Environment(\.openSettingsLegacy) private var openSettingsLegacy var body: some View { - Button("Open Settings") { try? openSettings() } + Button("Open Settings") { try? openSettingsLegacy() } } } ``` ### 2. Use in a MenuBarExtra Menu -If using a menu-based `MenuBarExtra`, do not apply `openSettingsAccess()` to the menu content. `openSettings()` cannot be used there due to limitations of SwiftUI. +If using a menu-based `MenuBarExtra`, do not apply `openSettingsAccess()` to the menu content. `openSettingsLegacy()` cannot be used there due to limitations of SwiftUI. Instead, use the custom `SettingsLink` initializer to add a Settings menu item capable of running code before and/or after opening the `Settings` scene. diff --git a/Sources/SettingsAccess/Environment.swift b/Sources/SettingsAccess/Environment.swift index 89b862c..ecf00c7 100644 --- a/Sources/SettingsAccess/Environment.swift +++ b/Sources/SettingsAccess/Environment.swift @@ -23,7 +23,8 @@ private struct OpenSettingsKey: EnvironmentKey { @available(watchOS, unavailable) extension EnvironmentValues { /// Opens the app's Settings scene. Call this as a method. - public var openSettings: OpenSettingsAccessAction { + @_disfavoredOverload + public var openSettingsLegacy: OpenSettingsAccessAction { get { self[OpenSettingsKey.self] } set { /* self[OpenSettingsKey.self] = newValue */ } // only allow use of the original default instance } diff --git a/Sources/SettingsAccess/OpenSettingsAccessAction.swift b/Sources/SettingsAccess/OpenSettingsAccessAction.swift index 9563865..24eb4e4 100644 --- a/Sources/SettingsAccess/OpenSettingsAccessAction.swift +++ b/Sources/SettingsAccess/OpenSettingsAccessAction.swift @@ -17,9 +17,9 @@ import SwiftUI /// Then call the instance to open the Settings scene. /// You call the instance directly because it defines a ``callAsFunction()`` method that Swift calls when you call the instance. /// -/// In order for `openSettings` to operate, the `openSettingsAccess` view modifier must be applied to an ancestor of the view hierarchy. +/// In order for `openSettingsLegacy` to operate, the `openSettingsAccess` view modifier must be applied to an ancestor of the view hierarchy. /// -/// Attach the `openSettingsAccess` view modifier to the base view whose subviews needs access to the `openSettings` method. +/// Attach the `openSettingsAccess` view modifier to the base view whose subviews needs access to the `openSettingsLegacy` method. /// /// ```swift /// @main @@ -40,15 +40,15 @@ import SwiftUI /// /// ```swift /// struct ContentView: View { -/// @Environment(\.openSettings) private var openSettings +/// @Environment(\.openSettingsLegacy) private var openSettingsLegacy /// /// var body: some View { -/// Button("Open Settings") { try? openSettings() } +/// Button("Open Settings") { try? openSettingsLegacy() } /// } /// } /// ``` public class OpenSettingsAccessAction: ObservableObject { - // Closure to run when `openSettings()` is called. + // Closure to run when `openSettingsLegacy()` is called. // Default to legacy Settings/Preferences window call. // This closure will be replaced with the new SettingsLink trigger later. private var closure: (() throws -> Void)? @@ -90,7 +90,7 @@ public class OpenSettingsAccessAction: ObservableObject { /// /// var body: some View { /// Button("Open Settings") { - /// try? openSettings() // Implicitly calls openSettings.callAsFunction() + /// try? openSettingsLegacy() // Implicitly calls openSettingsLegacy.callAsFunction() /// } /// } /// } diff --git a/Sources/SettingsAccess/OpenSettingsError.swift b/Sources/SettingsAccess/OpenSettingsError.swift index 31da8b7..15972be 100644 --- a/Sources/SettingsAccess/OpenSettingsError.swift +++ b/Sources/SettingsAccess/OpenSettingsError.swift @@ -8,7 +8,7 @@ import Foundation -/// Error cases thrown by the `openSettings()` environment method. +/// Error cases thrown by the `openSettingsLegacy()` environment method. public enum OpenSettingsError: Error { /// Current version of macOS is not considered a legacy platform. case unsupportedPlatform @@ -18,14 +18,14 @@ public enum OpenSettingsError: Error { /// The internal `SettingsLink` was not found or was not set up. /// - /// SettingsAccess internally uses a hidden `SettingsLink` that is added to a view hierarchy at the point the `openSettingsView()` view modifier is used. + /// SettingsAccess internally uses a hidden `SettingsLink` that is added to a view hierarchy at the point the `openSettingsAccess()` view modifier is used. /// However, SwiftUI has not set up or is not found. /// /// This will occur under one of two conditions: - /// - The `openSettingsView()` view modifier is missing. It was attached to the wrong view hierarchy, or is not attached to an ancestor of the view that is attempting to call `openSettings()`. - /// - A call to `openSettings()` was made before SwiftUI could construct the view that has `openSettingsView()` attached. A common case where this happens is when calling `openSettings()` asynchronously. + /// - The `openSettingsAccess()` view modifier is missing. It was attached to the wrong view hierarchy, or is not attached to an ancestor of the view that is attempting to call `openSettingsLegacy()`. + /// - A call to `openSettingsLegacy()` was made before SwiftUI could construct the view that has `openSettingsAccess()` attached. A common case where this happens is when calling `openSettingsLegacy()` asynchronously. /// - /// Also keep in mind that `openSettings()` is not usable within a menu-based MenuBarExtra menu. `openSettingsView()` should not be attached to the menu content. Instead, see ``SettingsLink(label:preAction:postAction:)``. + /// Also keep in mind that `openSettingsLegacy()` is not usable within a menu-based MenuBarExtra menu. `openSettingsAccess()` should not be attached to the menu content. Instead, see ``SettingsLink(label:preAction:postAction:)``. case settingsLinkNotConnected } diff --git a/Sources/SettingsAccess/OpenSettingsInjectionView.swift b/Sources/SettingsAccess/OpenSettingsInjectionView.swift index 59c4c78..eae6f05 100644 --- a/Sources/SettingsAccess/OpenSettingsInjectionView.swift +++ b/Sources/SettingsAccess/OpenSettingsInjectionView.swift @@ -14,8 +14,8 @@ import SwiftUI @available(tvOS, unavailable) @available(watchOS, unavailable) extension View { - /// Wires up the `openSettings` environment method for a view hierarchy that allows opening the - /// app's Settings scene. + /// Wires up the `openSettingsLegacy` environment method for a view hierarchy that allows + /// opening the app's Settings scene. /// /// Example usage: /// @@ -30,10 +30,10 @@ extension View { /// } /// /// struct ContentView: View { - /// @Environment(\.openSettings) var openSettings + /// @Environment(\.openSettingsLegacy) var openSettingsLegacy /// /// var body: some View { - /// Button("Open Settings") { try? openSettings() } + /// Button("Open Settings") { try? openSettingsLegacy() } /// } /// } /// ``` @@ -47,7 +47,7 @@ extension View { @available(tvOS, unavailable) @available(watchOS, unavailable) internal struct OpenSettingsInjectionView: View { - @Environment(\.openSettings) private var openSettings + @Environment(\.openSettingsLegacy) private var openSettingsLegacy let content: Content @@ -60,7 +60,7 @@ internal struct OpenSettingsInjectionView: View { SettingsLink { Rectangle().fill(.clear) } - .prePostActionsButtonStyle(performAction: openSettings.closureBinding) + .prePostActionsButtonStyle(performAction: openSettingsLegacy.closureBinding) .frame(width: 0, height: 0) .opacity(0) } @@ -68,7 +68,7 @@ internal struct OpenSettingsInjectionView: View { content } } - .environment(\.openSettings, openSettings) + .environment(\.openSettingsLegacy, openSettingsLegacy) } }