我有一个使用 SwiftData @Model 的程序。该模型具有一个独特的年份属性,用于跟踪各种 SwiftData 模型条目。我允许用户通过菜单更改年份。同时具有 TextField 和 Text 视图的视图仅在年份值更改时更新 Text 视图。重现错误的程序的最小可编译版本如下。数据模型和数据结构为:
@Model final public class Model {
var multiples: Multiples = Multiples(multiple1: 1.0)
@Attribute(.unique) var modelYear: Int
init(modelYear: Int) {
self.modelYear = modelYear
}
}
struct Multiples: Codable {
var multiple1: Double = 1.0
init(multiple1: Double){
self.multiple1 = multiple1
}
}
我有一个环境变量来跟踪年份。
extension EnvironmentValues {
@Entry() public var currentYear: Int = 2024
}
我有一个自定义的 TextField,它依赖于本地属性来防止我在 TextField 中键入时更新模型值。
public struct MyTextField: View {
@Binding var value: Double
@State var localValue: Double
@FocusState private var textFieldIsFocused: Bool
public init(value: Binding<Double>) {
self._value = value
self._localValue = State(initialValue: value.wrappedValue)
}
public var body: some View {
TextField("", value: $localValue, format: .number.precision(.fractionLength(2)))
.onHover{
hover in
if hover == false {
localValue = value
}
}
.onSubmit{
if value != localValue {
value = localValue
}
}
.textFieldStyle(.plain)
}
}
主应用程序和主窗口是:
@main
struct WindowtestApp: App {
init() {
let defaults = UserDefaults.standard
let year = defaults.object(forKey: "year") as? Int ?? 2024
do {
self.container = try ModelContainer(for: Model.self, configurations: ModelConfiguration(cloudKitDatabase: .none))
} catch{
print("Error")
exit(99)
}
container.mainContext.autosaveEnabled = true
let model = Model(modelYear: year)
if let fetchResult = try? container.mainContext.fetch(FetchDescriptor<Model>(predicate: #Predicate{$0.modelYear == year})) {
if fetchResult.isEmpty {
container.mainContext.insert(model)
}
} else {
container.mainContext.insert(model)
}
}
var availableYears: [Int] = [2024, 2025, 2026, 2027]
@AppStorage("year") var year: Int = 2024
var container: ModelContainer
var body: some Scene {
WindowGroup {
ContentView()
.modelContext(container.mainContext)
.environment(\.currentYear,year)
}
.commands{
CommandMenu("Tools"){
Menu("Years"){
ForEach( availableYears, id: \.self ) { y in
Button{
let context = container.mainContext
let fetches = try? context.fetch(FetchDescriptor<Model>())
let years = fetches != nil ? fetches!.map{$0.modelYear} : [2024]
if !years.contains(y) {
let newModel = Model(modelYear: y)
context.insert(newModel)
_ = try! context.save()
}
year = y
} label: {
Text("\(y)")
Image(systemName: y == year ? "checkmark.rectangle" : "rectangle")
}
}
}
}
}
}
}
struct ContentView: View{
@Environment(\.currentYear) var currentYear
@Environment(\.modelContext) var context
var body: some View{
@Bindable var model: Model = try! context.fetch(FetchDescriptor<Model>(predicate: #Predicate { $0.modelYear == currentYear}))[0]
TabView{
Tab(content: {
VStack{
Text(verbatim: "Tab for \(currentYear)")
MyTextField(value: $model.multiples.multiple1)
Text(verbatim: "\(Double(model.multiples.multiple1) * Double(currentYear))")
}
.frame(width:100)
.navigationTitle(Text(verbatim: "Tab for \(currentYear)"))
}){
Text("Tab")
}
}
.tabViewStyle(.sidebarAdaptable)
}
}
不起作用的行为是,当我切换年份时,MyTextField 值没有更新,但 Text 值确实更新,这表明存在正确的模型值。我希望在切换年份时两个视图都会更新。
重现该问题。
- 启动程序。在 TextField 框中选择一个年份的倍数。
- 选择“工具”菜单并选择新的年份。
- 为今年选择不同的倍数。
- 切换回上一年。
- MyTextField 值没有更新,但 Text 值更新了。Text 值是模型中存储的属性乘以年份。Text 值显示正确的结果,但 MyTextField 没有显示正确的倍数。
以下屏幕截图显示了该行为。
第一个显示为 2024 年的模型选择 2 的倍数。最底部的文本视图会更新以反映 2*2024。
下一个屏幕截图显示切换到 2025 年。
MyTextField 视图仍显示 2 的倍数。对于 2025 年,倍数已经是 3,这反映在更新的 Text 视图中,该视图显示 6025.0 (3*2025)。因此,模型似乎已正确更新,MyTextField 只是没有刷新视图以显示模型 multiple1 属性的最新版本。
这是因为您没有“使
localValue
”的“无效”MyTextField
。localValue
是@State
,因此只要MyTextField
存在,其值就会一直存在。当您切换到不同的年份时,localValue
不会改变,因为为什么要改变呢?什么都没有改变它。请注意,这是一行具有欺骗性的代码,它几乎总是不会执行您想要它执行的操作:
这看起来好像您正在重置
localValue
,但事实并非如此。initialValue
参数仅在视图首次出现时分配给状态。 在后续调用 时init
,initialValue
将被忽略,因为@State
已经初始化。您应该始终
@State
在属性初始化程序中初始化 s ,而不是在 中init
。如果编辑过程中不发生任何其他变化
model.multiples.multiple1
,您可以简单地onChange(of: value)
使用MyTextField
。如果在编辑过程中其他内容可能会发生变化
model.multiples.multiple1
,并且您不希望新值覆盖文本字段中的当前内容,MyTextField
则可以公开一种机制来告诉它应该重置其localValue
。这是一个例子:每当
invalidateLocalValueTrigger
更改时,文本字段都会重置其本地值。在中ContentView
,您可以简单地传递currentYear
给此参数。第三种方法是保持
MyTextField
不变,并使用.id(currentYear)
每当发生变化时,都会销毁旧的
MyTextField
并创建一个新的,从而导致再次初始化。MyTextField
currentYear
@State