Add Live Activity for level sensor (surfaces on CarPlay automatically)
Replaces the ActivityKit boilerplate with a real lock screen/Dynamic Island UI driven by live pitch/roll readings, toggled from the leveling device's detail screen. Since iOS 17 shows any running Live Activity on CarPlay's dashboard without a dedicated CarPlay app entitlement, this covers the requested CarPlay use case without one. Also fixes the widget extension never being embedded into the app (missing Embed Foundation Extensions build phase and target dependency), which meant no widget or Live Activity could ever have rendered regardless of code changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
da64b1b6bd
commit
bfe2b00a5c
@@ -9,72 +9,129 @@ import ActivityKit
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct VanAligneiOSWidgetAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
// Dynamic stateful properties about your activity go here!
|
||||
var emoji: String
|
||||
}
|
||||
/// Formatiert Neigungswerte wie in der App – °, eine Nachkommastelle, "–"
|
||||
/// wenn kein Messwert da ist. Eigene Kopie, da die Extension nicht von
|
||||
/// `Shared/Models/Metric.swift` abhängt.
|
||||
private func degreesText(_ value: Double?) -> String {
|
||||
guard let value else { return "–" }
|
||||
return String(format: "%.1f°", value)
|
||||
}
|
||||
|
||||
// Fixed non-changing properties about your activity go here!
|
||||
var name: String
|
||||
private struct LevelIcon: View {
|
||||
let isLevel: Bool
|
||||
let hasReading: Bool
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: hasReading ? (isLevel ? "checkmark.circle.fill" : "level") : "level")
|
||||
.foregroundStyle(hasReading ? (isLevel ? .green : .orange) : .secondary)
|
||||
}
|
||||
}
|
||||
|
||||
struct VanAligneiOSWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: VanAligneiOSWidgetAttributes.self) { context in
|
||||
// Lock screen/banner UI goes here
|
||||
VStack {
|
||||
Text("Hello \(context.state.emoji)")
|
||||
}
|
||||
.activityBackgroundTint(Color.cyan)
|
||||
.activitySystemActionForegroundColor(Color.black)
|
||||
ActivityConfiguration(for: LevelActivityAttributes.self) { context in
|
||||
LockScreenLevelView(attributes: context.attributes, state: context.state)
|
||||
.activityBackgroundTint(Color(white: 0.08))
|
||||
.activitySystemActionForegroundColor(.white)
|
||||
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
// Expanded UI goes here. Compose the expanded UI through
|
||||
// various regions, like leading/trailing/center/bottom
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text("Leading")
|
||||
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||
.font(.title2)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
Text("Trailing")
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(degreesText(context.state.pitch)).monospacedDigit()
|
||||
Text(degreesText(context.state.roll)).monospacedDigit()
|
||||
}
|
||||
.font(.headline)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
Text("Bottom \(context.state.emoji)")
|
||||
// more content
|
||||
Text(context.state.instruction ?? context.attributes.deviceName)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(context.state.isLevel ? .green : .primary)
|
||||
}
|
||||
} compactLeading: {
|
||||
Text("L")
|
||||
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||
} compactTrailing: {
|
||||
Text("T \(context.state.emoji)")
|
||||
Text(degreesText(context.state.pitch)).monospacedDigit()
|
||||
} minimal: {
|
||||
Text(context.state.emoji)
|
||||
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||
}
|
||||
.widgetURL(URL(string: "http://www.apple.com"))
|
||||
.keylineTint(Color.red)
|
||||
.keylineTint(context.state.isLevel ? .green : .orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VanAligneiOSWidgetAttributes {
|
||||
fileprivate static var preview: VanAligneiOSWidgetAttributes {
|
||||
VanAligneiOSWidgetAttributes(name: "World")
|
||||
/// Sperrbildschirm- und Banner-Ansicht. Dieselbe Ansicht landet automatisch
|
||||
/// auch im CarPlay-Dashboard, sobald das iPhone verbunden ist – ein eigenes
|
||||
/// CarPlay-App-Target ist dafür nicht nötig.
|
||||
private struct LockScreenLevelView: View {
|
||||
let attributes: LevelActivityAttributes
|
||||
let state: LevelActivityAttributes.ContentState
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
LevelIcon(isLevel: state.isLevel, hasReading: state.pitch != nil)
|
||||
.font(.system(size: 32))
|
||||
.frame(width: 40)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(attributes.deviceName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(state.instruction ?? "Keine Messwerte")
|
||||
.font(.headline)
|
||||
.foregroundStyle(state.isLevel ? .green : .white)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
reading("Längs", state.pitch)
|
||||
reading("Quer", state.roll)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
private func reading(_ title: String, _ value: Double?) -> some View {
|
||||
HStack(spacing: 6) {
|
||||
Text(title)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(degreesText(value))
|
||||
.font(.subheadline.monospacedDigit())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VanAligneiOSWidgetAttributes.ContentState {
|
||||
fileprivate static var smiley: VanAligneiOSWidgetAttributes.ContentState {
|
||||
VanAligneiOSWidgetAttributes.ContentState(emoji: "😀")
|
||||
}
|
||||
|
||||
fileprivate static var starEyes: VanAligneiOSWidgetAttributes.ContentState {
|
||||
VanAligneiOSWidgetAttributes.ContentState(emoji: "🤩")
|
||||
}
|
||||
extension LevelActivityAttributes {
|
||||
fileprivate static var preview: LevelActivityAttributes {
|
||||
LevelActivityAttributes(deviceName: "Nivellierung")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Notification", as: .content, using: VanAligneiOSWidgetAttributes.preview) {
|
||||
extension LevelActivityAttributes.ContentState {
|
||||
fileprivate static var level: LevelActivityAttributes.ContentState {
|
||||
LevelActivityAttributes.ContentState(pitch: 0.1, roll: -0.2, isLevel: true,
|
||||
instruction: "Steht eben", isCalibrated: true,
|
||||
updatedAt: .now)
|
||||
}
|
||||
|
||||
fileprivate static var tilted: LevelActivityAttributes.ContentState {
|
||||
LevelActivityAttributes.ContentState(pitch: 2.4, roll: -1.1, isLevel: false,
|
||||
instruction: "Heck steht höher, links steht höher",
|
||||
isCalibrated: true, updatedAt: .now)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Notification", as: .content, using: LevelActivityAttributes.preview) {
|
||||
VanAligneiOSWidgetLiveActivity()
|
||||
} contentStates: {
|
||||
VanAligneiOSWidgetAttributes.ContentState.smiley
|
||||
VanAligneiOSWidgetAttributes.ContentState.starEyes
|
||||
LevelActivityAttributes.ContentState.level
|
||||
LevelActivityAttributes.ContentState.tilted
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user