Periodically call an API with RxSwift
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
.
Make a network call every 10 seconds with RxSwift
There are a couple of problems with your Observable.create closure. You have to make sure that something is sent to the observer
in every path, otherwise the Observable will call the function and then not emit anything and you will not know why.
Also, you want to minimize the amount of logic being performed in the closure passed to create. You are doing way too much in there.
So let's simplify the code in the create closure as much as possible first:
extension RestManager {
func rx_makeRequest(withEndPoint endPoint: String, withHttpMethod method: HttpMethod) -> Observable<(response: MyHTTPURLResponse, data: Data)> {
Observable.create { observer in
self.makeRequest(withEndPoint: endPoint, withHttpMethod: method) { result in
if let response = result.response, let data = result.data {
observer.onNext((response, data))
observer.onCompleted()
}
else {
observer.onError(result.error ?? RxError.unknown)
}
}
return Disposables.create() // is there some way of canceling a request? If so, it should be done here.
}
}
}
This does the bare minimum. Just wraps the underlying callback and nothing else. Now your fetchMarkets
call is much simpler:
class MarketService: MarketServiceProtocol {
func fetchMarkets() -> Observable <[Market]> {
return RestManager.shared.rx_makeRequest(withEndPoint: "market/v2/get-summary?region=US", withHttpMethod: .get)
.do(onNext: { result in
guard 200...299 ~= result.response.httpStatusCode
else { throw URLError.httpRequestFailed(response: result.response, data: result.data) }
})
.map { try JSONDecoder().decode(MarketResult.self, from: $0.data).marketSummaryAndSparkResponse.markets }
}
}
Now to the meat of your question. How to make the network call every 10 seconds... Just wrap your network call in a flatMap like this:
Observable<Int>.interval(.seconds(10), scheduler: MainScheduler.instance)
.flatMapLatest { _ in
viewModel.fetchMarketViewModels()
}
.observe(on: MainScheduler.instance)
.bind(to: tableView.rx.items(cellIdentifier: HomeTableViewCell.cellIdentifier)) { index, viewModel, cell in
guard let cell = cell as? HomeTableViewCell else { return }
cell.setupData(viewModel: viewModel)
}
.disposed(by: self.disposableBag)
Learn more about flatMap
and its variants from this article.
Updating periodically with RxSwift
This is very similar to this question/answer.
You should use timer
and then flatMapLatest
:
Observable<Int>.timer(0, period: 20, scheduler: MainScheduler.instance)
.flatMapLatest { _ in
provider.request(.Issue)
}
.mapArray(Issue.self, keyPath: "issues")
// ...
Update view after async API call with RxSwift
First, create a generic return object to wrap communication errors.
enum APIResult<T> {
case success(T)
case error(Error)
}
Then, convert your completion handler to return an Observable
:
func getFoods() -> Observable<APIResult<[FoodType]>> {
return Observable<APIResult<[FoodType]>>.create { observer -> Disposable in
self.foodService.getAll(completionHandler: { result in
switch result {
case .Success(let foods):
observer.onNext(.success(foods))
break
case .Failure(let error):
observer.onNext(.error(error))
break
}
observer.onCompleted()
return Disposables.create()
})
}
}
Now simply process the observable as any other in RxSwift.
getFoods().subscribe(onNext: { result in
switch result {
case .success(let foods):
print("Received foods: \(foods)")
break
case .error(let error):
print("Received error: \(error)")
break
}
}.addDisposableTo(disposeBag)
Using these utility classes will help you mapping success results and split error and success signals to different observables. For example:
let foodsRequest = getFoods().splitSuccess
foodsRequest.error.subscribe(onNext: { error in
print("Received error: \(error)")
})
foodsRequest.success.subscribe(onNext: { foods in
print("Received foods: \(foods)")
}
You can also convert Realm objects to RxSwift observables:
let realm = try! Realm()
realm.objects(Lap).asObservable()
.subscribeNext {[weak self] laps in
self?.tableView.reloadData()
}
Take a look at Using Realm Seamlessly in an RxSwift App for more information and examples.
RxSwift repeated action
Use flatMap:
var loop: Observable<Element> {
return Observable<Int>.interval(5.0, scheduler: MainScheduler.instance).flatMap { _ in
return networkRequest() // returns Observable<Element>
}
}
Correct way to restart observable interval in RxSwift
What you're looking for is merge
. You have two Observable
s, one of which is an interval
and the other which represents preference changes. You want to merge
those into one Observable
with the elements from both, immediately as they come.
That would look like this:
// this should really come from somewhere else in your app
let preferencesChanged = PublishSubject<Void>()
// the `map` is so that the element type goes from `Int` to `Void`
// since the `merge` requires that the element types match
let timer = Observable<Int>.timer(0, period: 3, scheduler: MainScheduler.instance).map { _ in () }
Observable.of(timer, preferencesChanged)
.merge()
.flatMapLatest(makeRequest)
.subscribeNext(setSummary)
.addDisposableTo(disposeBag)
Also notice how I'm using timer
instead of interval
, since it allows us to specify when to fire for the first time, as well as the period for subsequent firings. That way, you don't need a startWith
. However, both ways work. It's a matter of preference.
One more thing to note. This is outside of the scope of your question (and maybe you kept it simple for the sake of the question) but instead of subscribeNext(setSummary)
, you should consider keeping the result as an Observable and instead bindTo
or drive
the UI or DB (or whatever "summary" is).
Related Topics
How to Fill a Circle Color by Percentage Value
Convenience Initialization of Uinavigationcontroller Subclass Makes Subclass Constant Init Twice
My Uiviews Muck-Up When I Combine Uipangesturerecognizer and Autolayout
How to Cache Images Only in Disk Using Kingfisher
How to Retrieve Image Stored in Firebase to Show It in View Image View
Get Latitude and Longitude Center of Google Map
Libmobilegestalt Mobilegestalt.C:890: Mgisdeviceoneoftype Is Not Supported on This Platform
Swift Lazy and Optional Properties
How to Test If "Allow Full Access" Permission Is Granted from Containing App
Swift, Parse.Com: How to Pass Data from Query
Cmpedometer Querypedometerdatafromdate Returns Error 103
Turning on Thread Sanitizer Results in Signal Sigabrt
Start/Stop Image View Rotation Animations
Use More Than One Firebase Database in Single App - Swift
Receipt Validation on iOS In-App-Purchase Returns Multiple Transaction
How Is This Slide-Up Menu from the iPhone Messages App Implemented
iOS Swift. How to Get a Gps Metadata from Just a Uiimage (Nsurl)