mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-09 00:55:52 -06:00
## Summary Converted `ConnectionURLViewController` from UIKit/Eureka forms to native SwiftUI (`ConnectionURLView`) with snapshot test coverage. **Changes:** - **ConnectionURLView.swift** (new): SwiftUI implementation with Form-based UI - URL input with validation (internal/external/cloud toggle) - Dynamic SSID/hardware address lists with add/delete - Location permission checks (iOS 14+ accuracy support) - Local push configuration with doc link - Promise-based save with error handling - **ConnectionSettingsViewController.swift**: Push SwiftUI view via `UIHostingController` instead of `ButtonRowWithPresent` - **ConnectionURLView.test.swift** (new): Snapshot tests for internal/external URL types in light/dark modes - **ConnectionURLViewController.swift**: Removed (393 lines) Net: -2 lines, modernized architecture, improved maintainability. ## 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 All functionality preserved from original implementation. Snapshot tests will generate reference images on first run. <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > Convert ConnectionURLViewController to SwiftUI and add snapshot tests </details> <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/home-assistant/iOS/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
175 lines
5.4 KiB
Swift
175 lines
5.4 KiB
Swift
import Foundation
|
|
import PromiseKit
|
|
import Shared
|
|
|
|
@MainActor
|
|
final class ConnectionURLViewModel: ObservableObject {
|
|
enum SaveError: LocalizedError {
|
|
case lastURL
|
|
case validation(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .lastURL:
|
|
return L10n.Settings.ConnectionSection.Errors.cannotRemoveLastUrl
|
|
case let .validation(message):
|
|
return message
|
|
}
|
|
}
|
|
|
|
var isFinal: Bool {
|
|
switch self {
|
|
case .lastURL, .validation:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
let server: Server
|
|
let urlType: ConnectionInfo.URLType
|
|
|
|
@Published var url: String
|
|
@Published var useCloud: Bool
|
|
@Published var localPush: Bool
|
|
@Published var ssids: [String]
|
|
@Published var hardwareAddresses: [String]
|
|
@Published var isChecking = false
|
|
@Published var showError = false
|
|
@Published var errorMessage = ""
|
|
@Published var canCommitAnyway = false
|
|
|
|
init(server: Server, urlType: ConnectionInfo.URLType) {
|
|
self.server = server
|
|
self.urlType = urlType
|
|
|
|
self.url = server.info.connection.address(for: urlType)?.absoluteString ?? ""
|
|
self.useCloud = server.info.connection.useCloud
|
|
self.localPush = server.info.connection.isLocalPushEnabled
|
|
self.ssids = server.info.connection.internalSSIDs ?? []
|
|
self.hardwareAddresses = server.info.connection.internalHardwareAddresses ?? []
|
|
}
|
|
|
|
var placeholder: String {
|
|
switch urlType {
|
|
case .internal:
|
|
return L10n.Settings.ConnectionSection.InternalBaseUrl.placeholder
|
|
case .external:
|
|
return L10n.Settings.ConnectionSection.ExternalBaseUrl.placeholder
|
|
case .remoteUI, .none:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func addSSID() {
|
|
let currentSSID = Current.connectivity.currentWiFiSSID()
|
|
if let currentSSID, !ssids.contains(currentSSID) {
|
|
ssids.append(currentSSID)
|
|
} else {
|
|
ssids.append("")
|
|
}
|
|
}
|
|
|
|
func removeSSID(at index: Int) {
|
|
ssids.remove(at: index)
|
|
}
|
|
|
|
func removeSSIDs(at offsets: IndexSet) {
|
|
ssids.remove(atOffsets: offsets)
|
|
}
|
|
|
|
func addHardwareAddress() {
|
|
let currentAddress = Current.connectivity.currentNetworkHardwareAddress()
|
|
if let currentAddress, !hardwareAddresses.contains(currentAddress) {
|
|
hardwareAddresses.append(currentAddress)
|
|
} else {
|
|
hardwareAddresses.append("")
|
|
}
|
|
}
|
|
|
|
func removeHardwareAddress(at index: Int) {
|
|
hardwareAddresses.remove(at: index)
|
|
}
|
|
|
|
func removeHardwareAddresses(at offsets: IndexSet) {
|
|
hardwareAddresses.remove(atOffsets: offsets)
|
|
}
|
|
|
|
func save(onSuccess: @escaping () -> Void) {
|
|
let givenURL = url.isEmpty ? nil : URL(string: url)
|
|
|
|
isChecking = true
|
|
|
|
firstly { () -> Promise<Void> in
|
|
try self.check(url: givenURL, useCloud: self.useCloud)
|
|
|
|
if self.useCloud, let remoteURL = self.server.info.connection.address(for: .remoteUI) {
|
|
return Current.webhooks.sendTest(server: self.server, baseURL: remoteURL)
|
|
}
|
|
|
|
if let givenURL, !self.useCloud {
|
|
return Current.webhooks.sendTest(server: self.server, baseURL: givenURL)
|
|
}
|
|
|
|
return .value(())
|
|
}.ensure {
|
|
self.isChecking = false
|
|
}.done {
|
|
self.commit()
|
|
onSuccess()
|
|
}.catch { error in
|
|
self.handleError(error)
|
|
}
|
|
}
|
|
|
|
private func check(url: URL?, useCloud: Bool) throws {
|
|
// Validate hardware addresses
|
|
if urlType.isAffectedByHardwareAddress {
|
|
let pattern = "^[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}$"
|
|
let regex = try? NSRegularExpression(pattern: pattern)
|
|
|
|
for address in hardwareAddresses where !address.isEmpty {
|
|
let range = NSRange(location: 0, length: address.utf16.count)
|
|
if regex?.firstMatch(in: address, range: range) == nil {
|
|
throw SaveError.validation(L10n.Settings.ConnectionSection.InternalUrlHardwareAddresses.invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if removing last URL
|
|
if url == nil {
|
|
let existingInfo = server.info.connection
|
|
let other: ConnectionInfo.URLType = urlType == .internal ? .external : .internal
|
|
if existingInfo.address(for: other) == nil,
|
|
!useCloud || !existingInfo.useCloud {
|
|
throw SaveError.lastURL
|
|
}
|
|
}
|
|
}
|
|
|
|
private func commit() {
|
|
let givenURL = url.isEmpty ? nil : URL(string: url)
|
|
|
|
server.update { info in
|
|
info.connection.set(address: givenURL, for: urlType)
|
|
info.connection.useCloud = useCloud
|
|
info.connection.isLocalPushEnabled = localPush
|
|
info.connection.internalSSIDs = ssids.filter { !$0.isEmpty }
|
|
info.connection.internalHardwareAddresses = hardwareAddresses
|
|
.map { $0.lowercased() }
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
}
|
|
|
|
private func handleError(_ error: Error) {
|
|
errorMessage = error.localizedDescription
|
|
|
|
if let saveError = error as? SaveError {
|
|
canCommitAnyway = !saveError.isFinal
|
|
} else {
|
|
canCommitAnyway = true
|
|
}
|
|
|
|
showError = true
|
|
}
|
|
}
|