UIViewRepresentable.updateUIView(_:context:) is not called when @State, @Binding or @EnvironmentObject are changed.

I have a GameView that has "@State var locationManager: LocationManager" which is passed to my MapView as a binding. The MapView conforms to UIViewRepresentable protocol. LocationManager conforms, among other things, to ObservableObject and has the following conformance-related code:


var didChange = PassthroughSubject<locationmanager, never="">()


    var lastKnownLocation: CLLocation {
        didSet {
           
            // Propagate the update to the observers.
            didChange.send(self)
            print("Finished propagating lastKnownLocation to the observers.")
        }

}


I suppose that GameView and consequently MapView must be updated every time LocationManager.lastKnownLocation changes. In practice I only see MapView.updateUIView() called when I exit the app per home button. At this point control gets in updateUIView() and when I open the app again (not compile-install-run), I get the update. This also happens once short after the GameView() was presented.


Is my understanding of how SwiftUI works wrong or is it some bug? How do I get it right?

Replies

If LocationManager conforms to ObservableObject, then you need to use the @ObservedObject attribute when passing it down. @State and @Binding are intended for value types, and so they fire when the value they wrap is changed. In the case of your LocationManager class, it looking at the pointer value that provides the reference to your class's instance storage—this is why the update is firing when your app quits and the value is (I would assume) set to nil.


Change your variables to use @ObservedObject rather than @State and you should be golden—that wrapper works by subscribing to the value publisher.


Last thing: ObservableObject can largely be automated for you: instead of declaring the didChange property yourself you can use the @Published attribute on any contents of your LocationManager to have all that synthesized for you.

Thank you very much, it has cleared my understanding. However that did not help, unfortunately. I have changed @State and @Binding to @ObservedObject and nothing changed. Then I have also updated the code to use "@Published" and it did not help either. Here is my code before the changes:


struct GameView: View { 
  @State var locationManager: LocationManager 


  var body: some View { 
  MapView(locationManager: $locationManager)
  }
}

struct MapView: UIViewRepresentable {

  @Binding var locationManager: LocationManager

  func makeUIView(context: Context) -> GMSMapView{ ... }

  func updateUIView(_ mapView: GMSMapView, context: Context) { ... }

}

class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {

  var didChange = PassthroughSubject<locationmanager, never="">()

  var lastKnownLocation: CLLocation {
  didSet {


  // Propagate the update to the observers.
  didChange.send(self)
  print("Finished propagating lastKnownLocation to the observers.")
  }
  }

  ...

}


And here it is after the proposed changes:


struct GameView: View { 
  @ObservedObject var locationManager: LocationManager 


  var body: some View { 
  MapView(locationManager: locationManager)
  }
}

struct MapView: UIViewRepresentable {

  @ObservedObject var locationManager: LocationManager

  func makeUIView(context: Context) -> GMSMapView{ ... }

  func updateUIView(_ mapView: GMSMapView, context: Context) { ... }

}

class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {

  @Published var lastKnownLocation: CLLocation
  ...

}

Today the thing worked as intended and I don't know why. Now it still does not, I suppose it is because I've made locationManagers @EnvironmentObject instead of @ObserverdObject. Would @EnvironmentObject pass for my needs?

New data: the view is updated if I change the device orientation (horizontal/vertical).

Seeing the same thing and can't figure it out.

Using `@EnvironmentObject`, but shouldn't matter.

And can confirm changing orientation, and likely most environment value, does trigger `updateUIView` to call.

I'm seeing this issue too.
For me, I'm passing down a @Binding of a @Published var to a View which is then nested within a UIHostingController.
I'm wondering if nesting in the UIHostingController is somehow breaking the connection between the binding and published var.
Anyone have any luck with this?

I managed to solve my issue.
I was embedding my SwiftUI View as a UIHostingController within a UIViewControllerRepresentable as a child view controller. I was also passing in other UIViewControllers to the UIViewControllerRepresentable which were embedded alongside the SwiftUI content. The UIViewControllers kept a reference to the parent UIViewController (UIViewControllerRepresentable) inside a variable so they could call helper methods. For some reason when I removed this reference the SwiftUI content went back to updating correctly to the @Binding. I then ported the methods to update to state changes inside `updateUIViewController` instead of calling them directly.
It was a bit of a specific case, and I'm not entirely sure why that reference was breaking the state changes, but hopefully someone finds this helpful.

  • I've run into a similar situation as you're describing. However, I am unclear how it was resolved based on your description. Would it be possible for you to share your UIViewControllerRepresentable?

    Thanks!

Add a Comment

In my case, a published property in an environment object is passed into a map view wrapped in UIViewRepresentable, and used in makeUIView(). When the property changes, it does not trigger creation of a new map view. Looks like a SwiftUI 🐞

Having the same problem. Regardless of whether I'm using ObservedObject or EnvironmentObject. Eventhough I see the position updating (the trace below is coming), there is no call to updateUIView, except the view is opened the first time or if the orientation changes.


public let objectWillChange = PassthroughSubject<Position,Never>()

@Published var position = Position() {

willSet {

print("Update position", newValue)

objectWillChange.send(newValue)

}

}

I made a ready-to-build skeleton project that clearly reproduces, and offers a workaround, to what I believe is the same issue:


https://github.com/mediweb/UIViewRepresentableBug


Executive summary:


  • a `struct` UIViewRepresentable's `updateUIView` does not get called when it should (in my demo: when an @ObservedObject changes).
  • but with a `final class` UIViewRepresentable, it does get called as expected.
  • This happens in Xcode 11.3.1 and 11.4-beta.


This indeed seems like a SwiftUI bug. I filed a bug report using the Feedback Assistant.


Note: I originally posted this issue on the Swift.org forums before being pointed here:

https://forums.swift.org/t/uiviewrepresentable-not-updated-when-observed-object-changed/33890/8