Why can't I change the order of the model's array?

262 views Asked by At

I'm currently programming my first SwiftData app.

I created two models:

Player.swift:

@Model
final class Player {
    @Relationship(inverse: \Game.players) var games: [Game]
    var name: String
    var id: UUID
    
    init(games: [Game] = [], name: String = "", id: UUID = UUID()) {
        self.games = games
        self.name = name
        self.id = id
    }
}

Game.swift:

@Model
final class Game {
    @Relationship var players: [Player]

    init(players: [Player] = []) {
        self.players = players
    } 
}

If I display the players of a game using the players variable, the order is always the same but I don't know why and what the sort descriptor is. If want to use .move to rearrange the order, but it doesn't work. The Player instantly jumps back. If I use .append(), the Player always appends at the same position, depending on which I use. For example, if I add Player(games: [], name: "Frank", id: UUID()), it always appends at second position, while Player(games: [], name: "Sean", id: UUID()) always appends on fourth position.

GameView.swift:

struct GameView: View {
    
    @Bindable var game = Game()
    
    var body: some View {
        NavigationStack {
            Form {
                Section("Spieler") {
                    ForEach(game.players) { player in
                        Text(player.name)
                    }
                    .onMove(perform: move)
                }
            }
            .navigationTitle("Game")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
    
    func move(from source: IndexSet, to destination: Int) {
        game.players.move(fromOffsets: source, toOffset: destination)
    }
}
2

There are 2 answers

0
Joakim Danielson On

The problem here is that you are not moving the elements of an ordinary array but an array property that is an relationship.

So basically what happens, as I understand it, is that when you perform the move and change the array this triggers SwiftUI to update the view which means it will access the players property of game.

I can't see any trace of this in the logs so the game will simply return the array it has and as it was sorted originally.

If you want to do it this way and persist this new order you must have some property that holds a players order and persist it in onMove, this of course gets extra complicated since you have a many to many relationship.

To add some code to this answer :) here is an example on how you can make onMove work in a simple maner but of course this isn't persisted. I still think it has a value since it shows that onMove works but not just when you use it directly on a relationship property.

struct GameView: View {
    var game: Game
    @State var players: [Player]

    init(game: Game) {
        self.game = game
        self.players = game.players
    }

    var body: some View {
        NavigationStack {
            Form {
                Section("Spieler") {
                    ForEach(players) { player in
                        Text(player.name)
                    }
                    .onMove(perform: move)
                }
            }
            .navigationTitle("Game")
            .navigationBarTitleDisplayMode(.inline)
        }
    }

    func move(from source: IndexSet, to destination: Int) {
        players.move(fromOffsets: source, toOffset: destination)
    }
}
0
ryan gordon On

SwiftData currently has a bug where arrays are randomly ordered