swiftui how to fetch core data values from Detail to Edit views

2.6k Views Asked by At

Learning swiftui by building an app with core data; stuck in an issue of data flow from Detail to Edit of AddEdit; the flows from AddEdit to List and from List to Detail are ok. Searched but didn't find useful info online or I don't understand. Here is a simplified project for the question. It complies ok on 13.2 beta and works on simulator, with the issue of blank Edit view from Detail.

views:

struct FileList: View {

    @FetchRequest(sortDescriptors: [ NSSortDescriptor(keyPath: \Item.fileName, ascending: false) ], animation: .default) var items: FetchedResults<Item>
    @State private var showAdd = false

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink(destination: FileDetail(item: item)) {
                        Text(item.fileName ?? "").font(.headline)
                    }
                }
            }
            .navigationTitle("List")
            .navigationBarItems(trailing: Button(action: {
                showAdd = true
            }, label: { Image(systemName: "plus.circle")
            })
            .sheet(isPresented: $showAdd) {
                FileAddEdit(items: VM())
            }
            )
        }
    }
}

struct FileList_Previews: PreviewProvider {
    static let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    static var previews: some View {
        FileList()
    }
}

struct FileDetail: View {
     
    @Environment(\.managedObjectContext) var context
    @Environment(\.presentationMode) var presentationMode
    @State var showingEdit = false
    @ObservedObject var item: Item

    var body: some View {
        VStack {
            Form {
                Text(self.item.fileName ?? "File Name")
                Button(action: {
                    showingEdit.toggle()
                }, label: {
                    title: do { Text("Edit")
                    }
                })
                .sheet(isPresented: $showingEdit) {
                    FileAddEdit(items: VM())
                }
            }
        }.navigationTitle("Detail")
    }
}

struct FileDetails_Previews: PreviewProvider {
    static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    static var previews: some View {
        let item = Item(context: moc)
        return NavigationView {
            FileDetail(item: item)
        }
    }
}

struct FileAddEdit: View {
    
    @Environment(\.managedObjectContext) var moc
    @ObservedObject var items = VM()
    
    var body: some View {
        NavigationView {
            VStack {
                Form {
                    TextField("File Name", text: $items.fileName)
                    Button(action: {
                        items.writeData(context: moc)
                    }, label: {
                    title: do { Text(items.updateFile == nil ? "Add" : "Edit")
                    }})
                }
            }
            .navigationTitle("\(items.updateFile == nil ? "Add" : "Edit")")
        }
    }
}

struct FileAddEdit_Previews: PreviewProvider {
    static var previews: some View {
        FileAddEdit(items: VM())
    }
}

VM:

class VM: ObservableObject {
    @Published var fileName = ""
    @Published var id = UUID()
    @Published var isNewData = false
    @Published var updateFile : Item!
    
    init() {
    }
    
    var temporaryStorage: [String] = []

    func writeData(context : NSManagedObjectContext) {
        if updateFile != nil {
            updateCurrentFile()
        } else {
            createNewFile(context: context)
        }
        do {
            try context.save()
        } catch {
            print(error.localizedDescription)
        }
    }
    
    func DetailItem(fileItem: Item){
        fileName = fileItem.fileName ?? ""
        id = fileItem.id ?? UUID()
        updateFile = fileItem
    }
    
    func EditItem(fileItem: Item){
        fileName = fileItem.fileName ?? ""
        id = fileItem.id ?? UUID()
        isNewData.toggle()
        updateFile = fileItem
    }
    
    private func createNewFile(context : NSManagedObjectContext) {
        let newFile = Item(context: context)
        newFile.fileName = fileName
        newFile.id = id
    }
    
    private func updateCurrentFile() {
        updateFile.fileName = fileName
        updateFile.id = id
    }
    
    private func resetData() {
        fileName = ""
        id = UUID()
        isNewData.toggle()
        updateFile = nil
    }
}

Much appreciated for your time and advices!

2

There are 2 best solutions below

6
lorem ipsum On BEST ANSWER

Here is a simplified version of your code Just paste this code into your project and call YourAppParent() in a body somewhere in your app as high up as possible since it creates the container.

import SwiftUI
import CoreData

//Class to hold all the Persistence methods
class CoreDataPersistence: ObservableObject{
    //Use preview context in canvas/preview
    let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
    
