RxSwift: Prevent multiple network requests - ios

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.

Related

Swift iOS ReactiveKit: calling the observer causes to trigger action multiple times?

I have Singleton class to which i have used to observe a property and trigger next action.
Singleton Class:
public class BridgeDispatcher: NSObject {
open var shouldRespondToBridgeEvent = SafePublishSubject<[String: Any]>()
open var shouldPop = SafePublishSubject<Void>()
open var shouldUpdate = SafePublishSubject<Void>()
public let disposeBag = DisposeBag()
open static let sharedInstance: BridgeDispatcher = BridgeDispatcher()
override init() {
super.init()
shouldRespondToBridgeEvent.observeNext { event in
if let type = event["type"] as? String {
switch type {
case "ShouldUpdate":
self.onShiftBlockDidUpdateHeight.next()
case "shouldPop":
self.onPopCurrentViewController.next(())
default:
print("Event not supported")
}
}
}.dispose(in: self.disposeBag)
}
}
Above method will trigger by calling:
BridgeDispatcher.sharedInstance.shouldRespondToBridgeEvent.next(body)
Register for onPopCurrentViewController:
BridgeDispatcher.sharedInstance.onPopCurrentViewController.observeNext { doSomething() }.dispose(in: BridgeDispatcher.sharedInstance.disposeBag)
On my application, BridgeDispatcher.sharedInstance.onPopCurrentViewController.observeNext{} method will be called multiple times due to the business logic, due to this doSomething() method will trigger multiple times when calling BridgeDispatcher.sharedInstance.shouldRespondToBridgeEvent.next(body).
Is this issue with my singleton design pattern or observeNext calling multiple times. (BridgeDispatcher.sharedInstance.onPopCurrentViewController.observeNext{} )
Need help.
I have used .updateSignal on ObservableComponent.
valueToUpdate.updateSignal.compactMap { (arg0) -> String? in
let (value, _, validationFailure) = arg0
return validationFailure == nil ? value?.value : nil
}
.removeDuplicates()
.debounce(for: 1.0)
.observeNext { [unowned self] _ in
self.doYourWork()
}
.dispose(in: self.bag)
It attempts to deal with the multiple calls in two ways: first by discarding any duplicate events, so if the duration hasn’t changed, then no call is made. Second, by debouncing the signal so if the user makes a bunch of changes we only call the method when they’re done making changes.

Prevent disposal of PublishSubject (RxSwift)

I'm struggling with specific use-case incorporating RxSwift's PublishSubject.
For sake of simplicity unimportant details were omitted.
There is a MVVM setup. In VC I have a UIButton, on tap of which a network call should dispatch. In ViewModel I have a buttonDidTapSubject: PublishSubject<Void>.
class ViewModel {
let disposeBag = DisposeBag()
let buttonDidTapSubject = PublishSubject<Void>()
let service: Service
typealias Credentials = (String, String)
var credentials: Observable<Credentials> {
return Observable.just(("testEmail", "testPassword"))
}
init(_ service: Service) {
self.service = service
buttonDidTapSubject
.withLatestFrom(credentials)
.flatMap(service.login) // login method has signature func login(_ creds: Credentials) -> Observable<User>
.subscribe(onNext: { user in print("Logged in \(user)") },
onError: { error in print("Received error") })
.disposed(by: disposeBag)
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
let button = UIButton()
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
}
In controller's viewDidLoad I make a binding:
override func viewDidLoad() {
button.rx.tap.asObservable()
.subscribe(viewModel.buttonDidTapSubject)
.disposed(by: disposeBag)
}
The problem is, since network request can fail and Observable that is returned from login(_:) method will produce an error, the whole subscription to buttonDidTapSubject in ViewModel will be disposed. And all other taps on a button will not trigger sequence to login in ViewModel.
Is there any way to avoid this kind of behavior?
You can use retry to prevent finishing the subcription. If you only want to retry in specific cases or errors you can also use retryWhen operator
In the view model:
lazy var retrySubject: Observable<Void> = {
return viewModel.buttonDidTapSubject
.retryWhen { error in
if (error == .networkError){ //check here your error
return .just(Void())
} else {
return .never() // Do not retry
}
}
}()
In the view controller I would have done it in another way:
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap.asObservable()
.flatMap { [weak self] _ in
return self?.viewModel.retrySubject
}
.subscribe(onNext: {
//do whatever
})
.disposed(by: disposeBag)
}
Not sure if still relevant - Use PublishRelay ( although it is RxCocoa )

