如何在 SwiftUI 中对 CoreData 关系使用选择器

How to use a picker on CoreData relationships in SwiftUI

大家好,

我正在尝试弄清楚 CoreData 关系如何与选择器等 UI 元素一起工作。

目前我有一个 3 视图应用程序(基于 Xcode 样板代码),它显示 parent 个实体的列表,其中有 children 其中有 child仁。我想要一个选择器 select 哪个 grandchild 一个 child 实体应该引用。

目前我有两个有趣的副作用:

  1. 当我 运行 应用程序作为预览时(所以有 pre-populated 数据...如果没有数据,此示例代码将中断),
  1. 当我 select 一个 child 并“保存”它时,child 摘要中的值不会改变,直到我在此时单击另一个 child在转换到模态视图之前值发生变化。

在 Swift 中呈现模态时,我对事件顺序的理解显然遗漏了一些东西UI...有什么能说明我做错了什么吗?

这里有一段视频可以更清楚地说明这一点: https://github.com/andrewjdavison/Test31/blob/main/Test31%20-%20first%20click%20issue.mov?raw=true

Git 示例存储库是 https://github.com/andrewjdavison/Test31.git,但总而言之:

数据模型:

查看源代码:

import SwiftUI
import CoreData


struct LicenceView : View {
    @Environment(\.managedObjectContext) private var viewContext
    @Binding var licence: Licence
    @Binding var showModal: Bool
    
    @State var selectedElement: Element
    @FetchRequest private var elements: FetchedResults<Element>
    
    init(currentLicence: Binding<Licence>, showModal: Binding<Bool>, context: NSManagedObjectContext) {
        self._licence = currentLicence
        self._showModal = showModal
        
        let fetchRequest: NSFetchRequest<Element> = Element.fetchRequest()
        fetchRequest.sortDescriptors = []
        self._elements = FetchRequest(fetchRequest: fetchRequest)
        
        _selectedElement = State(initialValue: currentLicence.wrappedValue.licenced!)
    }
        
    func save() {
        licence.licenced = selectedElement
        try! viewContext.save()
        showModal = false
        
    }
    
    var body: some View {
        VStack {
            Button(action: {showModal = false}) {
                Text("Close")
            }
            Picker(selection: $selectedElement, label: Text("Element")) {
                ForEach(elements, id: \.self) { element in
                    Text("\(element.desc!)")
                }
            }
            Text("Selected: \(selectedElement.desc!)")
            Button(action: {save()}) {
                Text("Save")
            }
        }
        
    }
}

struct RegisterView : View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @State var showModal: Bool = false
    var currentRegister: Register
    
    @State var currentLicence: Licence
    
    init(currentRegister: Register) {
        currentLicence = Array(currentRegister.licencedUsers! as! Set<Licence>)[0]
        self.currentRegister = currentRegister
    }
    
    var body: some View {
        VStack {
            List {
                ForEach (Array(currentRegister.licencedUsers! as! Set<Licence>), id: \.self) { licence in
                    Button(action: {currentLicence = licence; showModal = true}) {
                        HStack {
                            Text("\(licence.leasee!) : ")
                            Text("\(licence.licenced!.desc!)")
                        }
                    }
                }
            }
        }
        .sheet(isPresented: $showModal) {
            LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
        }
    }
}


struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Register.id, ascending: true)],
        animation: .default)
    private var registers: FetchedResults<Register>

    var body: some View {
        NavigationView {
            List {
                ForEach(registers) { register in
                    NavigationLink(destination: RegisterView(currentRegister: register)) {
                        Text("Register id \(register.id!)")
                    }
                }
            }
        }
    }
}

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


[1]: https://i.stack.imgur.com/AfaNb.png

我不是很明白这个

• selected grandchild in the picker is the grandchild of the first child, irrespective of which child you're dropped into in the first view.
• When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity

你能附上一个代表问题的视频吗?

但是我可以给你解决预览问题和第二个的问题

预览

如果您使用 Core Data 进行预览,则需要使用 viewContext 通过 MockData 创建并将其传递给您的视图。我在这里提供一个通用代码,可以根据您的每个视图进行修改:

