Skip to content

Commit

Permalink
Support for Xcode 16: Renamed openSettings to openSettingsLegacy (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
orchetect committed Jun 12, 2024
1 parent e1083e6 commit e7a0e5b
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 29 deletions.
12 changes: 9 additions & 3 deletions Demo/Demo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down
6 changes: 5 additions & 1 deletion Demo/Demo/DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion Sources/SettingsAccess/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/SettingsAccess/OpenSettingsAccessAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)?
Expand Down Expand Up @@ -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()
/// }
/// }
/// }
Expand Down
10 changes: 5 additions & 5 deletions Sources/SettingsAccess/OpenSettingsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/SettingsAccess/OpenSettingsInjectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
///
Expand All @@ -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() }
/// }
/// }
/// ```
Expand All @@ -47,7 +47,7 @@ extension View {
@available(tvOS, unavailable)
@available(watchOS, unavailable)
internal struct OpenSettingsInjectionView<Content: View>: View {
@Environment(\.openSettings) private var openSettings
@Environment(\.openSettingsLegacy) private var openSettingsLegacy

let content: Content

Expand All @@ -60,15 +60,15 @@ internal struct OpenSettingsInjectionView<Content: View>: View {
SettingsLink {
Rectangle().fill(.clear)
}
.prePostActionsButtonStyle(performAction: openSettings.closureBinding)
.prePostActionsButtonStyle(performAction: openSettingsLegacy.closureBinding)
.frame(width: 0, height: 0)
.opacity(0)
}
} else {
content
}
}
.environment(\.openSettings, openSettings)
.environment(\.openSettingsLegacy, openSettingsLegacy)
}
}

Expand Down

0 comments on commit e7a0e5b

Please sign in to comment.