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

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

Related

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

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

How to pass chain view controller presenter with observable

I'm new in the RxSwift development and I've an issue while presentation a view controller.
My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.
My DetailViewController is modally presented and needs a ViewModel as input parameter.
I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.
Here is my current code
DetailsViewController
class DetailsViewController: UIViewController {
#IBOutlet weak private var doneButton: Button!
#IBOutlet weak private var label: Label!
let viewModel: DetailsViewModel
private let bag = DisposeBag()
var onComplete: Driver<Void> {
doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
private func bind() {
let ouput = viewModel.bind()
ouput.id.drive(idLabel.rx.text)
.disposed(by: bag)
}
}
DetailsViewModel
class DetailsViewModel {
struct Output {
let id: Driver<String>
}
let item: Observable<Item>
init(with vehicle: Observable<Item>) {
self.item = item
}
func bind() -> Output {
let id = item
.map { $0.id }
.asDriver(onErrorJustReturn: "Unknown")
return Output(id: id)
}
}
MainViewController
class MainViewController: UIViewController {
#IBOutlet weak private var tableView: TableView!
private var bag = DisposeBag()
private let viewModel: MainViewModel
private var detailsViewController: DetailsViewController?
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
bind()
}
private func bind() {
let input = MainViewModel.Input(
selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
)
let output = viewModel.bind(input: input)
showItem(output.selectedItem)
}
private func showItem(_ item: Observable<Item>) {
let viewModel = DetailsViewModel(with: vehicle)
detailsViewController = DetailsController(with: viewModel)
item.flatMapFirst { [weak self] item -> Observable<Void> in
guard let self = self,
let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
self.present(detailsViewController, animated: true)
return detailsViewController.onComplete.asObservable()
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController? = nil
})
.disposed(by: bag)
}
}
MainViewModel
class MainViewModel {
struct Input {
let selectedItem: Observable<Item>
}
struct Output {
let selectedItem: Observable<Item>
}
func bind(input: Input) -> Output {
let selectedItem = input.selectedItem
.throttle(.milliseconds(500),
latest: false,
scheduler: MainScheduler.instance)
.asObservable()
return Output(selectedItem: selectedItem)
}
}
My issue is on showItem of MainViewController.
I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.
Having Item instead of Observable<Item> as input could let me use this kind of code:
item.flatMapFirst { item -> Observable<Void> in
guard let self = self else {
return Observable<Void>.never()
}
let viewModel = DetailsViewModel(with: item)
self.detailsViewController = DetailsViewController(with: viewModel)
guard let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
present(detailsViewController, animated: true)
return detailsViewController
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController = nil
})
.disposed(by: bag)
What is the right way to do this?
Thanks
You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.
Your second option is much closer to what you should have. It's a fine idea.
You might find my CLE library interesting. It takes care of the issue you are trying to handle here.

closure through typealias swift not working

