GlobalActor directive doesn't guarantee a function will be called on that actor - swift

Assuming I have defined a global actor:
#globalActor actor MyActor {
static let shared = MyActor()
}
And I have a class, in which a couple of methods need to act under this:
class MyClass {
#MyActor func doSomething(undoManager: UndoManager) {
// Do something here
undoManager?.registerUndo(withTarget: self) {
$0.reverseSomething(undoManager: UndoManager)
}
}
#MyActor func reverseSomething(undoManager: UndoManager) {
// Do the reverse of something here
print(\(Thread.isMainThread) /// Prints true when called from undo stack
undoManager?.registerUndo(withTarget: self) {
$0.doSomething(undoManager: UndoManager)
}
}
}
Assume the code gets called from a SwiftUI view:
struct MyView: View {
#Environment(\.undoManager) private var undoManager: UndoManager?
let myObject: MyClass
var body: some View {
Button("Do something") { myObject.doSomething(undoManager: undoManager) }
}
}
Note that when the action is undone the 'reversing' func it is called on the MainThread. Is the correct way to prevent this to wrap the undo action in a task? As in:
#MyActor func reverseSomething(undoManager: UndoManager) {
// Do the reverse of something here
print(\(Thread.isMainThread) /// Prints true
undoManager?.registerUndo(withTarget: self) {
Task { $0.doSomething(undoManager: UndoManager) }
}
}

I am surprised that the compiler does not generate a warning about calling a global actor 'MyActor'-isolated instance method in a synchronous nonisolated context (i.e. the closure). It would appear that the compiler is confused by the closure syntax within an actor isolated method.
Anyway, you can wrap it in a Task and it should run that on the appropriate actor:
#MyActor func doSomething(undoManager: UndoManager) {
// Do something here
undoManager.registerUndo(withTarget: self) { target in
Task { #MyActor in
target.reverseSomething(undoManager: undoManager)
}
}
}
That having been said, I have found erratic UndoManager behavior when using it from a background thread (i.e., not on the main actor).
So, especially because undo/redo is behavior generally initiated from the UI (on the main thread), I would keep it on the main thread, and only run the desired work on another actor. E.g.:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var input: String = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("enter value")
}
Button("Add record") {
viewModel.addAndPrepareUndo(for: input)
input = ""
}.disabled(input.isEmpty)
Button("Undo") {
viewModel.undo()
}.disabled(!viewModel.canUndo)
Button("Redo") {
viewModel.redo()
}.disabled(!viewModel.canRedo)
}
.padding()
}
}
#globalActor actor MyGlobalActor {
static let shared = MyGlobalActor()
}
#MainActor
class ViewModel: ObservableObject {
#MyGlobalActor
var values: [String] = []
#Published var canUndo = false
#Published var canRedo = false
private var undoManager = UndoManager()
func undo() {
undoManager.undo()
updateUndoStatus()
}
func redo() {
undoManager.redo()
updateUndoStatus()
}
func updateUndoStatus() {
canUndo = undoManager.canUndo
canRedo = undoManager.canRedo
}
func addAndPrepareUndo(for newValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.removeAndPrepareRedo(for: newValue)
}
updateUndoStatus()
Task { #MyGlobalActor in
values.append(newValue)
print(#function, values)
}
}
func removeAndPrepareRedo(for revertValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.addAndPrepareUndo(for: revertValue)
}
updateUndoStatus()
Task { #MyGlobalActor in
values.removeLast()
print(#function, values)
}
}
}
Now, this is a somewhat contrived example (for something this simple, we wouldn't have a simply array on a global actor), but hopefully it illustrates the idea.
Or, you can use a non-global actor:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var input: String = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("enter value")
}
Button("Add record") {
viewModel.addAndPrepareUndo(for: input)
input = ""
}.disabled(input.isEmpty)
Button("Undo") {
viewModel.undo()
}.disabled(!viewModel.canUndo)
Button("Redo") {
viewModel.redo()
}.disabled(!viewModel.canRedo)
}
.padding()
}
}
#MainActor
class ViewModel: ObservableObject {
var model = Model()
#Published var canUndo = false
#Published var canRedo = false
private var undoManager = UndoManager()
func undo() {
undoManager.undo()
updateUndoStatus()
}
func redo() {
undoManager.redo()
updateUndoStatus()
}
func updateUndoStatus() {
canUndo = undoManager.canUndo
canRedo = undoManager.canRedo
}
func addAndPrepareUndo(for newValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.removeAndPrepareRedo(for: newValue)
}
updateUndoStatus()
Task {
await model.append(newValue)
await print(#function, model.values())
}
}
func removeAndPrepareRedo(for revertValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.addAndPrepareUndo(for: revertValue)
}
updateUndoStatus()
Task {
await model.removeLast()
await print(#function, model.values())
}
}
}
actor Model {
private var strings: [String] = []
func append(_ string: String) {
strings.append(string)
}
func removeLast() {
strings.removeLast()
}
func values() -> [String] {
strings
}
}

