2

In the code below, part of a crossword app, I have created a Grid, which contains squares, an @Published array of Squares, and a GridView to display an instance of Grid, griddy. When I change griddy's squares in the GridView, the GridView gets recreated, as expected, and I see an "!" instead of an "A".

Now I've added one more level -- a Puzzle contains an @Published Grid, griddy. In PuzzleView, I therefore work with puzzle.griddy. This doesn't work, though. The letter doesn't change, and a break point in PuzzleView's body never gets hit.

Why? I'm working on an app where I really need these 3 distinct structures.

import SwiftUI


class Square : ObservableObject {
    @Published var letter: String
    
    init(letter: String){
        self.letter = letter
    }
}

class Grid : ObservableObject {
    @Published var squares:[[Square]] = [
        [Square(letter: "A"), Square(letter: "B"), Square(letter: "C")],
        [Square(letter: "D"), Square(letter: "E"), Square(letter: "F"), ]
    ]
    
}

class Puzzle: ObservableObject {
    @Published var griddy: Grid = Grid()
}

struct GridView: View {
    @EnvironmentObject var griddy: Grid
    
    var body: some View {
        VStack {
            Text("\(griddy.squares[0][0].letter)")
            Button("Change Numbers"){
                griddy.squares[0][0] = Square(letter:"!")
            }
        }
    }
}


struct PuzzleView: View {
    @EnvironmentObject var puzzle: Puzzle
    
    var body: some View {
        VStack {
            Text("\(puzzle.griddy.squares[0][0].letter)")
            Button("Change Numbers"){
                puzzle.griddy.squares[0][0] = Square(letter:"!")
            }
        }
    }
}



struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        // PuzzleView().environmentObject(Puzzle())
        GridView().environmentObject(Grid())
    }
}
Michael Rogers
  • 1,318
  • 9
  • 22
  • 2
    Here's a slightly different question, but my answer would be the same as there: https://stackoverflow.com/a/62988407/968155. The simplest solution is to make `Square` a value-type: `struct Square { var letter: String }` (no need for `@Published` or `ObservableObject`) – New Dev Jul 22 '20 at 20:12
  • I need Square to be an ObservableObject -- it's observed in a SquareView -- so I need it to be a class. But thanks for your response and the idea. – Michael Rogers Jul 22 '20 at 22:15
  • 1
    to be "observed" it doesn't need to be an `ObservableObject` - there are paradigms, like `Bindings` to pass values between views. It would makes sense for it to be an observable object if it had its own lifecycle events (e.g. value is updated based on some external trigger). In any case, the other answer tells you how to deal with `ObservableObject`s (the last approach mentioned there) – New Dev Jul 22 '20 at 22:20

3 Answers3

1

Technically you could just add this to your Puzzle object:

var anyCancellable: AnyCancellable? = nil

init() {
    self.anyCancellable = griddy.objectWillChange.sink(receiveValue: {
        self.objectWillChange.send()
    })
}

It's a trick I found in another SO post, it'll cause changes in the child object to trigger the parent's objectWillChange.

Edit:

I would like to note that if you change the child object:

puzzle.griddy = someOtherGriddyObject

then you will still be subscribed to the changes of the old griddy object, and you won't receive new updates.

You can probably get by this by just updating the cancellable after changing the object:

puzzle.griddy = someOtherGriddyObject
puzzle.anyCancellable = puzzle.griddy.objectWillChange.sink(receiveValue: {
        puzzle.objectWillChange.send()
    })
Samuel-IH
  • 703
  • 4
  • 7
1

Think in this direction: in MVVM SwiftUI concept every introduced ObservableObject entity must be paired with corresponding view ObservedObject (EnvironmentObject is the same as ObservedObject, just with different injection mechanism), so specific change in ViewModel would refresh corresponding View, there is no other magic/automatic propagations of changes from model to view.

So if Square is ObservableObject, there should be some SquareView, as

struct SquareView {
   @ObservedObject var vm: Square
   ...

so if you Puzzle observable includes Grid observable, then corresponding PuzzleView having observed puzzle should include GridView having observed grid, which should include SqareView having observed square.

Ie.

struct PuzzleView: View {
    @EnvironmentObject var puzzle: Puzzle
    
    var body: some View {
       GridView().environmentObject(puzzle.griddy)
    }
}

// ... and same for GridView, and so on...

This approach result in very optimal update, because modifying one Square makes only one corresponding SquareView refreshed.

But if you accumulate all Squares.objectWillChange in one container update, then modifying one Square result in total UI update, that is bad and for UI fluency and for battery.

Asperi
  • 228,894
  • 20
  • 464
  • 690
0

I'd say because Grid is reference type, and @Published is triggered when griddy is mutated.

class Puzzle: ObservableObject {
    @Published var griddy: Grid = Grid()
}

Note that griddy does not actually mutate since it is a reference.

But why does it work in @EnvironmentObject var griddy: Grid?

My guess is that SDK implicitly observes publishers in an @ObservableObject.

But maybe someone can provide better detail answer.

I personally would avoid long publisher chain, and just flatten the nested structure.

Jim lai
  • 1,224
  • 8
  • 12