简体   繁体   English

SwiftUI TextField 最大长度

[英]SwiftUI TextField max length

Is it possible to set a maximum length for TextField ?是否可以为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.我正在考虑使用onEditingChanged事件处理它,但它仅在用户开始/完成编辑时调用,而不是在用户键入时调用。 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")
}

A slightly shorter version of Paulw11's answer would be: Paulw11 的答案稍短的版本是:

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.您只需要一个用于 TextField 字符串的ObservableObject包装器。 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.将其视为每次发生更改时都会收到通知并能够将修改发送回 TextField 的解释器。 However, there's no need to create the PassthroughSubject , using the @Published modifier will have the same result, in less code.但是,无需创建PassthroughSubject ,使用@Published修饰符将获得相同的结果,但代码更少。

One mention, you need to use didSet , and not willSet or you can end up in a recursive loop.一提,您需要使用didSet ,而不是willSet ,否则您可能会陷入递归循环。

You can do it with Combine in a simple way.您可以使用Combine以简单的方式做到这一点。

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.使用 SwiftUI,UI 元素(如文本字段)绑定到数据模型中的属性。 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;通过让模型处理这一点,UI 代码变得更简单,您无需担心会通过其他代码为textValue分配更长的值; 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为了让您的场景使用数据模型对象,请将rootViewController中对SceneDelegate的分配更改为类似

UIHostingController(rootView: ContentView().environmentObject(UserData()))

The most elegant (and simple) way I know to set a character limit on the TextField is to use the native publisher event collect() .我知道在 TextField 上设置字符限制的最优雅(也是最简单)的方法是使用本地发布者事件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
        }
      }
  }
}

Use Binding extension.使用Binding扩展。

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()
    }
}

This is a quick fix for iOS 15 (wrap it in dispatch async):这是 iOS 15 的快速修复(将其包装在异步调度中):

@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 .编辑:目前 iOS 15 中存在一个错误/更改,下面的代码不再起作用

The simplest solution I could find is by overriding didSet :我能找到的最简单的解决方案是覆盖didSet

@Published var text: String = "" {
  didSet {
    if text.count > 10 {
      text.removeLast() 
    }
  }
}

Here is a full example to test with SwiftUI Previews:下面是一个使用 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())
  }
}

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.下面,这采用了与 Alex 的解决方案相同的方法(设置值,然后如果它无效,则将其设置回旧值),但它不需要更改@State属性的类型。 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).我想把它变成一个像 Paul 一样的集合,但我找不到一种方法来告诉 Binding 更新它的所有观察者(并且 TextField 缓存值,所以你需要做一些事情来强制更新)。

Note that all of these solutions are inferior to wrapping a UITextField.请注意,所有这些解决方案都不如包装 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.在我和 Alex 的解决方案中,由于我们使用重新分配,如果您使用箭头键移动到字段的另一部分并开始输入,即使字符没有改变,光标也会移动,这真的很奇怪。 In Paul's solution, since it uses prefix() , the end of the string will be silently lost, which is arguably even worse.在 Paul 的解决方案中,由于它使用了prefix() ,字符串的结尾会默默地丢失,这可以说是更糟。 I don't know any way to achieve UITextField's behavior of just preventing you from typing.我不知道有什么方法可以实现 UITextField 的阻止你打字的行为。

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 初始化更改为:

TextField($text.allowing { $0.count <= 10 }, ...)

Whenever iOS 14+ is available, it's possible to do this with onChange(of:perform:)只要 iOS 14+ 可用,就可以使用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 ).每次text更改时, onChange回调将确保文本不超过指定长度(使用prefix )。 In the example, I don't want the text to be longer than 1.在示例中,我不希望text长度超过 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.对于这个特定示例,最大长度为 1。每当第一次输入文本时,都会调用一次onChange 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 .如果尝试输入另一个字符, onChange将被调用两次:第一次回调参数将是aa ,因此text将设置为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.第二次将使用a的参数调用它并设置text ,这已经a的相同值a但这不会触发任何回调,除非输入值发生更改,因为onChange验证下面的相等性。

