4

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).

1 Answer 1

1

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
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

I can see where you're going with this. I need to check the Apple forum thread in detail. Your specific code did not work for me. Leaving it as-is, I get lots of compiler errors about type Any. So I did @State private var annotation = [CatalogItem](). That takes away the errors about Any, but leaves me with 1 compiler error saying: Instance method 'onReceive(_:perform:)' requires that 'Binding<[CatalogItem]>' conform to 'Publisher'.
My mistake. I corrected the annotation declaration to @State private var annotation: [CatalogItem] = []. That removed all the compiler errors. Unfortunately, the runtime warnings remain the same with the onReceive async view modifier.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.