Ios 使用发布者筛选字符串字段中的数字

Ios 使用发布者筛选字符串字段中的数字,ios,swift,combine,Ios,Swift,Combine,我正在为用于引入数量的textField构建包装器。我正试着用联合收割机建造一切。其中一个用例是,如果文本字段发送的stringValue包含一个字母,我会过滤这些字母并将新值重新分配给同一个var,因此文本字段会过滤这些值。还有一个代码将此值更改为int,以便其他组件可以读取int值。代码如下: class QuantityPickerViewModel: ObservableObject { private var subscriptions: Set<AnyCancellab

我正在为用于引入数量的textField构建包装器。我正试着用联合收割机建造一切。其中一个用例是,如果文本字段发送的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
    }
}
测试的输出为:

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

有人知道为什么没有赋值吗?谢谢

这本身并不是一个合并问题造成的,但似乎在属性上实际设置值之前,
发布的
发布者发出了。因此,基本上
“30a”
会覆盖
分配中设置的任何内容

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

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

即使我认为新的DEV的解决方案更好,因此官方的答案,我张贴我的联合解决方案,以防有人好奇。我通过分离用户输入和用户输出基本上消除了循环。在界面上,它看起来是一样的,但我可以创建一个userInput->value->user output结构:

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

我不明白为什么要在两个发布者之间配置一对循环管道?基本上,我正在试验并试图理解这个框架。但这里有一些我试图解决的用例:-当设置一个字符串值时,值变为该值的int表示形式-当设置值时,字符串值应该用该值的字符串表示形式覆盖-如果字符串中有一个不是数字过滤器的字符,则该字符串值用于文本字段绑定,该值是其他viewModels读取结果所依赖的值。所以基本上是这样的?哦,感谢链接,但不,这只是为了显示数字键盘,你仍然可以粘贴其他不是数字的东西。在这篇文章之后,我也可以解决这个具体问题,但我真正的问题是,我不理解的合并概念是什么,使得这段代码对我来说有一个意想不到的行为?我的期望是这段代码能正常工作,为什么不能呢?文本字段和数字的问题很简单,有多种解决方法,但我有兴趣了解wnat我做错了,你看到的是一个糟糕的答案。看看这些好答案至于你做错了什么,你需要听听我最初的问题。你似乎期望管道会神奇地出现,并在事实发生后循环更改绑定。我不知道这如何在用户键入文本字段时验证和过滤文本字段。@matt,我想我没有考虑任何文本字段。我知道OP提到了它,但如果没有代码,我不想浪费时间猜测。我说明了在这里不需要使用Combine来实现我认为OP想要的功能,即设置
stringValue
过滤掉非数字字符。你对他们的问题有不同的理解吗?太棒了!理解它为什么不起作用实际上是我的主要目标。我得出了同样的结论;发布者在willSet上被触发,因此您对变量所做的任何作为其副作用的操作都将被覆盖。我同意不需要联合收割机,但我认为问我们如何使用联合收割机仍然是一个公平的问题。谢谢你的回答!问题是,在SwiftUI上验证和过滤文本字段的整个问题非常奇怪,因为没有UITextFieldDelegate方法可以像在UIKit中那样让这一切变得如此简单。请注意,
isNumber
将验证所有类型的数字字符,包括像½这样的分数,↉, ⅓, ⅔, ¼, ¾, ⅕, ⅖, ⅗, ⅘, ⅙, ⅚, ⅐, ⅛, ⅜, ⅝, ⅞, ⅑. 最好是限制性更强,使用类似于
“0”…“9”~=$0
的谓词;你开始理解我在第一次评论中提出的观点。但问题仍然是为什么需要两条管道。这是联合国似的。如果第一条管道的工作只是分配给触发第二条管道的发布者,为什么这不是一条管道?
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")
    }
}