CoreData / SwiftUI List selection question

My code to show a list from a fetch that can be selected is (simplified) as follows

@FetchRequest(sortDescriptors: []) private var players: FetchedResults<PlayerEntity>
@State private var selectedPlayerID : PlayerEntity.ID?

var body: some View {
    
    NavigationSplitView {
        List(players, selection: $selectedPlayerID) { player in
            Text(player.shortName ?? "")
        }
        .navigationTitle("Players")
    } detail: {
        if let id = selectedPlayerID, let player = players.first(where: {$0.id == id}) {
            Text("Do Something")
        }
    }
}

I'm using the state variable of type ID PlayerEntity.ID? to hold the selection.

However, I noticed the sample app from migrating to SwiftData ("SampleTrips") is essentially doing it like this:

@FetchRequest(sortDescriptors: [SortDescriptor(\.startDate)])
    private var trips: FetchedResults<Trip>

    @State private var showAddTrip = false
    @State private var selection: Trip?
    @State private var path: [Trip] = []
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                ForEach(trips) { trip in
                    TripListItem(trip: trip)
//... removed some extra code
                }
            }
//... removed some extra code
            .navigationTitle("Upcoming Trips")
//... removed some extra code
        } detail: {
            if let selection = selection {
                NavigationStack {
                    TripDetailView(trip: selection)
                }
            }
        }

The sample code is able to pass an optional managed object type Trip? to hold the selection rather than the ID type. When I try to replicate that behavior, I can't. Does anyone know what would be different?

Replies

The sample code is able to pass an optional managed object type Trip? to hold the selection rather than the ID type. When I try to replicate that behavior, I can't.

Show the code you tried that didn't work. What is the problem you have with the code you tried? Do you get a build error? If so, what is the error? Do you get unexpected selection behavior? If so, state what you expect to see and what you see with the code you changed.

The code:

@FetchRequest(sortDescriptors: []) private var players: FetchedResults<PlayerEntity>
@State private var selectedPlayer : PlayerEntity?

var body: some View {
    
    NavigationSplitView {
        List(players, selection: $selectedPlayer) { player in
            Text(player.shortName ?? "")
        }
        .navigationTitle("Players")
    } detail: {
        if let player = selectedPlayer {
            Text("Do Something")
        }
    }
}

The list appears, but is not selectable, and the detail view doesn't get presented. I also tried using the same List/ForEach style of the sample code.

Hi @tdc ,

For selection in NavigationSplitView I recommend holding the selection with the ID since there is less overhead in checking the UUID vs the entire object.

That being said, for the selection variable to update correctly when not using ID, your selection variable must be of type PlayerEntity? and the PlayerEntity type must conform to Identifiable. You'll also want to use \.self as the id in your List initializer:

@State private var selectedPlayerID : PlayerEntity?
...
List(players,  id: \.self, selection: $selectedPlayer) {
..
}

Passing in an explicit id of self passes the entire object as the selection, which means that the selection variable and the id parameter of List now match. This should allow you do the selection in the same way as the sample, but again I would stick with the ID. The reason you did not need to pass an explicit id parameter in your original example is because PlayerEntity conforms to Identifiable and your selection variable is a UUID, so the id is inferred.

  • Thanks. How does the SampleTrip sample get away with not using id: .self? Is it possible that I've confused something additionally because my model for PlayerEntity includes an explicit uuid attribute in addition to any that are auto defined by CoreData (I can check this)

  • OK, wasn't having to do with my uuid attribute. Passing id:.self fixes it but still curious why the SampleTrip didn't do it. I was leaning towards using a PlayerEntity? because the "let player = players.first(where: {$0.id == id})" part seemed less efficient.

Add a Comment

Ah yes, the CoreData Identifiable, which I forgot to mention above. Since you do not directly conform to Identifiable with CoreData entities, the only way to conform to it is to include an attribute of type UUID. You'll notice in that sample that there is no attribute of id (or any other name) of type UUID, which means Trip is not Identifiable and therefore does not infer an id in the List. This is why they do not need to pass an explicit self id parameter. If you play around with the sample and add an id attribute of type UUID, you'll see that the selection stops working. Conversely, in your code, if you remove the UUID attribute, you probably will not need to pass in an explicit id either.

Switching over to another entity that doesn't have an explicit uuid : UUID attribute didn't make a difference. And from what I can tell, by default CoreData entries conform to Identifiable so that doesn't make sense to me.

  • CoreData uses NSManagedObjectID by default instead. That's interesting that the attribute didn't make a difference, but passing in \.self will fix this either way

  • I think since I named it "uuid" there would be nothing linking the expected "id" to it

Add a Comment

OK, so I have figured out what was going on in SampleTrip. It turns out that TripListItem includes a NavigationLink so copying that pattern

var body: some View {
		NavigationSplitView {
			List(players, selection: $selectedPlayer) { player in
				NavigationLink(value: player) {
					Text(player.shortName ?? "")
				}
			}
			.navigationTitle("Players")
		} detail: {
			if let p = selectedPlayer {
				Text("detail \(p.shortName ?? "")")
			} else {
				Text("detail empty")
			}
		}

Now works. So in this case, I don't need to pass in id. Any thoughts on whether using the NavigationLink here is the preferred approach? At least part of the mystery is resolved!