They don't look Swift because they aren't: this is an Objective-C API imported into Swift.
Unless you dealing with other APIs that require Formatter instance, it's usually easier to just start with a (String) -> String closure for your formatting and go from there.
You don't need to do anything on string(for:) because on obj is already a valid instance of String sans whitespace. If text also comes from other sources, you should ensure it yourself that it is without whitespace. That isn't the job for formatter.
The FormatterTextField seems to be different from other:
the Formatter TextField doesn't update on each key stroke, only update on return key
if you don't hit return and click away, the TextField stop updating itself even if the bind to var change.
if you hit return then they change in sync again.
The Formatter TextField is useful for when you don't want your state var change until the user hit return. Could be useful without needing to add some intermediate edit copy.
My Test Code
import SwiftUI
class SanSpaceFormatter: Formatter {
override func string(for obj: Any?) -> String? {
return obj as? String
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
print("in getObjectValue(), string = \(string)")
obj?.pointee = string.filter { !$0.isWhitespace } as AnyObject
return true
}
}
struct ContentView: View {
@State var text = "" {
didSet { print("text didSet(): \(text)") }
}
@State var number = 123
static let formatter = SanSpaceFormatter()
static let numberFormatter = NumberFormatter()
func onEditingChange(_ flag: Bool) {
if flag {
// begin editing
} else {
// end editiong
text.removeAll { $0.isWhitespace }
}
}
var body: some View {
VStack {
Text("You enter: \(text), \(number)")
.padding()
// method A: using a Formatter to strip out white space
// no update while typing, only update on hitting return
Text("This TextField use `Formatter`")
TextField("No space allowed", value: $text, formatter: Self.formatter)
Divider()
// method B using a binding to strip out white space
// update text on each key stroke, space is strip on each key stroke
let b = Binding(get: { text }, set: { text = $0.filter { !$0.isWhitespace } })
TextField("I will trip any space", text: b)
// method C: strip space onCommit
// only update on commit
TextField("onEditingChanged", text: $text, onEditingChanged: onEditingChange)
// method D: strip space onCommit
// only update on commit
TextField("onCommit", text: $text, onCommit: { text.removeAll { $0.isWhitespace } })
Text("NumberFormatter")
TextField("Number", value: $number, formatter: Self.numberFormatter)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Yes, it is a very different situation when you have a formatter:
When you use a Formatter, the type of the binding target is determined by the formatter (It is Binding<T> where T is the type supported by the formatter). In general, a partially edited string that represents an arbitrary type (say a Date) will not be a valid value of that type (e.g. "1" is not a valid date, but typing a valid date may start with it.)
On the other hand, without a formatter, the type of the binding is fixed: Binding<String>. In this case, a partially edited string is always a valid string and can be used to immediately update the binding.
Stripping a string is not the best use case for a Formatter.
In general, despite Apple's best efforts, there is still significant impedance mismatch between important parts of Apple's legacy APIs (which includes Foundation) with Swift. Dealing with NSFormatter and localization facilities of Foundation is still one of the most annoying things that creep into SwiftUI.
The numberPad keyboard doesn't have a "return" key to commit change on the TextField :(
so user can enter the value but no way to complete...making the Formatter version of TextField un-usable in this case.
TextField is extremely limited and can't add the typical solution, a keyboard accessory with a done button. You're better off wrapping UITextField for real keyboard and responder handling. There are a few examples and libraries out there to do it.
Are you saying certain UI things are not possible to implement with SwiftUI? I hope this is not true.
The other versions of SwiftUI.TextField can react to every keystroke: the value binding is updated on every keystroke.
But the version created with Formatter do not update value on every keystroke as explained by @hooman above. It waits for the return key then run the formatter, then update value.
When you need to validate user input on the TextField, you don't want to do it on every keystroke even if you can. You wait until the user ends input by hitting the return key.
I think the .numberPad keyboard should have the return key.
So this is not entirely the fault of SwiftUI.TextField, the problem is the lack of return key on the .numberPad keyboard.
You can implement all of these interactions just fine in SwiftUI, TextField just can’t do it. But people have been able to wrap UITextField, including the responder chain.