Giter Club home page Giter Club logo

Comments (13)

orchetect avatar orchetect commented on July 26, 2024

your "programmatic" example is called from the action handler of a Button and i found that without the Button click the same code fails into a trigger

Can you give specific code examples where this is happening? I am able to get openSettings() to work anywhere in the View hierarchy, not just in a Button action. If there's edge cases I'd like to know so the library can be improved.

i expected programmatic to mean "invokable in the absence of user interaction"

Yes, that's what it means. It's programmatic without requiring user interaction anywhere within the SwiftUI view hierarchy, but not outside of it. There is also one known exception which is detailed in the readme regarding usage within a menu-based MenuBarExtra. Otherwise, it works programmatically as described.

i found a solution that works and am sharing it back here in case you'd like to alter the programmatic invocation

I appreciate the research. The method that is employed in this library is less brittle than synthesizing key events or walking menus for a number of reasons.

from settingsaccess.

zshannon avatar zshannon commented on July 26, 2024

eg

struct ContentView: View {
    @Environment(\.openSettings) private var openSettings

    var body: some View {
        ZStack {
            Button("Open Settings") { openSettings() }
        }.onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                openSettings() // triggers "this shouldn't happen in sonoma" assertion
            }
        }
    }
}

fwiw i came to this library looking for a replacement for NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) which works globally. if that's not the goal of the library, no sweat, just figured it was worth trying to help the library achieve that goal before releasing one that does this.

totally get that these solutions are (carefully engineered) hacks, we're working with the scraps that apple calls a platform here

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

Ah I know what's happening there.

openSettings() itself is a closure. By default it is routed to the legacy Settings method. On macOS 14, when SwiftUI sets up a View, then internally openSettings() gets updated and replaced by the correct call to SettingsLink internally.

In your example, openSettings() is getting captured before it can be updated.

This is a good edge case revealing a weakness. What I can probably do is refactor openSettings() to be a method call instead of a closure itself which would solve this issue.

looking for a replacement for NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) which works globally

That is AFAIK not possible as of macOS 14 when using SwiftUI Settings scene, other than some risky workarounds like UI scripting or NSEvents (which could all break in future macOS updates, but who even knows). My goal was to find a more canonical or SwiftUI-compatible way to achieve this.

totally get that these solutions are (carefully engineered) hacks, we're working with the scraps that apple calls a platform
here

[...] everyone at apple who failed to stop SettingsLink from breaking programmatically opening settings [...]

Absolutely. I filed a radar with Apple asking for their private _openSettings method to be made public which would not be a perfect solution but a pretty good one. I'm not sure we can really ask for more. The more people that file radars the better though.

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

I've implemented the fix and tested it - it works correctly when openSettings is captured now. Will push a release momentarily.

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

Ok pushed release 1.3.0.

from settingsaccess.

zshannon avatar zshannon commented on July 26, 2024

nice! ok this works well enough for me, thanks! it'd be even better for me with throws instead of the assertion so i can catch and use the NSEvent workaround in that event, are you game for making that change?

here's the setup i ended up with to recreate the global call apple broke:

// in @main SwiftUI app, among other WindowGroups & MenuBarExtras
WindowGroup("Open Settings") {
    FakeViewToOpenSettingsInSonomaThanksAppleView(title: "Open Settings")
        .openSettingsAccess()
}
.handlesExternalEvents(matching: Set(arrayLiteral: "settings"))
.windowStyle(HiddenTitleBarWindowStyle())
.windowResizability(.contentSize)
struct FakeViewToOpenSettingsInSonomaThanksAppleView: View {
    @Environment(\.openSettings) private var openSettings
    var title: String

    var body: some View {
        ZStack {}
            .frame(width: 0, height: 0)
            .onAppear {
                DispatchQueue.main
                    /// NB: calling `openSettings` immediately doesn't work so wait a quick moment
                    .asyncAfter(deadline: .now() + 0.05) {
                        openSettings()
                        NSApplication.shared
                            .windows
                            .filter { $0.canBecomeKey }
                            .filter { $0.title == title }
                            .forEach { $0.close() }
                    }
            }
    }
}

