4

I'm trying to periodically (every 10 seconds) call an API that returns a Json object of model :

struct MyModel { 
   var messagesCount: Int?
   var likesCount: Int?
}

And update the UI if messageCount or likesCount value changes. I tried the Timer solution but i find it a little bit messy and i want a cleaner solution with RxSwift and RxAlamofire.

Any help is highly appreciated as i'm new to Rx.

Dr34m3r
  • 53
  • 1
  • 3
  • Only when app is in active state? Because it would not work otherwise. – TheTiger Sep 13 '18 at 10:45
  • you can use `timer` from `RxSwift Observable`. e.g `Observable.timer(0, period: 5, scheduler: MainScheduler.instance)` – kathayatnk Sep 13 '18 at 10:45
  • @TheTiger yes when the app is active – Dr34m3r Sep 13 '18 at 11:17
  • @kathayatn do you refer to an RxAlamofire request when creating the observable ? i am not sure that you can apply the `Timer` method on it. – Dr34m3r Sep 13 '18 at 11:22
  • @Dr34m3r I'm not familiar with `RxSwift` but is it possible to call a recursive function after delay of 10 seconds? It will work in this case. – TheTiger Sep 13 '18 at 11:28

2 Answers2

8

Welcome to StackOverflow!

There's quite a lot of operators required for this, and I would recommend to look them up on the ReactiveX Operator page, which I check every time I forget something.

First off, ensure MyModel conforms to Decodable so it can be constructed from a JSON response (see Codable).

let willEnterForegroundNotification = NotificationCenter.default.rx.notification(.UIApplicationWillEnterForeground)
let didEnterBackgroundNotification = NotificationCenter.default.rx.notification(.UIApplicationDidEnterBackground)

let myModelObservable = BehaviorRelay<MyModel?>(value: nil)

willEnterForegroundNotification
    // discard the notification object
    .map { _ in () }
    // emit an initial element to trigger the timer immediately upon subscription
    .startWith(())
    .flatMap { _ in 
        // create an interval timer which stops emitting when the app goes to the background
        return Observable<Int>.interval(10, scheduler: MainScheduler.instance)
            .takeUntil(didEnterBackgroundNotification)
    }
    .flatMapLatest { _ in 
        return RxAlamofire.requestData(.get, yourUrl)
            // get Data object from emitted tuple
            .map { $0.1 } 
            // ignore any network errors, otherwise the entire subscription is disposed
            .catchError { _ in .empty() } 
    } 
    // leverage Codable to turn Data into MyModel
    .map { try? JSONDecoder().decode(MyModel.self, from: $0) } }
    // operator from RxOptional to turn MyModel? into MyModel
    .filterNil() 
    .bind(to: myModelObservable)
    .disposed(by: disposeBag)

Then, you can just continue the data stream into your UI elements.

myModelObservable
    .map { $0.messagesCount }
    .map { "\($0) messages" }
    .bind(to: yourLabel.rx.text }
    .disposed(by: disposeBag)

I didn't run this code, so there might be some typos/missing conversions in here, but this should point you in the right direction. Feel free to ask for clarification. If are really new to Rx, I recommend going through the Getting Started guide. It's great! Rx is very powerful, but it took me a while to grasp.

Edit

As @daniel-t pointed out, the background/foreground bookkeeping is not necessary when using Observable<Int>.interval.

CloakedEddy
  • 1,965
  • 15
  • 27
  • 1
    You don't actually need the foreground background stuff. The timer will automatically stop when the app is in the background and restart when it comes back to the foreground. Otherwise, good job. – Daniel T. Sep 13 '18 at 21:07
  • That was exactly what i needed, thank you alot. And by the observing the Application's state (foreground, background) is necessary in my case as the with code that @CloakedEddy provided the ReplaySubject emitted all the events skipped while the app was on background. With Daniel's solution, the timer is not fired when the app is on background and doesn't flood the observer with tons of events on foreground. – Dr34m3r Sep 14 '18 at 15:31
6

CloakedEddy got real close with his answer and deserves upvotes. However he made it a little more complex than necessary. Interval uses a DispatchSourceTimer internally which will automatically stop and restart when the app goes to the background and comes back to the foreground. He also did a great job remembering to catch the error to stop the stream from unwinding.

I'm assuming the below code is in the AppDelegate or a high level Coordinator. Also, myModelSubject is a ReplaySubject<MyModel> (create it with: ReplaySubject<MyModel>.create(bufferSize: 1) that should be placed somewhere that view controllers have access to or passed down to view controllers.

Observable<Int>.interval(10, scheduler: MainScheduler.instance) // fire at 10 second intervals.
    .flatMapLatest { _ in
        RxAlamofire.requestData(.get, yourUrl) // get data from the server.
            .catchError { _ in .empty() }   // don't let error escape.
    }
    .map { $0.1 } // this assumes that alamofire returns `(URLResponse, Data)`. All we want is the data.
    .map { try? JSONDecoder().decode(MyModel.self, from: $0) } // this assumes that MyModel is Decodable
    .filter { $0 != nil } // filter out nil values
    .map { $0! } // now that we know it's not nil, unwrap it.
    .bind(to: myModelSubject) // store the value in a global subject that view controllers can subscribe to.
    .disposed(by: bag) // always clean up after yourself.
Daniel T.
  • 32,821
  • 6
  • 50
  • 72