Skip to main content

Handling State Changes

As described in the layout system, the StreamDeckLayout combined with the @StreamDeckView Macro does the heavy lifting for you by automatically recognizing view updates, and triggering an update of the rendered image on your Stream Deck device.

To update your StreamDeckLayout on events like key presses or dial rotations, the view that should react to state changes needs to be extracted in its own view, just as you would normally do with SwiftUI. If that view is annotated with the @StreamDeckView Macro, context-dependent variables like the viewIndex and viewSize are available in that view's scope.

Example

Here's an example of how to create a basic stateful StreamDeckLayout which changes the appearance on events like key presses or dial rotations.

For Stream Deck +, this layout will be rendered and react to interactions as follows:

An animation showing a stateful layout on a Stream Deck +
import StreamDeckKit
import SwiftUI

@StreamDeckView
struct StatefulStreamDeckLayout {
    var streamDeckBody: some View {
        StreamDeckLayout {
            StreamDeckKeyAreaLayout { _ in
                // To react to state changes within each StreamDeckKeyView, extract the view, just as you normally would in SwiftUI
                // Example:
                MyKeyView()
            }
        } windowArea: {
            // To react to state changes within each view, extract the view, just as you normally would in SwiftUI
            // Example:
            if streamDeck.info.product == .plus {
                StreamDeckDialAreaLayout { _ in
                    MyDialView()
                }
            } else if streamDeck.info.product == .neo {
                MyNeoPanelView()
            }
        }
    }

    @StreamDeckView
    struct MyKeyView {
        @State private var isPressed: Bool = false
        var streamDeckBody: some View {
            StreamDeckKeyView { pressed in
                self.isPressed = pressed
            } content: {
                VStack {
                    Text("\(viewIndex)") // `viewIndex` is provided by the `@StreamDeckView` macro
                    Text(isPressed ? "Key down" : "Key up")
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(isPressed ? .purple.opacity(0.5) : .purple) // Updating the background depending on the state
            }
        }
    }

    @StreamDeckView
    struct MyDialView {
        @State private var offset: CGSize = .zero
        @State private var scale: CGFloat = 1

        var streamDeckBody: some View {
            StreamDeckDialView { rotations in
                self.scale = min(max(scale + CGFloat(rotations) / 10, 0.5), 5)
            } press: { pressed in
                if pressed {
                    self.scale = 1
                    self.offset = .zero
                }
            } touch: { location in
                self.offset = CGSize(
                    width: location.x - viewSize.width / 2,
                    height: location.y - viewSize.height / 2 // `viewSize` is provided by the `@StreamDeckView` macro
                )
            } content: {
                Text("\(viewIndex)")
                    .scaleEffect(scale) // Updating the scale depending on the state
                    .offset(offset) // Updating the offset depending on the state
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color(white: Double(viewIndex) / 5 + 0.5))
            }
        }
    }

    @StreamDeckView
    struct MyNeoPanelView {
        @State private var offset: Double = 0
        @State private var date: Date = .now

        let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

        var streamDeckBody: some View {
            // Use StreamDeckNeoPanelLayout for Stream Deck Neo
            StreamDeckNeoPanelLayout { touched in
                offset -= touched ? 5 : 0
            } rightTouch: { touched in
                offset += touched ? 5 : 0
            } panel: {
                VStack {
                    Text(date.formatted(date: .complete, time: .omitted))
                    Text(date.formatted(date: .omitted, time: .standard)).bold().monospaced()
                }
                .offset(x: offset)
            }
            .background(Color(white: Double(1) / 5 + 0.5))
            .onReceive(timer, perform: { _ in
                date = .now
            })
        }
    }
}