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.
Related
I'm quite new to Combine and, instead of running all my tasks into the viewModel, I'm trying to better isolate the code that has to do with business logic.
Let's take a SignIn service as example. The service receives username and password and return token and userID.
The exposed call of the service is signIn that internally calls a private func networkCall. I'd like to implement the two functions to return a Publisher.
The role of networkCall should be calling the API and storing the received token, while the role of signIn is only to return a success or a failure.
This is my code, where I'm also highlighting where I'm getting stuck.
In general I don't know where is the right place to work with the information received from the API (and store the token). At the moment I'm doing it inside a .map call but it sounds wrong to me. Could you share some advice to improve this logic and especially explain which is the right place to run the business logic... I'm supposing that .map is not the right place! and .sink will just stop the chain.
struct SignInResponse:Codable{
var token:String
var userID:String
}
class SignInService {
// Perform the API call
private func networkCall(with request:SignInRequest)->AnyPublisher<SignInResponse, ServiceError>{
return URLSession.DataTaskPublisher(request: request, session: .shared)
.decode(type: SignInResponse.self, decoder: JSONDecoder())
.mapError{error in return ServiceError.error}
.eraseToAnyPublisher()
}
func signIn(username:String, password:String)->AnyPublisher<Result<String, ServiceError>, Never>{
let request = SignInRequest(with username:username, password:password)
return networkCall(with: request)
.map{ (response) -> Result<String, ServiceError> in
if response.token != ""{
// THIS SOUNDS EXTREMELLY WRONG. I SHOULD NOT USE MAP TO HANDLE THE TOKEN -------
self.storage.save(key: "token", value: response.token)
return Result.success(response.userID)
}else{
return Result.failure(ServiceError.unknown)
}
}
.replaceError(with: Result.failure(ServiceError.unknown))
.eraseToAnyPublisher()
}
......
}
From the model I call SignIn in this way:
func requestsSignIn(){
if let username = username, let password = password{
cancellable = service.signIn(username: username, password: password)
.sink(receiveValue: { (result) in
switch result{
case .failure(let error):
self.errorMessage = error.localizedDescription
case .success(let userID):
// the sigin succeeded do something here
}
})
}
}
Basically I agree with the existing answer. Your misconception here seems to be what a Combine pipeline is for. The idea is that either a useful value β here, your user ID β or an error (if appropriate; otherwise, nothing) should pop out the end of the pipeline. The subscriber at the end of the pipeline stands ready to receive either of those.
Thus it generally makes no sense to pass a Result object out the end of the pipeline, which must be further analyzed into a success or failure value. The goal of a Result object is merely to allow you to pass asynchronicity around, i.e. by handing someone else a completion handler to be called with a Result at some future time, just so as not to have to call with one of two values, i.e. either a real value or an error, using two Optional parameters.
Once a Combine publisher has published, though, asynchronicity has already happened, and you're getting the signal of this fact; that's what publishing means. The only thing you now need to preserve is whatever part or mutation of the signal is meaningful and useful to you.
Here is a fairly typical pipeline that does the sort of thing you want to do; I have not divided this into two separate pieces as you do, but of course you can divide it up however you like:
URLSession.DataTaskPublisher(request: request, session: .shared)
.map {$0.data}
.decode(type: SignInResponse.self, decoder: JSONDecoder())
.tryMap { response -> String in
if response.token == "" {
throw ServiceError.unknown
}
return response.userID
}
.receive(on:DispatchQueue.main)
.sink(receiveCompletion: {err in /* do something with error */ },
receiveValue: {userID in /* do something with userID */})
.store(in:&storage)
First, the result of a data task is a tuple, but all we need is the data part, so we map to that. Then we decode. Then we check for an empty token, and throw if we get one; otherwise, we map down to the user ID because that is the only useful result. Finally we switch to the main thread and capture the output using a sink, and store the sink in the usual Set<AnyCancellable> so that it persists long enough for something to happen.
Observe that if at any stage along the way we suffer a failure error, that error is immediately propagated all the way out the end of the pipeline. If the data task fails, it will be a URLError. If the decoding fails, it will be an Error reporting the issue, as usual with a decoder. If the token isn't there, it will be a ServiceError. At any point along the way, of course, you can catch and block or transform the error as it comes down the line if you wish.
As an alternative setup have signIn return a publisher with just Output String and Failure type Service.Error directly (the Result type becomes redundant with a Publisher).
Then, for an error like an empty token string in the response, use tryMap instead of map to transform the Result type from network function and have it throw an ServiceEror.emptyToken or something like that. That will cause the publisher to publish that as the Failure right away.
I have the following function
func refreshFeedItems(completion: #escaping ActivityFeedCompletion) {
let currentTab = feedTab
//Result<([FeedItem], Bool)>) -> Void
// Load the cache in and start the spinner for the network request we're about to make
completion(.success(cache[currentTab], true))
ActivityFeedService.sharedInstance.refreshCommunityFeed(tab: currentTab) { result in
// A quick user might switch tabs before this
// call completes since we call completion twice
guard currentTab == self.feedTab else {
return
}
switch result {
case .failure(let error):
Log.warn(error)
completion(.failure(error))
case .success(let items):
self.cache[self.feedTab] = items
let tuple = Result.success(items,true) as ActivityFeedCompletion
completion((tuple,false))
}
}
}
But this line
completion(.success(cache[currentTab], true))
and this one
let tuple = Result.success(items,true) as ActivityFeedCompletion
Both throw me an "Extra argument in call" error.
This is my acticvity completion typealias
typealias ActivityFeedCompletion = (Result<([FeedItem], Bool)>) -> Void
I am not sure why I am getting that error, I think it is misleading but I ran out of ideas of what to do to fix it.
The second error is pretty clear (the bridge cast is most likely redundant)
let tuple = Result.success(items,true) // as ActivityFeedCompletion
represents already the result so you have to write
completion(tuple)
The first error is probably something similar, it's unclear what cache is
You are hiding many relevant parts of your code, so I needed to fill many parts by guess. If my answer is far from what you expect, you should better update your question and show relevant parts of your code. For example, whole definition of your ActivityFeedCache.
With this definition:
typealias ActivityFeedCompletion = (Result<([FeedItem], Bool)>) -> Void
The success case of the Result of your ActivityFeedCompletion takes a single argument of tuple type ([FeedItem], Bool).
In this line:
completion(.success(cache[currentTab], true))
You are passing two arguments, to success, so the message is clear enough. You need to pass a single argument.
completion(.success((cache[currentTab], true)))
And the latter part:
let tuple = Result.success(items,true) as ActivityFeedCompletion
completion((tuple,false))
You are completely mistaking the types. Result cannot be converted to ActivityFeedCompletion, and you cannot pass a raw tuple (tuple,false) to completion which takes Result<([FeedItem], Bool)>.
Please try something like this:
completion(.success((items, true/* or false, which you want to pass? */)))
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.
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'm in the process of learning Swift and as an exercise, I'm writing a wrapper around SQLite. As I was experimenting, I realized that for queries that return rows (like SELECT), I could implement the SequenceType / GeneratorType protocols so that I can return a set of data for each sqlite3_step that I perform.
In practice, sqlite3_step either returns a row or is done, but in theory, it could error out. I'm not doing anything crazy with SQLite. It's just a simple data store for me, so I'm not rewriting schemas on the fly or potentially ripping the database out from under itself, but the fact remains that IN THEORY sqlite3_step could fail.
The question then is, is there a proper way to handle errors in the SequenceType / GeneratorType pattern? GeneratorType's next method doesn't support a throws parameter and returning nil just dictates the end of a sequence. Would there be a good way to handle the error and propagate it up the chain?
You have a few options, depending on what you're looking for.
If you need the Sequence to be lazy, you could use a ResultType kind of thing:
enum SQLiteRow<T> {
case Success(T), FailureTypeOne, FailureTypeTwo
}
Then, your next() method would return a SQLiteRow<T>?, where T is the type of your row.
This fits in nicely with for-loops, as you can use it like this:
for case let .Success(row) in queries {...
so the successful queries are bound to the row variable. This is only if you want to filter out the failed queries. If you wanted to stop everything, you could switch within the for loop, or have a function like this:
func sanitize<
S : SequenceType, T where
S.Generator.Element == SQLiteRow<T>
>(queries: S) -> SQLiteRow<[T]> {
var result: [T] = []
result.reserveCapacity(queries.underestimateCount())
for query in queries {
switch query {
case let .Success(x): result.append(x)
case .FailureTypeOne: return .FailureTypeOne
case .FailureTypeTwo: return .FailureTypeTwo
}
}
return SQLiteRow.Success(result)
}
That will take a sequence of possibly-failed queries, and give back either a sequence of successful queries (if none failed), or a failure type representing the first failure it came across.
However, the second option there isn't lazy. Another eager way to do it would be to use map, which (as of the latest beta) can take a closure which throws:
func makeQuery(x: String) throws -> String {
return x
}
let queries = ["a", "b", "c"]
do {
let successful = try queries.map(makeQuery)
} catch {
// handle
}
Unfortunately the lazy version of map doesn't throw, so you have to evaluate the whole sequence if you want to throw like this.