emorydunn / StreamDeckPlugin

A library for creating Stream Deck plugins in Swift.
https://emorydunn.github.io/StreamDeckPlugin/
MIT License
50 stars 7 forks source link

getGlobalSettings is throwing an error. #21

Closed SENTINELITE closed 9 months ago

SENTINELITE commented 1 year ago

Hey, Emory! When trying to access the Plugin's global settings, it's failing when unwrapping the StreamDeckPlugin.shared.uuid.

Error:

StreamDeck/Delegate+Sent.swift:57: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

@main
class CounterPlugin: PluginDelegate {
...
required init() {
getGlobalSettings()
}
}
emorydunn commented 1 year ago

At the time your delegate is created the shared plugin hasn't actually been initialized. So trying to get the global settings is making a request through StreamDeckPlugin.shared which doesn't exist yet.

The Settings API is stateless because events include the current settings when called, however as you've run into that doesn't work very well for global settings. I want to rework that API with property wrappers to make accessing settings easier.

emorydunn commented 1 year ago

I did some initial work on fixing the global settings in the GlobalSettings branch. There's now a GlobalSetting property wrapper that works much the same way as the existing Environment property wrapper does, but it writes changes back to the SD app.

If you've got some time to test let me know what you think. The example plugin is updated to show how it all works.

SENTINELITE commented 1 year ago

Hey, Emory!

I was just going to circle back to this! I'll give it a go in the next week or so, & report back!

I appreciate it!

SENTINELITE commented 1 year ago

A few observations.

I've set up my keys like so:

struct ForcedTitleGlobalKey: EnvironmentKey, GlobalSettingKey {
    static let defaultValue: Bool = false
}

struct AccessibilityGlobalKey: EnvironmentKey, GlobalSettingKey {
    static let defaultValue: Bool = false
}

& while this works, it seems a cumbersome if we have more than ~3 keys.

Ideally, I feel like a macro may better suit the direction of Swift.

@GlobalSettingKey
struct SDSSettings {
    var isForcedTitleGlobal: Bool
    var isAccessibilityGlobal: Bool
    //...
}

With that being said, I don't see why didReceiveGlobalSettings() on the PluginDelegate would need to exist?

emorydunn commented 1 year ago

The whole thing is inspired (read: ripped off) from SwiftUI, and is definitely a verbose way to declare everything.

I haven't looked into macros much, but they do seem suited to generating the boilerplate for the default value struct and getter and setter extension.

As for didReceiveGlobalSettings(), it will probably be removed, but it's still there while I work on the new API. That being said, there could be instances where being notified of a settings change at the plugin level might be useful, though nothing immediately comes to mind.

As an aside, you shouldn't need to declare both EnvironmentKey and GlobalSettingKey, unless there are cases where you want to use the same declarations for both temporary and persistent values.

emorydunn commented 1 year ago

Got a chance to test out macros, and I think I have a working implementation. Using an attached macro, while a bit cleaner, can't generate the properties in the GlobalSettings extension and I couldn't find a good way around it. I changed tactics a bit and tried a freestanding macro instead.

extension GlobalSettings {
    #globalSetting("count", defaultValue: 0, ofType: Int.self)
    #globalSetting("color", defaultValue: "#FFF", ofType: String.self)
}

The macro generates both the struct and the property for the key path inside the extension. I'm fairly happy with the implementation with the exception of having to include the type. So far I haven't found a way to infer the type to put into the generated code without switching on literally every Swift type.

emorydunn commented 9 months ago

I've been working on the settings API again, and as part of that changed when the plugin is initialized. The initializer is now called after the rest of the plugin is set up, so calling evens from the init should be safe now. This is on the main branch for now and will be in the next release.