NumberField or how to make TextField input a Double, Float or other numbers with dot

妖精的绣舞 提交于 2020-01-14 04:19:29

问题


According to comment in this question I made a custom SwifUI View based on a TextField. It use numeric keypad, you can't input there nothing but numbers and point, there can be only one point (dot), and you can pass a Bindable Double @State value through the View for input. But there is a bug: when you deleting a last zero in "xxx.0" - zero still comes out. When you deleting a dot - zero becomes a part of integer, so it goes to "xxx0"

Any idea how to fix it? I tried to make value an integer when deleting last number before dot - but I can't catch the moment when there is only one last dot in a string.

here's full code:

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($0)
                        })
                    }
                }
            }
        }

        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()
    }
}

回答1:


I'm not sure if I really do everything right, but it looks like I fix that. here's the code:

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($0)
                        })
                    }
                }
            }
        }

        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()
    }
}

But in debug I noticed the code in didSet executes several times. Not sure what is my mistake leading to that. Any suggestion?



来源:https://stackoverflow.com/questions/59621625/numberfield-or-how-to-make-textfield-input-a-double-float-or-other-numbers-with

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!