Xcode 14 “Publishing changes from within view updates is not allowed, this will cause undefined behavior”
Published on: September 7, 2022UPDATE FOR XCODE 14.1: This issue appears to have been partially fixed in Xcode 14.1. Some occurences of the warning are fixed, others aren't. In this post I'm collecting situations me and others run into and track whether they are fixed or not. If you have another sample that you think is similar, please send a sample of your code on Twitter as a Github Gist.
Dear reader, if you've found this page you're probably encountering the error from the post title. Let me start by saying this post does not offer you a quick fix. Instead, it serves to show you the instance where I ran into this issue in Xcode 14, and why I believe this issue is a bug and not an actual issue. I've last tested this with Xcode 14.0's Release Candidate. I've filed feedback with Apple, the feedback number is
FB11278036
in case you want to duplicate my issue.
Some of the SwiftUI code that I've been using fine for a long time now has recently started coming up with this purple warning.
Initially I thought that there was a chance that I was, in fact, doing something weird all along and I started chipping away at my project until I had something that was small enough to only cover a few lines, but still complex enough to represent the real world.
In this post I've collected some example of where I and other encounter this issue, along with whether it's been fixed or not.
[Fixed] Purple warnings when updating an @Published var from a Button in a List.
In my case, the issue happened with the following code:
class SampleObject: ObservableObject {
@Published var publishedProp = 1337
func mutate() {
publishedProp = Int.random(in: 0...50)
}
}
struct CellView: View {
@ObservedObject var dataSource: SampleObject
var body: some View {
VStack {
Button(action: {
dataSource.mutate()
}, label: {
Text("Update property")
})
Text("\(dataSource.publishedProp)")
}
}
}
struct ContentView: View {
@StateObject var dataSource = SampleObject()
var body: some View {
List {
CellView(dataSource: dataSource)
}
}
}
This code really does nothing outrageous or weird. A tap on a button will simply mutate an @Published
property, and I expect the list to update. Nothing fancy. However, this code still throws up the purple warning. Compiling this same project in Xcode 13.4.1 works fine, and older Xcode 14 betas also don't complain.
At this point, it seems like this might be a bug in List
specifically because changing the list to a VStack
or LazyVStack
in a ScrollView
does not give me the same warning. This tells me that there is nothing fundamentally wrong with the setup above.
Another thing that seems to work around this warning is to change the type of button that triggers the action. For example, using a bordered button as shown below also runs without the warning:
Button(action: {
dataSource.mutate()
}, label: {
Text("Update property")
}).buttonStyle(.bordered)
Or if you want your button to look like the default button style on iOS, you can use borderless
:
Button(action: {
dataSource.mutate()
}, label: {
Text("Update property")
}).buttonStyle(.borderless)
It kind of looks like anything except a default Button
in a List
is fine.
For those reasons, I sadly cannot give you a proper fix for this issue. The things I mentioned are all workarounds IMO because the original code should work. All I can say is please file a feedback ticket with Apple so we can hopefully get this fixed, documented, or otherwise explained. I'll be requesting a code level support ticket from Apple to see if an Apple engineer can help me figure this out.
Animating a map's position in SwiftUI
A Map in SwiftUI is presented using the following code:
struct ContentView: View {
@State var currentMapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 10.0, longitude: 0.0), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
var body: some View {
VStack {
Map(coordinateRegion: $currentMapRegion, annotationItems: allFriends) { friend in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
}
}
.ignoresSafeArea()
}
}
Notice how the Map
takes a Binding
for its coordinateRegion
. This means that whenever the map changes what we're looking at, our @State
can update and the other way around. We can assign a new MKCoordinateRegion
to our @State
property and the Map
will update to show the new location. It does this without animating the change. So let's say we do want to animate to a new position. For example, by doing the following:
var body: some View {
VStack {
Map(coordinateRegion: $currentMapRegion, annotationItems: allFriends) { friend in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: friend.cityLatitude ?? 0, longitude: friend.cityLongitude ?? 0)) {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
}
}
.ignoresSafeArea()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
currentMapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 80, longitude: 80),
span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
}
}
}
}
This code applies some delay and then eventually moves the map to a new position. The animation could also be triggered by a Button
or really anything else; how we trigger the animation isn't the point.
When the animation runs, we see lots and lots of warnings in the console (187 for me...) and they all say [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
.
We're clearly just updating our currentMapRegion
just once, and putting print
statements in the onAppear
tells us that the onAppear
and the withAnimation
block are all called exactly once.
I suspected that the Map
itself was updating its binding to animate from one position to the next so I changed the Map
setup code a little:
Map(coordinateRegion: Binding(get: {
self.currentMapRegion
}, set: { newValue, _ in
print("\(Date()) assigning new value \(newValue)")
self.currentMapRegion = newValue
}), annotationItems: allFriends) { friend in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: friend.cityLatitude ?? 0, longitude: friend.cityLongitude ?? 0)) {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
}
Instead of directly binding to the currentMapRegion
property, I made a custom instance of Binding
that allows me to intercept any write operations to see how many occur and why. Running the code with this in place, yields an interesting result:
2022-10-26 08:38:39 +0000 assigning new value MKCoordinateRegion(center: __C.CLLocationCoordinate2D(latitude: 62.973218679210305, longitude: 79.83448028564462), span: __C.MKCoordinateSpan(latitudeDelta: 89.49072082474844, longitudeDelta: 89.0964063502501))
2022-10-26 10:38:39.169480+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.169692+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.169874+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 08:38:39 +0000 assigning new value MKCoordinateRegion(center: __C.CLLocationCoordinate2D(latitude: 63.02444217894995, longitude: 79.96021270751967), span: __C.MKCoordinateSpan(latitudeDelta: 89.39019889305074, longitudeDelta: 89.09640635025013))
2022-10-26 10:38:39.186402+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.186603+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.186785+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 08:38:39 +0000 assigning new value MKCoordinateRegion(center: __C.CLLocationCoordinate2D(latitude: 63.04063284402105, longitude: 80.00000000000011), span: __C.MKCoordinateSpan(latitudeDelta: 89.35838016069978, longitudeDelta: 89.0964063502501))
2022-10-26 10:38:39.200000+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.200369+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
2022-10-26 10:38:39.200681+0200 MapBug[10097:899178] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
This is just a small part of the output of course but we can clearly see that the print
from the custom Binding
is executed in between warnings.
I can only conclude that this has to be some issue in Map
that we cannot solve ourselves. You might be able to tweak the custom binding a bunch to throttle how often it actually updates the underlying @State
but I'm not sure that's what we should want...
If you're seeing this issue too, you can reference FB11720091
in feedback that you file with Apple.
Huge thanks to Tim Isenman for sending me this sample.