I decided to try and use a TextField with other types as described in Chapter 6. I built a simple interface with string, int and double fields (see below) and I believe I built and wired it correctly. The string works as planned. I can edit its TextField and the change is carried through to UserDefaults and the func works correctly. If I edit the string and press the button without committing the edit (i.e. pressing enter) it still works.
Not so, the formatted TextFields. They work correctly if I commit my editing but if I fail to do it and click another field/control, they continue to display my edited values but nothing is propagated to UserDefaults and the func operates with the old values. I would expect that a “reactive” environment would either accept my changes when focus is shifted or, more naturally, revert those fields to their pre-edit state.
Curiously, I log the onEditingChanged and onCommit closures and I learned that onCommit is called before onEditingChanged(true). Seems unnatural! I’d like to know how to trap this loss of focus and revert my display to correct values.
Here’s my really basic code:
-
A “model” which contains a string, an int, a double and a func and makes them persist in UserDefaults. (Strangely, though I expect models to be UI neutral, I needed to import SwiftUI to access @AppStorage).
import Foundation
import SwiftUIstruct Model {
@AppStorage(“Model_Name”) var name: String = “Jack”
@AppStorage(“Model_Real”) var realNumber: Double = 6.5
@AppStorage(“Model_Integer”) var integer: Int = 6func run() { print("Running ...") print("Model_Name: ",self.name) print("Model_Real: ",self.realNumber) print("Model_Integer: ",self.integer) }
}
-
A “view model” to mediate between the model and the view. It’s a class to avail of @ObservableObject.
class ViewModel: ObservableObject {
@Published var model = Model()func runModel() { model.run() } var name: String { get { model.name } set { model.name = newValue } } var realNumber: Double { get { model.realNumber } set { model.realNumber = newValue } } var integer: Int { get { model.integer } set { model.integer = newValue } }
}
-
A view to allow users to update the values in the model and run the func.
import SwiftUI
struct ParameterView: View {
@ObservedObject var viewModel = ViewModel()var body: some View { VStack { HStack { Text("Name:") TextField("enter name here", text: $viewModel.name, onEditingChanged: { isEditing in isEditing ? print("Name editing starts: ", viewModel.name) : print("Name editing ends: ", viewModel.name) }, onCommit: { print("Name commit: ",viewModel.name) } ) } } .padding() HStack { Text("Name:") TextField("enter real number here ...", value: $viewModel.realNumber, formatter: realFormatter, onEditingChanged: { isEditing in isEditing ? print("Real editing starts: ", viewModel.realNumber) : print("Real editing ends: ", viewModel.realNumber) }, onCommit: { print("Real commit: ",viewModel.realNumber) } ) } .padding() HStack { Text("Integer:") TextField("enter nteger here ...", value: $viewModel.integer, formatter: integerFormatter, onEditingChanged: { isEditing in isEditing ? print("Integer editing starts: ", viewModel.integer) : print("Integer editing ends: ", viewModel.integer) }, onCommit: { print("Integer commit: ",viewModel.integer) } ) } .padding() Button("Run") { viewModel.runModel() } }
}
extension ParameterView {
var realFormatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 6
formatter.roundingMode = .halfEven
return formatter
}var integerFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .none return formatter }
}