Periodically call an API with RxSwift

4.7k views Asked by At

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.

2

There are 2 answers

2
CloakedEddy On BEST ANSWER

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.

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

0
Daniel T. On

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.