diff --git a/Documentation/GettingStarted.md b/Documentation/GettingStarted.md index 8e8dc406..0a51f724 100644 --- a/Documentation/GettingStarted.md +++ b/Documentation/GettingStarted.md @@ -50,15 +50,14 @@ This code snippet demonstrates rendering a blue color across all buttons and dis -To render content on specific areas, utilize the [Stream Deck Layout](Layout/README.md) system. `StreamDeckLayout` with the `@StreamDeckView` Macro provides predefined layout views to position content on a Stream Deck. +To render content on specific areas, utilize the [Stream Deck Layout](Layout/README.md) system. `StreamDeckLayout` provides predefined layout views to position content on a Stream Deck. ```swift import SwiftUI import StreamDeckKit -@StreamDeckView -struct MyFirstStreamDeckLayout { - var streamDeckBody: some View { +struct MyFirstStreamDeckLayout: View { + var body: some View { StreamDeckLayout { // Define key area // Use StreamDeckKeyAreaLayout for rendering separate keys diff --git a/Documentation/Layout/Animated.md b/Documentation/Layout/Animated.md index a15c4bb9..6d41aaf6 100644 --- a/Documentation/Layout/Animated.md +++ b/Documentation/Layout/Animated.md @@ -1,6 +1,6 @@ # Basic animations -As described in [Handling state changes](./Stateful.md), the `StreamDeckLayout` combined with the `@StreamDeckView` Macro is used to automatically update the image rendered on your Stream Deck Device on view state changes. +As described in [Handling state changes](./Stateful.md), the `StreamDeckLayout` is used to automatically update the image rendered on your Stream Deck Device on view state changes. Due to the underlying transformation of an SwiftUI view to an image that can be rendered on your Stream Deck device, SwiftUI's animations do not work as you might expect. However, the following example shows how you can create animations regardless, leveraging incremental state changes over time. @@ -19,10 +19,9 @@ For Stream Deck +, this layout will be rendered and react to interactions as fol import StreamDeckKit import SwiftUI -@StreamDeckView struct AnimatedStreamDeckLayout { - var streamDeckBody: some View { + var body: some View { StreamDeckLayout { StreamDeckKeyAreaLayout { _ in // To react to state changes within each StreamDeckKeyView, extract the view, just as you normally would in SwiftUI @@ -42,19 +41,20 @@ struct AnimatedStreamDeckLayout { } } - @StreamDeckView - struct MyKeyView { + struct MyKeyView: View { @State private var isPressed: Bool? @State private var scale: CGFloat = 1.0 @State private var rotationDegree: Double = .zero + + @Environment(\.streamDeckViewContext.index) var viewIndex - var streamDeckBody: some View { + var body: some View { StreamDeckKeyView { pressed in self.isPressed = pressed } content: { VStack { - Text("\(viewIndex)") // `viewIndex` is provided by the `@StreamDeckView` macro + Text("\(viewIndex)") Text(isPressed == true ? "Key down" : "Key up") } .scaleEffect(scale) // Update the scale depending on the state @@ -103,15 +103,16 @@ struct AnimatedStreamDeckLayout { } } - @StreamDeckView - struct MyDialView { + struct MyDialView: View { @State private var isPressed: Bool? @State private var position: CGPoint = .zero @State private var targetPosition: CGPoint? + + @Environment(\.streamDeckViewContext.size) var viewSize - var streamDeckBody: some View { + var body: some View { StreamDeckDialView { rotations in self.position.x += CGFloat(rotations) } press: { pressed in @@ -151,15 +152,14 @@ struct AnimatedStreamDeckLayout { if isPressed == nil || isPressed == true { self.position = CGPoint( x: viewSize.width / 2, - y: viewSize.height / 2 // `viewSize` is provided by the `@StreamDeckView` macro + y: viewSize.height / 2 ) } } } } - - @StreamDeckView - struct MyNeoPanelView { + + struct MyNeoPanelView: View { @State private var offset: Double = 0 @State private var targetOffset: Double = 0 @@ -168,7 +168,7 @@ struct AnimatedStreamDeckLayout { let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - var streamDeckBody: some View { + var body: some View { // Use StreamDeckNeoPanelLayout for Stream Deck Neo StreamDeckNeoPanelLayout { touched in targetOffset -= touched ? 50 : 0 diff --git a/Documentation/Layout/README.md b/Documentation/Layout/README.md index 55dc9798..368f0f5a 100644 --- a/Documentation/Layout/README.md +++ b/Documentation/Layout/README.md @@ -2,7 +2,7 @@ The `StreamDeckLayout` view is a fundamental component for building layouts for Stream Deck devices using SwiftUI. It provides a way to define the key area view with its keys and window view with its dials for a Stream Deck layout. This layout can be used to draw a customized layout onto a Stream Deck device and to recognize Stream Deck interactions in the SwiftUI way. -A `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. +A `StreamDeckLayout` does the heavy lifting for you by automatically recognizing view updates, and triggering an update of the rendered image on your Stream Deck device. The general structure of `StreamDeckLayout` is as follows: @@ -39,10 +39,9 @@ Here's an example of how to create a basic static `StreamDeckLayout`. For exampl import SwiftUI import StreamDeckKit -@StreamDeckView -struct StatelessStreamDeckLayout { +struct StatelessStreamDeckLayout: View { - var streamDeckBody: some View { + var body: some View { StreamDeckLayout { // Define key area // Use StreamDeckKeyAreaLayout for rendering separate keys diff --git a/Documentation/Layout/Stateful.md b/Documentation/Layout/Stateful.md index 81514159..0e5b9f70 100644 --- a/Documentation/Layout/Stateful.md +++ b/Documentation/Layout/Stateful.md @@ -1,8 +1,6 @@ # Handling state changes -As described in [The layout system](./README.md), 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. +As described in [The layout system](./README.md), the `StreamDeckLayout` does the heavy lifting for you by automatically recognizing view updates, and triggering an update of the rendered image on your Stream Deck device. ## Example @@ -19,10 +17,9 @@ For Stream Deck +, this layout will be rendered and react to interactions as fol import StreamDeckKit import SwiftUI -@StreamDeckView struct StatefulStreamDeckLayout { - var streamDeckBody: some View { + var body: some View { StreamDeckLayout { StreamDeckKeyAreaLayout { _ in // To react to state changes within each StreamDeckKeyView, extract the view, just as you normally would in SwiftUI @@ -42,17 +39,18 @@ struct StatefulStreamDeckLayout { } } - @StreamDeckView - struct MyKeyView { + struct MyKeyView: View { @State private var isPressed: Bool = false + + @Environment(\.streamDeckViewContext.index) var viewIndex - var streamDeckBody: some View { + var body: some View { StreamDeckKeyView { pressed in self.isPressed = pressed } content: { VStack { - Text("\(viewIndex)") // `viewIndex` is provided by the `@StreamDeckView` macro + Text("\(viewIndex)") Text(isPressed ? "Key down" : "Key up") } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -61,13 +59,14 @@ struct StatefulStreamDeckLayout { } } - @StreamDeckView - struct MyDialView { + struct MyDialView: View { @State private var offset: CGSize = .zero @State private var scale: CGFloat = 1 + + @Environment(\.streamDeckViewContext.size) var viewSize - var streamDeckBody: some View { + var body: some View { StreamDeckDialView { rotations in self.scale = min(max(scale + CGFloat(rotations) / 10, 0.5), 5) } press: { pressed in @@ -78,7 +77,7 @@ struct StatefulStreamDeckLayout { } 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 + height: location.y - viewSize.height / 2 ) } content: { Text("\(viewIndex)") @@ -89,16 +88,15 @@ struct StatefulStreamDeckLayout { } } } - - @StreamDeckView - struct MyNeoPanelView { + + struct MyNeoPanelView: View { @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 { + var body: some View { // Use StreamDeckNeoPanelLayout for Stream Deck Neo StreamDeckNeoPanelLayout { touched in offset -= touched ? 5 : 0 diff --git a/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift b/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift index 6a719455..afe3b39f 100644 --- a/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift +++ b/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift @@ -42,7 +42,7 @@ struct StatefulStreamDeckLayout: View { self.isPressed = pressed } content: { VStack { - Text("\(viewIndex)") // `viewIndex` is provided by the `@StreamDeckView` macro + Text("\(viewIndex)") Text(isPressed ? "Key down" : "Key up") } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -70,7 +70,7 @@ struct StatefulStreamDeckLayout: View { } 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 + height: location.y - viewSize.height / 2 ) } content: { Text("\(viewIndex)") diff --git a/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift b/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift index 167f8b68..9bef1693 100644 --- a/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift +++ b/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift @@ -44,7 +44,7 @@ struct AnimatedStreamDeckLayout: View { self.isPressed = pressed } content: { VStack { - Text("\(viewIndex)") // `viewIndex` is provided by the `@StreamDeckView` macro + Text("\(viewIndex)") Text(isPressed == true ? "Key down" : "Key up") } .scaleEffect(scale) // Updating the scale depending on the state @@ -142,7 +142,7 @@ struct AnimatedStreamDeckLayout: View { if isPressed == nil || isPressed == true { self.position = CGPoint( x: viewSize.width / 2, - y: viewSize.height / 2 // `viewSize` is provided by the `@StreamDeckView` macro + y: viewSize.height / 2 ) } } diff --git a/README.md b/README.md index b9215f59..47f7e396 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ This code snippet demonstrates rendering a blue color across all buttons and dis ### Rendering Layouts -To render content on specific areas, utilize the `StreamDeckLayout` system with the `@StreamDeckView` Macro. `StreamDeckLayout` provides predefined layout views to position content on a Stream Deck. +To render content on specific areas, utilize the `StreamDeckLayout` system. `StreamDeckLayout` provides predefined layout views to position content on a Stream Deck. ```swift import StreamDeckKit @@ -86,10 +86,9 @@ StreamDeckSession.setUp(newDeviceHandler: { $0.render(MyFirstStreamDeckLayout()) import SwiftUI import StreamDeckKit -@StreamDeckView -struct MyFirstStreamDeckLayout { +struct MyFirstStreamDeckLayout: View { - var streamDeckBody: some View { + var body: some View { StreamDeckLayout { // Define key area // Use StreamDeckKeyAreaLayout for rendering separate keys diff --git a/Sources/StreamDeckKit/Layout/StreamDeckViewContext.swift b/Sources/StreamDeckKit/Layout/StreamDeckViewContext.swift index c9aa3274..42a58862 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckViewContext.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckViewContext.swift @@ -28,8 +28,6 @@ import Foundation /// Provides information about the current context (screen, key-area, key, window, dial) in SwiftUI environments. -/// -/// This is used internally by the ``StreamDeckView`` macro and the ``StreamDeckLayout`` system. public struct StreamDeckViewContext { /// The Stream Deck device object. diff --git a/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift b/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift index 03deabe1..252a7119 100644 --- a/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift +++ b/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift @@ -28,11 +28,6 @@ import SwiftUI import StreamDeckKit -// The explicit implementation of the `StreamDeckView` protocol is a workaround. Normally we would use the `@StreamDeckView` -// macro here. But due to a bug in XCode 16 and 16.1 betas, the `if #available` check that the macro implemented, -// always threw an error. -// If you read this and XCode 16 was finally released, please check if just using the macro is working again. - struct SimulatorDialTouchView: View { @Environment(\.streamDeckViewContext) private var context diff --git a/Sources/StreamDeckSimulator/Views/StreamDeckSimulator.PreviewView.swift b/Sources/StreamDeckSimulator/Views/StreamDeckSimulator.PreviewView.swift index a94d3973..69c6e806 100644 --- a/Sources/StreamDeckSimulator/Views/StreamDeckSimulator.PreviewView.swift +++ b/Sources/StreamDeckSimulator/Views/StreamDeckSimulator.PreviewView.swift @@ -32,7 +32,7 @@ import SwiftUI public extension StreamDeckSimulator { /// A wrapper view to use ``StreamDeckSimulator`` in SwiftUI previews. /// - /// This code will show a Stream Deck Mini simulator that renders a view conforming to `StreamDeckView`. + /// This code will show a Stream Deck Mini simulator that renders some layout view. /// ```swift /// #Preview { /// StreamDeckSimulator.PreviewView(streamDeck: .mini) { device in