I have a series of UIViewControllers (popups) in my app that I use to gather information from the end user. I managed to chain them with promises like this:
firstly {
return showFirstPopup()
}
.then { info1 -> Promise<Info2> in
dismissFirstPopup()
//Do sth with info1...
return showSecondPopup()
}
.then { info2 -> Promise<Info3> in
dismissSecondPopup()
//Do sth with info2...
return showThirdPopup()
}
.then { info3 -> Promise<Info4> in
dismissThirdPopup()
//Do sth with info3...
return showForthPopup()
}
...
.catch { error in
//Handle any error or cancellation...
}
If, for example, the user presses back in the third popup I need to go back to the previous "then" and not cancel the whole flow.
Basically I need to be able to go back one step, let the user edit the data, then continue the flow.
Is there a way to do this with PromiseKit?
This is what I ended up using. Hope this helps someone else out there as well.
func myFunc(info: Info) -> Promise<()>
{
return Promise { fulfill, reject in
let firstPromise = shouldShowFirstPopup ? showFirstPopup() : Promise(value: info.info1)
firstPromise
.then { info1 -> Promise<Info2> in
info.info1 = info1
if (info.shouldShowSecondPopup) {
return showSecondPopup()
}
return Promise(value: info.info2)
}
.then { info2 -> Promise<Info3> in
info.info2 = info2
info.shouldShowSecondPopup = false
if (info.shouldShowThirdPopup) {
return showThirdPopup()
}
return Promise(value: info.info3)
}
.then { info3 -> Promise<()> in
info.info3 = info3
info.shouldShowThirdPopup = false
return processEverything()
}
.then { _ -> () in
fulfill(())
}
.catch { error in
switch error {
case MyErrors.backPressed(let popupType):
switch popupType {
case .firstPopup:
reject(MyErrors.userCanceled)
return
case .secondPopup:
info.shouldShowFirstPopup = true
info.shouldShowSecondPopup = true
case .thirdPopup:
info.shouldShowSecondPopup = true
info.shouldShowThirdPopup = true
default:
reject(MyErrors.defaultError(message: "Not implemented case exception"))
return
}
firstly {
return myFunc(info: info)
}
.then { _ -> () in
fulfill(())
}
.catch { error2 in
reject(error2)
}
default:
reject(error)
}
}
}
}
Here is what I found about PromiseKit :
PromiseKit has the concept of cancellation baked in. If the user cancels something then typically you don’t want to continue a promise chain, but you don’t want to show an error message either. So what is cancellation? It’s not success and it’s not failure, but it should handle more like an error, ie. it should skip all the subsequent then handlers. PromiseKit embodies cancellation as a special kind of error.
According to this you are going to skip all the subsequent then handlers.
Here is the link where I found this information : Promise Kit
Related
I have array of Observables, say [Observable <WriteTaskResult>]
I want to perform all write tasks keeping their order, and if any one of them fails then I want to perform Observable<ResetTaskResult>
Following function will return observable of type BatchTasksResult for tracking tasks progress.
Sample Code:
enum BatchTasksResult{
case elapsedTime(Double)
case failedFatal
case rolledback
case success
}
func writeBlocks(tasks: [WriteTask]) -> Observable<BatchTasksResult>{
return Observable.create {(observable) -> Disposable in
let allTasks: [Observable<WriteTaskResult>] = self.writeSomewhere(tasks)
Observable.concat(allTasks)
.subscribe { writeTaskResult in
observable.onNext(.elapsedTime(writeTaskResult.totalTime))
}
onError: { (err) in
// Perform Observable<ResetTaskResult>
// if ResetTask was successful then observable.onNext(.rolledback)
// if ResetTask failed then observable.onNext(.failedFatal)
}
onCompleted: {
observable.onNext(.success)
}
.disposed(by: disposeBag)
return Disposables.create()
}
}
How do I trigger rollback logic using Observable from onError of allTasks' observable?
Simple solution seems nested observable, but that's not good practice, I guess? I tried FlatMap, but it can't really solve "If any sinlge task fails, then rollback and reset" Any other solution to this?
There's no need to add the extra level of indirection with the create function. Every Observable operator already creates a new object.
And when you do use Observable.create, do not dispose in an external dispose bag and return a Disposables.create(). Just return the disposable that you just created.
Here's the appropriate way to do what you want:
func writeBlocks(tasks: [WriteTask], resetTask: Single<ResetTaskResult>) -> Observable<BatchTasksResult> {
// create the array of write tasks and concat them. You seem to have that down.
let result = Observable.concat(tasks.map(writeSomewhere(task:)).map { $0.asObservable() })
.share() // the share is needed because you are using the value twice below.
return Observable.merge(
// push out the elapsed time for each task.
result.map { BatchTasksResult.elapsedTime($0.totalTime) },
// when the last one is done, push out the success event.
result.takeLast(1).map { _ in BatchTasksResult.success }
)
.catch { _ in
resetTask // the resetTask will get subscribed to if needed.
.map { _ in BatchTasksResult.rolledback } // if successful emit a rollback
.catch { _ in Single.just(BatchTasksResult.failedFatal) } // otherwise emit the failure.
.asObservable()
}
}
func writeSomewhere(task: WriteTask) -> Single<WriteTaskResult> {
// create a Single that performs the write and emits a result.
}
I have this code, i want do a request a url
if(!m.sendRequest()){
appDelegate.setValues()
}
this line is executed first
appDelegate.setValues()
i want wait this condition
!m.sendRequest()
structure sendRequest with a completion
func sendRequest(com:#escaping((Bool) -> ())) {
Api.sendReq {
com(true)
}
}
sendRequest { value in
if value {
appDelegate.setValues()
}
}
I would like to register a user which is performed asynchronous. However, the calling function behaves synchronous since the program should only continue when a user is created successfully.
The current implementation is:
class SignUp: NSObject {
// ...
func signUpUser() throws -> Bool {
guard hasEmptyFields() else {
throw CustomErrorCodes.EmptyField
}
guard isValidEmail() else {
throw CustomErrorCodes.InvalidEmail
}
createUser( { (result) in
guard result else {
throw CustomErrorCodes.UserNameTaken
}
return true // Error: cannot throw....
})
}
func createUser( succeeded: (result: Bool) -> () ) -> Void {
let newUser = User()
newUser.username = username!
newUser.password = password!
// User is created asynchronously
createUserInBackground(newUser, onCompletion: {(succeed, error) -> Void in
if (error != nil) {
// Show error alert
} else {
succeeded(result: succeed)
}
})
}
}
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
However, this does not work since createUser is not a throwing function. How could I ensure that signUpUser() only returns true when an new user is created successfully?
You say:
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
But don't. That's not how asynchronous works. The whole idea is that you do not wait. If you're waiting, it's not asynchronous. That means you're blocking, and that's just what you mustn't do.
Instead, arrange to be called back at the end of your asynchronous process. That's when you'll hear that things have succeeded or not. Look at how a download task delegate is structured:
https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDownloadTask_class/
The download task calls back into the delegate to let it know whether we completed successfully or not. That is the relationship you want to have with your asynchronous task. You want to be like that delegate.
You need to adjust your thinking. Instead of trying to write a synchronous method that we need to wait for an asynchronous event, write a method that takes a completion closure. The method will return immediately, but once the asynchronous process is complete it wild invoke the completion closure. When you call such a method you pass in code in the incompletion closure that gets called once the job is done.
I'm wondering how to write my code in more elegant way... I have two requests, the second request have to wait for the first one. If the first one gets failed the whole sentence should failed, I'm wondering how to catch error in one common place?
enum TestError: ErrorType {
case Connection
}
private func runTest() {
rx_firstReq()
.subscribeNext() { _ in
return self.rx_secondReq()
.subscribeNext() { _ in
print("whole req sequence finished with success!")
}.addDisposableTo(self.myDisposeBag)
}.addDisposableTo(myDisposeBag)
}
func rx_firstReq() -> Observable<Bool> {
return Observable.create() { observable -> Disposable in
observable.onError(TestError.Connection) // We are assuming that first req gets failed
observable.onCompleted()
return NopDisposable.instance
}
}
func rx_secondReq() -> Observable<Bool> {
return Observable.create() { observable -> Disposable in
observable.onNext(true)
observable.onCompleted()
return NopDisposable.instance
}
}
As you see there is no any place for error handling... I have no idea how to model it, at this moment each next request in my chain gonna create next indentation level... in my opinion it is not good usage of the RxSwift... 😕
..some hint or link with example code with handling error in common place will be great for me.
Never use one subscribe in another subscribe ! :)
For your problem, flatMap is the solution.
rx_firstReq()
.flatMap { _ -> Observable<Bool> in
rx_secondReq()
}
.subscribe(next, error ...)
.disposed(by: bag)
voilà :)
I'm trying to generate an array of PFObjects called 'areaList'. I've been researching this quite a bit and understand that I could benefit from using a completion handler to handle the asynchronous nature of the loaded results. My ask, specifically, is to get some guidance on what I'm doing wrong as well as potential tips on how to achieve the result "better".
Here is my query function with completion handler:
func loadAreasNew(completion: (result: Bool) -> ()) -> [Area] {
var areaList = self.areaList
let areaQuery = PFQuery(className: "Area")
areaQuery.findObjectsInBackgroundWithBlock {
(areas: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for area in areas! {
let areaToAdd = area as! Area
areaList.append(areaToAdd)
// print(areaList) // this prints the list each time
// print(areaToAdd) // this prints the converted Area in the iteration
// print(area) // this prints the PFObject in the iteration
if areaList.count == areas!.count {
completion(result: true)
} else {
completion(result: false)
}
}
} else {
print("There was an error")
}
}
return areaList
}
Here is how I'm attempting to call it in viewDidLoad:
loadAreasNew { (result) -> () in
if (result == true) {
print(self.areaList)
} else {
print("Didn't Work")
}
}
I assigned this variable before viewDidLoad:
var areaList = [Area]()
In the console, I get the following:
Didn't Work
Didn't Work
Didn't Work
Didn't Work
[]
Representing the 5 items that I know are there in Parse...
This is an interesting question. First off, PFQuery basically has a built in completion handler, which is quiet nice! As you probably know, all of the code within the areaQuery.findObjectsInBackgroundWithBlock {...} triggers AFTER the server response. A completion most often serves the purpose of creating a block, with the ability of asynchronously returning data and errors.
Best practice would (IMO) to just call the code that you want to use with the results from your PFQuery right after your area appending loop (which I'm gonna take out because I'm picky like that), like so:
func loadAreasNew() {
var areaList = self.areaList
let areaQuery = PFQuery(className: "Area")
areaQuery.findObjectsInBackgroundWithBlock {
(areas: [PFObject]?, error: NSError?) -> Void in
if error == nil {
let areasFormatted = areas! As [Areas]
areasList += areasFormatted
//Something like this
self.codeINeedAreasFor(areasList)
}
} else {
print(error)
}
}
}
HOWEVER! If you really feel the need to use some completion handlers, check out this other answer for more info on how to use them. But keep in mind all tools have a time and a place...
There are a few issues here.
Your completion handler doesn't require you to define the name for your completion handler's parameters, so you could easily use completion: (Bool) -> ()
Further in your function, you're returning areaList. This should be put through the completion handler like this onComplete(areaList) and change your completion handler parameter to expect your area list.
Then, when you call your function, it could look more like this :
loadAreasNew { result in
if (result == true) {
print(self.areaList)
} else {
print("Didn't Work")
}
}
Here is my concern:
1) Don't pass in a local variable and make the function return it, it's meaningless and danger.
You may want to initiate an empty array and make your fetch, then "return" it.
2) The fetch request is processed in background, you will have no idea when it will have finished. If you return the array immediately it will always be an empty array.
Put the "return" in your completion too.
3) Parse already has a distance checking method, you don't have to do it manually. aPARSEQUERRY.where(key:,nearGeoPoint:,inKilometers:)
I will rewrite the function as:
func loadNewAreas(completion:([Area],err?)->()){
let areaQuery = PFQuery(className: "Area")
areaQuery.where("location",nearGeoPoint:MYCURRENTLOCATION,inKilometers:50)
areaQuery.findObjectInBackgroundWithBlock(){objects,err
if objects.count == 0
{
completion([],err)
}
let areas = Area.areasFromPFObjects(objects)
completion(areas,err)
}
}