I'm building a SwiftUI app using SwiftData @Query and struggling quite a bit with redraws and slow inserts.
- How can I ensure that redraws are automatically triggered on my Views (both ShowView and ContentView) after data is updated?
- How can I speed up my model update inserts?
Here's a simplified and representative version of my app:
// MARK: - Complete Copy+Paste Example:
import SwiftUI
import SwiftData
// MARK: - Entry
@main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Show.self)
}
}
// MARK: - View
struct ContentView: View {
@Environment(\.modelContext) private var modelContext: ModelContext
@State private var path = NavigationPath()
@Query private var shows: [Show]
var body: some View {
NavigationStack(path: $path) {
List {
ForEach(shows) { show in
Row(show: show)
}
}
.navigationDestination(for: Show.self) { show in
ShowView(show: show)
}
.toolbar {
Button {
// mimic 1st network call for basic show info
// works fine
let newShow = Show(name: .random(length: 5))
modelContext.insert(newShow)
} label: {
Image(systemName: "plus")
}
}
}
}
struct Row: View {
var show: Show
var body: some View {
NavigationLink(value: show) {
VStack(alignment: .leading) {
Text(show.name)
if let date = show.nextDate {
Text(date.formatted(date: .abbreviated, time: .shortened))
}
}
}
}
}
}
struct ShowView: View {
@Environment(\.modelContext) private var modelContext: ModelContext
@Bindable var show: Show
var body: some View {
VStack(alignment: .leading) {
Text(show.name)
if !show.episodes.isEmpty {
Text("Episodes: \(show.episodes.count)")
}
if let date = show.nextDate {
Text(date.formatted(date: .abbreviated, time: .shortened))
}
Button {
// 1. ISSUE: doesn't automatically force a SwiftUI redraw on this ShowView, or the main ContentView?
Task {
let actor = ShowActor(modelContainer: modelContext.container)
try await actor.update(show.persistentModelID)
}
} label: {
Text("Update")
}
}
}
}
// MARK: - ModelActor
@ModelActor
actor ShowActor {}
extension ShowActor {
func update(_ identifier: PersistentIdentifier) async throws {
guard let show = modelContext.model(for: identifier) as? Show else { return }
// mimics 2nd network call to add nextDate + episode info adds
show.nextDate = .randomDateInNext7Days()
// ISSUE: inserts are very slow, how to speed up?
for _ in 0...Int.random(in: 10...100) {
let episode = Episode(name: .random(length: 10))
modelContext.insert(episode) // crashes if episode isn't first insert before adding show?
episode.show = show
}
try modelContext.save()
}
}
// MARK: - Models
@Model
class Show {
var name: String
var nextDate: Date?
@Relationship(deleteRule: .cascade, inverse: \Episode.show) var episodes: [Episode]
init(name: String, nextDate: Date? = nil, episodes: [Episode] = []) {
self.name = name
self.nextDate = nextDate
self.episodes = episodes
}
}
@Model
class Episode {
var show: Show?
var name: String
init(show: Show? = nil, name: String) {
self.show = show
self.name = name
}
}
// MARK: - Helpers
extension String {
static func random(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyz"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
extension Date {
static func randomDateInNext7Days() -> Date {
Calendar.current.date(byAdding: .day, value: Int.random(in: 1...7), to: .now)!
}
}
// MARK: - Preview
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Show.self, configurations: config)
return ContentView()
.modelContainer(container)
}
Thanks in advance for your help!
Taskbecause View structs are value types that have no lifetime so must use the.task(id:)modifier in SwiftUI.List(shows)instead of aForEach(shows) { ...? I recently learned thatForEachdoes not properly refresh its body when the result of the@Queryupdate, i.e. theRowbody is not reevaluated.