SwiftUI TextField binding to Double not working with custom Formatter - swiftui

I have a Binding to a Double parameter that is set by a SwiftUI TextField. I use a custom Formatter that converts to and from a Double value. The TextField sends an empty string "" to the Formatter upon editing so the conversion fails and the Double parameter is not updated. The struct is called from a parent View which has a #ObjectBinding parameter and the Double is a parameter of that object.
I am currently using Xcode 11 beta 3 and macOS Catalina Beta 3. The TextField works if the parameter is a String. The problem appears to be that a non-String type, which requires a Formatter fails to properly update the #Binding value.
Here is the Formatter:
public class DoubleFormatter: Formatter {
override public func string(for obj: Any?) -> String? {
var retVal: String?
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
if let dbl = obj as? Double {
retVal = formatter.string(from: NSNumber(value: dbl))
} else {
retVal = nil
}
return retVal
}
override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
var retVal = true
if let dbl = Double(string), let objok = obj {
objok.pointee = dbl as AnyObject?
retVal = true
} else {
retVal = false
}
return retVal
}
}
Here is the SwiftUI View that takes the Double parameter in a TextField
struct HStackTextTextField : View {
var text: String
#Binding var value: Double
#State var valueState: Double
var body: some View {
HStack {
Text("\(value)") //shows the value failing to update
TextField("Number", value: $value, formatter: DoubleFormatter()) //Still Fails
Text("\(valueState)") //shows valueState updating properly
TextField("Number", value: $valueState, formatter: DoubleFormatter()) //works as expected
}
}
}
I expect the TextField value to update when I type, but it does not. When I trace the value in the the Formatter. The string provided to getObjectValue is "" instead of the value in the TextField.
UPDATE: As of catalina/Xcode beta 5, this still appears to be an issue when the View TextField parameter is defined as #Binding and passed to the View. It appears to work as expected if the TextField parameter is defined as #State and is local to the View.

I believe this is a bug in SwiftUI. (See my similar question: SwiftUI TextField with formatter not working?)
In beta 2, it didn't work at all. In beta 3, I think you'll find that your result gets passed to your formatter if (and only if) you hit return after typing in the field. Hopefully in beta 4 they'll finish fixing the bug!

SwiftUI 3.0 still facing the same issue when using custom formatter in TextField. In my case I was using number formatter with 2 fractions. Here is my workgaround I found which looks legit and works correctly. I used custom binding.
#Binding var amount: Double?
var body: some View {
VStack {
let amountBinding = Binding(
get: { self.amount ?? 0.0 },
set: { self.amount = Double(String(format:"%.2f", $0))})
TextField("€", value: amountBinding, formatter: NumberFormatters.twoFractionDigits, onEditingChanged: { changed in
// Editing changed
})
Spacer()
}
}
struct NumberFormatters {
static var twoFractionDigits: Formatter {
let formatter = NumberFormatter()
formatter.numberStyle = .none
formatter.maximumFractionDigits = 2
return formatter
}
}

Related

Trouble Passing Data From Form Entry

