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

时间:2019-09-06 13:13:45

标签: ios textfield swiftui

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

3 个答案:

答案 0 :(得分:7)

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

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

答案 1 :(得分:0)

这是用于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()
    }
  }
}

答案 2 :(得分:0)

我认为使用异步调度是错误的方法,可能会导致其他问题。这是一个实现,它使用 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> 并处理不同的输入,但使用 Double 并将泛型排除在外将简化其他事情。根据您的需要更改。

相关问题