why does typealias closure not transmit data and output nothing to the console? How to fix it?
class viewModel: NSObject {
var abc = ["123", "456", "789"]
typealias type = ([String]) -> Void
var send: type?
func createCharts(_ dataPoints: [String]) {
var dataEntry: [String] = []
for item in dataPoints {
dataEntry.append(item)
}
send?(dataEntry)
}
override init() {
super.init()
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel()
func type() {
viewModel.send = { item in
print(item)
}
}
override func viewDidLoad() {
super.viewDidLoad()
print("hello")
type()
}
}
I have a project in which a similar design works, but I can not repeat it
The pattern is fine, but the timing is off.
You’re calling createCharts during the init of the view model. But the view controller is setting the send closure after the init of the view model is done.
Bottom line, you probably don’t want to call createCharts during the init of the view model.
Possible solution is to create custom initializer:
class viewModel: NSObject {
...
init(send: type?) {
self.send = send
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel(send: { print($0) })
...
}

How to present PublishSubject as Observable in MVVM?

I have something like this:
protocol ViewModel: class {
var eventWithInitialValue: Observable<Int> { get }
}
class ViewModelImpl: ViewModel {
let eventWithInitialValue: BehaviorSubject<Int> = BehaviorSubject(value: 0)
init() {
eventWithInitialValue.onNext(1)
}
}
class ViewController: UIViewController {
weak var viewModel: ViewModel?
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel?
.eventWithInitialValue
.subscribe(onNext: {
print($0)
}).disposed(by: bag)
}
}
I want to communicate with viewModel fields from ViewController as an Observables. But inside viewModel this fields should be a [OneOf]Subject types (for safety reasons).
Implementation above have a next compile time error -> Type 'ViewModelImpl' does not conform to protocol 'ViewModel'
Can anyone help with the implementation of these requirements?
Your problem is not with Rx, your error is related to your protocol
This will solve the current issue
protocol ViewModel: class {
var eventWithInitialValue: BehaviorSubject<Int> { get }
}
class ViewModelImpl: ViewModel {
var eventWithInitialValue: BehaviorSubject<Int> = BehaviorSubject(value: 0)
init() {
eventWithInitialValue.onNext(1)
}
}
I think you got this Type 'ViewModelImpl' does not conform to protocol 'ViewModel' because you define the eventWithInitialValue's type in your implementation as BehaviorSubject.
What I can suggest is something like this
protocol ViewModel {
var data: Observable<Int> { get}
}
class ViewModelImpl: ViewModel {
private let dataSubject = BehaviorSubject(value: 1)
var data: Observable<Int> {
return dataSubject
}
}

MVVM with realm: Passing Realm-results across threads?

Using Xcode-8.2.1, Swift-3.0.2, RealmSwift-2.2.0, iOS-Simulator-10:
I try applying the MVVM pattern (explained by Steve Scott here) using Realm.
Everything works until the moment (inside the VIEW-part - see below) where I try to access a viewmodel-property. It says: Realm accessed from incorrect thread
How could I still make the MVVM-pattern do its job of separating model, view-model and view but, on the same time, get thread-safety with realm ?
Is there a way to make Realm-results (i.e. Results<BalancesDataEntry>) being passed across threads ??
Here is my code:
(the issue happens at the very bottom, inside the View-part)
// REALM-OBJECT:
import Foundation
import RealmSwift
class BalancesDataEntry: Object {
dynamic var category: String = ""
dynamic var index: Int = 0
}
MODEL:
import Foundation
import RealmSwift
class MVVMCBalancesModel: BalancesModel
{
fileprivate var entries = [BalancesDataEntry]()
let realm = try! Realm()
init() {
self.createDataEntries()
}
fileprivate func createDataEntries() {
let myBalance = BalancesDataEntry()
myBalance.index = 0
myBalance.category = "Love"
try! self.realm.write {
self.realm.deleteAll()
self.realm.add(myBalance)
}
}
func getEntries(_ completionHandler: #escaping (_ entries: [BalancesDataEntry]) -> Void)
{
// Simulate Aysnchronous data access
DispatchQueue.global().async {
let realmThread = try! Realm()
let returnArray: [BalancesDataEntry] = Array(realmThread.objects(BalancesDataEntry.self))
completionHandler(returnArray)
}
}
}
VIEW-MODEL:
import Foundation
import RealmSwift
class MVVMCBalancesViewModel: BalancesViewModel
{
weak var viewDelegate: BalancesViewModelViewDelegate?
weak var coordinatorDelegate: BalancesViewModelCoordinatorDelegate?
fileprivate var entries: [BalancesDataEntry]? {
didSet {
viewDelegate?.entriesDidChange(viewModel: self)
}
}
var model: BalancesModel? {
didSet {
entries = nil;
model?.getEntries({ (myEntries) in
self.entries = myEntries
})
}
}
var title: String {
return "My Balances"
}
var numberOfEntries: Int {
if let entries = entries {
return entries.count
}
return 0
}
func entryAtIndex(_ index: Int) -> BalancesDataEntry?
{
if let entries = entries , entries.count > index {
return entries[index]
}
return nil
}
func useEntryAtIndex(_ index: Int)
{
if let entries = entries, let coordinatorDelegate = coordinatorDelegate , index < entries.count {
coordinatorDelegate.balancesViewModelDidSelectData(self, data: entries[index])
}
}
}
VIEW:
import UIKit
class MVVMCBalancesViewController: UIViewController {
#IBOutlet weak var label1Outlet: UILabel!
#IBOutlet weak var label2Outlet: UILabel!
var viewModel: BalancesViewModel? {
willSet {
viewModel?.viewDelegate = nil
}
didSet {
viewModel?.viewDelegate = self
refreshDisplay()
}
}
var isLoaded: Bool = false
func refreshDisplay()
{
if let viewModel = viewModel , isLoaded {
// !!!!!!! HERE IS THE ISSUE: Realm accessed from incorrect thread !!!!
self.label1Outlet.text = viewModel.entryAtIndex(0)?.category
self.label2Outlet.text = viewModel.entryAtIndex(1)?.category
} else {
}
}
override func viewDidLoad()
{
super.viewDidLoad()
isLoaded = true
refreshDisplay();
}
}
extension MVVMCBalancesViewController: BalancesViewModelViewDelegate
{
func entriesDidChange(viewModel: BalancesViewModel)
{
}
}
You can use ThreadSafeReference to pass Realm's thread-confined types (Object, Results, List, LinkingObjects) to a different thread. The documentation's section on Passing Instances Across Threads contains this example of passing a single instance of an Object subclass across threads:
let person = Person(name: "Jane")
try! realm.write {
realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "background").async {
let realm = try! Realm()
guard let person = realm.resolve(personRef) else {
return // person was deleted
}
try! realm.write {
person.name = "Jane Doe"
}
}
It can be used similarly for Results.
I have found a workaround (see below): Maybe you have better solutions - please let me know!
Here is my github-code realm_mvvm_c on github
After introducing a new protocol and making (pretty much everything) conform to it, things worked out.
Here is the protocol called DataEntry:
import Foundation
protocol DataEntry: class {
var idx: Int { get set }
var category: String { get set }
}
Now, make everything conform to it, such as
--> the realm object (i.e. class BalancesDataEntry: Object, DataEntry {...)
--> the getEntries return value (i.e. func getEntries(_ completionHandler: #escaping (_ entries: [DataEntry]) -> Void))
--> the View-Model's entries (i.e. fileprivate var entries: [DataEntry]? {..)
--> all the corresponding Model- and View-Model protocols also need the DataEntry datatype (see git-repo for complete picture)
After that, it was enough to change the completion-handler return-array of the MODEL's method getEntries(..) to a newly created object-instance (ie. DataEntryDub) that is keept conform to the DataEntry protocol:
func getEntries(_ completionHandler: #escaping (_ entries: [DataEntry]) -> Void)
{
// Simulate Aysnchronous data access
DispatchQueue.global().async {
let realmThread = try! Realm()
class DataEntryDub: DataEntry {
var idx: Int
var category: String
init(idx: Int, category: String) {
self.idx = idx
self.category = category
}
}
var returnArray = [DataEntry]()
for entry in realmThread.objects(BalancesDataEntry.self) {
returnArray.append(DataEntryDub(idx: entry.idx, category: entry.category))
}
completionHandler(returnArray)
}
}
Here is my github-code realm_mvvm_c on github