29

I'm trying to understand why passing an @ObservedObject variable does not work for nested child views. The data is able to be passed but the changes only reflects at the root view where the @ObservedObject variable was created. The changes don't show in the subviews. Looking at Apple documentation (which has been updated for Xcode beta 5), the answer seems to be to create both an environment object and a regular variable in order to get the correct index from the environment object. Here is Apples example.

My understanding is that an @ObservedObject can be used to reference a variable from multiple views, but if you want the data to be accessible from any view then you should use an environment object. Therefore I believe that passing the @ObservedObject should be possible. The issue I believe is happening is that since ScreenTwo is passing the @Binding variable to DetailsView and that is what's causing a problem. To solve this I would think you need to keep passing the full @ObservedObject, but then again you would need some type of regular variable to get the proper index.

I feel like all of this should be much more straightforward.

import SwiftUI
import Combine

struct Sport: Identifiable{
    var id = UUID()
    var name : String
    var isFavorite = false
    var school : String
}

final class SportData: ObservableObject  {
    @Published var store =
        [
            Sport(name: "soccer", isFavorite: false, school: "WPI"),
            Sport(name: "tennis", isFavorite: false, school: "WPI"),
            Sport(name: "swimming", isFavorite: true, school: "WPI"),
            Sport(name: "running", isFavorite: true, school: "RIT"),
    ]
}

struct Testing: View {
    @ObservedObject var sports = SportData()

    var body: some View {
        NavigationView{
            List{
                ForEach($sports.store){ sport in
                    NavigationLink(destination: ScreenTwo(sport: sport)){
                        HStack {
                            Text(sport.value.name)
                            Spacer()
                            Text(sport.value.isFavorite.description)
                        }
                    }
                }
            }
        }.navigationBarTitle("Settings")
    }
}

struct ScreenTwo : View{
    @Binding var sport : Sport

    var body: some View{
        NavigationLink(destination: DetailsView(sport: $sport)){
            Text(sport.isFavorite.description)
        }
    }
}

struct DetailsView: View {
    @Binding var sport : Sport

    var body: some View {
        Button(action: {
            self.sport.isFavorite.toggle()
            self.sport.name = "Ricky"
        }) {
            Text(sport.isFavorite.description)
            Text(sport.name)
        }
    }
}



#if DEBUG
struct Testing_Previews: PreviewProvider {
    static var previews: some View {
        Testing()
    }
}
#endif
Richard Witherspoon
  • 4,082
  • 3
  • 17
  • 33
  • Seems to be working fine here. First you select Ricky, then you select the second `NavigationLink` by clicking false, then it shows a button which immediately changes both the `false` besides Ricky and the false to the right once pressed. Or was the second `NavigationLink` unwanted? – Fabian Aug 19 '19 at 06:19
  • I’m not sure I follow your logic. Heres what currently happens: 1. Press "soccer" in the first row 2. click the navigation link 3. click the "false soccer" button. 4. Go all the way to the root view and the first row will be "Ricky true". What should happen is that at step 3, the button should immediately change to "Ricky true", and then when you go back to the second screen it should say "true". The root screen should say "Ricky true". – Richard Witherspoon Aug 19 '19 at 06:25
  • Ah now I understand what you mean. Yes, that looks weird. As if SwiftUI doesn't want to redraw the content in `destination:`. It doesn't change `destination:` despite what I stated before, only a redraw does that. – Fabian Aug 19 '19 at 06:32
  • 2
    Exactly. It's not producing the expected behavior. Whether this is a bug, or some deeper logic I don't understand I’m unsure. – Richard Witherspoon Aug 19 '19 at 06:35
  • It's probably something about the `NavigationLink` inside a `NavigationLink` thing. Removing `ScreenTwo` seems to update the `destination:` correctly again. – Fabian Aug 19 '19 at 06:46
  • 1
    I noticed that too. However, don't think it's because of the NavigationLink. I think it has more to do with the fact that the ObservedObject is the source of truth for the variable and Binding is just a reference to it. So when ScreenTwo passes the Binding to another Binding, I think that causes the issue. In all of the reading I've done and examples I have looked at, no one passes variables like this. They only pass an ObservedObject to a single Binding. That Binding never gets passed to anything else. This confuses me because that seems to be the whole point of an ObservedObject. – Richard Witherspoon Aug 19 '19 at 07:01
  • Not to be annoying but have you considered using @EnvironmentObject instead? – Nerdy Bunz Aug 19 '19 at 10:49
  • @dfd Do you access values with `$sports.store[selectID].isFavorite` then? – Fabian Aug 19 '19 at 13:30
  • @dfd I’m trying to do it only using ObservedObject to understand it a little better. However, thanks for the input. Think you can put the relevant code as another answer? Then I could give you credit and it would be a little more clear. – Richard Witherspoon Aug 19 '19 at 16:14