This is a module where the user enters transaction data and then it is saved to coreData. EntryView calls getFormData for entry of form data then calls the function saveButton() for saving the data to coreData.
Things have been working great until I recently added two additional parameters gotCountry and gotHome. These parameters are defined in EntryView. The data I want is found in getFormData but I don't want to make it available to other parts of the app until the save button is pressed (func saveButton()) hence I need to pass the data from getFormData to saveButton().
One of the two warnings is Initialization of immutable value 'gotCountry' was never used; consider replacing with assignment to '_' or removing it if I place let in front of the parameters gotCountry and gotHome in getFormData. These are variables so shouldn't have let in front of them. Removing 'let' results in the error Type '()' cannot conform to 'View'
The parameters entryDT and entryPT are coming from form input data while gotCountry and gotHome are coming from calculated data available at time of entry.
Note that I have stripped out some of the code to see the passing of data better.
struct EntryView: View {
#EnvironmentObject var ctTotals: CountryTotals
#State private var entryDT = Date()
#State private var entryPT: Int = 0
#State private var gotCountry: String = ""
#State private var gotHome: Double = 0.0
var body: some View {
VStack (alignment: .leading){
ShowTabTitle(g: g, title: "Enter Transaction")
getFormData(entryDT: $entryDT, entryPT: $entryPT, gotCountry: $gotCountry, gotHome: $gotHome)
Button {
self.saveButton() // button pressed
} label: {
Text ("Save")
}
}
}
func saveButton() {
// save entry to core data
let newEntry = CurrTrans(context: viewContext)
// entry id
newEntry.id = UUID()
// entry date
newEntry.entryDT = entryDT
// entry payment type
newEntry.entryPT = Int64(entryPT)
ctTotals.sendTotals(gotCountry: gotCountry, gotHome: gotHome)
do {
try viewContext.save()
} catch {
}
// reset parameters for next entry
self.entryDT = Date()
self.entryPT = 0
}
}
struct getFormData: View {
#Binding var entryDT: Date
#Binding var entryPT: Int
#Binding var gotCountry: String
#Binding var gotHome: Double
var body: some View {
// get entry date and time
DatePicker("", selection: $entryDT, in: ...Date())
// select payment type
Picker(selection: $entryPT, label: Text("")) {}
// copy data to totals by country
gotCountry = currencies.curItem[userData.entryCur].cunName
gotHome = totalValue
}
}
struct CtModel: Codable, Identifiable, Hashable {
var id = UUID()
var ctName: String
var ctHome: Double
}
class CountryTotals: ObservableObject {
#Published var ctItem: [CtModel] {
didSet {
if let encoded = try? JSONEncoder().encode(ctItem) {
UserDefaults.standard.set(encoded, forKey: StorageKeys.ctTotals.rawValue)
}
}
}
init() {
}
func sendTotals(gotCountry: String, gotHome: Double) -> () {
let item = CtModel(ctName: gotCountry, ctHome: gotHome)
ctItem.append(item)
}
}

Why Is SwiftUI View Populated With Injected Object Only After Second Presentation of the View

