TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SwiftData objects and Multiple Selection in Lists

Forums > SwiftUI

Trying to enable selecting multiple items on a list of swiftData objects and I'm running into an issue with the selection binding.

sample code:

  @Model 
  class Item {
    var name: String
    var notes

    init..
  }

  @Model
  class ItemBag {
      var items: [Item]

      init...
  }

  ...
  //gets itemBag from previous view
  @Bindable var itemBag: ItemBag
  @State private var selection = Set<PersistentIdentifier>()

      var body: some View {
          List(selection: $selection) {
              ForEach(itemBag.items) { item in 
                  ItemListRowView(item: item, itemBag: itemBag)
        }
      }
      .toolbar {
          EditButton()
      }
    }
  ...

Prior to swiftData, my objects would all have a UUID, so the selection variable would be "selection = Set<UUID> but since SwiftData I have relied on the PersistentIdentifier generated with the @Model macro.

  • if I use what's in the sample code, selection = Set-PersistentIdentifier, or Set-Item- the selection bubbles dont appear using the EditButton
  • if I try to use selection = Set-Item.ID- I get an error that states " 'ID' is inaccessible due to 'internal' protection level"

Does anyone know what I should be binding to in order to trigger the selection bubbles in the view? Should I be giving my 'Item' model an additional variable for a UUID? would that conflict with swiftData?

Not sure what the best approach would be, but I can't really find anything else online.

this is what I mean when I say bubbles

1      

Have you tried to use Set<PersistentIdentifier.ID>() instead?

UPD. The above suggestion does not work though...