...setup a URL scheme as normal in info.plist...

and finally global programmatic opening settings like so:

NSWorkspace.shared.open(URL(string: "$YOUR-SCHEME-HERE://settings")!)

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

it'd be even better for me with throws instead of the assertion

Agreed - I'm working on an update to refactor things so the errors are informative and will remove the assert.

here's the setup i ended up with to recreate the global call apple broke:

A custom URI is clever and a great idea. Especially since it could take a parameter to open Settings and then switch to a specific settings tab page in the window (conceivably). I wish there was a cleaner way to do it -- unfortunate that it needs strapping on a WindowGroup with the window close logic.

The Settings scene itself is capable of using handlesExternalEvents directly. Haven't tried yet but it's worth exploring.

Settings {
    SettingsView()
}
.handlesExternalEvents(matching: ["settings"])

from settingsaccess.

zshannon avatar zshannon commented on July 26, 2024

i tried adding handlesExternalEvents to the Settings scene with no success but could've been missing something idk

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

Ok I've pushed the changes to main branch. openSettings() is now throwing and gives a specific error reason which might help with diagnosing issues and debugging.

I also improved some of the internals and the error handling will also throw if pre-Sonoma selectors fail.

Do you want to try it and see if it works for you?

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

Side point: While I understand the desire to have a truly global method to open Settings, is it actually necessary?

The Settings window is a part of the UI. As far as design architecture, it should be sufficient to invoke it from another part of the UI (which is possible with SettingsAccess with only few limitations). If you are wanting to open Settings from logic (ie: a non-UI package holding data model structures) then it seems like something that should be surfaced out of the model to the UI, then the UI can handle opening Settings.

Are there any use cases you are finding where you absolutely need a global method?

from settingsaccess.

zshannon avatar zshannon commented on July 26, 2024

Side point: While I understand the desire to have a truly global method to open Settings, is it actually necessary?

The Settings window is a part of the UI. As far as design architecture, it should be sufficient to invoke it from another part of the UI (which is possible with SettingsAccess with only few limitations). If you are wanting to open Settings from logic (ie: a non-UI package holding data model structures) then it seems like something that should be surfaced out of the model to the UI, then the UI can handle opening Settings.

Are there any use cases you are finding where you absolutely need a global method?

totally agree aesthetically-- my situation is (i suspect) quite unique, i'm augmenting the javascript runtime on the web with a browser extension that uses a native socket to perform RPCs in a native macOS application, and in some cases need to open settings for the macOS application from ui that's triggered by... an html button in the browser 👹

at the same time, for like 20 years opening settings on macOS has been this globally accessible single line of code that you just call et voila and i think that offering that back is a noble goal

from settingsaccess.

orchetect avatar orchetect commented on July 26, 2024

Ah yeah, I'm no stranger to unique situations.

globally accessible single line of code [...] is a noble goal

Not noble enough for Apple it seems - I hope they listen and give us more native options.

from settingsaccess.

iMaddin avatar iMaddin commented on July 26, 2024

Side point: While I understand the desire to have a truly global method to open Settings, is it actually necessary?

The Settings window is a part of the UI. As far as design architecture, it should be sufficient to invoke it from another part of the UI (which is possible with SettingsAccess with only few limitations). If you are wanting to open Settings from logic (ie: a non-UI package holding data model structures) then it seems like something that should be surfaced out of the model to the UI, then the UI can handle opening Settings.

Are there any use cases you are finding where you absolutely need a global method?

Hey guys, great discussion going on here! Thanks to this issue here I found a workaround to this exact problem.

My situation is that I have a macOS app with a mixture of SwiftUI and AppKit. Settings are made with SwiftUI, but I have a classic AppKit NSMenu with NSMenuItem that opens the SwiftUI Settings.

from settingsaccess.

Related Issues (6)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.