SwiftUI TextField max length

后端 未结 6 917
伪装坚强ぢ
伪装坚强ぢ 2020-11-30 06:52

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

相关标签:
6条回答
  • 2020-11-30 06:54

    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.

    0 讨论(0)
  • 2020-11-30 06:55

    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))
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-30 07:04

    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()))
    
    0 讨论(0)
  • 2020-11-30 07:04

    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 }, ...)
    
    0 讨论(0)
  • 2020-11-30 07:11

    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.

    0 讨论(0)
  • 2020-11-30 07:17

    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.

    0 讨论(0)
提交回复
热议问题