core-dataswiftui-navigationlinkdetailview

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


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!


Solution

  • 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()
        }
    }