Move live activity settings back to root settings and add samples

This commit is contained in:
Bruno Pantaleão
2026-04-23 15:16:24 +02:00
parent 7c852db624
commit dfe2e6c8fd
4 changed files with 147 additions and 154 deletions

View File

@@ -145,21 +145,6 @@ struct DebugView: View {
#endif
}
#if os(iOS) && !targetEnvironment(macCatalyst)
if #available(iOS 17.2, *) {
Section {
NavigationLink {
LiveActivitySettingsView()
} label: {
linkContent(
image: .init(systemSymbol: .livephoto),
title: L10n.LiveActivity.title
)
}
}
}
#endif
criticalSection
if tapsOnCasitaLogo < 10 {

View File

@@ -27,6 +27,7 @@ struct LiveActivitySettingsView: View {
)
statusSection
frequentUpdatesSection
if activities.isEmpty {
Section(L10n.LiveActivity.Section.active) {
@@ -62,15 +63,8 @@ struct LiveActivitySettingsView: View {
}
}
#if DEBUG
debugSection
#endif
privacySection
if #available(iOS 17.2, *) {
frequentUpdatesSection
}
samplesSection
}
.navigationTitle(L10n.LiveActivity.title)
.task { await loadActivities() }
@@ -101,7 +95,7 @@ struct LiveActivitySettingsView: View {
}
}
// MARK: - Debug (DEBUG builds only)
// MARK: - Samples
//
// Two sections: Static (fixed snapshots to verify layout) and Animated (multi-stage
@@ -121,131 +115,139 @@ struct LiveActivitySettingsView: View {
// It does NOT appear on the lock screen. Use a Dynamic Island device or
// simulator (iPhone 14 Pro+) to see it.
#if DEBUG
private var debugSection: some View {
Group {
Section {
// Minimum viable layout only the message field is set.
// Verifies the bare layout renders without icon, progress, or timer.
Button("Plain Message") {
startTestActivity(
tag: "debug-plain",
title: "Home Assistant",
state: .init(message: "Everything looks good at home.")
)
private var samplesSection: some View {
Section {
NavigationLink("Samples") {
List {
staticSamplesSection
animatedSamplesSection
}
// icon = nil code path. Layout must not shift or break when no icon is provided.
// color = nil so the progress bar uses the default HA-blue tint.
// criticalText ("Active") visible in DI compact trailing only.
Button("No Icon · Default Color") {
startTestActivity(
tag: "debug-no-icon",
title: "Script Running",
state: .init(
message: "Irrigation zone 3 is active",
criticalText: "Active",
progress: 35,
progressMax: 100
)
)
}
// Short 60-second countdown with no progress bar.
// Red color communicates urgency. Watch the timer count down in real time.
// Represents automations like alarm arming delays or reminder countdowns.
Button("Alarm · 60 sec Countdown") {
startTestActivity(
tag: "debug-alarm",
title: "Security Alarm",
state: .init(
message: "Motion at back door · Arms in 60 seconds",
criticalText: "60 sec",
chronometer: true,
countdownEnd: Date().addingTimeInterval(60),
icon: "mdi:alarm-light",
color: "#F44336"
)
)
}
// Every ContentState field active at the same time.
// Lock screen shows: icon live countdown progress bar.
// criticalText ("5 min") visible in DI compact trailing only.
// Use this to confirm no layout collisions when all fields are populated.
Button("All Fields · Max Load") {
startTestActivity(
tag: "debug-all",
title: "All Fields",
state: .init(
message: "All content state fields active",
criticalText: "5 min",
progress: 42,
progressMax: 100,
chronometer: true,
countdownEnd: Date().addingTimeInterval(5 * 60),
icon: "mdi:home-assistant",
color: "#03A9F4"
)
)
}
} header: {
Text("Debug · Static")
} footer: {
Text("Fixed state — no updates after start. Good for checking layout at a glance.")
}
Section {
// Progress bar advances through five named stages.
// criticalText tracks the current stage name in the DI compact trailing slot.
// Icon swaps from washing-machine to check-circle on the final update.
// Represents any multi-step appliance cycle automation.
Button("Washing Machine · Stage Labels (~12 s)") { startWashingMachineCycle() }
// Numeric percentage in criticalText updates alongside the progress bar.
// Color shifts from green to yellow-green as the charge nears 100 %.
// Represents any "% complete with time remaining" automation pattern.
Button("EV Charging · Numeric % (~16 s)") { startEVChargingSimulation() }
// The only scenario where both progress (playback position) and a live countdown
// (time remaining in track) are active and updating at the same time.
// Simulates a track change mid-sequence: progress resets, countdown resets.
Button("Media Player · Progress + Timer (~20 s)") { startMediaNowPlaying() }
// Message, criticalText, and icon all change on every update no progress bar.
// Represents automations where the status category itself changes (not just a value).
Button("Package Delivery · All Text Fields (~15 s)") { startPackageJourney() }
// No progress bar state communicated entirely through color and icon.
// Escalates orange (motion) red (person) green (all clear).
// Represents any alert-and-resolve automation pattern.
Button("Security Escalation · Color + Icon (~8 s)") { startSecuritySequence() }
// Cycles through wash stages then calls activity.end() with .default dismissal.
// The only scenario that tests the full lifecycle: start update end.
// After ending, the final "Done" state lingers on the lock screen (up to 4 h).
Button("Dishwasher · Full Lifecycle, Ends Itself (~12 s)") { startDishwasherAutoComplete() }
// Fires 6 updates 2 seconds apart (12 s total).
// On iOS 18 the system enforces ~15 s between rendered updates some will be
// silently dropped. Watch the counter skip values to see the rate limit in action.
// On the simulator and iOS 17 all 6 updates should render.
Button("Rate Limit · 6 Rapid Updates, 2 s Apart (~12 s)") { startRapidUpdateStressTest() }
} header: {
Text("Debug · Animated")
} footer: {
Text(
"Activity updates itself after you tap. Tap, then immediately lock (⌘L) " +
"to watch updates on the lock screen in real time."
)
.navigationTitle("Samples")
}
}
}
#endif
#if DEBUG
private var staticSamplesSection: some View {
Section {
// Minimum viable layout only the message field is set.
// Verifies the bare layout renders without icon, progress, or timer.
Button("Plain Message") {
startTestActivity(
tag: "debug-plain",
title: "Home Assistant",
state: .init(message: "Everything looks good at home.")
)
}
// MARK: - Debug helpers
// icon = nil code path. Layout must not shift or break when no icon is provided.
// color = nil so the progress bar uses the default HA-blue tint.
// criticalText ("Active") visible in DI compact trailing only.
Button("No Icon · Default Color") {
startTestActivity(
tag: "debug-no-icon",
title: "Script Running",
state: .init(
message: "Irrigation zone 3 is active",
criticalText: "Active",
progress: 35,
progressMax: 100
)
)
}
// Short 60-second countdown with no progress bar.
// Red color communicates urgency. Watch the timer count down in real time.
// Represents automations like alarm arming delays or reminder countdowns.
Button("Alarm · 60 sec Countdown") {
startTestActivity(
tag: "debug-alarm",
title: "Security Alarm",
state: .init(
message: "Motion at back door · Arms in 60 seconds",
criticalText: "60 sec",
chronometer: true,
countdownEnd: Date().addingTimeInterval(60),
icon: "mdi:alarm-light",
color: "#F44336"
)
)
}
// Every ContentState field active at the same time.
// Lock screen shows: icon live countdown progress bar.
// criticalText ("5 min") visible in DI compact trailing only.
// Use this to confirm no layout collisions when all fields are populated.
Button("All Fields · Max Load") {
startTestActivity(
tag: "debug-all",
title: "All Fields",
state: .init(
message: "All content state fields active",
criticalText: "5 min",
progress: 42,
progressMax: 100,
chronometer: true,
countdownEnd: Date().addingTimeInterval(5 * 60),
icon: "mdi:home-assistant",
color: "#03A9F4"
)
)
}
} header: {
Text("Sample · Static")
} footer: {
Text("Fixed state — no updates after start. Good for checking layout at a glance.")
}
}
private var animatedSamplesSection: some View {
Section {
// Progress bar advances through five named stages.
// criticalText tracks the current stage name in the DI compact trailing slot.
// Icon swaps from washing-machine to check-circle on the final update.
// Represents any multi-step appliance cycle automation.
Button("Washing Machine · Stage Labels (~12 s)") { startWashingMachineCycle() }
// Numeric percentage in criticalText updates alongside the progress bar.
// Color shifts from green to yellow-green as the charge nears 100 %.
// Represents any "% complete with time remaining" automation pattern.
Button("EV Charging · Numeric % (~16 s)") { startEVChargingSimulation() }
// The only scenario where both progress (playback position) and a live countdown
// (time remaining in track) are active and updating at the same time.
// Simulates a track change mid-sequence: progress resets, countdown resets.
Button("Media Player · Progress + Timer (~20 s)") { startMediaNowPlaying() }
// Message, criticalText, and icon all change on every update no progress bar.
// Represents automations where the status category itself changes (not just a value).
Button("Package Delivery · All Text Fields (~15 s)") { startPackageJourney() }
// No progress bar state communicated entirely through color and icon.
// Escalates orange (motion) red (person) green (all clear).
// Represents any alert-and-resolve automation pattern.
Button("Security Escalation · Color + Icon (~8 s)") { startSecuritySequence() }
// Cycles through wash stages then calls activity.end() with .default dismissal.
// The only scenario that tests the full lifecycle: start update end.
// After ending, the final "Done" state lingers on the lock screen (up to 4 h).
Button("Dishwasher · Full Lifecycle, Ends Itself (~12 s)") { startDishwasherAutoComplete() }
// Fires 6 updates 2 seconds apart (12 s total).
// On iOS 18 the system enforces ~15 s between rendered updates some will be
// silently dropped. Watch the counter skip values to see the rate limit in action.
// On the simulator and iOS 17 all 6 updates should render.
Button("Rate Limit · 6 Rapid Updates, 2 s Apart (~12 s)") { startRapidUpdateStressTest() }
} header: {
Text("Sample · Animated")
} footer: {
Text(
"Activity updates itself after you tap. Tap, then immediately lock (⌘L) " +
"to watch updates on the lock screen in real time."
)
}
}
// MARK: - Sample helpers
/// Starts a single-state activity (no subsequent updates).
private func startTestActivity(tag: String, title: String, state: HALiveActivityAttributes.ContentState) {
@@ -565,8 +567,6 @@ struct LiveActivitySettingsView: View {
)
}
#endif
private var privacySection: some View {
Section {
Label(L10n.LiveActivity.Privacy.message, systemSymbol: .lockShield)

View File

@@ -149,22 +149,30 @@ enum SettingsItem: String, Hashable, CaseIterable {
allCases.filter { item in
// Filter based on platform
#if targetEnvironment(macCatalyst)
if item == .servers || item == .gestures || item == .kiosk || item == .watch || item == .carPlay ||
item == .complications || item == .nfc || item == .help ||
item == .whatsNew {
let hiddenItems: [SettingsItem] = [
.servers,
.gestures,
.kiosk,
.watch,
.carPlay,
.complications,
.nfc,
.help,
.whatsNew,
.liveActivities,
]
if hiddenItems.contains(item) {
return false
}
#endif
// Live Activities are shown in DebugView
if item == .liveActivities {
return false
}
return true
}
}
static var generalItems: [SettingsItem] {
[.general, .gestures, .location, .notifications, .kiosk]
[.general, .gestures, .location, .notifications, .kiosk, .liveActivities]
}
static var integrationItems: [SettingsItem] {

View File

@@ -246,7 +246,7 @@ struct SettingsView: View {
Label {
HStack(spacing: DesignSystem.Spaces.one) {
Text(item.title)
if item == .kiosk {
if [.kiosk, .liveActivities].contains(item) {
LabsLabel()
}
}