Adds a real-data LocalNetworkWidget to the HUD showing the Mac's approximate location (city/region via CLGeocoder reverse-geocode), the user's phone presence on the LAN (● HERE / ○ AWAY via Bonjour NWBrowser matching configured name patterns), and a live list of Bonjour-advertising LAN devices (iPhones, iPads, Macs, Apple TVs, HomePods, etc.) with a total count. All data is real — NWBrowser browses 13 service types continuously; CoreLocation supplies GPS coordinates reverse-geocoded to a place name. On first launch, macOS will prompt for Location Services permission; the widget gracefully shows "(awaiting permission)" until granted. Bonjour discovery is async — the device list starts empty and populates within a few seconds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
62 lines
2.2 KiB
Swift
62 lines
2.2 KiB
Swift
import Foundation
|
|
import CoreLocation
|
|
|
|
/// Wraps CLLocationManager. Asks for authorization once at init, then keeps an
|
|
/// up-to-date snapshot of the Mac's location + a reverse-geocoded place name.
|
|
final class LocationProvider: NSObject, CLLocationManagerDelegate {
|
|
static let shared = LocationProvider()
|
|
|
|
private let manager = CLLocationManager()
|
|
private let geocoder = CLGeocoder()
|
|
private(set) var currentLocation: CLLocation?
|
|
private(set) var placeName: String = "—"
|
|
private var lastGeocodeAt: Date = .distantPast
|
|
|
|
override init() {
|
|
super.init()
|
|
manager.delegate = self
|
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
|
manager.distanceFilter = 100 // only update on ≥100m moves
|
|
}
|
|
|
|
func start() {
|
|
let status = CLLocationManager.authorizationStatus()
|
|
switch status {
|
|
case .notDetermined:
|
|
manager.requestAlwaysAuthorization()
|
|
case .authorized, .authorizedAlways:
|
|
manager.startUpdatingLocation()
|
|
default:
|
|
// Denied / restricted — we just leave placeName as "—"
|
|
break
|
|
}
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
|
if status == .authorized || status == .authorizedAlways {
|
|
manager.startUpdatingLocation()
|
|
}
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
guard let loc = locations.last else { return }
|
|
currentLocation = loc
|
|
|
|
// Reverse-geocode at most every 60 seconds to avoid rate-limit
|
|
let now = Date()
|
|
if now.timeIntervalSince(lastGeocodeAt) >= 60 {
|
|
lastGeocodeAt = now
|
|
geocoder.reverseGeocodeLocation(loc) { [weak self] placemarks, _ in
|
|
guard let p = placemarks?.first else { return }
|
|
let city = p.locality ?? p.subAdministrativeArea ?? "—"
|
|
let region = p.administrativeArea ?? ""
|
|
self?.placeName = region.isEmpty ? city : "\(city), \(region)"
|
|
}
|
|
}
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
// No-op; widget will just show "—"
|
|
}
|
|
}
|