elgatosf / streamdeck-kit-ipad

Swift library for controlling Stream Deck hardware from an iPadOS application.
https://docs.elgato.com/ipad
MIT License
25 stars 2 forks source link

Additional Examples / Best Practices #39

Open blakekrone opened 4 months ago

blakekrone commented 4 months ago

Hello, working through the code and I'm wondering if we can get a best practices for multi-key setups similar to what is in the emoji example SD Connect app.

When using Stateful keys with StreamDeckKeyAreaLayout is it best practice to put the individual keys inside the KeyView or have multiple different KeyViews defined?

StreamDeckKeyAreaLayout { _ in // To react to state changes within each StreamDeckKeyView, extract the view, just as you normally would in SwiftUI // Example: Key1() Key2() Key3() }

Seems to create weird layouts given the StreamDeckKeyAreaLayout is actually a For Loop.

Ideally I would just want to detect what device it is, then provide a keyIndex value for where a button resides.

zahnooo commented 4 months ago

Hi @blakekrone , thanks for the questions and the feedback! We'll work on extending the examples based on this.

In the meantime, here is some guidance:

I'm wondering if we can get a best practices for multi-key setups similar to what is in the emoji example SD Connect app.

The emoji example in the SD Connect app works by setting random emojis from a static list of emojis to the keys, and storing the key-to-emoji information locally, and we're not handling any keys individually here.

When using Stateful keys with StreamDeckKeyAreaLayout is it best practice to put the individual keys inside the KeyView or have multiple different KeyViews defined? StreamDeckKeyAreaLayout { _ in // To react to state changes within each StreamDeckKeyView, extract the view, just as you normally would in SwiftUI // Example: Key1() Key2() Key3() }

Seems to create weird layouts given the StreamDeckKeyAreaLayout is actually a For Loop.

This creates weird layouts because each SwiftUI.View passed to the keyView: KeyViewProvider of StreamDeckKeyAreaLayout is treated as its own Stream Deck Key, as you correctly said.

What you should do instead is use the index: Int closure parameter of StreamDeckKeyAreaLayout and provide one view per index. Same goes for the index: Int closure parameter of StreamDeckDialAreaLayout for the Stream Deck XL dial windows. See example on the bottom.

Ideally I would just want to detect what device it is, then provide a keyIndex value for where a button resides.

There are several levels where you can detect and react on the device type, see below!

Example

To provide different layouts per device, you can either react to the device closure parameter of the newDeviceHandler of StreamDeckSession.setUp, i.e. like so:

StreamDeckSession.setUp(
    newDeviceHandler: { device in 
        switch device.info.product {
            case .mini: device.render(StreamDeckMiniLayout())
            case .regular: device.render(StreamDeckRegularLayout())
            case .xl: device.render(StreamDeckXLLayout())
             // case .neo: [...] and so on for the other devices
        }    
    }
)

Or provide just one layout and determine the device type within the layout definition.

For example:

@StreamDeckView
struct StreamDeckUI {

     var streamDeckBody: some View {
        StreamDeckLayout {
            switch streamDeck.info.product { // `streamDeck` is provided by the `@StreamDeckView` macro
            case .mini: // 6 keys
                StreamDeckKeyAreaLayout { index in
                    switch index {
                    case 0: Key1() // one view per index! 
                    case 1: Key2()
                    case 2: Key3()
                    case 3: Key4()
                    case 4: Key5()
                    case 5: Key6()
                    default: fatalError("This case never happens")
                    }
                }
            case .regular: // 15 keys
                StreamDeckKeyAreaLayout { index in
                    switch index {
                    case 0: Key1()
                    case 1: Key2()
                    case 2: Key3()
                    // !! Shortened - add cases as desired for all indices from 0 to 14
                    case 12: Key13()
                    case 13: Key14()
                    case 14: Key15()
                    default: fatalError("This case never happens") // or handle empty keys with i.e. `Color.black`
                    }
                }
            // case .xl: [...], and so on
            default: Color.black
            }
        } windowArea: {
            switch streamDeck.info.product {
            case .xl:
                StreamDeckDialAreaLayout { index in
                    switch index {
                    case 0: Dial1()
                    case 1: Dial2()
                    case 2: Dial3()
                    case 3: Dial4()
                    default: fatalError("This case never happens") // or handle empty dial window views with i.e. `Color.black`
                    }
                }

            case .neo:
                MyNeoPanelView()
            default: Color.black
            }
        }
    }

}
blakekrone commented 4 months ago

Thanks @zahnooo for the additional information. That's the route I was working with the switch() inside the Layout, it just didn't seem efficient to always be doing a switch statement each iteration. But I suppose, with the max keys (currently) being 32 that is trivial for a switch statement.

zahnooo commented 4 months ago

I guess there are several ways how to save some code here, but that depends on what you're trying to achieve.

For example, depending on how flexible / adaptive you want your layout to be depending on the Stream Deck device, you could also consider storing your keys in an array and return them by index - this way you could add your most important actions in the beginning and less important ones in the end, and the less important ones would be ignored if you run out of keys.

Something like this:


@StreamDeckView
struct StreamDeckUI {

    // Static list of your key definitions 
    private static let keys = [MostImportantKey(), SecondMostImportantKey()]

    var streamDeckBody: some View {
        StreamDeckLayout {
            StreamDeckKeyAreaLayout { index in
                if index >= StreamDeckUI.keys.count {
                    Color.black // ignore out-of-bounds keys
                } else {
                    StreamDeckUI.keys[index] // return key by index in array 
                }
            }
        }
    }

}