Skip to content

Commit 69b0743

Browse files
committed
Address maintainer review: handler pattern, fullscreen screensaver, GRDB 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
1 parent 7786ec9 commit 69b0743

File tree

12 files changed

+295
-1476
lines changed

12 files changed

+295
-1476
lines changed

HomeAssistant.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,7 @@
12141214
42KIOSK0002000000000009 /* KioskSettingsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42KIOSK0001000000000009 /* KioskSettingsTable.swift */; };
12151215
42KIOSK000200000000000B /* KioskSettings.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42KIOSK000100000000000A /* KioskSettings.test.swift */; };
12161216
42KIOSK000200000000000C /* KioskSettingsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42KIOSK0001000000000009 /* KioskSettingsTable.swift */; };
1217+
42KIOSK000200000000000D /* KioskModeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42KIOSK000100000000000D /* KioskModeHandler.swift */; };
12171218
465BC1EE2D8DB87A00A30A60 /* LocationHistoryEntryListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465BC1ED2D8DB87A00A30A60 /* LocationHistoryEntryListItemView.swift */; };
12181219
4697F4BF2D8A3F7500C5C467 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4697F4BE2D8A3F7500C5C467 /* XCTest.framework */; platformFilter = ios; };
12191220
4697F4C12D8A416400C5C467 /* SharedTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46C62B882D8A3687002C0001 /* SharedTesting.framework */; };
@@ -3017,6 +3018,7 @@
30173018
42KIOSK0001000000000008 /* WebViewController+Kiosk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebViewController+Kiosk.swift"; sourceTree = "<group>"; };
30183019
42KIOSK0001000000000009 /* KioskSettingsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskSettingsTable.swift; sourceTree = "<group>"; };
30193020
42KIOSK000100000000000A /* KioskSettings.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskSettings.test.swift; sourceTree = "<group>"; };
3021+
42KIOSK000100000000000D /* KioskModeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskModeHandler.swift; sourceTree = "<group>"; };
30203022
442182FC55CEB695729D80CA /* Pods-watchOS-Shared-watchOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-watchOS-Shared-watchOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-watchOS-Shared-watchOS/Pods-watchOS-Shared-watchOS.release.xcconfig"; sourceTree = "<group>"; };
30213023
465BC1ED2D8DB87A00A30A60 /* LocationHistoryEntryListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHistoryEntryListItemView.swift; sourceTree = "<group>"; };
30223024
4697F4BE2D8A3F7500C5C467 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
@@ -6376,6 +6378,7 @@
63766378
isa = PBXGroup;
63776379
children = (
63786380
42KIOSK0001000000000003 /* KioskConstants.swift */,
6381+
42KIOSK000100000000000D /* KioskModeHandler.swift */,
63796382
42KIOSK0001000000000001 /* KioskModeManager.swift */,
63806383
42KIOSK0001000000000002 /* KioskSettings.swift */,
63816384
42KIOSK0003000000000004 /* Overlay */,
@@ -9523,6 +9526,7 @@
95239526
42KIOSK0002000000000006 /* KioskClockScreensaverView.swift in Sources */,
95249527
42KIOSK0002000000000007 /* KioskSecretExitGestureView.swift in Sources */,
95259528
42KIOSK0002000000000008 /* WebViewController+Kiosk.swift in Sources */,
9529+
42KIOSK000200000000000D /* KioskModeHandler.swift in Sources */,
95269530
);
95279531
runOnlyForDeploymentPostprocessing = 0;
95289532
};
Lines changed: 7 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import Combine
21
import Shared
3-
import SwiftUI
42
import UIKit
53

