使用 Combine 根据字符串启用按钮
Make button enabled depending on strings using Combine
我有一个名为 proceed 的登录按钮,并且有 2 个 @Published
属性表示 UITextField
中当前键入的文本。
我想要的是,将 .filter { ![=13=].0.isEmpty && ![=13=].1.isEmpty}
的结果分配给 UIButton
的 Bool
变量 isEnabled
目前我得到了这个:
passwordTxtf.textView.$text
.combineLatest(loginTxtf.textView.$text)
.filter { ![=10=].0.isEmpty && ![=10=].1.isEmpty}
.sink(receiveValue: { [weak self] in
guard let weakSelf = self else { return }
weakSelf.loginEntered = [=10=]
weakSelf.passEntered =
weakSelf.setProceedEnableState()
})
.store(in: &subscriptions)
其中 setProceedEnableState()
是一个检查 class 变量并因此确定按钮启用状态的函数。
但我想以某种方式在“管道”期间分配它,或者做更优雅的方式
您可以构造代码,以便将来自文本视图的每个信号分配给 Subject
。这是一个基于您的用例的小示例,
let isProcessEnabled = PassthroughSubject<Bool, Never>()
let loginEntered = PassthroughSubject<Bool, Never>()
let passEntered = CurrentValueSubject<Bool, Never>(false)
passwordTxtf.textView.$text.map { ![=10=].isEmpty }
.subscribe(passEntered)
.store(in: &store)
loginTxtf.textView.$text.map { ![=10=].isEmpty }
.subscribe(loginEntered)
.store(in: &store)
Publishers.CombineLatest(passEntered, loginEntered)
.map { [=10=] && }
.subscribe(isProcessEnabled)
.store(in: &store)
isProcessEnabled.sink { isEnabled in
print("Is processing enabled", isEnabled)
}.store(in: &store)
这样做的最大好处是您可以将 isProcessEnabled
、loginEntered
和 passEntered
移动到您的视图模型或其他一些对象,这将使整个代码更易于测试。此外,您还可以看到,您可以以某种有趣的方式组合这些主题中的每一个,以创建一些新的发布者/主题。
class ViewModel {
private var store = Set<AnyCancellable>()
var isProcessingEnabled: AnyPublisher<Bool, Never> {
return processEnabled.eraseToAnyPublisher()
}
var isLoginEntered: AnyPublisher<Bool, Never> {
return loginEntered.eraseToAnyPublisher()
}
var isPassEntered: AnyPublisher<Bool, Never> {
return passwordEntered.eraseToAnyPublisher()
}
func subscribeLoginText(publisher: AnyPublisher<String, Never>) {
publisher.map { ![=11=].isEmpty }.subscribe(loginEntered).store(in: &store)
}
func subscribePasswordText(publisher: AnyPublisher<String, Never>) {
publisher.map { ![=11=].isEmpty }.subscribe(passwordEntered).store(in: &store)
}
private var processEnabled = CurrentValueSubject<Bool, Never>(false)
private var loginEntered = PassthroughSubject<Bool, Never>()
private var passwordEntered = PassthroughSubject<Bool, Never>()
init() {
Publishers.CombineLatest(passwordEntered, loginEntered)
.map { [=11=] && }
.subscribe(processEnabled)
.store(in: &store)
}
}
而且,您可以看到,现在您已经对应用程序进行了模块化,并且您的视图模型不需要知道视图的细节/viewcontroller。最重要的是,您的代码现在非常可测试。您可以创建一个 viewmodel 实例并断言您的假设。
这是上述视图模型实现的一个小示例测试用例,
class ViewModelTest: XCTestCase {
var store: Set<AnyCancellable> = []
var login = PassthroughSubject<String, Never>()
var password = PassthroughSubject<String, Never>()
var isProcessingEnabled = CurrentValueSubject<Bool, Never>(false)
var viewModel: ViewModel = ViewModel()
override func setUp() {
store = []
viewModel = ViewModel()
login = PassthroughSubject<String, Never>()
password = PassthroughSubject<String, Never>()
isProcessingEnabled = CurrentValueSubject<Bool, Never>(false)
viewModel.isProcessingEnabled.subscribe(isProcessingEnabled).store(in: &store)
viewModel.subscribeLoginText(publisher: login.eraseToAnyPublisher())
viewModel.subscribePasswordText(publisher: password.eraseToAnyPublisher())
}
func testThatProcessingIsDisabledInitially() {
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsDisabledWhenOnlyLoginIsEmpty() {
login.send("")
password.send("b")
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsDisabledWhenOnlyPasswordIsEmpty() {
login.send("a")
password.send("")
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsEnabledWhenLoginAndPasswordAreNotEmpty() {
login.send("a")
password.send("b")
XCTAssertTrue(isProcessingEnabled.value, "Processing is disabled")
}
}
我有一个名为 proceed 的登录按钮,并且有 2 个 @Published
属性表示 UITextField
中当前键入的文本。
我想要的是,将 .filter { ![=13=].0.isEmpty && ![=13=].1.isEmpty}
的结果分配给 UIButton
Bool
变量 isEnabled
目前我得到了这个:
passwordTxtf.textView.$text
.combineLatest(loginTxtf.textView.$text)
.filter { ![=10=].0.isEmpty && ![=10=].1.isEmpty}
.sink(receiveValue: { [weak self] in
guard let weakSelf = self else { return }
weakSelf.loginEntered = [=10=]
weakSelf.passEntered =
weakSelf.setProceedEnableState()
})
.store(in: &subscriptions)
其中 setProceedEnableState()
是一个检查 class 变量并因此确定按钮启用状态的函数。
但我想以某种方式在“管道”期间分配它,或者做更优雅的方式
您可以构造代码,以便将来自文本视图的每个信号分配给 Subject
。这是一个基于您的用例的小示例,
let isProcessEnabled = PassthroughSubject<Bool, Never>()
let loginEntered = PassthroughSubject<Bool, Never>()
let passEntered = CurrentValueSubject<Bool, Never>(false)
passwordTxtf.textView.$text.map { ![=10=].isEmpty }
.subscribe(passEntered)
.store(in: &store)
loginTxtf.textView.$text.map { ![=10=].isEmpty }
.subscribe(loginEntered)
.store(in: &store)
Publishers.CombineLatest(passEntered, loginEntered)
.map { [=10=] && }
.subscribe(isProcessEnabled)
.store(in: &store)
isProcessEnabled.sink { isEnabled in
print("Is processing enabled", isEnabled)
}.store(in: &store)
这样做的最大好处是您可以将 isProcessEnabled
、loginEntered
和 passEntered
移动到您的视图模型或其他一些对象,这将使整个代码更易于测试。此外,您还可以看到,您可以以某种有趣的方式组合这些主题中的每一个,以创建一些新的发布者/主题。
class ViewModel {
private var store = Set<AnyCancellable>()
var isProcessingEnabled: AnyPublisher<Bool, Never> {
return processEnabled.eraseToAnyPublisher()
}
var isLoginEntered: AnyPublisher<Bool, Never> {
return loginEntered.eraseToAnyPublisher()
}
var isPassEntered: AnyPublisher<Bool, Never> {
return passwordEntered.eraseToAnyPublisher()
}
func subscribeLoginText(publisher: AnyPublisher<String, Never>) {
publisher.map { ![=11=].isEmpty }.subscribe(loginEntered).store(in: &store)
}
func subscribePasswordText(publisher: AnyPublisher<String, Never>) {
publisher.map { ![=11=].isEmpty }.subscribe(passwordEntered).store(in: &store)
}
private var processEnabled = CurrentValueSubject<Bool, Never>(false)
private var loginEntered = PassthroughSubject<Bool, Never>()
private var passwordEntered = PassthroughSubject<Bool, Never>()
init() {
Publishers.CombineLatest(passwordEntered, loginEntered)
.map { [=11=] && }
.subscribe(processEnabled)
.store(in: &store)
}
}
而且,您可以看到,现在您已经对应用程序进行了模块化,并且您的视图模型不需要知道视图的细节/viewcontroller。最重要的是,您的代码现在非常可测试。您可以创建一个 viewmodel 实例并断言您的假设。
这是上述视图模型实现的一个小示例测试用例,
class ViewModelTest: XCTestCase {
var store: Set<AnyCancellable> = []
var login = PassthroughSubject<String, Never>()
var password = PassthroughSubject<String, Never>()
var isProcessingEnabled = CurrentValueSubject<Bool, Never>(false)
var viewModel: ViewModel = ViewModel()
override func setUp() {
store = []
viewModel = ViewModel()
login = PassthroughSubject<String, Never>()
password = PassthroughSubject<String, Never>()
isProcessingEnabled = CurrentValueSubject<Bool, Never>(false)
viewModel.isProcessingEnabled.subscribe(isProcessingEnabled).store(in: &store)
viewModel.subscribeLoginText(publisher: login.eraseToAnyPublisher())
viewModel.subscribePasswordText(publisher: password.eraseToAnyPublisher())
}
func testThatProcessingIsDisabledInitially() {
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsDisabledWhenOnlyLoginIsEmpty() {
login.send("")
password.send("b")
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsDisabledWhenOnlyPasswordIsEmpty() {
login.send("a")
password.send("")
XCTAssertFalse(isProcessingEnabled.value, "Processing is enabled")
}
func testThatProcessingIsEnabledWhenLoginAndPasswordAreNotEmpty() {
login.send("a")
password.send("b")
XCTAssertTrue(isProcessingEnabled.value, "Processing is disabled")
}
}