有没有可能为TextField
设置最大长度?我考虑使用onEditingChanged
事件处理它,但该事件仅在用户开始/完成编辑时调用,而在用户输入时不调用。我也阅读了文档,但尚未找到任何解决方法。是否有任何变通方法?
TextField($text, placeholder: Text("Username"), onEditingChanged: { _ in
print(self.$text)
}) {
print("Finished editing")
}
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))
}
}
}
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,否则可能会陷入递归循环。
使用 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()
}
}
TextFieldStyle
内部使用它? - Adrian使用现代API(iOS 14+),这基本上是一行代码。
let limit = 10
//...
TextField("", text: $text)
.onChange(of: text) { _ in
text = String(text.prefix(limit))
}
.onChange(of: text) { newValue in text = String(newValue.prefix(100)) }
时,这才对我有效 //例如,提供newValue in
指令 - Adrian Föder使用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.rootViewController
从UIHostingController(rootView: ContentView())
改为UIHostingController(rootView: ContentView().environmentObject(UserData()))
。否则应用程序会崩溃。如果您能提及这一点,那就更好了。 - M Reza当 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)"))
}
}
每次更改文本(使用 prefix
)时,onChange
回调函数都会确保文本不超过指定长度。在这个例子中,我不希望文本超过 1 个字符。onChange
一次。如果尝试再输入一个字符,则会调用两次:第一次回调参数将是 aa
,因此 text
将被设置为 a
。第二次将使用参数 a
调用它,并设置 text
,即使已经是 a
,但这不会触发任何其他回调,除非输入值发生了变化,因为 onChange
在下面验证相等性。"a" != ""
,调用 onChange
一次,将 text 设置为与其已有的值相同。 "a" == "a"
,没有更多的 onChange
调用。
- 第二次输入 “aa” :"aa" != "a"
,第一次调用 onChange,调整 text 并将其设置为 a
,"a" != "aa"
,使用调整后的值第二次调用 onChange,"a" == "a"
,onChange 不会执行。
- 以此类推,每次输入更改都会触发两次 onChange
。我所知道的设置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的快速修复方法(用dispatch async将其包装起来):
@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预览进行测试:
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())
}
}
DispatchQueue.main.async
将didSet
的主体包装起来可以解决在Xcode 13 / iOS 15上的问题。感谢您找到了这个解决方法。 - Nathan Dudleyextension 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 }, ...)
我将一些答案汇总成我满意的内容。
在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
}
})
}
}
// do other things here like limiting to number etc...
后面加上.filter { $0.isNumber }
- RefuX
UIKit
实现的。我正在寻找在SwiftUI中实现的方法。 - M Reza