如何在 SwiftUI 中对 CoreData 关系使用选择器
How to use a picker on CoreData relationships in SwiftUI
大家好,
我正在尝试弄清楚 CoreData 关系如何与选择器等 UI 元素一起工作。
目前我有一个 3 视图应用程序(基于 Xcode 样板代码),它显示 parent 个实体的列表,其中有 children 其中有 child仁。我想要一个选择器 select 哪个 grandchild 一个 child 实体应该引用。
目前我有两个有趣的副作用:
- 当我 运行 应用程序作为预览时(所以有 pre-populated 数据...如果没有数据,此示例代码将中断),
- 选择器中的 selected grandchild 是第一个的 grandchild
child,不管你在第一个 child
查看。
- 当我后退并选择另一个 child 时,现在选择的从 child 实体
中获取了正确的初始 selection
- 当我 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 是去这里的方法。
所以我遇到的问题的解决方法是:
- 使用@ObservedObject 而不是@State 来传递 CoreData 对象
- 确保选择器定义了标签。我阅读的文档暗示如果您使用“.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")
}
}
}
}
大家好,
我正在尝试弄清楚 CoreData 关系如何与选择器等 UI 元素一起工作。
目前我有一个 3 视图应用程序(基于 Xcode 样板代码),它显示 parent 个实体的列表,其中有 children 其中有 child仁。我想要一个选择器 select 哪个 grandchild 一个 child 实体应该引用。
目前我有两个有趣的副作用:
- 当我 运行 应用程序作为预览时(所以有 pre-populated 数据...如果没有数据,此示例代码将中断),
- 选择器中的 selected grandchild 是第一个的 grandchild child,不管你在第一个 child 查看。
- 当我后退并选择另一个 child 时,现在选择的从 child 实体 中获取了正确的初始 selection
- 当我 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(isPresented: $showModal, onDismiss: viewDismissed) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
好的。非常感谢 Lorem 让我找到答案。也感谢罗马,但事实证明,他的解决方案虽然解决了我的一个关键问题,但确实引入了低效率 - 并且没有解决第二个问题。
如果其他人遇到同样的问题,我将保留 Github 存储库,但关键是当您在周围共享 CoreData 对象时不应使用 @State。 @ObservedObject 是去这里的方法。
所以我遇到的问题的解决方法是:
- 使用@ObservedObject 而不是@State 来传递 CoreData 对象
- 确保选择器定义了标签。我阅读的文档暗示如果您使用“.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")
}
}
}
}