Fix device signing/companion ID, duplicate-device crash, background updates, and add level bubble to Live Activity
- Assign the shared development team to the widget extension and watch complication targets, and correct the watch app's companion bundle identifier so on-device installs succeed after the bundle ID rename to de.s0.fototeddy.VanAligneiOS. - Guard against two ConfiguredDevices sharing a peripheralID (e.g. a double-tapped "Sichern"), which crashed BluetoothManager's Dictionary(uniqueKeysWithValues:) on launch. - Keep Bluetooth running in the background while a Live Activity is active, so lock screen/CarPlay keep receiving fresh readings instead of freezing at the last value before backgrounding. - Render the actual bubble-level graphic (not just a static icon) in the Live Activity's lock screen view and expanded Dynamic Island. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
bfe2b00a5c
commit
9a3b5cb472
@@ -27,6 +27,65 @@ private struct LevelIcon: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verkleinerte Libelle für Sperrbildschirm/CarPlay und die erweiterte
|
||||
/// Dynamic Island – dieselbe Optik wie `LevelBubble` in der App, aber ohne
|
||||
/// deren Abhängigkeit auf `Shared/Models/LevelState.swift`, das in der
|
||||
/// Extension nicht mitkompiliert wird.
|
||||
private struct LevelBubbleGlyph: View {
|
||||
let pitch: Double?
|
||||
let roll: Double?
|
||||
let isLevel: Bool
|
||||
var diameter: CGFloat = 54
|
||||
|
||||
/// Bis zu welcher Neigung die Blase ausschlägt, wie in der App-Libelle.
|
||||
private let range: Double = 6
|
||||
/// Muss mit `LevelState.levelTolerance` übereinstimmen.
|
||||
private let tolerance: Double = 0.5
|
||||
|
||||
private var hasReading: Bool { pitch != nil || roll != nil }
|
||||
|
||||
private var bubbleColor: Color {
|
||||
guard let pitch, let roll else { return .white.opacity(0.4) }
|
||||
let deviation = max(abs(pitch), abs(roll))
|
||||
if deviation <= tolerance { return .green }
|
||||
if deviation <= 2 { return .orange }
|
||||
return .red
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let radius = diameter / 2
|
||||
let bubble = diameter * 0.22
|
||||
let travel = radius - bubble / 2 - 3
|
||||
let offsetX = clamped(roll) / range * travel
|
||||
let offsetY = clamped(pitch) / range * travel
|
||||
let toleranceRadius = max(tolerance / range * travel, bubble * 0.55)
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.1))
|
||||
Circle()
|
||||
.strokeBorder(Color.white.opacity(0.3), lineWidth: 1)
|
||||
Circle()
|
||||
.strokeBorder(isLevel ? Color.green : Color.white.opacity(0.25),
|
||||
lineWidth: isLevel ? 2 : 1)
|
||||
.frame(width: toleranceRadius * 2, height: toleranceRadius * 2)
|
||||
Circle()
|
||||
.fill(bubbleColor)
|
||||
.frame(width: bubble, height: bubble)
|
||||
// Positiver Pitch heisst: Heck steht höher, die Blase wandert
|
||||
// also nach oben – wie bei der echten Wasserwaage in der App.
|
||||
.offset(x: offsetX, y: -offsetY)
|
||||
.opacity(hasReading ? 1 : 0.3)
|
||||
}
|
||||
.frame(width: diameter, height: diameter)
|
||||
}
|
||||
|
||||
private func clamped(_ value: Double?) -> Double {
|
||||
guard let value else { return 0 }
|
||||
return min(max(value, -range), range)
|
||||
}
|
||||
}
|
||||
|
||||
struct VanAligneiOSWidgetLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: LevelActivityAttributes.self) { context in
|
||||
@@ -37,8 +96,8 @@ struct VanAligneiOSWidgetLiveActivity: Widget {
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||
.font(.title2)
|
||||
LevelBubbleGlyph(pitch: context.state.pitch, roll: context.state.roll,
|
||||
isLevel: context.state.isLevel, diameter: 44)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
@@ -73,9 +132,7 @@ private struct LockScreenLevelView: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
LevelIcon(isLevel: state.isLevel, hasReading: state.pitch != nil)
|
||||
.font(.system(size: 32))
|
||||
.frame(width: 40)
|
||||
LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(attributes.deviceName)
|
||||
|
||||
Reference in New Issue
Block a user