SwiftUI TextField最大长度

Mo *_*ani 9 ios swift swiftui

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

TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
  print(self.$text)
}) {
  print("Finished editing")
}
Run Code Online (Sandbox Code Playgroud)

Raj*_*han 47

使用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
    }
}
Run Code Online (Sandbox Code Playgroud)

例子

struct DemoView: View {
    @State private var textField = ""
    var body: some View {
        TextField("8 Char Limit", text: self.$textField.max(8)) // Here
            .padding()
    }
}
Run Code Online (Sandbox Code Playgroud)


gab*_*zsa 35

它基本上是现代 API 的单行代码(iOS 14+)

let limit = 10

//...

TextField("", text: $text)
    .onChange(of: text) { _ in
        text = String(text.prefix(limit))
    }
Run Code Online (Sandbox Code Playgroud)

  • 这应该是简单用例的第一种方法。 (3认同)
  • 简短而甜蜜。很适合我的需要。 (2认同)

小智 30

你可以用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))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这太棒了,我喜欢它。谢谢你! (2认同)
  • 这应该被接受的答案。实时工作,与仅在回车时工作的绑定管理器解决方案不同 (2认同)

Nev*_*oon 21

只要 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)"))
  }
}
Run Code Online (Sandbox Code Playgroud)

怎么运行的。每次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两次


Pau*_*w11 16

使用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)
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

通过让模型负责这一点,UI代码变得更加简单,您不必担心会textValue通过其他代码将更长的值分配给用户。该模型根本不允许这样做。

为了让您的场景中使用的数据模型对象,分配改变你rootViewControllerSceneDelegate喜欢的东西

UIHostingController(rootView: ContentView().environmentObject(UserData()))
Run Code Online (Sandbox Code Playgroud)

  • 谢谢您,一切顺利!但是我应该提到,我必须将SceneDelegate类中的window.rootViewController从UIHostingController(rootView:ContentView())更改为UIHostingController(rootView:ContentView()。environmentObject(UserData()))。否则,应用程序将崩溃。如果您还介意提及此问题,它将是一个更大的答案。 (4认同)
  • 由于某种原因,这似乎对我在最新的测试版上不起作用 (3认同)

kga*_*dis 10

这是 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()
        }
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)

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

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

@Published var text: String = "" {
  didSet {
    if text.count > 10 {
      text.removeLast() 
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

以下是使用 SwiftUI 预览进行测试的完整示例:

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())
  }
}
Run Code Online (Sandbox Code Playgroud)


Ale*_*ang 8

Paulw11的答案稍短一些:

class TextBindingManager: ObservableObject {
    @Published var text = "" {
        didSet {
            if text.count > characterLimit && oldValue.count <= characterLimit {
                text = oldValue
            }
        }
    }
    let characterLimit = 5
}

struct ContentView: View {
    @ObservedObject var textBindingManager = TextBindingManager()

    var body: some View {
        TextField("Placeholder", text: $textBindingManager.text)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

提及一句,鉴于我们在不需要的时候覆盖了文本,因此您需要使用didSet,而不是,willSet否则您可能会陷入递归循环。

  • 这在 Xcode 13 中不起作用 (13认同)
  • @AlexIoja-Yang 该解决方案在 SwiftUI 版本 2 中不起作用。您能否将其更新为版本 2? (3认同)
  • 很确定我正在按照您的方式实现此操作,但它不起作用。我仍然可以输入比限制更多的字符。 (2认同)
  • 这不行。 (2认同)

Rob*_*ier 8

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

请注意,所有这些解决方案都不如包装 UITextField。在我和亚历克斯的解决方案中,由于我们使用重新分配,如果您使用箭头键移动到字段的另一部分并开始输入,即使字符没有改变,光标也会移动,这真的很奇怪。在 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
                    }
                })
    }
}
Run Code Online (Sandbox Code Playgroud)

有了这个,您可以将 TextField 初始化更改为:

TextField($text.allowing { $0.count <= 10 }, ...)
Run Code Online (Sandbox Code Playgroud)

  • 此解决方案在 SwiftUI 版本 2 中不起作用。您能否将其更新到版本 2? (4认同)

Ref*_*fuX 6

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

用法:

class MyViewModel: View {
    @Published var text: String
    var textMaxLength = 3
}
Run Code Online (Sandbox Code Playgroud)
struct MyView {
    @ObservedObject var viewModel: MyViewModel

    var body: some View {
         TextField("Placeholder", text: $viewModel.text)
             .limitText($viewModel.text, maxLength: viewModel.textMaxLength)
    }
}
Run Code Online (Sandbox Code Playgroud)
extension View {
    func limitText(_ field: Binding<String>, maxLength: Int) -> some View {
        modifier(TextLengthModifier(field: field, maxLength: maxLength))
    }
}
Run Code Online (Sandbox Code Playgroud)
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
                }
            })
    }
}
Run Code Online (Sandbox Code Playgroud)


Ruf*_*rza 5

我所知道的在 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()) {
        text = String($0.prefix(characterLimit))
      }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 结合在这里很好用! (2认同)