diff --git a/Changelog.md b/Changelog.md index 365ae5d3..3973fb80 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.1.0] - 2024-09-23 + +Removes the need for a macro. + +You can now declare your Stream Deck layouts as regular Swift UI views. There is no need to implement `StreamDeckView` anymore. + +### Removed + +- The dependency on [Stream Deck Kit - Macros](https://github.com/elgatosf/streamdeck-kit-macros) +- The `StreamDeckView` protocol + ## [1.0.0] - 2024-08-22 This is the first official release. 🎉 diff --git a/Documentation/GettingStarted.md b/Documentation/GettingStarted.md index 8e8dc406..f113c3da 100644 --- a/Documentation/GettingStarted.md +++ b/Documentation/GettingStarted.md @@ -24,7 +24,7 @@ If you want to add it to your own libraries `Package.swift`, use this code inste ```swift dependencies: [ - .package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "1.0.0") + .package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "1.1.0") ] ``` @@ -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..ed461dec 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,11 @@ 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 { + @Environment(\.streamDeckViewContext.device) var streamDeck + + 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 +43,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 +105,17 @@ 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 + @Environment(\.streamDeckViewContext.index) var viewIndex - var streamDeckBody: some View { + var body: some View { StreamDeckDialView { rotations in self.position.x += CGFloat(rotations) } press: { pressed in @@ -151,15 +155,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 +171,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..be0a81cb 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,15 @@ 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 { + // Use the `streamDeckViewContext` environment variable to get information about + // the device, the view size or the current key index. + // Values will be different, depending on which layout level (e.g. window, key + // or background) we are. + @Environment(\.streamDeckViewContext.device) var streamDeck + + 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..3bb5fa2a 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,11 @@ 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 { + @Environment(\.streamDeckViewContext.device) var streamDeck + + 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 +41,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 +61,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 +79,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 +90,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/1_StatelessStreamDeckLayout.swift b/Example/Example App/Examples/1_StatelessStreamDeckLayout.swift index f00fcf8b..717596c2 100644 --- a/Example/Example App/Examples/1_StatelessStreamDeckLayout.swift +++ b/Example/Example App/Examples/1_StatelessStreamDeckLayout.swift @@ -8,10 +8,11 @@ import StreamDeckKit import SwiftUI -@StreamDeckView -struct StatelessStreamDeckLayout { +struct StatelessStreamDeckLayout: View { - var streamDeckBody: some View { + @Environment(\.streamDeckViewContext.device) var streamDeck + + var body: some View { StreamDeckLayout { // Define key area // Use StreamDeckKeyAreaLayout for rendering separate keys diff --git a/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift b/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift index 35e2e268..afe3b39f 100644 --- a/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift +++ b/Example/Example App/Examples/2_StatefulStreamDeckLayout.swift @@ -8,10 +8,11 @@ import StreamDeckKit import SwiftUI -@StreamDeckView -struct StatefulStreamDeckLayout { +struct StatefulStreamDeckLayout: View { - var streamDeckBody: some View { + @Environment(\.streamDeckViewContext.device) var streamDeck + + 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 @@ -31,17 +32,17 @@ struct StatefulStreamDeckLayout { } } - @StreamDeckView - struct MyKeyView { + struct MyKeyView: View { + @Environment(\.streamDeckViewContext.index) var viewIndex @State private var isPressed: Bool = false - 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) @@ -50,13 +51,15 @@ struct StatefulStreamDeckLayout { } } - @StreamDeckView - struct MyDialView { + struct MyDialView: View { + + @Environment(\.streamDeckViewContext.index) var viewIndex + @Environment(\.streamDeckViewContext.size) var viewSize @State private var offset: CGSize = .zero @State private var scale: CGFloat = 1 - 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 @@ -67,7 +70,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)") @@ -79,15 +82,14 @@ 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/3_AnimatedStreamDeckLayout.swift b/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift index fe145ad4..9bef1693 100644 --- a/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift +++ b/Example/Example App/Examples/3_AnimatedStreamDeckLayout.swift @@ -8,10 +8,11 @@ import StreamDeckKit import SwiftUI -@StreamDeckView -struct AnimatedStreamDeckLayout { +struct AnimatedStreamDeckLayout: View { - var streamDeckBody: some View { + @Environment(\.streamDeckViewContext.device) var streamDeck + + 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 @@ -31,19 +32,19 @@ struct AnimatedStreamDeckLayout { } } - @StreamDeckView - struct MyKeyView { + struct MyKeyView: View { + @Environment(\.streamDeckViewContext.index) var viewIndex @State private var isPressed: Bool? @State private var scale: CGFloat = 1.0 @State private var rotationDegree: Double = .zero - 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) // Updating the scale depending on the state @@ -92,15 +93,16 @@ struct AnimatedStreamDeckLayout { } } - @StreamDeckView - struct MyDialView { + struct MyDialView: View { + @Environment(\.streamDeckViewContext.index) var viewIndex + @Environment(\.streamDeckViewContext.size) var viewSize @State private var isPressed: Bool? @State private var position: CGPoint = .zero @State private var targetPosition: CGPoint? - var streamDeckBody: some View { + var body: some View { StreamDeckDialView { rotations in self.position.x += CGFloat(rotations) } press: { pressed in @@ -140,15 +142,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 @@ -157,7 +158,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/Example/Example App/Examples/BaseStreamDeckView.swift b/Example/Example App/Examples/BaseStreamDeckView.swift index dcc79e58..4076a07f 100644 --- a/Example/Example App/Examples/BaseStreamDeckView.swift +++ b/Example/Example App/Examples/BaseStreamDeckView.swift @@ -8,12 +8,19 @@ import StreamDeckKit import SwiftUI -@StreamDeckView struct BaseStreamDeckView: View { + @Environment(\.streamDeckViewContext) var context @Environment(\.exampleDataModel) var dataModel + var body: some View { + content + .onChange(of: dataModel.selectedExample) { + context.updateRequired() + } + } + @ViewBuilder - var streamDeckBody: some View { + private var content: some View { switch dataModel.selectedExample { case .stateless: StatelessStreamDeckLayout() diff --git a/Example/StreamDeckKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/StreamDeckKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 098e84d7..00000000 --- a/Example/StreamDeckKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "streamdeck-kit-macros", - "kind" : "remoteSourceControl", - "location" : "https://github.com/elgatosf/streamdeck-kit-macros", - "state" : { - "revision" : "47324ebb0e2517219dd795d2915987f06d46e47e", - "version" : "0.0.1" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - } - ], - "version" : 2 -} diff --git a/Package.resolved b/Package.resolved index c9e1b02e..45824189 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "streamdeck-kit-macros", - "kind" : "remoteSourceControl", - "location" : "https://github.com/elgatosf/streamdeck-kit-macros", - "state" : { - "revision" : "47324ebb0e2517219dd795d2915987f06d46e47e", - "version" : "0.0.1" - } - }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index a4f72e8d..61d87903 100644 --- a/Package.swift +++ b/Package.swift @@ -21,10 +21,6 @@ let package = Package( .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.12.0" - ), - .package( - url: "https://github.com/elgatosf/streamdeck-kit-macros", - from: "0.0.1" ) ], targets: [ @@ -35,10 +31,7 @@ let package = Package( ), .target( name: "StreamDeckKit", - dependencies: [ - "StreamDeckCApi", - .product(name: "StreamDeckView", package: "streamdeck-kit-macros") - ] + dependencies: ["StreamDeckCApi"] ), .target( name: "StreamDeckCApi", diff --git a/README.md b/README.md index b9215f59..24f7efd1 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ If you want to add it to your own libraries `Package.swift`, use this code inste ```swift dependencies: [ - .package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "1.0.0") + .package(url: "https://github.com/elgatosf/streamdeck-kit-ipad.git", upToNextMajor: "1.1.0") ] ``` @@ -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,22 +86,23 @@ StreamDeckSession.setUp(newDeviceHandler: { $0.render(MyFirstStreamDeckLayout()) import SwiftUI import StreamDeckKit -@StreamDeckView -struct MyFirstStreamDeckLayout { +struct MyFirstStreamDeckLayout: View { - var streamDeckBody: some View { + @Environment(\.streamDeckViewContext.device) var streamDeck + + var body: some View { StreamDeckLayout { // Define key area // Use StreamDeckKeyAreaLayout for rendering separate keys - StreamDeckKeyAreaLayout { context in + StreamDeckKeyAreaLayout { keyIndex in // Define content for each key. - // StreamDeckKeyAreaLayout provides a context for each available key, + // StreamDeckKeyAreaLayout provides an index for each available key, // and StreamDeckKeyView provides a callback for the key action // Example: StreamDeckKeyView { pressed in - print("pressed \(pressed)") + print("pressed \(pressed) at index \(keyIndex)") } content: { - Text("\(context.index)") + Text("\(keyIndex)") .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.teal) } diff --git a/Sources/StreamDeckKit/Layout/StreamDeckDialAreaLayout.swift b/Sources/StreamDeckKit/Layout/StreamDeckDialAreaLayout.swift index d4971453..9403621d 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckDialAreaLayout.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckDialAreaLayout.swift @@ -137,5 +137,8 @@ public struct StreamDeckDialAreaLayout: View { default: break } } + .onChange(of: _nextID) { _ in + context.updateRequired() + } } } diff --git a/Sources/StreamDeckKit/Layout/StreamDeckDialView.swift b/Sources/StreamDeckKit/Layout/StreamDeckDialView.swift index 2ff56126..fe123224 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckDialView.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckDialView.swift @@ -113,5 +113,8 @@ public struct StreamDeckDialView: View { default: break } } + .onChange(of: _nextID) { _ in + context.updateRequired() + } } } diff --git a/Sources/StreamDeckKit/Layout/StreamDeckKeyAreaLayout.swift b/Sources/StreamDeckKit/Layout/StreamDeckKeyAreaLayout.swift index 1da16f4a..cf7f5c3c 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckKeyAreaLayout.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckKeyAreaLayout.swift @@ -75,5 +75,8 @@ public struct StreamDeckKeyAreaLayout: View { } } } + .onChange(of: _nextID) { _ in + context.updateRequired() + } } } diff --git a/Sources/StreamDeckKit/Layout/StreamDeckKeyView.swift b/Sources/StreamDeckKit/Layout/StreamDeckKeyView.swift index b9602542..13c600c4 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckKeyView.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckKeyView.swift @@ -74,5 +74,8 @@ public struct StreamDeckKeyView: View { action(pressed) } } + .onChange(of: _nextID) { _ in + context.updateRequired() + } } } diff --git a/Sources/StreamDeckKit/Layout/StreamDeckLayout.swift b/Sources/StreamDeckKit/Layout/StreamDeckLayout.swift index bd24681d..b7e8a81a 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckLayout.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckLayout.swift @@ -29,7 +29,16 @@ import Combine import Foundation import SwiftUI -@_exported import StreamDeckView +private var _id: UInt64 = 0 + +// Used in conjunction with View.onChange to get a post-render callback. +var _nextID: UInt64 { + if _id == UInt64.max { + _id = 0 + } + _id += 1 + return _id +} /// The basic view to build a layout for Stream Deck from. /// @@ -90,6 +99,9 @@ public struct StreamDeckLayout: View { } } .frame(width: context.size.width, height: context.size.height) + .onChange(of: _nextID) { _ in + context.updateRequired() + } } } diff --git a/Sources/StreamDeckKit/Layout/StreamDeckNeoPanelLayout.swift b/Sources/StreamDeckKit/Layout/StreamDeckNeoPanelLayout.swift index f306374b..1e89a617 100644 --- a/Sources/StreamDeckKit/Layout/StreamDeckNeoPanelLayout.swift +++ b/Sources/StreamDeckKit/Layout/StreamDeckNeoPanelLayout.swift @@ -69,5 +69,8 @@ public struct StreamDeckNeoPanelLayout: View { } } } + .onChange(of: _nextID) { _ in + context.updateRequired() + } } } 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/StreamDeckSimulator.swift b/Sources/StreamDeckSimulator/StreamDeckSimulator.swift index a862e9b3..ed13a89b 100644 --- a/Sources/StreamDeckSimulator/StreamDeckSimulator.swift +++ b/Sources/StreamDeckSimulator/StreamDeckSimulator.swift @@ -55,11 +55,25 @@ public final class StreamDeckSimulator { } private class PassThroughWindow: UIWindow { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // Get view from superclass. guard let hitView = super.hitTest(point, with: event) else { return nil } - // If the returned view is the `UIHostingController`'s view, ignore. - return rootViewController?.view == hitView || rootViewController?.view.superview == hitView ? nil : hitView + + if isInsideSimulator(point, with: event) { + // When the location check passes, return the view. + return hitView + } else { + // When the returned view is the `UIHostingController`'s view, ignore. + return rootViewController?.view == hitView || rootViewController?.view.superview == hitView ? nil : hitView + } + } + + func isInsideSimulator(_ point: CGPoint, with event: UIEvent?) -> Bool { + guard let viewToCheck: UIView = rootViewController?.view.subviews.first else { + return super.point(inside: point, with: event) + } + return viewToCheck.point(inside: convert(point, to: viewToCheck), with: event) } } diff --git a/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift b/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift index 74cac2b4..252a7119 100644 --- a/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift +++ b/Sources/StreamDeckSimulator/Views/SimulatorDialTouchView.swift @@ -28,12 +28,7 @@ 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: StreamDeckView { +struct SimulatorDialTouchView: View { @Environment(\.streamDeckViewContext) private var context private var viewSize: CGSize { context.size } @@ -41,9 +36,7 @@ struct SimulatorDialTouchView: StreamDeckView { let client: StreamDeckSimulatorClient? - @MainActor - @ViewBuilder - var streamDeckBody: some View { + var body: some View { StreamDeckDialView { Color.clear } @@ -52,7 +45,7 @@ struct SimulatorDialTouchView: StreamDeckView { .onTapGesture(coordinateSpace: .local) { localLocation in guard let client = client else { return } let x = CGFloat(viewIndex) * viewSize.width + localLocation.x - Task { await client.emit(.touch(.init(x: x, y: localLocation.y))) } + client.emit(.touch(.init(x: x, y: localLocation.y))) } .gesture( DragGesture(minimumDistance: 10, coordinateSpace: .local) @@ -60,28 +53,11 @@ struct SimulatorDialTouchView: StreamDeckView { guard let client = client else { return } let startX = CGFloat(viewIndex) * viewSize.width + value.startLocation.x let endX = CGFloat(viewIndex) * viewSize.width + value.location.x - Task { - await client.emit(.fling( - start: .init(x: startX, y: value.startLocation.y), - end: .init(x: endX, y: value.location.y) - )) - } + client.emit(.fling( + start: .init(x: startX, y: value.startLocation.y), + end: .init(x: endX, y: value.location.y) + )) } ) } - - @MainActor - var body: some View { - if #available (iOS 17, *) { - return streamDeckBody - .onChange(of: StreamDeckKit._nextID) { - context.updateRequired() - } - } else { - return streamDeckBody - .onChange(of: StreamDeckKit._nextID) { _ in - context.updateRequired() - } - } - } } 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 diff --git a/Tests/StreamDeckSDKTests/Helper/TestViews.swift b/Tests/StreamDeckSDKTests/Helper/TestViews.swift index 0b134b61..cd6b22ac 100644 --- a/Tests/StreamDeckSDKTests/Helper/TestViews.swift +++ b/Tests/StreamDeckSDKTests/Helper/TestViews.swift @@ -51,11 +51,11 @@ enum TestViews { @Published var lastEvent: Event = .none } - @StreamDeckView - struct SimpleKey { + struct SimpleKey: View { + @Environment(\.streamDeckViewContext.index) var viewIndex @StateObject var model = SimpleEventModel() - var streamDeckBody: some View { + var body: some View { StreamDeckKeyView { isPressed in model.lastEvent = .press(isPressed) } content: { @@ -74,11 +74,11 @@ enum TestViews { } } - @StreamDeckView - struct SimpleDialView { + struct SimpleDialView: View { + @Environment(\.streamDeckViewContext.index) var viewIndex @StateObject var model = SimpleEventModel() - var streamDeckBody: some View { + var body: some View { StreamDeckDialView { steps in model.lastEvent = .rotate(steps) } press: { pressed in @@ -96,9 +96,8 @@ enum TestViews { } } - @StreamDeckView - struct SimpleLayout { - var streamDeckBody: some View { + struct SimpleLayout: View { + var body: some View { StreamDeckLayout( keyArea: { StreamDeckKeyAreaLayout { _ in @@ -115,11 +114,10 @@ enum TestViews { } struct TouchAreaTestLayout: View { - @StreamDeckView - struct WindowLayout { // swiftlint:disable:this nesting + struct WindowLayout: View { // swiftlint:disable:this nesting @StateObject var model = SimpleEventModel() - var streamDeckBody: some View { + var body: some View { ZStack { StreamDeckDialAreaLayout( rotate: { _, steps in @@ -150,11 +148,10 @@ enum TestViews { } struct NeoTouchKeyTestLayout: View { - @StreamDeckView - struct WindowLayout { // swiftlint:disable:this nesting + struct WindowLayout: View { // swiftlint:disable:this nesting @StateObject var model = SimpleEventModel() - var streamDeckBody: some View { + var body: some View { ZStack { StreamDeckNeoPanelLayout { touched in model.lastEvent = .neoLeftTouch(touched)