use*_*742 6 core-data swift swiftui combine
Node具有name, createdAt, 多对多关系children和一对一关系parent(均为可选)的实体的核心数据模型。使用 CodeGen 类定义。
使用@FetchRequest带有 谓词的 a parent == nil,可以获取根节点并随后使用关系遍历树。
根节点 CRUD 可以很好地刷新视图,但对子节点的任何修改在重新启动之前都不会显示,尽管更改已保存在 Core Data 中。
下面代码中最简单的示例说明了子节点删除的问题。删除在 Core Data 中有效,但如果删除是在子级上,则视图不会刷新。如果在根节点上,视图刷新工作正常。
我是 Swift 的新手,所以如果这是一个相当基本的问题,我很抱歉,但是如何在更改子节点时刷新视图?
import SwiftUI
import CoreData
extension Node {
class func count() -> Int {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Node> = Node.fetchRequest()
do {
let count = try context.count(for: fetchRequest)
print("found nodes: \(count)")
return count
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(entity: Node.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
var nodes: FetchedResults<Node>
var body: some View {
NavigationView {
List {
NodeWalkerView(nodes: Array(nodes.map { $0 as Node }) )
}
.navigationBarItems(trailing: EditButton())
}
.onAppear(perform: { self.loadData() } )
}
func loadData() {
if Node.count() == 0 {
for i in 0...3 {
let node = Node(context: self.managedObjectContext)
node.name = "Node \(i)"
for j in 0...2 {
let child = Node(context: self.managedObjectContext)
child.name = "Child \(i).\(j)"
node.addToChildren(child)
for k in 0...2 {
let subchild = Node(context: self.managedObjectContext)
subchild.name = "Subchild \(i).\(j).\(k)"
child.addToChildren(subchild)
}
}
}
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
}
}
struct NodeWalkerView: View {
@Environment(\.managedObjectContext) var managedObjectContext
var nodes: [Node]
var body: some View {
ForEach( self.nodes, id: \.self ) { node in
NodeListWalkerCellView(node: node)
}
.onDelete { (indexSet) in
let nodeToDelete = self.nodes[indexSet.first!]
self.managedObjectContext.delete(nodeToDelete)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
}
}
struct NodeListWalkerCellView: View {
@ObservedObject var node: Node
var body: some View {
Section {
Text("\(node.name ?? "")")
if node.children!.count > 0 {
NodeWalkerView(nodes: node.children?.allObjects as! [Node] )
.padding(.leading, 30)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
编辑:
一个微不足道但不令人满意的解决方案是NodeListWakerCellView使用另一个来检索孩子,@FetchRequest但这感觉不对,因为对象已经可用。为什么要运行另一个查询?但也许这是目前附加发布功能的唯一方法?
我想知道是否有另一种方法可以Combine直接向孩子们使用出版商,也许在.map?
struct NodeListWalkerCellView: View {
@ObservedObject var node: Node
@FetchRequest var children: FetchedResults<Node>
init( node: Node ) {
self.node = node
self._children = FetchRequest(
entity: Node.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: false)],
predicate: NSPredicate(format: "%K == %@", #keyPath(Node.parent), node)
)
}
var body: some View {
Section {
Text("\(node.name ?? "")")
if node.children!.count > 0 {
NodeWalkerView(nodes: children.map({ $0 as Node }) )
.padding(.leading, 30)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
NSManagedObjectContextObjectsDidChange Notification通过观察和刷新,您可以轻松观察到所有变化View。
在下面的代码中,您可以按如下方式复制该问题。
2.将以下代码粘贴到项目中
运行NodeContentView模拟器
在第一个屏幕中选择一个节点
编辑节点名称
单击“返回”按钮
请注意,所选变量的名称没有更改。
怎么解决”
取消注释//NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)位于initCoreDataPersistence
按照步骤 3-6 操作
请注意,这次更新了节点名称。
import SwiftUI
import CoreData
extension Node {
public override func awakeFromInsert() {
super.awakeFromInsert()
self.createdAt = Date()
}
}
///Notice the superclass the code is below
class NodePersistence: CoreDataPersistence{
func loadSampleData() {
if NodeCount() == 0 {
for i in 0...3 {
let node: Node = create()
node.name = "Node \(i)"
for j in 0...2 {
let child: Node = create()
child.name = "Child \(i).\(j)"
node.addToChildren(child)
for k in 0...2 {
let subchild: Node = create()
subchild.name = "Subchild \(i).\(j).\(k)"
child.addToChildren(subchild)
}
}
}
save()
}
}
func NodeCount() -> Int {
let fetchRequest: NSFetchRequest<Node> = Node.fetchRequest()
do {
let count = try context.count(for: fetchRequest)
print("found nodes: \(count)")
return count
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
struct NodeContentView: View {
//Create the class that sets the appropriate context
@StateObject var nodePers: NodePersistence = .init()
var body: some View{
NodeListView()
//Pass the modified context
.environment(\.managedObjectContext, nodePers.context)
.environmentObject(nodePers)
}
}
struct NodeListView: View {
@EnvironmentObject var nodePers: NodePersistence
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
var nodes: FetchedResults<Node>
var body: some View {
NavigationView {
List {
NodeWalkerView(nodes: Array(nodes))
}
.navigationBarItems(trailing: EditButton())
.navigationTitle("select a node")
}
.onAppear(perform: { nodePers.loadSampleData()} )
}
}
struct NodeWalkerView: View {
@EnvironmentObject var nodePers: NodePersistence
//This breaks observation, it has no SwiftUI wrapper
var nodes: [Node]
var body: some View {
Text(nodes.count.description)
ForEach(nodes, id: \.objectID ) { node in
NavigationLink(node.name.bound, destination: {
NodeListWalkerCellView(node: node)
})
}
.onDelete { (indexSet) in
for idx in indexSet{
nodePers.delete(nodes[idx])
}
}
}
}
struct NodeListWalkerCellView: View {
@EnvironmentObject var nodePers: NodePersistence
@ObservedObject var node: Node
var body: some View {
Section {
//added
TextField("name",text: $node.name.bound) //<---Edit HERE
.textFieldStyle(.roundedBorder)
if node.children?.allObjects.count ?? -1 > 0{
NavigationLink(node.name.bound, destination: {
NodeWalkerView(nodes: node.children?.allObjects.typeArray() ?? [])
.padding(.leading, 30)
})
}else{
Text("empty has no children")
}
}.navigationTitle("Edit name on this screen")
}
}
extension Array where Element: Any{
func typeArray<T: Any>() -> [T]{
self as? [T] ?? []
}
}
struct NodeContentView_Previews: PreviewProvider {
static var previews: some View {
NodeContentView()
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}
///Generic CoreData Helper not needed just to make stuff easy.
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
//The setup for this is in XCode when you create a new project
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
init(){
//Observe all the changes in the context, then refresh the View that observes this using @StateObject, @ObservedObject or @EnvironmentObject
//There are other options, like NSPersistentStoreCoordinatorStoresDidChange for the coordinator
//https://developer.apple.com/documentation/foundation/nsnotification/name/1506884-nsmanagedobjectcontextobjectsdid
//NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
}
///Creates an NSManagedObject of any type
func create<T: NSManagedObject>() -> T{
T(context: context)
//Can set any defaults in awakeFromInsert() in an extension for the Entity
//or override this method using the specific type
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
save()
}
///Creates a sample
func addSample<T: NSManagedObject>() -> T{
return create()
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
internal func save(){
do{
try context.save()
}catch{
print(error)
}
}
@objc
func refreshView(){
objectWillChange.send()
}
}
Run Code Online (Sandbox Code Playgroud)
CoreDataPersistenceclass是可以与任何实体一起使用的泛型。只需将其复制到您的项目中,您就可以将其用作superclass您自己的 CoreData ViewModel,或者如果您没有任何要覆盖或添加的内容,则按原样使用它。
该解决方案的关键部分是未注释的行以及
selector告诉View重新加载的行。其他一切都是额外的
代码看起来很多,因为这是OP提供的,但解决方案包含在CoreDataPersistence. 请注意 theNodeContentView也context应该@FetchRequest与 the 相匹配CoreDataPersistence
选项2
对于这个特定的用例(子级与父级具有相同的类型),您可以使用Listwith children,init它简化了很多设置,并且大大减少了更新问题。
extension Node {
public override func awakeFromInsert() {
super.awakeFromInsert()
self.createdAt = Date()
}
@objc
var typedChildren: [Node]?{
self.children?.allObjects.typeArray()
}
}
struct NodeListView: View {
@EnvironmentObject var nodePers: NodePersistence
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
var nodes: FetchedResults<Node>
var body: some View {
NavigationView {
List(Array(nodes) as [Node], children: \.typedChildren){node in
NodeListWalkerCellView(node: node)
}
.navigationBarItems(trailing: EditButton())
.navigationTitle("select a node")
}
.onAppear(perform: { nodePers.loadSampleData()} )
}
}
struct NodeListWalkerCellView: View {
@EnvironmentObject var nodePers: NodePersistence
@ObservedObject var node: Node
var body: some View {
HStack {
//added
TextField("name",text: $node.name.bound)
.textFieldStyle(.roundedBorder)
Button("delete", role: .destructive, action: {
nodePers.delete(node)
})
}.navigationTitle("Edit name on this screen")
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1508 次 |
| 最近记录: |