Files
MetalPOC/Sources/Data/NetworkScanner.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

84 lines
3.2 KiB
Swift

import Foundation
import Network
/// Lightweight Bonjour scanner. Browses a curated list of service types and
/// accumulates discovered service names. Thread-safe snapshot via `currentDevices()`.
final class NetworkScanner {
static let shared = NetworkScanner()
struct Device: Equatable {
let name: String
let serviceType: String
let firstSeen: Date
}
private let queue = DispatchQueue(label: "NetworkScanner.queue")
private var browsers: [NWBrowser] = []
private var devices: [String: Device] = [:] // name → Device (dedupe by name)
private var started = false
private let serviceTypes = [
"_companion-link._tcp", // iPhone Handoff
"_apple-mobdev2._tcp", // iOS device
"_airplay._tcp", // AirPlay (Apple TV, AirPlay receivers)
"_raop._tcp", // AirPlay audio (HomePods, speakers)
"_homekit._tcp", // HomeKit accessories
"_hap._tcp", // HomeKit Accessory Protocol
"_googlecast._tcp", // Chromecast / Google devices
"_smb._tcp", // SMB file sharing (Macs, NAS)
"_afpovertcp._tcp", // Apple file sharing
"_workstation._tcp", // Generic workstation broadcast
"_device-info._tcp", // Apple device info
"_ssh._tcp", // SSH servers
"_http._tcp" // HTTP services
]
func start() {
queue.async {
guard !self.started else { return }
self.started = true
for type in self.serviceTypes {
let descriptor = NWBrowser.Descriptor.bonjour(type: type, domain: nil)
let parameters = NWParameters()
parameters.includePeerToPeer = true
let browser = NWBrowser(for: descriptor, using: parameters)
browser.browseResultsChangedHandler = { [weak self] results, _ in
self?.handle(results: results, type: type)
}
browser.stateUpdateHandler = { _ in
// No-op; we don't tear down browsers
}
browser.start(queue: self.queue)
self.browsers.append(browser)
}
}
}
private func handle(results: Set<NWBrowser.Result>, type: String) {
queue.async {
for result in results {
if case .service(let name, _, _, _) = result.endpoint {
if self.devices[name] == nil {
self.devices[name] = Device(name: name, serviceType: type, firstSeen: Date())
}
}
}
}
}
/// Snapshot of currently-discovered devices, sorted by name.
func currentDevices() -> [Device] {
queue.sync {
Array(devices.values).sorted { $0.name < $1.name }
}
}
/// Returns true if any discovered device name contains `pattern` (case-insensitive).
func hasDevice(matching pattern: String) -> Bool {
guard !pattern.isEmpty else { return false }
return queue.sync {
devices.values.contains { $0.name.localizedCaseInsensitiveContains(pattern) }
}
}
}