NavigationLink on item from Query results in infinite loop

I have a situation where tapping on a NavigationLink on an item from a SwiftData Query results in an infinite loop, causing the app the freeze. If you run the code, make sure to add at least 1 item, then tap on it to see the issue.

Here is the code for a small sample app I made to illustrate the issue:

import SwiftUI
import SwiftData

@main
struct TestApp: App {
    
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
        
        do {
            let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
            return container
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

struct ContentView: View {
    var body: some View {
        NavigationStack {
            ListsView()
        }
    }
}

struct ListsView: View {
    @Environment(\.modelContext) private var modelContext
    
    @Query(filter: #Predicate<Item> { _ in true }) 
    private var items: [Item]
    
    var body: some View {
        List(items) { item in
            NavigationLink {
                ItemDetail()
            } label: {
                VStack {
                    Text("\(item.name) | \(item.date.formatted())")
                }
            }
        }
        
        Button("Add") {
            let newItem = Item(name: "Some item", date: .now)
            modelContext.insert(newItem)
            try? modelContext.save()
        }
    }
}

struct ItemDetail: View {
    private var model = ItemModel()
    
    var body: some View {
        VStack {
            Text("detail")
        }
    }
}

fileprivate var count = 0
class ItemModel {
    var value: Int
    
    init() {
        value = 99
        print("\(count)")
        count += 1
    }
}

@Model
final class Item {
    let name: String
    let date: Date
    
    init(name: String, date: Date) {
        self.name = name
        self.date = date
    }
}

In the test app above, the code in the initializer of ItemModel will run indefinitely. There are a few things that will fix this issue:

  • comment out the private var model = ItemModel() line in ItemDetail view
  • replace the @Query with a set list of Items
  • move the contents of the ListsView into the ContentView instead of referencing ListsView() inside the NavigationStack

But I'm not sure why this infinite loop is happening with the initializer of ItemModel. It seems like a SwiftData and/or SwiftUI bug, because I don't see a reason why this would happen. Any ideas? Has anyone run into something similar?

Post not yet marked as solved Up vote post of Flyingsand Down vote post of Flyingsand
175 views

Replies

I ran into a similar problem. It seems if a NavigationStack is placed in the parent view of a child view with a @Query with predicate, the child view will get invalidated when navigating to or back from the destination view. It seems if the destination View also references the model, that too will get invalidated leading to it navigating back before trying to navigate to the new item leading to a loop.

I managed to work around it a couple ways, but the simplest is adding a dummy intermediate View between the View with the NavigationStack and the one with the @Query with predicate like this:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            DummyView()
        }
    }
}

struct DummyView: View {
    var body: some View {
        ListsView()
    }
}

struct ListsView: View {
    @Environment(\.modelContext) private var modelContext
    
    @Query(filter: #Predicate<Item> { _ in true }) 

// ...
}