ios - 如何创建只接受数字和单个点的SwiftUI TextField?

如何创建一个swiftui文本字段,允许用户仅输入数字和单个点?换句话说,它在用户输入时逐位检查,如果输入是数字或点,并且文本字段没有其他点,则接受该数字,否则忽略该数字条目。不能使用步进器。

superpuccio2019-09-09 02:36

SwiftUI不允许您为指定一组允许的字符TextField。实际上,这与UI本身无关,而与您如何管理背后的模型有关。在这种情况下,模型是后面的文字TextField。因此,您需要更改视图模型。

如果$@Published属性上使用符号,则可以访问属性本身Publisher后面的符号@Published。然后,您可以将自己的订阅者附加到发布者,并执行所需的任何检查。在这种情况下,我使用该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()

您可以在任何地方使用它。

caram2019-10-29 00:59:26

如何在现有应用中使用?即DecimalTextField($ myValue)?

Aspid2020-01-08 20:40:30

@caram我做了一个可行的例子。它不是完美的,可以改进,但是没有更好。在那里我这样使用它: DecimalTextField("123", numericValue: $numeric)

参考代码如下:

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()
    }
}
brotskydotcom2020-02-14 14:55:48

非常感谢您提供了这个非常有用的答案,它教会了我很多关于Combine和SwiftUI使用pub / sub的知识。我相信可以简化此方法,以消除显式订阅以及由此引起的线程问题。请参见本要点,以获取包括使用示例的工作示例。

本文翻译自stackoverflow.com,查看原文请点击:ios - How to create SwiftUI TextField that accepts only numbers and a single dot? 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值