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.
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 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.
Related
I am new to SwiftUI and Firebase and I am trying to build my first app. I am storing Game documents in Firestore and one of the fields is an array containing the user ids of the players as you can see in the image.
Game data structure
That being said, I am trying to list all games of a given user and have all the players listed in each one of the cells (the order is important).
In order to create the list of games in the UI I created a GameCellListView and a GameCellViewModel. The GameCellViewModel should load both the games and the array of users that correspond to the players of each game. However I am not being able to load the users to an array. I have to go through the players array and query the database for each Id and append to a User array; then I should be able to return this User array. Since I'm using a for loop, I can't assign the values to the array and then return it. I tried using map(), but I can't perform a query inside of it.
The goal is to load that "all" var with a struct that receives a game and its players GamePlayers(players: [User], game: Game)
It should look something like the code snippet below, but the users array always comes empty. This function runs on GameCellViewModel init.
I hope you can understand my problem and thank you in advance! Been stuck on this for 2 weeks now
func loadData() {
let userId = Auth.auth().currentUser?.uid
db.collection("games")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.games = querySnapshot.documents.compactMap { document in
do {
let extractedGame = try document.data(as: Game.self)
var user = [User]()
let users = extractedGame!.players.map { playerId -> [User] in
self.db.collection("users")
.whereField("uid", isEqualTo: playerId)
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
user = documents.compactMap { queryDocumentSnapshot -> User? in
return try? queryDocumentSnapshot.data(as: User.self)
}
}
return user
}
self.all.append(GamePlayers(players: users.first ?? [User](), game: extractedGame!))
return extractedGame
}
catch {
print(error)
}
return nil
}
}
}
}
There are a lot of moving parts in your code and so to isolate points of failure would require seeing additional code so just be aware of that upfront. That said, if you are relatively new to Firestore or Swift then I would strongly suggest you first get a handle on this function using basic syntax. Once you're comfortable with the ins and outs of async looping then I would suggest refactoring the code using more advanced syntax, like you have here.
Your function requires performing async work within each loop iteration (of each document). You actually need to do this twice, async work within a loop within a loop. Be sure this is what you really want to do before proceeding because there may be cleaner ways, which may include a more efficient NoSQL data architecture. Regardless, for the purposes of this function, start with the most basic syntax there is for the job which is the Dispatch Group in concert with the for-loop. Go ahead and nest these until you have it working and then consider refactoring.
func loadData() {
// Always safely unwrap the user ID and never assume it is there.
guard let userId = Auth.auth().currentUser?.uid else {
return
}
// Query the database.
db.collection("games").whereField("userId", isEqualTo: userId).order(by: "createdTime").addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
// We need to loop through a number of documents and perform
// async tasks within them so instantiate a Dispatch Group
// outside of the loop.
let dispatch = DispatchGroup()
for doc in querySnapshot.documents {
// Everytime you enter the loop, enter the dispatch.
dispatch.enter()
do {
// Do something with this document.
// You want to perform an additional async task in here,
// so fire up another dispatch and repeat these steps.
// Consider partitioning these tasks into separate functions
// for readability.
// At some point in this do block, we must leave the dispatch.
dispatch.leave()
} catch {
print(error)
// Everytime you leave this iteration, no matter the reason,
// even on error, you must leave the dispatch.
dispatch.leave()
// If there is an error in this iteration, do not return.
// Return will return out of the method itself (loadData).
// Instead, continue, which will continue the loop.
continue
}
}
dispatch.notify(queue: .main) {
// This is the completion handler of the dispatch.
// Your first round of data is ready, now proceed.
}
} else if let error = error {
// Always log errors to console!!!
// This should be automatic by now without even having to think about it.
print(error)
}
}
}
I also noticed that within the second set of async tasks within the second loop, you're adding snapshot listeners. Are you really sure you want to do this? Don't you just need a plain document get?
I'm dealing with a legacy libraries where I'm not at liberty to modify their code, and am trying to use Combine to weave them into something more easy to use. My situation is that a method call can either return a response, or a response and two notifications. The response-only is a success scenario, the response + 2 notifications is an error scenario. I want to combine both response and payload from the two notifications into an error that I can pass on to my app. The really fun thing is that I don't have a guarantee if the response or notifications come first, nor which of the notifications comes first. The notifications come in on a different thread than the response. The good thing is that they come in "just about the same time".
For handling a notification, I do
firstNotificationSink = notificationCenter.publisher(for: .firstErrorPart, object: nil)
.sink { [weak self] notification in
// parse and get information about the error
}
secondNotificationSink = notificationCenter.publisher(for: .secondErrorPart, object: nil)
.sink { [weak self] notification in
// parse and get more information about the error
}
and asking the legacy library for a response is:
func doJob() -> String {
let resultString = libDoStuff(reference)
}
Is there a way for me to use Combine to merge these three signals into one, given i.e. a 50ms timeframe? Meaning, if I get the result and two notifications, I have an error response I can pass on to my app, and if I have only the result and no notifications arrived in 50ms, then I can pass that success response to my app?
The part about combining the three signals is easy: use .zip. That's not very interesting. The interesting part of the problem is that you want a pipeline that signals whether a notification arrived within a certain time limit. Here's an example of how to do that (I'm not using your actual numbers, it's just a demo):
import UIKit
import Combine
enum Ooops : Error { case oops }
class ViewController: UIViewController {
var storage = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
print("start")
NotificationCenter.default.publisher(for: Notification.Name("yoho"))
.map {_ in true}
.setFailureType(to: Ooops.self)
.timeout(0.5, scheduler: DispatchQueue.main) { Ooops.oops }
.replaceError(with: false)
.sink {print($0)}
.store(in: &self.storage)
DispatchQueue.main.asyncAfter(deadline:.now()+0.2) {
NotificationCenter.default.post(name: Notification.Name("yoho"), object: self)
}
}
}
If the asyncAfter delay is 0.2, we get true (followed by false, but that's not important; we could change that if we wanted to). If the delay is 0.9, we get false. So the point is, the first value we get distinguishes correctly whether we got a signal in the required time.
Okay, so the rest is trivial: you just hook up your three signals with .zip, as I said before. It emits a tuple after all three publishers have emitted their first signal — and that's all the information you need, because you've got the result from the method call plus Bools that tell you whether the notifications arrived within the time limit. You can now read that tuple and analyze it, and do whatever you like. The .zip operator has a map function so you can emit the result of your analysis in good order. (If you wanted to transform the result of the map function into an error, that would require a further operator, but again, that's easy.)
Reformed question
I have reformed my question. To the common case.
I want to generate items with RxSwift in background thread (loading from disk, long-running calculations, etc.), and observe items in MainThread. And I want to be sure that no items will be delivered after dispose (from main thread).
According to documentation (https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#disposing):
So can this code print something after the dispose call is executed? The answer is: it depends.
If the scheduler is a serial scheduler (ex. MainScheduler) and dispose is called on the same serial scheduler, the answer is no.
Otherwise it is yes.
But in case of using subscribeOn and observerOn with different schedulers - we cannot guarantee that nothing will be emitted after dispose (manual or by dispose bag, it does not matter).
How should I generate items (images, for example) in background and be sure that result will not be used after the dispose?
I made workaround in real project, but I want to solve this problem and to understand how should we avoid it in the same cases.
In my test project I have used small periods - they demonstrate the problem perfectly!
import RxSwift
class TestClass {
private var disposeBag = DisposeBag()
private var isCancelled = false
init(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
assert(Thread.isMainThread)
load(longRunningTaskDuration: longRunningTaskDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + cancelAfter) { [weak self] in
self?.cancel()
}
}
private func load(longRunningTaskDuration: TimeInterval) {
assert(Thread.isMainThread)
// We set task not cancelled
isCancelled = false
DataService
.shared
.longRunngingTaskEmulation(sleepFor: longRunningTaskDuration)
// We want long running task to be executed in background thread
.subscribeOn(ConcurrentDispatchQueueScheduler.init(queue: .global()))
// We want to process result in Main thread
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { [weak self] (result) in
assert(Thread.isMainThread)
guard let strongSelf = self else {
return
}
if !strongSelf.isCancelled {
print("Should not be called! Task is cancelled!")
} else {
// Do something with result, set image to UIImageView, for instance
// But if task was cancelled, this method will set invalid (old) data
print(result)
}
}, onError: nil)
.disposed(by: disposeBag)
}
// Cancel all tasks. Can be called in PreapreForReuse.
private func cancel() {
assert(Thread.isMainThread)
// For test purposes. After cancel, old task should not make any changes.
isCancelled = true
// Cancel all tasks by creating new DisposeBag (and disposing old)
disposeBag = DisposeBag()
}
}
class DataService {
static let shared = DataService()
private init() { }
func longRunngingTaskEmulation(sleepFor: TimeInterval) -> Single<String> {
return Single
.deferred {
assert(!Thread.isMainThread)
// Enulate long running task
Thread.sleep(forTimeInterval: sleepFor)
// Return dummy result for test purposes.
return .just("Success")
}
}
}
class MainClass {
static let shared = MainClass()
private init() { }
func main() {
Timer.scheduledTimer(withTimeInterval: 0.150, repeats: true) { [weak self] (_) in
assert(Thread.isMainThread)
let longRunningTaskDuration: TimeInterval = 0.050
let offset = TimeInterval(arc4random_uniform(20)) / 1000.0
let cancelAfter = 0.040 + offset
self?.executeTest(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
}
}
var items: [TestClass] = []
func executeTest(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
let item = TestClass(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
items.append(item)
}
}
Call MainClass.shared.main() somewhere to start.
We call method to load some data and later we call cancel (all from Main Thread). After cancel we sometimes receive the result (in main thread too), but it is old already.
In real project TestClass is a UITableViewCell subclass and cancel method is called in prepareForReuse. Then cell is being reused and new data is set to the cell. And later we get the result of OLD task. And old image is set to the cell!
ORIGINAL QUESTION (OLD):
I would like to load image with RxSwift in iOS. I want to load image in background, and to use it in main thread. So I subscribeOn background thread, and observeOn main thread. And function will look like this:
func getImage(path: String) -> Single<UIImage> {
return Single
.deferred {
if let image = UIImage(contentsOfFile: path) {
return Single.just(image)
} else {
return Single.error(SimpleError())
}
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
}
But I get problems with cancelation. Because different schedulers are used to create items and to call dispose (disposing from main thread), subscription event can be raised after dispose is called. So in my case of using in UITableViewCell I receive invalid (old) image.
If I create item (load image) in the same scheduler that observes (Main thread), everything works fine!
But I would like to load images in background and I want it will be canceled after disposing (in prepareForReuse method or in new path set method). What is the common template for this?
EDIT:
I have created a test project, where I can emulate the problem when the event is received after dispose.
And I have one simple solution that works. We should emit items in the same scheduler. So we should capture scheduler and emit items there (after long running task completes).
func getImage2(path: String) -> Single<UIImage> {
return Single
.create(subscribe: { (single) -> Disposable in
// We captrure current queue to execute callback in
// TODO: It can be nil if called from background thread
let callbackQueue = OperationQueue.current
// For async calculations
OperationQueue().addOperation {
// Perform any long-running task
let image = UIImage(contentsOfFile: path)
// Emit item in captured queue
callbackQueue?.addOperation {
if let result = image {
single(.success(result))
} else {
single(.error(SimpleError()))
}
}
}
return Disposables.create()
})
.observeOn(MainScheduler.instance)
}
But it is not in Rx way. And I think this is not the best solution.
May be I should use CurrentThreadScheduler to emit items, but I cannot understand how. Is there any tutorial or example of items generation with schedulers usage? I did not find any.
Interesting test case. There is a small bug, it should be if strongSelf.isCancelled instead of if !strongSelf.isCancelled. Apart from that, the test case shows the problem.
I would intuitively expect that it is checked whether a dispose has already taken place before emitting, if it happens on the same thread.
I found additionally this:
just to make this clear, if you call dispose on one thread (like
main), you won't observe any elements on that same thread. That is a
guarantee.
see here: https://github.com/ReactiveX/RxSwift/issues/38
So maybe it is a bug.
To be sure I opened an issue here:
https://github.com/ReactiveX/RxSwift/issues/1778
Update
It seems it was actually a bug. Meanwhile, the fine people at RxSwift have confirmed it and fortunately fixed it very quickly. See the issue link above.
Testing
The bug was fixed with commit bac86346087c7e267dd5a620eed90a7849fd54ff. So if you are using CocoaPods, you can simply use something like the following for testing:
target 'RxSelfContained' do
use_frameworks!
pod 'RxAtomic', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
pod 'RxSwift', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
end
like the code below, when I want to return a pedometer data through a method for some connivence, but the method returned earlier than the data be retrieved.I think this maybe a concurrency issue. How could I return the data in a right way for future use?Thx
func queryPedometerTodayTotalData() -> Int {
var pedometerDataOfToday: CMPedometerData?
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate(), withHandler: { (pedometerData, error) in
pedometerDataOfToday = pedometerData!
print("this print after got data in a background thread:\(pedometerDataOfToday)")
})
print("This should print before last print, and there haven't got the data now: \(pedometerDataOfToday)")
return pedometerDataOfToday
}
You're right about it being a concurrency issue. You should use the result inside the handler of the queryPedometerDataFromDate.
One way of achieving this would be to use a completion block for your queryPedometerTodayTotalData method instead of having it return a value, like this:
func queryPedometerTodayTotalData(completion:((CMPedometerData?)->())) {
var pedometerDataOfToday: CMPedometerData?
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate(), withHandler: { (pedometerData, error) in
pedometerDataOfToday = pedometerData!
completion(pedometerData)
})
}
func testQueryPedometerTodayTotalData() {
self.queryPedometerTodayTotalData { (data) in
print(data)
}
}
It is a concurrency issue. queryPedometerDataFromDate is an asynchronous method. it executes the completion block whenever iOS deems it (which would usually be after it has retrieved the data) so that is why the 2nd print line prints first and doesnt return your result.
You need to either use the pedometerData from the completion block inside the completion block, or call a method/delegate that will handle the result of the completion block.
I'm dipping toes into RxSwift and would like to create a "streaming API" for one of my regular API calls.
My idea is to take the regular call (which already uses observables without any problems) and have a timer fire such calls and send the results on the same observable, so the view controller can update automatically, so instead of doing this (pseudocode follows):
func getLocations() -> Observable<[Location]> {
return Observable<[Location]>.create {
sink in
NSURLSession.sharedSession.rx_JSON(API.locationsRequest).map {
json in
return json.flatMap { Location($0) }
}
}
}
I'd like for this to happen (pseudocode follows):
func getLocations(interval: NSTimeInterval) -> Observable<[Location]> {
return Observable<[Location]>.create {
sink in
NSTimer(interval) {
NSURLSession.sharedSession.rx_JSON(API.locationsRequest).map {
json in
sink.onNext(json.flatMap { Location($0) })
}
}
}
}
Last thing I tried was adding an NSTimer to the mix, but I can't figure out how to take the reference to the sink and pass it around to the method called by the timer to actually send the events down the pipe, given that the handler for the timer must be on a standalone method. I tried throwing in the block timer extensions from BlocksKit but the timer was fired every second instead of being fired at the specified interval, which defeated the purpose.
I've also read about the Interval operator but I'm not sure it's the right way to go.
Any pointers on how to get this right?
The end goal would be to have the timer re-fire only after the previous call has finished (either success or fail).
You should do something like the code below:
func getLocations(interval: NSTimeInterval) -> Observable<[CLLocation]> {
return Observable<[CLLocation]>.create { observer in
let interval = 20.0
let getLocationDisposable = Observable<Int64>.interval(interval, scheduler: MainScheduler.instance)
.subscribe { (e: Event<Int64>) in
NSURLSession.sharedSession.rx_JSON(API.locationsRequest).map {
json in
observer.onNext(json.flatMap { Location($0) })
}
}
return AnonymousDisposable {
getLocationDisposable.dispose()
}
}
}
The code above fire every 20 seconds the API.locationsRequest and send the result on the same observable, Please note that you have to dispose the Interval when the maim observable dispose.