I'm building a macOS app where I have an observable Formatter
object that uses @AppStorage
to store the significant digit settings (see Formatter.swift
). The number formatter is passed to the other views as an environment object using the main app struct MyApp.swift
. In the preferences window SettingsView.swift
, the significant digits are adjusted using steppers. Finally, the number formatter is assigned to text fields in ContentView.swift
to format the input.
The problem is the text fields in the content view do not automatically update their format when the significant digits are changed in the settings view. The text labels automatically update because they read the app storage values directly. But the text fields are not observing the change to the number formatter. If I change the settings, restart the app, then the text fields will properly show the updated format. But how do I tell the text field to update when the formatter significant digits change?
Formatter.swift
import Foundation
import SwiftUI
class Formatter: ObservableObject {
@AppStorage("minSigDigits") var minSigDigits = 1
@AppStorage("maxSigDigits") var maxSigDigits = 6
var numberFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.usesSignificantDigits = true
formatter.minimumSignificantDigits = self.minSigDigits
formatter.maximumSignificantDigits = self.maxSigDigits
return formatter
}
func resetValues() {
self.minSigDigits = 1
self.maxSigDigits = 6
}
}
MyApp.swift
import SwiftUI
@main
struct MyApp: App {
@StateObject var formatter = Formatter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(formatter)
}
Settings {
SettingsView().environmentObject(formatter)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var min: Double = 0.0
@State private var max: Double = 1.0
@EnvironmentObject var formatter: Formatter
var body: some View {
VStack {
Text("Min Sig. Digits = \(formatter.minSigDigits)")
TextField("enter min value", value: $min, formatter: formatter.numberFormatter)
Text("Max Sig. Digits = \(formatter.maxSigDigits)")
TextField("enter max value", value: $max, formatter: formatter.numberFormatter)
}
.padding()
.frame(width: 400, height: 300)
}
}
SettingsView.swift
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var formatter: Formatter
var body: some View {
VStack {
Stepper("Min Significant Digits: \(formatter.minSigDigits)", value: $formatter.minSigDigits, in: 1...5)
Stepper("Max Significant Digits: \(formatter.maxSigDigits)", value: $formatter.maxSigDigits, in: 6...10)
Button("Reset Values") {
formatter.resetValues()
}
}
.padding()
.frame(width: 300, height: 200)
}
}
CodePudding user response:
Add an .id
to the TextFields, that will force a redraw on change:
Text("Min Sig. Digits = \(formatter.minSigDigits)")
TextField("enter min value", value: $min, formatter: formatter.numberFormatter)
.id(formatter.minSigDigits)
Text("Max Sig. Digits = \(formatter.maxSigDigits)")
TextField("enter max value", value: $max, formatter: formatter.numberFormatter)
.id(formatter.maxSigDigits)
EDIT:
...or set an .id
to the whole view to redraw all:
VStack {
Text("Min Sig. Digits = \(formatter.minSigDigits)")
TextField("enter min value", value: $min, formatter: formatter.numberFormatter)
Text("Max Sig. Digits = \(formatter.maxSigDigits)")
TextField("enter max value", value: $max, formatter: formatter.numberFormatter)
}
.id(formatter.maxSigDigits formatter.minSigDigits)
The basic logic is, every time the id changes, SwiftUI will redraw that part.
CodePudding user response:
Your formatter.numberFormatter
getter is creating an object every time (actually 2 objects because its called twice) its called in body, you can't do that in SwiftUI. Body needs to be fast because it is called repeatedly and creating objects that are immediately discarded slows it down.
You need to rearchitect so that instead of notifying SwiftUI when the default changes with @AppStorage
, you need to listen to the defaults yourself, update the numberFormatter with the new values, and tell SwiftUI to redraw.
One way would be to make the numberFormatter
an @Published
. Then use Combine to listen to changes to both of the user defaults you are interested in, create a new formatter using the values, then assign the end of the pipeline to the numberFormatter
published property which will notify SwiftUI. This is actually what Combine's ObservableObject
is designed for. You could optimise on this further by not having it @Published
and instead just a let and then manually send the objectWillChange()
and then update the let formatter to use the new params from defaults.