Related

AsyncStream spams view, where AsyncPublisher does not

I'm running into a behavior with AsyncStream I don't quite understand.
When I have an actor with a published variable, I can "subscribe" to it via an AsyncPublisher and it behaves as expected, updating only when there is a change in value. If I create an AsyncStream with a synchronous context (but with a potential task retention problem) it also behaves as expected.
The weirdness happens when I try to wrap that publisher in an AsyncStream with an asyncronous context. It starts spamming the view with an update per loop it seems, NOT only when there is a change.
What am I missing about the AsyncStream.init(unfolding:oncancel:) which is causing this behavior?
https://developer.apple.com/documentation/swift/asyncstream/init(unfolding:oncancel:)?
import Foundation
import SwiftUI
actor TestService {
static let shared = TestService()
#MainActor #Published var counter:Int = 0
#MainActor public func updateCounter(by delta:Int) async {
counter = counter + delta
}
public func asyncStream() -> AsyncStream<Int> {
return AsyncStream.init(unfolding: unfolding, onCancel: onCancel)
//() async -> _?
func unfolding() async -> Int? {
for await n in $counter.values {
//print("\(location)")
return n
}
return nil
}
//optional
#Sendable func onCancel() -> Void {
print("confirm counter got canceled")
}
}
public func syncStream() -> AsyncStream<Int> {
AsyncStream { continuation in
let streamTask = Task {
for await n in $counter.values {
continuation.yield(n)
}
}
continuation.onTermination = { #Sendable _ in
streamTask.cancel()
print("StreamTask Canceled")
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
TestActorButton()
HStack {
//TestActorViewA() //<-- uncomment at your own risk.
TestActorViewB()
TestActorViewC()
}
}
.padding()
}
}
struct TestActorButton:View {
var counter = TestService.shared
var body: some View {
Button("increment counter") {
Task { await counter.updateCounter(by: 2) }
}
}
}
struct TestActorViewA:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Fires constantly.
for await value in await counter.asyncStream() {
print("View A Value: \(value)")
counterVal = value
}
}
}
}
struct TestActorViewB:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Behaves like one would expect. Fires once per change.
for await value in await counter.$counter.values {
print("View B Value: \(value)")
counterVal = value
}
}
}
}
struct TestActorViewC:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Also only fires on update
for await value in await counter.syncStream() {
print("View C Value: \(value)")
counterVal = value
}
}
}
}
The real solution to wrapping a publisher appears to be to stick to the synchronous context initializer and have it cancel it's own task:
public func stream() -> AsyncStream<Int> {
AsyncStream { continuation in
let streamTask = Task {
for await n in $counter.values {
//do hard work to transform n
continuation.yield(n)
}
}
continuation.onTermination = { #Sendable _ in
streamTask.cancel()
print("StreamTask Canceled")
}
}
}
From what I can tell the "unfolding" style initializer for AsyncStream is simply not a fit for wrapping an AsyncPublisher. The "unfolding" function will "pull" at the published value from within the stream, so the stream will just keep pushing values from that infinite well.
It seems like the "unfolding" style initializer is best used when processing a finite (but potentially very large) list of items, or when generating ones values from scratch... something like:
struct NumberQueuer {
let numbers:[Int]
public func queueStream() -> AsyncStream<Int> {
var iterator = AsyncArray(values: numbers).makeAsyncIterator()
print("Queue called")
return AsyncStream.init(unfolding: unfolding, onCancel: onCancel)
//() async -> _?
func unfolding() async -> Int? {
do {
if let item = try await iterator.next() {
return item
}
} catch let error {
print(error.localizedDescription)
}
return nil
}
//optional
#Sendable func onCancel() -> Void {
print("confirm NumberQueue got canceled")
}
}
}
public struct AsyncArray<Element>: AsyncSequence, AsyncIteratorProtocol {
let values:[Element]
let delay:TimeInterval
var currentIndex = -1
public init(values: [Element], delay:TimeInterval = 1) {
self.values = values
self.delay = delay
}
public mutating func next() async throws -> Element? {
currentIndex += 1
guard currentIndex < values.count else {
return nil
}
try await Task.sleep(nanoseconds: UInt64(delay * 1E09))
return values[currentIndex]
}
public func makeAsyncIterator() -> AsyncArray {
self
}
}
One can force the unfolding type to work with an #Published by creating a buffer array that is checked repeatedly. The variable wouldn't actually need to be #Published anymore. This approach has a lot of problems but it can be made to work. If interested, I put it in a repo with a bunch of other AsyncStream examples. https://github.com/carlynorama/StreamPublisherTests
This article was very helpful to sorting this out: https://www.raywenderlich.com/34044359-asyncsequence-asyncstream-tutorial-for-ios
As was this video: https://www.youtube.com/watch?v=UwwKJLrg_0U

