簡體   English   中英

SwiftUI TextField 最大長度

[英]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": "a" != "" ,一次調用onChange會將文本設置為與其已有的值相同的值。 "a" == "a" ,不再調用onChange
  • 第二次輸入 "aa": "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。 此外,無論輸入任意長度的文本,它都會在提交時被截斷。

SwiftUI TextField 最大長度

我相信 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM