Add kiosk mode core infrastructure (PR 1/5)#4422
Add kiosk mode core infrastructure (PR 1/5)#4422nstefanelli wants to merge 41 commits intohome-assistant:mainfrom
Conversation
Implement foundational kiosk mode functionality for wall-mounted displays: Core Components: - KioskModeManager: Central singleton coordinating screen state, brightness, screensaver timing, and settings persistence - KioskSettings: Comprehensive settings model (~100 configurable options) with full Codable support for sync with Home Assistant - KioskConstants: Centralized constants for timing, UI sizing, and defaults Screensaver System: - ScreensaverViewController: UIKit host managing screensaver lifecycle - ClockScreensaverView: SwiftUI clock with large/minimal/digital/analog styles - Pixel shift support for OLED burn-in prevention - Brightness scheduling (day/night modes) Settings & Security: - KioskSettingsView: SwiftUI settings interface with grouped sections - SecretExitGestureView: Configurable corner tap gesture to access settings - Biometric authentication support for exiting kiosk mode Utilities: - IconMapper: MDI to SF Symbols translation - TouchFeedbackManager: Haptic/audio feedback coordination - AnimationUtilities: Shared animation configurations Integration: - WebViewController+Kiosk extension for WebView integration - Callback-based architecture for loose coupling This is PR 1 of 5 implementing kiosk mode. Future PRs will add: - PR 2: HA entity integration and sensors - PR 3: Remote commands via notifications - PR 4: Camera motion/presence detection - PR 5: Photo screensaver and advanced features Discussion: https://github.com/orgs/home-assistant/discussions/2403
Address PR review feedback: Replace all Image(systemName:) and Label(..., systemImage:) with type-safe SFSafeSymbols equivalents using systemSymbol: parameter.
Address PR review feedback requesting test coverage for kiosk mode. Tests added for: - KioskSettings Codable encode/decode roundtrip - DashboardConfig, EntityTrigger, ClockEntityConfig serialization - QuickAction and TriggerAction enum encoding - TimeOfDay comparison logic (isBefore, edge cases) - DeviceOrientation matching (including landscape variants) - Enum display names (ScreensaverMode, ClockStyle, ScreenCorner) - CameraPopupSize parameters - IconMapper MDI to SF Symbol translation Total: 28 new test cases covering core kiosk functionality.
Changes: - Rename files with Kiosk prefix (SecretExitGestureView, ScreensaverViewController, ClockScreensaverView) - Replace NotificationCenter with KioskModeObserver protocol for settings/mode/pixel-shift notifications - Change PR contextualization comments to TODO comments - Localize all hardcoded strings to L10n in KioskSettingsView - Use TouchFeedbackManager instead of inline UIImpactFeedbackGenerator - Add setupKioskMode() call to WebViewController for testing
…nup automatically
- Default biometric/passcode auth to disabled - Skip auth on exit if both auth methods are disabled - Require authentication to access settings while in kiosk mode (if auth is enabled) - Dismiss settings if user cancels authentication
- Wire up kioskPrefersStatusBarHidden/kioskPrefersHomeIndicatorAutoHidden in WebViewController to actually hide status bar when kiosk mode is active - Add 30-second screensaver timeout option to picker - Add corresponding L10n localization string for 30sec timeout
- Fix iPad split view: add .navigationViewStyle(.stack) to settings modal - Fix auth bypass: capture authRequired at init, use persisted settings for exit auth - Fix toggle consistency: always use toggle for enable/disable kiosk mode - Fix auth dismiss: delay auth prompt to let modal fully present
- Remove unnecessary Screen/Screensaver status labels from settings - Add pauseIdleTimer/resumeIdleTimer to prevent screensaver while settings open - Settings view pauses idle timer on appear, resumes on disappear
- Remove Lock Navigation setting (not implemented in this PR) - Add Done button to toolbar for closing settings when opened via secret gesture
- Fix settings modal dismissal bug by removing aggressive dismiss() calls on auth cancellation; add interactive overlay with Cancel/Try Again buttons - Remove TouchFeedbackManager entirely (iPads don't have haptic feedback) - Migrate KioskSettings persistence to GRDB using FetchableRecord/PersistableRecord pattern matching CarPlayConfig and WatchConfig - Consolidate biometric/passcode auth toggles into single "Device Authentication" toggle since iOS always prefers biometric when enrolled - Add KioskSettingsRecord wrapper for JSON blob storage in SQLite - Update localization strings for new auth UI
- Revert unrelated Podfile.lock changes - Restore public modifiers on WebViewController methods - Change settingsJSON column to camelCase convention - Remove objc associated objects, move properties to WebViewController - Remove comments from Localizable.strings - Remove unused IconMapper.swift and AnimationUtilities.swift - Use DesignSystem values for spacing/cornerRadius in KioskConstants - Use Current.date() in KioskClockScreensaverView for testability - Use NavigationStack on iOS 16+ to fix modal dismiss issue
…ructure Upstream restructured WebView/ → Frontend/ and split WebViewController into multiple files. Adapted kiosk mode integration: - Moved WebViewController+Kiosk.swift to Frontend/Extensions/ - Added kiosk stored properties and Combine import to new WebViewController.swift - Added StatusBarForwardingNavigationController to new WebViewWindowController.swift - Fixed status bar hiding (UINavigationController forwarding + statusBarView) - Fixed settings modal dismissal (UINavigationController + onDismiss closure) - Replaced Date() with Current.date() for testability - Removed navigationLockdown placeholder setting - Fixed test references to removed properties (allowBiometricExit → requireDeviceAuthentication) - Removed IconMapper tests (class was removed in earlier review feedback)
SwiftGen needs to be re-run after adding kiosk L10n keys to Localizable.strings.
…bility - Rebuilt project.pbxproj with proper kiosk file references - Updated KioskSettingsTable to conform to new DatabaseTableProtocol (added tableName, definedColumns properties, migrateColumns call) - Added CaseIterable to DatabaseTables.KioskSettings enum
This comment was marked as resolved.
This comment was marked as resolved.
…RDB simplification Architecture: - Create KioskModeHandler with weak WebViewControllerProtocol and DI for KioskModeManager - WebViewController+Kiosk.swift is now a thin delegation layer - Present screensaver as fullscreen VC (.overFullScreen) instead of child view - Remove delay workaround in hideScreensaver — dismiss handles animation - Dismiss screensaver before presenting settings modal GRDB: - Simplify KioskSettingsRecord — remove manual JSON encode/decode - Use .jsonText column type so GRDB handles Codable automatically - Follows same pattern as CarPlayConfig Strip unimplemented features (PR1 only): - KioskSettings: ~1063 → ~230 lines, keep only 25 implemented settings - Remove 10+ unused types (DashboardConfig, EntityTrigger, PhotoSource, etc.) - ScreensaverMode: 7 → 3 cases (blank, dim, clock) - KioskConstants: remove Motion, Audio, Battery, Panel, Shadow enums - KioskModeManager: remove unused callbacks and wake logic - Remove TODO comments and placeholder settings from UI Small fixes: - Extract KioskDateFormatters enum (reusable cached formatters) - Use .background(Color.black.ignoresSafeArea()) instead of ZStack - Use DesignSystem.Spaces.two for spacing - All displayNames use L10n localization keys
…ns when unused - Remove TimeOfDay struct, brightnessScheduleEnabled, dayBrightness, nightBrightness, dayStartTime, nightStartTime settings - Remove isNightTime(), scheduleBrightnessUpdate(), brightnessTimer from KioskModeManager - Remove TimeOfDayPicker and schedule UI from KioskSettingsView - Remove scheduleCheckInterval from KioskConstants - Remove related L10n keys and tests - Simplify brightness to manual-only (applyBrightness) - Clock options section now only shows when screensaver is enabled and mode is clock
What the video demonstrates:
All three screensaver modes (clock, dim, blank) and all four clock styles are functional — video focuses on the core enable/auth/exit flow. I'm happy to pull auth - but it has been working in my testing - can you help to identify where you're seeing an issue? Screensaver.Test.and.Exit.movEnable.and.Auth.Test.mov |
| public enum AppState: String, Codable { | ||
| case active = "active" | ||
| case background = "background" | ||
| } |
There was a problem hiding this comment.
| public enum AppState: String, Codable { | |
| case active = "active" | |
| case background = "background" | |
| } | |
| public enum AppState: String, Codable { | |
| case active | |
| case background | |
| } |
I tested in a real device and it is working, so it must have been something with my simulator, let's keep it, but in the future this PR could have been split even in smaller PRs, it would have helped reviewing and avoid a lot of rework on your side. Regarding the UI of the "block screen" for the authentication (the overlay that prevents user to access kiosk settings), can you make it look like out onboarding permission screens? White (.systemBackground) background and 2 buttons at the bottom? Just to keep some consistency |
|
I've been following this with interest as I'm interested in using an iPad as a home monitor. However, I understood that "Kiosk mode" would mean being able to hide the app's own title bar and menu bar (I guess I mean a fullscreen dashboard). Is this not the case? Any chance this could be added or would that be considered a different feature which should be added as a separate PR? |
|
@davystrong this could be added in an iteration PR, frontend already offer a command to "enter" in kiosk mode where it hides visual elements, but this PR is too big already, so let's merge it first. Agree @nstefanelli ? |
|
Found one more Simulator.Screen.Recording.-.iPad.Air.11-inch.M4.-.2026-03-11.at.10.24.53.mov |
|
When screensaver is starting it has a weird "blink" before the fade-in Simulator.Screen.Recording.-.iPad.Air.11-inch.M4.-.2026-03-11.at.10.32.20.mov |
|
I would also default the secret gesture to bottom right, since it's where most probably no other element would be positioned |
|
Agreed — this is planned for a future PR. The kiosk mode frontend integration will allow compatibility with HA's dashboard kiosk modes (hiding sidebar, header, etc.) as a dedicated iteration. |
- Merge KioskModeHandler into KioskModeManager per maintainer request; manager now holds weak WebViewControllerProtocol reference directly and handles screensaver presentation, UI lockdown, and settings modal - Replace custom 42KIOSK* pbxproj IDs with standard 24-char hex IDs - Rename loadSettings/saveSettings to settings()/save() per convention - Remove doc comment from KioskSettingsRecord (redundant with table def) - Use fetchOne(db, key:) for explicit primary key lookup - Remove explicit raw values from ScreenState, AppState, ScreenCorner - Remove .ignoresSafeArea() from clock view (parent VC handles it) - Fix screensaver blink: configure content before modal presentation - Use presentOverlayController for consistent settings sheet sizing - Convert authRequired from @State to computed property (fixes stale state) - Restyle auth gate overlay to match HA onboarding pattern - Move clock options into screensaver section - Use setLocalizedDateFormatFromTemplate for locale-aware date formatting - Use Current.date() for lastActivityTime init (testability) - Conditional isIdleTimerDisabled restore in disableKioskMode - Handle preventAutoLock and screensaverEnabled in settingsDidChange - Tests assert against L10n values instead of hardcoded English strings - Remove dead hide() method and unused cancellables - Default secret exit gesture corner changed to bottomRight



Summary
Reopening kiosk mode PR (previously #4218) after addressing all review feedback from @bgoncal. Apologies for the delay — some family matters pulled me away, but I'm excited to get this implemented and am fully committed to seeing it through.
This PR adds the foundational infrastructure for kiosk mode — a feature designed for wall-mounted iPad displays running Home Assistant dashboards.
What's included:
Changes since #4218 (all review feedback addressed):
Date()withCurrent.date()for testabilityKioskTesting Instructions
Previous review context
All feedback from @bgoncal in #4218 has been addressed. See that PR for full discussion history.
Test plan
🤖 Generated with Claude Code