【问题标题】:Combine equivalent of RxSwift's bind(to:)组合相当于 RxSwift 的 bind(to:)
【发布时间】:2022-07-09 13:43:46
【问题描述】:

我是一位经验丰富的 RxSwift 用户,并且在 RxSwift 中有一个很好的工作 MVVM 结构。我是Combine 的新手,但看在上帝的份上,我无法弄清楚如何在Combine 中做类似的事情。最大的障碍是组合中的 bind(to:) 等效项。我不知道如何将 1 个变量的结果链接到另一个变量。

这是我在 RxSwift 中要做的:

protocol UseCase {
  func execute(id: Int) -> Single<CustomClass>
}

class DefaultUseCase: UseCase {
  func execute(id: Int) -> Single<CustomClass> {
    // Do network call and return in Single format
  }
}


class ViewModel {

  struct Input {
    let load = PublishRelay<Void>()
  }

  struct Output {
    let isButtonEnabled: Driver<Bool>
  }

  let disposeBag = DisposeBag()
  let input = Input()
  let output: Output

  init(id: Int, useCase: UseCase = DefaultUseCase()) {
    let isButtonEnabled = BehaviorRelay<Bool>(value: false)

    let action = Action<Void, CustomClass> { id in 
      return useCase.execute(id: id)
    }

    self.output = Output(isButtonEnabled: isButtonEnabled.asDriver())

    input
      .load
      .bind(to: useCase.inputs)
      .disposed(by: disposeBag)

    action
      .elements
      .map { // map CustomClass to Bool }
      .bind(to: isButtonEnabled)
      .disposed(by: disposeBag)
  }
}

动作类来自这个框架:https://github.com/RxSwiftCommunity/Action

我不知道如何在 Combine 中做类似的事情,我已经阅读了一些教程,但这对我来说没有意义。看起来您需要感觉像一千个变量才能使用 viewModel 将 1 个值传递给您的视图/视图控制器。

我正在寻找一段与上面的 RxSwift 代码完全相同的代码,但在结合一些解释。

【问题讨论】:

  • 所以看起来最大的障碍是bind(to:) 等价,建议在问题中更清楚地说明这一点,它会增加你得到回复的机会。
  • 感谢@Cristik,不知道这并不完全清楚。将编辑问题!
  • 它混淆了事件的来源。如果你有一个 Observable 链a-&gt;b-&gt;c,那么就有明确的源到汇关系。事件流经从ac 的链。但是,如果这些元素中的每一个都是主题或中继,那么事件可以插入到链中的任何点,并且是对范式本质的诅咒。
  • “主题 [和中继] 提供了一种方便的方式来探索 Rx,但不建议将它们用于日常使用。” -- Intro to Rx
  • “函数式反应式编程的本质是在声明时完全指定一个值的动态行为。” -- Heinrich Apfelmus 主题和中继无视这一点。它们对于转换非响应式代码和处理反馈循环是必要的,但不应该是您解决问题的第一件事。

标签: swift mvvm rx-swift combine


【解决方案1】:

首先,让我们简化您的视图模型:

class ViewModel {
    
    struct Input {
        let load = PublishRelay<Void>()
    }
    
    struct Output {
        let isButtonEnabled: Driver<Bool>
    }
    
    let input = Input()
    let output: Output
    
    init(id: Int, useCase: UseCase = DefaultUseCase()) {
        let isButtonEnabled = input.load
            .flatMapLatest { [useCase] in
                useCase.execute(id: id)
                    .map { _ in /* map CustomClass to Bool */ true }
                    .catchAndReturn(false)
            }
            .asDriver(onErrorRecover: { _ in fatalError() })

        self.output = Output(isButtonEnabled: isButtonEnabled)
    }
}

我不是你的 Input 结构的粉丝,但我正在使用它...

一旦你这样做了,就很容易看到如何翻译它:

class ViewModelʹ {
    struct Input {
        let load = PassthroughSubject<Void, Never>()
    }
    struct Output {
        let isButtonEnabled: AnyPublisher<Bool, Never>
    }

    let input = Input()
    let output: Output

    init(id: Int, useCase: UseCase) {
        let isButtonEnabled = input.load
            .map { [useCase] in
                useCase.execute(id: id)
                    .map { _ in /* map CustomClass to Bool */ true }
                    .catch { _ in Just(false) }
            }
            .switchToLatest()
            .eraseToAnyPublisher()
        
        self.output = Output(isButtonEnabled: isButtonEnabled)
    }
}

针对评论的更新

以下是您将如何使用多个输出的用例响应(在 iOS 13 中编译):

class ViewModelʹ {
    struct Input {
        let load = PassthroughSubject<Void, Never>()
    }
    struct Output {
        let isButtonEnabled: AnyPublisher<Bool, Never>
        let somethingElse: AnyPublisher<String, Never>
    }

    let input = Input()
    let output: Output

    init(id: Int, useCase: UseCase) {
        let result = input.load
            .map { [useCase] in
                useCase.execute(id: id)
                    .catch { _ in Empty() }
            }
            .switchToLatest()
            .share()

        let isButtonEnabled = result
            .map { _ in /* map CustomClass to Bool */ true }
            .eraseToAnyPublisher()

        let somethingElse = result
            .map { _ in /* map CustomClass to String */ "" }
            .eraseToAnyPublisher()

        self.output = Output(
            isButtonEnabled: isButtonEnabled,
            somethingElse: somethingElse
        )
    }
}

当然,很大程度上取决于您希望如何处理错误。以上吞了他们,但你might want to expose them for yet another output

不过,所有这些都变成了一般教程,而不是回答问题。

【讨论】:

  • 如果我需要对多个@Published 使用useCase 执行的结果怎么办?如何从 useCase “存储” AnyPublisher 以便将其映射到正确的值?
  • 只是share() 结果。无需存储任何东西。
  • 如果用例的执行结果需要对其他变量进行不同的映射呢?因为我假设share().switchToLatest().eraseToAnypublisher() 之后发生并且到 Bool 的映射代码发生在之前?如果映射不同怎么办?
  • 我也支持 iOS 13,当我删除 useCase 的执行的自定义映射,并在最后添加 .share() 时,我得到一个错误 switchToLatest() is only available in iOS 14
  • 更新了我的答案以解释上述内容。
【解决方案2】:
extension Publisher {
    func bind<T: Subject>(to subject: T) -> AnyCancellable
    where T.Output == Self.Output,
          T.Failure == Self.Failure {
              
        self.subscribe(subject)
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-07
    • 1970-01-01
    • 2015-11-27
    • 2021-10-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多