swiftui 如何从细节获取核心数据值到编辑视图

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

通过构建具有核心数据的应用程序来学习 swiftui;卡在AddEdit的Detail到Edit的数据流问题;从 AddEdit 到 List 以及从 List 到 Detail 的流程都可以。搜索但没有在网上找到有用的信息或我不明白。这是该问题的简化项目。它在 13.2 beta 上符合要求并在模拟器上工作,但存在 Detail 中空白编辑视图的问题。

观看次数:

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
    }
}

非常感谢您的时间和建议!

下面是我制作的一个工作示例,它扩展了默认的 Core Data SwiftUI 应用程序模板,以在 sheet 中添加对项目时间戳的编辑。 sheet 加载子上下文中的项目,以便可以进行编辑,如果取消,编辑将被丢弃,但如果保存,则将更改推送到视图上下文并保存。如果您不熟悉用于编辑的子上下文,我推荐 Apple 的旧 CoreDataBooks sample project.

您需要知道的主要事情是,当我们使用 sheet 编辑某些内容时,我们使用的版本采用的是项目而不是布尔值。这使您可以正确配置编辑视图。

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[[=10=]] }.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)
    }
}

这是您的代码的简化版本只需将此代码粘贴到您的项目中,然后在您的应用程序的 body 某处调用 YourAppParent(),因为它创建了容器。

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
        }
    }
}

要添加需要对象的预览,您可以将此代码与新的 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)
        }
    }
}

并且由于 create() 现在是通用的,您可以使用实体的 extension 为变量添加默认值。

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