    ///Creates an NSManagedObject of **ANY** type
    func create<T: NSManagedObject>() -> T{
        T(context: context)
        //For adding Defaults see the `extension` all the way at the bottom of this post
    }
    ///Updates an NSManagedObject of any type
    func update<T: NSManagedObject>(_ obj: T){
        //Make any changes like a last modified variable
        //Figure out the type if you want type specific changes
        if obj is FileEnt{
            //Make type specific changes
            let name = (obj as! FileEnt).fileName
            print("I'm updating FileEnt \(name ?? "no name")")
        }else{
            print("I'm Something else")
        }
        
        save()
    }
    ///Creates a sample FileEnt
    //Look at the preview code for the `FileEdit` `View` to see when to use.
    func addSample() -> FileEnt{
        let sample: FileEnt = create()
        sample.fileName = "Sample"
        sample.fileDate = Date.distantFuture
        return sample
    }
    ///Deletes  an NSManagedObject of any type
    func delete(_ obj: NSManagedObject){
        context.delete(obj)
        save()
    }
    func resetStore(){
        context.rollback()
        save()
    }
    func save(){
        do{
            try context.save()
        }catch{
            print(error)
        }
    }
}
//Entry Point
struct YourAppParent: View{
    @StateObject var coreDataPersistence: CoreDataPersistence = .init()
    var body: some View{
        FileListView()
            //@FetchRequest needs it
            .environment(\.managedObjectContext, coreDataPersistence.context)
            .environmentObject(coreDataPersistence)
    }
}
struct FileListView: View {
    @EnvironmentObject var persistence: CoreDataPersistence
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \FileEnt.fileDate, ascending: true)],
        animation: .default)
    private var allFiles: FetchedResults<FileEnt>
    
    var body: some View {
        NavigationView{
            List{
                //Has to be lazy or it will create a bunch of objects because the view gets preloaded
                LazyVStack{
                    NavigationLink(destination: FileAdd(), label: {
                        Text("Add file")
                        Spacer()
                        Image(systemName: "plus")
                    })
                }
                ForEach(allFiles) { aFile in
                    NavigationLink(destination: FileDetailView(aFile: aFile)) {
                        Text(aFile.fileDate?.description ?? "no date")
                    }.swipeActions(edge: .trailing, allowsFullSwipe: true, content: {
                        Button("delete", role: .destructive, action: {
                            persistence.delete(aFile)
                        })
                    })
                }
            }
        }
    }
}
struct FileListView_Previews: PreviewProvider {
    static var previews: some View {
        YourAppParent()
//            let pers = CoreDataPersistence()
//            FileListView()
//                @FetchRequest needs it
//                .environment(\.managedObjectContext, pers.context)
//                .environmentObject(pers)
    }
}
struct FileDetailView: View {
    @EnvironmentObject var persistence: CoreDataPersistence
    @ObservedObject var aFile: FileEnt
    @State var showingFileEdit: Bool = false
    
    var body: some View{
        Form {
            Text(aFile.fileName ?? "")
        }
        Button(action: {
            showingFileEdit.toggle()
        }, label: {
            Text("Edit")
        })
            .sheet(isPresented: $showingFileEdit, onDismiss: {
                //Discard any changes that were not saved
                persistence.resetStore()
            }) {
                FileEdit(aFile: aFile)
                    //sheet needs reinject
                    .environmentObject(persistence)
            }
    }
}

///A Bridge to FileEdit that creates the object to be edited
struct FileAdd:View{
    @EnvironmentObject var persistence: CoreDataPersistence
    //This will not show changes to the variables in this View
    @State var newFile: FileEnt? = nil
    var body: some View{
        Group{
            if let aFile = newFile{
                FileEdit(aFile: aFile)
            }else{
                //Likely wont ever be visible but there has to be a fallback
                ProgressView()
                    .onAppear(perform: {
                        newFile = persistence.create()
                    })
            }
        }
        .navigationBarHidden(true)
        
    }
}
struct FileEdit: View {
    @EnvironmentObject var persistence: CoreDataPersistence
    @Environment(\.dismiss) var dismiss
    //This will observe changes to variables
    @ObservedObject var aFile: FileEnt
    var viewHasIssues: Bool{
        aFile.fileDate == nil || aFile.fileName == nil
    }
    var body: some View{
        Form {
            TextField("required", text: $aFile.fileName.bound)
            //DatePicker can give the impression that a date != nil
            if aFile.fileDate != nil{
                DatePicker("filing date", selection: $aFile.fileDate.bound)
            }else{
                //Likely wont ever be visible but there has to be a fallback
                ProgressView()
                    .onAppear(perform: {
                        //Set Default
                        aFile.fileDate = Date()
                    })
            }
        }
        
        Button("save", role: .none, action: {
            persistence.update(aFile)
            dismiss()
        }).disabled(viewHasIssues)
        Button("cancel", role: .destructive, action: {
            persistence.resetStore()
            dismiss()
        })
    }
}