So:所以:

  • first input "a": "a" != "" , one call to onChange which will set text to the same value as it already has.首先输入 "a": "a" != "" ,一次调用onChange会将文本设置为与其已有的值相同的值。 "a" == "a" , no more calls to onChange "a" == "a" ,不再调用onChange
  • second input "aa": "aa" != "a" , first call to onChange, text is adjusted and set to a , "a" != "aa" , second call to onChange with adjusted value, "a" == "a" , onChange is not executed第二次输入 "aa": "aa" != "a" , 第一次调用 onChange, 文本被调整并设置为a , "a" != "aa" , 第二次调用 onChange 调整值, "a" == "a" , onChange 不执行
  • and so on and so forth, every other input change will trigger onChange twice以此类推,每一个其他输入变化都会触发onChange两次

It's basically a one-liner with the modern APIs (iOS 14+)它基本上是现代 API (iOS 14+)

let limit = 10

//...

TextField("", text: $text)
    .onChange(of: text) {
        text = String(text.prefix(limit))
    }

Combined a bunch of answers into something I was happy with.将一堆答案组合成我满意的东西。
Tested on iOS 14+在 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
                }
            })
    }
}

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.您将看到正确的文本长度被分配给 $number。 Also, whatever arbitrary length of text is entered, it gets truncated on Commit.此外,无论输入任意长度的文本,它都会在提交时被截断。

SwiftUI TextField max length SwiftUI TextField 最大长度

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.我相信 Roman Shelkford 的答案使用了比 Alex Ioja-Yang 更好的方法,或者至少是一种更适用于 iOS 15 的方法。但是,Roman 的答案被硬编码为单个变量,因此不能重复使用.

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.) (我尝试将此作为编辑添加到 Roman 的评论中,但我的编辑被拒绝。我目前没有发表评论的声誉。所以我将其作为单独的答案发布。)

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))
    }
}

Regarding the reply of @Paulw11, for the latest Betas I made the UserData class work again like that:关于@Paulw11 的回复,对于最新的 Beta,我让 UserData 类再次像这样工作:

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.我将willSet更改为didSet因为前缀立即被用户的输入覆盖。 So using this solution with didSet, you will realize that the input is cropped right after the user typed it in.因此,将此解决方案与 didSet 一起使用,您将意识到输入在用户输入后立即被裁剪。

In MVVM super simple, bind TextField or TextEditor text to published property in your viewModel.在 MVVM 超级简单中,将 TextField 或 TextEditor 文本绑定到 viewModel 中的已发布属性。

@Published var detailText = "" {
     didSet {
        if detailText.count > 255 {
             detailText = String(detailText.prefix(255))
        }
     }
}

Here's a simple solution.这是一个简单的解决方案。

TextField("Phone", text: $Phone)
.onChange(of: Phone, perform: { value in 
   Phone=String(Search.Phone.prefix(10))
})

Masked phone number with limit.有限制的蒙面电话号码。 No need to use 3rd party library like iPhoneNumberField.无需使用 iPhoneNumberField 之类的 3rd 方库。

@ViewBuilder
var PhoneInputView: some View {
     TextField("Phone Area", text: getMaskedPhoneNumber())
            .keyboardType(.phonePad)
}

private func getMaskedPhoneNumber() -> Binding<String> {
    let maskedPhoneNumber = Binding(
        get: { self.user.phoneNumber.applyPatternOnNumbers(pattern: "(###) ### ## ##", maxCount: 10},
        set: { self.user.phoneNumber = $0.applyPatternOnNumbers(pattern: "(###) ### ## ##", maxCount: 10)}
    )
    
    return maskedPhoneNumber
}

extension String {

func applyPatternOnNumbers(pattern: String, replacmentCharacter: Character = "#", maxCount: Int) -> String {
    var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
    
    if pureNumber.count > maxCount {
        return pureNumber.prefix(maxCount).lowercased()
    }
    
    for index in 0 ..< pattern.count {
        guard index < pureNumber.count else { return pureNumber }
        let stringIndex = String.Index(utf16Offset: index, in: self)
        let patternCharacter = pattern[stringIndex]
        guard patternCharacter != replacmentCharacter else { continue }
        pureNumber.insert(patternCharacter, at: stringIndex)
    }
    
    return pureNumber
}
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM