Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/macro #22

Merged
merged 17 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Example/Example App/StreamDeckLayoutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ struct StreamDeckLayoutView: View {
}
}

struct NumberDisplayKey: StreamDeckView {
@Environment(\.streamDeckViewContext) var context
@StreamDeckView
struct NumberDisplayKey {
@State var isPressed: Bool = false

var emoji: String { emojis[context.index] }
var emoji: String { emojis[viewIndex] }

var streamDeckBody: some View {
StreamDeckKeyView { isPressed in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "509.1.1"
}
}
],
"version" : 2
}
22 changes: 18 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "StreamDeckKit",
platforms: [.iOS(.v17)],
platforms: [.iOS(.v17), .macOS(.v10_15)],
products: [
.library(
name: "StreamDeckKit",
Expand All @@ -20,7 +21,11 @@ let package = Package(
.package(
url: "https://github.com/pointfreeco/swift-snapshot-testing",
from: "1.12.0"
)
),
.package(
url: "https://github.com/apple/swift-syntax.git",
from: "509.0.0"
)
],
targets: [
.target(
Expand All @@ -30,18 +35,27 @@ let package = Package(
),
.target(
name: "StreamDeckKit",
dependencies: ["StreamDeckCApi"]
dependencies: ["StreamDeckCApi", "StreamDeckMacros"]
),
.target(
name: "StreamDeckCApi",
linkerSettings: [.linkedFramework("IOKit")]
),
.macro(
name: "StreamDeckMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.testTarget(
name: "StreamDeckSDKTests",
dependencies: [
"StreamDeckKit",
"StreamDeckMacros",
"StreamDeckSimulator",
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
]
)
]
Expand Down
4 changes: 2 additions & 2 deletions Sources/StreamDeckKit/Layout/Environment+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import SwiftUI

public struct StreamDeckViewContextKey: EnvironmentKey {
struct StreamDeckViewContextKey: EnvironmentKey {

public static var defaultValue: StreamDeckViewContext = .init(
static var defaultValue: StreamDeckViewContext = .init(
device: StreamDeck(
client: StreamDeckClientDummy(),
info: .init(),
Expand Down
19 changes: 0 additions & 19 deletions Sources/StreamDeckKit/Layout/StreamDeck+Layout.swift

This file was deleted.

21 changes: 5 additions & 16 deletions Sources/StreamDeckKit/Layout/StreamDeckLayoutRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,17 @@ final class StreamDeckLayoutRenderer {

private var cancellable: AnyCancellable?

private let imageSubject = PassthroughSubject<UIImage, Never>()

public var imagePublisher: AnyPublisher<UIImage, Never> {
imageSubject.eraseToAnyPublisher()
}

private var dirtyViews = [DirtyMarker]()

public init() {
}
init() {}

@MainActor
public init<Content: View>(content: Content, device: StreamDeck) {
init<Content: View>(content: Content, device: StreamDeck) {
render(content, on: device)
}

@MainActor
public func render<Content: View>(_ content: Content, on device: StreamDeck) {
func render<Content: View>(_ content: Content, on device: StreamDeck) {
cancellable?.cancel()

dirtyViews = .init([.screen])
Expand All @@ -39,9 +32,7 @@ final class StreamDeckLayoutRenderer {
device: device,
dirtyMarker: .screen,
size: device.capabilities.screenSize ?? .zero
) { [weak self] in
self?.updateRequired($0)
}
)

let view = content
.environment(\.streamDeckViewContext, context)
Expand All @@ -58,7 +49,7 @@ final class StreamDeckLayoutRenderer {
}
}

public func stop() {
func stop() {
cancellable?.cancel()
}

Expand All @@ -72,8 +63,6 @@ final class StreamDeckLayoutRenderer {
log("Layout did change")
let caps = device.capabilities

imageSubject.send(image)

guard !dirtyViews.isEmpty else {
log("no dirty views")
return
Expand Down
38 changes: 7 additions & 31 deletions Sources/StreamDeckKit/Layout/StreamDeckViewContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,39 @@ import Foundation

/// Provides information about the current context (screen, key-area, key, window, dial) in SwiftUI environments.
///
/// You can access the current context via the environment like this:
/// ```swift
/// @Environment(\.streamDeckViewContext) var context
/// ```
/// This is used internally by the ``StreamDeckView`` macro and the ``StreamDeckLayout`` system.
public struct StreamDeckViewContext {

private final class IDGenerator {
private var _id: UInt64 = 0
var next: UInt64 {
if _id == UInt64.max {
_id = 0
}
_id += 1
return _id
}
}

typealias DirtyHandler = (DirtyMarker) -> Void

/// The Stream Deck device object.
public let device: StreamDeck

private(set) var dirtyMarker: DirtyMarker

/// The size of the current drawing area.
///
/// Depending on if you access this value in a key area, a window or a key.
public private(set) var size: CGSize
public var size: CGSize

/// The index of an input element.
///
/// The value will be valid, when the current drawing area represents an input element like a key. Otherwise it will be `-1`.
public private(set) var index: Int

private let onDirty: DirtyHandler?
public var index: Int

private let idGenerator = IDGenerator()

public var nextID: UInt64 { idGenerator.next }
private(set) var dirtyMarker: DirtyMarker

init(
device: StreamDeck,
dirtyMarker: DirtyMarker,
size: CGSize,
index: Int = -1,
onDirty: StreamDeckViewContext.DirtyHandler? = nil
index: Int = -1
) {
self.device = device
self.dirtyMarker = dirtyMarker
self.size = size
self.index = index
self.onDirty = onDirty
}

@MainActor
func updateRequired() {
onDirty?(dirtyMarker)
public func updateRequired() {
device.renderer.updateRequired(dirtyMarker)
}

func with(dirtyMarker: DirtyMarker, size: CGSize, index: Int) -> Self {
Expand Down
13 changes: 13 additions & 0 deletions Sources/StreamDeckKit/Models/StreamDeck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Combine
import Foundation
import SwiftUI
import UIKit

/// An object that represents a physical Stream Deck device.
Expand All @@ -27,6 +28,8 @@ public final class StreamDeck {
var operationsTask: Task<Void, Never>?
var didSetInputEventHandler = false

let renderer = StreamDeckLayoutRenderer()

private let inputEventsSubject = PassthroughSubject<InputEvent, Never>()

/// A publisher of user input events.
Expand Down Expand Up @@ -68,7 +71,10 @@ public final class StreamDeck {
self.client = client
self.info = info
self.capabilities = capabilities

startOperationTask()

onClose(renderer.stop)
}

/// Check if the hardware supports the given feature.
Expand Down Expand Up @@ -174,4 +180,11 @@ public final class StreamDeck {
enqueueOperation(.showLogo)
}

/// Render the provided content on this device as long as the device remains open.
/// - Parameter content: The SwiftUI view to render on this device.
@MainActor
func render<Content: View>(_ content: Content) {
renderer.render(content, on: self)
}

}
56 changes: 34 additions & 22 deletions Sources/StreamDeckKit/Views/StreamDeckView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,49 @@

import SwiftUI

private var _id: UInt64 = 0

public var _nextID: UInt64 {
if _id == UInt64.max {
_id = 0
}
_id += 1
return _id
}

/// Protocol for views rendered on StreamDeck.
/// This automatically tells StreamDeckLayout that the drawing area of this view needs to be updated on the device.
///
/// - Note: Use this implicitly by applying the ``StreamDeckView()`` macro.
public protocol StreamDeckView: View {
/// The type of view representing the streamDeckBody of this view.
associatedtype StreamDeckBody: View
/// The content of the view.
@MainActor @ViewBuilder var streamDeckBody: Self.StreamDeckBody { get }
}

/// Defines and implements conformance of the StreamDeckView protocol.
///
/// This macro adds Stream Deck context information and state tracking. Enabling you to to handle different devices and keys.
///
/// ```swift
/// struct NumberDisplayKey: StreamDeckView {
/// @Environment(\.streamDeckViewContext) var context
/// @StreamDeckView
/// struct NumberDisplayKey {
/// @State var isPressed: Bool = false
///
/// var body: some View {
/// var streamDeckBody: some View {
/// StreamDeckKeyView { isPressed in
/// // Changing state will trigger a re-render on Stream Deck
/// self.isPressed = isPressed
/// } content: {
/// isPressed ? Color.orange : Color.clear
/// ZStack {
/// isPressed ? Color.orange : Color.clear
/// // Show the current key index
/// Text("\(viewIndex)")
/// }
/// }
/// }
/// }
/// ```
public protocol StreamDeckView: View {
associatedtype Content: View

var context: StreamDeckViewContext { get }

@MainActor @ViewBuilder var streamDeckBody: Self.Content { get }
}

extension StreamDeckView {
@MainActor
public var body: some View {
streamDeckBody
.onChange(of: context.nextID) { _, _ in
context.updateRequired()
}
}
}
@attached(extension, conformances: StreamDeckView)
@attached(member, names: named(_$streamDeckViewContext), named(body), named(streamDeck), named(viewSize), named(viewIndex))
public macro StreamDeckView() = #externalMacro(module: "StreamDeckMacros", type: "StreamDeckViewMacro")
Loading