macosswiftuiswiftdata

Why doesn't list selection work when my list is from a DB?


In the below code I cannot select any of my list items:

import SwiftUI
import SwiftData

struct ItemView: View {
    
    @Environment(\.modelContext) private var context
    @Query(sort: \Item.title) private var items: [Item]
    
    @State private var selection: String? = nil
    
    var body: some View {
        if items.isEmpty {
            ContentUnavailableView("Enter your first item.", systemImage: "bookmark.fill")
        } else {
            List(items, id: \.self, selection: $selection) { item in
                Text(item.title)
            }
        }
    }
}

However in a very similar example that uses an array for my list items rather than a DB I can select items:

import SwiftUI

struct ContentView: View {
    @State private var selection: String?
    @State private var isOn: Bool = false
    
    let names = [
        "Cyril",
        "Lana",
        "Mallory",
        "Sterling"
    ]

    var body: some View {
        NavigationStack {
            List(names, id: \.self, selection: $selection) { name in
                Toggle(isOn: $isOn) {
                    Text(name)
                }
            }
            .navigationTitle("List Selection")
            .toolbar {
                
            }
        }
    }
}

My console log shows:

Can't find or decode reasons
Failed to get or decode unavailable reasons
NSBundle file:///System/Library/PrivateFrameworks/MetalTools.framework/ principal class is nil because all fallbacks have failed

What am I doing wrong?


Solution

  • The reason you don't get a selection is because you are missing the .tag(...).

    When you use the array names, it is provided for you, but not when you use the items: [Item] (the types don't match).

    Example code:

     struct ContentView: View {
         
         @Environment(\.modelContext) private var context
         @Query(sort: \Item.title) private var items: [Item]
         
         @State private var selection: String? 
         
         var body: some View {
             Text(selection ?? "no selection")
             if items.isEmpty {
                 ContentUnavailableView("Enter your first item.", systemImage: "bookmark.fill")
             } else {
                 List(items, selection: $selection) { item in
                     Text(item.title)
                         .tag(item.title)  // <--- here
                 }
             }
         }
     }
    

    Note, using selection: String? is not a great idea, try using selection: PersistentIdentifier? and use .tag(item.id) instead. The tag must match the type of the selection exactly.

    You could of course also use @State private var selection: Item? with .tag(item).

    struct ContentView: View {
        
        @Environment(\.modelContext) private var context
        @Query(sort: \Item.title) private var items: [Item]
        
        @State private var selection: Item? // <--- here
        
        var body: some View {
            Text(selection?.title ?? "no selection") // <--- for testing
            if items.isEmpty {
                ContentUnavailableView("Enter your first item.", systemImage: "bookmark.fill")
            } else {
                List(items, selection: $selection) { item in
                    Text(item.title)
                        .tag(item)  // <--- here
                }
            }
        }
    }
    

    Note also, do not use List(items, id: \.self, ..., let the Item id do the job as shown in the example code.