【问题标题】:SwiftUI TextField max lengthSwiftUI TextField 最大长度
【发布时间】:2019-10-21 21:07:24
【问题描述】:

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

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

【问题讨论】:

  • 在textField中设置(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String)
  • @YogeshPatel @manishsharma93 谢谢,但这些是UIKit 方法。我正在寻找一种在 SwiftUI 中实现它的方法。

标签: ios swift swiftui


【解决方案1】:

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

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

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

【讨论】:

  • 谢谢。在我看来,这应该是首选答案。
  • 谢谢。只有一个问题。当按下键盘上的 RETURN 按钮时,TextField 的内容会消失
  • 我不喜欢这个解决方案的 didSet 块。这太复杂了,不能按预期工作。问题是当您直接在代码中(而不是通过 GUI)设置文本的值时。假设 text 是一个空字符串。然后,当您分配一个太长的字符串时,didSet 块会将文本的值设置为 oldValue,它是一个空字符串。您真正想要的是文本最终将具有分配字符串的第一个 [characterLimit] 字符。更好的是只使用 didSet { self.text = String(text.prefix(characterLimit))}
  • @G.Marc 我不知道你为什么说使用 didSet 块太复杂了,但随后提出了一个维护它的替代方案。问题还提出了用户编辑输入的情况。尽管确实可以粘贴某些内容然后在没有解释的情况下完全刷出,但这只需要更好地处理块中的oldValuenewValue。使用您的解决方案,如果用户将光标移动到输入的开始/中间并开始编辑会发生什么?您认为这样的用户体验更好吗?
  • @AlexIoja-Yang 这个解决方案在 SwiftUI 版本 2 中不起作用。你能更新它的版本 2 吗?
【解决方案2】:

您可以通过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))
        }
    }
}

【讨论】:

  • 这太棒了,我喜欢它。谢谢!
  • 这应该是被接受的答案。与仅在回车时有效的绑定管理器解决方案不同,实时工作
  • 您对String(username.prefix(upper)) 的使用也很棒,因为否则会将“same-y”值传播到绑定。
  • 你将如何将它与观察对象一起使用? func limitText(_ upper: Int, text Binding) ???
  • 此方法是否允许获取username 的先前值?
【解决方案3】:

使用 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 分配更长的值;模型根本不允许这样做。

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

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

【讨论】:

  • 谢谢你,完美!但是我应该提到,我必须将 SceneDelegate 类中的 window.rootViewControllerUIHostingController(rootView: ContentView()) 更改为 UIHostingController(rootView: ContentView().environmentObject(UserData())) 。否则应用程序崩溃。如果您也介意提及这一点,那将是一个更好的答案。
  • 您是否尝试将其与新的 BindableObject willChange 机制一起使用?它在 TextField 中创建了一种奇怪的行为,并且在我的情况下很容易崩溃。
  • 由于某种原因,这似乎不适用于我在最新的测试版中
【解决方案4】:

为了使其灵活,您可以将 Binding 包装在另一个 Binding 中,该 Binding 应用您想要的任何规则。下面,这采用了与 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 }, ...)

【讨论】:

  • 此解决方案在 SwiftUI 版本 2 中不起作用。请您更新版本 2 吗?
【解决方案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()) {
        let s = String($0.prefix(characterLimit))
        if text != s {
          text = s
        }
      }
  }
}

【讨论】:

  • 在这里很好地使用了Combine!
  • 这里会导致无限循环
  • 这个限制是你不能访问之前的值。
【解决方案6】:

使用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
    }
}

例子

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

【讨论】:

    【解决方案7】:

    这是 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 目前存在一个错误/更改,其中代码低于does not work anymore

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

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

    以下是使用 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())
      }
    }
    

    【讨论】:

    • 在文本字段中粘贴单词时是否有效?
    【解决方案8】:

    编写一个自定义 Formatter 并像这样使用它:

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

    现在是 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))
            }
    
        }
    

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

    【讨论】:

    • 当我尝试这个时,文本仍然进入文本字段,它只是在最后被截断。我相信目标是防止用户输入超出允许范围的文本。丢弃看起来像是输入的文本似乎是一种非常糟糕的用户体验。
    【解决方案9】:

    SwiftUI TextField max length

    我相信 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))
        }
    }
    

    【讨论】:

      【解决方案10】:

      只要 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 调用并将text 设置为aa 相同的值,但这不会触发更多回调,除非输入值更改,如onChange 验证底层是否相等。

      所以:

      • 首先输入“a”:"a" != "",再调用一次onChange,它会将文本设置为与已有值相同的值。 "a" == "a",不再拨打onChange
      • 第二次输入“aa”:"aa" != "a",第一次调用onChange,文本调整并设置为a"a" != "aa",第二次调用onChange调整值,"a" == "a",onChange没有执行李>
      • 以此类推,每次输入更改都会触发onChange两次

      【讨论】:

        【解决方案11】:

        关于@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 一起使用,您会发现输入在用户输入后立即被裁剪。

        【讨论】:

        • 这不再有效。苹果改变了什么?如果是这样,我该如何让它再次工作?
        【解决方案12】:

        将一堆答案组合成我满意的东西。
        在 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
                        }
                    })
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-09-30
          • 2013-05-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-02-16
          • 1970-01-01
          相关资源
          最近更新 更多