UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: ChildView gets data via @Bindable model but doesn't refresh after updating properties of the model

Forums > SwiftUI

Hi,

I'm struggling with the following SwiftUI code in my personal project, I hope someone can help me.

Whenever I click one of these buttons, the score of the team gets successfully updated in the database. But the score in this GameView still shows the old score. If I go back to the parent ContentView, the ContentView shows the updated score, and when I navigate back to the childview GameView, also the GameView shows the correct score.

@Bindable is doing a good job it seems, because it correctly updates the score in game.participations.home.score, but I'm puzzled to why the GameView isn't immediately displaying the updated score.

This applies to both preview as running the app in the simulator: clicking the button doesn't display the updated score on the button

struct GameView: View {
    @Bindable var game: Game

    var body: some View {

        HStack {
            Spacer()
            Button(action: {addPoint(participation: game.participations.home)}, label: {
                VStack {
                    Text("\(game.participations.home.team.name)").font(.headline)
                    Text("\(game.participations.home.score)").font(.largeTitle)
                }
            })

            Spacer()
            Button(action: {addPoint(participation: game.participations.out)}, label: {
                VStack {
                    Text("\(game.participations.out.team.name)").font(.headline)
                    Text("\(game.participations.out.score)").font(.largeTitle)
                }
            })
            Spacer()
        }.padding(.horizontal)
            .navigationTitle("\(game.participations.home.team.name) - \(game.participations.out.team.name)")
            .navigationBarTitleDisplayMode(.inline)
    }

    func addPoint(participation: Participation) {
        participation.sections.last?.points.append(Point(date: .now))
    }
}

Some code of the ContentView:

struct ContentView: View {
    @Query var games: [Game]
    @Environment(\.modelContext) var modelContext

