I need the wrappedValue in TextField to be immediately uppercased as text is entered.
In this video, it works:
https://www.youtube.com/watch?v=aQE3kbCA0nk&ab_channel=SwiftandTips
But here, for some reason, it doesn't want to work. I might be missing something, but the text is formatted as uppercased only in Text(text)...
Importantly:
Everything should work inside the property wrapper and update in the view (TextField). There should be no modifiers like onChange or onReceive near the TextField, meaning no implementation in the view.
Code:
import SwiftUI
@propertyWrapper
struct StringFormat: DynamicProperty {
@State private var value: String = ""
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.uppercased()
}
var wrappedValue: String {
get { value }
nonmutating set { value = newValue.uppercased() }
}
var projectedValue: Binding<String> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0.uppercased() }
)
}
}
struct ContentView: View {
@StringFormat var text = ""
var body: some View {
VStack(spacing: 24) {
Text(text)
.font(.title3)
TextField("TextField", text: $text)
.font(.title3)
.disableAutocorrection(true)
.padding()
}
}
}
#Preview {
ContentView()
}
Now:
Must be:
It is not possible to do this without additional modifiers on the built-in TextField
, at least on iOS 17.
The wrappedValue
of the Binding
you pass to the TextField
, is not in sync with the actual text in the TextField
at all times. Though the design of the API might give strong impressions that the text:
binding will be synchronised with the actual at all times, that's simply not how TextField
is implemented.
Your StringFormat
property wrapper works as expected. Its wrappedValue
is indeed all caps, as demonstrated by the Text
. It's just that TextField
does not display that value. This is TextField
's problem, not StringFormat
's.
This is like asking why TextDisplay
in the following code doesn't display "Foo".
struct ContentView: View {
@State var text = "Foo"
var body: some View {
TextDisplay(text: $text)
}
}
Does this show that @State
is broken? Of course not! You just haven't seen how TextDisplay
is implemented. You had assumed that it will display the text in the binding because of its name and parameters. It's actually implemented like this:
struct TextDisplay: View {
@Binding var text: String
var body: some View {
Text("I'm not using the Binding at all!")
}
}
The quote from the documentation of DynamicProperty.update
is a weak argument. The documentation simply says that it will call body
when the property changes.
SwiftUI calls this function before rendering a view’s
body
to ensure the view has the most recent value.
Assuming TextField
does store the binding you passed to it somewhere in its stored properties, the documentation just says that TextField.body
will get called. What does TextField.body
do exactly? God knows. SwiftUI is not open source.
A stronger argument for this is a bug is the documentation of TextField.init
, where the text
parameter is described as "the text to display and edit". One could argue that the text
parameter is not "displayed" in this case.
That said, from a user's perspective, I personally think that this design is better. I think the TextField
should update what it's displaying after the user ends editing. Having the text field change what I entered is very annoying in my opinion.