SwiftUI TextField max length
IosSwiftSwiftuiIos Problem Overview
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 user begins/finishes editing and not called while user is typing. I've also read the docs but haven't found anything yet. Is there any workaround?
TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
print(self.$text)
}) {
print("Finished editing")
}
Ios Solutions
Solution 1 - Ios
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.
Solution 2 - Ios
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))
}
}
}
Solution 3 - Ios
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()))
Solution 4 - Ios
The most elegant (and simple) way I know to set a character limit on the TextField is to use the native publisher event collect()
.
Usage:
struct ContentView: View {
@State private var text: String = ""
var characterLimit = 20
var body: some View {
TextField("Placeholder", text: $text)
.onReceive(text.publisher.collect()) {
let s = String($0.prefix(characterLimit))
if text != s {
text = s
}
}
}
}
Solution 5 - Ios
Use Binding
extension.
extension Binding where Value == String {
func max(_ limit: Int) -> Self {
if self.wrappedValue.count > limit {
DispatchQueue.main.async {
self.wrappedValue = String(self.wrappedValue.dropLast())
}
}
return self
}
}
Example
struct DemoView: View {
@State private var textField = ""
var body: some View {
TextField("8 Char Limit", text: self.$textField.max(8)) // Here
.padding()
}
}
Solution 6 - Ios
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 }, ...)
Solution 7 - Ios
This is a quick fix for iOS 15 (wrap it in dispatch async):
@Published var text: String = "" {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
while self.text.count > 80 {
self.text.removeLast()
}
}
}
}
EDIT: There currently is a bug / change in iOS 15 where code below does not work anymore.
The simplest solution I could find is by overriding didSet
:
@Published var text: String = "" {
didSet {
if text.count > 10 {
text.removeLast()
}
}
}
Here is a full example to test with SwiftUI Previews:
class ContentViewModel: ObservableObject {
@Published var text: String = "" {
didSet {
if text.count > 10 {
text.removeLast()
}
}
}
}
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
TextField("Placeholder Text", text: $viewModel.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: ContentViewModel())
}
}
Solution 8 - Ios
Whenever iOS 14+ is available, it's possible to do this with onChange(of:perform:)
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
TextField("Name", text: $text, prompt: Text("Name"))
.onChange(of: text, perform: {
text = String($0.prefix(1))
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice(.init(rawValue: "iPhone SE (1st generation)"))
}
}
How it works. Every time the text
changes, the onChange
callback will make sure the text is not longer than the specified length (using prefix
). In the example, I don't want the text
to be longer than 1.
For this specific example, where the max length is 1. Whenever the text is entered for the very first time, onChange
is called once. If one tries to enter another character, onChange
will be called twice: first time the callback argument will be, say, aa
, so the text
will be set to a
. The second time it will be called with an argument of a
and set text
, which is already a
to the same value of a
but this will not trigger any more callbacks unless the input value is changed, as onChange
verifies equality underneath.
So:
- first input "a":
"a" != ""
, one call toonChange
which will set text to the same value as it already has."a" == "a"
, no more calls toonChange
- second input "aa":
"aa" != "a"
, first call to onChange, text is adjusted and set toa
,"a" != "aa"
, second call to onChange with adjusted value,"a" == "a"
, onChange is not executed - and so on and so forth, every other input change will trigger
onChange
twice
Solution 9 - Ios
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.
Solution 10 - Ios
https://stackoverflow.com/questions/56476007/swiftui-textfield-max-length/64199094#64199094
I believe Roman Shelkford's answer uses a better approach than that of Alex Ioja-Yang, or at least an approach that works better with iOS 15. However, Roman's answer is hard-coded to a single variable, so can't be re-used.
Below is a version that is more extensible.
(I tried adding this as an edit to Roman's comment, but my edit was rejected. I don't currently have the reputation to post a comment. So I'm posting this as a separate answer.)
import SwiftUI
import Combine
struct ContentView: View {
@State var firstName = ""
@State var lastName = ""
var body: some View {
TextField("First name", text: $firstName)
.onReceive(Just(firstName)) { _ in limitText(&firstName, 15) }
TextField("Last name", text: $lastName)
.onReceive(Just(lastName)) { _ in limitText(&lastName, 25) }
}
}
func limitText(_ stringvar: inout String, _ limit: Int) {
if (stringvar.count > limit) {
stringvar = String(stringvar.prefix(limit))
}
}
Solution 11 - Ios
Combined a bunch of answers into something I was happy with.
Tested on iOS 14+
Usage:
class MyViewModel: View {
@Published var text: String
var textMaxLength = 3
}
struct MyView {
@ObservedObject var viewModel: MyViewModel
var body: some View {
TextField("Placeholder", text: $viewModel.text)
.limitText($viewModel.text, maxLength: viewModel.textMaxLength)
}
}
extension View {
func limitText(_ field: Binding<String>, maxLength: Int) -> some View {
modifier(TextLengthModifier(field: field, maxLength: maxLength))
}
}
struct TextLengthModifier: ViewModifier {
@Binding var field: String
let maxLength: Int
func body(content: Content) -> some View {
content
.onReceive(Just(field), perform: { _ in
let updatedField = String(
field
// do other things here like limiting to number etc...
.enumerated()
.filter { $0.offset < maxLength }
.map { $0.element }
)
// ensure no infinite loop
if updatedField != field {
field = updatedField
}
})
}
}
Solution 12 - Ios
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.