mirror of
https://github.com/home-assistant/iOS.git
synced 2026-04-12 15:26:45 -05:00
Split WebViewController functionality into multiple extension files (Alerts, EmptyState, Gestures, Navigation, Onboarding, Settings, StatusBar, URLLoading, WebViewSetup) and add FrontEndConnectionState enum. Update Xcode project to include the new source files and adjust several Database test file references and a local Swift package path (Sources/SharedPush). Also add empty input/output path arrays to several CocoaPods embed phases. This refactors web view features into modular components and wires them into the project file. <!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
185 lines
7.5 KiB
Swift
185 lines
7.5 KiB
Swift
import HAKit
|
|
import Shared
|
|
import SwiftUI
|
|
import UIKit
|
|
@preconcurrency import WebKit
|
|
|
|
// MARK: - URL Loading & Connection Lifecycle
|
|
|
|
extension WebViewController {
|
|
func observeConnectionNotifications() {
|
|
for name: Notification.Name in [
|
|
HomeAssistantAPI.didConnectNotification,
|
|
UIApplication.didBecomeActiveNotification,
|
|
] {
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(connectionInfoDidChange),
|
|
name: name,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(scheduleReconnectBackgroundTimer),
|
|
name: UIApplication.didEnterBackgroundNotification,
|
|
object: nil
|
|
)
|
|
|
|
tokens.append(server.observe { [weak self] _ in
|
|
self?.connectionInfoDidChange()
|
|
})
|
|
}
|
|
|
|
@objc func connectionInfoDidChange() {
|
|
DispatchQueue.main.async { [self] in
|
|
loadActiveURLIfNeeded()
|
|
}
|
|
}
|
|
|
|
@objc func loadActiveURLIfNeeded() {
|
|
guard !loadActiveURLIfNeededInProgress else {
|
|
Current.Log.info("loadActiveURLIfNeeded already in progress, skipping")
|
|
return
|
|
}
|
|
|
|
loadActiveURLIfNeededInProgress = true
|
|
Current.Log.info("loadActiveURLIfNeeded called")
|
|
|
|
let loadBlock: () -> Void = { [weak self] in
|
|
defer {
|
|
self?.loadActiveURLIfNeededInProgress = false
|
|
}
|
|
|
|
guard let self else { return }
|
|
guard let webviewURL = server.info.connection.webviewURL() else {
|
|
Current.Log.info("not loading, no url")
|
|
showNoActiveURLError()
|
|
return
|
|
}
|
|
|
|
hideNoActiveURLError()
|
|
|
|
guard webView.url == nil || webView.url?.baseIsEqual(to: webviewURL) == false else {
|
|
// we also tell the webview -- maybe it failed to connect itself? -- to refresh if needed
|
|
webView.evaluateJavaScript("checkForMissingHassConnectionAndReload()", completionHandler: nil)
|
|
return
|
|
}
|
|
|
|
guard UIApplication.shared.applicationState != .background else {
|
|
Current.Log.info("not loading, in background")
|
|
return
|
|
}
|
|
|
|
// if we aren't showing a url or it's an incorrect url, update it -- otherwise, leave it alone
|
|
let request: URLRequest
|
|
|
|
if Current.settingsStore.restoreLastURL,
|
|
let initialURL, initialURL.baseIsEqual(to: webviewURL) {
|
|
Current.Log.info("restoring initial url path: \(initialURL.path)")
|
|
request = URLRequest(url: initialURL)
|
|
} else if let currentURL = webView.url, currentURL.path.count > 1 {
|
|
// Preserve the current path when the base URL changes (e.g., switching between internal/external)
|
|
var components = URLComponents(url: webviewURL, resolvingAgainstBaseURL: true)
|
|
components?.path = currentURL.path
|
|
if let query = currentURL.query {
|
|
// Preserve external_auth if present, add other query items
|
|
var queryItems = components?.queryItems ?? []
|
|
let currentQueryItems = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)?
|
|
.queryItems ?? []
|
|
for item in currentQueryItems where item.name != "external_auth" {
|
|
queryItems.append(item)
|
|
}
|
|
components?.queryItems = queryItems
|
|
}
|
|
components?.fragment = currentURL.fragment
|
|
let newURL = components?.url ?? webviewURL
|
|
Current.Log.info("preserving current path on base URL change: \(newURL.path)")
|
|
request = URLRequest(url: newURL)
|
|
} else {
|
|
Current.Log.info("loading default url path: \(webviewURL.path)")
|
|
request = URLRequest(url: webviewURL)
|
|
}
|
|
|
|
load(request: request)
|
|
}
|
|
|
|
if Current.isCatalyst {
|
|
loadBlock()
|
|
} else {
|
|
Current.connectivity.syncNetworkInformation {
|
|
loadBlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func showNoActiveURLError() {
|
|
// Load about:blank in webview to prevent any current connections
|
|
load(request: URLRequest(url: URL(string: "about:blank")!))
|
|
Current.Log.info("Loading about:blank in webview due to no activeURL")
|
|
|
|
// Alert the user that there's no URL that the App can use
|
|
let controller = ConnectionSecurityLevelBlockView(server: server).embeddedInHostingController()
|
|
controller.modalPresentationStyle = .fullScreen
|
|
controller.isModalInPresentation = true
|
|
controller.view.tag = WebViewControllerOverlayedViewTags.noActiveURLError.rawValue
|
|
controller.modalTransitionStyle = .crossDissolve
|
|
|
|
guard ![
|
|
WebViewControllerOverlayedViewTags.noActiveURLError.rawValue,
|
|
WebViewControllerOverlayedViewTags.settingsView.rawValue,
|
|
WebViewControllerOverlayedViewTags.onboardingPermissions.rawValue,
|
|
].contains(presentedViewController?.view.tag ?? -1) else {
|
|
Current.Log.info("'No active URL' screen was not presented because of high priority view already visible")
|
|
return
|
|
}
|
|
|
|
presentOverlayController(controller: controller, animated: true)
|
|
}
|
|
|
|
func hideNoActiveURLError() {
|
|
if presentedViewController?.view.tag == WebViewControllerOverlayedViewTags.noActiveURLError.rawValue {
|
|
presentedViewController?.dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
@objc func scheduleReconnectBackgroundTimer() {
|
|
precondition(Thread.isMainThread)
|
|
|
|
guard isViewLoaded, server.info.version >= .externalBusCommandRestart else { return }
|
|
|
|
// On iOS 15, Apple switched to using NSURLSession's WebSocket implementation, which is pretty bad at detecting
|
|
// any kind of networking failure. Even more troubling, it doesn't realize there's a failure due to background
|
|
// so it spends dozens of seconds waiting for a connection reset externally.
|
|
//
|
|
// We work around this by detecting being in the background for long enough that it's likely the connection will
|
|
// need to reconnect, anyway (similar to how we do it in HAKit). When this happens, we ask the frontend to
|
|
// reset its WebSocket connection, thus eliminating the wait.
|
|
//
|
|
// It's likely this doesn't apply before iOS 15, but it may improve the reconnect timing there anyhow.
|
|
|
|
reconnectBackgroundTimer = Timer.scheduledTimer(
|
|
withTimeInterval: 5.0,
|
|
repeats: true,
|
|
block: { [weak self] timer in
|
|
if let self, Current.date().timeIntervalSince(timer.fireDate) > 30.0 {
|
|
_ = webViewExternalMessageHandler.sendExternalBus(message: .init(command: "restart"))
|
|
}
|
|
|
|
if UIApplication.shared.applicationState == .active {
|
|
timer.invalidate()
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
/// Updates the app database and panels for the current server
|
|
/// Called after view appears and on pull to refresh to avoid blocking app launch
|
|
func updateDatabaseAndPanels() {
|
|
// Update runs in background automatically, returns immediately
|
|
Current.appDatabaseUpdater.update(server: server, forceUpdate: false)
|
|
Current.panelsUpdater.update()
|
|
}
|
|
}
|