Mike
Mike

Reputation: 91

Model updates trigger "Publishing changes from within view updates is not allowed" error when using Map in SwiftUI

I am using the code below to do the following.

  1. Create a new item every 5 seconds and append it to the model
  2. Display a list of items in the listView
  3. Display a map of the items in the mapView

If I am in the listView, the list gets properly updated every 5 seconds with the new item. No error message. If I am in the mapView, the map also gets updated (a new marker every 5 seconds), but I get error "[SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior." Since list and map both display the same model data, I wonder why map complains and list does not. The actual model update is on the main actor, so why is it complaining.

Any idea?

//Model
struct TestApp1Model {
    struct TestItem: Identifiable {
        var id = UUID()
        var name: String
        var latitude: Double
        var longitude: Double
    }

    var items = [TestItem]()
}

// ViewModel
class TestApp1ViewModel: ObservableObject {
    @Published private var model = TestApp1Model()
    private var timer:Timer?
 
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
            Task { @MainActor in
                self.addItem()
            }
        }
    }
    
    var items:[TestApp1Model.TestItem] {
        model.items
    }
    
    @MainActor func addItem () {
        let name = "Item " + model.items.count.description
        let latitude = Double.random(in: 45...55)
        let longitude = Double.random(in: 5...11)
        model.items.append(TestApp1Model.TestItem(name: name, latitude: latitude, longitude: longitude))
    }
}

// View
struct TestApp1View: View {
    @StateObject var testVM = TestApp1ViewModel()
    @State var region:MKCoordinateRegion
    
    init() {
        self.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 50, longitude: 8), span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 6))
    }
    
    var body: some View {
        TabView {
            listView
                .tabItem {
                    Image(systemName: "list.bullet")
                    Text("List")
                }
                .backgroundStyle(Color.white)
            mapView
                .tabItem {
                    Image(systemName: "map")
                    Text("Map")
                }
                .backgroundStyle(Color.white)
        }
    }
    
    var listView: some View {
        VStack {
            List (testVM.items) { item in
                HStack {
                    Text(item.name)
                    Text(item.latitude.description)
                    Text(item.longitude.description)
               }
            }
        }
    }

    var mapView: some View {
        Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true,annotationItems: testVM.items) {item in
            MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)) {
                Image(systemName: "plus")
                    .foregroundColor(.red)
            }
        }
        .ignoresSafeArea()
    }
}

Upvotes: 1

Views: 2811

Answers (4)

Tony
Tony

Reputation: 816

Add a manual binding with its setter working in the main thread.

This code was producing the same warning:

struct AreaMap: View {
    @Binding var region: MKCoordinateRegion
    var body: some View {
        Map(coordinateRegion: $region)
    }
}

Where region was passed in the initializer of AreaMap from its parent view.

And this resolved the issue for me:

struct AreaMap: View {
    @Binding var region: MKCoordinateRegion
    var body: some View {
        let binding = Binding(
            get: { self.region },
            set: { newValue in
                DispatchQueue.main.async {
                    self.region = newValue
                }
            }
        )
        return Map(coordinateRegion: binding)
    }
}

Upvotes: 0

blu-Fox
blu-Fox

Reputation: 656

I'm having the same issue with Map(). It started appearing with Xcode 14. Other people see this run-time warning in other scenarios. For now, I assume it's a bug that will be fixed in future Xcode updates.

See: https://developer.apple.com/forums/thread/711899

And: https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/

Upvotes: 0

JohnH
JohnH

Reputation: 251

I've come across similar issues with SwiftUI's Map to the point where I'm convinced it's a bug. In the cases I've encountered the warning message appears once for each annotation on the map, even when the annotations aren't changing but the view is being redrawn due to a change in a published property. I'm giving up on SwiftUI's Map. Going back to MKMapView - better support and all these warnings go away.

Upvotes: 1

Olle Ekberg
Olle Ekberg

Reputation: 824

You should publish your array of items, not the model.

struct TestItem: Identifiable {
    let id = UUID()
    var name: String
    var latitude: Double
    var longitude: Double
}

class Model: ObservableObject {
    @Published var items = [TestItem]()
    private var timer: Timer?

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
            Task { @MainActor in
                self.addItem()
            }
        }
    }

    @MainActor func addItem () {
        let name = "Item " + items.count.description
        let latitude = Double.random(in: 45...55)
        let longitude = Double.random(in: 5...11)
        items.append(TestItem(name: name, latitude: latitude, longitude: longitude))
    }
}

Now your views will update each time a new item is added.

Upvotes: 1

Related Questions