From 93e7bca8cfd9e543ced74992af8ae91acdb9a427 Mon Sep 17 00:00:00 2001 From: DigiH <17110652+DigiH@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:52:52 +0200 Subject: [PATCH 1/2] Enable swipe from left and from right in OH admin pages Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com> --- openHAB/UI/OpenHABWebViewController.swift | 5 ++++- openHAB/UI/SwiftUI/Rows/WebRowView.swift | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openHAB/UI/OpenHABWebViewController.swift b/openHAB/UI/OpenHABWebViewController.swift index ffeba79e2..a17418ade 100644 --- a/openHAB/UI/OpenHABWebViewController.swift +++ b/openHAB/UI/OpenHABWebViewController.swift @@ -119,9 +119,10 @@ class OpenHABWebViewController: OpenHABViewController { super.viewDidLoad() navigationController?.interactivePopGestureRecognizer?.isEnabled = true attachWebViewToLayout(webView) + webView.allowsBackForwardNavigationGestures = true loadingOverlay.backgroundColor = .systemBackground - loadingOverlay.isHidden = true + loadingOverlay.isHidden = false loadingOverlay.translatesAutoresizingMaskIntoConstraints = false view.addSubview(loadingOverlay) NSLayoutConstraint.activate([ @@ -517,6 +518,8 @@ class OpenHABWebViewController: OpenHABViewController { webview.isInspectable = true } + webview.allowsBackForwardNavigationGestures = true + // Avoid safe-area content insets which can leave a small gap at the bottom on iPad until a reload. webview.scrollView.contentInsetAdjustmentBehavior = .never webview.scrollView.contentInset = .zero diff --git a/openHAB/UI/SwiftUI/Rows/WebRowView.swift b/openHAB/UI/SwiftUI/Rows/WebRowView.swift index 00c4a090a..b43ca5cc3 100644 --- a/openHAB/UI/SwiftUI/Rows/WebRowView.swift +++ b/openHAB/UI/SwiftUI/Rows/WebRowView.swift @@ -143,6 +143,7 @@ struct WebRowView: UIViewRepresentable { func makeUIView(context: Context) -> WKWebView { let homeId = Preferences.shared.currentHomePreferences.id let webView = WKWebView(frame: .zero, configuration: WebRowViewConfigurationFactory.make(homeId: homeId)) + webView.allowsBackForwardNavigationGestures = true webView.navigationDelegate = context.coordinator return webView } From 3dfbb71ea43244f8ed726a7f2e1587b51ece3b4b Mon Sep 17 00:00:00 2001 From: Tim Mueller-Seydlitz Date: Fri, 19 Jun 2026 10:11:04 +0200 Subject: [PATCH 2/2] Fix web history back gesture conflicting with side menu right-edge swipe Replace allowsBackForwardNavigationGestures (which adds both left- and right-edge gestures) with a custom left-edge UIScreenEdgePanGestureRecognizer that calls window.history.back() via JS. This eliminates the right-edge WKWebView gesture that was competing with SideMenu's right-edge swipe, causing the current page to flash when swiping to open the side menu. Signed-off-by: Tim Mueller-Seydlitz --- openHAB/UI/OpenHABWebViewController.swift | 85 ++++++++++++++++++----- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/openHAB/UI/OpenHABWebViewController.swift b/openHAB/UI/OpenHABWebViewController.swift index a17418ade..379140d73 100644 --- a/openHAB/UI/OpenHABWebViewController.swift +++ b/openHAB/UI/OpenHABWebViewController.swift @@ -39,6 +39,7 @@ class OpenHABWebViewController: OpenHABViewController { private var lastLoadedURL: String? // Track the last successfully loaded URL from didFinish private var isConfirmingExternalURL = false private var externalURLCooldownUntil: Date? + private weak var webHistoryBackGesture: UIScreenEdgePanGestureRecognizer? var hasLoadedPage: Bool { !currentTarget.isEmpty @@ -119,7 +120,7 @@ class OpenHABWebViewController: OpenHABViewController { super.viewDidLoad() navigationController?.interactivePopGestureRecognizer?.isEnabled = true attachWebViewToLayout(webView) - webView.allowsBackForwardNavigationGestures = true + configureWebHistoryGestures(for: webView) loadingOverlay.backgroundColor = .systemBackground loadingOverlay.isHidden = false @@ -518,7 +519,7 @@ class OpenHABWebViewController: OpenHABViewController { webview.isInspectable = true } - webview.allowsBackForwardNavigationGestures = true + configureWebHistoryGestures(for: webview) // Avoid safe-area content insets which can leave a small gap at the bottom on iPad until a reload. webview.scrollView.contentInsetAdjustmentBehavior = .never @@ -551,6 +552,56 @@ class OpenHABWebViewController: OpenHABViewController { } } +extension OpenHABWebViewController: UIGestureRecognizerDelegate { + private func configureWebHistoryGestures(for webView: WKWebView) { + webView.allowsBackForwardNavigationGestures = false + + guard webHistoryBackGesture?.view !== webView else { + return + } + + if let webHistoryBackGesture { + webHistoryBackGesture.view?.removeGestureRecognizer(webHistoryBackGesture) + } + + let backGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleWebHistoryEdgePan(_:))) + backGesture.edges = .left + backGesture.delegate = self + webView.addGestureRecognizer(backGesture) + webHistoryBackGesture = backGesture + } + + @objc private func handleWebHistoryEdgePan(_ gesture: UIScreenEdgePanGestureRecognizer) { + guard gesture.state == .ended else { return } + + let translation = gesture.translation(in: webView) + let velocity = gesture.velocity(in: webView) + let hasIntent = abs(translation.x) > 35 || abs(velocity.x) > 250 + guard hasIntent else { return } + + guard gesture.edges == .left, + translation.x > 0 || velocity.x > 0 else { return } + + navigateWebHistory( + direction: "back", + script: "if (window.history.length > 1) { window.history.back(); true; } else { false; }" + ) + } + + private func navigateWebHistory(direction: String, script: String) { + webView.evaluateJavaScript(script) { _, error in + if let error { + Logger.viewController.error("web history \(direction, privacy: .public) failed: \(error.localizedDescription, privacy: .public)") + } + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + gestureRecognizer === webHistoryBackGesture + } +} + extension OpenHABWebViewController: WKScriptMessageHandler { @MainActor func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { @@ -841,21 +892,6 @@ extension OpenHABWebViewController: WKNavigationDelegate { } } -private extension URL { - var isNativeWebURL: Bool { - guard let scheme = scheme?.lowercased() else { return true } - return ["http", "https", "about", "blob", "data", "javascript"].contains(scheme) - } - - /// RFC 6454 origin: scheme + host + port, with default ports (80/443) omitted. - var webOrigin: String? { - guard let scheme = scheme?.lowercased(), let host else { return nil } - let defaultPort = scheme == "https" ? 443 : scheme == "http" ? 80 : nil - let portSuffix = (port != nil && port != defaultPort) ? ":\(port!)" : "" - return "\(scheme)://\(host)\(portSuffix)" - } -} - extension OpenHABWebViewController: WKUIDelegate { func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, @@ -882,3 +918,18 @@ extension OpenHABWebViewController: WKUIDelegate { } // swiftlint:enable async_without_await } + +private extension URL { + var isNativeWebURL: Bool { + guard let scheme = scheme?.lowercased() else { return true } + return ["http", "https", "about", "blob", "data", "javascript"].contains(scheme) + } + + /// RFC 6454 origin: scheme + host + port, with default ports (80/443) omitted. + var webOrigin: String? { + guard let scheme = scheme?.lowercased(), let host else { return nil } + let defaultPort = scheme == "https" ? 443 : scheme == "http" ? 80 : nil + let portSuffix = (port != nil && port != defaultPort) ? ":\(port!)" : "" + return "\(scheme)://\(host)\(portSuffix)" + } +}