extension Optional where Wrapped == String {
    var _bound: String? {
        get {
            return self
        }
        set {
            self = newValue
        }
    }
    var bound: String {
        get {
            return _bound ?? ""
        }
        set {
            _bound = newValue
        }
    }
    
}
extension Optional where Wrapped == Date {
    var _bound: Date? {
        get {
            return self
        }
        set {
            self = newValue
        }
    }
    public var bound: Date {
        get {
            return _bound ?? Date.distantPast
        }
        set {
            _bound = newValue
        }
    }
}

For adding a preview that requires an object you can use this code with the new CoreDataPersistence

/// How to create a preview that requires a CoreData object.
struct FileEdit_Previews: PreviewProvider {
    static let pers = CoreDataPersistence()
    static var previews: some View {
        VStack{
            FileEdit(aFile: pers.addSample()).environmentObject(pers)
        }
    }
}

And since the create() is now generic you can use the Entity's extension to add defaults to the variables.

extension FileEnt{ 
    public override func awakeFromInsert() {
        //Set defaults here
        self.fileName = ""
        self.fileDate = Date()
    }
}
1
malhal On

Below is a working example I made that extends the default Core Data SwiftUI app template to add editing of the Item's timestamp in a sheet. The sheet loads the item in the child context so edits can be made and if cancelled the edits are discarded but if saved then the changes are pushed in to the view context and it is saved. If you are unfamilar with child contexts for editing I recommend Apple's old CoreDataBooks sample project.

The main thing you need to know is when we are using a sheet to edit something we use the version that takes an item rather than a boolean. That allows you to configure the editing View correctly.

    import SwiftUI
    import CoreData
    
    struct ItemEditorConfig: Identifiable {
        let id = UUID()
        let context: NSManagedObjectContext
        let item: Item
        
        init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
            // create the scratch pad context
            context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
            context.parent = viewContext
            // load the item into the scratch pad
            item = context.object(with: objectID) as! Item
        }
    }
    
    struct ItemEditor: View {
        @ObservedObject var item: Item // this is the scratch pad item
        @Environment(\.managedObjectContext) private var context
        @Environment(\.dismiss) private var dismiss
        let onSave: () -> Void
        @State var errorMessage: String?
        
        var body: some View {
            NavigationView {
                Form {
                    Text(item.timestamp!, formatter: itemFormatter)
                    if let errorMessage = errorMessage {
                        Text(errorMessage)
                    }
                    Button("Update Time") {
                        item.timestamp = Date()
                    }
                }
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("Cancel") {
                            dismiss()
                        }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Save") {
                            // first save the scratch pad context then call the handler which will save the view context.
                            do {
                                try context.save()
                                errorMessage = nil
                                onSave()
                            } catch {
                                let nsError = error as NSError
                                errorMessage  = "Unresolved error \(nsError), \(nsError.userInfo)"
                            }
                        }
                    }
                }
            }
        }
    }
    
    
    struct DetailView: View {
        @Environment(\.managedObjectContext) private var viewContext
        @ObservedObject var item: Item
        @State var itemEditorConfig: ItemEditorConfig?
        
        var body: some View {
            
            Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: edit) {
                            Text("Edit")
                        }
                    }
                }
                .sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
                    ItemEditor(item: config.item) {
                        do {
                            try viewContext.save()
                        } catch {
                            // Replace this implementation with code to handle the error appropriately.
                            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                            let nsError = error as NSError
                            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                        }
                        itemEditorConfig = nil
                    }
                    .environment(\.managedObjectContext, si.context)
                }
        }
        
        func edit() {
            itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: item.objectID)
        }
        
        func didDismiss() {
            // Handle the dismissing action.
        }
    }
    
    struct ContentView: View {
        @Environment(\.managedObjectContext) private var viewContext
    
        @FetchRequest(
            sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
            animation: .default)
        private var items: FetchedResults<Item>
        
        var body: some View {
            NavigationView {
                
                List {
                    ForEach(items) { item in
                        NavigationLink {
                            DetailView(item: item)
                        } label: {
                            Text(item.timestamp!, formatter: itemFormatter)
                        }
                    }
                    .onDelete(perform: deleteItems)
                }
                .toolbar {
                    
                    ToolbarItem(placement: .navigationBarTrailing) {
                        EditButton()
                    }
                    ToolbarItem {
                        Button(action: addItem) {
                            Label("Add Item", systemImage: "plus")
                        }
                    }
                }
                Text("Select an item")
            }
        }
    
        
        private func addItem() {
            withAnimation {
                let newItem = Item(context: viewContext)
                newItem.timestamp = Date()
    
                do {
                    try viewContext.save()
                } catch {
                    // Replace this implementation with code to handle the error appropriately.
                    // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                    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 {
                    // Replace this implementation with code to handle the error appropriately.
                    // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                    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)
        }
    }