SwiftUI Class won't DeInit

I've got a class class MapSearch that I instantiate when I need to auto-complete address results. It works perfectly but it never deinitializes and I can't figure out why.
Easily test by creating the files below. Use the back button after navigating to the test page and watch the console messages. You will see that the view model initializes and deinitializes as it should, but you'll only see MapSearch initialize.
HomeView.swift
import SwiftUI
struct HomeView: View {
var body: some View {
NavigationView {
NavigationLink(destination: TestView(viewModel: TestViewModel()) {
Text("TestView")
}
}
}
}
TestView.swift
import SwiftUI
struct TestView: View {
#StateObject var viewModel: ViewModel
var body: some View {
Text("Hello World")
}
}
TestViewModel.swift
import Foundation
extension TestView {
#MainActor
class ViewModel: ObservableObject {
#Published var mapSearch: MapSearch()
init() {
print("Test View Model Initialized")
}
deinit {
print("Test View Model Deinitialized")
}
}
}
MapSearch.swift
import Combine
import CoreLocation
import Foundation
import MapKit
/// Uses MapKit and CoreLocation to auto-complete an address
class MapSearch: NSObject, ObservableObject {
#Published var countryName: String = "United States"
#Published var locationResults: [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables: Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm, countryName: self.countryName)
})
.sink(receiveCompletion: { (_) in
}, receiveValue: { (results) in
// Show country specific results
self.locationResults = results.filter { $0.subtitle.contains(self.countryName) }
})
.store(in: &cancellables)
print("MapSearch Initialized")
}
deinit {
print("MapSearch Deinitialized")
}
func searchTermToResults(searchTerm: String, countryName: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// deal with the error here, it will finish the Combine publisher stream
currentPromise?(.failure(error))
}
}
The MapSearch class needed to be adjusted to add [weak self] in the combine calls. Now it deinits properly.
Here's the code for reference:
import Combine
import CoreLocation
import Foundation
import MapKit
/// Uses MapKit and CoreLocation to auto-complete an address
class MapSearch: NSObject, ObservableObject {
#Published var countryName: String = "United States"
#Published var locationResults: [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables: Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ [weak self] (currentSearchTerm) in
(self?.searchTermToResults(searchTerm: currentSearchTerm, countryName: self?.countryName ?? "")) ??
Future { [weak self] promise in
self?.searchCompleter.queryFragment = self?.searchTerm ?? ""
self?.currentPromise = promise
}
})
.sink(receiveCompletion: { (_) in
}, receiveValue: { [weak self] (results) in
// Show country specific results
self?.locationResults = results.filter { $0.subtitle.contains(self?.countryName ?? "") }
})
.store(in: &cancellables)
print("MapSearch Initialized")
}
deinit {
print("MapSearch Deinitialized")
}
func searchTermToResults(searchTerm: String, countryName: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { [weak self] promise in
self?.searchCompleter.queryFragment = searchTerm
self?.currentPromise = promise
}
}
}
extension MapSearch: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// deal with the error here, it will finish the Combine publisher stream
currentPromise?(.failure(error))
}
}

SwiftUI #Binding reloading on push/pop with different navigation items

