11

In this specific case, when I try to change an @EnvironmentObject's @Published var, I find that the view is not invalidated and updated immediately. Instead, the change to the variable is only reflected after navigating away from the modal and coming back.

import SwiftUI

final class UserData: NSObject, ObservableObject  {
    @Published var changeView: Bool = false
}

struct MasterView: View {
    @EnvironmentObject var userData: UserData
    @State var showModal: Bool = false

    var body: some View {
        Button(action: { self.showModal.toggle() }) {
            Text("Open Modal")
        }.sheet(isPresented: $showModal, content: {
            Modal(showModal: self.$showModal)
                .environmentObject(self.userData)
        } )
    }
}

struct Modal: View {
    @EnvironmentObject var userData: UserData
    @Binding var showModal: Bool

    var body: some View {
        VStack {
            if userData.changeView {
                Text("The view has changed")
            } else {
                Button(action: { self.userData.changeView.toggle() }) {
                    Text("Change View")
                }
            }
        }
    }
}



#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MasterView().environmentObject(UserData())
    }
}
#endif

video showing view changing only after modal is dismissed

Is this a bug or am I doing something wrong?

This works if changeView is a @State var inside Modal. It also works if it's a @State var inside MasterView with a @Binding var inside Modal. It just doesn't work with this setup.

Zain
  • 1,569
  • 1
  • 13
  • 19
  • 1
    Probably I have the same problem. When I remove the willSet in favor of Published something doesn't work anymore – Giuseppe Sapienza Aug 15 '19 at 15:32
  • @GiuseppeSapienza, check out my answer and see if it helps you. I was facing a issue after beta 5 with updating a `List` and this fixed it. –  Aug 15 '19 at 16:42
  • @dfd thanks now is working (p.s iOS 13 beta 7 released few minutes ago) – Giuseppe Sapienza Aug 15 '19 at 17:52
  • 1
    @GiuseppeSapienza, glad to help. This is the first year I've been this involved with betas. I was surprised when iOS beta 6 was released last week (and now beta 7?) but have been wondering it Xcode beta 6 isn't scheduled (like the others) every two weeks or if they are having some issue. It's been frustrating with how quickly `SwiftUI` and/or `Combine` changes were happening for seemingly no reason. This one? Ought to be interesting if iOS beta 7 fixes it, if Xcode beta 6 fixes it, or if it's intended behavior. –  Aug 15 '19 at 17:57
  • @dfd I thinks it's normal, it's like when apple released Swift, they changed a lot of stuff in every version. Anyway seems that the last beta (Xcode beta 7) doesn't fix this problem – Giuseppe Sapienza Aug 20 '19 at 15:53
  • @GiuseppeSapienza, I've been peripherally dealing with betas since 2016 (and remember using betas in 2014 when Swift was first announced) but there was a moment yesterday when iOS was beta 7, Calatlina was beta 6, and Xcode was beta 5 - odd. (And a day earlier? It was 7-5-5.) Xcode is now in beta 6, not 7. But I certainly don't blame you for the confusion! I found Xcode beta 6 to be *much* less painful for everything. Does the manual use of `objectWillChange` still work for you? If not, maybe post a question. –  Aug 20 '19 at 16:46

3 Answers3

8

A couple of things.

  • Your setup doesn't work if you move the Button into MasterView either.
  • You don't have a import Combine in your code (don't worry, that alone doesn't help).

Here's the fix. I don't know if this is a bug, or just poor documentation - IIRC it states that objectWillChange is implicit.

Along with adding import Combine to your code, change your UserData to this:

final class UserData: NSObject, ObservableObject  {
    var objectWillChange = PassthroughSubject<Void, Never>()
    @Published var changeView: Bool = false {
        willSet {
            objectWillChange.send()
        }
    }
}

I tested things and it works.

4

Changing

final class UserData: NSObject, ObservableObject  {

to

final class UserData: ObservableObject  {

does fix the issue in Xcode11 Beta6. SwiftUI does seem to not handle NSObject subclasses implementing ObservableObject correctly (at least it doesn't not call it's internal willSet blocks it seems).

Fabian
  • 5,040
  • 2
  • 23
  • 35
  • 1
    VERY nice and upvoted! I had the accepted answer for beta 5 (I think, it's a moving target) but didn't try removing `NSObject` in any way. Hey, my code worked - and this time around, that's a major thing! Now if I could only find a way to update my environment object via a `UIViewControllRepresentable` UIButton tap.... (@Fabian, you seem like one of the best - if you can do this, I'll post a question. Been trying things for days.) –  Aug 26 '19 at 21:12
  • Is the environment object being updated and just update for the views using the data not being called? Anyway I don't see a connection between the new question and the problem with `NSObject`. Is the environment object there also using `NSObject`? – Fabian Aug 27 '19 at 17:21
  • it's being updated. The full description is I have a `UINavigationController` with two `UIViewControllers` in the stack, sort of a master-detail thing. I need to (a) have an `Add` and `Edit` button in the nav bar, and I already can update/edit the table view that is bound to the model. It's the add button that obviously needs to update the state. I'd actually do this in "pure" SwiftUI except there are too many pain points for now, so I'm looking to trigger something in my model on a button tap.... continued.... –  Aug 27 '19 at 17:27
  • Here's the question I posted earlier today: Here's my question: https://stackoverflow.com/questions/57676251/how-can-i-have-a-uibutton-uiviewcontrollerrepresentable-trigger-an-action-in-m –  Aug 27 '19 at 17:28
1

In Xcode 11 GM2, If you have overridden objectWillChange, then it needs to call send() on setter of a published variable.

If you don't overridden objectWillChange, once the published variables in @EnvironmentObject or @ObservedObject change, the view should be refreshed. Since in Xcode 11 GM2 objectWillChange already has a default instance, it is no longer necessary to provide it in the ObservableObject.

Evan
  • 1,152
  • 1
  • 11
  • 18
  • This is the correct answer. In almost all cases, just remove your own declaration of `objectWillChange` and there's no need anymore to manually call `objectWillChange.send()` inside `willSet` of your `@Published` properties. – Fabian Streitel Nov 17 '19 at 14:17