Alternatives to lots of nested completion blocks? - ios

I'm using OAuthSwift to access the goodreads api. First I have an api call to get the user id. Then, using that id, I get the books the user has in a shelf. Then I check if there are any books with a certain title, and if there aren't, I call the api again to create that book. And then, being sure the book is there, I post a status update on its progress.
This is the code to get the user id:
oAuth.client.get(
"https://www.goodreads.com/api/auth_user",
success: {
data, response in
// Here I use the returned ID to get the books.
},
failure: {
error in
//…
}
)
Once I got the ID, I need to use it for the next api call, which gets the books. The thing is, I can't just add the next call after that first one, because they're asynchronous, so the second call would start before I got the ID from the first one.
What I'm doing, then, is to add the second api call inside that success closure (I pointed it out in the code with a comment). So I have two very similar pieces of code one inside the other; when the first api call succeeds it calls the next. But then I have to use the result of that second call to decide if I'll add a new book or not. So I have a third api call (to add the book) inside the success block of the second call, which is inside the success block of the first call… and you can see things are starting to get pretty confusing. And it doesn't even stop there: I have a fourth api call to post a status update, and it's inside the completion handler of the third call, which is inside that of the second, which is inside that of the first.
So… what should I do instead of that? I'm sorry if it's a silly question, but I'm new at swift and at programming overall.

I think Apple has provided best way of handling this scenario using GCD, GCD provides serial queue to execute blocks/API in serial manner. That is in FIFO Order
let serialQueue : dispatch_queue_t = dispatch_queue_create("com.yourSerialQueue.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue) { () -> Void in
//Call First API
print("First API call succeed")
};
dispatch_async(serialQueue) { () -> Void in
//Call Second API
print("Second API call succeed")
};
dispatch_async(serialQueue) { () -> Void in
//Call Third API
print("Third API call succeed")
};
dispatch_async(serialQueue) { () -> Void in
//Call Fourth API
print("Fourth API call succeed")
};
OutPut:
First API call succeed
Second API call succeed
Third API call succeed
Fourth API call succeed

