Is it possible to set a maximum length for TextField
? I was thinking of handling it using onEditingChanged
event but it is only called when the us
Write a custom Formatter and use it like this:
class LengthFormatter: Formatter {
//Required overrides
override func string(for obj: Any?) -> String? {
if obj == nil { return nil }
if let str = (obj as? String) {
return String(str.prefix(10))
}
return nil
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
obj?.pointee = String(string.prefix(10)) as AnyObject
error?.pointee = nil
return true
}
}
}
Now for TextField:
struct PhoneTextField: View {
@Binding var number: String
let myFormatter = LengthFormatter()
var body: some View {
TextField("Enter Number", value: $number, formatter: myFormatter, onEditingChanged: { (isChanged) in
//
}) {
print("Commit: \(self.number)")
}
.foregroundColor(Color(.black))
}
}
You will see the correct length of text get assigned to $number. Also, whatever arbitrary length of text is entered, it gets truncated on Commit.
You can do it with Combine
in a simple way.
Like so:
import SwiftUI
import Combine
struct ContentView: View {
@State var username = ""
let textLimit = 10 //Your limit
var body: some View {
//Your TextField
TextField("Username", text: $username)
.onReceive(Just(username)) { _ in limitText(textLimit) }
}
//Function to keep text length in limits
func limitText(_ upper: Int) {
if username.count > upper {
username = String(username.prefix(upper))
}
}
}
With SwiftUI, UI elements, like a text field, are bound to properties in your data model. It is the job of the data model to implement business logic, such as a limit on the size of a string property.
For example:
import Combine
import SwiftUI
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData,Never>()
var textValue = "" {
willSet {
self.textValue = String(newValue.prefix(8))
didChange.send(self)
}
}
}
struct ContentView : View {
@EnvironmentObject var userData: UserData
var body: some View {
TextField($userData.textValue, placeholder: Text("Enter up to 8 characters"), onCommit: {
print($userData.textValue.value)
})
}
}
By having the model take care of this the UI code becomes simpler and you don't need to be concerned that a longer value will be assigned to textValue
through some other code; the model simply won't allow this.
In order to have your scene use the data model object, change the assignment to your rootViewController
in SceneDelegate
to something like
UIHostingController(rootView: ContentView().environmentObject(UserData()))
To make this flexible, you can wrap the Binding in another Binding that applies whatever rule you want. Underneath, this employs the same approach as Alex's solutions (set the value, and then if it's invalid, set it back to the old value), but it doesn't require changing the type of the @State
property. I'd like to get it to a single set like Paul's, but I can't find a way to tell a Binding to update all its watchers (and TextField caches the value, so you need to do something to force an update).
Note that all of these solutions are inferior to wrapping a UITextField. In my solution and Alex's, since we use reassignment, if you use the arrow keys to move to another part of the field and start typing, the cursor will move even though the characters aren't changing, which is really weird. In Paul's solution, since it uses prefix()
, the end of the string will be silently lost, which is arguably even worse. I don't know any way to achieve UITextField's behavior of just preventing you from typing.
extension Binding {
func allowing(predicate: @escaping (Value) -> Bool) -> Self {
Binding(get: { self.wrappedValue },
set: { newValue in
let oldValue = self.wrappedValue
// Need to force a change to trigger the binding to refresh
self.wrappedValue = newValue
if !predicate(newValue) && predicate(oldValue) {
// And set it back if it wasn't legal and the previous was
self.wrappedValue = oldValue
}
})
}
}
With this, you can just change your TextField initialization to:
TextField($text.allowing { $0.count <= 10 }, ...)
Regarding the reply of @Paulw11, for the latest Betas I made the UserData class work again like that:
final class UserData: ObservableObject {
let didChange = PassthroughSubject<UserData, Never>()
var textValue = "" {
didSet {
textValue = String(textValue.prefix(8))
didChange.send(self)
}
}
}
I changed willSet
to didSet
because the prefix was immediately overwritten by the user`s input. So using this solution with didSet, you will realize that the input is cropped right after the user typed it in.
Latest edit:
It has been pointed out that since SwiftUI 2, this no longer works, so depending on the version you're using, this may no longer be the correct answer
Original answer:
A slightly shorter version of Paulw11's answer would be:
class TextBindingManager: ObservableObject {
@Published var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
let characterLimit: Int
init(limit: Int = 5){
characterLimit = limit
}
}
struct ContentView: View {
@ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.text)
}
}
All you need is an ObservableObject
wrapper for the TextField string. Think of it as an interpreter that gets notified every time there's a change and is able to send modifications back to the TextField. However, there's no need to create the PassthroughSubject
, using the @Published
modifier will have the same result, in less code.
One mention, you need to use didSet
, and not willSet
or you can end up in a recursive loop.