3 Answers3

22

For ObservableObject the pairing ObservedObject makes view refresh, so to solve the task in question I would recommend the following approach:

Demo

Usage of ObservingObject/ObjservedObject pattern

Code

import SwiftUI
import Combine

class Sport: ObservableObject, Hashable, Identifiable {

    static func == (lhs: Sport, rhs: Sport) -> Bool {
        lhs.name == rhs.name && lhs.isFavorite == rhs.isFavorite && lhs.school == rhs.school
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(isFavorite)
        hasher.combine(school)
    }

    @Published var name : String
    @Published var isFavorite = false
    @Published var school : String

    init(name: String, isFavorite: Bool, school: String) {
        self.name = name
        self.isFavorite = isFavorite
        self.school = school
    }
}

final class SportData: ObservableObject  {
    @Published var store =
        [
            Sport(name: "soccer", isFavorite: false, school: "WPI"),
            Sport(name: "tennis", isFavorite: false, school: "WPI"),
            Sport(name: "swimming", isFavorite: true, school: "WPI"),
            Sport(name: "running", isFavorite: true, school: "RIT"),
    ]
}

struct TestingObservedObject: View {
    @ObservedObject var sports = SportData()

    var body: some View {
        NavigationView{
            List{
                ForEach(sports.store){ sport in
                    NavigationLink(destination: ScreenTwo(sport: sport)) {
                        HStack {
                            Text("\(sport.name)")
                            Spacer()
                            Text(sport.isFavorite.description)
                        }
                    }
                    .onReceive(sport.$isFavorite) { _ in self.sports.objectWillChange.send() }
                }
            }
        }.navigationBarTitle("Settings")
    }
}

struct ScreenTwo : View{
    @ObservedObject var sport : Sport

    var body: some View{
        NavigationLink(destination: DetailsView(sport: sport)){
            Text(sport.isFavorite.description)
        }
    }
}

struct DetailsView: View {
    @ObservedObject var sport : Sport

    var body: some View {
        Button(action: {
            self.sport.isFavorite.toggle()
            self.sport.name = "Ricky"
        }) {
            Text(sport.isFavorite.description)
            Text(sport.name)
        }
    }
}



#if DEBUG
struct Testing_Previews: PreviewProvider {
    static var previews: some View {
        TestingObservedObject()
    }
}
#endif
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
4

@EnvironmentObject - A property wrapper type for an observable object supplied by a parent or ancestor view.

environmentObject(_:) - Supplies an ObservableObject to a view subhierachy.(this is a viewModifire)

So that we can share @ObservableObject through environmentObject(_:). To accept it, sub view should have @EnvironmentObject (ex :- @EnvironmentObject var viewModel: MyViewModel)

YodagamaHeshan
  • 4,996
  • 2
  • 26
  • 36
  • 1
    Thanks for this answer. However, I have a Question about this: I have a `Fetch Request` inside my `Root View`. A `Computed Property` will specify which of the Results are passed down the View Hierarchy using `.environmentObject(_:)`. Since the Results of the `Fetch Request` can change and therefore, the `Computed Property` returns another Result, do all Child Views get updated automatically? – christophriepe May 06 '21 at 18:57
1

You should define sport field as @EnviromentObject instead of @ObservedObject on ScreenTwo and DetailsView. Also set enviroment object with NavigationLink(destination: ScreenTwo()).environmentObject(sport) on TestingObservedObject view.

np2314
  • 645
  • 5
  • 14
Arif Ulusoy
  • 244
  • 2
  • 11
  • Thanks for this answer. However, I have a Question about this: I have a `Fetch Request` inside my `Root View`. A `Computed Property` will specify which of the Results are passed down the View Hierarchy using `.environmentObject(_:)`. Since the Results of the `Fetch Request` can change and therefore, the `Computed Property` returns another Result, do all Child Views get updated automatically? – christophriepe May 06 '21 at 19:03