2

我是 swift 新手,我无法理解环境变量的工作原理。

在 Core Data 中,我创建了一个名为“API”的新实体,它具有一个属性 id:Int32。

然后在 SwiftUI 中,我想找到 id 的最大值。我写了一个请求,但是每当我使用传递给视图作为环境变量 managedObjectContext 时,它总是使我的应用程序/预览崩溃。这是使用 NSManagedObjectContext.fetch(NSFetchRequest) 后的崩溃信息(使用 FetchRequest 仅提供带有异常 EXC_BAD_INSTRUCTION 的堆栈跟踪)

...
Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

External Modification Warnings:
Thread creation by external task.

Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The fetch request's entity 0x600003c54160 'API' appears to be from a different NSManagedObjectModel than this context's'
terminating with uncaught exception of type NSException
abort() called
CoreSimulator 704.12 - Device: iPhone 11 (8356FF2A-5F0A-42F7-AA32-396FADCF2BF6) - Runtime: iOS 13.4 (17E255) - DeviceType: iPhone 11

Application Specific Backtrace 1:
0   CoreFoundation                      0x00007fff23e3dcce __exceptionPreprocess + 350
1   libobjc.A.dylib                     0x00007fff50b3b9b2 objc_exception_throw + 48
2   CoreData                            0x00007fff239c6b99 -[NSManagedObjectContext executeFetchRequest:error:] + 5004
3   libswiftCoreData.dylib              0x00007fff513b63d4 $sSo22NSManagedObjectContextC8CoreDataE5fetchySayxGSo14NSFetchRequestCyxGKSo0gH6ResultRzlF + 68
...

请记住,此错误会根据我使用的项目而变化。在我的主要项目中,我遇到了这样的错误:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'WebsiteAPI''

这是我正在使用的代码

import SwiftUI
import CoreData

struct test: View {
    private var id: Int32
    @Environment(\.managedObjectContext) var managedObjectContext

    var body: some View {
        Text("id=\(id)")
    }

    public init(context: NSManagedObjectContext) {
        self.id = -1

        //this crashes and gives no usefull information
//        let request2 = FetchRequest<API>(
//            entity: API.entity(),
//            sortDescriptors: [NSSortDescriptor(keyPath: \API.id, ascending: false)]
//        )
//        self.id = request2.wrappedValue.first?.id ?? 1

        guard let context2 = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
            fatalError("Unable to read managed object context.")
        }

        let request = NSFetchRequest<API>(entityName: "API")
        request.sortDescriptors = [NSSortDescriptor(keyPath: \API.id, ascending: false)]
        do {
            var commits = try context.fetch(request)   // OK
            commits = try context2.fetch(request)  // OK
            //commits = try self.managedObjectContext.fetch(request)  // causing crash
            self.id = Int32(commits.count)
        } catch let error {
            print(error.localizedDescription)
        }
    }
}

struct test_Previews: PreviewProvider {
    static var previews: some View {
        guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
            fatalError("Unable to read managed object context.")
        }
        return test(context: context).environment(\.managedObjectContext, context)
    }
}

所有评论的行崩溃应用程序。为什么从 AppDelegate.persistentContainer.viewContext 获取上下文工作得很好,但是使用我认为应该相同的环境变量 managedObjectContext 不起作用?我花了5个小时,检查了几乎所有东西,尝试了很多东西但没有成功。最后我可以继续从 AppDelegate 获取上下文,但是环境变量有什么问题?我错过了一些常识还是只是一个错误?我对在 Xcode 中遇到的错误感到头疼,从清除构建文件夹后缺少自动完成到更改所有引用的结构/文件名后出现数百个错误,尽管之后成功构建。每天重启几次 Xcode 以使其正常工作对我来说是正常的。

还有一些我注意到的事情,当我将 FetchRequest 创建为变量并在正文中的某个列表中使用它时,它起作用了。问题仅在于,当我尝试在代码/函数/init 中手动获取内容时,例如按钮操作或方法 onAppear、init 等。我尝试在物理设备上运行应用程序并显示预览。一样的效果。

我将 Xcode 11.4 与 Swift 5 一起使用。

4

1 回答 1

1