64
// MARK: - Kiosk Mode Extension
@@ -9,221 +7,28 @@ extension WebViewController {
97
/// Setup kiosk mode integration with KioskModeManager
108
/// Call this from viewDidLoad
119
func setupKioskMode() {
12-
let manager = KioskModeManager.shared
13-
14-
// Wire up callbacks from KioskModeManager
15-
manager.onNavigate = { [weak self] path in
16-
self?.navigateToKioskPath(path)
17-
}
18-
19-
manager.onRefresh = { [weak self] in
20-
self?.refresh()
21-
}
22-
23-
manager.onKioskModeChange = { [weak self] enabled in
24-
self?.updateKioskModeLockdown(enabled: enabled)
25-
}
26-
27-
manager.onShowScreensaver = { [weak self] mode in
28-
self?.showScreensaver(mode: mode)
29-
}
30-
31-
manager.onHideScreensaver = { [weak self] in
32-
self?.hideScreensaver()
33-
}
34-
35-
// Observe kiosk mode and settings changes using Combine (auto-cleanup on dealloc)
36-
var cancellables = Set<AnyCancellable>()
37-
38-
manager.$isKioskModeActive
39-
.receive(on: DispatchQueue.main)
40-
.sink { [weak self] _ in
41-
self?.kioskModeDidChange()
42-
}
43-
.store(in: &cancellables)
44-
45-
manager.$settings
46-
.receive(on: DispatchQueue.main)
47-
.sink { [weak self] _ in
48-
self?.kioskSettingsDidChange()
49-
}
50-
.store(in: &cancellables)
51-
52-
kioskCancellables = cancellables
53-
54-
// Setup the screensaver
55-
setupScreensaver()
56-
57-
// Setup secret exit gesture (only when not showing screensaver)
58-
setupSecretExitGesture()
59-
60-
// Apply initial state if already in kiosk mode
61-
if manager.isKioskModeActive {
62-
updateKioskModeLockdown(enabled: true)
63-
}
64-
}
65-
66-
// MARK: - Screensaver
67-
68-
private func setupScreensaver() {
69-
let controller = KioskScreensaverViewController()
70-
screensaverController = controller
71-
72-
// Forward the callback for showing settings
73-
controller.onShowSettings = { [weak self] in
74-
self?.showKioskSettings()
75-
}
76-
77-
addChild(controller)
78-
view.addSubview(controller.view)
79-
controller.view.translatesAutoresizingMaskIntoConstraints = false
80-
81-
NSLayoutConstraint.activate([
82-
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
83-
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
84-
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
85-
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
86-
])
87-
88-
controller.didMove(toParent: self)
89-
controller.view.isHidden = true
90-
}
91-
92-
private func setupSecretExitGesture() {
93-
let controller = KioskSecretExitGestureViewController()
94-
secretExitGestureController = controller
95-
96-
controller.onShowSettings = { [weak self] in
97-
self?.showKioskSettings()
98-
}
99-
100-
addChild(controller)
101-
view.addSubview(controller.view)
102-
controller.view.translatesAutoresizingMaskIntoConstraints = false
103-
104-
NSLayoutConstraint.activate([
105-
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
106-
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
107-
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
108-
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
109-
])
110-
111-
controller.didMove(toParent: self)
112-
}
113-
114-
private func showScreensaver(mode: ScreensaverMode) {
115-
guard let controller = screensaverController else { return }
116-
117-
Current.Log.info("Showing screensaver: \(mode.rawValue)")
118-
119-
controller.view.isHidden = false
120-
view.bringSubviewToFront(controller.view)
121-
controller.show(mode: mode)
122-
}
123-
124-
private func hideScreensaver() {
125-
guard let controller = screensaverController else { return }
126-
127-
Current.Log.info("Hiding screensaver")
128-
controller.hide()
129-
130-
// Delay hiding the view until the animation completes
131-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
132-
controller.view.isHidden = true
133-
}
134-
}
135-
136-
// MARK: - Navigation
137-
138-
private func navigateToKioskPath(_ path: String) {
139-
Current.Log.info("Kiosk navigating to: \(path)")
140-
141-
// Use the existing navigateToPath method
142-
navigateToPath(path: path)
143-
}
144-
145-
// MARK: - UI Lockdown
146-
147-
private func updateKioskModeLockdown(enabled: Bool) {
148-
let settings = KioskModeManager.shared.settings
149-
150-
// Update iOS system status bar and home indicator visibility
151-
// The navigation controller must be updated first so it re-queries its child
152-
if let navController = navigationController {
153-
navController.setNeedsStatusBarAppearanceUpdate()
154-
navController.setNeedsUpdateOfHomeIndicatorAutoHidden()
155-
}
156-
setNeedsStatusBarAppearanceUpdate()
157-
setNeedsUpdateOfHomeIndicatorAutoHidden()
158-
159-
// Hide/show the custom status bar background view
160-
if let statusBarView {
161-
let shouldHide = enabled && settings.hideStatusBar
162-
statusBarView.isHidden = shouldHide
163-
}
10+
let handler = KioskModeHandler(webViewController: self)
11+
kioskHandler = handler
12+
handler.setup()
16413
}
16514