I have a project which uses Core Data to store measurement values. The user can add new measurements to be persisted, and the user can edit persisted measurements.
The issue that I am experiencing is seen when attempting to edit a persisted measurement. After selecting a persisted measurement, the user is presented with the view to edit and save the measurement. The selected measurement is passed from the list to the presented view, where the value populates a TextField. Unfortunately, the value does not populate the TextField when the view is presented the first time within the app. Only after the second presentation does the measurement value populate the TextField.
The user can present the view to add a new measurement to be persisted, cancel and dismiss it, select an existing measurement, and that measurement's value will be displayed in the presented TextField. It seems that the initial presentation of the view used to add/edit a measurement does not contain the selected measurement on the first presentation. Only after the first presentation and dismissal will the value populate the TextField.
Below, you can see a 22sec GIF, which displays the current behavior.
In the GIF, you can see that a persisted measurement is selected, and the presented view's TextField is not populated with the measurement's value. Only on the second presentation is it populated. The last half of the GIF shows the process for persisting a new measurement and that the TextField is populated with that measurement's value on a subsequent presentation.
If you wish to reproduce the described behavior, then you can find the project's repository here, using the feature/edit-measurement branch, which the URL points to.
Steps to Reproduce
Launch application
Select any parameter from the list
Tap the trailing navigation button
Enter a value into the TextField
Tap the Save button
Return to the root view (list of parameters)
Select the parameter of which you saved the measurement value in step 5
Select the newly-persisted measurement in the list
Notice the unpopulated TextField
Tap the cancel button or dismiss the view by dragging downward
Select the same measurement that was selected in step 7
Notice the populated TextField
Below is the implementation of the view that displays persisted measurements:
import SwiftUI
struct ParameterMeasurementsLogView: View {
// MARK: Properties
let parameter: Parameter
#Environment(\.managedObjectContext) var managedObjectContext
#StateObject private var measurementStore = MeasurementStore()
#State private var displayMeasurementEntryView = false
#State private var selectedMeasurementIndex: Int?
private var measurementsRequest: FetchRequest<ParameterMeasurement>
private var measurements: FetchedResults<ParameterMeasurement> { measurementsRequest.wrappedValue }
private var measurementValues: [Double] { (measurements.map { $0.value }) }
private var measurementDeltas: [Double?] { measurementValues.deltasBetweenElements() }
private var measurementFormatter: MeasurementFormatter {
let formatter = MeasurementFormatter()
let numberFormatter = NumberFormatter()
numberFormatter.alwaysShowsDecimalSeparator = false
numberFormatter.maximumFractionDigits = 2
numberFormatter.numberStyle = .decimal
numberFormatter.usesGroupingSeparator = true
formatter.numberFormatter = numberFormatter
formatter.unitOptions = .providedUnit
formatter.unitStyle = .medium
return formatter
}
var body: some View {
List {
ForEach(measurements.indices, id: \.self) { index in
Button(action: {
selectedMeasurementIndex = index
displayMeasurementEntryView = true
}, label: {
HStack(content: {
VStack(alignment: .leading) {
Text(formattedMeasurement(at: index))
if index < measurements.count - 1 {
HStack(content: {
Image(systemName: deltaIconName(at: index))
Text(deltaString(for: index))
})
}
if let date = measurements[index].date {
FormattedDateTimeView(date: date)
}
}
})
})
}
.onDelete(perform: deleteMeasurements(at:))
}
.navigationTitle(parameter.name)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: Button(action: {
displayMeasurementEntryView = true
}, label: {
Image(systemName: Icon.plusCircleFill)
}))
.sheet(isPresented: $displayMeasurementEntryView, onDismiss: {
selectedMeasurementIndex = nil
}) {
ParameterMeasurementEntryView(parameter: parameter, entryMode: entryMode())
.environment(\.managedObjectContext, managedObjectContext)
}
}
// MARK: Initialization
init(parameter: Parameter) {
self.parameter = parameter
let entity = ParameterMeasurement.entity()
let sortDescriptors = [NSSortDescriptor(key: #keyPath(ParameterMeasurement.date), ascending: false)]
let predicateFormat = "%K =[c] %#"
let predicateArguments = [#keyPath(ParameterMeasurement.parameterName), parameter.name]
let predicate = NSPredicate(format: predicateFormat, argumentArray: predicateArguments)
measurementsRequest = FetchRequest(entity: entity, sortDescriptors: sortDescriptors, predicate: predicate, animation: .none)
}
// MARK: Deletion
private func deleteMeasurements(at offsets: IndexSet) {
offsets.forEach { managedObjectContext.delete(measurements[$0]) }
PersistenceStack.saveContext()
}
// MARK: Helpers
private func formattedMeasurement(at index: Int) -> String {
let value = measurements[index].value
switch parameter.measurementUnit {
case .unitDispersion(units: _, defaultUnit: let unit):
let measurement = Measurement<Unit>(value: value, unit: unit)
return measurementFormatter.string(from: measurement)
}
}
private func deltaIconName(at index: Int) -> String {
guard let delta = measurementDeltas[index] else { fatalError("Expected delta") }
if delta == 0 { return Icon.arrowUpArrowDown }
return delta > 0 ? Icon.arrowUp : Icon.arrowDown
}
private func deltaString(for index: Int) -> String {
guard let delta = measurementDeltas[index] else { return "" }
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 0
let absolute = abs(delta)
guard let formatted = formatter.string(from: absolute as NSNumber) else { fatalError("Expected formatted delta") }
return formatted
}
private func deltaBetweenMeasurement(at firstIndex: Int, and secondIndex: Int) -> Double {
measurementValues[firstIndex] - measurementValues[secondIndex]
}
private func entryMode() -> MeasurementEntryMode {
if let index = selectedMeasurementIndex {
return .edit(measurement: measurements[index])
}
return .add
}
}
Below, you can see the implementation of the view used to add/edit a measurement and persist it.
import SwiftUI
struct ParameterMeasurementEntryView: View {
// MARK: Properties
let parameter: Parameter
let entryMode: MeasurementEntryMode
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State private var measurementValueString = ""
private var measurementValue: Double? { Double(measurementValueString) }
private var cancelButton: some View {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
}
private var saveButton: some View {
Button(action: {
saveNewMeasurement()
dismiss()
}, label: {
Text("Save")
})
.disabled(disableSaveButton())
}
var body: some View {
NavigationView(content: {
Form(content: {
Section(header: Text("Measurement")) {
HStack {
TextField("Value", text: $measurementValueString)
Text(defaultUnitSymbol())
}
}
})
.navigationTitle(parameter.name)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: cancelButton, trailing: saveButton)
.onAppear(perform: setMeasurmentTextIfEditingMeasurement)
})
}
// MARK: Initialization
init(parameter: Parameter, entryMode: MeasurementEntryMode) {
self.parameter = parameter
self.entryMode = entryMode
}
// MARK: Helpers
private func dismiss() {
presentationMode.wrappedValue.dismiss()
}
private func disableSaveButton() -> Bool {
let measurementIsInvalid = measurementValue == nil
if case let .edit(measurement) = entryMode {
let enteredValueEqualsCurrentValue = Double(measurementValueString) == measurement.value
return measurementIsInvalid || enteredValueEqualsCurrentValue
}
return measurementIsInvalid
}
private func saveNewMeasurement() {
guard let value = measurementValue else { return assertionFailure("Expected measurement value") }
let measurement = ParameterMeasurement(entity: ParameterMeasurement.entity(), insertInto: managedObjectContext)
measurement.value = value
measurement.date = Date()
measurement.parameterName = parameter.name
PersistenceStack.saveContext()
}
private func defaultUnitSymbol() -> String {
switch parameter.measurementUnit {
case .unitDispersion(_, defaultUnit: let defaultUnit): return defaultUnit.symbol
}
}
private func setMeasurmentTextIfEditingMeasurement() {
if case let .edit(measurment) = entryMode { measurementValueString = String(measurment.value) }
}
}
MeasurementEntryMode is a simple enum that allows the list to tell the add/entry view if it's adding a new measurement or editing an existing one.
import Foundation
enum MeasurementEntryMode {
// MARK: Cases
case add, edit(measurement: ParameterMeasurement)
}
What is causing the persisted measurement's value to not be displayed in the TextField on the first presentation of the add/edit view but be displayed on the second presentation?
Even the following trivial example yields the same results:
struct PrimaryView: View {
#State private var selectedIndex: Int?
#State private var showDetail = false
var body: some View {
NavigationView {
List {
ForEach(Array(0...50).indices, id: \.self) { index in
Button(action: {
selectedIndex = index
showDetail = true
}, label: {
Text("\(index)")
})
}
}
.sheet(isPresented: $showDetail, onDismiss: {
selectedIndex = nil
}) {
Text(String(describing: selectedIndex))
}
}
}
}
According to Justin Stanley's response to my question on Twitter, simply moving the code to set whether the detail view is shown from the Button's action to an View.onChange(of:perform:) modifier fixes the issue.

Binding<String?> on the SwiftUI View TextField

I have the following view model:
struct RegistrationViewModel {
var firstname: String?
}
I want to bind the firstname property in the TextField as shown below:
TextField("First name", text: $registrationVM.firstname)
.textFieldStyle(RoundedBorderTextFieldStyle())
I keep getting an error that Binding is not allowed.
To bind objects your variable needs to conform to one of the new wrappers #State, #Binding, #ObservableObject, etc.
Because your RegistrationViewModel doesn't conform to View the only way to do it is to have your RegistrationViewModel conform to ObservableObject.
class RegistrationViewModel: ObservableObject {
#Published var firstname: String?
}
Once that is done you can call it View using
#ObservedObject var resgistrationVM: RegistrationViewModel = RegistrationViewModel()
or as an #EnvironmentObject
https://developer.apple.com/tutorials/swiftui/handling-user-input
Also, SwiftUI does not work well with optionals but an extension can handle that very easily.
SwiftUI Optional TextField
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}