ViewSwiftUI 中的结构体是值类型,不能以正常方式初始化任何对象,因为 View 结构体仅在状态更改期间创建,然后就消失了。因此,他们创建的任何对象都会立即丢失。例如,在您的 init 方法中创建NSFetchRequestNSSortDescriptor以及所有获取的对象。View 结构通常在每次状态发生变化并且父主体运行时初始化,因此您将创建数千个堆对象,这些对象将填满内存并减慢 SwiftUI 的爬行速度。这些问题可以在 Instruments->SwiftUI "analysis for tracking .body invocations for View types" 中诊断。

显然,我们确实需要对象,所以这就是属性包装器的用武之地。通过在您的对象分配前加上一个属性包装器,然后以一种特殊的方式创建对象,它只初始化一次,并且每次都将相同的实例赋予新结构重新创建它的时间。正如我所说,这在 SwiftUI 中一直发生,更频繁或更不频繁取决于您在组织 View 结构层次结构上付出了多少努力。健康警告:目前在线提供的大多数示例代码对此都付出了零努力,并且不必要地更新了大量的视图层次结构,因为它们的视图结构像视图控制器一样设计,而不是使它们尽可能小并且只具有实际用于 body 的属性。

要解决您的问题,您需要使用属性包装器@StateObject来安全地初始化您的对象,并且它必须符合ObservableObject so that SwiftUI can be notified that the object will be changing so that after all objects have notified it can call body which will certainly be needed, unless the developer did not use the object in their body in which case the code is badly written. The object is created once just before the View's body is called, and then every time the View is recreated it is given the existing object rather than creating a new one. When the view is no longer shown it is automatically deinit. UseonAppear to configure the object the first time the View appears, andonChange to update it. Fust have a funcfetch that supplies themanagedObjectContext and yourid fetch param and in it create aNSFetchedResultsController perform the fetch and set thefetchedObjects on an@Published property that theView can use. When the object sets its items it will automatically cause theView body to be called again updating. SwiftUI compares the body to the previously returned body and uses the differences to render the screen (using actualUIView`s)。这是我制作的完整工作示例:

import SwiftUI
import CoreData

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            MasterView(name:"Master")
                .navigationTitle("Master")
        }
    }
}


class ItemsFetcher : NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
    var managedObjectContext : NSManagedObjectContext?
    
    @Published
    private(set) var items : Array<Item> = []

    lazy var fetchedResultsController : NSFetchedResultsController<Item> = {
        let frc = NSFetchedResultsController<Item>(fetchRequest: Item.myFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
        frc.delegate = self
        return frc
    }()
    
    func fetch(name:String, ascending: Bool){
        fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "name = %@", name)
        fetchedResultsController.fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: ascending)]
        try! fetchedResultsController.performFetch()
        items = fetchedResultsController.fetchedObjects ?? []
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        objectWillChange.send()
        items = fetchedResultsController.fetchedObjects ?? []
    }
}

struct MasterView: View {
    @Environment(\.managedObjectContext) private var viewContext
    let name: String
    @State
    var ascending = false
    
    @StateObject private var itemsFetcher = ItemsFetcher()
    
    var body: some View {
        List {
            ForEach(itemsFetcher.items) { item in
                Text("Item at \(item.timestamp!, formatter: itemFormatter)")
            }
            .onDelete(perform: deleteItems)
        }
        .toolbar {
            #if os(iOS)
            ToolbarItem(placement: .navigation){
                EditButton()
            }
            #endif
            ToolbarItem(placement: .automatic){
                Button(action: addItem) {
                    Label("Add Item", systemImage: "plus")
                }
            }
            ToolbarItem(placement: .bottomBar){
                Button(action: {
                    ascending.toggle()
                }) {
                    Text(ascending ? "Descending" : "Ascending")
                }
            }
        }
        .onAppear() {
            itemsFetcher.managedObjectContext = viewContext
            fetch()
        }
        .onChange(of: ascending) { newValue in
            fetch()
        }
        
    }
    
    func fetch(){
        itemsFetcher.fetch(name: name, ascending: ascending)
    }
    
    private func addItem() {
        
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
            newItem.name = "Master"

            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 {itemsFetcher.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)
    }
}
于 2020-09-11T11:10:25.667 回答