    var body: some View {
        NavigationStack {
            List {
                ForEach(games) { game in
                    NavigationLink(value: game) {
                        HStack {
                            VStack {
                                Text(game.participations.home.team.name)
                                    .font(.title2)
                                Text("\(game.homeTeamScore)")
                            }

                            Spacer()
                            VStack {
                                Text(game.participations.out.team.name)
                                    .font(.title2)
                                Text("\(game.outTeamScore)")
                            }
                        }
                    }

                }
                .onDelete(perform: deleteGames)

These are the models:

@Model
class Game {
    var date: Date
    var participations: [Participation]

    init(date: Date, participations: [Participation]) {
        self.date = date
        self.participations = participations
    }

    var homeTeamScore: Int {
        participations.filter({participation in participation.isHomeTeam}).first!.score
    }

    var outTeamScore: Int {
        participations.filter({participation in !participation.isHomeTeam}).first!.score
    }

}
@Model
class Participation {
    var team: Team
    var isHomeTeam: Bool
    var sections: [Section]

    init(team: Team, isHomeTeam: Bool, sections: [Section]) {
        self.team = team
        self.isHomeTeam = isHomeTeam
        self.sections = sections
    }

    var score : Int {
        sections.map({$0.score}).reduce(0, +)
    }
}

extension Array where Element : Participation {
    var home: Participation {
        return self.filter({participation in participation.isHomeTeam}).first!
    }

    var out: Participation {
        return self.filter({participation in !participation.isHomeTeam}).first!
    }
}
@Model
class Team {
    var name: String
    var isYourTeam: Bool

    init(name: String, isYourTeam: Bool) {
        self.name = name
        self.isYourTeam = isYourTeam
    }
}
@Model
class Section {
    var points: [Point]

    init(points: [Point]) {
        self.points = points
    }

    var score: Int {
        return points.count
    }
}
@Model
class Point {
    var date: Date

    init(date: Date) {
        self.date = date
    }
}

UPDATE: if I:

  • EITHER remove swiftdata from the project and make these classes @Observable (and Hashable, Equatable where needed), then clicking the buttons in the GameView will immediately show the update score in the GameView
  • OR add a button to the ContentView to update the score, then the ContentView immediately shows the updated score
        Button ("increase score") {
            games.first!.participations.first!.sections.last?.points.append(Point(date: .now))
        }

So I must be overlooking something when accepting a SwiftData Model in a child view via @Bindable and writing to that object.

I've made a video, hopefully this helps (please remove the spaces from the url to make it work): https ://app.screencast.com/BeD69yrfRsNZu

   

I found a workaround.

It seems that Swiftui shows the updated score IF:

  • I display a value in the view that is a direct property of the Bindable game AND
  • I reassign a value to the property, it can even have the same value as before

For instance I display the date of the game in the view:

Text("\(game.date.formatted(date: .abbreviated, time: .omitted))")

AND

At the moment I update the score, I also reassign the value of this date:

game.date = game.date

So the score, which is derived from an object related to the @Bindable field, gets only refreshed if I reassign (or update) a property directly on the @Bindable field.

So I have this workaround, which is good. But can anyone tell if this behaviour is intended? Or am I perhaps misusing something in SwiftUI?

Thanks in advance!

   

First: Thanks for coming back and sharing your findings. I was on the same track in Playgrounds to test this same assumption.

Second: One of my peeves. If you and I were chatting in a pub with a pint, you'd (probably) describe your views by saying,

"Look at this screen. It shows the score of a single game and allows you to update the game. Then when you hit the back button it returns you to a list of all the games and their scores. You can tap any game in this GAMES LIST VIEW to see the details of the game."

That's how I'd imagine it. You don't have a ContentView. You have a GamesListView. ContentView is the FooBar of SwiftUI. It's the placeholder for a view when first created. Consider renaming ContentView each time you create a new view.

Intended Behaviour?

(You and I are in agreement here...) Now for the behaviour part. I think what you're seeing is intended behaviour. You've alterted SwiftUI that you want to bind a game object between the (ahem!) GamesListView and the detailed GameView. So SwiftUI allows you to update the contents of the binding in the GameView, and the object is also updated in the parent view.

But why then, doesn't the score update? Why does the view not redraw itself?

What is being observed? It's the game object that is being observed, not the participation array. I think this is because the structure of the game object didn't change. You still have a game date, and you still have an array of participation objects. Indeed, when you change the date contents, you're essentially changing the entire game object. Since it's the game object that's being observed, SwiftUI detects the subtle change and asks the view to redraw itself.

Still unsure

But I might also argue that the number of items in the participation array changed, so doesn't this get reflected in the game object?

Keep Coding!

If I find more info, I'll be back.

   

Hi Obelix,

Thank you very much for coming back to me, and providing me with the feedback. From now on I'll always immediately rename the ContentView!

To be clear: the number of items in the participation array doesn't change. I do add an item to the points array when clicking the button. Here's how the relationship looks like:

a game has an array with two participations
a participation has an array with 1 to many sections
a section has an array with 0 to many points

(I might get rid of the sections between participation and points and store the section differently)

   

Hi Obelix

One of my peeves ContentView is the FooBar of SwiftUI. It's the placeholder for a view when first created.

I will give you one of your stories.

You have a box, you open the box to look at the Contents. In The Contents there are a number of items (ListofGames ,Game, ScoreSheet etc), However they are all in the Content of the box.

While some time I might rename the ContentView (rarely!). I use this as the first View to hold all the other Views. Might have a TabView or "Menu" List or simular or a view to start something!

So I do not think of ContentView as a placeholder but as where the app starts (where the app open to).

You have mention this a few times and felt that while it might YOUR peeves it not others. People should write code that they are happy to use (while some thing are Swift convention and these are not set in stone!). A bit like which is the better architecture compared to other!

Do like your way of look at problems in a different way.

   

My current workaround consists out of displaying a property of the game on screen, but currently I cannot think of a property on the game level that would make sense displaying here. All relevant info is available through relationships: team names, number of points. Even the date property is not useful because currently it represents when this class was created, but I might remove it because I'll be more interested in when the game actually starts (will provide a start (timer-like) button to actually start the game).

So currently I'm forced to put a property on screen which doesn't make sense in my case.

If anyone has a better workaround, or can suggest me a better way of composing the model, please let me know!

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.