使用发布者过滤字符串字段中的数字
Use publishers to filter numbers in a string field
我正在为用于引入数量的 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 {
[=10=].filter {[=10=].isNumber}
}
.print("post-filter")
.map {
Int([=10=])
}
.assign(to: \.value, on: self)
.store(in: &subscriptions)
$value.map {
[=10=] != nil ? String([=10=]!): ""
}
.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")
有谁知道为什么没有赋值?谢谢
这不是 Combine 问题本身导致的,但似乎 Published
发布者在 属性 上实际设置值之前发出。所以,基本上 "30a"
会覆盖 assign
.
中设置的任何内容
无论如何,这个循环的管道链似乎有点可疑。我也不认为你真的需要 Combine 这里 - 它可以用两个计算属性和一个公共存储 属性:
来解决
@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" ~= [=10=] })
}
}
即使我认为 New Dev 的解决方案更好,因此是官方答案,我也会 post 我的 Combine 解决方案以防有人好奇。我基本上通过分离用户输入和用户输出来消除循环。对于界面,它看起来是一样的,但我可以创建一个 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 {
[=10=].filter {[=10=].isNumber}
}
.map {
Int([=10=])
}
.assign(to: \.value, on: self)
.store(in: &subscriptions)
$value
.map {
[=10=] == nil ? "": String([=10=]!)
}
.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")
}
}
我正在为用于引入数量的 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 {
[=10=].filter {[=10=].isNumber}
}
.print("post-filter")
.map {
Int([=10=])
}
.assign(to: \.value, on: self)
.store(in: &subscriptions)
$value.map {
[=10=] != nil ? String([=10=]!): ""
}
.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")
有谁知道为什么没有赋值?谢谢
这不是 Combine 问题本身导致的,但似乎 Published
发布者在 属性 上实际设置值之前发出。所以,基本上 "30a"
会覆盖 assign
.
无论如何,这个循环的管道链似乎有点可疑。我也不认为你真的需要 Combine 这里 - 它可以用两个计算属性和一个公共存储 属性:
来解决@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" ~= [=10=] })
}
}
即使我认为 New Dev 的解决方案更好,因此是官方答案,我也会 post 我的 Combine 解决方案以防有人好奇。我基本上通过分离用户输入和用户输出来消除循环。对于界面,它看起来是一样的,但我可以创建一个 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 {
[=10=].filter {[=10=].isNumber}
}
.map {
Int([=10=])
}
.assign(to: \.value, on: self)
.store(in: &subscriptions)
$value
.map {
[=10=] == nil ? "": String([=10=]!)
}
.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")
}
}