Last active
April 27, 2019 19:59
ViewModel+RealLifeExample
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import RxSwift | |
import Action | |
enum SceneStateOutput { | |
case idle | |
case sendingNoRecipient | |
case sendingSomeRecipients(sendingList: [String: String]) | |
case sending | |
} | |
protocol SceneViewModelInputsType { | |
var updateUploadList: PublishSubject<(id: String, parameter: String)> { get } | |
var fromAnotherScene: PublishSubject<AnotherSceneViewModelType> { get } | |
} | |
protocol SceneViewModelOutputsType { | |
var state: Observable<SceneStateOutput> { get } | |
var dataSource: Observable<[Item]> { get } | |
} | |
protocol SceneViewModelActionsType { | |
var pushAnotherScene: CocoaAction { get } | |
var uploadStuff: Action<Void, DatabaseWriteResult> { get } | |
} | |
protocol SceneViewModelType { | |
var inputs: SceneViewModelInputsType { get } | |
var outputs: SceneViewModelOutputsType { get } | |
var actions: SceneViewModelActionsType { get } | |
} | |
final class SceneViewModel: SceneViewModelType { | |
var inputs: SceneViewModelInputsType { return self } | |
var outputs: SceneViewModelOutputsType { return self } | |
var actions: SceneViewModelActionsType { return self } | |
// Setup | |
private let sceneService: SceneServiceType | |
private let coordinator: SceneCoordinatorType | |
private let disposeBag: DisposeBag | |
// Inputs | |
var updateUploadList: PublishSubject<(id: String, parameter: String)> | |
var fromAnotherScene: PublishSubject<AnotherSceneViewModelType> | |
// Outputs | |
var state: Observable<SceneStateOutput> | |
var dataSource: Observable<[Item]> | |
// ViewModel Life Cycle | |
private let itemsForDataSource: Variable<[Item]> | |
private let stuffToSend: Variable<Stuff?> | |
private let sendingList: Variable<[String: String]> | |
private let isSendingStuff: Variable<Bool> | |
init(service: SceneServiceType, coordinator: SceneCoordinatorType) { | |
// Setup | |
self.sceneService = service | |
self.coordinator = coordinator | |
self.disposeBag = DisposeBag() | |
self.itemsForDataSource = Variable([]) | |
self.stuffToSend = Variable(nil) | |
self.sendingList = Variable([:]) | |
self.isSendingStuff = Variable(false) | |
// Inputs | |
updateUploadList = PublishSubject() | |
fromAnotherScene = PublishSubject() | |
// Outputs | |
state = Observable | |
.combineLatest( | |
stuffToSend.asObservable(), | |
sendingList.asObservable(), | |
isSendingStuff.asObservable(), | |
resultSelector: {( (stuffToSend: $0.0, | |
sendingList: $0.1, | |
isSendingStuff: $0.2) | |
)} | |
) | |
.map { stuffToSend, sendingList, isSendingStuff -> SceneStateOutput in | |
guard stuffToSend != nil else { return .idle } | |
guard !sendingList.isEmpty else { return .sendingNoRecipient } | |
guard isSendingStuff else { return .sendingSomeRecipients } | |
return .sending | |
} | |
.shareReplay(1) | |
dataSource = itemsForDataSource.asObservable() | |
// ViewModel Life Cycle | |
let fetchItems = sceneService | |
.fetchItems() | |
.startWith([]) | |
Observable.combineLatest(fetchItems, state) | |
.map { items, state in | |
let updatedItems = items.flatMap { item -> Item in | |
switch state { | |
case .sendingModeNoRecipient: | |
item.bool = true | |
item.parameter = nil | |
case .sendingModeSomeRecipients(let sendingList): | |
item.bool2 = true | |
item.parameter = sendingList[item.id] | |
case .sending: | |
item.bool2 = false | |
default: | |
item.bool = true | |
item.bool2 = false | |
item.parameter = nil | |
} | |
return item | |
} | |
return updatedItems | |
} | |
.bind(to: itemsForDataSource) | |
.disposed(by: disposeBag) | |
fromAnotherScene | |
.flatMapLatest { $0.actions.getStuff..execute() } | |
.bind(to: stuffToSend) | |
.disposed(by: disposeBag) | |
updateUploadList | |
.subscribe(onNext: { [unowned self] with in | |
self.sendingList.value[with.id] = with.parameter | |
}) | |
.disposed(by: disposeBag) | |
uploadStuff.executing | |
.bind(to: isSendingStuff) | |
.disposed(by: disposeBag) | |
uploadStuff.elements | |
.filter { $0 == .success } | |
.subscribe(onNext: { [unowned self] _ in | |
self.stuffToSend.value = nil | |
self.sendingList.value = [:] | |
}) | |
.disposed(by: disposeBag) | |
} | |
// Actions | |
lazy var pushAnotherScene: CocoaAction = { | |
return Action { [weak self] in | |
guard let strongSelf = self else { return .empty() } | |
let anotherSceneViewModel = AnotherSceneViewModel( | |
service: AnotherSceneService(), | |
coordinator: strongSelf.coordinator) | |
let anotherScene = Scene.anotherScene(anotherSceneViewModel) | |
return strongSelf.coordinator.transition(to: anotherScene, type: .push(animated: true)) | |
} | |
}() | |
lazy var uploadStuff: Action<Void, DatabaseWriteResult> = { | |
return Action { [weak self] in | |
guard let strongSelf = self else { return .empty() } | |
let stuffStream = strongSelf.stuffToSend.asObservable().unwrap().take(1) | |
let sendingListStream = strongSelf.sendingList.asObservable().filter { !$0.isEmpty }.take(1) | |
return Observable.zip(stuffStream, sendingListStream) | |
.flatMap { [weak self] stuff, sendingList -> Observable<DatabaseWriteResult> in | |
guard let strongSelf = self else { return .empty() } | |
return strongSelf.sceneService.sendStuff(stuff, to: sendingList) | |
} | |
.take(1) | |
} | |
}() | |
} | |
extension SceneViewModel: SceneViewModelInputsType, SceneViewModelOutputsType, SceneViewModelActionsType { } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@Herakleis First off, thanks so much for your great resources! In addition to this example above, I've found your blog articles super helpful, so thanks for the time that you've put into those.
Quick question on your example here; can you elaborate a bit more as to why you define the actions (pushAnotherScene and uploadStuff, in this case) as lazy vars?
The problem I’m trying to solve is extending your example here to include a login (through Realm Cloud), and in order to do that I need to pass login credentials from the LoginViewController to the LoginViewModel, and just want to make sure I set this up in an Rx-y, Action-y way.
Many thanks in advance,
Scott