I've got a very simple app example that has two views: a MasterView and a DetailView. The MasterView is presented inside a ContentView with a NavigationView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView(viewModel: MasterViewModel())
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton()
)
}
}
}
struct MasterView: View {
#ObservedObject private var viewModel: MasterViewModel
init(viewModel: MasterViewModel) {
self.viewModel = viewModel
}
var body: some View {
print("Test")
return DataStatusView(dataSource: self.$viewModel.result) { texts -> AnyView in
print("Closure")
return AnyView(List {
ForEach(texts, id: \.self) { text in
NavigationLink(
destination: DetailView(viewModel: DetailViewModel(stringToDisplay: text))
) {
Text(text)
}
}
})
}.onAppear {
if case .waiting = self.viewModel.result {
self.viewModel.fetch()
}
}
}
}
struct DetailView: View {
#ObservedObject private var viewModel: DetailViewModel
init(viewModel: DetailViewModel) {
self.viewModel = viewModel
}
var body: some View {
self.showView().onAppear {
self.viewModel.fetch()
}
.navigationBarTitle(Text("Detail"))
}
func showView() -> some View {
switch self.viewModel.result {
case .found(let s):
return AnyView(Text(s))
default:
return AnyView(Color.red)
}
}
}
The DataStatusView is a simple view to manage some state:
public enum ResultState<T, E: Error> {
case waiting
case loading
case found(T)
case failed(E)
}
struct DataStatusView<Content, T>: View where Content: View {
#Binding private(set) var dataSource: ResultState<T, Error>
private let content: (T) -> Content
private let waitingContent: AnyView?
#inlinable init(dataSource: Binding<ResultState<T, Error>>,
waitingContent: AnyView? = nil,
#ViewBuilder content: #escaping (T) -> Content) {
self._dataSource = dataSource
self.waitingContent = waitingContent
self.content = content
}
var body: some View {
self.buildMainView()
}
private func buildMainView() -> some View {
switch self.dataSource {
case .waiting:
return AnyView(Color.red)
case .loading:
return AnyView(Color.green)
case .found(let data):
return AnyView(self.content(data))
case .failed:
return AnyView(Color.yellow)
}
}
}
and the view models are a very simple "pretend to make a network call" vm:
final class MasterViewModel: ObservableObject {
#Published var result: ResultState<[String], Error> = .waiting
init() { }
func fetch() {
self.result = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.result = .found(["This", "is", "a", "test"])
}
}
}
final class DetailViewModel: ObservableObject {
#Published var result: ResultState<String, Error> = .waiting
private let stringToDisplay: String
init(stringToDisplay: String) {
self.stringToDisplay = stringToDisplay
}
func fetch() {
self.result = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.result = .found(self.stringToDisplay)
}
}
}
Now the problem I'm having is that every time I go from Master -> Detail view the block inside the DataStatusView is called. This is a problem because the "DetailView" is constantly re-created (and therefore its vm too, which causes the loading of the detail's data to fail).
This is happening because when I go from master -> detail the buttons in the navigation bar change (or at least that's the hypothesis). When I remove the lines:
.navigationBarItems(
leading: EditButton()
)
This works as "expected".
What is the "SwiftUI" way of dealing with this? A sample project that shows this issue is here: https://github.com/kerrmarin/swiftui-mvvm-master-detail

Test PublishSubject for ViewState

I'm trying to test the main functionality of my ViewModel. The important step is to test te loaded state completed. But for sure, for a better test it could be interesting to test al states.
I was reading a lot of post and information about RxTest and RxBlocking but I'm not able to test this module. If someone can help me, it would be great!
struct Product: Equatable { }
struct Promotion { }
protocol ProductsRepository {
func fetchProducts() -> Observable<Products>
func fetchPromotions() -> Observable<[Promotion]>
}
struct ProductCellViewModel: Equatable {
let product: Product
}
struct Products {
let products: [Product]
}
enum ProductsViewState: Equatable {
case loading
case empty
case error
case loaded ([ProductCellViewModel])
}
class ProductsViewModel {
var repository: ProductsRepository
let disposeBag = DisposeBag()
private var productCellViewModel: [ProductCellViewModel]
private var promotions: [Promotion]
// MARK: Input
init(repository: ProductsRepository) {
self.repository = repository
productCellViewModel = [ProductCellViewModel]()
promotions = [Promotion]()
}
func requestData(scheduler: SchedulerType) {
state.onNext(.loading)
resetCalculate()
repository.fetchProducts()
.observeOn(scheduler)
.flatMap({ (products) -> Observable<[ProductCellViewModel]> in
return self.buildCellViewModels(data: products)
}).subscribe(onNext: { (cellViewModels) in
self.productCellViewModel = cellViewModels
}, onError: { (error) in
self.state.onNext(.error)
}, onCompleted: {
self.repository.fetchPromotions()
.flatMap({ (promotions) -> Observable<[Promotion]> in
self.promotions = promotions
return Observable.just(promotions)
}).subscribe(onNext: { (_) in
self.state.onNext(.loaded(self.productCellViewModel))
}, onError: { (error) in
self.state.onNext(.error)
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
}
// MARK: Output
var state = PublishSubject<ProductsViewState>()
// MARK: ViewModel Map Methods
private func buildCellViewModels(data: Products) -> Observable <[ProductCellViewModel]> {
var viewModels = [ProductCellViewModel]()
for product in data.products {
viewModels.append(ProductCellViewModel.init(product: product))
}
return Observable.just(viewModels)
}
func resetCalculate() {
productCellViewModel = [ProductCellViewModel]()
}
}
The goal is to be able to test all of ProductsViewState after viewmodel.requestData() is being called
The key here is that you have to inject your scheduler into the function so you can inject a test scheduler. Then you will be able to test your state. BTW that state property should be a let not a var.
class ProductsViewModelTests: XCTestCase {
var scheduler: TestScheduler!
var result: TestableObserver<ProductsViewState>!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
result = scheduler.createObserver(ProductsViewState.self)
disposeBag = DisposeBag()
}
func testStateLoaded() {
let mockRepo = MockProductsRepository(products: { .empty() }, promotions: { .empty() })
let viewModel = ProductsViewModel(repository: mockRepo)
viewModel.state.bind(to: result).disposed(by: disposeBag)
viewModel.requestData(scheduler: scheduler)
scheduler.start()
XCTAssertEqual(result.events, [.next(0, ProductsViewState.loading), .next(1, .loaded([]))])
}
func testState_ProductsError() {
let mockRepo = MockProductsRepository(products: { .error(StubError()) }, promotions: { .empty() })
let viewModel = ProductsViewModel(repository: mockRepo)
viewModel.state.bind(to: result).disposed(by: disposeBag)
viewModel.requestData(scheduler: scheduler)
scheduler.start()
XCTAssertEqual(result.events, [.next(0, ProductsViewState.loading), .next(1, .error)])
}
func testState_PromotionsError() {
let mockRepo = MockProductsRepository(products: { .empty() }, promotions: { .error(StubError()) })
let viewModel = ProductsViewModel(repository: mockRepo)
viewModel.state.bind(to: result).disposed(by: disposeBag)
viewModel.requestData(scheduler: scheduler)
scheduler.start()
XCTAssertEqual(result.events, [.next(0, ProductsViewState.loading), .next(1, .error)])
}
}
struct StubError: Error { }
struct MockProductsRepository: ProductsRepository {
let products: () -> Observable<Products>
let promotions: () -> Observable<[Promotion]>
func fetchProducts() -> Observable<Products> {
return products()
}
func fetchPromotions() -> Observable<[Promotion]> {
return promotions()
}
}

How to handle navigation with observables using Rx-MVVM-C

Hello I am trying to do a project with RxSwift and I am stuck trying to do in a properly way the connection between the Coordinator and the ViewModel.
Goal
Using observables, the Coordinator receives and event (in that case, when a row has been tapped) then does whatever.
Scenario
Giving a Post (String)
typealias Post = String
I have the following Coordinator:
class Coordinator {
func start() {
let selectedPostObservable = PublishSubject<Post>()
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
selectedPostObservable.subscribe { post in
//Do whatever
}
}
}
The selectedPostObservable is what I don't know how to connect it in a "clean" way with the viewModel.
As ViewModel:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
let result = input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.asDriver(onErrorJustReturn: nil)
return Output()
}
}
The result variable is what I should connect with selectedPostObservable.
And the ViewController (although I think is not relevant for the question):
class ViewController: UIViewController {
//...
var viewModel: ViewModel!
var tableView: UITableView!
//...
func bindViewModel() {
let input = ViewModel.Input(selectedIndexPath: tableView.rx.itemSelected.asObservable())
viewModel.transform(input: input)
}
}
Thank you so much.
Working with the structure you are starting with, I would put the PublishSubject in the ViewModel class instead of the Coordinator. Then something like this:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
let selectedPost = PublishSubject<Post>()
let bag = DisposeBag()
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.bind(to: selectedPost)
.disposed(by: bag)
return Output()
}
}
class Coordinator {
func start() {
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
viewModel.selectedPost.subscribe { post in
//Do whatever
}
.disposed(by: viewModel.bag)
}
}