How to properly send a Signal with an initial value using ReactiveCocoa?

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)

RxSwift network status observable

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)

REST API, Swift, Automatic Update

I'm currently struggling to find an easy-to-use programming approach/design pattern, which solves the following problem:
I've got an REST API where the iOS app can request the required data. The data is needed in different ViewControllers. But the problem is, that the data should "always" be up to date. So I need to set up a timer which triggers a request every 5-20 seconds, or sth like that. Everytime the data changes, the view needs to be updated (at the current viewcontroller, which is displayed).
I tried some stuff with delegation and MVC Pattern, but it's kind a messy. How is it done the right way?
In my current implementation I only can update the whole UICollectionView, not some specific cells, because I don't know how the data changed. My controller keeps track of the data from the api and updates only if the hash has changed (if data changed on the server). My models always holds the last fetched data.
It's not the perfect solution, in my opinion..
I also thought about models, that keep themselves up to date, to abstract or virtualise my Rest-API. In this case, my controller doesn't even know, that it isn't directly accessible data.
Maybe someone can help me out with some kind of programming model, designpattern or anything else. I'm happy about anything!
UPDATE: current implementation
The Controller, which handles all the data
import Foundation
import SwiftyJSON
import SwiftyTimer
class OverviewController {
static let sharedInstance = OverviewController()
let interval = 5.seconds
var delegate : OverviewControllerUpdateable?
var model : OverviewModel?
var timer : NSTimer!
func startFetching() -> Void {
self.fetchData()
timer = NSTimer.new(every: interval) {
self.fetchData()
}
timer.start(modes: NSRunLoopCommonModes)
}
func stopFetching() -> Void {
timer.invalidate()
}
func getConnections() -> [Connection]? {
return model?.getConnections()
}
func getConnectionsSlave() -> [Connection]? {
return model?.getConnectionsSlave()
}
func getUser() -> User? {
return model?.getUser()
}
func countConnections() -> Int {
if let count = model?.getConnections().count {
return count
}
return 0
}
func countConnectionsSlave() -> Int {
if let count = model?.getConnectionsSlave().count {
return count
}
return 0
}
func fetchData() {
ApiCaller.doCall(OverviewRoute(), completionHandler: { (data, hash) in
if let actModel = self.model {
if (actModel.getHash() == hash) {
//no update required
return
}
}
var connections : [Connection] = []
var connectionsSlave : [Connection] = []
for (_,connection):(String, JSON) in data["connections"] {
let connectionObj = Connection(json: connection)
if (connectionObj.isMaster == true) {
connections.append(connectionObj)
} else {
connectionsSlave.append(connectionObj)
}
}
let user = User(json: data["user"])
//model needs update
let model = OverviewModel()
model.setUser(user)
model.setConnections(connections)
model.setConnectionsSlave(connectionsSlave)
model.setHash(hash)
self.model = model
//prevent unexpectedly found nil exception
if (self.delegate != nil) {
self.delegate!.reloadView()
}
}, errorHandler: { (errors) in
}) { (progress) in
}
}
}
protocol OverviewControllerUpdateable {
func reloadView()
}
The model, which holds the data:
class OverviewModel {
var user : User!
var connections : [Connection]!
var connectionsSlave : [Connection]!
var connectionRequests : [ConnectionRequest]!
var hash : String!
...
}
And in the ViewController, I use it like this:
class OverviewVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, OverviewControllerUpdateable {
let controller = OverviewController.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
self.controller.delegate = self
self.controller.startFetching()
}
//INSIDE THE UICOLLECTIONVIEW DELEGATE METHODS
...
if let user : User = controller.getUser() {
cell.intervalTime = interval
cell.nameLabel.text = "Ihr Profil"
}
...
func reloadView() {
self.userCollectionView.reloadData()
}
}
You could use a Singleton object to fetch your data periodically, then post notifications (using NSNotificationCenter) when the data is updated. Each view controller dependent on the data would listen for these notifications, then reload UI based on the updated data.

Resources