I have a program that uses a SwiftData @Model. The model has a unique attribute of a year to track the various SwiftData model entries. I allow the user to change the year through a menu. Views that have both a TextField and Text view are only updating the Text view when the year value changes. A minimally compilable version of the program, which reproduces the error is as follows. The data model and a data struct are:
@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
}
}
I have an environment variable to track the year.
extension EnvironmentValues {
@Entry() public var currentYear: Int = 2024
}
I have a custom TextField, which relies on a local property to prevent updating of the model values when I type in the 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)
}
}
The main app and main window is:
@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)
}
}
The behavior that is not working is that when I switched the year, the MyTextField value is not updating, but the Text value does update, which shows that the correct model values are present. I would expect that both views would update when the year switches.
To reproduce the problem.
- Start the program. Select a multiple for the year in the TextField box.
- Select the "Tool" menu and select a new year.
- Select a different multiple for this year.
- Switch back to the previous year.
- The MyTextField value does not update, but the Text value does. The Text value is the stored property in the model multiplied by the year. The Text value shows the correct result, but the MyTextField is not showing the correct multiple.
The following screenshots show the behavior.
The first shows selecting a multiple of 2 for the Model with year 2024. The bottom most text view updates to reflect 2*2024.
The next screenshot shows switching to year 2025.
The MyTextField view still shows a multiple of 2. For the 2025 year, the multiple is already 3, which is reflected in the updated Text view, which shows 6025.0 (3*2025). So the model appears to update correctly, the MyTextField is just not refreshing the view to show the most recent version of the Model multiple1 property.