53

是否可以设置最大长度TextField?我正在考虑使用onEditingChanged事件处理它,但它仅在用户开始/完成编辑时调用,而不是在用户键入时调用。我也阅读了文档,但还没有找到任何东西。有什么解决方法吗?

TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
  print(self.$text)
}) {
  print("Finished editing")
}
4

12 回答 12

61

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

您只需要一个ObservableObjectTextField 字符串的包装器。将其视为每次发生更改时都会收到通知并能够将修改发送回 TextField 的解释器。但是,无需创建PassthroughSubject, 使用@Published修饰符将获得相同的结果,并且代码更少。

一提,您需要使用didSet, 而不是,willSet否则您可能会陷入递归循环。

于 2019-10-13T14:49:20.830 回答
45

你可以用Combine一种简单的方式做到这一点。

像这样:

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))
        }
    }
}
于 2020-10-04T19:40:34.650 回答
23

使用 SwiftUI,UI 元素(如文本字段)绑定到数据模型中的属性。数据模型的工作是实现业务逻辑,例如限制字符串属性的大小。

例如:

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

通过让模型处理这一点,UI 代码变得更简单,您无需担心会textValue通过其他代码分配更长的值;该模型根本不允许这样做。

为了让您的场景使用数据模型对象,请将分配给您的rootViewControllerin更改SceneDelegate为类似

UIHostingController(rootView: ContentView().environmentObject(UserData()))
于 2019-06-06T12:08:46.690 回答
8

为了使其灵活,您可以将绑定包装在另一个应用您想要的规则的绑定中。下面,这采用了与 Alex 的解决方案相同的方法(设置值,然后如果它无效,则将其设置回旧值),但它不需要更改@State属性的类型。我想把它变成一个像 Paul 一样的集合,但我找不到一种方法来告诉 Binding 更新它的所有观察者(并且 TextField 缓存值,所以你需要做一些事情来强制更新)。

请注意,所有这些解决方案都不如包装 UITextField。在我的解决方案和 Alex 的解决方案中,由于我们使用重新分配,如果您使用箭头键移动到字段的另一部分并开始输入,即使字符没有改变,光标也会移动,这真的很奇怪。在 Paul 的解决方案中,由于它使用prefix()了 ,字符串的结尾会默默地丢失,这可以说是更糟。我不知道有什么方法可以实现 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
                    }
                })
    }
}

有了这个,您只需将 TextField 初始化更改为:

TextField($text.allowing { $0.count <= 10 }, ...)
于 2020-08-12T22:03:42.893 回答
7

我知道在 TextField 上设置字符限制的最优雅(也是最简单)collect()的方法是使用本机发布者事件。

用法:

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
        }
      }
  }
}
于 2021-05-16T17:50:21.540 回答
6

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

编辑:目前 iOS 15 中存在一个错误/更改,下面的代码不再起作用

我能找到的最简单的解决方案是覆盖didSet

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

下面是一个使用 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())
  }
}
于 2021-03-21T15:34:45.597 回答
5

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()
    }
}
于 2021-06-28T17:11:54.670 回答
4

只要 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)"))
  }
}

这个怎么运作。每次text更改时,onChange回调将确保文本不超过指定长度(使用prefix)。在示例中,我不希望text超过 1。

对于这个特定示例,最大长度为 1。每当第一次输入文本时,onChange都会调用一次。如果尝试输入另一个字符,onChange将被调用两次:第一次回调参数将是,比如说,,aa所以text将设置为a。第二次将使用a和 set的参数调用它text,这已经a是相同的值,a但是除非输入值发生更改,否则不会触发任何回调,因为onChange在下面验证相等性。

所以:

  • 首先输入 "a": "a" != "",调用onChange它会将文本设置为与已有的值相同的值。"a" == "a", 不再调用onChange
  • 第二次输入 "aa": "aa" != "a", 第一次调用 onChange, 文本被调整并设置为a, "a" != "aa", 第二次调用 onChange 调整值, "a" == "a", onChange 不执行
  • 以此类推,每隔一个输入更改将触发onChange两次
于 2021-11-19T09:09:04.243 回答
2

编写一个自定义格式化程序并像这样使用它:

    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
            }

        }
}

现在对于文本字段:

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

    }

您将看到正确的文本长度被分配给 $number。此外,无论输入任意长度的文本,它都会在提交时被截断。

于 2019-09-21T03:28:08.747 回答
2

SwiftUI TextField 最大长度

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

下面是一个扩展性更强的版本。

(我尝试将此作为编辑添加到 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))
    }
}
于 2021-11-01T15:33:24.260 回答
2

将一堆答案组合成我满意的东西。
在 iOS 14+ 上测试

用法:

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
                }
            })
    }
}
于 2021-12-15T23:16:43.330 回答
1

关于@Paulw11 的回复,对于最新的 Beta,我让 UserData 类再次像这样工作:

final class UserData: ObservableObject {
    let didChange = PassthroughSubject<UserData, Never>()
    var textValue = "" {
        didSet {
            textValue = String(textValue.prefix(8))
            didChange.send(self)
        }
    }
}

我改为willSet因为didSet前缀立即被用户的输入覆盖。因此,将此解决方案与 didSet 一起使用,您将意识到输入在用户输入后立即被裁剪。

于 2019-10-15T20:07:39.573 回答