使用 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)

这样做的最大好处是您可以将 isProcessEnabledloginEnteredpassEntered 移动到您的视图模型或其他一些对象,这将使整个代码更易于测试。此外,您还可以看到,您可以以某种有趣的方式组合这些主题中的每一个,以创建一些新的发布者/主题。

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