iosswiftswiftuienvironmentobject

SwiftUI EnvironmentObject does not redraw the current view but its parent view


I'm trying to understand how @EnvironmentObject affects redrawing when a property in ObservableObject changes.

As per Apple's doc on EnvironmentObject,

An environment object invalidates the current view whenever the observable object changes.

If the above statement is true, wouldn't the below code invalidate and recreate ChildView when you press the button in ContentView. And that should print the following output.

initializing parent
initializing child
// after pressing the button
initializing child 

Contrary to my above assumption, it actually prints

initializing parent
initializing child
// after pressing the button
initializing parent 

Can anyone explain why this is the case? Why is the ParentView being recreated even though ParentView is not depending on Library?

class Library: ObservableObject {
  @Published var item: Int = 0
}

struct ContentView: View {
  @StateObject var library: Library = Library()
  var body: some View {
    VStack {
      ParentView()
        .environmentObject(library)
      Button {
        library.item += 1
      } label: {
        Text("increment")
      }
    }
  }
}

struct ParentView: View {
  init() {
    print("initializing parent")
  }
  var body: some View {
    VStack {
      Text("Parent view")
      ChildView()
    }
  }
}

struct ChildView: View {
  @EnvironmentObject var library: Library
  init() {
    print("initializing child")
  }
  var body: some View {
    Text("Child view")
  }
}


Solution

  • SwiftUI View´s can be a little tricky.

    An environment object invalidates the current view whenever the observable object changes.

    does not mean the object itself is recreated. It just means the body of the view gets called and the view rebuilds itself.

    Remember the struct is not the View itself, it´s just a "description".

    I´ve added some print statements to make this more clear:

    struct ContentView: View {
        @StateObject var library: Library = Library()
        
        init(){
            print("initializing content")
        }
        
        var body: some View {
            VStack {
                let _ = print("content body")
                ParentView()
                    .environmentObject(library)
                Button {
                    library.item += 1
                } label: {
                    Text("increment")
                }
            }
        }
    }
    
    struct ParentView: View {
        init() {
            print("initializing parent")
        }
        var body: some View {
            VStack {
                let _ = print("parent body")
                Text("Parent view")
                ChildView()
                
            }
        }
    }
    
    struct ChildView: View {
        @EnvironmentObject var library: Library
        init() {
            print("initializing child")
        }
        var body: some View {
            let _ = print("child body")
            Text("child")
        }
    }
    

    this initially prints:

    initializing content
    content body
    initializing parent
    parent body
    initializing child
    child body
    

    and after pressing the button:

    content body
    initializing parent
    child body
    

    As you see the body of those View´s depending on Library get their respective body reevaluated.

    The ParentView initializer runs because in your ContentView you call ParentView() in the body so a new struct "describing" your View is created. The ParentView´s view itself stays the same so its body var is not called.

    This WWDC 2021 video about SwiftUI Views will help you better understand this.