SwiftUI TextField最大长度

时间:2019-06-06 10:46:23

标签: ios swift swiftui

是否可以为2019-06-06 16:03:48.315 1930-2875/? I/GnssLocationProvider: WakeLock acquired by sendMessage(REPORT_SV_STATUS, 0, com.android.server.location.GnssLocationProvider$SvStatusInfo@494d84f) 2019-06-06 16:03:48.316 1930-1944/? I/GnssLocationProvider: WakeLock released by handleMessage(REPORT_SV_STATUS, 0, com.android.server.location.GnssLocationProvider$SvStatusInfo@494d84f) 设置最大长度?我当时正在考虑使用TextField事件来处理它,但仅在用户开始/完成编辑时才调用它,而在用户键入时不调用它。我也阅读了文档,但还没有找到任何东西。有什么解决方法吗?

onEditingChanged

9 个答案:

答案 0 :(得分:12)

使用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;该模型根本不允许这样做。

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

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

答案 1 :(得分: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)
    }
}

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

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

答案 2 :(得分:8)

您可以使用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))
        }
    }
}

答案 3 :(得分:4)

要使其灵活,可以将绑定包装在另一个绑定中,该绑定将应用所需的任何规则。在下面,它采用与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 }, ...)

答案 4 :(得分:3)

我所知道的在 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))
      }
  }
}

答案 5 :(得分: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
            }

        }
}

现在是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。另外,无论输入任意长度的文本,都会在“提交”时将其截断。

答案 6 :(得分: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配合使用,您将意识到在用户键入输入内容后,输入内容便会被裁剪。

答案 7 :(得分:0)

我能找到的最简单的解决方案是覆盖 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 :(得分:0)

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