将 SwiftUI 按钮绑定到 AnySubscriber,例如 RxCocoa 的按钮点击
Posted
技术标签:
【中文标题】将 SwiftUI 按钮绑定到 AnySubscriber,例如 RxCocoa 的按钮点击【英文标题】:Binding a SwiftUI Button to AnySubscriber like RxCocoa's button tap 【发布时间】:2020-01-21 20:23:31 【问题描述】:我使用以下基于UIViewController
和RxSwift/RxCocoa
的代码编写了一个非常简单的MVVM 模式来绑定UIButton
点击事件来触发一些Observable
工作并监听结果:
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController
@IBOutlet weak var someButton: UIButton!
var viewModel: ViewModel!
private var disposeBag = DisposeBag()
override func viewDidLoad()
super.viewDidLoad()
viewModel = ViewModel()
setupBindings()
private func setupBindings()
someButton.rx.tap
.bind(to: self.viewModel.input.trigger)
.disposed(by: disposeBag)
viewModel.output.result
.subscribe(onNext: element in
print("element is \(element)")
).disposed(by: disposeBag)
class ViewModel
struct Input
let trigger: AnyObserver<Void>
struct Output
let result: Observable<String>
let input: Input
let output: Output
private let triggerSubject = PublishSubject<Void>()
init()
self.input = Input(trigger: triggerSubject.asObserver())
let resultObservable = triggerSubject.flatMap Observable.just("TEST")
self.output = Output(result: resultObservable)
它编译并运行良好。但是,我需要用SwiftUI
来Combin
ify 这个模式,所以我将该代码转换为以下代码:
import SwiftUI
import Combine
struct ContentView: View
var viewModel: ViewModel
var subscriptions = Set<AnyCancellable>()
init(viewModel: ViewModel)
self.viewModel = viewModel
setupBindings()
var body: some View
Button(action:
// <---- how to trigger viewModel's trigger from here
, label:
Text("Click Me")
)
private func setupBindings()
self.viewModel.output.result.sink(receiveValue: value in
print("value is \(value)")
)
.store(in: &subscriptions) // <--- doesn't compile due to immutability of ContentView
class ViewModel
struct Input
let trigger: AnySubscriber<Void, Never>
struct Output
let result: AnyPublisher<String, Never>
let input: Input
let output: Output
private let triggerSubject = PassthroughSubject<Void, Never>()
init()
self.input = Input(trigger: AnySubscriber(triggerSubject))
let resultPublisher = triggerSubject
.flatMap Just("TEST")
.eraseToAnyPublisher()
self.output = Output(result: resultPublisher)
由于两个错误(在代码中注释),此示例无法编译:
(1)问题一:如何像上面RxSwift
的情况一样,从按钮的actionclosure中触发发布者的工作?
(2) 问题 2 与架构设计有关,而不是编译错误:
错误说:... Cannot pass immutable value as inout argument: 'self' is immutable ...
,那是因为SwiftUI
视图是结构,它们被设计为只能通过各种绑定(@State
、@ObservedObject
等...)进行更改,我有两个相关的子问题问题2:
[A]:在SwiftUI
视图中sink
发布者是否被认为是一种不好的做法?这可能需要一些解决方法来将cancellable
存储在View
的结构范围内?
[B]:就 MVVM 架构模式而言,哪个更适合 SwiftUI/Combine
项目:使用具有 [ Input[Subscribers]、Output[AnyPublishers] ] 模式的 ViewModel,或者
ObservableObject
ViewModel 带有 [@Published
属性]?
【问题讨论】:
如果有人开始回答他们会重复官方文档,所以从SwiftUI: State and Data Flow开始。简而言之,使用ObservableObject
- 它是原生的、简单的,并且自动与 SwiftUI 集成。
【参考方案1】:
我在理解最佳 mvvm 方法时遇到了同样的问题。 推荐也看看这个帖子Best data-binding practice in Combine + SwiftUI?
将发布我的工作示例。应该很容易转换成你想要的。
SwiftUI 视图:
struct ContentView: View
@State private var dataPublisher: String = "ggg"
@State private var sliderValue: String = "0"
@State private var buttonOutput: String = "Empty"
let viewModel: SwiftUIViewModel
let output: SwiftUIViewModel.Output
init(viewModel: SwiftUIViewModel)
self.viewModel = viewModel
self.output = viewModel.bind(())
var body: some View
VStack
Text(self.dataPublisher)
Text(self.sliderValue)
Slider(value: viewModel.$sliderBinding, in: 0...100, step: 1)
Button(action:
self.viewModel.buttonBinding = ()
, label:
Text("Click Me")
)
Text(self.buttonOutput)
.onReceive(output.dataPublisher) value in
self.dataPublisher = value
.onReceive(output.slider) (value) in
self.sliderValue = "\(value)"
.onReceive(output.resultPublisher) (value) in
self.buttonOutput = value
AbstractViewModel:
protocol ViewModelProtocol
associatedtype Output
associatedtype Input
func bind(_ input: Input) -> Output
视图模型:
final class SwiftUIViewModel: ViewModelProtocol
struct Output
let dataPublisher: AnyPublisher<String, Never>
let slider: AnyPublisher<Double, Never>
let resultPublisher: AnyPublisher<String, Never>
typealias Input = Void
@SubjectBinding var sliderBinding: Double = 0.0
@SubjectBinding var buttonBinding: Void = ()
func bind(_ input: Void) -> Output
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.delay(for: 5.0, scheduler: DispatchQueue.main)
.map "Just for testing - \($0)"
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.share()
.eraseToAnyPublisher()
let resultPublisher = _buttonBinding.anyPublisher()
.dropFirst()
.flatMap Just("TEST")
.share()
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher,
slider: _sliderBinding.anyPublisher(),
resultPublisher: resultPublisher)
SubjectBinding 属性包装器:
@propertyWrapper
struct SubjectBinding<Value>
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue: Value)
subject = CurrentValueSubject<Value, Never>(wrappedValue)
func anyPublisher() -> AnyPublisher<Value, Never>
return subject.eraseToAnyPublisher()
var wrappedValue: Value
get
return subject.value
set
subject.value = newValue
var projectedValue: Binding<Value>
return Binding<Value>(get: () -> Value in
return self.subject.value
) (value) in
self.subject.value = value
【讨论】:
检查 github.com/serbats/Reactive-Combine-MVVM-Templates 以获得一些 MVVM Xcode 模板【参考方案2】:所以我最近也想知道如何做到这一点,因为我们还没有开始在 SwiftUI 中写出视图。
我创建了一个辅助对象,它封装了从函数调用到发布者的转换。我称它为中继。
@available(ios 13.0, *)
struct Relay<Element>
var call: (Element) -> Void didCall.send
var publisher: AnyPublisher<Element, Never>
didCall.eraseToAnyPublisher()
// MARK: Private
private let didCall = PassthroughSubject<Element, Never>()
具体来说,您可以声明一个私有中继并像这样使用它;
Button(action: relay.call,
label:
Text("Click Me")
)
然后你就可以随心所欲了。
relay.publisher
【讨论】:
这很好!通过在视图中公开 didTapLogin: Relay以上是关于将 SwiftUI 按钮绑定到 AnySubscriber,例如 RxCocoa 的按钮点击的主要内容,如果未能解决你的问题,请参考以下文章