SwiftUI TextField未在同级视图中更新带视频

问题描述

我有一个简单的待办事项清单演示应用程序,该应用程序是为了解SwiftUI与Core Data之间的关系而构建的。当我在Task视图中修改TaskDetail时,所做的更改不会反映在TextField视图中的TaskRow内。这两个视图都是ContentView的子视图。

Sudo Fix :如果我将TextField更改为Text,则视图将按预期更新;但是,我需要从该行编辑title中的Task属性

第二个选项:似乎每个教程都避免使用Core Data更新子视图中的数据。我可以使用@EnvironmentObject轻松地跨视图(使用结构)同步数据。但是,保持环境数据和Core Data存储同步听起来像一场噩梦。我希望有一种更简单的方法:D

发布视频https://youtu.be/JV-jQHpXE4Y

代码

ContentView.swift

import SwiftUI
import CoreData

struct ContentView: View {
    
    @Environment(\.managedobjectContext) var context
    @FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(keyPath: \Task.position,ascending: true)]) var tasks: FetchedResults<Task>
    
    init() {
        print("INIT - Content View")
    }
    
    var body: some View {
        NavigationView {
            vstack {
                todoList
                newButton
            }
        }
    }
}

extension ContentView {
    
    var todoList: some View {
        List {
            ForEach(self.tasks,id: \.id) { task in
                NavigationLink(destination: TaskDetail(task: task)) {
                    TaskRow(task: task)
                }
            }
            .onDelete { indices in
                for index in indices {
                    self.context.delete(self.tasks[index])
                    try? self.context.save()
                }
            }
            .onMove(perform: move)
        }
        .navigationBarItems(trailing: EditButton())
    }
    
    var newButton: some View {
        Button(action: {
            self.newTask()
        },label: {
            Text("Add Random Task")
        }).padding([.bottom,.top],20)
    }
}

extension ContentView {
    private func newTask() {
        let things = ["Cook","Clean","Eat","Workout","Program"]
        
        let newItem = Task(context: self.context)
        newItem.id = UUID()
        newItem.title = things.randomElement()!
        newItem.position = Int64(self.tasks.count)
        newItem.completed = Bool.random()
        
        try? self.context.save()
    }
    
    private func move(from source: IndexSet,to destination: Int) {
        // Make an array of items from fetched results
        var revisedItems: [Task] = self.tasks.map{ $0 }
        
        // change the order of the items in the array
        revisedItems.move(fromOffsets: source,toOffset: destination )
        
        // update the userOrder attribute in revisedItems to
        // persist the new order. This is done in reverse order
        // to minimize changes to the indices.
        for reverseIndex in stride(from: revisedItems.count - 1,through: 0,by: -1) {
            revisedItems[reverseIndex].position = Int64(reverseIndex)
            try? self.context.save()
        }
    }
}

TaskRow.swift


import SwiftUI
import CoreData

struct TaskRow: View {

    @Environment(\.managedobjectContext) var context
    @Observedobject var task: Task
    
    @State private var title: String
    
    init(task: Task) {
        self.task = task
        self._title = State(initialValue: task.title ?? "")
        print("INIT - TaskRow Initialized: title=\(title),completed=\(task.completed)")
    }
    
    var body: some View {
        HStack {
            TextField(self.task.title ?? "",text: self.$title) {
                self.task.title = self.title
                self.save()
            }.foregroundColor(.black)
//            Text(self.task.title ?? "")
            Spacer()
            Text("\(self.task.position)")
            Button(action: {
                self.task.completed.toggle()
                self.save()
            },label: {
                Image(systemName: self.task.completed ? "checkmark.square" : "square")
            }).buttonStyle(BorderlessButtonStyle())
        }
    }
}

extension TaskRow {
    func save() {
        try? self.context.save()
        print("SAVE - TaskRow")
    }
}

TaskDetail.swift



import SwiftUI

struct TaskDetail: View {
    
    @Environment(\.managedobjectContext) var context
    @Observedobject var task: Task
    
    @State private var title: String
    
    init(task: Task) {
        self.task = task
        self._title = State(initialValue: task.title ?? "")
        print("INIT - TaskDetail Initialized: title=\(title),completed=\(task.completed)")
    }
    
