使用发布者过滤字符串字段中的数字

Posted

技术标签:

【中文标题】使用发布者过滤字符串字段中的数字【英文标题】:Use publishers to filter numbers in a string field 【发布时间】:2021-03-21 21:53:58 【问题描述】:

我正在为用于引入数量的 textField 构建一个包装器。我正在尝试使用 Combine 来构建一切。其中一个用例在于,如果文本字段发送的 stringValue 有一个字母,我会过滤这些字母并将新值重新分配给同一个 var,因此文本字段会过滤这些值。还有一个代码可以将此值更改为 int,以便其他组件可以读取该 int 值。代码如下:

class QuantityPickerViewModel: ObservableObject 
    private var subscriptions: Set<AnyCancellable> = Set<AnyCancellable>()
    @Published var stringValue: String = ""
    @Published var value : Int? = nil
    
    init(initialValue: Int?) 
        $stringValue
            .removeDuplicates()
            .print("pre-filter")
            .map 
                $0.filter $0.isNumber
            
            .print("post-filter")
            .map 
                Int($0)
            
            .assign(to: \.value, on: self)
            .store(in: &subscriptions)

        $value.map 
            $0 != nil ? String($0!): ""
        
        .print("Value")
        .assign(to: \.stringValue, on:self)
        .store(in: &subscriptions)
    
        value = initialValue
    

我使用测试验证行为,我将只测试失败的测试:

class QuantityPickerViewModelTest: AppTestCase 
    var model: QuantityPickerViewModel!
    override func setUpWithError() throws 
        super.setUp()
        model = QuantityPickerViewModel(initialValue: 10)
    
    
    func test_changeStringValueWithLetters_filtersLettersAndChangesValue() 
        model.stringValue = "30a"
        
        XCTAssertEqual(model.value, 30)
        XCTAssertEqual(model.stringValue, "30") // fails saying stringValue is still "30a"
    

测试的输出是:

Test Case '-[SourdoughMasterTests.QuantityPickerViewModelTest test_changeStringValueWithLetters_filtersLettersAndChangesValue]' started.
pre-filter: receive subscription: (RemoveDuplicates)
post-filter: receive subscription: (Print)
post-filter: request unlimited
pre-filter: request unlimited
pre-filter: receive value: ()
post-filter: receive value: ()
Value: receive subscription: (PublishedSubject)
Value: request unlimited
Value: receive value: ()
Value: receive value: (10)
pre-filter: receive value: (10)
post-filter: receive value: (10)
Value: receive value: (10)
pre-filter: receive value: (30a)
post-filter: receive value: (30)
Value: receive value: (30)
pre-filter: receive value: (30)
post-filter: receive value: (30)
Value: receive value: (30)
/Users/jpellat/workspace/SourdoughMaster/SourdoughMasterTests/QuantityPickerViewModelTest.swift:54: error: -[SourdoughMasterTests.QuantityPickerViewModelTest test_changeStringValueWithLetters_filtersLettersAndChangesValue] : XCTAssertEqual failed: ("30a") is not equal to ("30")

有谁知道为什么没有分配值?谢谢

【问题讨论】:

我不明白你为什么要在两个发布者之间配置一对循环管道? 基本上我正在试验并试图理解这个框架。但这里是我试图解决的用例: - 设置字符串值时,值更改为此值的 int 表示形式 - 设置值时,字符串值应该被此值的字符串表示形式覆盖 - 如果在字符串有一个不是数字的字符过滤它的想法是字符串值用于文本字段绑定,该值是其他viewModels读取结果所依赖的值。 所以基本上像***.com/questions/58733003/… ? 哦,谢谢你的链接,但不,这只是为了显示数字键盘,你仍然可以粘贴其他不是数字的东西。我也可以在这篇帖子programmingwithswift.com/numbers-only-textfield-with-swiftui 之后解决这个具体问题,但我真正的问题是,我不明白的组合概念是什么让这段代码对我产生了意想不到的行为?我的期望是这段代码可以工作,为什么不呢?文本字段和数字的问题很简单,有多种解决方法,但我有兴趣了解我在这里做错了 您正在寻找一个错误的答案。看看的答案。 ——至于你做错了什么,你需要听我最初的问题。您似乎期望管道会在事后神奇地循环并循环更改绑定。 【参考方案1】:

导致这种情况的不是合并问题本身,但似乎Published 发布者在属性上实际设置值之前发出。所以,基本上"30a" 会覆盖assign 中设置的任何内容。

无论如何,这个循环管道链似乎有点可疑。我也不认为你真的需要在这里合并 - 它可以通过两个计算属性和一个公共存储属性来解决:

@Published 
private var _value: Int? = nil

var value: Int? 
   get  _value 
   set  _value = newValue 


