I have a method 'getProducts' in my view model:
struct MyViewModel {
func getProducts(categoryId: Int) -> Observable<[Product]> {
return api.products(categoryId: categoryId)
}
var isRunning: Observable <Bool> = {
...
}
}
api.products is a private variable which uses URLSession rx extension: session.rx.data(...) in the background.
I would like to have some isRunning observer in my view model which I could subscribe to to know if it's do a network request.
Is it something I could do without making any amendments to my api class?
I'm new in reactive programming so any help would be appreciated.
Thanks.
Here's a solution using a helper class written by RxSwift authors in RxSwift Examples called ActivityIndicator.
The ideas is simple
struct MyViewModel {
/// 1. Create an instance of ActivityIndicator in your viewModel. You can make it private
private let activityIndicator = ActivityIndicator()
/// 2. Make public access to observable part of ActivityIndicator as you already mentioned in your question
var isRunning: Observable<Bool> {
return activityIndicator.asObservable()
}
func getProducts(categoryId: Int) -> Observable<[Product]> {
return api.products(categoryId: categoryId)
.trackActivity(activityIndicator) /// 3. Call trackActivity method in your observable network call
}
}
In related ViewController you can now subscribe to isRunning property. For instance:
viewModel.isLoading.subscribe(onNext: { loading in
print(loading)
}).disposed(by: bag)
Related
I have this view model in my code:
import RxSwift
protocol ViewModelInput {
func buttonTouched()
}
protocol ViewModelOutput {
var done : PublishRelay<Bool> { get set }
}
protocol ViewModelType {
var inputs: ViewModelInput { get }
var outputs: ViewModelOutput { get }
}
public final class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
var inputs: ViewModelInput { return self }
var outputs: ViewModelOutput { return self }
internal var done = PublishRelay<Bool>.init()
init() {}
func buttonTouched() {
self.outputs.done.accept(true)
}
}
And I'm using it's "output" like this:
// Somewhere else in my app
viewModel.outputs.done
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
// whatever
}).disposed(by: disposeBag)
To be honest I don't need that Boolean value with PublishRelay. I don't even need onNext() event. All I need is to notify my coordinator (part of app that uses this view model) about onCompleted(). However there is still some <Bool> generic type added to my output. I don't need any of that. Is there any cleaner way to achieve that?
I though about traits like Completable but as far as I understand I need to emit completed-event inside create() method or use Completable.empty(). Or maybe I don't understand traits that good, I don't know.
Any ideas?
I haven't done any RxSwift in a while, but have you tried making the type PublishRelay<Void>? Once you do that you can just pass () to outputs.done.accept(()) in your buttonTouched() method and not have to worry about passing arbitrary information that isn't needed
I think #Steven0351 is right with the < Void> approach. Just 2 little things:
It should also work by terminating the subject instead of emitting a Void value. It looks cleaner in the subscription as well.
I guess you are subscribing your outputs.done subject in the UI. In that case you might want to use Drivers. That way there's no need to specify observation on main scheduler (among other Drivers advantages).
ViewModel
internal var done = PublishRelay<Void>.init()
func buttonTouched() {
self.outputs.done.onCompleted()
}
ViewController
viewModel.outputs.done
.asDriver()
.drive(onCompleted: { [weak self] in
// whatever
}).disposed(by: disposeBag)
I have read articles about MVC, MVP and MVVM architecture but I am not cleat about how to create each architecture in my iOS app. Which classes/controller files I need to use to make for each architecture. What is the difference between them if we are using with Storyboard/Xib/Programmatically?
As I am using Xcode default MVC structure for iOS apps but I want to create my new project with MVVM structure but I am not sure how to create that structure.
Any help would be highly appreciated.
Thanks in advance.
This is an oversimplification of the many variants of these design patterns, but this is how I like to think about the differences between the two.
MVC
MVP
MVVM
for more information you can look at here
MVVM architecture in iOS can be easily implemented without using third party dependencies. For data binding, we can use a simple combination of Closure and didSet to avoid third-party dependencies.
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: #escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
An example of data binding from ViewController:
final class ExampleViewController: UIViewController {
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
// self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
}
// Or in one line:
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {
let items: Observable<[ItemViewModel]> = Observable([])
// Implmentation details...
}
Later it can be replaced with SwiftUI and Combine (when a minimum iOS version in of your app is 13)
In this article, there is a more detailed description of MVVM
https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3
In MVVM, the View (which is really the UIViewController) asks the View Model for information.
textLabel.text = viewModel.textToShow
The view model has a representation of everything needed to construct the UI. The View asks the View Model for those values (like, what string to show).
In MVP, the Presenter tells the View what to do.
view.showText(textToShow)
The view controller implements a protocol that translates this request into view controller specifics:
func showText(_ text: String) {
textLabel.text = text
}
Here's an example of MVP: https://stackoverflow.com/a/54499119/246895.
Sorry I could not come up with better title than that, Ill modify it if anybody suggests a better one after.
I have a protocol
#objc public protocol MyCollectionViewProtocol {
func scrollViewShouldScrollToTop()
}
I have declared it to be #objc because unfortunately DelegateProxy does not work with non NSObject protocols (I assume, if somebody can clarify that, will be a great help)
My collectionView
public class MyCollectionView: UICollectionView {
weak var cvDelegate : MyCollectionViewProtocol?
... //rest of the code isnt related to this question in particular
Now I declare delegate proxy as
open class RxMyCollectionViewDelegateProxy : DelegateProxy<MyCollectionView, MyCollectionViewProtocol>
, DelegateProxyType
, MyCollectionViewProtocol {
public static func currentDelegate(for object: MyCollectionView) -> MyCollectionViewProtocol? {
return object.cvDelegate
}
public static func setCurrentDelegate(_ delegate: MyCollectionViewProtocol?, to object: MyCollectionView) {
object.cvDelegate = delegate
}
public weak private(set) var collectionView: MyCollectionView?
internal lazy var shouldScrollPublishSubject: PublishSubject<Void> = {
let localSubject = PublishSubject<Void>()
return localSubject
}()
public init(collectionView: ParentObject) {
self.collectionView = collectionView
super.init(parentObject: collectionView, delegateProxy: RxMyCollectionViewDelegateProxy.self)
}
// Register known implementations
public static func registerKnownImplementations() {
self.register { RxMyCollectionViewDelegateProxy(collectionView: $0) }
}
//implementation of MyCollectionViewProtocol
public func scrollViewShouldScrollToTop() {
shouldScrollPublishSubject.onNext(())
self._forwardToDelegate?.scrollViewShouldScrollToTop()
}
deinit {
shouldScrollPublishSubject.onCompleted()
}
}
Finally I declare my Reactive extension for MyCollectionView as
extension Reactive where Base: MyCollectionView {
public var delegate: DelegateProxy<MyCollectionView, MyCollectionViewProtocol> {
return RxMyCollectionViewDelegateProxy.proxy(for: base)
}
public var shouldScrollToTop: ControlEvent<Void> {
let source = RxMyCollectionViewDelegateProxy.proxy(for: base).shouldScrollPublishSubject
return ControlEvent(events: source)
}
}
Finally, I use it as
collectionView.rx.shouldScrollToTop.debug().subscribe(onNext: { (state) in
print("I should scroll to top")
}, onError: { (error) in
print("errored out")
}, onCompleted: {
print("completed")
}, onDisposed: {
print("Disposed")
}).disposed(by: disposeBag)
Question
Because none of the online tutorials(Raywenderlich)/courses (Udemy)/Books(Raywenderlich) explains how to convert the swift protocol to Rx style am confused as what I am doing is correct or wrong. The code works but even the worst designed code might work, hence I wanna be sure what am doing is correct or am messing something. I wrote the above code following the approach used in UIScrollView+Rx.swift and RxScrollViewDelegateProxy.swift
Though the code above works only for protocols without any return type example method I used above func scrollViewShouldScrollToTop() has no return type associated with it. I could not imagine how could I use the DelegateProxy above to convert the protocol methods with return types, like numberOfRowsInSection which has Int as return type.
I happened to look at the RxDataSource implementation and realized in order to convert cellForRowAtIndexPath RxDataSource constructor expects you to pass the block as a init parameter and executes it whenever tableView calls cellForRowAtIndexPath in its proxyDelegate.
Now I could do the same thing if thats the only way out. Need to know is that how am supposed to code it or can I modify ProxyDelegate implementation above to convert the protocol method with return types.
I'm trying to send a Signal from one ViewModel to another one. Basically I want the second ViewModel to use the same Signal as the first ViewModel, but I also need the initial value at init state in the second ViewModel . So far I have manage to solve this by sending the Signal<Person, NoError> and the Person model.
struct Person {
let name: String
let age: Int
}
In PersonListViewModel the Signal is defined as output where the stream is handled.
protocol PersonListViewModelOutputs {
var goToPersonDetail: Signal<Person, NoError> { get }
}
PersonDetailViewModel:
protocol PersonDetailViewModelInputs {
func viewDidLoad()
func configureWith(personSignal: Signal<Person, NoError>, initialPerson: Person)
}
protocol PersonDetailViewModelOutputs {
var person: Signal<Person, NoError> { get }
}
protocol PersonDetailViewModelType {
var inputs: PersonDetailViewModelInputs { get }
var outputs: PersonDetailViewModelOutputs { get }
}
public final class PersonDetailViewModel: PersonDetailViewModelType, PersonDetailViewModelInputs, PersonDetailViewModelOutputs {
init(){
self.person = self.configureWithPersonPropery.signal.skipNil()
}
private let configureWithPersonProperty = MutableProperty<Person?>(nil)
func configureWith(personSignal: Signal<Person, NoError>, initialPerson: Person) {
configureWithPersonProperty.value = initialPerson
configureWithPersonProperty <~ personSignal.producer
}
}
However this solution seems to bring unnecessary parameter inside func configureWith(...) and I guess there could be a better way to solve it.
For example, is it possible to get the last emitted value from personSignal: Signal<Person, NoError> inside func configureWith(...) without sending the Person Struct?
Since your signal is NoError, you can just pass in a Property instance instead of a Signal. A property is basically a signal that is guaranteed to have a value and can't send an error. You can create one with an initial value and an existing signal:
let prop = Property(initial: initialPerson, then: signal)
I am currently having an issue with multiple network requests executing when using RxSwift Observables. I understand that if one creates a cold observable and it has multiple observers, the observable will execute its block each time it is subscribed to.
I have tried to create a shared subscription observable that executes the network request once, and multiple subscribers will be notified of the result. Below is the what I have tried.
Sequence of events
Create the view model with the tap event of a uibutton
Create the serviceStatus Observable as a public property on the view model. This Observable is mapped from the buttonTapped Observable. It then filters out the "Loading" status. The returned Observable has a shareReplay(1) executed on it to return a shared subscription.
Create the serviceExecuting Observable as a public property on the view model. This observable is mapped from the serviceStatus Observable. It will return true if the status is "Loading"
Bind the uilabel to the serviceStatus Observable
Bind the activity indicator to the serviceExecuting Observable.
When the button is tapped, the service request is executed three time where I would be expecting it to be executed only once. Does anything stand out as incorrect?
Code
class ViewController {
let disposeBag = DisposeBag()
var button: UIButton!
var resultLabel: UILabel!
var activityIndicator: UIActivityIndicator!
lazy var viewModel = { // 1
return ViewModel(buttonTapped: self.button.rx.tap.asObservable())
}
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.serviceStatus.bindTo(self.resultLabel.rx_text).addDispsoableTo(disposeBag) // 4
self.viewModel.serviceExecuting.bindTo(self.activityIndicator.rx_animating).addDispsoableTo(disposeBag) // 5
}
}
class ViewModel {
public var serviceStatus: Observable<String> { // 2
let serviceStatusObseravble = self.getServiceStatusObservable()
let filtered = serviceStatusObseravble.filter { status in
return status != "Loading"
}
return filtered
}
public var serviceExecuting: Observable<Bool> { // 3
return self.serviceStatus.map { status in
return status == "Loading"
}
.startWith(false)
}
private let buttonTapped: Observable<Void>
init(buttonTapped: Observable<Void>) {
self.buttonTapped = buttonTapped
}
private func getServiceStatusObservable() -> Observable<String> {
return self.buttonTapped.flatMap { _ -> Observable<String> in
return self.createServiceStatusObservable()
}
}
private func createServiceStatusObservable() -> Observable<String> {
return Observable.create({ (observer) -> Disposable in
someAsyncServiceRequest() { result }
observer.onNext(result)
})
return NopDisposable.instance
})
.startWith("Loading")
.shareReplay(1)
}
EDIT:
Based on the conversation below, the following is what I was looking for...
I needed to apply a share() function on the Observable returned from the getServiceStatusObservable() method and not the Observable returned from the createServiceStatusObservable() method. There were multiple observers being added to this observable to inspect the current state. This meant that the observable executing the network request was getting executed N times (N being the number of observers). Now every time the button is tapped, the network request is executed once which is what I needed.
private func getServiceStatusObservable() -> Observable<String> {
return self.buttonTapped.flatMap { _ -> Observable<String> in
return self.createServiceStatusObservable()
}.share()
}
.shareReplay(1) will apply to only one instance of the observable. When creating it in createServiceStatusObservable() the sharing behavior will only affect the one value returned by this function.
class ViewModel {
let serviceStatusObservable: Observable<String>
init(buttonTapped: Observable<Void>) {
self.buttonTapped = buttonTapped
self.serviceStatusObservable = Observable.create({ (observer) -> Disposable in
someAsyncServiceRequest() { result in
observer.onNext(result)
}
return NopDisposable.instance
})
.startWith("Loading")
.shareReplay(1)
}
private func getServiceStatusObservable() -> Observable<String> {
return self.buttonTapped.flatMap { [weak self] _ -> Observable<String> in
return self.serviceStatusObservable
}
}
}
With this version, serviceStatusObservable is only created once, hence it's side effect will be shared everytime it is used, as it is the same instance.