Reputation: 53
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.
Upvotes: 4
Views: 4835
Reputation: 1995
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
.
Upvotes: 8
Reputation: 33979
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.
Upvotes: 6