(Modified title and added Contact List example with code and behavior differences between Lists and LazyVStack/ForEach.)
Within a NavigationView/LazyVStack, mutually-exclusive ForEach loops group query results that come from a @FetchRequest. When an entity modification causes it to "switch" groups, Core Data is updated, the entity gets displayed in the appropriate group, but the entity has the previous data.
The code example creates two groups: 1) words that begin with a letter within A through M, and 2) words that begin with a letter within N through Z. Modifying a word from either group so that it should appear in the other group ... say change "A" to "Z" ... has the entity now appear in the other, proper group, but with the previous word.
Other key observations:
- Selecting the just-changed word ... from "A" to "Z" in the mentioned example ... brings up the ModifyView (via NavigationLink) showing the previous data ... an "A" and not the "Z".
- Changing LazyVStack to VStack produces the desired behavior: the change is shown correctly and in the proper section.
- Closing the app (swipe up in iOS) and reopening produces the desired behavior: the change is shown correctly and in the proper section.
- All updates to words that do not cause a group change work perfectly! For example, changing from B to C or from N to O works.
struct List: View {
@FetchRequest(
sortDescriptors: [SortDescriptor(\Entity.name_, order: .forward)]
) var allEntities: FetchedResults<Entity>
@State var showAddView = false
var body: some View {
NavigationView {
ScrollView {
LazyVStack { // Works as expected if VStack
// First ForEach: first half of alphabet
ForEach(allEntities.filter { fh in fh.firstHalfOfAlphabet() })
{ entity in
NavigationLink(destination: Modify(entity: entity)) {
Text(entity.name).font(.title).padding()
}
}
Text("A to M above ---- N to Z below")
// Second ForEach: second half of alphabet
ForEach(allEntities.filter { sh in !sh.firstHalfOfAlphabet() })
{ entity in
NavigationLink(destination: Modify(entity: entity)) {
Text(entity.name).font(.title).padding()
}
}
}
}
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showAddView) { New() }
.toolbar {
ToolbarItem() {
Button(action: { showAddView = true },
label: { Image(systemName: "plus.circle") }
)
}
}
}
}
}
Code to Modify an entity:
struct Modify: View {
@ObservedObject var entity: Entity
@Environment(\.managedObjectContext) var moc
@State var theName: String // Local working value
@Environment(\.dismiss) var dismiss
init(entity: Entity) {
self.entity = entity
_theName = State<String>(initialValue: entity.name)
}
var body: some View {
VStack {
TextField("Name", text: $theName).padding()
HStack {
Button("Save") {
entity.name = theName // Update @ObservedObject
try? moc.save() // Save to database
dismiss()
}
}
}.font(.title)
}
}
The Core Data database has the single entry name_ as a String. Helper code:
extension Entity {
var name: String {
get { return name_ ?? "Unknown" }
set { name_ = newValue }
}
}
extension Entity: Comparable {
// Compares first letters of the names to determine "smallest" name
public static func < (lhs: Entity, rhs: Entity) -> Bool {
return lhs.name < rhs.name
}
func firstHalfOfAlphabet() -> Bool {
if self.name.first!.uppercased() < "N" { return true }
else { return false }
}
}
NEW CONTENT. Implemented @SectionedFetchRequest as shown in the code below. However, the "Other Key Observations" noted above still apply. For example, using LazyVStack (rather than VStack) is problematic because updates are not reflected if a modification causes a switch from the first half to the second half of the alphabet. It's as if using LazyVStack provides the change "no reason to update".
struct List: View {
@SectionedFetchRequest<Bool, Entity>(
sectionIdentifier: \.firstHalfOfAlphabet,
sortDescriptors: [SortDescriptor(\.name_, order: .forward)]
) private var sectionedEntities
@State var showAddView = false
var body: some View {
NavigationView {
ScrollView {
LazyVStack { // Works as expected if VStack
ForEach(sectionedEntities) { section in
Section(header: HeaderView(sectionId: section.id)) {
ForEach(section) { entity in
NavigationLink(destination: Modify(entity: entity)) {
Text(entity.name).font(.title).padding()
}
}
}
}
}
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showAddView) { New() }
.toolbar {
ToolbarItem() {
Button(action: { showAddView = true },
label: { Image(systemName: "plus.circle") }
)
}
}
}
}
}
}
CONTACT LIST UPDATE. Added for relatability to show the issue. Contact List done with SectionedFetchRequest two ways: 1. Using a List (works), 2. Using LazyVStack and ForEach (fails for some updates).
- Contact List using a List
struct ContentView: View {
@SectionedFetchRequest<String, Contact>(
sectionIdentifier: \.lastNameInitial,
sortDescriptors: [
NSSortDescriptor(keyPath: \Contact.lastName, ascending: true),
NSSortDescriptor(keyPath: \Contact.firstName, ascending: true),
]
) var sectionedContacts
var body: some View {
NavigationView {
List(sectionedContacts) { section in
Section(header: Text("Lastnames with '\(section.id)'")) {
ForEach(section) { contact in
NavigationLink(destination: ModifyContact(contact: contact)) {
ContactView(contact: contact)
}
}
}
}
}
}
}
Lastname modification behavior: Result: Modifying Ommitt to Emmitt updates name and properly goes to the 'E' section.
- Contact List using LazyVStack and ForEach
struct ContentView: View {
@SectionedFetchRequest<String, Contact>(
sectionIdentifier: \.lastNameInitial,
sortDescriptors: [
NSSortDescriptor(keyPath: \Contact.lastName, ascending: true),
NSSortDescriptor(keyPath: \Contact.firstName, ascending: true),
]
) var sectionedContacts
var body: some View {
NavigationView {
LazyVStack {
ForEach(sectionedContacts) { section in
Section(header: Text("Lastnames '\(section.id)'")) {
ForEach(section) { contact in
NavigationLink(destination: ModifyContact(contact: contact)) { ContactView(contact: contact)
}
}
}
}
}
}
}
}
Lastname modification behavior: Result: Modifying Ommitt to Emmitt does NOT show name update but properly goes to the 'E' section.
Note: With a change to the first letter of the last name, no visual updates occur to last name, first name, or phone number even though the contact is sectioned properly as shown above. Without a change to the first letter of the last name, all fields display correctly.
Pairing a SectionedFetchRequest with LazyVStack fails to properly update when changes are made to the underlying data that cause a change in the item's section.
The alternative working examples above can be considered workarounds:
Another workaround is this: for a given item's update that would lead to a switch in section, rather than Update, perform Delete then Create for that changed entity. This approach works for the SectionedFetchRequest/LazyVStack examples above and works in code for an app I'm developing.