Files
MetalPOC/Sources/Data/LocationProvider.swift
james.maesandClaude Sonnet 4.6 878aa28b31 feat(widgets): LocalNetwork widget — Bonjour scanner + CoreLocation place name
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>
2026-05-14 15:41:22 -05:00

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 "—"
}
}