I search on the web, but what I found is not what I expect.
I have a function with a block inside, and this function return before doing the treatment into the block. So my function return nil...
NSString* returnTheFolder()
{
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
GTLServiceDrive *service;
__block NSMutableDictionary* titlesAndIdentifiers;
__block __strong NSString* rootFolder;
__block NSArray* allIdentifiers;
NSString* userDefaultValue = [userDefaults objectForKey:#"User1"];
titlesAndIdentifiers = [[NSMutableDictionary alloc]init];
service = [[GTLServiceDrive alloc] init];
service.authorizer = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KeychainForName clientID:ClientID clientSecret:ClientSecret];
GTLQueryDrive *query =
[GTLQueryDrive queryForFilesList];
query.maxResults = 9999999;
query.q = #"'root' in parents";
[service executeQuery:query completionHandler:^(GTLServiceTicket *ticket, GTLDriveFileList* files, NSError *error) {
for (GTLDriveFile *folder in files)
{
if ([folder.mimeType isEqualToString:#"application/vnd.google-apps.folder"])
{
[titlesAndIdentifiers setValue:folder.identifier forKey:folder.title];
allIdentifiers = [[NSArray alloc]initWithArray:[titlesAndIdentifiers allKeysForObject:userDefaultValue]];
rootFolder = [allIdentifiers objectAtIndex:0];
}
}
}];
return rootFolder;
}
Which method can I use for execute a block immediately in my function ?
Thanks a lot everyone !!!
The Google library you are using is designed to be asynchronous - it is making calls to a web service and they can take an arbitrarily long time to complete. You must take this into consideration when thinking of a synchronous solution - you may block for an arbitrarily long time.
The best solution for you is to redesign your code so it to is asynchronous.
If you cannot make your code asynchronous for some reason then you can make an asynchronous call appear synchronous, but you must be careful. In essence all you need to do is use a semaphore: have the callback block to the asynchronous call signal the semaphore, and after making the asynchronous call wait on the semaphore. However for this to work you need to know that the thread you are waiting on the semaphore is not the same thread that will be used for the callback - or the callback will be blocked. You need to determine what guarantees the Google library makes about the thread the callback will be invoked on and write your code appropriately.
And if all that sounds too complicated go back to the first recommendation - make your code asynchronous!
HTH
Instead of returning a value like this.You can make a function that does not return anything and takes a block as an argument.
The block will take the value as a parameter.
Change your existing code to the code below
-(void)returnTheFolder:(void (^) (NSString *rootFolder))completion
{
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
GTLServiceDrive *service;
__block NSMutableDictionary* titlesAndIdentifiers;
__block __strong NSString* rootFolder;
__block NSArray* allIdentifiers;
NSString* userDefaultValue = [userDefaults objectForKey:#"User1"];
titlesAndIdentifiers = [[NSMutableDictionary alloc]init];
service = [[GTLServiceDrive alloc] init];
service.authorizer = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KeychainForName clientID:ClientID clientSecret:ClientSecret];
GTLQueryDrive *query =
[GTLQueryDrive queryForFilesList];
query.maxResults = 9999999;
query.q = #"'root' in parents";
[service executeQuery:query completionHandler:^(GTLServiceTicket *ticket, GTLDriveFileList* files, NSError *error) {
for (GTLDriveFile *folder in files)
{
if ([folder.mimeType isEqualToString:#"application/vnd.google-apps.folder"])
{
[titlesAndIdentifiers setValue:folder.identifier forKey:folder.title];
allIdentifiers = [[NSArray alloc]initWithArray:[titlesAndIdentifiers allKeysForObject:userDefaultValue]];
rootFolder = [allIdentifiers objectAtIndex:0];
completion(rootFolder);
}
}
}];
}
And then call like below
[self returnTheFolder:^(NSString *rootFolder){
NSLog(#"Root Folder = %#",rootFolder);
}];
Cheers.
Related
I'm trying to get an array of urls from my backend.
I use AFNetworking and I have a HTTPUtil class implemented as singleton to handle my requests.
HTTPUtil.m
#import "HTTPUtil.h"
#implementation HTTPUtil
+(instancetype)sharedInstance{
NSLog(#"sharedInstance"); //to check the order
static HTTPUtil* manager;
static dispatch_once_t once;
dispatch_once(&once, ^{
manager = [[HTTPUtil alloc] init];
});
manager.responseSerializer = [AFJSONResponseSerializer serializer];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
return manager;
}
-(void)getImageArrayFromURL:(NSString *)url success:(void(^)(NSArray* array))success failure:(void(^)(NSError* error))failure{
NSLog(#"getting..."); //to check the order
[self GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask* task, id response){
NSLog(#"Response: %#", response);
NSString* imgStr = [[response objectForKey:kResponseDataKey] objectForKey:#"img"];
//convert nsstring to nsarray
NSArray* array = [StringUtil arrayFromString:imgStr];
//construct urls
NSMutableArray* ret = [[NSMutableArray alloc] init];
NSMutableString* url;
for (NSString* rawStr in array) {
url = [NSMutableString stringWithString:kUrlBase];
[url appendString:[rawStr stringByReplacingOccurrencesOfString:#"/" withString:#"+"]];
[ret addObject:url];
}
success(ret);
}failure:^(NSURLSessionDataTask* task, NSError* error){
NSLog(#"Error: %#", error);
failure(error);
}];
}
In my view controller, I call the method to fetch the array.
_vCycleScrollView = [SDCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 0, 0, 0) delegate:self placeholderImage:[UIImage imageNamed:#"checked"]];
NSMutableString* url = [NSMutableString stringWithString:kUrlBase];
[url appendString:#"activityImgArray"];
//
__block NSArray* imgarr;
[[HTTPUtil sharedInstance] getImageArrayFromURL:url success:^(NSArray* array){
imgarr = [NSArray arrayWithArray:array];
}failure:^(NSError* error){
NSLog(#"%#", error);
}];
NSLog(#"adding...");
_vCycleScrollView.imageURLStringsGroup = imgarr;
[self.view addSubview:_vCycleScrollView];
[_vCycleScrollView mas_makeConstraints:^(MASConstraintMaker* make){
make.top.equalTo(self.view);
make.left.equalTo(self.view);
make.right.equalTo(self.view);
make.height.mas_equalTo(180);
make.width.equalTo(self.view.mas_width);
}];
In the console, I got
2016-05-20 14:41:19.411 SCUxCHG[10470:4909076] sharedInstance
2016-05-20 14:41:19.415 SCUxCHG[10470:4909076] getting...
2016-05-20 14:41:19.417 SCUxCHG[10470:4909076] adding...
2016-05-20 14:41:19.591 SCUxCHG[10470:4909076]
Response: {
data = {
img = "[activity/test1, acti/1]";
};
message = success;
result = 0;
}
I thought imgArr should be assigned in the success block and it shouldn't be nil when I assign it to _vCycleScrollView.imageURLStringsGroup.
However, I can tell from the output in the console that the HTTP request is sent after NSLog(#"adding..."); and that leads to the fact that imgArr is still nil when _vCycleScrollView.imageURLStringsGroup = imgarr; is executed.
Why is that?
Yes below code is in block so this will continue in background
[[HTTPUtil sharedInstance] getImageArrayFromURL:url success:^(NSArray* array){
imgarr = [NSArray arrayWithArray:array];
}failure:^(NSError* error){
NSLog(#"%#", error);
}];
solution - You should add _vCycleScrollView.imageURLStringsGroup = imgarr; inside of success block because you d0 not know when it will completed Or there is another way you should not call in block or should not create block.
Try bellow:
__block NSArray* imgarr;
[[HTTPUtil sharedInstance] getImageArrayFromURL:url success:^(NSArray* array){
imgarr = [NSArray arrayWithArray:array];
NSLog(#"adding...");
_vCycleScrollView.imageURLStringsGroup = imgarr;
}failure:^(NSError* error){
NSLog(#"%#", error);
}];
The completion block is executed once data is fetched.
In your case code continues to execute after the completion block is set but data hasn't been fetched yet, that's why imgarr is nil.
That's the whole idea: That blocks are executed out of order. The trick is that you don't wait for a block to finish. Instead, the block finishes and then it does what is needed. The code in your viewcontroller isn't going to work, can't work, and we don't want it to work. Instead, the callback block deposits the image somewhere, and then tells the tableview to reload the row.
I'm trying to use the iOS-GTLYouTube library for displaying videos. So far this is what I've got:
//Get the youtube video list
GTLServiceYouTube *service = [[GTLServiceYouTube alloc] init];
// Set the APIKey
service.APIKey = #"APIKey";
GTLQueryYouTube *query = [GTLQueryYouTube queryForVideosListWithIdentifier:[vidListIds copy] part:#"id,snippet"];
GTLServiceTicket *ticket = [ service executeQuery:query
completionHandler:^(GTLServiceTicket *ticket, id object, NSError *error)
{
// This callback block is run when the fetch completes
if (error == nil)
{
GTLYouTubeVideoListResponse *products = object;
// iteration of items and subscript access to items.
for (GTLYouTubeVideo *item in products)
{
GTLYouTubeVideoSnippetThumbnails *thumbnails = item.snippet.thumbnails;
[thumbnailMutableList addObject:thumbnails];
NSLog(#"Title: %#", item.snippet.title);
}
}
else
{
NSLog(#"Error: %#", error.description);
}
}];
I don't know why but this code didn't seem to do anything. There's neither the "Title:" log nor the "Error" log. I know that it was done in a separate thread so it worked asynchronously. But I've waited for 1-2 minutes and still nothing is shown. Can anybody help me here? Thanks.
Never mind. In the NSMutableString *vidListIds I forgot to append a comma. Silly me. Thanks for reading though. Sorry for the false alarm. :)
What are the best practices for making a serial queue of NSURLSessionTasks ?
In my case, I need to:
Fetch a URL inside a JSON file (NSURLSessionDataTask)
Download the file at that URL (NSURLSessionDownloadTask)
Here’s what I have so far:
session = [NSURLSession sharedSession];
//Download the JSON:
NSURLRequest *dataRequest = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task =
[session dataTaskWithRequest:dataRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Figure out the URL of the file I want to download:
NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSURL *downloadURL = [NSURL urlWithString:[json objectForKey:#"download_url"]];
NSURLSessionDownloadTask *fileDownloadTask =
[session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:playlistURL]]
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSLog(#"completed!");
}];
[fileDownloadTask resume];
}
];
Apart from the fact that writing a completion block within another completion looks messy, I am getting an EXC_BAD_ACCESS error when I call [fileDownloadTask resume]... Even though fileDownloadTask is not nil!
So, what is the best of way of sequencing NSURLSessionTasks?
You need to use this approach which is the most straight forward: https://stackoverflow.com/a/31386206/2308258
Or use an operation queue and make the tasks dependent on each others
=======================================================================
Regarding the HTTPMaximumConnectionsPerHost method
An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1
HTTPMaximumConnectionsPerHost only ensure that one shared connection will be used for the tasks of that session but it does not mean that they will be processed serially.
You can verify that on the network level using http://www.charlesproxy.com/, you wil discover that when setting HTTPMaximumConnectionsPerHost, your tasks will be still be started together at the same time by NSURLSession and not serially as believed.
Expriment 1:
Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
With task1: url = download.thinkbroadband.com/20MB.zip
With task2: url = download.thinkbroadband.com/20MB.zip
calling [task1 resume];
calling [task2 resume];
Result: task1 completionBlock is called then task2 completionBlock is called
The completion blocks might be called in the order you expected in case the tasks take the same amount of time however if you try to download two different thing using the same NSURLSession you will discover that NSURLSession does not have any underlying ordering of your calls but only completes whatever finishes first.
Expriment 2:
Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
task1: url = download.thinkbroadband.com/20MB.zip
task2: url = download.thinkbroadband.com/10MB.zip (smaller file)
calling [task1 resume];
calling [task2 resume];
Result: task2 completionBlock is called then task1 completionBlock is called
In conclusion you need to do the ordering yourself, NSURLSession does not have any logic about ordering requests it will just call the completionBlock of whatever finishes first even when setting the maximum number of connections per host to 1
PS: Sorry for the format of the post I do not have enough reputation to post screenshots.
Edit:
As mataejoon has pointed out, setting HTTPMaximumConnectionsPerHost to 1 will not guarantee that the connections are processed serially. Try a different approach (as in my original answer bellow) if you need a reliable serial queue of NSURLSessionTask.
An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1:
+ (NSURLSession *)session
{
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[configuration setHTTPMaximumConnectionsPerHost:1];
session = [NSURLSession sessionWithConfiguration:configuration];
});
return session;
}
then add tasks to it in the order you want.
NSURLSessionDataTask *sizeTask =
[[[self class] session] dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
#import "SessionTaskQueue.h"
#interface SessionTaskQueue ()
#property (nonatomic, strong) NSMutableArray * sessionTasks;
#property (nonatomic, strong) NSURLSessionTask * currentTask;
#end
#implementation SessionTaskQueue
- (instancetype)init {
self = [super init];
if (self) {
self.sessionTasks = [[NSMutableArray alloc] initWithCapacity:15];
}
return self;
}
- (void)addSessionTask:(NSURLSessionTask *)sessionTask {
[self.sessionTasks addObject:sessionTask];
[self resume];
}
// call in the completion block of the sessionTask
- (void)sessionTaskFinished:(NSURLSessionTask *)sessionTask {
self.currentTask = nil;
[self resume];
}
- (void)resume {
if (self.currentTask) {
return;
}
self.currentTask = [self.sessionTasks firstObject];
if (self.currentTask) {
[self.sessionTasks removeObjectAtIndex:0];
[self.currentTask resume];
}
}
#end
and use like this
__block __weak NSURLSessionTask * wsessionTask;
use_wself();
wsessionTask = [[CommonServices shared] doSomeStuffWithCompletion:^(NSError * _Nullable error) {
use_sself();
[self.sessionTaskQueue sessionTaskFinished:wsessionTask];
...
}];
[self.sessionTaskQueue addSessionTask:wsessionTask];
I use NSOperationQueue (as Owen has suggested). Put the NSURLSessionTasks in NSOperation subclasses and set any dependancies. Dependent tasks will wait until the task they are dependent on is completed before running but will not check the status (success or failure) so add some logic to control the process.
In my case, the first task checks if the user has a valid account and creates one if necessary. In the first task I update a NSUserDefault value to indicate the account is valid (or there is an error). The second task checks the NSUserDefault value and if all OK uses the user credentials to post some data to the server.
(Sticking the NSURLSessionTasks in separate NSOperation subclasses also made my code easier to navigate)
Add the NSOperation subclasses to the NSOperationQueue and set any dependencies:
NSOperationQueue *ftfQueue = [NSOperationQueue new];
FTFCreateAccount *createFTFAccount = [[FTFCreateAccount alloc]init];
[createFTFAccount setUserid:#"********"]; // Userid to be checked / created
[ftfQueue addOperation:createFTFAccount];
FTFPostRoute *postFTFRoute = [[FTFPostRoute alloc]init];
[postFTFRoute addDependency:createFTFAccount];
[ftfQueue addOperation:postFTFRoute];
In the first NSOperation subclass checks if account exists on server:
#implementation FTFCreateAccount
{
NSString *_accountCreationStatus;
}
- (void)main {
NSDate *startDate = [[NSDate alloc] init];
float timeElapsed;
NSString *ftfAccountStatusKey = #"ftfAccountStatus";
NSString *ftfAccountStatus = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:#"CHECKING" forKey:ftfAccountStatusKey];
// Setup and Run the NSURLSessionTask
[self createFTFAccount:[self userid]];
// Hold it here until the SessionTask completion handler updates the _accountCreationStatus
// Or the process takes too long (possible connection error)
while ((!_accountCreationStatus) && (timeElapsed < 5.0)) {
NSDate *currentDate = [[NSDate alloc] init];
timeElapsed = [currentDate timeIntervalSinceDate:startDate];
}
if ([_accountCreationStatus isEqualToString:#"CONNECTION PROBLEM"] || !_accountCreationStatus) [self cancel];
if ([self isCancelled]) {
NSLog(#"DEBUG FTFCreateAccount Cancelled" );
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:#"ERROR" forKey:ftfAccountStatusKey];
}
}
In the next NSOperation post data:
#implementation FTFPostRoute
{
NSString *_routePostStatus;
}
- (void)main {
NSDate *startDate = [[NSDate alloc] init];
float timeElapsed;
NSString *ftfAccountStatusKey = #"ftfAccountStatus";
NSString *ftfAccountStatus = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
if ([ftfAccountStatus isEqualToString:#"ERROR"])
{
// There was a ERROR in creating / accessing the user account. Cancel the post
[self cancel];
} else
{
// Call method to setup and run json post
// Hold it here until a reply comes back from the operation
while ((!_routePostStatus) && (timeElapsed < 3)) {
NSDate *currentDate = [[NSDate alloc] init];
timeElapsed = [currentDate timeIntervalSinceDate:startDate];
NSLog(#"FTFPostRoute time elapsed: %f", timeElapsed);
}
}
if ([self isCancelled]) {
NSLog(#"FTFPostRoute operation cancelled");
}
}
I am given a list of youtube IDs. For each of this IDS, I want to use the YouTube API to get the title, duration, and thumbnail. I then want the user to be able to click these thumbnails which takes them to a UIWebView where they can watch the video.
I'm really stuck on how I can actually create a video object from the ID. I dont really understand the process in given in Google's sample code, they get the title based on a playlist. I dont want it based on a playlist, I want it based on a stand-alone ID or URL.
Using a playlist query, the code uses:
if ([playlistID length] > 0) {
GTLServiceYouTube *service = self.youTubeService;
GTLQueryYouTube *query = [GTLQueryYouTube queryForPlaylistItemsListWithPart:#"snippet,contentDetails"];
query.playlistId = playlistID;
query.maxResults = 50;
_playlistItemListTicket = [service executeQuery:query
completionHandler:^(GTLServiceTicket *ticket,
GTLYouTubePlaylistItemListResponse *playlistItemList,
NSError *error) {
// Callback
_playlistItemList = playlistItemList;
_playlistFetchError = error;
_playlistItemListTicket = nil;
[self updateUI];
}];
}
Which I understand, but looking through GTLQueryYouTube I cant see anything that relates to a single video.
Does anybody have any ideas? Thanks!
Eventually I used:
GTLServiceYouTube *service = [[GTLServiceYouTube alloc] init];
// Services which do not require sign-in may need an API key from the
service.APIKey = #"myKey";
GTLQueryYouTube *query = [GTLQueryYouTube queryForVideosListWithPart:#"id, snippet, fileDetails"];
query.identifier = #"xxxxxxxxxx, yyyyyyyyyy";
GTLServiceTicket *ticket = [service executeQuery:query
completionHandler:^(GTLServiceTicket *ticket, id object, NSError *error) {
// This callback block is run when the fetch completes
if (error == nil) {
GTLYouTubeSearchListResponse *products = object;
// iteration of items and subscript access to items.
for (GTLYouTubeSearchResult *item in products) {
//NSMutableDictionary *dictionary = [item JSONValueForKey:#"id"];
GTLYouTubeThumbnailDetails *thumbnails = item.snippet.thumbnails;
GTLYouTubeThumbnail *thumbnail = thumbnails.high;
[imageArray addObject:thumbnail.url];
[titleArray addObject:item.snippet.title];
//NSLog(#"id: %#", [dictionary objectForKey:#"videoId"]);
//NSLog(#"Title: %#", item.snippet.title);
}
}else{
NSLog(#"Error: %#", error.description);
}
[self.thumbnailView reloadData];
}];
So created a queryForVideosListWithPart using a list of video ids separated by comma
I get this warning in Xcode
warning: Attempting to create USE_BLOCK_IN_FRAME variable with block
that isn't in the frame.
Xcode redirect me to my NSStream
_naturStream = [[NSInputStream alloc] initWithData:natur];
It is random when it does this error, and my application crashes when it is triggered. Anyone tried similar problem ?
thanks
EDIT
in the appDelegate.h
#property (nonatomic, strong) NSInputStream *naturStream;
In the appDelegate.m:
NSData *natur = [NSData dataWithContentsOfURL:[NSURL URLWithString:_locString]];
_naturStream = [[NSInputStream alloc] initWithData:natur];
[_naturStream open];
if (_naturStream) {
NSError *parseError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithStream:_naturStream options:NSJSONReadingAllowFragments error:&parseError];
if ([jsonObject respondsToSelector:#selector(objectForKey:)]) {
for (NSDictionary *natur in [jsonObject objectForKey:#"results"]) {
_poi = [[POI alloc]init];
[_poi setTitle:[natur objectForKey:#"title"]];
[_poi setLat:[[natur objectForKey:#"lat"]floatValue]];
[_poi setLon:[[natur objectForKey:#"lng"]floatValue]];
[_poi setDistance:[natur objectForKey:#"distance"]];
[_poi setWebUrl:[natur objectForKey:#"webpage"]];
[_naturArray addObject:_poi];
}
}
}
else {
NSLog(#"Failed to open stream.");
}
[_naturStream close];
}
I realized that i forgot [_naturStream close] i don't know if it has solved the problem or not ?
EDIT
Another thing,.... I use a Thread for fetching the JSON data:
dispatch_queue_t jsonParsingQueue = dispatch_queue_create("jsonParsingQueue", NULL);
// execute a task on that queue asynchronously
dispatch_async(jsonParsingQueue, ^{
[self parseJSON];
dispatch_async(dispatch_get_main_queue(), ^{
[_kortvisning updateAnno];
[visListe updateList];
});
});
// release the dispatch queue
dispatch_release(jsonParsingQueue);
Sounds like you're using ARC - if _naturStream is an instance variable for an objective C class, you might need to pull it out and add a __block reference so that ARC knows the scope correctly - but I'm guessing because I don't see how the block is used with the NSInputStream (if you post that part we might know). A good bit is here: http://nachbaur.com/blog/using-gcd-and-blocks-effectively
-- edit --
Ok, now that you posted the rest, I bet it has to do with the _kortvisning and visListe variables. I think you want to pull those out right after you create your queue something like
__block KortVisning *localKortVisning = _kortvisning;
__block NSMutableArray *localVisListe = visListe;
Then access those directly from your final completion handler you're sending back to the main queue.