I´m trying to wait for the response using Restkit with Blocks.
Example:
NSArray *myArray = ["RESULT OF REST-REQUEST"];
// Working with the array here.
One of my Block-requests:
- (UIImage*)getPhotoWithID:(NSString*)photoID style:(NSString*)style authToken:(NSString*)authToken {
__block UIImage *image;
NSDictionary *parameter = [NSDictionary dictionaryWithKeysAndObjects:#"auth_token", authToken, nil];
RKURL *url = [RKURL URLWithBaseURLString:#"urlBase" resourcePath:#"resourcePath" queryParameters:parameter];
NSLog(#"%#", [url absoluteString]);
[[RKClient sharedClient] get:[url absoluteString] usingBlock:^(RKRequest *request) {
request.onDidLoadResponse = ^(RKResponse *response) {
NSLog(#"Response: %#", [response bodyAsString]);
image = [UIImage imageWithData:[response body]];
};
}];
return image;
}
You can't return anything in this method since the getting of the image will be asynchronous - it must be -(void).
So, what do you do? You should put the action calling this method inside the response block. Be wary of retain cycles in the block.
__block MyObject *selfRef = self;
[[RKClient sharedClient] get:[url absoluteString] usingBlock:^(RKRequest *request) {
request.onDidLoadResponse = ^(RKResponse *response) {
NSLog(#"Response: %#", [response bodyAsString]);
image = [UIImage imageWithData:[response body]];
[selfRef doSomethingWithImage:image];
};
}];
The code above won't work with ARC turned on (XCode default since iOS 5.0). __block variables are no longer exempted from auto-retain under ARC. Use __weak instead of __block in iOS 5.0 and above to break the retain cycle.
Related
I am pretty new to iOS development, and entered a position where I need to maintain a large existing project in obj-c.
I have a sidebar-menu which is a webview. When program starts it makes a url request to check whether there is a newer version of the menu, and in that case retrieves the latest version.
Right now when the app runs for the first time it shows the old version, and from the second time and on it shows the current version.
When I tried debugging I've seen that the method that compares between local and remote version gets an empty value for the remote version. As far as I can understand it, the url request for the latest version is async, and therefore the code continues to execute before the request returns the current version.
Following an answer from StackOverflow, I've tried to call the getDataConfiguration method from within viewDidLoad instead of from AppDelegate, but that didn't work.
Would appreciate any help!
relevant code:
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { .
...
[DataManager getDataConfiguration:^(DataConfiguration *dataConfiguration, NSError *error) {
[AppData sharedInstance].dataConfiguration=dataConfiguration;
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:dataConfiguration];
[standardDefaults setObject:encodedObject forKey:DATA_KEY];
[standardDefaults synchronize];
}];
[DataManager getProductMap:^(ProductsArray *products, NSError *error) {
[AppData sharedInstance].productsArray=products;
}];
DataManager.m
+(void)getDataConfiguration:(void (^)(DataConfiguration * dataConfiguration, NSError *error))completion
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:[Configuration sharedInstance].infoJSONURL parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
DataConfiguration * dataConfiguration = [DataConfiguration modelObjectWithDictionary:responseObject];
completion(dataConfiguration,nil);
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
+(void)updateHtmlFiles:(void (^)(NSError *error))completion{
float upToDateMenuVersion = [[AppData sharedInstance] dataConfiguration].general.menuVersion;
float localMenuVersion = [self getLocalMenuVersion];
if(upToDateMenuVersion != localMenuVersion){
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSString *url = [NSString stringWithFormat:#"%#?v=%f", [Configuration sharedInstance].menuHTMLFileURL, [[NSDate new] timeIntervalSince1970]];
[manager GET:url parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSDictionary *htmlFiles = [userDefaults dictionaryForKey:#"HTML_FILES"];
NSMutableDictionary *mutableHtmlFiles = [NSMutableDictionary new];
NSString *myString = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
[mutableHtmlFiles setValue:myString forKey:#"MENU"];
[userDefaults setObject:mutableHtmlFiles forKey:#"HTML_FILES"];
[self setLocalMenuVersion:upToDateMenuVersion];
completion(nil);
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSDictionary *htmlFiles = [userDefaults dictionaryForKey:#"HTML_FILES"];
if(htmlFiles == nil){
NSString *menuFile = [[NSBundle mainBundle] pathForResource:#"menu" ofType:#"html"];
htmlFiles = #{#"MENU":[NSString stringWithContentsOfFile:menuFile encoding:NSUTF8StringEncoding error:nil]};
[userDefaults setObject:htmlFiles forKey:#"HTML_FILES"];
}
NSLog(#"Error: %#", error);
}];
}
}
+(void) setLocalMenuVersion: (float) version{
[[NSUserDefaults standardUserDefaults] setFloat:version forKey:#"menuVersion"];
}
+(float) getLocalMenuVersion {
return [[NSUserDefaults standardUserDefaults] floatForKey:#"menuVersion"];
}
Menu.m
- (void)viewDidLoad {
[super viewDidLoad];
_firstLoad = YES;
...
[self initWebView];
}
-(void) initWebView {
if(_webView == nil){
_webView = [[WKWebView alloc] initWithFrame:_webViewPlaceholder.frame];
[_webView.scrollView setZoomScale:3 animated:YES];
_webView.navigationDelegate = self;
_webView.UIDelegate = self;
NSString *javaScriptText = #"document.body.style.zoom = 3;";
[_webView evaluateJavaScript:javaScriptText completionHandler:nil];
[self.view addSubview:_webView];
_webView.scrollView.bounces = NO;
[self updateHtml];
[AppData updateHeaderAndMenu:^(NSError *error){
[self updateHtml];
}];
}
}
- (void)viewDidAppear:(BOOL)animated{
_webView.frame = CGRectMake(_webViewPlaceholder.frame.origin.x,_webViewPlaceholder.frame.origin.y, _webViewPlaceholder.frame.size.width, _webViewPlaceholder.frame.size.height);
}
-(void)updateHtml{
NSDictionary *htmlFiles = [AppData getHeaderAndMenu];
NSString *menu = [htmlFiles objectForKey:#"MENU"];
[_webView loadHTMLString:menu baseURL: [[NSBundle mainBundle] bundleURL]];
}
AppData.m
+(void)updateHeaderAndMenu:(void (^)(NSError *error))completion{
[DataManager updateHtmlFiles:completion];
}
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
...
[AppData updateHeaderAndMenu:^(NSError *error){ [self loadHeader]; }];
_firstLoad = YES;
...
The updateHeaderAndMenu method has a completion block which is called after the async operation completes without an error.
I'm assuming ViewController.m holds a reference to Menu?
If that is the case, viewDidLoad calls the updateHeaderAndMenu method and will execute the completionBlock (if there is no error). In this block I can already see that a method is called loadHeader. You could call [self.menu updateHtml]; there and this would probably work.
...
[AppData updateHeaderAndMenu:^(NSError *error){
[self loadHeader];
// [self.menu updateHtml];
}];
_firstLoad = YES;
...
I'm doing some guess work here but I think this would update your webview after the DataManager completes the http request.
Edit:
As to the order of execution. Here is a breakdown:
This is the method definition in AppData
+(void)updateHeaderAndMenu:(void (^)(NSError *error))completion{
[DataManager updateHtmlFiles:completion];
}
You can see completion (which is a block parameter) is passed on to the updateHtmlFiles method in DataManager:
+(void)updateHtmlFiles:(void (^)(NSError *error))completion{
...
completion(nil);
...
}
Eventually the completion parameter (which is a block) is called when the async http request completes. You can look at blocks as kind of inline methods which can be passed as a parameter. Google working with blocks ios to see the official Apple documentation for this.
So the order of execution is:
Menu calls updateHeaderAndMenu in AppData
which calls updateHtmlFiles in DataManager and passes on completion
http request completes and calls completion.
the content of the block is executed all the way back in Menu which is:
{
[self loadHeader];
// [self.menu updateHtml];
}
loadHeader is executed ...
If you want to get a better overview of what is called when, you can use breakpoints inside your code.
I have an app which downloads a set of photos from a server. I am using an Asynchronous request because I don't want the UI to be blocked. However, I am finding that the request is very slow and takes ages to load.
I know you can set the queue type to [NSOperationQueue mainQueue] but that just puts the Asynchronous request back on the main thread which defeats the whole point of making the request Asynchronously in the first place.
Is there anyway to speed up the request or to tell iOS: "Run this request in the background, but do it ASAP, don't leave it till the end of the queue"???
Here is my code:
// Set up the photo request.
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:PHOTO_URL, pass_venue_ID, PHOTO_CLIENT_ID, PHOTO_CLIENT_SECRET]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// Begin the asynchromous image loading.
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error == nil) {
// Convert the response data to JSON.
NSError *my_error = nil;
NSDictionary *feed = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&my_error];
// Check to see if any images exist
// for this particular place.
int images_check = [[NSString stringWithFormat:#"%#", [[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"count"]] intValue];
if (images_check > 0) {
// Download all the image link properties.
images_prefix = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"prefix"];
images_suffix = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"suffix"];
images_width = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"width"];
images_height = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"height"];
// Set the image number label.
number_label.text = [NSString stringWithFormat:#"1/%lu", (unsigned long)[images_prefix count]];
// Download up to 5 images.
images_downloaded = [[NSMutableArray alloc] init];
// Set the download limit.
loop_max = 0;
if ([images_prefix count] > 5) {
loop_max = 5;
}
else {
loop_max = [images_prefix count];
}
for (NSUInteger loop = 0; loop < loop_max; loop++) {
// Create the image URL.
NSString *image_URL = [NSString stringWithFormat:#"%#%#x%#%#", images_prefix[loop], images_width[loop], images_height[loop], images_suffix[loop]];
// Download the image file.
NSData *image_data = [NSData dataWithContentsOfURL:[NSURL URLWithString:image_URL]];
// Store the image data in the array.
[images_downloaded addObject:image_data];
}
// Load the first image.
[self load_image:image_num];
}
else if (images_check <= 0) {
// error...
}
}
else {
// error
}
}];
Thanks for your time, Dan.
i think your problem isnt the request running slow, its that you are updating UI elements not on the main thread, surround any UI updates (like setting the text on labels) with
dispatch_sync(dispatch_get_main_queue(), ^{
<#code#>
});
As Fonix said its not iOS that responding slow but dataWithContentsOfURL doesn't work in background thread. Apple's recommendation is that you should use NSURLConnection asynchronously with delegates
- didReceiveResponse
- didReceiveData
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:theURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:_mAuthenticationTimeoutInterval];
In these methods you can make use of chunks of data as well.
If you actually want these multiple downloads to be faster you should use parallel downloading using NSOperationQueue. You can refer enter link description here
I think a good solution could be using AFNetworking when combined with NSOperation, check this code I wrote to do more than one operation asynchronously
NSMutableArray *operations = [[NSMutableArray alloc] init];
for(NSObject *obj in caches) {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:url];
//...set up your mutable request options here
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
operation.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"application/json"];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSInteger statusCode = operation.response.statusCode;
if(statusCode==200) {
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"API Call error:%#", error.localizedDescription);
}];
[[requestManager operationQueue] addOperation:operation];
[operations addObject:operation];
if([operations count] >= MAX_API_CALL) break;
}
[AFHTTPRequestOperation batchOfRequestOperations:operations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
} completionBlock:^(NSArray *operations) {
NSError *error;
for (AFHTTPRequestOperation *op in operations) {
if (op.isCancelled){
}
if (op.responseObject){
// process your responce here
}
if (op.error){
error = op.error;
}
}
}];
I am trying to redo some code to use AFNetworking. I have this method below:
-(NSArray *)GetTableDataOfPhase:(NSString *)phase
{
NSString *phaseRequestString = [NSString stringWithFormat:#"%#?jobNo=%#",kIP,phase];
NSURL *JSONURL = [NSURL URLWithString:phaseRequestString];
NSURLResponse* response = nil;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:JSONURL];
NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
if(data == nil)
return nil;
NSError *myError;
NSArray *tableArray = [[NSArray alloc]initWithArray:[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&myError]];
return tableArray;
}
and right now I am trying to alter it so it still returns an array, I have tried doing this:
-(NSArray *)GetTableDataOfPhase:(NSString *)phase
{
NSString *phaseRequestString = [NSString stringWithFormat:#"%#?jobNo=%#",kIP,phase];
NSURL *JSONURL = [NSURL URLWithString:phaseRequestString];
NSURLResponse* response = nil;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:JSONURL];
AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:request] autorelease];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSData* data = [NSURLConnection sendSynchronousRequest:responseObject returningResponse:&response error:nil];
if(data == nil)
return nil;
NSError *myError;
NSArray *tableArray = [[NSArray alloc]initWithArray:[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&myError]];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
[operation start];
return tableArray;
}
but I got this error:
/Users/jamessuske/Documents/My Programs/SSiPad(Device Only)ios7/SchedulingiPadApplication/Classes/LHJSonData.m:168:46: Incompatible block pointer types sending 'void *(^)(AFHTTPRequestOperation *, id)' to parameter of type 'void (^)(AFHTTPRequestOperation *, id)'
and this warning:
/Users/jamessuske/Documents/My Programs/SSiPad(Device Only)ios7/SchedulingiPadApplication/Classes/LHJSonData.m:170:97: Sending 'NSURLResponse *const *' to parameter of type 'NSURLResponse **' discards qualifiers
This is how I am calling it:
- (void)GetRequest
{
//refresh table view
[dataSource.editedCellHolder removeAllObjects];
[dataSource.cellHolder removeAllObjects];
[dataSource.cellHolderDisplay removeAllObjects];
NSArray *tableData = [dataSource.areaData GetTableDataOfPhase:[NSString stringWithFormat:#"%#%#",areaPickerSelectionString,unitPickerSelectionString]];
if(tableData == nil)
[self CustomAlert:#"Data was not recieved from the server, please check internet/VPN settings, Or contact Software Vendor for assistance."];
[dataSource PopulateTableData:tableData];
[indicatorView stopAnimating];
[indicatorView removeFromSuperview];
[loadingView removeFromSuperview];
loadingView = nil;
indicatorView =nil;
[NSTimer scheduledTimerWithTimeInterval:0.2f target:self selector:#selector(DisplayTable) userInfo:nil repeats:NO];
}
A couple of things:
Using AFNetworking, you should entirely lose the NSURLConnection request.
Likewise, the default responseSerializer does the JSON parsing for you, so you can lose the NSJSONSerialization parsing. AFNetworking does all of that for you.
Likewise, don't build URL parameters manually, but rather again let AFNetworking do that for you. By default, AFNetworking uses a requestSerializer that will build the request for you.
Your old method ran synchronously, which is generally a bad idea. Instead, you should use asynchronous patterns (e.g. a completionHandler block).
So, pulling all of this together, it probably looks like:
- (void)getTableDataOfPhase:(NSString *)phase completionHandler:(void (^)(NSArray *resultsObject, NSError *error))completionHandler
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *parameters = #{#"jobNo" : phase};
[manager GET:kIP parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
completionHandler(responseObject, nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completionHandler(nil, error);
}];
}
And you'd call it like so:
[self getTableDataOfPhase:#"..." completionHandler:^(NSArray *resultsObject, NSError *error) {
if (resultsObject) {
// use NSArray here
} else {
NSLog(#"error = %#", error);
}
}];
// but don't try to use the `resultsObject` array here!
i have a request which returns information from a php web service. I'm having trouble adding this to a array which can be used in my UICollectionView. It seems like whatever i do i cant return the data. I think it is because i'm returning the array before i've added any objects. I've tried placing the NSLog several places, but without luck. What am i doing wrong?
When i place this NSLog(#"%d", imagesArray.count); beyond the request it returns 0.
ViewDidAppear:
-(void)viewDidAppear:(BOOL)animated {
imagesArray = [[NSMutableArray alloc] init];
[self getImages];
}
getImages method:
-(void)getImages {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[manager GET:#"http://URL.COM" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:responseObject options:kNilOptions error:nil];
NSString *theTitle = [[json objectForKey:#"response"] valueForKey:#"title"];
NSString *theUrl = [[json objectForKey:#"response"] valueForKey:#"imageUrl"];
[imagesArray addObject:[[NSMutableDictionary alloc] initWithObjectsAndKeys:
theTitle, #"title",
theUrl, #"url",
nil]];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
NSLog(#"%d", imagesArray.count);
}
AFNetworking is Asynchronous, you have to place the log inside the success block. Otherwise the array will always be empty.
One good solution would be to pass a block to your getImages function like that
-(void) getImages:(void (^)(BOOL result))callback {
// your code here then you call callback(YES or NO) inside your success or failure block.
}
[self getImages:^(BOOL result){
if(result)
//we got the images, we can now display them etc.
}];
Fortunately I know where my memory pressure issue is coming from, and I have tried a number of techniques such as wrapping a block in an #autorelease block and setting objects to nil but still no success.
Sorry for dumping too much code here, I tried to cut it down to the essentials. Here is the code for downloading and saving images:
NSMuttableArray *photosDownOps = [NSMuttableArray array];
NSURL *URL = [...];
NSURLRequest *request = [...];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer = [AFImageResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
dispatch_queue_t amBgSyncQueue = dispatch_queue_create("writetoFileThread", NULL);
dispatch_async(amBgSyncQueue, ^{
[self savePhotoToFile:(UIImage *)responseObject usingFileName:photo.id];
});
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if ([error code] != NSURLErrorCancelled)
NSLog(#"Error occured downloading photos: %#", error);
}];
[photosDownOps addObject:op];
NSArray *photosDownloadOperations = [AFURLConnectionOperation batchOfRequestOperations:photosDownloadOperatons
progressBlock:^(NSUInteger nof, NSUInteger tno) {
} completionBlock:^(NSArray *operations) {
NSLog(#"all photo downloads completed");
}];
[self.photosDownloadQueue addOperations:photosDownloadOperations waitUntilFinished:NO];
+ (void) savePhotoToFile:(UIImage *)imageToSave usingFileName:(NSNumber *)photoID{
#autoreleasepool {
NSData * binaryImageData = UIImageJPEGRepresentation(imageToSave, 0.6);
NSString *filePath = [Utilities fullPathForPhoto:photoID];
[binaryImageData writeToFile:filePath atomically:YES];
binaryImageData = nil;
imageToSave = nil;
}
}
This situation though only happens with iPhone 4s devices that I have tested on, it does not happen on iPhone 5 models.
I managed to solve this by extending NSOperation and within the main block immediately after I receive the data I write it out to file:
- (void)main{
#autoreleasepool {
//...
NSData *imageData = [[NSData alloc] initWithContentsOfURL:imageUrl];
if (imageData) {
NSError *error = nil;
[imageData writeToFile:imageSavePath options:NSDataWritingAtomic error:&error];
}
//...
}
}
This NSOperation object was then added a NSOperationQueue I already had.
Try to create your own class to download image using NSUrlConnection and in the delegate method append that data to your file just see the below code
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:aPath];
[fileHandle seekToEndOfFile];
[fileHandle writeData:data];
[fileHandle closeFile];
}
This will help you in memory management as all the data which is download is not need to cache .