how to update SwiftUI Map via MapCamera approach with data from @Observable? (see code)

Anyone able to see how to use the new SwiftUI Map and WWDC @Observable concept to dynamically update my SwiftUI Map position and rotation based on the dynamic changes it picks up from my @Observable object.

Note the updates are coming through as the Text labels show this. But how to get the Map position referencing the same values and updating them? The "onAppear" approach doesn't seem to work.

import SwiftUI
import MapKit

@Observable
final class NewLocationManager : NSObject, CLLocationManagerDelegate {
    var location: CLLocation? = nil
    var direction: CLLocationDirection = 0
    private let locationManager = CLLocationManager()
    
    func startCurrentLocationUpdates() async throws {
        if locationManager.authorizationStatus == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
        for try await locationUpdate in CLLocationUpdate.liveUpdates() {
            guard let location = locationUpdate.location else { return }
            print("NewLocationManager: \(location.coordinate.latitude), \(location.coordinate.longitude)")
            
            self.location = location
            self.direction = self.direction + 1
        }
    }
    
}

struct ContentView: View {
    var locationMgr = NewLocationManager()
    @State private var mapCamPos: MapCameraPosition = .automatic
    private let bigBen = CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.124570)
    
    var body: some View {
        ZStack {
            Map(position: $mapCamPos)
            .onAppear {       // Does NOT work - how to get position/direction updates working to Map (map should be moving/rotating)
                mapCamPos =  .camera(MapCamera(
                    centerCoordinate: self.locationMgr.location?.coordinate ?? bigBen,
                    distance: 800,
                    heading: self.locationMgr.direction
                ))
            }
            VStack (alignment: .leading) {
                Text("Location  from observable: \(locationMgr.location?.description ?? "NIL")")  // This works (they get updates regularly)
                Text("Direction from observable: \(locationMgr.direction)")                      // This works (they get updates regularly)
                Spacer()
            }
        }
        .task {
            try? await locationMgr.startCurrentLocationUpdates()
        }
        
    }
}

#Preview {
    ContentView()
}

Tag: wwdc2023-10043

Accepted Reply

Here you go

//
//  ContentView.swift
//  ForumMapsQuestion
//
//  Created by SB on 2024-01-05.
//

import SwiftUI
import MapKit

struct ContentView: View {
    
        @StateObject var locationMgr = NewLocationManager()
        @State private var mapCamPos: MapCameraPosition = .automatic
        
        var body: some View {
            ZStack {
                Map(position: $mapCamPos)
                    .onReceive(locationMgr.$direction) { direction in
                        mapCamPos =  .camera(MapCamera(
                            centerCoordinate: self.locationMgr.location.coordinate,
                            distance: 800,
                            heading: direction
                        ))
                    }
                    .onReceive(locationMgr.$location) { location in
                        mapCamPos =  .camera(MapCamera(
                            centerCoordinate: location.coordinate,
                            distance: 800,
                            heading: self.locationMgr.direction
                        ))
                    }
               
                VStack (alignment: .leading) {
                    VStack {
                        Text("Location  from observable: \(locationMgr.location.description)")
                        Text("Direction from observable: \(locationMgr.direction)")
                       
                    }
                    .padding()
                    .background(.gray)
                    .clipShape(RoundedRectangle(cornerSize: CGSize(width: 20, height: 10)))
                    .opacity(0.7)
                    Spacer()
                }
            }
        }
}

#Preview {
    ContentView()
}


final class NewLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    @Published var location: CLLocation = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.124570), altitude: .zero, horizontalAccuracy: .zero, verticalAccuracy: .zero, timestamp: Date.now)
    @Published var direction: CLLocationDirection = .zero
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.startUpdatingLocation()
        locationManager.delegate = self
        Task { [weak self] in
            try? await self?.requestAuthorization()
        }
    }
    
    func requestAuthorization() async throws {
        if locationManager.authorizationStatus == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
    }
    
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        locations.forEach { [weak self] location in
            Task { @MainActor [weak self]  in
                self?.location = location
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        Task { @MainActor [weak self]  in
            self?.direction = newHeading.trueHeading
        }
    }
    
}
  • @SB - Thank you for this code. I am learning SwiftUI Map() and this was very helpful. One note: the init() method needs "locationManager.startUpdatingHeading()" or the didUpdateHeading delegate is never called. Easy fi, but this took me a while to find!

Add a Comment

Replies

Here you go

//
//  ContentView.swift
//  ForumMapsQuestion
//
//  Created by SB on 2024-01-05.
//

import SwiftUI
import MapKit

struct ContentView: View {
    
        @StateObject var locationMgr = NewLocationManager()
        @State private var mapCamPos: MapCameraPosition = .automatic
        
        var body: some View {
            ZStack {
                Map(position: $mapCamPos)
                    .onReceive(locationMgr.$direction) { direction in
                        mapCamPos =  .camera(MapCamera(
                            centerCoordinate: self.locationMgr.location.coordinate,
                            distance: 800,
                            heading: direction
                        ))
                    }
                    .onReceive(locationMgr.$location) { location in
                        mapCamPos =  .camera(MapCamera(
                            centerCoordinate: location.coordinate,
                            distance: 800,
                            heading: self.locationMgr.direction
                        ))
                    }
               
                VStack (alignment: .leading) {
                    VStack {
                        Text("Location  from observable: \(locationMgr.location.description)")
                        Text("Direction from observable: \(locationMgr.direction)")
                       
                    }
                    .padding()
                    .background(.gray)
                    .clipShape(RoundedRectangle(cornerSize: CGSize(width: 20, height: 10)))
                    .opacity(0.7)
                    Spacer()
                }
            }
        }
}

#Preview {
    ContentView()
}


final class NewLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    @Published var location: CLLocation = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.124570), altitude: .zero, horizontalAccuracy: .zero, verticalAccuracy: .zero, timestamp: Date.now)
    @Published var direction: CLLocationDirection = .zero
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.startUpdatingLocation()
        locationManager.delegate = self
        Task { [weak self] in
            try? await self?.requestAuthorization()
        }
    }
    
    func requestAuthorization() async throws {
        if locationManager.authorizationStatus == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
    }
    
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        locations.forEach { [weak self] location in
            Task { @MainActor [weak self]  in
                self?.location = location
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        Task { @MainActor [weak self]  in
            self?.direction = newHeading.trueHeading
        }
    }
    
}
  • @SB - Thank you for this code. I am learning SwiftUI Map() and this was very helpful. One note: the init() method needs "locationManager.startUpdatingHeading()" or the didUpdateHeading delegate is never called. Easy fi, but this took me a while to find!

Add a Comment

thanks heaps - I'll try this shortly..... Couple of quick questions if I may:

a) Is the new WWDC2013 "@Observable" approach I was trying to use not quite able to handle this by the way, re why you've had to go to ObservableObject?

b) In terms of moving to SwiftUI, then should SwiftUI "Map" be able to do all one needs? Or may there be cases where you would want to stay with the custom MKMapView but using UIViewRepresentable to bring it into SwiftUI