Say I have 2 functions with 2 different Observable return types :
func getWatchedMovies() -> Observable<[TraktMovie]>
func getDetails(id: Int, language: String) - > Observable<TMDbMovie>
I'd like to flatMap each value in my getWatchedMovies() request to be able to request the details of each movie like this (I'm not sure it's the best way to do it though..)
traktDataManager?
.getWatchedMovies()
.flatMap({ (traktMovies) -> Observable<[TraktMovie]> in
let moviesObs = Observable.from(traktMovies)
let movieDetails = moviesObs.flatMap {
self.tmdbDataManager!.getMovieDetails(id: $0.ids.tmdb, language: Device.lang)
}
})
The thing is, I need to add each TraktMovie to Realm AND update a TraktMovie property, named tmdbMovie, with the nested request value of type TMDbMovie in Realm too.
What I mean is :
first, I need to loop in my [TraktMovie] array to save each value of it in Realm (say an object named traktMovie)
for traktMovie in traktMovies {
let realm = try! Realm()
realm.write {
realm.add(traktMovie)
}
}
second, I need to retrieve the details of each TraktMovie object with the second request (e.g. getDetails(_ , _)) : with something like flatMap ?
third, I need to update each traktMovie object property as follow with the value retrieved with the getDetails request (say tmdbMovie for the retrieved value):
traktMovie.setValue(tmdbMovie, forKeyPath: "tmdbMovie")
Here I have an object retrieved from the first request(getWatchedMovies()) named traktMovie and I update one of its property named tmdbMovie with the object retrieved from the second request (getDetails(_, _)) also named tmdbMovie
The thing is my first request returns an array and the second only a single object.
If I return the TMDbMovie object, I got only one object with onNext event and I loose my [TraktMovie] array.
Hope I'm clear enough.
Help is really appreciated ! ๐
You can try to use Observable.zip for this as in example below:
getWatchedMovies()
.flatMap({ [unowned self] (traktMovies) -> Observable<[TraktMovie]> in
let movieDetails = traktMovies.flatMap { movie in
// you can save in realm here
return Observable.just(movie)
.withLatestFrom(self.getMovieDetails(id: 0, language: "")) { movie, details in
// here you have both movie & movieDetails
return movie
}
}
return Observable.zip(movieDetails, { return $0 })
})
It may be a bit risky, if one of getMovieDetails will fail it will fail whole stream, as well it will require all getMovieDetails to emit onNext event in order that zipped Observable to emit a value.
Related
I have a global var notes: Set<Note> that contains notes initialized with downloaded data.
In the code below, does Swift know to skip the initialization of my Note object if notes already contains it?
for dictionary in downloadedNoteDictionaries {
let note = Note(dictionary: dictionary)
notes.insert(note)
}
I'm wondering because my app downloads dozens of notes per request and initializing a Note object seems rather computationally expensive.
If the answer to my question is no, then how could I improve my code's performance?
My Note classโwhich I just realized should probably be a struct insteadโhas the property let id: Int64 as its sole essential component, but apparently, you can't access an element of a set by its hash value? I don't want to use Set's instance method first(where:) because it has a complexity of O(n), and notes could contain millions of Note objects.
You cannot rely on Swift to eliminate the construction of a new Note in your code. Your Set needs to ask the Note for its hashValue, and may need to call == with your Note as an argument. Those computations require the Note object. Possibly if Swift can inline everything, it can notice that your hashValue and == depend only on the id property, but it is certainly not guaranteed to notice or to act on that information.
It sounds like you should be using an [Int64: Note] instead of a Set<Note>.
No, Swift will not avoid creating the new Note object. The problem here is trying to determine if an object already exists in a set. In order to check if an object already exists in the set, you must have some way to identify this object consistently and have that identification persist across future reads and writes to that set. Assuming we want to adopt Swift's hashing enhancements which deprecates the old methods for having to manually provide a hashValue for any object conforming to the Hashable, we should not use a Set as a solution to this problem. The reason is because Swift's new recommended hashing methods use a random seed to generate hashes for added security. Depending on hash values to identify an element in a set alone would therefore not be possible.
It seems that your method of identifying these objects are by an id. I would do as Rob suggests and use a dictionary where the keys are the id. This would help you answer existential questions in order avoid instantiating a Note object.
If you need the resulting dictionary as a Set, you can still create a new Set from this resulting dictionary sequence.
It is possible to pull out a particular element from a set as long as you know how to identify it, and the operations would be O(1). You would use these two methods:
https://developer.apple.com/documentation/swift/set/2996833-firstindex
https://developer.apple.com/documentation/swift/set/2830801-subscript
Here is an example that I was able to run in a playground. However, this assumes that you have a way to identify in your data / model the id of the Note object beforehand in order to avoid creating the Note object. In this case, I am assuming that a Note shares an id with a Dictionary it holds:
import UIKit
struct DictionaryThatNoteUses {
var id: String
init(id: String = UUID().uuidString) {
self.id = id
}
}
struct Note: Hashable {
let dictionary: DictionaryThatNoteUses
var id: String {
return dictionary.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Note, rhs: Note) -> Bool {
return lhs.id == rhs.id
}
}
var downloadedNoteDictionaries: [DictionaryThatNoteUses] = [
DictionaryThatNoteUses(),
DictionaryThatNoteUses(),
DictionaryThatNoteUses()
]
var notesDictionary: [String: Note] = [:]
var notesSet: Set<Note> = []
// Add a dictionary with the same id as the first dictionary
downloadedNoteDictionaries.append(downloadedNoteDictionaries.first!) // so now there are four Dictionary objects in this array
func createDictionaryOfNotes() {
func avoidCreatingDuplicateNotesObject(with id: String) {
print("avoided creating duplicate notes object with id \(id)") // prints once because of the duplicated note at the end matching the first, so a new Note object is not instantiated
}
downloadedNoteDictionaries.forEach {
guard notesDictionary[$0.id] == nil else { return avoidCreatingDuplicateNotesObject(with: $0.id) }
let note = Note(dictionary: $0)
notesDictionary[note.id] = note
}
}
createDictionaryOfNotes()
// Obtain a set for set operations on those unique Note objects
notesSet = Set<Note>(notesDictionary.values)
print("number of items in dictionary = \(notesDictionary.count), number of items in set = \(notesSet.count)") // prints 3 and 3 because the 4th object was a duplicate
// Grabbing a specific element from the set
// Let's test with the second Note object from the notesDictionary
let secondNotesObjectFromDictionary = notesDictionary.values[notesDictionary.values.index(notesDictionary.values.startIndex, offsetBy: 1)]
let thirdNotesObjectFromDictionary = notesDictionary.values[notesDictionary.values.index(notesDictionary.values.startIndex, offsetBy: 2)]
if let secondNotesObjectIndexInSet = notesSet.firstIndex(of: secondNotesObjectFromDictionary) {
print("do the two objects match: \(notesSet[secondNotesObjectIndexInSet] == secondNotesObjectFromDictionary)") // prints true
print("does the third object from dictionary match the second object from the set: \(thirdNotesObjectFromDictionary == notesSet[secondNotesObjectIndexInSet])") // prints false
}
When retrieving data from Firebase, we typically use the code below (or some reiteration of it)
...observeSingleEvent(of: .value, with: { snapshot in
var tempPosts = [PostModel]()
for child in snapshot.chidren{
}
return tempPosts
})
but I don't exactly get what this code is doing? Can someone explain it on a high level? I've tried printing data on mulitple spots but the only data I'm getting is: Snap post or [App.PostModel]
This code is used for observing data change in your database. You don't need to send requests from time to time for getting the latest data.
When the data changes, it will trigger the closure you given so that you can do things. For more reference, you could read this doc.
You could try this to covert snapshot into dictionary:
for child in snapshot.children {
let dataS = child as! DataSnapshot
let dict = dataS.value as? [String : AnyObject]
// handle the data
}
The code in your question uses .observeSingleEvent. What that means is that it's requesting data from Firebase immediately one time and will not observe any future changes or fire any other events.
The data is returned in in the closure as a 'snapshot' and is a 'picture' of what that data looks like at a point in time. (snapshot...picture? Pretty snappy huh)
Firebase data is only valid within the closure; any code following the closure will execute before Firebase has time to retrieve data from the server, so ensure you work with Firebase data inside that closure.
The for loop iterates over the child nodes within the snaphot one at a time. For example, the snapshot could contain child snapshots of each user in a /users node. You can then get the users data from each child snapshot.
The return statement should never be used within a asynchronous closure as you cannot return data (in that fashion) from a closure, so that line should be removed. You could however leverage an completion handler like this
func getUser(with userID: String, completion: #escaping ((_ user: UserClass) -> Void)) {
//get the user info from the snapshot, create a user object and pass it back
// via the completion
completion(user)
}
to work with the data outside the closure.
I'm fairly new to RxSwift and have been banging my head against the following problem for two days now.
I wrapped a closure that reads a partial JSON formatted string from an API:
func readResult() -> Observable<String> {
return Observable<String>.create { observable -> Disposable in
API.readValue() { (result: Result<String>) in
switch result {
case .success(let value): observable.onNext(value)
case .failure(let error): observable.onError(error)
}
observable.onCompleted()
}
return Disposables.create()
}
}
As the result from readValue only contains a chunk of the JSON formatted string, and I need to recursively call this method to get the full string. Therefore, it is important to start a new reading only when the previous one has finished.
I tried using an Observable.timer and scan to accumulate the results until I can successfully decode the json, but using a timer does not guarantee that the previous reading finished.
I also thought about using concat but as I don't know the length of the full JSON string in advance, I cannot write something like this:
Observable.concat(readResult(), readResult())
How could I ensure that the readResult function gets called until I can successfully decode the resulting JSON string?
In principle, .reduce() should be the right tool for the job.
Not sure why are you building Observable from scratch the hard way instead of using .from() factory method.
I would probably do it as follows (pseudocode):
let subject = PublishSubject<Observable<String>>.create()
let result = subject.switchLatest().reduce { /* update result */ }
subject.onNext(
Observable.from( /* service call */ ).subscribeOn( /* some serial scheduler */ )
) // repeat as needed
UPDATE
See the more specific solution in comments.
I learn the sample code in RxSwift. In the file GithubSignupViewModel1.swift, the definition of validatedUsername is:
validatedUsername = input.username //the username is a textfiled.rx_text
.flatMapLatest { username -> Observable<ValidationResult> in
print("-------->1:")
return validationService.validateUsername(username)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(.Failed(message: "Error contacting server"))
}
.shareReplay(1)
the validateUsername method is finally called the following method:
func usernameAvailable(username: String) -> Observable<Bool> {
// this is ofc just mock, but good enough
print("-------->2:")
let URL = NSURL(string: "https://github.com/\(username.URLEscaped)")!
let request = NSURLRequest(URL: URL)
return self.URLSession.rx_response(request)
.map { (maybeData, response) in
print("-------->3:")
return response.statusCode == 404
}
.catchErrorJustReturn(false)
}
Here is my confusion:
whenever I input a character quickly in the username textfield, message -------->1:, -------->2: showed, and a little later message -------->3: showed, but only showed one -------->3: message.
When I input characters slower, message -------->1:, -------->2:, -------->3: showed successively.
But when I change the flatMapLatest to flatMap, how many characters I input, I will get the same number of -------->3: message.
So how did the flatMapLatest work here?
How the flatMapLatest filter the early response from NSURLResponse ?
I read some about the flatMapLatest, but none of them will explain my confusion.
What I saw is something like:
let a = Variable(XX)
a.asObservable().flatMapLatest(...)
When changed a.value to another Variable, the Variable(XX) will not influence the subscriber of a.
But the input.username isn't changed, it is always a testfield.rx_text! So how the flatMapLatest work?
TheDroidsOnDroid's answer is clear for me:
FlatMapLatest diagram
Well, flatMap() gets one value, then performs long task, and when it
gets the next value, previous task will still finish even when the new
value arrives in the middle of the current task. It isnโt really what
we need because when we get a new text in the search bar, we want to
cancel the previous request and start another. Thatโs what
flatMapLatest() does.
http://www.thedroidsonroids.com/blog/ios/rxswift-examples-3-networking/
You can use RxMarbles app on Appstore to play around with operators.
It's not clear what your confusion is about. Are you questioning the difference between flatMap and flatMapLatest? flatMap will map to a new Observable, and if it needs to flatMap again, it will in essence merge the two mapped Observables into one. If it needs to flatMap again, it will merge it again, etc.
With flatMapLatest, when a new Observable is mapped, it overwrites the last Observable if there was one. There is no merge.
EDIT:
In response to your comment, the reason you aren't seeing any "------>3:" print is because those rx_request Observables were disposed before they could compete, because flatMapLatest received a new element, and this mapped to a new Observable. Upon disposal, rx_request probably cancels the request and will not run the callback where you're printing. The old Observable is disposed because it no longer belongs to anyone when the new one takes its place.
I find this https://github.com/ReactiveX/RxSwift/blob/master/Rx.playground/Pages/Transforming_Operators.xcplaygroundpage/Contents.swift to be useful
Transforms the elements emitted by an Observable sequence into Observable sequences, and merges the emissions from both Observable sequences into a single Observable sequence. This is also useful when, for example, when you have an Observable sequence that itself emits Observable sequences, and you want to be able to react to new emissions from either Observable sequence. The difference between flatMap and flatMapLatest is, flatMapLatest will only emit elements from the most recent inner Observable sequence.
let disposeBag = DisposeBag()
struct Player {
var score: Variable<Int>
}
let ๐ฆ๐ป = Player(score: Variable(80))
let ๐ง๐ผ = Player(score: Variable(90))
let player = Variable(๐ฆ๐ป)
player.asObservable()
.flatMap { $0.score.asObservable() } // Change flatMap to flatMapLatest and observe change in printed output
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
๐ฆ๐ป.score.value = 85
player.value = ๐ง๐ผ
๐ฆ๐ป.score.value = 95 // Will be printed when using flatMap, but will not be printed when using flatMapLatest
๐ง๐ผ.score.value = 100
With flatMap, we get
80
85
90
95
100
With flatMapLatest, we get
80
85
90
100
In this example, using flatMap may have unintended consequences. After
assigning ๐ง๐ผ to player.value, ๐ง๐ผ.score will begin to emit
elements, but the previous inner Observable sequence (๐ฆ๐ป.score) will
also still emit elements. By changing flatMap to flatMapLatest, only
the most recent inner Observable sequence (๐ง๐ผ.score) will emit
elements, i.e., setting ๐ฆ๐ป.score.value to 95 has no effect.
flatMapLatest is actually a combination of the map and switchLatest
operators.
Also, I find https://www.raywenderlich.com/158205/rxswift-transforming-operators this to be useful
flatMap
keeps up with each and every observable it creates, one for each element added onto the source observable
flatMapLatest
What makes flatMapLatest different is that it will automatically switch to the latest observable and unsubscribe from the the previous one.
I think this diagram from Ray Wenderlich tutorial can help.
So if we think about an event being emitted from a search box as the user types each time an event is received flatMap would fire off a separate request even if a current request is in flight. flatMapLatest in contrast disposes of the first stream. In the wrapper around URLSession this calls cancel on the request. So if you type really quickly you should see fewer requests returning. There's a brilliant video explaining just this here: https://youtu.be/z8ukiv5flcw . Here's the source to the wrapper around URLSession (notice task.cancel on dispose):
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
// smart compiler should be able to optimize this out
let d: Date?
if URLSession.rx.shouldLogRequest(request) {
d = Date()
}
else {
d = nil
}
let task = self.base.dataTask(with: request) { data, response, error in
if URLSession.rx.shouldLogRequest(request) {
let interval = Date().timeIntervalSince(d ?? Date())
print(convertURLRequestToCurlCommand(request))
#if os(Linux)
print(convertResponseToString(response, error.flatMap { $0 as NSError }, interval))
#else
print(convertResponseToString(response, error.map { $0 as NSError }, interval))
#endif
}
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
I tried to make a serial queue for network operations with GCD like this:
let mySerialQueue = dispatch_queue_create("com.myApp.mySerialQueue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0))
func myFunc() {
dispatch_async(mySerialQueue) {
do {
// Get object from the database if it exists
let query = PFQuery(className: aClass)
query.whereKey(user, equalTo: currentUser)
let result = try? query.getFirstObject()
// Use existing object or create a new one
let object = result ?? PFObject(className: aClass)
object.setObject(currentUser, forKey: user)
try object.save()
} catch {
print(error)
}
}
}
The code first looks for an existing object in the database.
If it finds one, it updates it. If it doesn't find one, it creates a new one. This is using the Parse SDK and only synchronous network functions (.getFirstObject, .save).
For some reason it seems that this is not executed serially, because a new object is sometimes written into the database, although one existed already that should have been updated only.
Am I missing something about the GCD?
From the documentation on dispatch_queue_attr_make_with_qos_class:
relative_priority: A negative offset from the maximum supported scheduler priority for the given quality-of-service class. This value must be less than 0 and greater than MIN_QOS_CLASS_PRIORITY
Therefore you should be passing in a value less than 0 for this.
However, if you have no need for a priority, you can simply pass DISPATCH_QUEUE_SERIAL into the attr argument when you create your queue. For example:
let mySerialQueue = dispatch_queue_create("com.myApp.mySerialQueue", DISPATCH_QUEUE_SERIAL)