[英]SwiftUI TextField max length
是否可以為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)
}
}
您只需要一個用於 TextField 字符串的ObservableObject
包裝器。 將其視為每次發生更改時都會收到通知並能夠將修改發送回 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
中對SceneDelegate
的分配更改為類似
UIHostingController(rootView: ContentView().environmentObject(UserData()))
我知道在 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
}
}
}
}
使用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()
}
}
這是 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())
}
}
為了使其靈活,您可以將綁定包裝在另一個應用您想要的規則的綁定中。 下面,這采用了與 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 }, ...)
只要 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
,這已經a
的相同值a
但這不會觸發任何回調,除非輸入值發生更改,因為onChange
驗證下面的相等性。
所以:
"a" != ""
,一次調用onChange
會將文本設置為與其已有的值相同的值。 "a" == "a"
,不再調用onChange
"aa" != "a"
, 第一次調用 onChange, 文本被調整並設置為a
, "a" != "aa"
, 第二次調用 onChange 調整值, "a" == "a"
, onChange 不執行onChange
兩次它基本上是現代 API (iOS 14+)
let limit = 10
//...
TextField("", text: $text)
.onChange(of: text) {
text = String(text.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
}
})
}
}
編寫一個自定義格式化程序並像這樣使用它:
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))
}
}
關於@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 一起使用,您將意識到輸入在用戶輸入后立即被裁剪。
在 MVVM 超級簡單中,將 TextField 或 TextEditor 文本綁定到 viewModel 中的已發布屬性。
@Published var detailText = "" {
didSet {
if detailText.count > 255 {
detailText = String(detailText.prefix(255))
}
}
}
這是一個簡單的解決方案。
TextField("Phone", text: $Phone)
.onChange(of: Phone, perform: { value in
Phone=String(Search.Phone.prefix(10))
})
有限制的蒙面電話號碼。 無需使用 iPhoneNumberField 之類的 3rd 方庫。
@ViewBuilder
var PhoneInputView: some View {
TextField("Phone Area", text: getMaskedPhoneNumber())
.keyboardType(.phonePad)
}
private func getMaskedPhoneNumber() -> Binding<String> {
let maskedPhoneNumber = Binding(
get: { self.user.phoneNumber.applyPatternOnNumbers(pattern: "(###) ### ## ##", maxCount: 10},
set: { self.user.phoneNumber = $0.applyPatternOnNumbers(pattern: "(###) ### ## ##", maxCount: 10)}
)
return maskedPhoneNumber
}
extension String {
func applyPatternOnNumbers(pattern: String, replacmentCharacter: Character = "#", maxCount: Int) -> String {
var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
if pureNumber.count > maxCount {
return pureNumber.prefix(maxCount).lowercased()
}
for index in 0 ..< pattern.count {
guard index < pureNumber.count else { return pureNumber }
let stringIndex = String.Index(utf16Offset: index, in: self)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacmentCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
return pureNumber
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.