mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-15 07:49:30 -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>
327 lines
11 KiB
Swift
327 lines
11 KiB
Swift
import CoreLocation
|
|
import Foundation
|
|
|
|
/**
|
|
A comprehensive location management system that provides a clean abstraction over Core Location
|
|
with notification-based updates and protocol-oriented architecture for testability.
|
|
|
|
## Overview
|
|
|
|
The LocationManager provides:
|
|
- Permission state management and monitoring
|
|
- Request methods for different authorization levels
|
|
- Automatic notification broadcasting when permissions change
|
|
- Protocol-based design for dependency injection and testing
|
|
|
|
## Usage
|
|
|
|
```swift
|
|
let locationManager = LocationManager()
|
|
|
|
// Listen for permission changes
|
|
NotificationCenter.default.addObserver(
|
|
forName: .locationPermissionDidChange,
|
|
object: nil,
|
|
queue: .main
|
|
) { notification in
|
|
if let userInfo = notification.userInfo,
|
|
let state = userInfo["permissionState"] as? LocationPermissionState {
|
|
// Handle permission change
|
|
}
|
|
}
|
|
|
|
// Request permissions
|
|
locationManager.requestLocationPermission()
|
|
locationManager.requestAlwaysLocationPermission()
|
|
```
|
|
*/
|
|
|
|
// MARK: - Notification Names
|
|
|
|
public extension Notification.Name {
|
|
/// Posted when location permission state changes
|
|
///
|
|
/// The notification's `userInfo` dictionary contains:
|
|
/// - `"permissionState"`: The new `LocationPermissionState` value
|
|
static let locationPermissionDidChange = Notification.Name("locationPermissionDidChange")
|
|
}
|
|
|
|
// MARK: - Location Permission State
|
|
|
|
/**
|
|
Represents the current state of location permissions for the application.
|
|
|
|
This enum provides a Swift-native wrapper around `CLAuthorizationStatus` with
|
|
more descriptive cases and easier pattern matching.
|
|
*/
|
|
public enum LocationPermissionState {
|
|
/// The user has not yet been asked for location permission
|
|
case notDetermined
|
|
|
|
/// The user has explicitly denied location permission
|
|
case denied
|
|
|
|
/// Location services are restricted, typically due to parental controls
|
|
case restricted
|
|
|
|
/// Location permission is granted only when the app is in use
|
|
case authorizedWhenInUse
|
|
|
|
/// Location permission is granted always (background and foreground)
|
|
case authorizedAlways
|
|
|
|
/**
|
|
Creates a LocationPermissionState from a CLAuthorizationStatus.
|
|
|
|
- Parameter authorizationStatus: The Core Location authorization status
|
|
*/
|
|
init(from authorizationStatus: CLAuthorizationStatus) {
|
|
switch authorizationStatus {
|
|
case .notDetermined:
|
|
self = .notDetermined
|
|
case .denied:
|
|
self = .denied
|
|
case .restricted:
|
|
self = .restricted
|
|
case .authorizedWhenInUse:
|
|
self = .authorizedWhenInUse
|
|
case .authorizedAlways:
|
|
self = .authorizedAlways
|
|
@unknown default:
|
|
self = .notDetermined
|
|
}
|
|
}
|
|
|
|
public init(userInfo: [AnyHashable: Any]) {
|
|
if let permissionState = userInfo["permissionState"] as? LocationPermissionState {
|
|
self = permissionState
|
|
} else {
|
|
fatalError(
|
|
"Failed to initialize LocationPermissionState: userInfo dictionary must contain a \"permissionState\" key with a LocationPermissionState value"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Protocol
|
|
|
|
/**
|
|
Protocol defining the interface for location management operations.
|
|
|
|
This protocol abstracts location permission management to enable:
|
|
- Dependency injection in production code
|
|
- Easy mocking for unit tests
|
|
- Swappable implementations for different use cases
|
|
|
|
## Required Implementation
|
|
|
|
Conforming types must provide:
|
|
- Current permission state monitoring
|
|
- Permission request methods
|
|
- Location services availability checking
|
|
*/
|
|
public protocol LocationManagerProtocol: AnyObject {
|
|
/// The current location permission state for the application
|
|
var currentPermissionState: LocationPermissionState { get }
|
|
|
|
/// The level of location accuracy the user has authorized for the application
|
|
///
|
|
/// This property indicates whether the user has granted full or reduced accuracy
|
|
/// location access. When reduced accuracy is in effect, location data is less precise.
|
|
///
|
|
/// Possible values:
|
|
/// - `.fullAccuracy`: The user has granted full location accuracy
|
|
/// - `.reducedAccuracy`: The user has enabled approximate location mode
|
|
///
|
|
/// - Note: Users can control accuracy authorization in Settings, independent of
|
|
/// whether they've granted location permission itself.
|
|
var accuracyAuthorization: CLAccuracyAuthorization { get }
|
|
|
|
/// Whether location services are enabled on this device
|
|
///
|
|
/// - Returns: `true` if location services are enabled system-wide, `false` otherwise
|
|
var isLocationServicesEnabled: Bool { get }
|
|
|
|
/// Requests basic location permission (when in use only)
|
|
///
|
|
/// This method requests permission to access location only when the app is active.
|
|
/// If permission has already been granted or denied, this method has no effect.
|
|
func requestLocationPermission()
|
|
}
|
|
|
|
// MARK: - Implementation
|
|
|
|
/**
|
|
Concrete implementation of LocationManagerProtocol using Core Location.
|
|
|
|
This class provides a complete location management solution with:
|
|
- Automatic permission state monitoring via CLLocationManagerDelegate
|
|
- Notification broadcasting when permissions change
|
|
- Intelligent permission request handling based on current state
|
|
- Thread-safe notification posting
|
|
|
|
## Thread Safety
|
|
|
|
LocationManager is designed to be used from the main thread, as required by Core Location.
|
|
All delegate methods and notifications are handled on the main thread.
|
|
|
|
## Notification Broadcasting
|
|
|
|
When location permissions change, this class automatically posts
|
|
`Notification.Name.locationPermissionDidChange` notifications with the new state
|
|
included in the userInfo dictionary.
|
|
*/
|
|
final class LocationManager: NSObject, LocationManagerProtocol {
|
|
// MARK: - Properties
|
|
|
|
/// The underlying Core Location manager
|
|
private let coreLocationManager = CLLocationManager()
|
|
|
|
/// Notification center for broadcasting permission changes
|
|
private let notificationCenter: NotificationCenter
|
|
|
|
// MARK: - Public Properties
|
|
|
|
/// The current location permission state
|
|
///
|
|
/// This property dynamically queries the Core Location manager for the current
|
|
/// authorization status and converts it to our custom enum.
|
|
var currentPermissionState: LocationPermissionState {
|
|
LocationPermissionState(from: coreLocationManager.authorizationStatus)
|
|
}
|
|
|
|
/// The level of location accuracy authorized by the user
|
|
///
|
|
/// Returns the current accuracy authorization level, which determines the precision
|
|
/// of location data provided to the app. This is independent of whether location
|
|
/// permission itself has been granted.
|
|
///
|
|
/// - Returns: `.fullAccuracy` if the user has granted precise location access,
|
|
/// or `.reducedAccuracy` if approximate location mode is enabled
|
|
var accuracyAuthorization: CLAccuracyAuthorization {
|
|
coreLocationManager.accuracyAuthorization
|
|
}
|
|
|
|
/// Whether location services are enabled system-wide
|
|
///
|
|
/// This checks if location services are enabled at the device level.
|
|
/// Even if your app has permission, location won't work if this returns false.
|
|
var isLocationServicesEnabled: Bool {
|
|
CLLocationManager.locationServicesEnabled()
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
/**
|
|
Creates a new LocationManager instance.
|
|
|
|
- Parameter notificationCenter: The notification center to use for broadcasting
|
|
permission changes. Defaults to `NotificationCenter.default`.
|
|
*/
|
|
init(notificationCenter: NotificationCenter = .default) {
|
|
self.notificationCenter = notificationCenter
|
|
super.init()
|
|
setupLocationManager()
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
/**
|
|
Configures the Core Location manager with appropriate settings.
|
|
|
|
Sets up:
|
|
- Delegate assignment
|
|
- Desired accuracy (best available)
|
|
*/
|
|
private func setupLocationManager() {
|
|
coreLocationManager.delegate = self
|
|
coreLocationManager.desiredAccuracy = kCLLocationAccuracyBest
|
|
}
|
|
|
|
/**
|
|
Posts a notification when location permission state changes.
|
|
|
|
The notification includes the current permission state in its userInfo dictionary
|
|
under the key "permissionState".
|
|
*/
|
|
private func postPermissionChangeNotification() {
|
|
let userInfo = ["permissionState": currentPermissionState]
|
|
notificationCenter.post(
|
|
name: .locationPermissionDidChange,
|
|
object: self,
|
|
userInfo: userInfo
|
|
)
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/**
|
|
Requests basic location permission (when in use only).
|
|
|
|
This method handles the permission request intelligently:
|
|
- If location services are disabled system-wide, the request is ignored
|
|
- If permission is not determined, it requests "when in use" authorization
|
|
- If permission is already granted or denied, no action is taken
|
|
|
|
## Permission Flow
|
|
|
|
1. Checks if location services are enabled
|
|
2. Evaluates current authorization status
|
|
3. Requests appropriate permission if needed
|
|
|
|
- Note: Permission changes are automatically broadcast via NotificationCenter
|
|
*/
|
|
func requestLocationPermission() {
|
|
switch coreLocationManager.authorizationStatus {
|
|
case .notDetermined:
|
|
coreLocationManager.requestWhenInUseAuthorization()
|
|
case .denied, .restricted, .authorizedWhenInUse, .authorizedAlways:
|
|
postPermissionChangeNotification()
|
|
@unknown default:
|
|
coreLocationManager.requestWhenInUseAuthorization()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - CLLocationManagerDelegate
|
|
|
|
/**
|
|
Core Location delegate implementation.
|
|
|
|
This extension handles Core Location callbacks and translates them into
|
|
our notification-based system for cleaner separation of concerns.
|
|
*/
|
|
extension LocationManager: CLLocationManagerDelegate {
|
|
/**
|
|
Called when the location authorization status changes.
|
|
|
|
This method automatically broadcasts a notification with the new permission state,
|
|
allowing other parts of the app to respond to permission changes without tight coupling.
|
|
|
|
- Parameter manager: The location manager whose authorization status changed
|
|
*/
|
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
postPermissionChangeNotification()
|
|
|
|
if manager.authorizationStatus == .authorizedWhenInUse {
|
|
// If we just got when-in-use authorization, we can now request always authorization
|
|
manager.requestAlwaysAuthorization()
|
|
}
|
|
}
|
|
|
|
/**
|
|
Called when the location manager encounters an error.
|
|
|
|
Currently logs errors for debugging purposes. In production, you might want to
|
|
handle specific error types or notify the user appropriately.
|
|
|
|
- Parameters:
|
|
- manager: The location manager that encountered the error
|
|
- error: The error that occurred
|
|
*/
|
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
// Handle location errors if needed
|
|
print("Location manager failed with error: \(error.localizedDescription)")
|
|
}
|
|
}
|