var stringValue: String 
   get  _value?.description ?? "" 
   set 
      _value = Int(newValue.filter  "0"..."9" ~= $0 )
   

【讨论】:

我看不出这是如何在用户输入时验证和过滤文本字段的。 @matt,我想我没有考虑任何 TextField。我知道 OP 提到了它,但是如果没有代码,我不想浪费时间猜测。我在说明没有必要在这里使用 Combine 来实现我认为 OP 想要的,即设置 stringValue 过滤掉非数字字符。你对他们的问题有不同的理解吗? 太棒了!了解它为什么不起作用实际上是我的主要目标。我得出了同样的结论;发布者在 willSet 上触发,因此您对变量所做的任何作为其副作用的操作都会被覆盖。我同意不需要合并,但我认为问我们如何使用合并来做到这一点仍然是一个公平的问题。感谢您的回答! 问题是 validate-and-filter-a-text-field 的整个问题在 SwiftUI 上运行得非常奇怪,因为没有 UITextFieldDelegate 方法可以让这一切变得如此简单UIKit。 请注意,isNumber 将验证所有类型的数字字符,包括像 ½、↉、⅓、⅔、¼、¾、⅕、⅖、⅗、⅘、⅙、⅚、⅐、⅛ 这样的分数,⅜,⅝,⅞,⅑。最好限制更多,并使用像"0"..."9" ~= $0 这样的谓词。【参考方案2】:

即使我认为 New Dev 的解决方案更好,因此也给出了官方答案,我也会发布我的 Combine 解决方案,以防有人好奇。我基本上通过分离用户输入和用户输出来消除循环。对于界面,它看起来一样,但我可以创建一个 userInput -> value -> 用户输出结构:

class QuantityPickerViewModel: ObservableObject 
    private var subscriptions: Set<AnyCancellable> = Set<AnyCancellable>()
    var stringValue: String 
        get 
            userOutput
        
        set 
            userInput = newValue
        
    
    @Published var value : Int? = nil
    @Published private var userInput: String = ""
    private var userOutput: String = ""
    
    init(initialValue: Int?) 
        $userInput
            .map 
                $0.filter $0.isNumber
            
            .map 
                Int($0)
            
            .assign(to: \.value, on: self)
            .store(in: &subscriptions)
        
        $value
            .map 
                $0 == nil ? "": String($0!)
            
            .assign(to: \.userOutput, on: self)
            .store(in: &subscriptions)
        
        value = initialValue
    

如果您对规格感到好奇,以下是测试:

class QuantityPickerViewModelTest: XCTestCase 
    var model: QuantityPickerViewModel!
    override func setUpWithError() throws 
        super.setUp()
        model = QuantityPickerViewModel(initialValue: 10)
    

    func test_initWith10_valueAfterInit_is10() 
        XCTAssertEqual(model.value, 10)
        XCTAssertEqual(model.stringValue, "10")
    

    func test_initWithNil_valueAfterInit_isNilAndEmptyString() 
        model = QuantityPickerViewModel(initialValue: nil)
        XCTAssertNil(model.value)
        XCTAssertEqual(model.stringValue, "")
    
    
    func test_changeStringValue_changesValue() 
        model.stringValue = "20"
        
        XCTAssertEqual(model.value, 20)
        XCTAssertEqual(model.stringValue, "20")
    
    
    func test_changeValue_changesStringValue() 
        model.value = 20
        
        XCTAssertEqual(model.value, 20)
        XCTAssertEqual(model.stringValue, "20")
    
    
    func test_changeValueToNil_changesStringValueToEmpty() 
        model.value = nil
        
        XCTAssertEqual(model.value, nil)
        XCTAssertEqual(model.stringValue, "")
    
    
    func test_changeStringValueWithLetters_filtersLettersAndChangesValue() 
        model.stringValue = "30a"
        
        XCTAssertEqual(model.value, 30)
        XCTAssertEqual(model.stringValue, "30")
    

【讨论】:

这样更好;您开始理解我在第一条评论中提出的观点。但问题仍然是为什么需要两条管道。这不像组合。如果第一个管道的工作只是分配给触发第二个管道的发布者,为什么这不是一个管道?

以上是关于使用发布者过滤字符串字段中的数字的主要内容,如果未能解决你的问题,请参考以下文章

OData:通配符(startswith)过滤 url 请求中的数字(ID)字段

asp 过滤中文字符

在 C# 中使用 mysql 文档存储(Nosql)中的日期字段进行过滤

元数据库中的 SQL 自定义过滤器不显示使用 [[ ]] 的自定义字段

使用 bash 脚本从输入字符串中过滤掉 ABC 字符、数字 123 和特殊字符

SQL中怎么设置字段为10位数字。