Publishing changes from within MAP() view updates is not allowed, this will cause undefined behavior

164 Views Asked by At

I've seen similar questions on this but none that are up to date or work successfully, I've followed Apples Developer docs, various videos, and guides on how to do this but it still throws these warnings

2023-01-11 11:33:16.044044-0500 Walkman[50986:4824621] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.

It feels like I've tried everything from onChange() to @StateObject instead of @ObservedObject... It's those few lines because the warnings go away if the Map gets commented out.

The code that's causing the error is the

struct OrderView: View {
    @ObservedObject var cartManager: CartManager
//    @ObservedObject var locationManager: LocationManager
    
    @EnvironmentObject var locationManager: LocationManager
    @EnvironmentObject var viewModel: AppViewModel
    @State private var tracking:MapUserTrackingMode = .follow
    @State private var driversIn: Bool = false
    
    var body: some View {
        NavigationView {
//            Text("Testing")
            Map(
                coordinateRegion: $locationManager.region,
                interactionModes: MapInteractionModes.all,
                showsUserLocation: true,
                userTrackingMode: $tracking,
                annotationItems: locationManager.MapLocations,
                annotationContent: { item in
                    MapMarker(coordinate: item.coordinate, tint: .red)
//                    MapAnnotation(coordinate: item.location) {
//                        Rectangle()
//                            .stroke(Color.purple, lineWidth: 3)
//                            .frame(width: 10, height: 10)
//                    }
                }
            )
            .navigationTitle("Orders")

            .onAppear {
                locationManager.getNearbyDrivers(query: GeoQuery(distance: 400, geo: locationManager.createGeo()))
            }
            .ignoresSafeArea()
//            .onReceive(locationManager.$drivers, perform: { drivers in
////                print("DEBUG: DRIVERS _ \(drivers)")
//                for driver in drivers {
//                    print("DEBUG: DRIVER TEST _ \(driver)")
//                    locationManager.MapLocations.append(MapLocation(id: driver.id, name: driver.name, latitude: driver.location.coordinates[0], longitude: driver.location.coordinates[1]))
//                }
//            })
        }
        
        
    }
}

Here is my LocationManager

final class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
    @Published var region = MKCoordinateRegion(center: MapDetails.startingLocation, span: MapDetails.defaultSpan)
    @Published var authorizationStatus: CLAuthorizationStatus?
    @Published var location: CLLocation?
    @Published var MapLocations = [MapLocation]()
    
    @Published var drivers = [Driver]()
    var locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse:
            authorizationStatus = .authorizedWhenInUse
            manager.startUpdatingLocation()
            break
        case .restricted, .denied:
            authorizationStatus = .restricted
            manager.stopUpdatingLocation()
            break
        case .notDetermined:
            authorizationStatus = .notDetermined
            manager.requestWhenInUseAuthorization()
            break
        default:
            break
        }
    }
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        DispatchQueue.main.async {
            self.location = location
//            self.MapLocations.append(MapLocation(name: "1", latitude: location.coordinate.latitude, longitude: location.coordinate.longitude))
            // MARK - API for location calls, send when users location updates
            print("DEBUG: Location: \(self.location?.coordinate.longitude ?? 0.00)")
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("DEBUG: \(error.localizedDescription)")
    }
    
    func createGeo() -> GeoJSON {
        return GeoJSON(coordinates: [locationManager.location?.coordinate.latitude ?? 0.00, locationManager.location?.coordinate.longitude ?? 0.00])
    }

    
    func getNearbyDrivers(query: GeoQuery) {
        guard let url = URL(string: "http://localhost:8000/drivers-nearby") else { fatalError("Missing URL") }
        
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        let payload: [String: Any] = [
            "distance": query.distance,
            "geo": ["type": "Point", "coordinates": query.geo.coordinates]
        ]
        let data = try! JSONSerialization.data(withJSONObject: payload, options: [])
        request.httpBody = data

        let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                print("Request error: ", error)
                return
            }
            guard let response = response as? HTTPURLResponse else { return }
            
            if response.statusCode == 200 {
                guard let data = data else { return }
                DispatchQueue.main.async {
                    do {
                        let drivers = try JSONDecoder().decode([Driver].self, from: data)
                        
                        print("DEBUG: Drivers: \(drivers)")
                        self.drivers = drivers
                        for driver in self.drivers {
                            let location = MapLocation(id: driver.id, name: driver.name, latitude: driver.location.coordinates[0], longitude: driver.location.coordinates[1])
                            var driverIsVisible: Bool {
                                return self.MapLocations.contains(where: { annotation in
                                    guard var driverAnno = annotation as? MapLocation else { return false }
                                    print("DEBUG: cast driverAnno | driverAnno \(driverAnno.id) | driver \(driver.id)")
                                    if driverAnno.id == driver.id {
                                        print("DEBUG: Handle update driver position")
                                        driverAnno.latitude = driver.location.coordinates[0]
                                        driverAnno.longitude = driver.location.coordinates[1]
                                        return true
                                    }
                                    return false
                                })
                            }
                            
                            if !driverIsVisible {
                                print("DEBUG: Inserting map annotation \(location.id)")
                                self.MapLocations.append(location)
                            }
                        }
                        
                    } catch let error {
                        print("Error decoding: ", error)
                    
                    }
                }
            }
        }
        
        dataTask.resume()
    }
    

    
////    private func geocode() {
////        guard let location = self.location else { return }
////        geocoder.reverseGeocodeLocation(location) { (places, error) in
////            if error == nil {
////                self.placemark = places?[0]
////            } else {
////                self.placemark = nil
////            }
////        }
////    }
}
2

There are 2 best solutions below

0
On

I had a similar issue, and all the web answers seem to suggest this is an Apple bug which might be true but there is a workaround.

Patrick above already hinted at the culprit, it has to do with @Published variable, but removing it is not the correct solution for you. The problem is because region is a @Published variable and it is also used as a Binding variable (i.e. $locationManager.region in your code). There is nothing wrong to have code pattern like this, but I am guessing Map framework doesn't like it (thus the apple bug).

workaround is you need to have an intermediate step in the SwiftUI view that will act on the changes in the @Published variable and bind to the UI, so you don't bind the published variable directly to the coordinateRegion

example:

struct ContentView: View {
 @State var mapRegion: MKCoordinateRegion = .init(.null) // this is the intermediate step mentioned above, your Map will bind to this instead of @Published variable
....
.....
......
   Map(...) {
   }
   .onReceive(viewModel.$mapRegion, perform: { region in
        self.mapRegion = region // subscribe to the @published variable
   })
}
2
On

Removing @Published from region in LocationManager will fix this warning.