SwiftUI: Publishing changes from within view updates is not allowed, this will cause undefined behavior (when using `ViewModel` approach)
Asked Answered
D

3

21

I've read a number of questions about this error that relates to dismissing sheets, but none dealing with SwiftUI's Map. The following code generates this error. Nothing is being updated in the view model. I'm simply passing a binding to a region into the Map initializer. Using a local state variable for region works with no error. I'm running Xcode 14.0. If I remove the @Published property wrapper then the error goes away. So I'm confused as to how the view model should notify the view that the region has changed, perhaps due to location updates.

import SwiftUI
import MapKit

class MM : ObservableObject {
    @Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
}

struct SimpleMap: View {
    @ObservedObject var mm = MM()
    @State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))

    var body: some View {
        //Error
        Map(coordinateRegion: $mm.region)
        
        //No Error
        //Map(coordinateRegion: $region)
    }
}

Desiderative answered 23/9, 2022 at 16:36 Comment(11)
Use StateObject instead of ObservedObjectMande
ObservableObject object initialised with @StateObject and when you pass it to other views, in those views you need to declare it with @ObservedObjectAdelbert
we don't use view model objects in SwiftUIHexachlorophene
@StateObject does not change a thing. Essentially, ObservableObjects with Published properties are not allowed to be reference bound since iOS16 and Swift 4. You need to use an @State property without ever touching it manually ever again.Parolee
So in iOS 16 using ObservableObject with Published is no longer correct? So what is the use of Published now?Crumley
@MartinMajewski would you be able to provide an official reference to the issue you mentioned, please.Christhood
@Desiderative have you solved this issue?Singspiel
@Singspiel I've switched to using MKMapView. Swift UI's Map is just too limited and perhaps only applicable for simple applicationsDesiderative
@Martin Majewski I believe you are right, but I don't understand your proposed solution. Would you mind to write a short answer related to the example code?Lizzielizzy
Oh, I'm sorry, I overlooked your questions. My response was more on the sarcastic side. In October of 2022, Xcode had this bad circular reference issue that drove me crazy, too. The only solution was to either use @State and never wrote anything to it manually (sarcastic b/c useless!) or to go with the UIKit-version of MapKit and wrap it into a ViewRepresentable. I went with the latter option. With Xcode 14.2, this supposed bug seems resolved for the most part. Maybe the SwiftUI MapKit version is acceptable to use now?Parolee
There's ai a bit of incorrect information in these comments. In iOS 16 using ObservableObject with Published is still allowed, and necessary if you have to be compatible with older OSs. In iOS 16 you don't use Published if you use the Observable macro. The code above is technically correct, but you could just use a second state var for mm.region instead of putting it in a viewmodel.Imbibition
D
0

You need to add @MainActor attribute to the ViewModel, and replace @ObervedObject with @StateObject.

Here's an example of how it could be implemented:

import SwiftUI
import MapKit

struct SimpleMap: View {
    @StateObject var vm = VM()

    var body: some View {
        Map(coordinateRegion: $vm.region)
    }
}

extension SimpleMap {
    @MainActor class VM: ObservableObject {
        @Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
    }
}

The most important thing here is the @MainActor attribute.

Downhill answered 31/10, 2023 at 10:14 Comment(0)
S
0

Did you try updating your code like this?

class MM : ObservableObject {
    @Published var _region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
    var region: MKCoordinateRegion {
        get { _region }
        set {
            DispatchQueue.main.async {
                self._region = newValue
            }
        }
    }
}

struct SimpleMap: View {
    @StateObject var mm = MM()
    
    var body: some View {
        //Error
        Map(coordinateRegion: $mm.region)
    }
}

Making changes to the Publishers or State variables while the view is being updated, causes the issue. To prevent the issue, we should update the Publisher values asynchronously in the main queue. Let me know if this doesn't work.

Smog answered 6/7 at 19:9 Comment(0)
W
0

Why does this happen?

No matter what you do it is just a working around solution because this warnings is a reported issue of SwiftUI.Map prior to iOS 17.0.

Publishing changes from within view updates is not allowed, this will cause undefined behavior.

Binding of region:

Whenever the map received a new center, an example from (0, 0) to (10, 10), the map internally sets the region to a sequence of values as below to animate the change [(1, 1), (2, 2), ..., (10, 10)].

You can try replacing the Binding with a custom to get a closer look.

Map(
    coordinateRegion: Binding(get: {
        print("Get region: \(region)")
        region
    }, set: { newValue in
        print("Set region: \(newValue)")
        region = newValue
    })
)

MapAnnotation:

If the map was updated, the visible map annotations will get updated which is totally fine. But MapAnnoation somehow modifies the internal states of the map causing the warning.

Solutions

iOS 17.0 and above

Please use the new API, this is the documentation.

iOS 16.0 and lower

final class MapViewModel: ObservableObject {
    var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 48.876192, longitude: 2.333455),
        latitudinalMeters: 3000,
        longitudinalMeters: 3000
    )
}

struct MapView: View {
    @StateObject var viewModel = MapViewModel()
    
    var body: some View {
        Map(
            coordinateRegion: $viewModel.region,
            annotationItems: viewModel.annotationItems,
            annotationContent: annotation(for:)
        )
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .ignoresSafeArea()
    }
}
Waggle answered 1/8 at 8:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.