Swiftui TextField with Formatter error with numeric value

I have this simply code, that work fine if I digit only numbers or one point:
import SwiftUI
struct ContentView: View {
#State private var doublevalue: Double?
private var doubleformatter: NumberFormatter {
let f = NumberFormatter()
f.numberStyle = .decimal
f.minimumFractionDigits = 5
f.maximumFractionDigits = 5
return f
}
var body: some View {
TextField("0.00", value: $doublevalue, formatter: doubleformatter)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
if I digit letter I have this error:
How to remove this error?

Updating EnvironmentObject Variable by Assignment in Button

I have a throwaway project I am using to try to familiarize myself with SwiftUI. Essentially, I have various types of apples, that I have made available through an EnvironmentObject variable. The project parallels the Landmarks tutorial that I have been through, but I am expanding on the use of objects such as steppers and buttons, etc.
I am currently attempting to have a button, when pressed, save the UUID of a certain variety of apple and send it back to the original view. It is not working, and I am not sure why. It seems like a problem of the environmentObject assignment not escaping the closure for the action:. Have have set print statements and Text views to display the values of the variables at certain points. While it seems to set the variable in the closure, it doesn't escape the closure and the variable is never really updated.
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserData()))
self.window = window
window.makeKeyAndVisible()
}
}
struct AppleData: Codable, Hashable, Identifiable {
let id: UUID
var appleType: String
var numberOfBaskets: Int
var numberOfApplesPerBasket: [Int]
var fresh: Bool
static let `default` = Self(id: UUID(uuidString: "71190FD1-C8E0-4A65-996E-9CE84D200FBA")!,
appleType: "appleType",
numberOfBaskets: 1,
numberOfApplesPerBasket: [0],
fresh: true) // for purposes of automatic preview
func image(forSize size: Int) -> Image {
ImageStore.shared.image(name: appleType, size: size)
}
}
let appleData: [AppleData] = load("apples.json")
var appleUUID: UUID?
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
... // Code Omitted For Brevity
}
final class UserData: ObservableObject {
let willChange = PassthroughSubject<UserData, Never>()
var apples = appleData {
didSet {
willChange.send(self)
}
}
var appleId = appleUUID {
didSet {
willChange.send(self)
}
}
}
struct ContentView : View {
#EnvironmentObject private var userData: UserData
var body: some View {
NavigationView {
List {
ForEach(appleData) { apple in
NavigationLink(
destination: AppleDetailHost(apple: apple).environmentObject(self.userData)
) {
Text(verbatim: apple.appleType)
}
}
Text("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}
... // Code Omitted For Brevity
}
}
struct AppleDetail : View {
#EnvironmentObject var userData: UserData
#State private var basketIndex: Int = 0
var apple: AppleData
var totalApples: Int {
apple.numberOfApplesPerBasket.reduce(0, +)
}
var body: some View {
VStack {
... // Code Omitted For Brevity
}
Button(action: {
print("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
self.userData.appleId = self.apple.id
print("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}) {
Text("Use Apple")
}
Text("self.apple.id: \(self.apple.id.uuidString)")
Text("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}
... // Code Omitted For Brevity
}
The output of the print statements in the Button in AppleDetail is:
self.userData.appleId: Nil
self.userData.appleId: 28EE7739-5E5A-4CA4-AFF5-7A6BFE025250
The Text view that shows self.userData.appleId in ContentView is always Nil. Any help would be greatly appreciated.
In beta 5, the ObservableObject no longer uses willChange. It uses objectWillChange instead. In addition, it also autosynthesizes the subject, so you do not have to write it yourself (although you could overwrite it if you want).
On top of that, there's a new property wrapper (#Published), that will make changes on a property to have the publisher emit. No need to manually call .send(), as it will be done automatically. So if in your code, you rewrite your UserData class like this, it will work fine:
final class UserData: ObservableObject {
#Published var apples = appleData
#Published var appleId = appleUUID
}