|
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
|
|
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
}
}
|
|
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)
}
}
}
|
|
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
}
}
|
|
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!
|