puzzledbeginner
puzzledbeginner

Reputation: 41

How to re-render SwiftUI Map with MapAnnotation without runtime warnings for "undefined behavior"?

In a SwiftUI app I have a MapView struct with the body containing a Map View with items based on an observed object (the data model). The MapFilterView in the sheet allows the user to toggle the displayed item categories in the data model. Map correctly reacts to a published change in the observed object by re-rendering itself. If Map displays MapMarkers for the items, no runtime warnings are shown when var body re-renders after a change of the observed object. However, as soon as I replace MapMarker with MapAnnotation and the body re-renders, I get "[SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior." This warning is shown for each item that was visible in the Map before the re-render.

My goal is to have no runtime warnings and some sort of marker on the map, where a tap on the marker pushes to a detail view of the item. MapMarker does not seem to allow navigation. MapAnnotation can include a NavigationLink, but all variants of MapAnnotation that I have tried - with or without NavigationLink - generate the above runtime warning. Is it because of the trailing closure of the MapAnnotation? Is it because MapAnnotation has as Content a SwiftUI View, whereas MapMarker and MapPin are only structs without a View? Can anyone suggest a workaround with no runtime warnings and a working push navigation?

Code with MapMarker, no runtime warnings

import SwiftUI
import MapKit

struct ItemAnnotationView: View {
    let mapItem: CatalogItem
    var body: some View {
        Image(systemName: "mappin.circle.fill")
            .resizable()
            .scaledToFit()
            .foregroundColor(Color(categoryColours[mapItem.catalogType]!))
            .frame(width: 25, height: 25)
    }
}

struct MapView: View {
    
    @EnvironmentObject var dataModel: DataModel

    @State private var showingFilter = false
    @State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 44.65, longitude: 4.42), latitudinalMeters: 75000, longitudinalMeters: 75000)

    var locationManager: Void = CLLocationManager()
        .requestWhenInUseAuthorization()
    
    var body: some View {
        let _ = Self._printChanges()
        // REMOVE FOR FINAL VERSION
        NavigationView {
            ZStack (alignment: .bottomTrailing){
                Map(coordinateRegion: $region, showsUserLocation: true, annotationItems: dataModel.shownItems)
                    { mapitem in
                        MapMarker(coordinate: mapitem.mapLocation, tint: Color(categoryColours[mapitem.catalogType]!))
                    }

                Button(action: {
                    showingFilter.toggle()
                }){
                        Label("Categories", systemImage: "checkmark.circle")
                    }
                    .padding(6)
                    .background(Color.black)
                    .font(.footnote)
                    .foregroundColor(.white)
                    .clipShape(Capsule())
                    .offset(x: -10, y: -35)
            }
            .navigationTitle("Map")
            
            WelcomeView(viewType: .map)
        }
        .sheet(isPresented: $showingFilter) {
            MapFilterView(showMapFilterView: $showingFilter)
        }
    }
}

As soon as I replace

MapMarker(coordinate: mapitem.mapLocation, tint: Color(categoryColours[mapitem.catalogType]!))

with

MapAnnotation(coordinate: mapitem.mapLocation) { ItemAnnotationView(mapItem: mapitem) }

the runtime warnings are generated for the already displayed annotations.

I originally had the code below with a working NavigationLink to a detail view, but because it also relies on MapAnnotation, it generates the same runtime warnings.

                        MapAnnotation(coordinate: mapitem.mapLocation) {
                            NavigationLink {
                                ItemDetail(item: mapitem)
                            } label: {
                            Image(systemName: "mappin.circle.fill")
                                .resizable()
                                .scaledToFit()
                                .foregroundColor(Color(categoryColours[mapitem.catalogType]!))
                                .frame(width: 25, height: 25)
                            }
                        }

Any ideas on how to circumvent the runtime warnings and have a working push navigation to a detail view?

The issue with MapAnnotation occurs with Xcode 14.0.1 / iOS 16.0, as with Xcode 14.1 beta 2 / iOS 16.1 beta (20B5045d).

Upvotes: 4

Views: 1614

Answers (1)

Alex Valter
Alex Valter

Reputation: 121

As far as I understood this problem occurred in all versions of Xcode 14.0.1

Try the following:

struct MapView: View {
    @EnvironmentObject var dataModel: DataModel
    @State private var showingFilter = false
    @State private var annotation = []
    @State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 44.65, longitude: 4.42), latitudinalMeters: 75000, longitudinalMeters: 75000)

    var locationManager: Void = CLLocationManager()
        .requestWhenInUseAuthorization()
    
    var body: some View {
        let _ = Self._printChanges()
        // REMOVE FOR FINAL VERSION
        NavigationView {
            ZStack (alignment: .bottomTrailing){
                Map(coordinateRegion: $region, showsUserLocation: true, annotationItems: annotation)
                    { mapitem in
                        MapAnnotation(coordinate: mapitem.mapLocation) {
                            NavigationLink {
                                ItemDetail(item: mapitem)
                            } label: {
                            Image(systemName: "mappin.circle.fill")
                                .resizable()
                                .scaledToFit()
                                .foregroundColor(Color(categoryColours[mapitem.catalogType]!))
                                .frame(width: 25, height: 25)
                            }
                        }
                    }
                    .onReceive(dataModel.$shownItems) { receive in
                        self.annotation = receive
                    }


                Button(action: {
                    showingFilter.toggle()
                }){
                        Label("Categories", systemImage: "checkmark.circle")
                    }
                    .padding(6)
                    .background(Color.black)
                    .font(.footnote)
                    .foregroundColor(.white)
                    .clipShape(Capsule())
                    .offset(x: -10, y: -35)
            }
            .navigationTitle("Map")
            
            WelcomeView(viewType: .map)
        }
        .sheet(isPresented: $showingFilter) {
            MapFilterView(showMapFilterView: $showingFilter)
        }
    }
}

But this may not solve the problem and you have to wait for Apple to fix it or in your case it will work. At the very least

Also Please check this article https://developer.apple.com/forums/thread/711899

Regards

Alex Valter

UPD...

I think I figured it out! The thing is that in the new version of Xcode for some reason the behavior of the standard animation elements for some reason is very different, perhaps the swiftui team in the future will explain it. Here is the solution when we try to update an EnvironmentObject we need to wrap it in a synchronous thread

Just edit my code above in one line

    .onReceive(dataModel.$shownItems) { receive in
     DispatchQueue.main.async {
      self.annotation = receive
    }
}

Upvotes: 1

Related Questions