NumberField или как заставить TextField вводить Double, Float или другие числа с точкой - PullRequest
2 голосов
/ 07 января 2020

Согласно комментарию в этот вопрос Я сделал пользовательский SwifUI View на основе TextField. Он использует цифровую клавиатуру c, вы не можете вводить туда ничего, кроме цифр и точек, может быть только одна точка (точка), и вы можете передать значение Bindable Double @State через View для ввода. Но есть ошибка: при удалении последнего нуля в «xxx.0» - ноль все равно выходит. Когда вы удаляете точку - ноль становится частью целого числа, поэтому он переходит к «xxx0»

some behavior

Есть идеи, как это исправить? Я пытался сделать значение целым числом при удалении последнего числа перед точкой - но я не могу уловить момент, когда в строке есть только одна последняя точка.

вот полный код:

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

Ответы [ 2 ]

2 голосов
/ 04 февраля 2020

Единственный способ избежать потери десятичной точки и конечных нулей при наборе текста - отслеживать целые и дробные цифры строки через вызовы поля цифр c, что означает сохранение точности di git ценности как часть состояния суперпредставления. См. эту суть для полнофункционального (Swift 5) представления, которое использует эту технику. Чтобы увидеть, что произойдет, если вы не сохраните точность di git в суперпредставлении, сравните поведение первого и второго полей в предварительном просмотре ниже: первое будет обрабатывать ввод, как и ожидалось, второе удалит любой трейлинг .0 как только значение изменится.

Screen grab of control in use

1 голос
/ 08 января 2020

Я не уверен, действительно ли я все делаю правильно, но, похоже, я это исправлю. вот код:

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

Но при отладке я заметил, что код в didSet выполняется несколько раз. Не уверен, что моя ошибка привела к этому. Любое предложение?

...