SwiftUI TextField 最大长度
SwiftUI TextField max length
是否可以为 TextField
设置最大长度?我正在考虑使用 onEditingChanged
事件来处理它,但它仅在用户 begins/finishes 编辑时调用,而不是在用户键入时调用。我也阅读了文档,但还没有找到任何东西。有什么解决方法吗?
TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
print(self.$text)
}) {
print("Finished editing")
}
使用 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()))
编写自定义格式化程序并像这样使用它:
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 的文本长度正确。此外,无论输入任意长度的文本,它都会在提交时被截断。
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
,否则您可能会陷入递归循环。
关于@Paulw11 的回复,对于最新的 Beta,我让 UserData class 再次像那样工作:
final class UserData: ObservableObject {
let didChange = PassthroughSubject<UserData, Never>()
var textValue = "" {
didSet {
textValue = String(textValue.prefix(8))
didChange.send(self)
}
}
}
我将 willSet
更改为 didSet
因为前缀立即被用户输入覆盖。因此,将此解决方案与 didSet 一起使用,您将意识到输入在用户输入后立即被裁剪。
为了使其更加灵活,您可以将 Binding 包装在另一个应用您想要的任何规则的 Binding 中。在下面,这采用与 Alex 的解决方案相同的方法(设置值,然后如果它无效,将其设置回旧值),但它不需要更改 @State
[=20= 的类型].我想把它放到像 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 { [=11=].count <= 10 }, ...)
您可以使用 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))
}
}
}
这是 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())
}
}
据我所知,最优雅(也最简单) 的在 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([=10=].prefix(characterLimit))
if text != s {
text = s
}
}
}
}
使用 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()
}
}
我相信 Roman Shelkford 的回答使用了比 Alex Ioja-Yang 更好的方法,或者至少是一种更适合 iOS15 的方法。但是,Roman 的回答被硬编码为单个变量, 所以不能重复使用。
下面是一个扩展性更强的版本。
(我尝试将此添加为对 Roman 评论的编辑,但我的编辑被拒绝了。我目前没有 post 评论的声誉。所以我 posting这是一个单独的答案。)
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+ 可用,就可以使用 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([=10=].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
设置为 a
与 a
相同的值,但这不会触发任何回调除非更改输入值,因为 onChange
验证下面的相等性。
所以:
- 首先输入“a”:
"a" != ""
,调用 onChange
会将文本设置为与已有值相同的值。 "a" == "a"
,不再调用 onChange
- 第二次输入“aa”:
"aa" != "a"
,第一次调用 onChange,调整文本并设置为 a
,"a" != "aa"
,第二次调用调整后的值,"a" == "a"
, onChange 没有执行
- 依此类推,每隔一个输入变化都会触发
onChange
两次
将一堆答案组合成我满意的东西。
在 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 { [=13=].offset < maxLength }
.map { [=13=].element }
)
// ensure no infinite loop
if updatedField != field {
field = updatedField
}
})
}
}
这是一个简单的解决方案。
TextField("Phone", text: $Phone)
.onChange(of: Phone, perform: { value in
Phone=String(Search.Phone.prefix(10))
})
是否可以为 TextField
设置最大长度?我正在考虑使用 onEditingChanged
事件来处理它,但它仅在用户 begins/finishes 编辑时调用,而不是在用户键入时调用。我也阅读了文档,但还没有找到任何东西。有什么解决方法吗?
TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
print(self.$text)
}) {
print("Finished editing")
}
使用 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()))
编写自定义格式化程序并像这样使用它:
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 的文本长度正确。此外,无论输入任意长度的文本,它都会在提交时被截断。
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
,否则您可能会陷入递归循环。
关于@Paulw11 的回复,对于最新的 Beta,我让 UserData class 再次像那样工作:
final class UserData: ObservableObject {
let didChange = PassthroughSubject<UserData, Never>()
var textValue = "" {
didSet {
textValue = String(textValue.prefix(8))
didChange.send(self)
}
}
}
我将 willSet
更改为 didSet
因为前缀立即被用户输入覆盖。因此,将此解决方案与 didSet 一起使用,您将意识到输入在用户输入后立即被裁剪。
为了使其更加灵活,您可以将 Binding 包装在另一个应用您想要的任何规则的 Binding 中。在下面,这采用与 Alex 的解决方案相同的方法(设置值,然后如果它无效,将其设置回旧值),但它不需要更改 @State
[=20= 的类型].我想把它放到像 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 { [=11=].count <= 10 }, ...)
您可以使用 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))
}
}
}
这是 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())
}
}
据我所知,最优雅(也最简单) 的在 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([=10=].prefix(characterLimit))
if text != s {
text = s
}
}
}
}
使用 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()
}
}
我相信 Roman Shelkford 的回答使用了比 Alex Ioja-Yang 更好的方法,或者至少是一种更适合 iOS15 的方法。但是,Roman 的回答被硬编码为单个变量, 所以不能重复使用。
下面是一个扩展性更强的版本。
(我尝试将此添加为对 Roman 评论的编辑,但我的编辑被拒绝了。我目前没有 post 评论的声誉。所以我 posting这是一个单独的答案。)
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+ 可用,就可以使用 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([=10=].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
设置为 a
与 a
相同的值,但这不会触发任何回调除非更改输入值,因为 onChange
验证下面的相等性。
所以:
- 首先输入“a”:
"a" != ""
,调用onChange
会将文本设置为与已有值相同的值。"a" == "a"
,不再调用onChange
- 第二次输入“aa”:
"aa" != "a"
,第一次调用 onChange,调整文本并设置为a
,"a" != "aa"
,第二次调用调整后的值,"a" == "a"
, onChange 没有执行 - 依此类推,每隔一个输入变化都会触发
onChange
两次
将一堆答案组合成我满意的东西。
在 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 { [=13=].offset < maxLength }
.map { [=13=].element }
)
// ensure no infinite loop
if updatedField != field {
field = updatedField
}
})
}
}
这是一个简单的解决方案。
TextField("Phone", text: $Phone)
.onChange(of: Phone, perform: { value in
Phone=String(Search.Phone.prefix(10))
})