feat(apple/widgets): Xcode canvas previews for the Live Activity + Hosts widget
Every surface renders in the canvas without running the app or starting a real Activity: Lock Screen banner across all four session stages (streaming/backgrounded/reconnecting/ended), Dynamic Island expanded/compact/minimal, and the Hosts widget in all four families with sample hosts (plus the empty state one timeline click away). Sample state is fileprivate to the widget files — never in PunktfunkShared. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -184,3 +184,47 @@ private struct EmptyHostView: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews (Xcode canvas)
|
||||||
|
//
|
||||||
|
// Select the PunktfunkWidgetsExtension scheme and open the canvas (⌥⌘↩). The widget
|
||||||
|
// `#Preview(as:widget:timeline:)` form feeds sample entries directly — the App-Group store is
|
||||||
|
// never read, so the canvas works without a paired device or saved hosts. The small preview's
|
||||||
|
// second entry shows the empty state one timeline click away.
|
||||||
|
|
||||||
|
private let previewHosts: [StoredHost] = [
|
||||||
|
StoredHost(
|
||||||
|
name: "Studio", address: "192.168.1.20",
|
||||||
|
lastConnected: .now.addingTimeInterval(-40 * 60)),
|
||||||
|
StoredHost(
|
||||||
|
name: "Living Room", address: "192.168.1.30",
|
||||||
|
lastConnected: .now.addingTimeInterval(-26 * 3600)),
|
||||||
|
StoredHost(
|
||||||
|
name: "Workstation", address: "10.0.0.5",
|
||||||
|
lastConnected: .now.addingTimeInterval(-6 * 86400)),
|
||||||
|
]
|
||||||
|
|
||||||
|
#Preview("Small", as: .systemSmall) {
|
||||||
|
HostsWidget()
|
||||||
|
} timeline: {
|
||||||
|
HostsEntry(date: .now, hosts: previewHosts)
|
||||||
|
HostsEntry(date: .now, hosts: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Medium", as: .systemMedium) {
|
||||||
|
HostsWidget()
|
||||||
|
} timeline: {
|
||||||
|
HostsEntry(date: .now, hosts: previewHosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Lock Screen circular", as: .accessoryCircular) {
|
||||||
|
HostsWidget()
|
||||||
|
} timeline: {
|
||||||
|
HostsEntry(date: .now, hosts: previewHosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Lock Screen rectangular", as: .accessoryRectangular) {
|
||||||
|
HostsWidget()
|
||||||
|
} timeline: {
|
||||||
|
HostsEntry(date: .now, hosts: previewHosts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -138,3 +138,79 @@ private struct EndButton: View {
|
|||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews (Xcode canvas)
|
||||||
|
//
|
||||||
|
// Select the PunktfunkWidgetsExtension scheme and open the canvas (⌥⌘↩) — the activity
|
||||||
|
// `#Preview(as:using:)` form renders every surface WITHOUT running the app or starting a real
|
||||||
|
// Activity: `.content` is the Lock Screen banner, `.dynamicIsland(.expanded/.compact/.minimal)`
|
||||||
|
// the island states (canvas device must be a Dynamic Island phone for those). Each listed
|
||||||
|
// content state becomes a frame in the canvas timeline strip, so all four session stages are one
|
||||||
|
// click apart. Sample state lives here (fileprivate), never in PunktfunkShared.
|
||||||
|
|
||||||
|
extension PunktfunkSessionAttributes {
|
||||||
|
fileprivate static var preview: PunktfunkSessionAttributes {
|
||||||
|
PunktfunkSessionAttributes(hostID: UUID(), hostName: "Studio", launchTitle: "Hades II")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PunktfunkSessionAttributes.ContentState {
|
||||||
|
fileprivate static var streaming: Self {
|
||||||
|
.init(
|
||||||
|
stage: .streaming, startedAt: .now.addingTimeInterval(-754),
|
||||||
|
modeLine: "2752×2064 @120 · HEVC · HDR", latencyMs: 8, mbps: 84.2)
|
||||||
|
}
|
||||||
|
fileprivate static var backgrounded: Self {
|
||||||
|
.init(
|
||||||
|
stage: .background, startedAt: .now.addingTimeInterval(-1975),
|
||||||
|
modeLine: "2752×2064 @120 · HEVC · HDR",
|
||||||
|
backgroundDeadline: .now.addingTimeInterval(9 * 60))
|
||||||
|
}
|
||||||
|
fileprivate static var reconnecting: Self {
|
||||||
|
.init(
|
||||||
|
stage: .reconnecting, startedAt: .now.addingTimeInterval(-754),
|
||||||
|
modeLine: "2752×2064 @120 · HEVC · HDR")
|
||||||
|
}
|
||||||
|
fileprivate static var ended: Self {
|
||||||
|
.init(
|
||||||
|
stage: .ending, startedAt: .now.addingTimeInterval(-3541),
|
||||||
|
modeLine: "2752×2064 @120 · HEVC · HDR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Lock Screen", as: .content, using: PunktfunkSessionAttributes.preview) {
|
||||||
|
PunktfunkSessionLiveActivity()
|
||||||
|
} contentStates: {
|
||||||
|
PunktfunkSessionAttributes.ContentState.streaming
|
||||||
|
PunktfunkSessionAttributes.ContentState.backgrounded
|
||||||
|
PunktfunkSessionAttributes.ContentState.reconnecting
|
||||||
|
PunktfunkSessionAttributes.ContentState.ended
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(
|
||||||
|
"Island expanded", as: .dynamicIsland(.expanded),
|
||||||
|
using: PunktfunkSessionAttributes.preview
|
||||||
|
) {
|
||||||
|
PunktfunkSessionLiveActivity()
|
||||||
|
} contentStates: {
|
||||||
|
PunktfunkSessionAttributes.ContentState.streaming
|
||||||
|
PunktfunkSessionAttributes.ContentState.backgrounded
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(
|
||||||
|
"Island compact", as: .dynamicIsland(.compact),
|
||||||
|
using: PunktfunkSessionAttributes.preview
|
||||||
|
) {
|
||||||
|
PunktfunkSessionLiveActivity()
|
||||||
|
} contentStates: {
|
||||||
|
PunktfunkSessionAttributes.ContentState.streaming
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(
|
||||||
|
"Island minimal", as: .dynamicIsland(.minimal),
|
||||||
|
using: PunktfunkSessionAttributes.preview
|
||||||
|
) {
|
||||||
|
PunktfunkSessionLiveActivity()
|
||||||
|
} contentStates: {
|
||||||
|
PunktfunkSessionAttributes.ContentState.streaming
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user