import ARKit import CoreLocation import CoreMotion import SceneKit import SwiftUI struct ARSunOverlayView: UIViewRepresentable { @Binding var sunNodesReady: Bool func makeCoordinator() -> Coordinator { Coordinator(sunNodesReady: $sunNodesReady) } func makeUIView(context: Context) -> ARSCNView { let view = ARSCNView(frame: .zero) view.delegate = context.coordinator view.scene = SCNScene() view.automaticallyUpdatesLighting = true let config = ARWorldTrackingConfiguration() config.worldAlignment = .gravityAndHeading view.session.run(config) context.coordinator.attach(to: view) return view } func updateUIView(_ uiView: ARSCNView, context: Context) {} static func dismantleUIView(_ uiView: ARSCNView, coordinator: Coordinator) { uiView.session.pause() coordinator.stop() } final class Coordinator: NSObject, ARSCNViewDelegate, CLLocationManagerDelegate { private let locationManager = CLLocationManager() private let motionManager = CMMotionManager() private weak var sceneView: ARSCNView? private var heading: CLLocationDirection = 0 private var coordinate: CLLocationCoordinate2D? @Binding private var sunNodesReady: Bool init(sunNodesReady: Binding) { _sunNodesReady = sunNodesReady super.init() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.headingFilter = 1 locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() locationManager.startUpdatingHeading() startMotion() } func attach(to sceneView: ARSCNView) { self.sceneView = sceneView addSunPathNodesIfPossible() } func stop() { motionManager.stopDeviceMotionUpdates() locationManager.stopUpdatingHeading() locationManager.stopUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard coordinate == nil, let location = locations.last else { return } coordinate = location.coordinate addSunPathNodesIfPossible() } func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { heading = newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading addSunPathNodesIfPossible() } private func startMotion() { guard motionManager.isDeviceMotionAvailable else { return } motionManager.deviceMotionUpdateInterval = 0.1 motionManager.startDeviceMotionUpdates() } private func addSunPathNodesIfPossible() { guard let sceneView, let coordinate, !sunNodesReady else { return } let samples = SunMath.sunPathSamples(for: Date(), coordinate: coordinate) let sorted = samples.sorted { $0.key < $1.key } let root = SCNNode() let northOffset = (heading).radians let radius: Float = 1.8 for (_, pos) in sorted { let elevation = Float(pos.elevation.radians) let azimuth = Float((pos.azimuth).radians) - Float(northOffset) let x = radius * cos(elevation) * sin(azimuth) let y = radius * sin(elevation) let z = -radius * cos(elevation) * cos(azimuth) let sphere = SCNSphere(radius: 0.03) sphere.firstMaterial?.diffuse.contents = UIColor.systemYellow let node = SCNNode(geometry: sphere) node.position = SCNVector3(x, y, z) root.addChildNode(node) } sceneView.scene.rootNode.addChildNode(root) sunNodesReady = true } } } private extension Double { var radians: Double { self * .pi / 180.0 } }