16615
// MARK: - Status Bar & Home Indicator
16716

16817
/// Override in WebViewController to check kiosk mode
16918
var kioskPrefersStatusBarHidden: Bool {
170-
let manager = KioskModeManager.shared
171-
return manager.isKioskModeActive && manager.settings.hideStatusBar
19+
kioskHandler?.prefersStatusBarHidden ?? false
17220
}
17321

17422
/// Override in WebViewController to check kiosk mode
17523
var kioskPrefersHomeIndicatorAutoHidden: Bool {
176-
KioskModeManager.shared.isKioskModeActive
177-
}
178-
179-
// MARK: - Settings
180-
181-
private func showKioskSettings() {
182-
Current.Log.info("Showing kiosk settings")
183-
184-
// Use UINavigationController to avoid SwiftUI NavigationView dismissal bugs on iOS 15
185-
// Pass an explicit dismiss closure since SwiftUI's @Environment(\.dismiss) doesn't work
186-
// reliably when UIHostingController is embedded in UINavigationController presented via UIKit
187-
let settingsView = KioskSettingsView(onDismiss: { [weak self] in
188-
self?.dismiss(animated: true) { [weak self] in
189-
self?.refreshStatusBarAppearance()
190-
}
191-
})
192-
let hostingController = UIHostingController(rootView: settingsView)
193-
let navController = UINavigationController(rootViewController: hostingController)
194-
navController.modalPresentationStyle = .pageSheet
195-
present(navController, animated: true)
196-
}
197-
198-
/// Force a complete status bar appearance refresh after modal dismissal
199-
private func refreshStatusBarAppearance() {
200-
navigationController?.setNeedsStatusBarAppearanceUpdate()
201-
setNeedsStatusBarAppearanceUpdate()
202-
navigationController?.setNeedsUpdateOfHomeIndicatorAutoHidden()
203-
setNeedsUpdateOfHomeIndicatorAutoHidden()
204-
}
205-
206-
// MARK: - Observers
207-
208-
private func kioskModeDidChange() {
209-
let manager = KioskModeManager.shared
210-
Current.Log.info("Kiosk mode changed: \(manager.isKioskModeActive)")
211-
212-
updateKioskModeLockdown(enabled: manager.isKioskModeActive)
213-
}
214-
215-
private func kioskSettingsDidChange() {
216-
// Re-apply lockdown settings in case they changed
217-
let manager = KioskModeManager.shared
218-
if manager.isKioskModeActive {
219-
updateKioskModeLockdown(enabled: true)
220-
}
24+
kioskHandler?.prefersHomeIndicatorAutoHidden ?? false
22125
}
22226

22327
// MARK: - Touch Handling
22428

22529
/// Call this when user touches the screen to record activity
30+
/// Required because WKWebView consumes touch events before UIKit idle detection
22631
func recordKioskActivity() {
227-
KioskModeManager.shared.recordActivity(source: "touch")
32+
kioskHandler?.recordActivity()
22833
}
22934
}

Sources/App/Frontend/WebView/WebViewController.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
6969

7070
// MARK: - Kiosk Mode Properties
7171

72-
var screensaverController: KioskScreensaverViewController?
73-
var secretExitGestureController: KioskSecretExitGestureViewController?
74-
var kioskCancellables = Set<AnyCancellable>()
72+
var kioskHandler: KioskModeHandler?
7573

7674
var underlyingPreferredStatusBarStyle: UIStatusBarStyle = .lightContent
7775

0 commit comments

Comments
 (0)