簡體   English   中英

如何創建只接受數字和單個點的 SwiftUI TextField?

[英]How to create SwiftUI TextField that accepts only numbers and a single dot?

如何創建一個允許用戶只輸入數字和單個點的 swiftui 文本字段? 換句話說,它會在用戶輸入時逐位檢查,如果輸入是數字或點並且文本字段沒有另一個點,則接受數字,否則將忽略數字輸入。 使用步進器不是一種選擇。

SwiftUI 不允許您為TextField指定一組允許的字符。 實際上,這與 UI 本身無關,而是與您如何管理背后的模型有關。 在這種情況下,模型是TextField后面的文本。 所以,你需要改變你的視圖模型。

如果您在@Published屬性上使用$符號,您可以訪問@Published屬性本身背后的Publisher 然后您可以將您自己的訂閱者附加到發布者並執行您想要的任何檢查。 在這種情況下,我使用sink函數將基於閉包的訂閱者附加到發布者:

/// Attaches a subscriber with closure-based behavior.
///
/// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber.
/// - parameter receiveValue: The closure to execute on receipt of a value.
/// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
public func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable

實施:

import SwiftUI
import Combine

class ViewModel: ObservableObject {
    @Published var text = ""
    private var subCancellable: AnyCancellable!
    private var validCharSet = CharacterSet(charactersIn: "1234567890.")

    init() {
        subCancellable = $text.sink { val in
            //check if the new string contains any invalid characters
            if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
                //clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
                DispatchQueue.main.async {
                    self.text = String(self.text.unicodeScalars.filter {
                        self.validCharSet.contains($0)
                    })
                }
            }
        }
    }

    deinit {
        subCancellable.cancel()
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        TextField("Type something...", text: $viewModel.text)
    }
}

重要的是要注意:

  • $text@Published屬性上的$符號)為我們提供了Published<String>.Publisher類型的對象,即發布者
  • $viewModel.text$上的符號@ObservableObject )使我們類型的對象Binding<String>

那是完全不同的兩件事。

編輯:如果您願意,您甚至可以使用此行為創建您自己的自定義TextField 假設您要創建一個DecimalTextField視圖:

import SwiftUI
import Combine

struct DecimalTextField: View {
    private class DecimalTextFieldViewModel: ObservableObject {
        @Published var text = ""
        private var subCancellable: AnyCancellable!
        private var validCharSet = CharacterSet(charactersIn: "1234567890.")

        init() {
            subCancellable = $text.sink { val in                
                //check if the new string contains any invalid characters
                if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
                    //clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
                    DispatchQueue.main.async {
                        self.text = String(self.text.unicodeScalars.filter {
                            self.validCharSet.contains($0)
                        })
                    }
                }
            }
        }

        deinit {
            subCancellable.cancel()
        }
    }

    @ObservedObject private var viewModel = DecimalTextFieldViewModel()

    var body: some View {
        TextField("Type something...", text: $viewModel.text)
    }
}

struct ContentView: View {
    var body: some View {
        DecimalTextField()
    }
}

這樣您就可以使用您的自定義文本字段,只需編寫:

DecimalTextField()

您可以隨時隨地使用它。

這是 TextField 驗證的簡單解決方案:(更新)

struct ContentView: View {
@State private var text = ""

func validate() -> Binding<String> {
    let acceptableNumbers: String = "0987654321."
    return Binding<String>(
        get: {
            return self.text
    }) {
        if CharacterSet(charactersIn: acceptableNumbers).isSuperset(of: CharacterSet(charactersIn: $0)) {
            print("Valid String")
            self.text = $0
        } else {
            print("Invalid String")
            self.text = $0
            self.text = ""
        }
    }
}

var body: some View {
    VStack {
        Spacer()
        TextField("Text", text: validate())
            .padding(24)
        Spacer()
    }
  }
}

我認為使用異步調度是錯誤的方法,可能會導致其他問題。 這是一個實現,它使用Double支持的屬性實現相同的功能,並在每次在綁定視圖中鍵入時手動迭代字符。

final class ObservableNumber: ObservableObject {

    let precision: Int

    @Published
    var value: String {
        didSet {
            var decimalHit = false
            var remainingPrecision = precision
            let filtered = value.reduce(into: "") { result, character in

                // If the character is a number that by adding wouldn't exceed the precision and precision is set then add the character.
                if character.isNumber, remainingPrecision > 0 || precision <= 0 {
                    result.append(character)

                    // If a decimal has been hit then decrement the remaining precision to fulfill
                    if decimalHit {
                        remainingPrecision -= 1
                    }

                // If the character is a decimal, one hasn't been added already, and precision greater than zero then add the decimal.
                } else if character == ".", !result.contains("."), precision > 0 {
                    result.append(character)
                    decimalHit = true
                }
            }

            // Only update value if after processing it is a different value.
            // It will hit an infinite loop without this check since the published event occurs as a `willSet`.
            if value != filtered {
                value = filtered
            }
        }
    }

    var doubleValue: AnyPublisher<Double, Never> {
        return $value
            .map { Double($0) ?? 0 }
            .eraseToAnyPublisher()
    }

    init(precision: Int, value: Double) {
        self.precision = precision
        self.value = String(format: "%.\(precision)f", value)
    }
}

此解決方案還確保您只有一個小數,而不是允許多個"."實例"." .

注意額外的計算屬性將它“放回”到Double 這允許您繼續將數字作為數字而不是String做出反應,並且必須在任何地方進行強制轉換/轉換。 你可以很容易地添加盡可能多的計算屬性,只要你以你期望的方式轉換它,你就可以像Int或任何數字類型一樣對它做出反應。

還要注意的是,您還可以將其ObservableNumber<N: Numeric>泛型ObservableNumber<N: Numeric>並處理不同的輸入,但使用Double並將泛型排除在外將簡化其他事情。 根據您的需要更改。

簡單的解決方案是設置 .numberPad 鍵盤類型:

  TextField(
     "0.0", 
     text: $fromValue
  )
  .keyboardType(UIKeyboardType.numberPad)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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