Another way to add UUID property to your datamodel and use like so.

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    @Query private var friends: [FriendModel]
    @State private var selections = Set<UUID>()

    var body: some View {
        NavigationStack {
            List(friends, id: \.modelID, selection: $selections) { friend in
                HStack {
                    Text(friend.firstName)
                    Text(friend.lastName)
                }
                .navigationTitle("List of friends")

            }
            .toolbar {
                EditButton()
            }
            .onChange(of: selections) { oldValue, newValue in
                print(newValue)
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(FriendModel.preview)
}

@Model
class FriendModel {
    var modelID: UUID
    var firstName: String
    var lastName: String

    init(modelID: UUID = UUID(), firstName: String, lastName: String) {
        self.modelID = modelID
        self.firstName = firstName
        self.lastName = lastName
    }
}

extension FriendModel {
    @MainActor
    static var preview: ModelContainer {
        let container = try! ModelContainer(for: FriendModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))

        let friend1 = FriendModel(firstName: "John", lastName: "Doe")
        let friend2 = FriendModel(firstName: "Jane", lastName: "Smith")
        let friend3 = FriendModel(firstName: "Alice", lastName: "Johnson")
        let friend4 = FriendModel(firstName: "Bob", lastName: "Brown")
        let friend5 = FriendModel(firstName: "Emily", lastName: "Jones")
        let friend6 = FriendModel(firstName: "Michael", lastName: "Williams")
        let friend7 = FriendModel(firstName: "Sarah", lastName: "Taylor")
        let friend8 = FriendModel(firstName: "David", lastName: "Wilson")
        let friend9 = FriendModel(firstName: "Emma", lastName: "Anderson")
        let friend10 = FriendModel(firstName: "James", lastName: "Martinez")

        let friends: [FriendModel] = [friend1, friend2, friend3, friend4, friend5, friend6, friend7, friend8, friend9, friend10]

        for friend in friends {
            container.mainContext.insert(friend)
        }
        return container
    }
}

1      

Thank you for the model code - the example you provided works as is but unfortunately does not work in the example I provided.

In the example I provided (and the actual code I'm working with) the objects in the list come from an @Binable var passed in from the parent view at which point adding a UUID and using it in the way recommended in the example no longer works

Both this:

  @Bindable var itemBag: ItemBag
  @State private var selection = Set<UUID>()

      var body: some View {
          List(selection: $selection) {
              ForEach(itemBag.items, id: \.modelID) { item in 
                  ItemListRowView(item: item, itemBag: itemBag)
                  }
              }
              .toolbar {
                      EditButton()
              }
      }

and this:

@Bindable var itemBag: ItemBag
  @State private var selection = Set<UUID>()

  var body: some View {
        List(itemBag.items, id: \.modelID, selection: $selection) { item in 
              ItemListRowView(item: item, itemBag: itemBag)
        }
        .toolbar {
              EditButton()
        }
  }

no longer work - it now adds the selection to the Set<<UUID>> but no longer triggers the selection bubbles like in your example.

It's extra odd because if I add an .onDelete() modifier to the ForEach, it pops the delete circles into view when the edit button is pressed.

I'd love to use the built in ones as they have baked in drag to select, however until I figure it out I've made a custom work-around by accessing the EditMode Environment Object and custom buttons.

My temp solution looks like this

  @Environment(\.editMode) private var editMode

  private var isEditing: Bool {

  if editMode?.wrappedValue.isEditing == true {
    return true
  } else {
    return false
  }

  .........

  //button to delete when editing is active

    Button {
        deleteItems()
    } label: {
      Image(systemName: "trash")
    }
    .disabled(isEditing ? false : true)

 ......... 

  List(selection: $selection) {
        ForEach(itemBag.item, id: \.modelID) { item in
              Button {
                    if selection.contains(item.modelID) {
                          selection.remove(item.modelID)
                    } else {
                          selection.insert(item.modelID)
                    }
               } label: {

                     if isEditing == true {
                          Image(systemName: selection.contains(item.modelID) ? "circle.inset.filled" : "circle")
                                .foregroundStyle(selection.contains(item.modelID) ? .accent : .primary)
                      }

                      ItemListRowView(item: item, itemBag: itemBag)

                 }
        }
  }

1      

Just modified the code to look maybe closer to your set up, still works fine. Pay attention to model relationships, maybe something in your code prevents that...

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    @Query private var friendsList: [FriendsList]

    var body: some View {
        NavigationStack {
            List {
                ForEach(friendsList) { list in
                    NavigationLink(list.name) {
                        DetailView(friendsList: list)
                    }
                }
            }
        }
    }
}

struct DetailView: View {
    @Bindable var friendsList: FriendsList
    @State private var selections = Set<UUID>()

    var body: some View {
        NavigationStack {
            List(friendsList.friends, id: \.modelID, selection: $selections) { friend in
                HStack {
                    Text(friend.firstName)
                    Text(friend.lastName)
                }
                .navigationTitle("List of friends")

            }
            .toolbar {
                EditButton()
            }
            .onChange(of: selections) { oldValue, newValue in
                print(newValue)
            }
        }
    }
}

#Preview {
    NavigationStack {
        ContentView()
            .modelContainer(FriendModel.preview)
    }
}

@Model
class FriendModel {
    var modelID: UUID
    var firstName: String
    var lastName: String

    var friendsList: FriendsList?

    init(modelID: UUID = UUID(), firstName: String, lastName: String) {
        self.modelID = modelID
        self.firstName = firstName
        self.lastName = lastName
    }
}

@Model
class FriendsList {
    var name: String
    @Relationship(inverse: \FriendModel.friendsList) var friends: [FriendModel] = []

    init(name: String, friends: [FriendModel] = []) {
        self.name = name
        self.friends = friends
    }
}

extension FriendModel {
    @MainActor
    static var preview: ModelContainer {
        let container = try! ModelContainer(for: FriendModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))

        let friend1 = FriendModel(firstName: "John", lastName: "Doe")
        let friend2 = FriendModel(firstName: "Jane", lastName: "Smith")
        let friend3 = FriendModel(firstName: "Alice", lastName: "Johnson")
        let friend4 = FriendModel(firstName: "Bob", lastName: "Brown")
        let friend5 = FriendModel(firstName: "Emily", lastName: "Jones")
        let friend6 = FriendModel(firstName: "Michael", lastName: "Williams")
        let friend7 = FriendModel(firstName: "Sarah", lastName: "Taylor")
        let friend8 = FriendModel(firstName: "David", lastName: "Wilson")
        let friend9 = FriendModel(firstName: "Emma", lastName: "Anderson")
        let friend10 = FriendModel(firstName: "James", lastName: "Martinez")

        let friends: [FriendModel] = [friend1, friend2, friend3, friend4, friend5, friend6, friend7, friend8, friend9, friend10]

        for friend in friends {
            container.mainContext.insert(friend)
        }

        let friendList = FriendsList(name: "FriendList 1", friends: friends)

        container.mainContext.insert(friendList)
        return container
    }
}

1      

After comparing the code and combing through my actual code I figured out the problem.

The issue isn't with the list, but rather the fact that I have the list nested in a form, which I only just realized isn't captured in my initial mock data.

For whatever reason while nested in a form, the edit buttoin will trigger change for an .onDelete call, but doesn't have any effect regarding selection. The same happens if I nest the sample data you provided into a form.

I'm going to adjust my code so it pops to a new view, outside of the form, to allow multiple selection/deleting.

Thank you for helping me brainstorm this and giving me examples to show it's possible (thereby forcing me to search elsewhere for the problem).

Happy coding!

1      

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.

Learn more here

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.