0

Edit with minimal reproducible example:

@main
struct SwiftDataTestApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Team.self, Player.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \Team.name) var teams: [Team]
    @State private var viewModel = ViewModel()

    var body: some View {
        VStack {
            List {
                ForEach(teams) { team in
                    Section(header: Text(team.name)) {
                        ForEach(team.players) { player in
                            Text("\(player.name) #\(player.number)")
                        }
                    }
                }
            }

            Button(action: {
                let player1 = Player(name: "John", number: 1)
                let player2 = Player(name: "Joe", number: 2)
                let team = Team(name: "Team1", players: [player1, player2])
                modelContext.insert(team)
                try? modelContext.save()
            }, label: {
                Text("initialize")
            })
            Button(action: testChangeNumber) {
                Label("Change player number", systemImage: "plus")
            }
        }
    }

    private func testChangeNumber() {
        if let team = teams.first, let player = team.players.first {
            viewModel.updatePlayer(playerID: player.id, teamID: team.id)
        }
    }

    
}

@MainActor
@Observable class ViewModel {
    private let modelContext: ModelContext = SwiftDataService.shared.getContext()
    
    func updatePlayer(playerID: UUID, teamID: UUID) {
        
        let descriptor = FetchDescriptor<Team>(predicate: #Predicate { $0.id == teamID })
        
        guard let team = try? modelContext.fetch(descriptor).first else {
            return
        }
        
        if let playerIndex = team.players.firstIndex(where: { $0.id == playerID }) {
            let player = team.players[playerIndex]
            player.number = 7
            
            try? modelContext.save()
        }
    }
}

class SwiftDataService {
    private let modelContainer: ModelContainer
    private let modelContext: ModelContext
    
    @MainActor
    static let shared = SwiftDataService()
    
    @MainActor
    private init() {
        self.modelContainer = try! ModelContainer(
            for: Team.self, Player.self,
            configurations: ModelConfiguration(isStoredInMemoryOnly: false)
        )
        self.modelContext = modelContainer.mainContext
    }
    
    func getContext() -> ModelContext {
        modelContext
    }

}

@Model
class Player {
    @Attribute(.unique) var id: UUID
    var name: String
    var number: Int
    
    @Relationship(inverse: \Team.players) var team: Team?
    
    init(id: UUID = UUID(), name: String, number: Int) {
        self.id = id
        self.name = name
        self.number = number
    }
}

@Model
class Team {
    @Attribute(.unique) var id: UUID
    var name: String
    @Relationship var players: [Player]
    
    init(id: UUID = UUID(), name: String, players: [Player] = []) {
        self.id = id
        self.name = name
        self.players = players
        
        for player in players {
            player.team = self
        }
    }
}

First tap "Initialize". This will create a team with two players, John #1 and Joe #2. Then tap the "Change player number" button. The view will not update. However if you restart the app, the first player will now show with the correct #7.

The question is how to get the view to update when a change to an individual player information occurs.

2
  • I cannot reproduce with minimal assumptions. Please show a minimal reproducible example. Commented Jun 14 at 21:11
  • @Sweeper I added all the code necessary to reproduce. Commented Jun 14 at 23:50

1 Answer 1

2

What you have there is a very twisted setup to achieve something very trivial.

The short answer is that if you want to update the player number, then just update the player number:

private func testChangeNumber() {
    if let team = teams.first, let player = team.players.first {
        // viewModel.updatePlayer(playerID: player.id, teamID: team.id)
            player.number = 7
    }
}

The above will update the player number in the view right away. It works because SwiftData models are already Observable, and you're making the change from the view itself, so SwiftUI knows to trigger a view update.

Otherwise, adding @Observable to your ViewModel achieves nothing, because the view model has no properties to observe. @Observable doesn't observe functions, it observes changes to properties (that are part of the class itself).

If you really want to update the player via the view model, at least pass the player as a parameter:

func updatePlayer(player: Player) {
    player.number = 7
}

And then in your testChangeNumber function:

viewModel.updatePlayer(player: player)

This should also trigger a view update right away, but it will work the same whether the ViewModel is marked with @Observable or not.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.