I do not have deep knowledge of GCD yet, for your scenario what i can suggest you in real quick is call each API with completion block as below, (this is what i can help you as of now)
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(backgroundQueue, ^{
[self callGetUserIdAPIAndCompletion:^(NSDictionary *dictResponse) {
[self callGetBooksAPIForUserId:dictResponse[#"userId"] AndCompletion:^(NSDictionary *dictResponse) {
[self callAddBookAPIForUserId:dictResponse[#"userId"] bookId:dictResponse[#"bookId"] AndCompletion:^(NSDictionary *dictResponse) {
[self callPostStatusUpdateAPIForUserId:dictResponse[#"userId"] status:#"status" AndCompletion:^(NSDictionary *dictResponse) {
}];
}];
}];
}];
});
And write your API calls functions as below
-(void)callGetUserIdAPIAndCompletion:(void(^)(NSDictionary *dictResponse))completion {
NSDictionary *dictResponse;
//Call First API
NSLog(#"API 1 completes");
completion(dictResponse);
}
-(void)callGetBooksAPIForUserId:(NSString*)userId AndCompletion:(void(^)(NSDictionary *dictResponse))completion {
NSDictionary *dictResponse;
//Call Second API with userId
NSLog(#"API 2 completes");
completion(dictResponse);
}
-(void)callAddBookAPIForUserId:(NSString*)userId bookId:(NSString*)bookId AndCompletion:(void(^)(NSDictionary *dictResponse))completion {
NSDictionary *dictResponse;
//Call Third API with userId and bookId
NSLog(#"API 3 completes");
completion(dictResponse);
}
-(void)callPostStatusUpdateAPIForUserId:(NSString*) userId status:(NSString*) status AndCompletion:(void(^)(NSDictionary *dictResponse))completion {
NSDictionary *dictResponse;
//Call Fourth API with userId and status
NSLog(#"API 4 completes");
completion(dictResponse);
}

Related

iOS AFNetwork 3.0: Is there a faster way to send multiple API requests and wait until all of it is finished?

I am currently using the following method to send GET API requests. This method works, but I was wondering if there is a faster way. All I need regarding requirements is to know when all of the Deleted mail has been synced. Any tips or suggestions are appreciated.
- (void)syncDeletedMail:(NSArray *)array atIdx:(NSInteger)idx {
if (idx < array.count) {
NSInteger idNumber = array[idx];
[apiClient deleteMail:idNumber onSuccess:^(id result) {
[self syncDeletedMail:array atIdx:(idx + 1)];
} onFailure:^(NSError *error){
[self syncDeletedMail:array atIdx:(idx + 1)];
}];
} else {
NSLog(#"finished");
}
}
Edit: I don't care what order it is completed (not sure if it matters in terms of speed), as long as all the API requests come back completed.
You can just send deleteMail requests at once and use dispatch_group to know when all the requests are finished. Below is the implementation,
- (void)syncDeletedMail:(NSArray *)array {
dispatch_group_t serviceGroup = dispatch_group_create();
for (NSInteger* idNumber in array)
{
dispatch_group_enter(serviceGroup);
[apiClient deleteMail:idNumber onSuccess:^(id result) {
dispatch_group_leave(serviceGroup);
} onFailure:^(NSError *error){
dispatch_group_leave(serviceGroup);
}];
}
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
NSLog(#"All email are deleted!");
});
}
Here you can see all the requests are fired at the same time so it will reduce the time from n folds to 1.
Swift Version of #Kamran :
let group = DispatchGroup()
for model in self.cellModels {
group.enter()
HTTPAPI.call() { (result) in
// DO YOUR CHANGE
switch result {
...
}
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
// UPDATE UI or RELOAD TABLE VIEW etc.
// self.tableView.reloadData()
}
I suppose your request is due to the fact that you might have huge amounts of queued delete requests, not just five or ten of them.
In this case, I'd also try and consider adding a server side API call that allows you to delete more than just one item at a time, maybe up to ten or twenty, so that you could also reduce the overhead of the network traffic you'd be generating (a single GET isn't just sending the id of the item you are deleting but also a bunch of data that will basically sent on and on again for each and every call) by grouping the mails in batches.

How to cancel multiple networking requests using Moya

I am currently using Moya to structure my networking calls. Per their docs, I have configured it as the following:
enum SomeAPIService {
case endPoint1(with: Object)
case endPoint2(duration: Int)
}
When calling an endpoint (in this case, endPoint1), I do the following:
let provider = MoyaProvider<SomeAPIService>()
provider.request(.endPoint1(object: object)) { (result) in
switch result {
case let .success(moyaResponse):
finished(Result.success(value: moyaResponse.value))
case let .failure(error):
let backendError = BackendError(localizedTitle: "", localizedDescription: "Some error", code: moyaResponse.statusCode)
finished(Result.failure(error: backendError))
}
})
My goal is, upon the user performing an action, cancel all the networking requests that's happening.
Accordingly, Moya does allow one to cancel requests from the discussion here. From the most upvoted comment, someone mentioned let request_1 = MoyaRequestXXXXX and then ruest_1.cancel()
My problem is:
How would I keep pointer to the requests?
provider doesn't have a cancel() function - so how should I be calling it?
Any help is much appreciated.
Edit:
Per the helpful suggestion about using [Cancellable], I did the following:
(1) In my app's singleton instance called Operator, I added var requests = [Cancellable]()
(2) Every API call is added to the requests array as a Cancellable, like so:
let provider = MoyaProvider<SomeAPIService>()
Operator.shared.requests.append(provider as! Cancellable) //causing error
provider.request(.endPoint1(object: object)) { (result) in
//rest of the block omitted
I think I am not getting the syntax correct, and am adding the provider and not the request. However, since the request is itself a block, where would be the place to add the request?
The request method returns a Cancellable. From the documentation we can read:
The request() method returns a Cancellable, which has only one public function, cancel(), which you can use to cancel the request.
So according to this, I made a simple test and call:
var requests: [Cancellable] = []
#objc func doRequests() {
for i in 1...20 {
let request = provider.request(MyApi.someMethod) {
result in
print(result)
}
requests.append(request)
}
requests.forEach { cancellable in cancellable.cancel() } // here I go through the array and cancell each request.
requests.removeAll()
}
I set up a proxy using Charles and it seems to be working as expected. No request was sent - each request was cancelled.
So, the answer to your questions is:
You can keep it in [Cancellable] array.
Go through the array and cancel each request that you want to cancel.
EDIT
The main problem is that you adding the provider to the array and you try to map provider as Cancellable, so that cause the error.
You should add reqest to the array. Below you can see the implementation.
let provider = MoyaProvider<SomeAPIService>()
let request = provider.request(.endPoint1(object: object)) { // block body }
Operator.shared.requests.append(request)
//Then you can cancell your all requests.
I would just cancel the current provider session + tasks:
provider.manager.session.invalidateAndCancel()

How to stub method with response block using OCMock

I have a method that calls a request with a response block inside. I want to stub my response and return fake data. How can this be done?
-(void)method:(NSString*)arg1{
NSURLRequest *myRequest = ...........
[self request:myRequest withCompletion:^(NSDictionary* responseDictionary){
//Do something with responseDictionary <--- I want to fake my responseDictionary
}];
}
- (void)request:(NSURLRequest*)request withCompletion:(void(^)(NSDictionary* responseDictionary))completion{
//make a request and passing a dictionary to completion block
completion(dictionary);
}
If I understand you correctly, you want to mock request:withCompletion: and call the passed completion block.
Here is how I have done this in the past. I've adapted this code to your call, but I cannot check it for compilation errors, but it should show you how to do it.
id mockMyObj = OCClassMock(...);
OCMStub([mockMyObj request:[OCMArg any] completion:[OCMArg any]])).andDo(^(NSInvocation *invocation) {
/// Generate the results
NSDictionary *results = ....
// Get the block from the call.
void (^__unsafe_unretained completionBlock)(NSDictionary* responseDictionary);
[invocation getArgument:&callback atIndex:3];
// Call the block.
completionBlock(results);
});
You will need the __unsafe_unretained or things will go wrong. Can't remember what right now. You could also combine this with argument captures as well if you wanted to verify the passed arguments such as the setup of the request.

Executing functions and blocks through time

I don't understand how Objective-C loop system works. I have function (hope names are right, rather check in code) which executes query from Health Kit. I got my mind blown when I realised that function pass return value before query finishes.
__block bool allBeckuped = true;
HKSampleQuery *mySampleQuery = [[HKSampleQuery alloc] initWithSampleType:mySampleType
predicate:myPredicate
limit:HKObjectQueryNoLimit
sortDescriptors:#[mySortDescriptor]
resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if(!error && results)
{
for(HKQuantitySample *samples in results)///main hk loop
{
allBeckuped = false;
NSLog(#"1");
}
}
}];//end of query
[healthStore executeQuery:mySampleQuery];
NSLog(#"2");
return allBeckuped;
I'm trying to check if there are any new data, but I don't know where to put condition for that, because nslog2 is called before nslog 1.
Any words I should Google up?
Any words I should google up?
You can start with: asynchronous design, blocks, GCD/Grand Central Dispatch should help as well - you're not using it but asynchronous designs often do.
Look at the initWithSampleType: method you are calling, it is an example of a method following the asynchronous model. Rather than return a result immediately, which is the synchronous model you are probably used to, its last argument, resultsHandler:, is a block which the method calls at some future time passing the result of its operation to it.
This is the pattern you will need to learn and follow.
Your method which contains the call to initWithSampleType: cannot return a result (e.g. your allBeckuped) synchronously. So it needs to take a "results handler" block argument, and the block you pass to initWithSampleType: should call the block passed to your method - and so the asynchronous flow of control is weaved.
HTH

Calling Objective-c function in swift

Im trying to implement CometChat in my swift application. I managed to import the Objective-c framework successfully via a bridging header. But now I'm stuck trying to call Objective-C methods from swift.
This is the method from the interface i want to call:
- (void)loginWithURL:(NSString *)siteURL
username:(NSString *)username
password:(NSString *)password
success:(void(^)(NSDictionary *response))success
failure:(void(^)(NSError *error))failure;
And this is how the method is called from Objective-C:
[cometChat loginWithURL:#"localhost/cometchat/" username:usernameTextField.text password:passwordTextField.text success:^(NSDictionary *response) {
NSLog(#"SDK log : Username/Password Login Success %#",response);
[self handleLogin];
} failure:^(NSError *error) {
NSLog(#"SDK log : Username/Password Login Error%#",error);
[self handleLoginError:#[#0,error]];
}];
So far i have this:
cometChat.loginWithURL("localhost/cometchat/", username: EmailField.text, password: PasswordField.text){
(success: [NSDictionary], failure:NSError) in {
println("did i make it here?")
}
}
The problem is, it says that there is missing argument "success", but its a mystery to me how it can be an argument, when it clearly returns the response. I want to know how to put together this method call. I also used the objectivec2swift converter, but it wasn't any help. Also, i have no clue what the # means before the #[#0,error]
I know its a beginners question, but i already wasted a whole day on this, since i couldn't find any tutorials on how to call such "complex" Obj-C methods from swift.
Try this :-
cometChat.loginWithURL("localhost/cometchat/", username: "abc", password: "123", success: { (response) -> Void in
print("SDK log : Username/Password Login Success \(response)")
}) { ( error) -> Void in
print("SDK log : Username/Password Login Error \(error)")
}
When you look at the Objective-C signature, you see that the method takes two closures: success is a void function that takes a dictionary, and failure is a void function that takes an error.
In your Swift code you have only one closure: a void function that takes a dictionary and an error.
You either need to change the Objective-C method to take just one closure, or change the Swift code to provide two closures.
When you call a function, and the last parameter is a block / closure, then you can write the last parameter after the function call in { }. That applies to the last block only.
Anyway, you are trying to pass a closure with two parameters success and failure. You need to pass two closures, one as the success parameter of your function, with a parameter response, and one either as the failure parameter of your function, or following the function, with a parameter error.

Resources