Intro:
I have basically learned Obj-C and Cocoa Touch from Stackoverflow over the last few weeks, so bravo community, bravo.
Alas I have come to a question I cannot find an answer to/don't know how to Google.
I am making an app that 100% interfaces with an API service. Here's the process:
App launch -> Retrieve token
Subsequent requests -> HTTP Header token
My first view controller is a loading icon while a synchronous request takes place.
I would like to make Async requests in the future as currently my UI gets blocked while these requests take place.
My PHP background has caused me to set up my files like this, so let me know if this is incorrect.
APIConnect.h/.m -> Holds all my API methods
-(NSDictionary *)APIcall:(NSString *)api postData:(NSDictionary *)postData
TableViewController.h/.m -> Table based off data loaded from the method above.
When selecting a row, it moves forward in the UI and retrieves the details of that item (another API call).
When moving forward, it's apparent the main thread is blocked.
I therefore have tried to set up an async method
-(NSDictionary *)asyncAPIcall:(NSString *)api postData:(NSDictionary *)postData
APIConnect.m (abbreviated):
-(NSDictionary *)asyncAPIcall:(NSString *)api postData:(NSDictionary *)postData
{
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error){
if ([data length] > 0 && error == nil)
[self returnedData:data];
else
[self downloadError:error];
}];
}
-(void)returnedData:(NSData*)data
{
NSMutableDictionary *returnData = [[NSMutableDictionary alloc] init];
[returnData setDictionary:[NSJSONSerialization JSONObjectWithData:data
options:0
error:nil]];
NSLog(#"Returned Data %#", returnData);
[self setReturnURLData:returnData];
}
I really don't know how to ask my question, so maybe you can help me with that... However in theory I would like to:
Make an async request within APIConnect.m
Once async is done, it returns data to TableViewController.m
Once TableViewController receives data it will run it's processing and run [self.tableView reloadData];
All meanwhile, an activity indicator or something is visible to show that it's working on getting something
Any guidance on what to Google would be super helpful.
Related
I have some data that I am getting from a server and then displaying in my UIViewController class. To do this, I have two classes. The UIViewController and another one named ServerCommunicator. UIViewController is the delegate for ServerCommunicator class. The serverCommunicator looks as follows:
- (void)fetchServerData:(NSString *) serverAddress{
NSURL *url = [[NSURL alloc] initWithString:serverAddress];
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:url] queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error) {
[self.delegate fetchingSongsFailedWithError:error];
} else {
[self.delegate receivedSongsJSON:data];
}
}];
}
The UIViewController allocates the serverCommunicator, sets itself as delegate and then issue the fetch request.
- (void)viewDidLoad {
[super viewDidLoad];
self.songServerCommunicator = [[serverCommunicator alloc] init];
self.songServerCommunicator.delegate = self;
[self.songServerCommunicator fetchServerData:<some_server_ip>];
}
After it does that it implements the required protocol method:
- (void)receivedSongsJSON:(NSData *)data{
NSLog(#"received server response");
/* Parses the data and displays in textfield/imageview */
}
My problem is that when I do display the data received in the delegate method, it doesn't get reflected right away in the UI. It is very weird, sometimes it gets shown 20 seconds laters on its own, other times it takes like a minute. I am not sure whats going on. I know for a fact that the data was fetched right away because the logged message gets printed way before the UIView gets updated.
Thanks for any help on this.
Make sure you are on the main thread when you update the UI
Other people have pointed out the problem, but they did not provide the solution in concrete code. This is the coded solution:
- (void)fetchServerData:(NSString *) serverAddress{
NSURL *url = [[NSURL alloc] initWithString:serverAddress];
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:url] queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
dispatch_async(
dispatch_get_main_queue(),
^void {
if (error) {
[self.delegate fetchingSongsFailedWithError:error];
} else {
[self.delegate receivedSongsJSON:data];
}
}
);
}];
}
You must understand how Grand Central Dispatch works in iOS. GCD is an abstraction layer for multithreading.
The main queue, which your UI runs on, is a serial queue. That means you can't have two blocks of code running at the same time. It would be poor user experience if you were to do a long network query on the main queue, because it would prevent any UI code from running. This would make the app appear like it is frozen to the user.
To solve the freezing UI issue, iOS gives you other queues to do work without blocking up the main queue. iOS provides ways to create your own custom queues or use pre-made global concurrent queues. The queues that NSURLConnection uses is not known to us because we don't have the Cocoa source code. But what we do know is that NSURLConnection is definitely not using the main queue because UI is not frozen while it is grabbing data from a server.
Since NSURLConnection is running on a different thread than what the main queue (UI) runs on, you cannot just update UI at any random time. This is a common problem with multithreaded programs. When multiple threads access the same block of code, instructions may be interleaved and both blocks get unexpected results. One of the unexpected results that you experienced was the 20 second delay of the UI finally being updated. To prevent interleaving, you need to run all the UI code on the same thread. You do this by enqueuing your UI update code to the end of the main queue.
The method receivedSongsJSON() is called from a callback given to [NSURLConnection sendAsynchronousRequest] which I think is being called from a background thread.
Even if the method receivedSongsJSON() is declared in your UIViewController it will be executed in background thread if it is called from one.
As #robdashnash has indicated make sure you call all the UI updating code from main thread. If you are not sure how to do that please check the documentation of Grand Central Dispatch (GCD) here.
Let's say I want to use the rest api of some service, Twitbookr, to get a user's profile information. So I get the user to log in via my app and I get the credentials needed to make the call.
After this I load a new ViewController. I want to populate the fields in this view with the user's profile information. So I make my first call to the api:
[NSURLConnection sendAsynchronousRequest:req
queue:NSOperation.mainQueue
completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
// successfully fetch data using credentials
// assume complete code here including dispatch_async etc
self.data = data;
})];
Then I want to use the data:
self.nameLabel.text = [self.data objectForKey:#"name"];
self.userid = [self.data objectForKey:#"userid"];
But the request hasn't finished yet, so the program throws an error.
And I want to make more calls to get different types of data, for example, the pictures from the frontpage album, which rely on the userid which I can only get from the request above. So how do I make my next subsequent call making sure I already have the userid from the first call?
What's the correct way to handle this situation?
Should I be using synchronous requests instead?
Should I put up a loading symbol until all of my requests are done? If so, how do I test that the requests have actually finished? And what's the point of them being asynchronous?
Try this
[NSURLConnection sendAsynchronousRequest:req
queue:NSOperation.mainQueue
completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
// successfully fetch data using credentials
self.data = data;
//Use data here
dispatch_async(dispatch_get_main_queue(), ^{
//Update UI here
});
})];
When you use AsynchronousRequest,use data in completion block. I think your code
self.nameLabel.text = self.data.name;
is wrong,because self.data looks like NSData,and it not has a property of name
Update:As Mike said,only update UI on main thread.
I probably ask my question the wrong way and risk being blocked by stackoverflow completely. I have Asperger and no social skills, so I am very sorry for asking my last (?) question (because systems like these are only made for people without handicaps).
I am using GCD to load images and video from Instagram. I do this in an app that is very 'busy' with its user interface, and I want to keep that running smoothly, so I load the Instagram-media in the background.
The code below (which I probably formatted the wrong way so I apologize up front) works fine and does exactly want I want it to do. It loads images in the background (I left video's out to keep things simple) and my main UI is response while the images load. I display them between loads, and there works fine too.
However.
After 10 minutes, sometimes 20 minutes, sometimes 30 minutes and even sometimes after two hours my app gets OSSpinLockLock freezes. If I remove the code below I get no media, but the app never freezes.
I have searched for days on the web about alternative ways to do the job and to find an explanation for the OSSpinLockLock. No luck. Only thing I found was that using GCD could not result in an OSSpinLockLock. I have used all my knowledge of Instruments (which I must admit is more limited than I thought), but I cannot find a fault.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^(void) {
[[InstagramEngine sharedEngine] getMediaAtLocation:location count:kInstagramLocationBufferSize maxId:nil withSuccess:^(NSArray* media, InstagramPaginationInfo* paginationInfo) {
if (media && media.count>0) {
for (InstagramMedia* mediaObject in media) {
NSData* data = [[NSData alloc] initWithContentsOfURL:mediaObject.standardResolutionImageURL];
if (data) {
UIImage* img = [UIImage imageWithData:data];
if (img)
[self.locationBuffer addObject:img];
data = nil;
}
}
}
} failure:^(NSError *error) {
;
}];
});
If you look at this code, do you see anything that might cause that lock? Because I most certainly don't.
self.locationBuffer is declared in the .h as
#property (nonatomic,strong) NSMutableArray* locationBuffer;
and is properly allocated and initialized (otherwise it would be rather clear what the problem was). I have also tried not to put the UIImage but the NSData in the array but that made no difference whatsoever.
On my iPad mini retina for instance the CPU-load goes to 195% and stays around that number for a very long time. Eventually, sometimes after several hours, the app crashes.
Any suggestions would be very welcome.
Edit: As I see now on the ips-file on the iPad itself (which for some mysterious reason I cannot paste into this webpage (is stackoverflow still in an experimental stage?)) I see that the iPad did spent 16.000+ seconds on NSURLConnection...
my tought is that some timeout or failure blocks your gcd queues for too much time. try rewriting that code with NSOperationQueue, that way you can stop the queue on errors or view/controller going away.
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSOperationQueue_class/index.html
Update 1:
Here is my trial at your code, i added queue and logs, check them and also check the timeout value. This way if a request does not finish (most likely) you can trace it. All requests are serial so if one of them stops you should immediately notice it.
You can create more than one queue and access them sequentially (round robin) to have more requests simultaneously. I would not go with more than 4 queues which is also the default for most desktop internet browsers.
// keep the same queue for all request (class member?), don't put it in the same block
NSOperationQueue* queue= [[NSOperationQueue alloc] init];
// keep this in another code block from queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^(void) {
[[InstagramEngine sharedEngine] getMediaAtLocation:location count:kInstagramLocationBufferSize maxId:nil withSuccess:^(NSArray* media, InstagramPaginationInfo* paginationInfo) {
if (media && media.count>0) {
for (InstagramMedia* mediaObject in media) {
NSURL* url= mediaObject.standardResolutionImageURL;
NSLog(#"Start loading from %#", url);
NSURLRequest* req= [NSURLRequest requestWithURL:mediaObject.standardResolutionImageURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30]; // 30 seconds timeout
[NSURLConnection sendAsynchronousRequest:req queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(#"Stop loading from %# %# %#", url, response, connectionError);
if (data != nil && connectionError == nil) {
UIImage* img = [UIImage imageWithData:data];
if (img) {
// if you are triggering some ui update run on main thread
// [self.locationBuffer addObject:img];
[self.locationBuffer performSelectorOnMainThread:#selector(addObject:)
withObject:img
waitUntilDone:NO];
}
}
}];
}
}
} failure:^(NSError *error) {
NSLog(#"Error getting media list: %#", error);
}];
});
OSSpinLocks has bug on iOS8.3+: report
,which leads to such freezes.
I think you should replace OSSpinLocks with something else to fix this in your app (maybe create your own realization of spin lock - search SO for OSSpinLock).
This question is regarding the best way to implement connections and parsers in my app.
For Instance, my application contains 15 URL request which can return 15 expected return XML format responses.
Usually I Follow the following method.
1. URLManager //This file manages URL and request paths
2. MyConnection//In this Class, I make NSURLConnection and one delegate to return back to Controller when opertion is done. I distinguish URLs using enum.
3. Several Parsers for parsing NSData which my controller gets from Connection file. as parsing finished, I use Delegate to pass Parsed Data to Controller which is usually objects in an array to use in Controller.
By this way I need to make many parsers and also had to face stuck GUI for a whiile until values gets downloaded and parsed.
I want to know the standard way to develop an iOS application so that it could give best performance and stability.
Thanks
According to me.
The Best way is to use sendAsynchronousRequest with block coding
enum for the webservice respoce.
typedef void (^onGetSuccess)(BOOL success,NSArray *arrProduct);
**Webservice Calling**
-(void)getData:(onGetSuccess)block{
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:GET_SERVERT_URL]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if ([data length]>0) {
NSString *str=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *dict=[NSDictionary dictionaryWithXMLString:str];
NSMutableArray *arrRespoce=[dict objectForKey:#"dict"];
block(YES,arrRespoce);
}
else{
block(false,nil);
}
}];
}
For the xml to dict convertion
XMLDictionary
from one screen on click of a button i am navigating on other screen, i am navigating properly but screen is getting hang till the screen to which i am navigating gets web service response
(in viewDidLoad i am calling a web service)
how to fix this
Thanks
Move web service call to viewWillAppear or viewDidAppear, so call will initiate after controller view appears on screen.
Ideally you should perform the web service call in the background i.e. not on the main thread. Use NSOperation and NSOperationQueue or AFNetworking or Grand Central Dispatch. You can then initiate the call in viewDidLoad itself.
Here are a few links that can get you started.
How To Use NSOperations and NSOperationQueues
Networking Made Easy With AFNetworking
iOS Quick Tip: Interacting with Web Services - for using GCD
Hope that helps!
This is happen because of webservice contains heavy data that's whay you need to try this.
Load your images using [NSURLConnection sendAsynchronousRequest:queue:completionHandler: then use NSCache to prevent downloading the same image again and again.
As suggested by many developers go for SDWebimage and it does include the above strategy to download the images files .You can load as many images you want and the same URL won't be downloaded several times as per the author of the code
Example on
[NSURLConnection sendAsynchronousRequest:queue:completionHandler:
NSURL *url = [NSURL URLWithString:#"your_URL"];
NSURLRequest *myUrlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest: myUrlRequest queue: queue completionHandler: ^ (NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil)
//doSomething With The data
else if (error != nil && error.code == ERROR_CODE_TIMEOUT)
//time out error
else if (error != nil)
//download error
}];
Then use NSCache...