在您的 Persistance 结构 (CoreData Manager) 中用您的预览项目声明变量预览:


static var preview: PersistenceController = {
    let result = PersistenceController(inMemory: true)
    let viewContext = result.container.viewContext
    // Here you create your Mock Data
    let newItem = Item(context: viewContext)
    newItem.yourProperty = yourValue
    
    do {
        try viewContext.save()
    } catch {
       // error handling
    }
    return result
}()

确保它在它的初始化中有 inMemory: Bool,因为它负责分离真正的 viewContext 和 previewContext:


init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "TestCD")
    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    }
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
}

从您的 viewContext 创建 Mock Item 并将其传递给预览:

     struct YourView_Previews: PreviewProvider {
        static var previews: some View {
            let context = PersistenceController.preview.container.viewContext
            let request: NSFetchRequest<Item> = Item.fetchRequest()
            let fetchedItems = try! context.fetch(request)
            YourView(item: fetchedItems)
        }
    }

如果您使用 @FetchRequest@FetchedResults 会更容易,因为它们会为您创建和获取对象。只需像这样实现预览:

     struct YourView_Previews: PreviewProvider {
        static var previews: some View {
            YourView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
        }
    }

这是 Xcode 在项目初始化时创建的 Persistence 结构:

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        let item = Item(context: viewContext)
        item.property = yourProperty
        
        do {
            try viewContext.save()
        } catch {
          
        }
        return result
    }()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "TestCD")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
               
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

第二题

核心数据对象是用 classes 构建的,因此它们的类型是引用。当您将 属性 更改为 class 时,它不会通知视图结构使用新值重绘。 (例外是 classes,创建它们是为了通知更改。)

您需要明确告诉您的 RegisterView 结构在您关闭 LicenceView 后重绘自身。您可以通过在 RegisterView - @State var id = UUID() 中再创建一个变量来实现。然后在 VStack

的末尾附加一个 .id(id) 修饰符
VStack {
    //your code
}.id(id)

最后,创建一个函数 viewDismissed,它将更改结构中的 id 属性:

func viewDismissed() {
    id = UUID()
}

现在,使用可选参数 onDismiss

将此函数附加到您的 sheet
.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
      LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
        }

好的。非常感谢 Lorem 让我找到答案。也感谢罗马,但事实证明,他的解决方案虽然解决了我的一个关键问题,但确实引入了低效率 - 并且没有解决第二个问题。

如果其他人遇到同样的问题,我将保留 Github 存储库,但关键是当您在周围共享 CoreData 对象时不应使用 @State。 @ObservedObject 是去这里的方法。

所以我遇到的问题的解决方法是:

  1. 使用@ObservedObject 而不是@State 来传递 CoreData 对象
  2. 确保选择器定义了标签。我阅读的文档暗示如果您使用“.self”作为 ForEach 中对象的 ID,它会自动生成,但似乎这并不总是可靠的。所以在我的选择器中添加“.tag(元素作为元素?)”在这里有所帮助。 注意:它必须是可选类型,因为 CoreData 使所有属性类型都是可选的。

这两个人解决了问题。

修改后的“LicenceView”结构在这里,但整个解决方案在 repo 中。

干杯!

struct LicenceView : View {
    @Environment(\.managedObjectContext) private var viewContext
    @ObservedObject var licence: Licence
    @Binding var showModal: Bool
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Element.desc, ascending: true)],
        animation: .default)
    private var elements: FetchedResults<Element>
    
    func save() {
        try! viewContext.save()
        showModal = false
    }
    
    var body: some View {
        VStack {
            Button(action: {showModal = false}) {
                Text("Close")
            }
            Picker(selection: $licence.licenced, label: Text("Element")) {
                ForEach(elements, id: \.self) { element in
                    Text("\(element.desc!)")
                        .tag(element as Element?)
                }
            }
            Text("Selected: \(licence.licenced!.desc!)")
            Button(action: {save()}) {
                Text("Save")
            }
        }
        
    }
}