    var body: some View {
        Form {
            Section {
                TextField(self.title,text: self.$title) {
                    self.task.title = self.title
                    self.save()
                }.foregroundColor(.black)
            }
            Section {
                Button(action: {
                    self.task.completed.toggle()
                    self.save()
                },label: {
                    Image(systemName: self.task.completed ? "checkmark.square" : "square")
                }).buttonStyle(BorderlessButtonStyle())
            }
        }
    }
}

extension TaskDetail {
    func save() {
        try? self.context.save()
        print("SAVE - TaskDetail")
    }
}

Task的核心数据模型

Core Data Model

编辑

这与TextField中的'PlaceHolder'文本(第一个参数)有关。如果我在Task修改TaskDetail,然后导航回到ContentView,它似乎没有更新。但是,如果我删除行中的文本(突出显示,退格键),则“ PlaceHolder”文本将包含更新后的值。

奇怪的是,退出应用程序并重新启动它会以深色字体显示TextField中所做的更改(预期的行为而无需重新启动)。

解决方法

尝试以下

    var body: some View {
        HStack {
            TextField(self.task.title ?? "",text: self.$title) {
                self.task.title = self.title
                self.save()
            }.foregroundColor(.black)
            .onReceive(task.objectWillChange) { _ in     // << here !!
               if task.title != self.title {
                   task.title = self.title
               }
            }
,

使用SwiftUI很有趣(我对此没有经验)。但是我可以在不同论坛上有关TextField的大多数问题中看到,绑定值可以由.constant创建。因此,使用此:

TextField(self.task.title ?? "",text: .constant(self.task.title!))

现在应该可以使用。

GIF中的演示

enter image description here

,

使用@Binding代替@State

请记住,TextField实际上是SwiftUI View(通过继承),这一点很重要。父子关系实际上是TaskRow -> TextField

@State用于表示视图的“状态”。尽管此值可以传递,但它并不意味着要被其他视图写入(它具有唯一的真理来源)。

在上述情况下,我实际上是将title(通过$前缀)传递给另一个视图,同时期望父级或子级修改title属性。 @Binding支持视图或属性与视图之间的双向通信。

@State Apple文档:https://developer.apple.com/documentation/swiftui/state

@绑定Apple文档:https://developer.apple.com/documentation/swiftui/binding

Jared Sinclair的包装器规则:https://jaredsinclair.com/2020/05/07/swiftui-cheat-sheet.html

更改TaskRowTaskDetail视图可解决此问题:

TaskRow.swift


import SwiftUI
import CoreData

struct TaskRow: View {

    @Environment(\.managedObjectContext) var context
    @ObservedObject var task: Task
    
    @Binding private var title: String
    
    init(task: Task) {
        self.task = task
        self._title = Binding(get: {
            return task.title ?? ""
        },set: {
            task.title = $0
        })
        print("INIT - TaskRow Initialized: title=\(task.title ?? ""),completed=\(task.completed)")
    }
    
    var body: some View {
        HStack {
            TextField("Task Name",text: self.$title) {
                self.save()
            }.foregroundColor(.black)
            Spacer()
            Text("\(self.task.position)")
            Button(action: {
                self.task.completed.toggle()
                self.save()
            },label: {
                Image(systemName: self.task.completed ? "checkmark.square" : "square")
            }).buttonStyle(BorderlessButtonStyle())
        }
    }
}

extension TaskRow {
    func save() {
        try? self.context.save()
        print("SAVE - TaskRow")
    }
}

TaskDetail.swift



import SwiftUI

struct TaskDetail: View {
    
    @Environment(\.managedObjectContext) var context
    @ObservedObject var task: Task
    
    @Binding private var title: String
    
    init(task: Task) {
        self.task = task
        self._title = Binding(get: {
            return task.title ?? ""
        },set: {
            task.title = $0
        })
        print("INIT - TaskDetail Initialized: title=\(task.title ?? ""),completed=\(task.completed)")
    }
    
    var body: some View {
        Form {
            Section {
                TextField("Task Name",text: self.$title) {
                    self.save()
                }.foregroundColor(.black)
            }
            Section {
                Button(action: {
                    self.task.completed.toggle()
                    self.save()
                },label: {
                    Image(systemName: self.task.completed ? "checkmark.square" : "square")
                }).buttonStyle(BorderlessButtonStyle())
            }
        }
    }
}

extension TaskDetail {
    func save() {
        try? self.context.save()
        print("SAVE - TaskDetail")
    }
}