【问题标题】:How to use a picker on CoreData relationships in SwiftUI如何在 SwiftUI 中对 CoreData 关系使用选择器
【发布时间】:2021-11-06 15:13:23
【问题描述】:

大家好,

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

目前,我有一个 3 视图应用程序(基于 Xcode 样板代码),它显示父实体列表,其中有子实体和子实体。我想要一个选择器来选择子实体应该引用哪个孙子。

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

  1. 当我将应用程序作为预览运行时(因此有预填充的数据...此示例代码会在没有数据的情况下中断),
  • picker 中选中的孙子是第一个的孙子 孩子,不管你第一次遇到哪个孩子 查看。
  • 当我退回并选择另一个子实体时,现在被选中的子实体会从子实体中获取正确的初始选择
  1. 当我选择一个子项并“保存”它时,子项摘要中的值不会改变,直到我单击另一个子项,此时值会在转换到模态视图之前发生变化。

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

这里有一个视频可以更清楚地说明这一点: 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

【问题讨论】:

  • 可能是因为您没有观察到 ObservableObjects/ CoreData 对象的变化。不要使用 @Binding@StateObservableObjects/ CoreData 对象使用 @ObservedObject 代替。这也将让您摆脱LicenceViewselectedElement 中的@FetchRequest,因为您可以改用使用$licence.licenced。通常,如果您发现自己在自定义 init 中完成所有这些工作,则 SwiftUI 不能很好地使用自定义 inits,您需要重新考虑您的方法。 SwiftUI 会随时重新加载 View,并且您的变量将重置。
  • 另外,在RegisterView 中使用sheet(item:)@State var currentLicence: Licence? 可能会更好。知道你不会看到这个变量有任何变化,因为它没有观察,它的唯一目的是触发sheet。如果您想查看更改,您必须在子视图中使用@ObservedObject
  • 您的选择器缺少标签。选择不知道有变化
  • 可能是类型问题尝试将您的标签更改为.tag(element as Element?),当然licence.licenced 应该是Element
  • 您还应该查看this @ObservedObject 不应该在它所在的View 中初始化。它应该来自以前的View。如果你在init 上这样做,你将会有泄漏和一些不稳定的情况@

标签: core-data swiftui picker


【解决方案1】:

我真的不明白这个

• 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 中使用预览,您需要使用 viewContextcreated 和 MockData 并将其传递给您的视图。在这里,我提供了一个通用代码,可以针对您的每个视图进行修改:

在您的Persistance 结构(CoreData 管理器)中,使用您的预览项目声明一个变量预览:


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

确保它的 init 中有 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)")
            }
        })
    }
}

第二个问题

Core Data 对象是用类构建的,所以它们的类型是一个引用。当您更改属性是一个类时,它不会通知视图结构用新值重绘。 (例外是类,它们是为了通知更改而创建的。)

您需要明确告诉您的RegisterView 结构在您关闭您的LicenceView 后重新绘制自己。您可以通过在 RegisterView - @State var id = UUID() 中再创建一个变量来实现。然后在VStack 末尾附加一个.id(id) 修饰符

VStack {
    //your code
}.id(id)

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

func viewDismissed() {
    id = UUID()
}

现在,使用可选参数 onDismiss 将此函数附加到您的工作表

.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
      LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
        }

【讨论】:

  • 谢谢 - 第二个很好地完成了元素更新。我很快就会为另一个问题添加一个视频。
  • 使用 id 技巧是低效的。 CoreData 对象是可观察的对象,Apple 提供了用于观察变化的包装器。你应该从 WWDC21 观看 Demystify SwiftUI,而不是有针对性地刷新你只是重新加载它
  • 谢谢 Lorem... 同意。努力让 CoreData 的所有内容都切换到 Observable 对象...
【解决方案2】:

好的。非常感谢 Lorem 让我得到答案。也感谢 Roma,但事实证明,他的解决方案虽然可以解决我的一个关键问题,但确实会导致效率低下 - 并且没有解决第二个问题。

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

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

  1. 使用@ObservedObject 而不是@State 来传递CoreData 对象
  2. 确保选择器已定义标签。我阅读的文档暗示,如果您使用“.self”作为 ForEach 中对象的 id,则会自动生成,但这似乎并不总是可靠的。所以在我的选择器中添加“.tag(element as Element?)”对这里有帮助。 注意:它必须是可选类型,因为 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")
            }
        }
        
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-21
    • 1970-01-01
    • 2020-12-12
    • 2021-07-18
    • 2020-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多