Reputation: 786
I'm trying to get a numeric field updated so I'm using a TextField with the formatter: parameter set. It formats the number into the entry field just fine, but does not update the bound value when edited. The TextField works fine (on Strings) without the formatter specified. Is this a bug or am I missing something?
UPDATE: As of Xcode 11 beta 3 it kind of works. Now if you edit the numeric TextField, the bound value is updated after you hit return. The String TextField is still updated after each keypress. I guess they don't want to send the value to be formatted to the formatter with every key press, or maybe there is/will be a modifier for TextField to tell it to do that.
Note that The API has changed slightly; the old TextField init()s are deprecated and a new titleKey String field has been added as the first parameter which appears as placeholder text in the field.
struct TestView : View {
@State var someText = "Change me!"
@State var someNumber = 123.0
var body: some View {
Form {
// Xcode 11 beta 2
// TextField($someText)
// TextField($someNumber, formatter: NumberFormatter())
// Xcode 11 beta 3
TextField("Text", text: $someText)
TextField("Number", value: $someNumber, formatter: NumberFormatter())
Spacer()
// if you change the first TextField value, the change shows up here
// if you change the second (the number),
// it does not *until you hit return*
Text("text: \(self.someText), number: \(self.someNumber)")
// the button does the same, but logs to the console
Button(action: { print("text: \(self.someText), number: \(self.someNumber)")}) {
Text("Log Values")
}
}
}
}
If you type in the first (String) TextField, the value in the Text view is updated immediately. If you edit the second (Numeric), nothing happens. Similarly tapping the Button shows an updated value for the String, but not the number. I've only tried this in the simulator.
Upvotes: 58
Views: 34766
Reputation: 425
Swift 5.5 and iOS 15 have new formatting APIs.
I was looking for a clean currency formatter and came across this documentation.
See Documentation here: ParseableFormatStyle
This still does not update the TextField bound value as you type. However, you no longer are required to press return to trigger the formatting. You can simply exit the TextField. It also behaves as expected when you click back into the TextField to edit your original value.
Here is a working example:
import SwiftUI
struct FormatTest: View {
@State var myNumber: Double?
@State var myDate: Date.FormatStyle.FormatInput?
var body: some View {
Form {
TextField("", value: $myNumber, format: .currency(code: "USD"), prompt: Text("Enter a number:"))
TextField("", value: $myDate, format: .dateTime.month(.twoDigits).day(.twoDigits).year(), prompt: Text("MM/DD/YY"))
Text(myDate?.formatted(.dateTime.weekday(.wide)) ?? "")
}
}
}
struct FormatTest_Previews: PreviewProvider {
static var previews: some View {
FormatTest()
}
}
Upvotes: 5
Reputation: 1
Currently iOS 14 TextField with value initialiser is not updating the state.
I found a workaround for this bug and can be used NSNumber, Double ... and a NumberFormatter. This a new brand TextField that accept NSNumber and NumberFormatter
extension TextField {
public init(_ prompt: LocalizedStringKey, value: Binding<NSNumber>, formatter: NumberFormatter) where Text == Label {
self.init(
prompt,
text: .init(get: {
formatter.string(for: value.wrappedValue) ?? String()
}, set: {
let string = $0
.replacingOccurrences(of: formatter.groupingSeparator, with: "")
value.wrappedValue = formatter.number(from: string) ?? .init(value: Float.zero)
})
)
}
}
Or you can implement you own Logic inside the binding get and set methods
TextField("placeholder", text: .init(
get: {
decimalFormatter.string(from: number) ?? ""
},
set: {
let string = $0
.replacingOccurrences(of: decimalFormatter.groupingSeparator, with: "")
_number.wrappedValue = decimalFormatter.number(from: string)
?? .init(value: Double.zero)
}
))
Upvotes: 0
Reputation: 310
import Foundation
import SwiftUI
struct FormattedTextField<T: Equatable>: View {
let placeholder: LocalizedStringKey
@Binding var value: T
let formatter: Formatter
var valueChanged: ((T) -> Void)? = nil
var editingChanged: ((Bool) -> Void)? = nil
var onCommit: (() -> Void)? = nil
@State private var isUpdated = false
var proxy: Binding<String> {
Binding<String>(
get: {
formatter.string(for: value) ?? ""
},
set: {
var obj: AnyObject? = nil
formatter.getObjectValue(&obj, for: $0, errorDescription: nil)
if let newValue = obj as? T {
let notifyUpdate = newValue == value
value = newValue
valueChanged?(value)
if notifyUpdate {
isUpdated.toggle()
}
}
}
)
}
var body: some View {
TextField(
placeholder,
text: proxy,
onEditingChanged: { isEditing in
editingChanged?(isEditing)
},
onCommit: {
onCommit?()
}
)
.tag(isUpdated ? 0 : 1)
}
}
Upvotes: 0
Reputation: 595
In the interest of keeping it clean and lightweight, I wound up casting types with a getter/setter in the view model and keeping the text type TextField.
Quick and dirty(ish), but it works and doesn't feel like I'm fighting SwiftUI.
View Body
struct UserDetails: View {
@ObservedObject var userViewModel: UserViewModel
init(user: PedalUserViewModel) {
userViewModel = user
}
var body: some View {
VStack {
Form {
Section(header: Text("Personal Information")) {
TextField("Age", text: $userViewModel.userAge)
.keyboardType(.numberPad)
.modifier(DoneButton())
}
}
}
}
}
ViewModel
class UserViewModel: ObservableObject {
@ObservedObject var currentUser: User
var anyCancellable: AnyCancellable?
init(currentUser: User) {
self.currentUser = currentUser
self.anyCancellable = self.currentUser.objectWillChange.sink{ [weak self] (_) in
self?.objectWillChange.send()
}
}
var userAge: String {
get {
String(currentUser.userAge)
}
set {
currentUser.userAge = Int(newValue) ?? 0
}
}
}
Upvotes: 2
Reputation: 1019
Inspired by above accepted proxy answer, here is a ready to use struct with fair amount of code. I really hope Apple can add an option to toggle the behavior.
struct TextFieldRow<T>: View {
var value: Binding<T>
var title: String
var subtitle: String?
var valueProxy: Binding<String> {
switch T.self {
case is String.Type:
return Binding<String>(
get: { self.value.wrappedValue as! String },
set: { self.value.wrappedValue = $0 as! T } )
case is String?.Type:
return Binding<String>(
get: { (self.value.wrappedValue as? String).bound },
set: { self.value.wrappedValue = $0 as! T })
case is Double.Type:
return Binding<String>( get: { String(self.value.wrappedValue as! Double) },
set: {
let doubleFormatter = NumberFormatter()
doubleFormatter.numberStyle = .decimal
doubleFormatter.maximumFractionDigits = 3
if let doubleValue = doubleFormatter.number(from: $0)?.doubleValue {
self.value.wrappedValue = doubleValue as! T
}
}
)
default:
fatalError("not supported")
}
}
var body: some View {
return HStack {
VStack(alignment: .leading) {
Text(title)
if let subtitle = subtitle, subtitle.isEmpty == false {
Text(subtitle)
.font(.caption)
.foregroundColor(Color(UIColor.secondaryLabel))
}
}
Spacer()
TextField(title, text: valueProxy)
.multilineTextAlignment(.trailing)
}
}
}
Upvotes: 0
Reputation: 16399
Plan B. Since using value:
and NumberFormatter
doesn’t work, we can use a customised TextField
. I have wrapped the TextField
inside a struct
, so that you can use it as transparently as possible.
I am very new to both Swift and SwiftUI, so there is no doubt a more elegant solution.
struct IntField: View {
@Binding var int: Int
@State private var intString: String = ""
var body: some View {
return TextField("", text: $intString)
.onReceive(Just(intString)) { value in
if let i = Int(value) { int = i }
else { intString = "\(int)" }
}
.onAppear(perform: {
intString = "\(int)"
})
}
}
and in the ContentView:
struct ContentView: View {
@State var testInt: Int = 0
var body: some View {
return HStack {
Text("Number:")
IntField(int: $testInt);
Text("Value: \(testInt)")
}
}
}
Basically, we work with a TextField("…", text: …)
, which behaves as desired, and use a proxy text field.
Unlike the version using value:
and NumberFormatter
, the .onReceive
method responds immeditately, and we use it to set the real integer value, which is bound. While we’re at it, we check whether the text really yields an integer.
The .onAppear
method is used to fill the string from the integer.
You can do the same with FloatField
.
This might do the job until Apple finishes the job.
Upvotes: 9
Reputation: 1062
I know this has some accepted answers, but the above answers seem to have glitchy UX results when inputing values (at least for doubles). So I decided to write my own solution. It is largely inspired by the answers here so I would first try the other examples here before trying this one as it is a lot more code.
WARNING Although I have been an iOS developer for a long time, I'm fairly new to SwiftUI. So this is far from expert advice. I would love feedback on my approach but be nice. So far this has been working out well on my new project. However, I doubt this is as efficient as Apple's formatters.
protocol NewFormatter {
associatedtype Value: Equatable
/// The logic that converts your value to a string presented by the `TextField`. You should omit any values
/// - Parameter object: The value you are converting to a string.
func toString(object: Value) -> String
/// Once the change is allowed and the input is final, this will convert
/// - Parameter string: The full text currently on the TextField.
func toObject(string: String) -> Value
/// Specify if the value contains a final result. If it does not, nothing will be changed yet.
/// - Parameter string: The full text currently on the TextField.
func isFinal(string: String) -> Bool
/// Specify **all** allowed inputs, **including** intermediate text that cannot be converted to your object **yet** but are necessary in the input process for a final result. It will allow this input without changing your value until a final correct value can be determined.
/// For example, `1.` is not a valid `Double`, but it does lead to `1.5`, which is. Therefore the `DoubleFormatter` would return true on `1.`.
/// Returning false will reset the input to the previous allowed value.
/// - Parameter string: The full text currently on the TextField.
func allowChange(to string: String) -> Bool
}
struct NewTextField<T: NewFormatter>: View {
let title: String
@Binding var value: T.Value
let formatter: T
@State private var previous: T.Value
@State private var previousGoodString: String? = nil
init(_ title: String, value: Binding<T.Value>, formatter: T) {
self.title = title
self._value = value
self._previous = State(initialValue: value.wrappedValue)
self.formatter = formatter
}
var body: some View {
let changedValue = Binding<String>(
get: {
if let previousGoodString = self.previousGoodString {
let previousValue = self.formatter.toObject(string: previousGoodString)
if previousValue == self.value {
return previousGoodString
}
}
let string = self.formatter.toString(object: self.value)
return string
},
set: { newString in
if self.formatter.isFinal(string: newString) {
let newValue = self.formatter.toObject(string: newString)
self.previousGoodString = newString
self.previous = newValue
self.value = newValue
} else if !self.formatter.allowChange(to: newString) {
self.value = self.previous
}
}
)
return TextField(title, text: changedValue)
}
}
Then you can create a custom formatter for a Double
like this one:
/// An object that converts a double to a valid TextField value.
struct DoubleFormatter: NewFormatter {
let numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.allowsFloats = true
numberFormatter.numberStyle = .decimal
numberFormatter.maximumFractionDigits = 15
return numberFormatter
}()
/// The logic that converts your value to a string used by the TextField.
func toString(object: Double) -> String {
return numberFormatter.string(from: NSNumber(value: object)) ?? ""
}
/// The logic that converts the string to your value.
func toObject(string: String) -> Double {
return numberFormatter.number(from: string)?.doubleValue ?? 0
}
/// Specify if the value contains a final result. If it does not, nothing will be changed yet.
func isFinal(string: String) -> Bool {
return numberFormatter.number(from: string) != nil
}
/// Specify **all** allowed values, **including** intermediate text that cannot be converted to your object **yet** but are necessary in the input process for a final result.
/// For example, `1.` is not a valid `Double`, but it does lead to `1.5`, which is. It will allow this input without changing your value until a final correct value can be determined.
/// Returning false will reset the input the the previous allowed value. For example, when using the `DoubleFormatter` the input `0.1j` would result in false which would reset the value back to `0.1`.
func allowChange(to string: String) -> Bool {
let components = string.components(separatedBy: ".")
if components.count <= 2 {
// We allow an Integer or an empty value.
return components.allSatisfy({ $0 == "" || Int($0) != nil })
} else {
// If the count is > 2, we have more than one decimal
return false
}
}
}
To you can use this new component like this:
NewTextField(
"Value",
value: $bodyData.doubleData.value,
formatter: DoubleFormatter()
)
Here are a few other usages that I can think of:
/// Just a simple passthrough formatter to use on a NewTextField
struct PassthroughFormatter: NewFormatter {
func toString(object: String) -> String {
return object
}
func toObject(string: String) -> String {
return string
}
func isFinal(string: String) -> Bool {
return true
}
func allowChange(to string: String) -> Bool {
return true
}
}
/// A formatter that converts empty strings to nil values
struct EmptyStringFormatter: NewFormatter {
func toString(object: String?) -> String {
return object ?? ""
}
func toObject(string: String) -> String? {
if !string.isEmpty {
return string
} else {
return nil
}
}
func isFinal(string: String) -> Bool {
return true
}
func allowChange(to string: String) -> Bool {
return true
}
}
Upvotes: 3
Reputation: 3974
You can use Binding to convert Double<-->String for TextField
struct TestView: View {
@State var someNumber = 123.0
var body: some View {
let someNumberProxy = Binding<String>(
get: { String(format: "%.02f", Double(self.someNumber)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.someNumber = value.doubleValue
}
}
)
return VStack {
TextField("Number", text: someNumberProxy)
Text("number: \(someNumber)")
}
}
}
You can use computed property way to solve this issue. (thanks @ iComputerfreak)
struct TestView: View {
@State var someNumber = 123.0
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%.02f", Double(self.someNumber)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.someNumber = value.doubleValue
}
}
)
}
var body: some View {
VStack {
TextField("Number", text: someNumberProxy)
Text("number: \(someNumber)")
}
}
}
Upvotes: 53
Reputation: 4169
It seems while using value:
as an input, SwiftUI does not reload the view for any key that users tap on. And, as you mentioned, it reloads the view when users exit the field or commit it.
On the other hand, SwiftUI reloads the view (immediately) using text:
as an input whenever a key is pressed. Nothing else comes to my mind.
in my case, I did it for someNumber2
as below:
struct ContentView: View {
@State var someNumber = 123.0
@State var someNumber2 = "123"
var formattedNumber : NSNumber {
let formatter = NumberFormatter()
guard let number = formatter.number(from: someNumber2) else {
print("not valid to be converted")
return 0
}
return number
}
var body: some View {
VStack {
TextField("Number", value: $someNumber, formatter: NumberFormatter())
TextField("Number2", text: $someNumber2)
Text("number: \(self.someNumber)")
Text("number: \(self.formattedNumber)")
}
}
}
Upvotes: 12