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:
importStreamDeckKitimportSwiftUI@StreamDeckViewstructStatefulStreamDeckLayout {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 { _inMyDialView() } } elseif streamDeck.info.product == .neo {MyNeoPanelView() } } }@StreamDeckViewstructMyKeyView {@Stateprivatevar isPressed: Bool=falsevar streamDeckBody: some View {StreamDeckKeyView { pressed in self.isPressed = pressed } content: {VStack {Text("\(viewIndex)")// `viewIndex` is provided by the `@StreamDeckView` macroText(isPressed ?"Key down":"Key up") } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(isPressed ? .purple.opacity(0.5) : .purple) // Updating the background depending on the state
} } }@StreamDeckViewstructMyDialView {@Stateprivatevar offset: CGSize = .zero@Stateprivatevar scale: CGFloat =1var streamDeckBody: some View {StreamDeckDialView { rotations in self.scale =min(max(scale + CGFloat(rotations)/10, 0.5), 5) } press: { pressed inif 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)) } } }@StreamDeckViewstructMyNeoPanelView {@Stateprivatevar offset: Double=0@Stateprivatevar date: Date = .nowlet timer = Timer.publish(every:1, on: .main, in: .common).autoconnect()var streamDeckBody: some View {// Use StreamDeckNeoPanelLayout for Stream Deck NeoStreamDeckNeoPanelLayout { 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 }) } }}