Hi all,
I've successfully worked through the SwiftData challenges (regular and bonus) but am struggling a bit with view refreshing after deleting - it seems once you drift from displaying/deleting list items things become a bit more complex. I'm working through an evolved version of the SwiftData example project to stetch my SwiftData skills, stealing from some other things I'm developing.
My main issue is getting my views to react to deletions in the SwiftData query. When adding new items to SwiftData in the 'Edit' view, they show up immediately in the 'Display' view. However, when deleting, they delete from the list in 'Edit' but do not disappear from 'Display' until leaving the List and coming back to it. Is there something I'm missing in my SwiftData implementation, or is this just a current limitation of SwiftData? The examples I've found online are all List based, and I'm struggling to implement it in a case that's outside of that construct.
I feel like my issue is related to this topic on SwiftData deletions-that-aren't-actually-deleted, but am unsure how to resolve this within the query: https://www.hackingwithswift.com/quick-start/swiftdata/how-to-check-whether-a-swiftdata-model-object-has-been-deleted
Appreciate any thoughts you have!
First, my SwiftData classes:
Lists
import Foundation
import SwiftData
@Model
class Lists {
var listName: String
var listDetails: String
var listDateAdded: Date
@Relationship(deleteRule: .cascade, inverse: \ListSeries.lists) var listSeries = [ListSeries]()
init(listName: String = "", listDetails: String = "", listDateAdded: Date = .now) {
self.listName = listName
self.listDetails = listDetails
self.listDateAdded = listDateAdded
}
}
ListSeries
import Foundation
import SwiftData
@Model
class ListSeries {
var seriesID: Int
var seriesDateAdded: Date
var seriesName: String
var lists: Lists?
init(seriesID: Int, seriesDateAdded: Date, seriesName: String) {
self.seriesID = seriesID
self.seriesDateAdded = seriesDateAdded
self.seriesName = seriesName
}
}
Then the views I'm working with:
ContentView
import SwiftData
import SwiftUI
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var path = [Lists]()
@State private var sortOrder = SortDescriptor(\Lists.listName)
@State private var searchText = ""
var body: some View {
NavigationStack(path: $path) {
ListView(sort: sortOrder, searchString: searchText)
.navigationDestination(for: Lists.self, destination: DisplayListDetailView.init)
.searchable(text: $searchText)
.toolbar {
Button("Add List", systemImage: "plus", action: addList)
Menu("Sort", systemImage: "arrow.up.arrow.down") {
Picker("Sort", selection: $sortOrder) {
Text("Name")
.tag(SortDescriptor(\Lists.listName))
}
.pickerStyle(.inline)
}
}
}
}
func addList() {
let list = Lists()
modelContext.insert(list)
path = [list]
}
}
#Preview {
ContentView()
}
ListView
import SwiftData
import SwiftUI
struct ListView: View {
@Environment(\.modelContext) var modelContext
@Query(sort: [SortDescriptor(\Lists.listName, order: .reverse), SortDescriptor(\Lists.listName)]) var lists: [Lists]
var body: some View {
List {
ForEach(lists) { list in
NavigationLink(value: list) {
VStack(alignment: .leading) {
Text(list.listName)
.font(.headline)
Text(list.listDateAdded.formatted(.dateTime.day().month().year()))
}
}
}
.onDelete(perform: deleteLists)
}
.preferredColorScheme(.dark)
}
init(sort: SortDescriptor<Lists>, searchString: String) {
_lists = Query(filter: #Predicate {
if searchString.isEmpty {
return true
} else {
return $0.listName.localizedStandardContains(searchString)
}
}, sort: [sort])
}
func deleteLists(_ indexSet: IndexSet) {
for index in indexSet {
let list = lists[index]
modelContext.delete(list)
}
}
}
#Preview {
ListView(sort: SortDescriptor(\Lists.listName), searchString: "")
}
DisplayListView
import SwiftData
import SwiftUI
struct DisplayListView: View {
@Environment(\.modelContext) var modelContext
@Query(sort: [SortDescriptor(\Lists.listName, order: .reverse), SortDescriptor(\Lists.listName)]) var lists: [Lists]
var body: some View {
List {
ForEach(lists) { list in
NavigationLink(value: list) {
VStack(alignment: .leading) {
Text(list.listName)
.font(.headline)
Text(list.listDateAdded.formatted(.dateTime.day().month().year()))
}
}
}
}
.preferredColorScheme(.dark)
}
}
DisplayListDetailView
import SwiftData
import SwiftUI
struct DisplayListDetailView: View {
@Environment(\.modelContext) private var modelContext
@Bindable var list: Lists
@State private var newSeriesName = ""
var sortedSeries: [ListSeries] {
list.listSeries.sorted {
$0.seriesName < $1.seriesName
}
}
@State private var showingSearch = false
@State private var showingEdit = false
@State private var series = Series.example
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width * 0.22
let screenHeight = screenWidth * 1.5
ScrollView {
VStack(alignment: .leading) {
Text("\(list.listName)").bold()
.padding(.horizontal)
.padding(.top, 4)
Text("\(list.listDetails)")
.opacity(0.8)
.padding(.horizontal)
.padding(.vertical, 4)
Rectangle()
.frame(height: 1)
.foregroundColor(.gray)
.opacity(0.4)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), content: {
ForEach(sortedSeries, id: \.self) { series in
SeriesPoster(tvId: series.seriesID, screenWidth: screenWidth, screenHeight: screenHeight)
}
})
.padding(.horizontal)
Rectangle()
.frame(height: 1)
.foregroundColor(.gray)
.opacity(0.4)
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.preferredColorScheme(.dark)
.sheet(isPresented: $showingEdit) {
EditListDetailsView(list: list)
}
}
.toolbar {
Button() {
showingEdit.toggle()
} label: {
Label("Edit List", systemImage: "square.and.pencil")
}
}
}
}
}
EditListDetailsView
import SwiftData
import SwiftUI
struct EditListDetailsView: View {
@Environment(\.modelContext) private var modelContext
@Bindable var list: Lists
@State private var newSeriesName = ""
var sortedSeries: [ListSeries] {
list.listSeries.sorted {
$0.seriesName < $1.seriesName
}
}
@State private var showingSearch = false
@State private var series = Series.example
var body: some View {
Form {
TextField("List Name", text: $list.listName)
TextField("Details", text: $list.listDetails, axis: .vertical)
DatePicker("Date", selection: $list.listDateAdded)
Section("Series in List") {
ForEach(sortedSeries) { series in
HStack {
SeriesPoster(tvId: series.seriesID, screenWidth: 50, screenHeight: 75)
Text(series.seriesName)
}
}
.onDelete(perform: deleteSeries)
}
Section("Add a Series") {
HStack {
Button("Add") {
showingSearch.toggle()
}
}
}
}
.navigationTitle("Edit List")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showingSearch) {
ListSeriesSearch(list: list, isPresented: $showingSearch)
}
}
func addSeries() {
guard newSeriesName.isEmpty == false else { return }
withAnimation {
let series = ListSeries(seriesID: 1234, seriesDateAdded: Date.now, seriesName: newSeriesName)
list.listSeries.append(series)
newSeriesName = ""
}
}
func deleteSeries(_ indexSet: IndexSet) {
for index in indexSet {
let series = sortedSeries[index]
modelContext.delete(series)
}
}
}
#Preview {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Lists.self, configurations: config)
let example = Lists(listName: "Example Destination", listDetails: "Example details go here and will automatically expand vertically as they are edited.")
return EditListDetailsView(list: example)
.modelContainer(container)
} catch {
fatalError("Failed to create model container.")
}
}