6
votes

I have three Views in my app. The first one shows a List of Games. The second has a list of players for each game. And the third one has a score-List for each player. So the structure looks like this:

struct GameView: View {

  @environmentObject var model: Model

  var body: some View {
    NavigationView {
      List(model.games) {game in
        NavigationLink(destination: PlayerView(game: game)) {
          // Some View
        }
      }
    }
  }
}
struct PlayerView: View {

  @environmentObject var model: Model
  var game Game
  var gameIndex: Int {
    model.games.firstIndex() {$0 == game}!
  }

  var body: some View {
    TextField("Game", $model.games[gameIndex].title)
    List(game.players) {player in
      NavigationLink(destination: ScoreView(player: player, gameIndex: gameIndex)) {
        // Some View
      }
    }
  }
}
struct ScoreView: View {

  @environmentObject var model: Model
  var player: Player
  var gameIndex: Int

  var playerIndex: Int {
    model.games[gameIndex].players.firstIndex() { $0 == player }!
  }


  var body: some View {
    TextField("Player", $model.games[gameIndex].players[playerIndex].name)
    List(player.scores) {score in
        // Some View
    }
  }
}

My problem is: For every view that goes deeper into the hierarchy I have to go all the way back to my environmentObject and get the path through my model-arrays with indexes, that I have to pass through every view. I want every view being updated if I change the TextField value (just as an example for every data changing in the app) and if I'm moving back- and forwards through the NavigationView. I'm sure, there is some bad construction error, but I don't get the right answer.

3
I think ObservableObject would serve you better than EnvironmentObject in this case. Each view can have access to only the data that it needs, reducing the need for long statements accessing multiple layers of an object. - Benjamin Kindle
@BenjaminKindle care to elaborate? :) - Sajjon
I'll try to provide a more detailed answer as a real answer. Your question is specifically about avoiding accessing values "deep" in an object such as $model.games[gameIndex].players[playerIndex].name, correct? I don't really understand the last sentence of the question. - Benjamin Kindle
One more thing not directly related to the question: I think you need to switch the position of List and NavigationView in GameView. You only want one NavigationView, but by putting it inside List a NavigationView gets created for every item in the list. - Benjamin Kindle

3 Answers

1
votes

For something like what you are doing, I would use ObservedObject which is good for passing specific data from one view to the other - especially when there are properties in the object that may change and need to be reacted to.

The inputs you have make a lot of sense, but I think you should be able to only use the input rather than also accessing the model EnvironmentObject. For example, in the PlayerView code, you could do game.title instead of $model.games[gameIndex].title. The problem is that as your code is, changes to the game title will not be reflected That's where ObservedObject comes in. Like EnvironmentObject, ObservedObjects will tell SwiftUI to update the view when the object is updated. Both EnvironmentObject and ObservedObject require the class to conform to the same ObservableObject protocol (formerly known as BindableObject).

The main change you are going to have to make is adding the ObservedObject property wrapper to the view inputs like game and player. You also might need to add the ObservableObject conformance to the Game and Player classes if you haven't already.

this article explains the different types of data binding in SwiftUI, including ObservedObject.

Edit - changes not showing

One possible "gotcha" that may cause changes not to show is that each object needs to emit the ObjectWillChange event not only when its own values change, but also if any of its children's values change. This is even more complicated if there is an array of ObservableObjects involved.

This is the code that I use in a situation like this:

// an array of cancelables because we need to subscribe to every object in the players array.
var playerCans: [Cancellable]?

@Published var players: [Player] = [Player(), Player()] {
    didSet {
        // necessary because if the array gets set to something new,
        // we want to notify of changes to the new array, not the old one
        setupCans()
    }
}

init() {
    setupCans()
}

func setupCans() {
    // Not sure if canceling is necessary, but doing it just in case
    self.playerCans?.forEach({ (can) in
        can.cancel()
    })
    self.playerCans = players.map { (player) in
        return player.objectWillChange.sink {
            self.objectWillChange.send()
        }
    }
}
1
votes

I have exactly the same use case and I'm struggling too with the choice between @ObservedObject and @EnvironmentObject.

I've read a lot, and have seen some video's (on YouTube and Udemy) from developers. Like Nico, I have an preference for @EnviromentObject. I also have two list views connected, then a detail view, some edit views, settings views etc.

It looks like it is easier to use @EnvironmentObject instead of passing the data back and forth with @ObservedObject. But... is the total amount of data also an argument?

Any thoughts?

1
votes

Given some environment objects like:

class EnvironmentObject1: ObservableObject {
    @Published var value: Bool
    ...
}

On any view that needs one of those environment objects you just add the @EnvironmentObject annotation on it:

struct MenuContent: View {
    @EnvironmentObject var object1: EnvironmentObject1
}

And instead of passing the environment object through all the chain of intermediate views you can create a ViewModifier that simplifies all of that:

struct EnvironmentObjectsHolder: ViewModifier {
    static var object1: EnvironmentObject1 = EnvironmentObject1()
    static var object2: EnvironmentObject1 = EnvironmentObject2()

    func body(content: Content) -> some View {
        content
            .environmentObject(Self.object1)
            .environmentObject(Self.object2)
    }
}

And when you need the view you just use the modifier:

struct SomeView: View {

    var body: some View {
        MenuContent()
           .modifier(EnvironmentObjectsHolder())

    }
}

Check this post: https://medium.com/swlh/swiftui-and-the-missing-environment-object-1a4bf8913ba7