In SwiftUI in iOS 18.1, `SectionedFetchRequest` is not refreshed when changes are done to the fetched entity's attributes.

Hi,

This issue started with iOS 18, in iOS 17 it worked correctly. I think there was a change in SectionedFetchRequest so maybe I missed it but it did work in iOS 17.

I have a List that uses SectionedFetchRequest to show entries from CoreData. The setup is like this:

struct ManageBooksView: View {
    @SectionedFetchRequest<Int16, MyBooks>(
        sectionIdentifier: \.groupType,
        sortDescriptors: [SortDescriptor(\.groupType), SortDescriptor(\.name)]
    )
    private var books: SectionedFetchResults<Int16, MyBooks>

    var body: some View {
        NavigationStack {
            List {
                ForEach(books) { section in
                    Section(header: Text(section.id)) {
                        ForEach(section) { book in
                            NavigationLink {
                                EditView(book: book)
                            } label: {
                                Text(book.name)
                            }
                        }
                    }
                }
            }
            .listStyle(.insetGrouped)
        }
    }
}
struct EditView: View {
       private var book: MyBooks
    init(book: MyBooks) {
       print("Init hit")
       self.book = book
    }
}

Test 1: So now when I change name of the Book entity inside the EditView and do save on the view context and go back, the custom EditView is correctly hit again.

Test 2: If I do the same changes on a different attribute of the Book entity the custom init of EditView is not hit and it is stuck with the initial result from SectionedFetchResults.

I also noticed that if I remove SortDescriptor(\.name) from the sortDescriptors and do Test 1, it not longer works even for name, so it looks like the only "observed" change is on the attributes inside sortDescriptors.

Any suggestions will be helpful, thank you.

Answered by DTS Engineer in 822824022

Thanks for your reminding. This post had slipped through the cracks...

Also thanks for your project, which does reproduce the issue. The issue seems to be that SwiftUI believes the change doesn't impact the second ForEach in the following code snippet, and hence doesn't trigger the update.

List {
    let _ = Self._printChanges() // This prints "ContentView: @56 changed."
    ForEach(items) { section in
        let _ = Self._printChanges() // This prints "ContentView: unchanged."
        Section {
            ForEach(section) { item in
                NavigationLink {
                    EditView(book: item)
                } label: {
                    VStack {
                        Text(item.timestamp!, formatter: itemFormatter)
                        Text("Text - \(item.text!)")
                    }
                }
            }
        }
    }
}

Interesting enough, if I wrap the NavigationLink with a custom view, SwiftUI will change the way it "optimizes" the graph, and trigger the update as we would expect:

List {
    ForEach(items) { section in
        Section {
            ForEach(section) { item in
                ListItemView(item: item)
            }
        }
    }
}

struct ListItemView: View {
    @ObservedObject var item: Item
    var body: some View {
        NavigationLink {
            EditView(book: item)
        } label: {
            VStack {
                Text(item.timestamp!, formatter: itemFormatter)
                Text("Text - \(item.text!)")
            }
        }
    }
}

I don't have a lot of insight about how SwiftUI determines the "dirty" views, but wrapping the view hierarchy into a custom view so SwiftUI doesn't optimize away the update seems reasonable to me.

If you don't mind, please give it a try and share it that works in your real-world app.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

While waiting for some official statement why this is happening in iOS 18 and not iOS 17, I found a workaround to force a refresh of the managedObjectContext when changes are saved.

Code sample:

@Environment(\.managedObjectContext) private var managedObjectContext

var body: some View {
    NavigationStack {
        Text("")
    }
    .onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)) { _ in
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            withAnimation {
                managedObjectContext.refreshAllObjects()
            }
        }
    }
}

It's not perfect but it gets the job done to update the list when any attributes are changed, not only those used as SortDescriptor. Hope this helps anyone facing this problem.

What you described probably happens because SwiftUI believes the result set of SectionedFetchRequest doesn't change, which is because:

  • The managed objects aren't changed. When checking the equality of Core Data managed objects, the system may use objectID, and in your case, changing an attribute doesn't change the object ID.

  • The order of the list item doesn't change.

