是否可以设置最大长度TextField
?我正在考虑使用onEditingChanged
事件处理它,但它仅在用户开始/完成编辑时调用,而不是在用户键入时调用。我也阅读了文档,但还没有找到任何东西。有什么解决方法吗?
TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
print(self.$text)
}) {
print("Finished editing")
}
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)
}
}
您只需要一个ObservableObject
TextField 字符串的包装器。将其视为每次发生更改时都会收到通知并能够将修改发送回 TextField 的解释器。但是,无需创建PassthroughSubject
, 使用@Published
修饰符将获得相同的结果,并且代码更少。
一提,您需要使用didSet
, 而不是,willSet
否则您可能会陷入递归循环。
你可以用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))
}
}
}
使用 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
in更改SceneDelegate
为类似
UIHostingController(rootView: ContentView().environmentObject(UserData()))
为了使其灵活,您可以将绑定包装在另一个应用您想要的规则的绑定中。下面,这采用了与 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 }, ...)
我知道在 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
}
}
}
}
这是 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())
}
}
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()
}
}
只要 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" != ""
,调用onChange
它会将文本设置为与已有的值相同的值。"a" == "a"
, 不再调用onChange
"aa" != "a"
, 第一次调用 onChange, 文本被调整并设置为a
, "a" != "aa"
, 第二次调用 onChange 调整值, "a" == "a"
, onChange 不执行onChange
两次编写一个自定义格式化程序并像这样使用它:
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。此外,无论输入任意长度的文本,它都会在提交时被截断。
我相信 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))
}
}
将一堆答案组合成我满意的东西。
在 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
}
})
}
}
关于@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 一起使用,您将意识到输入在用户输入后立即被裁剪。