NavigationSplitView + FetchResults

Hello,

I'm using a NavigationSplitView and feeding the sidebar using FetchResults. In the detail view, the user can update the field name of the entity and then, the sidebar becomes useless, selecting random rows and hiding some of them, like these pictures:

I could reproduce the issue using a sample project. If you want to have the project to test it, create a simple project on Xcode: iOS app / +Use Core Data & +Host in CloudKit

Then, add a field "name String?" in the Entity and use this code in ContentView.swift:

import SwiftUI
import CoreData

extension Binding {
    public func replaceNil<T>(_ defaultValue: T) -> Binding<T> where Value == Optional<T> {
        return .init {
            wrappedValue ?? defaultValue
        } set: {
            wrappedValue = $0
        }
    }
}

struct Header: View {
    @ObservedObject var item: Item
    
    var body: some View {
        VStack {
            if let name = item.name, !name.isEmpty {
                Text(name)
            } else {
                Text("---")
                    .foregroundColor(.secondary)
            }
        }
    }
}

struct ItemDetail: View {
    @ObservedObject var item: Item
    
    var body: some View {
        VStack {
            Spacer()
            if let timestamp = item.timestamp {
                Text(timestamp, formatter: itemFormatter)
            } else {
                Text("---")
                    .foregroundColor(.secondary)
            }
            Button("Update") {
                item.timestamp = Date.now
            }
            TextField("Name", text: $item.name.replaceNil(""))
            Spacer()
        }
        .padding()
    }
}

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @State private var selected: Item.ID?

    @FetchRequest(
        sortDescriptors: [
            SortDescriptor(\.name, order: .forward),
            SortDescriptor(\.timestamp, order: .forward),
        ],
        animation: .default)
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selected) {
                ForEach(items) { item in
                    Header(item: item)
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            if let selected, let index = items.firstIndex(where: { $0.id == selected }) {
                ItemDetail(item: items[index])
            } else {
                Text("Select something")
            }
        }
    }

    private func addItem() {
        withAnimation {
            do {
                let newItem = Item(context: viewContext)
                newItem.name = "Name"
                newItem.timestamp = Date()
                try viewContext.save()
                selected = newItem.id
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .medium
    return formatter
}()

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
            .previewInterfaceOrientation(.landscapeLeft)
            
    }
}

Any ideas?

Could you detail more the use case and show Item definition ? Has it got a UUID ?

It is managed by CoreData:

I'm using the id of the object, but previously I was using a field "itemID" as a UUID, but it was the same behaviour.

It will be something like:

@objc(Item)
public class Item: NSManagedObject {

}

extension Item {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }

    @NSManaged public var itemId: UUID?
    @NSManaged public var name: String?
    @NSManaged public var timestamp: Date?

}

extension Item : Identifiable {

}

I have added an extension to set the id, but it is still the same:

extension Item {
    public var id: UUID { itemId ?? UUID() }
}

Thanks,

NavigationSplitView + FetchResults
 
 
Q