ReactiveSwift pipeline flatMap body transform not executed - swift

I have the following pipeline setup, and for some reason I can't understand, the second flatMap is skipped:
func letsDoThis() -> SignalProducer<(), MyError> {
let logError: (MyError) -> Void = { error in
print("Error: \(error); \((error as NSError).userInfo)")
}
return upload(uploads) // returns: SignalProducer<Signal<(), MyError>.Event, Never>
.collect() // SignalProducer<[Signal<(), MyError>.Event], Never>
.flatMap(.merge, { [uploadContext] values -> SignalProducer<[Signal<(), MyError>.Event], MyError> in
return context.saveSignal() // SignalProducer<(), NSError>
.map { values } // SignalProducer<[Signal<(), MyError>.Event], NSError>
.mapError { MyError.saveFailed(error: $0) } // SignalProducer<[Signal<(), MyError>.Event], MyError>
})
.flatMap(.merge, { values -> SignalProducer<(), MyError> in
if let error = values.first(where: { $0.error != nil })?.error {
return SignalProducer(error: error)
} else {
return SignalProducer(value: ())
}
})
.on(failed: logError)
}
See the transformations/signatures starting with the upload method.
When I say skipped I mean even if I add breakpoints or log statements, they are not executed.
Any idea how to debug this or how to fix?
Thanks.
EDIT: it is most likely has something to do with the map withing the first flatMap, but not sure how to fix it yet.
See this link.
EDIT 2: versions
- ReactiveCocoa (10.1.0):
- ReactiveObjC (3.1.1)
- ReactiveObjCBridge (6.0.0):
- ReactiveSwift (6.1.0)
EDIT 3: I found the problem which was due to my method saveSignal sending sendCompleted.
extension NSManagedObjectContext {
func saveSignal() -> SignalProducer<(), NSError> {
return SignalProducer { observer, disposable in
self.perform {
do {
try self.save()
observer.sendCompleted()
}
catch {
observer.send(error: error as NSError)
}
}
}
}
Sending completed make sense, so I can't change that. Any way to change the flatMap to still do what I intended to do?

I think the reason your second flatMap is never executed is that saveSignal never sends a value; it just finishes with a completed event or an error event. That means map will never be called, and no values will ever be passed to your second flatMap. You can fix it by doing something like this:
context.saveSignal()
.mapError { MyError.saveFailed(error: $0) }
.then(SignalProducer(value: values))
Instead of using map (which does nothing because there are no values to map), you just create a new producer that sends the values after saveSignal completes successfully.

Related

Does a Completion Handler end the function?

Perhaps I do not understand the concept of a completion handler, but I have a function like so;
func someFunction(completion: #escaping (Bool, LoginError?) -> Void) {
self.checkDevice() { allowed, error in
if let e = error {
completion(false, e)
}
completion(true, nil)
}
}
While being light on what checkDevice() does, the basic premise is that it performs an asynchronous network call, and returns either true with no error (nil), or returns false with an error.
When I run this code, I am finding that the completion handler is being called twice. It sends a completion as a tuple (as false, error) and also as (true, nil). I've done some debugging and there seems to be no manner in which someFunction() is called twice.
I was of the belief that once a completion is sent, the function would end. In my test case, I am forcing an error from checkDevice(), which should result in me sending the completion as (false, error), but I am seeing both (false, error) and (true, nil). Does a completion not immediately end the function?
I was of the belief that once a completion is sent, the function would end.
No, why would that be? When you call this function it’s like calling any other function. The name has no magic power.
Rewrite it like this:
func someFunction(completion: #escaping (Bool, LoginError?) -> Void) {
self.checkDevice() { allowed, error in
if let e = error {
completion(false, e)
return // *
}
completion(true, nil)
}
}
Actually, I should mention that this is a poor way to write your completion handler. Instead of taking two parameters, a Bool and an Optional LoginError to be used only if the Bool is false, it should take one parameter — a Result, which carries both whether we succeeded or failed and, if we failed, what the error was:
func someFunction(completion: #escaping (Result<Void, Error>) -> Void) {
self.checkDevice() { allowed, error in
completion( Result {
if let e = error { throw e }
})
}
}
As you can see, using a Result as your parameter allows you to respond much more elegantly to what happened.
Indeed, checkDevice itself could pass a Result into its completion handler, and then things would be even more elegant (and simpler).
A completion handler does not end a function. The typical way to end a function is to insert a return.
See comment re: your specific case
Consider including parameter names (for reference). And the completion should only be called once. You can do that using return or by using a full if-else conditional.
func someFunction(completion: #escaping (_ done: Bool, _ error: LoginError?) -> Void) {
checkDevice() { (allowed, error) in
if let e = error {
completion(false, e)
} else {
completion(true, nil)
}
}
}
func someFunction(completion: #escaping (_ done: Bool, _ error: LoginError?) -> Void) {
checkDevice() { (allowed, error) in
if let e = error {
completion(false, e)
return
}
completion(true, nil)
}
}
By giving parameters names, when calling the function, you can now reference the signature to denote the meanings of its parameters:
someFunction { (done, error) in
if let error = error {
...
} else if done {
...
}
}

RxSwift catch networking and reachability errors

I try to use retryOnBecomesReachable method from the RX example files in my networking layer
extension ObservableConvertibleType {
func retryOnBecomesReachable(_ valueOnFailure:E, reachabilityService: ReachabilityService?) -> Observable<E> {
return self.asObservable()
.catchError { (e) -> Observable<E> in
return reachabilityService.reachability
.skip(1)
.filter { $0.reachable }
.flatMap({ _ -> Observable<E> in
return Observable.error(e)
})
.startWith(valueOnFailure)
}
.retry()
}
}
// My layer
request
.flatMapLatest{ request in
provider.rx.request(request)
.map{ User.self }
.map{ RequestState.loaded($0) }
.retryOnBecomesReachable(.error(.notConnectedToInternet), reachabilityService: reachabilityService)
.catchError({ .just(.error($0)) })
.startWith(.startLoading)
}
Without this method, all works awesome. All error catching and returning .just(.error($0)) sequence.
With this method, the retry feature works awesome. But when something happens (mapping, decoding or other error) I get .notConnectedToInternet. I think the reason in .startWith(valueOnFailure) method. I tried to move, remove, change position but nothing helps. I'm stuck.
What should I do to use retry feature and catch errors correct?
I think that basically changing .startWith(valueOnFailure) to startWith(e) might work for you. Another option is to check if the error is a reachability error to begin with inside the catch block.
e.g.
.catchError { e in
guard e == SomeError.notConnectedToInternet else {
return .error(e)
}
... rest of your code

Returning Observable is not working

I am just playing with RxSwift to better understand the concepts. I am trying to validate email. However, the subscriber doesn't call when I return an Observable back. I thought this might work. Any suggestions?
var emailAddressValid: Observable<String>{
return self.userEmailAddress.asObservable().filter({ (userEmail) -> Bool in
userEmail.count > 0
}).flatMap({ (userEmail) -> Observable<String> in
if userEmail == "abcd#gmail.com"{
//This works perfectly
return .just("007")
} else {
let emailDomain = userEmail.components(separatedBy: "#").last
if emailDomain != nil {
//nothing happens in onNext
//This returns back an Observable<String>
return DataService.instance.checkDomainIsRegistered(domainName: emailDomain!)
} else {
return .just("0")
}
}
})
}
Although the app works. However, there isn't any compiler error as well. But the onNext in the Observer doesn't work when I return DataService.instance.checkDomainIsRegistered(domainName: emailDomain!)
func checkDomainIsRegistered(domainName: String) -> Observable<String>{
print(domainName)
return Observable<String>.create{ data in
self._REF_DOMAIN_NAMES.queryOrdered(byChild: "domainName").queryEqual(toValue: domainName).observeSingleEvent(of: .value, with: { (domainNameSnapshot) in
if(domainNameSnapshot.exists()){
print("1")
data.onNext("1")
} else {
print("0")
data.onNext("0")
}
}, withCancel: { (error) in
data.onError(error)
})
data.onCompleted()
return Disposables.create()
}
}
In your example, the call to
self._REF_DOMAIN_NAMES.queryOrdered(byChild: "domainName")
.queryEqual(toValue: domainName)
.observeSingleEvent(of: .value, with: { (domainNameSnapshot) in })
is likely being dispatched to a background thread or queue.
In the meantime, your Observable checkDomainIsRegistered then completes with data.onCompleted() while probably running on the main thread.
The results is that onNext() is never being allowed to be called. You can verify this is happening by temporarily removing the onCompleted().
And if called data.onError(error), event stream may be broken. You should catch that.

Chaining operations with RxSwift

I want to chain following operations
createUserandVerify
Create Anonymous User (user)
Verify User -> verifiedUser
If verification successful return verifiedUser else return user
Get stuff with coredata getStuff
If stuff.count > 0 Upload stuff with user credentials uploadStuff
Finally report the result of all operations
I wrote createUserandVerify as below. I wonder how should I write uploadStuff in reactive way. Upload function depends on user credentials. Therefore It must only run after createUserandVerify. I know I could just check count of array inside uploadStuff and return empty but I wonder the best practices.
func createUserandVerify() -> Single<User> {
return Service.sharedInstance.generateAnonUser()
.flatMap{ user in
if Service.sharedInstance.isOldRegisteredUser {
print("It is old user")
// We need to verify the receipt
return Service.sharedInstance.verifyReceipt()
.flatMap { verifiedUser in
print("Returning Verified new user [Verification Success]")
return Single.just((verifiedUser))
}.catchError{ error ->Single<User> in
print("Returning firstly created user [Verification Failed]")
print("Error Type: \(error)")
return Single.just(user)
}
} else {
//Normal anonymous old user
print("Returning firstly created user [Anonymous]")
return Single.just(user)
}
}
}
Assumptions (since I have not worked with Single I changed them to Observable):
func createUserandVerify() -> Observable<User>
func getStuff() -> [Stuff]
func uploadStuff(_ user: User) -> Observable<String>
createUserandVerify() should publish errors with onError so uploadStuff will not be called if something goes wrong.
Possible solution:
enum CustomError: Error {
case instanceMissing
case notEnoughStuff
}
createUserandVerify()
.flatMap { [weak self] (user) -> Observable<String> in
guard let strongSelf = self else { throw CustomError.instanceMissing }
guard strongSelf.getStuff().count > 0 else { throw CustomError.notEnoughStuff }
return strongSelf.uploadStuff(user)
}
.subscribe(
onNext: { stringResult in
// print result from 'uploadStuff'
print(stringResult)
},
onError: { error in
// will be reached if something goes
// wrong in 'createUserandVerify' or 'uploadStuff'
// or if one of your custom errors in 'flatMap' are thrown
print(error)
})
.disposed(by: disposeBag)
You could also make getStuff reactive by returning an Observable or Single and also include it in the chain via flatMap.

RxSwift, Share + retry mechanism

I have a network request that can Succeed or Fail
I have encapsulated it in an observable.
I have 2 rules for the request
1) There can never be more then 1 request at the same time
-> there is a share operator i can use for this
2) When the request was Succeeded i don't want to repeat the same
request again and just return the latest value
-> I can use shareReplay(1) operator for this
The problem arises when the request fails, the shareReplay(1) will just replay the latest error and not restart the request again.
The request should start again at the next subscription.
Does anyone have an idea how i can turn this into a Observable chain?
// scenario 1
let obs: Observable<Int> = request().shareReplay(1)
// outputs a value
obs.subscribe()
// does not start a new request but outputs the same value as before
obs.subscribe()
// scenario 2 - in case of an error
let obs: Observable<Int> = request().shareReplay(1)
// outputs a error
obs.subscribe()
// does not start a new request but outputs the same value as before, but in this case i want it to start a new request
obs.subscribe()
This seems to be a exactly doing what i want, but it consists of keeping state outside the observable, anyone know how i can achieve this in a more Rx way?
enum Err: Swift.Error {
case x
}
enum Result<T> {
case value(val: T)
case error(err: Swift.Error)
}
func sample() {
var result: Result<Int>? = nil
var i = 0
let intSequence: Observable<Result<Int>> = Observable<Int>.create { observer in
if let result = result {
if case .value(let val) = result {
return Observable<Int>.just(val).subscribe(observer)
}
}
print("do work")
delay(1) {
if i == 0 {
observer.onError(Err.x)
} else {
observer.onNext(1)
observer.onCompleted()
}
i += 1
}
return Disposables.create {}
}
.map { value -> Result<Int> in Result.value(val: value) }
.catchError { error -> Observable<Result<Int>> in
return .just(.error(err: error))
}
.do(onNext: { result = $0 })
.share()
_ = intSequence
.debug()
.subscribe()
delay(2) {
_ = intSequence
.debug()
.subscribe()
_ = intSequence
.debug()
.subscribe()
}
delay(4) {
_ = intSequence
.debug()
.subscribe()
}
}
sample()
it only generates work when we don't have anything cached, but thing again we need to use side effects to achieve the desired output
As mentioned earlier, RxSwift errors need to be treated as fatal errors. They are errors your stream usually cannot recover from, and usually errors that would not even be user facing.
For that reason - a stream that emits an .error or .completed event, will immediately dispose and you won't receive any more events there.
There are two approaches to tackling this:
Using a Result type like you just did
Using .materialize() (and .dematerialize() if needed). These first operator will turn your Observable<Element> into a Observable<Event<Element>>, meaning instead of an error being emitted and the sequence terminated, you will get an element that tells you it was an error event, but without any termination.
You can read more about error handling in RxSwift in Adam Borek's great blog post about this: http://adamborek.com/how-to-handle-errors-in-rxswift/
If an Observable sequence emits an error, it can never emit another event. However, it is a fairly common practice to wrap an error-prone Observable inside of another Observable using flatMap and catch any errors before they are allowed to propagate through to the outer Observable. For example:
safeObservable
.flatMap {
Requestor
.makeUnsafeObservable()
.catchErrorJustReturn(0)
}
.shareReplay(1)
.subscribe()