NSManagedObject is not observable, but you can use ObservedObject to publish the changes:

struct EditView: View {
    //private var book: MyBooks
    @ObservedObject var book: MyBooks
    ...
}

With that, the change that EditView makes on an object should be published and be detected by SwiftUI.

If that doesn't work, please provide a workable code snippet that reproduces the issue. I'd take a further look.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Hi @DTS Engineer Thank for responding to my topic.

Marking the object with ObservedObject does not resolve the issue, the change is still not propagated back to the list that uses SectionedFetchRequest. I created a runnable project from the Xcode templates that shows the issue.

  • Clone this repo https://github.com/VladimirAmiorkov/SwiftUI-SectionedFetchRequest
  • Open the Edit List Test.xcodeproj
  • Run the project
  • Press the "+" to add some items, notice the "Text- Init 0"
  • Tap on the row to pen EditView
  • Notice the "Text- Init 0" that is show inside EditView
  • Tap on the button "Change text"
  • Notice the "Text- Init 0 ..." that is show inside EditView changed
  • Go back
  • Notice the "Text- Init 0" in the List is not updated (issue happens here)
  • Restart app
  • Notice the "Text- Init 0" in the List is updated

Same code works correct and updates the list if I switch from SectionedFetchRequest to FetchRequest. You can uncomment it in the project.

Or it also works if I add SortDescriptor(\.text) to the SectionedFetchRequest but that is not what I want as I do not want to list all "edible" entities of the object in the request.

Thank you for looking into this.

@DTS Engineer Did you see my comment above, is there anything else you need from me regarding this issue investigation?

Accepted Answer

Thanks for your reminding. This post had slipped through the cracks...

Also thanks for your project, which does reproduce the issue. The issue seems to be that SwiftUI believes the change doesn't impact the second ForEach in the following code snippet, and hence doesn't trigger the update.

List {
    let _ = Self._printChanges() // This prints "ContentView: @56 changed."
    ForEach(items) { section in
        let _ = Self._printChanges() // This prints "ContentView: unchanged."
        Section {
            ForEach(section) { item in
                NavigationLink {
                    EditView(book: item)
                } label: {
                    VStack {
                        Text(item.timestamp!, formatter: itemFormatter)
                        Text("Text - \(item.text!)")
                    }
                }
            }
        }
    }
}

Interesting enough, if I wrap the NavigationLink with a custom view, SwiftUI will change the way it "optimizes" the graph, and trigger the update as we would expect:

List {
    ForEach(items) { section in
        Section {
            ForEach(section) { item in
                ListItemView(item: item)
            }
        }
    }
}

struct ListItemView: View {
    @ObservedObject var item: Item
    var body: some View {
        NavigationLink {
            EditView(book: item)
        } label: {
            VStack {
                Text(item.timestamp!, formatter: itemFormatter)
                Text("Text - \(item.text!)")
            }
        }
    }
}

I don't have a lot of insight about how SwiftUI determines the "dirty" views, but wrapping the view hierarchy into a custom view so SwiftUI doesn't optimize away the update seems reasonable to me.

If you don't mind, please give it a try and share it that works in your real-world app.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I am not an expert in SwiftUI mechanics and how it marks views as "dirty" but the solution you provided looks 100% correct. But the initial issue definitely looks like an internal bug in SwiftUI.

My theory is that SwiftUI updates ListItemView because ListItemView.item is an ObservedObject, which tells SwiftUI that a list item view should be updated when there is a change on the item or its attribute.

Without wrapping the navigation link that way, SwiftUI determines that the item is not changed because item.objectID is the same, and infers that there is no need to update the navigation link label. EditView does have an ObservedObject attribute, but it's the destination view of the link, and is updated. Note that NSManagedObject is not Observable, and that its equality is based on objectID.

But yeah, SwiftUI folks will have the final answer.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

In SwiftUI in iOS 18.1, &#96;SectionedFetchRequest&#96; is not refreshed when changes are done to the fetched entity's attributes.
 
 
Q