NumberField 或如何使 TextField 输入 Double、Float 或其他带点的数字
NumberField or how to make TextField input a Double, Float or other numbers with dot
根据 中的评论,我基于 TextField
制作了自定义 SwifUI View
。它使用数字键盘,只能输入数字和点,只能输入一个点(dot),可以通过Bindable
Double
@State
值View
用于输入。
但是有一个错误:当您删除 "xxx.0" 中的最后一个零时 - 零仍然会出现。当你删除一个点时 - 零成为整数的一部分,所以它会变成 "xxx0"
知道如何解决吗?我试图在删除点之前的最后一个数字时将值设为整数 - 但我无法捕捉到字符串中只有最后一个点的时刻。
完整代码如下:
import SwiftUI
import Combine
struct DecimalTextField: View {
public let placeHolder: String
@Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
@Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
switch substring.count{
case 0:
if self.numericValue != 0{
self.numericValue = 0
}
case 1 :
var newValue: Double = 0
if let lastChar = substring[0].last{
if lastChar == Character("."){
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
self.numericValue = newValue
default:
self.numericValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
@Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
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([=10=])
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
@ObservedObject private var viewModel: DecimalTextFieldViewModel
init(placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: numericValue.wrappedValue == Double.zero ? "" : String(numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
@State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField(placeHolder: "123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
我不确定我是否真的做对了所有事情,但看起来我已经解决了。
这是代码:
import SwiftUI
import Combine
fileprivate func getTextOn(double: Double) -> String{
let rounded = double - Double(Int(double)) == 0
var result = ""
if double != Double.zero{
result = rounded ? String(Int(double)) : String(double)
}
return result
}
struct DecimalTextField: View {
public let placeHolder: String
@Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
@Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
if substring.count == 0{
if self.numericValue != 0{
self.numericValue = 0
}
}else if substring.count == 1{
var newValue: Double = 0
if let lastChar = substring[0].last{
let ch = String(lastChar)
if ch == "."{
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
if self.numericValue != newValue{
self.numericValue = newValue
}
}else{
let newValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
if self.numericValue != newValue{
self.numericValue = newValue
}
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
@Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
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([=10=])
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
@ObservedObject private var viewModel: DecimalTextFieldViewModel
init(_ placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: getTextOn(double: numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
@State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField("123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
但在调试中我注意到 didSet 中的代码执行了多次。不知道是什么导致了我的错误。有什么建议吗?
避免在键入时丢失小数点和尾随 0 的唯一方法是在调用数字字段时跟踪字符串的整数和小数位,这意味着将数字精度值作为超级视图的状态。有关使用此技术的全功能 (Swift 5) 视图,请参阅 this gist。要查看如果不在超级视图中保持数字精度会发生什么,请比较下面预览中第一个和第二个字段的行为:第一个将按预期处理输入,第二个将尽快删除任何尾随的 .0值发生变化。
根据 TextField
制作了自定义 SwifUI View
。它使用数字键盘,只能输入数字和点,只能输入一个点(dot),可以通过Bindable
Double
@State
值View
用于输入。
但是有一个错误:当您删除 "xxx.0" 中的最后一个零时 - 零仍然会出现。当你删除一个点时 - 零成为整数的一部分,所以它会变成 "xxx0"
知道如何解决吗?我试图在删除点之前的最后一个数字时将值设为整数 - 但我无法捕捉到字符串中只有最后一个点的时刻。
完整代码如下:
import SwiftUI
import Combine
struct DecimalTextField: View {
public let placeHolder: String
@Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
@Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
switch substring.count{
case 0:
if self.numericValue != 0{
self.numericValue = 0
}
case 1 :
var newValue: Double = 0
if let lastChar = substring[0].last{
if lastChar == Character("."){
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
self.numericValue = newValue
default:
self.numericValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
@Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
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([=10=])
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
@ObservedObject private var viewModel: DecimalTextFieldViewModel
init(placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: numericValue.wrappedValue == Double.zero ? "" : String(numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
@State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField(placeHolder: "123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
我不确定我是否真的做对了所有事情,但看起来我已经解决了。 这是代码:
import SwiftUI
import Combine
fileprivate func getTextOn(double: Double) -> String{
let rounded = double - Double(Int(double)) == 0
var result = ""
if double != Double.zero{
result = rounded ? String(Int(double)) : String(double)
}
return result
}
struct DecimalTextField: View {
public let placeHolder: String
@Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
@Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
if substring.count == 0{
if self.numericValue != 0{
self.numericValue = 0
}
}else if substring.count == 1{
var newValue: Double = 0
if let lastChar = substring[0].last{
let ch = String(lastChar)
if ch == "."{
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
if self.numericValue != newValue{
self.numericValue = newValue
}
}else{
let newValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
if self.numericValue != newValue{
self.numericValue = newValue
}
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
@Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
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([=10=])
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
@ObservedObject private var viewModel: DecimalTextFieldViewModel
init(_ placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: getTextOn(double: numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
@State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField("123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
但在调试中我注意到 didSet 中的代码执行了多次。不知道是什么导致了我的错误。有什么建议吗?
避免在键入时丢失小数点和尾随 0 的唯一方法是在调用数字字段时跟踪字符串的整数和小数位,这意味着将数字精度值作为超级视图的状态。有关使用此技术的全功能 (Swift 5) 视图,请参阅 this gist。要查看如果不在超级视图中保持数字精度会发生什么,请比较下面预览中第一个和第二个字段的行为:第一个将按预期处理输入,第二个将尽快删除任何尾随的 .0值发生变化。