Async downloads with NSURLConnection prove unreliable - ios

I have been using NSURLConnection to perform some async downloads. Basically I retrieve a PFFile from Parse backend and display it in a web view. I offer an option to download to device. Everything is good to go and all tasks perform as required, however, lately i've noticed if I perform downloads at a high rate of speed some of them don't show up in the Downloads VC. I don't know how to test it, but my theory is it's getting cut off because the priority is set to High, EVEN THOUGH, users can't do anything until the download is complete. You have to exit out of the current UIWebView to select another document. This isn't a consist behavior which makes it harder for me to narrow down. It only happens when I download > exit > open > & download another document right away > exit > repeat until done and there is no specific document it does it with.
THE PROBLEM : downloads perform, and I get an NSLog of file created, however, when I go to the Downloads VC the document doesn't show up in the NSFRC, but still shows as a valid file in the directory.
THE PROCESS
How the download happens,
The UIWebView loads a PDF queried from Parse.com. No problem here.
A ProgressHUD displays
Once loaded the progressHUD goes away.
If they want to download the PDF they just click on the action sheet index for downloading and it starts the download process.
Another Progress HUD displays as you can see in the NSURL didReceiveData displays. This HUD does not allow user interaction until the download is complete. So you can't exit the View Controller to select another pdf until it's done completely done downloading. So NO i am not conducting numerous downloads simultaneously, it's one download at a time.
Then the user can exit after download complete, select another UITableViewCell which loads the respective PDF and repeats the process.
Loading the PDF :
PFQuery *EGQuery = [PFQuery queryWithClassName:#"Classname"];
[EGQuery whereKey:#"KeyName" equalTo:self.title];
[EGQuery getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error) {
if ([object objectForKey:#"EGFile"]) {
PFFile *file = [object objectForKey:#"EGFile"];
self.pdfURL = [file url];
self.pdfName = [file name]; //Ends up as EG_2014_04 this is what I append to the filePath where it's stored so the filePath will be /PDFs/EG_2014_04
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.pdfURL]]];
} else {
}
Download method :
NSManagedObjectContext *context = [self managedObjectContext];
NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Downloads"];
[request setPredicate:[NSPredicate predicateWithFormat:#"pubnumber = %#", self.title]];
[request setFetchLimit:1];
NSUInteger count = [context countForFetchRequest:request error:&error];
if (count == NSNotFound) {
} else if (count == 0) {
NSURL *url = [NSURL URLWithString:pdfURL]; //pdfURL is PFFile url
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[connection start];
NSLog(#"Background time remaining = %.1f seconds", [UIApplication sharedApplication].backgroundTimeRemaining);
}];
NSData *pdfData = [[NSData alloc]
initWithContentsOfURL:[NSURL URLWithString:self.pdfURL]];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:#"PDFs"];
NSString *filePath = [path stringByAppendingPathComponent:self.pdfName];
NSFileManager *fm = [NSFileManager defaultManager];
[fm createFileAtPath:filePath contents:pdfData attributes:nil];
if ([fm createFileAtPath:filePath contents:pdfData attributes:nil])
{
dispatch_async(dispatch_get_main_queue(), ^{
NSManagedObject *newDWNLD = [NSEntityDescription insertNewObjectForEntityForName:#"Downloads" inManagedObjectContext:context];
[newDWNLD setValue:self.title forKey:#"pubnumber"];
[newDWNLD setValue:self.pubTitle forKey:#"pubtitle"];
[newDWNLD setValue:self.pdfName forKey:#"puburl"]; // this is what I use for the file path name in the PDF directory and this is how I call it in my NSFRC
});
NSLog(#"File was created");
} else {
NSLog(#"File not created");
}
});
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
expectedLength = MAX([response expectedContentLength], 1);
currentLength = 0;
HUD.dimBackground = YES;
HUD.mode = MBProgressHUDModeAnnularDeterminate;
HUD.labelText = #"Downloading...";
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
currentLength += [data length];
HUD.progress = currentLength / (float)expectedLength;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
HUD.customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"success.png"]];
HUD.mode = MBProgressHUDModeCustomView;
HUD.labelText = #"Success!";
HUD.detailsLabelText = #"Added to Downloads";
HUD.dimBackground = YES;
[HUD hide:YES afterDelay:1.6];
//Cancel Background task if completed
//[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
//self.backgroundTask = UIBackgroundTaskInvalid;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"Error %#", error);
[HUD hide:YES];
}
DOWNLOADS VC
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Downloads" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:50];
NSSortDescriptor *azSort = [[NSSortDescriptor alloc] initWithKey:#"pubnumber" ascending:YES];
NSArray *azSortArray = #[azSort];
[fetchRequest setSortDescriptors:azSortArray];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"pubnumber" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController;
}
cellForRowAtIndexPath:
NSString *title = [NSString stringWithFormat:#"%#", [context valueForKey:#"pubnumber"]];
cell.textLabel.text = title;
etc etc
EDIT It seems to download all of them, and 'creates' a file, however it doesn't display all of them in the VC.
application DidFinishLaunchingWithOptions:
NSString *createPaths;
NSArray *documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
createPaths = [[documents objectAtIndex:0] stringByAppendingPathComponent:#"PDFs"];
NSLog(#"PDFs directory: %#", [[NSFileManager defaultManager] contentsOfDirectoryAtPath:createPaths error:&error]);
The above accrucately logs all the files that I downloaded, but they just aren't either getting recognized as fully downloaded from the UIWebView or creating the file path is going awry somehow during the download process so it's not displaying in the downloads view controller but the other documents do show up. It's consistently 1-2 missing from each session but like i stated before, its never the same document missing, it's whatever feels like missing is missing. Say I download 10 then close the app then re open it 9 will only be there in the Downloads VC sometimes 8, but it logs as being a valid filePath in the directory.

The problem appears to be your managed object context thread confinement, specifically you aren't confining. You get the context on the original thread, presumably the main thread, but then you capture it in the background block and access it directly.
You should take the result of saving your file and send it back to the main thread, then create and save the new managed object there.

Related

How to call my view controller method into appdelegate.m?

I am having action for login button in view controller but i have to use some condition in appdelegate.m that if user logged in already then viewcontroller login action method will fire and if not logged in then only login page will open?
Please help me
in AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([[NSUserDefaults standardUserDefaults]boolForKey:#"IsFirstTime"])
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle: nil];
HomePageVC *lvc = [storyboard instantiateViewControllerWithIdentifier:#"HomePageVC"];
[(UINavigationController *)self.window.rootViewController pushViewController:lvc animated:NO];
}
else
{
[[NSUserDefaults standardUserDefaults]setBool:YES forKey:#"IsFirstTime"];
[[NSUserDefaults standardUserDefaults]synchronize];
}
return YES;
}
in viewcontroller.m
- (IBAction)Login:(id)sender
{
[self.indicator startAnimating];//The ActivityIndicator Starts Animating Here
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:BaseUrl#"login"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[request addValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[request addValue:#"*/*" forHTTPHeaderField:#"Accept"];
[request setHTTPMethod:#"POST"];
NSString *mapData = [NSString stringWithFormat:#"userName=gautam.kar#eyeforweb.com&userPassword=1234567&api_key=ZWZ3QDEyMw==&api_password=456789"];
NSData *postData = [mapData dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
[request setHTTPBody:postData];
NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(error == nil)
{
NSString *text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"Data = %#",text);
NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
NSLog(#"jsondic= %#",jsonDic);
NSDictionary *userDataDic = [jsonDic objectForKey:#"record"];
[DataModel setEmailAdd:[userDataDic objectForKey:#"emailAdd"]];
[DataModel setName:[userDataDic objectForKey:#"Name"]];
[DataModel setCity:[userDataDic objectForKey:#"city"]];
[DataModel setCountry:[userDataDic objectForKey:#"country"]];
[DataModel setRegistrationID:[userDataDic objectForKey:#"registrationID"]];
[DataModel setPhoneNo:[userDataDic objectForKey:#"phoneAdd"]];
[DataModel setState:[userDataDic objectForKey:#"state"]];
[DataModel settimeZone:[userDataDic objectForKey:#"timezone"]];
[DataModel setDisclaimer:[userDataDic objectForKey:#"disclaimer"]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.indicator stopAnimating];//The ActivityIndicator Stops Animating when Response Arrives
NSString * text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"text= %#",text);
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
[self checkUserSuccessfulLogin:json];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.indicator stopAnimating];
});
NSLog(#"Error : %#",error.description);
}
}];
[postDataTask resume];
}
- (void)checkUserSuccessfulLogin:(id)json
{
// NSError *error;
NSDictionary *dictionary = (NSDictionary *)json;
if ([[dictionary allKeys] containsObject:#"login"])
{
if ([[dictionary objectForKey:#"login"] boolValue])
{
NSString *strID = [[NSUserDefaults standardUserDefaults] stringForKey:#"textField1Text"];
NSString *strPWD = [[NSUserDefaults standardUserDefaults] stringForKey:#"textField2Text"];
[[NSUserDefaults standardUserDefaults] setValue:[dictionary objectForKey:#"user_id"] forKey:#"CurrentUserLoggedIn"];
NSString *strUser = [[NSUserDefaults standardUserDefaults] stringForKey:#"CurrentUserLoggedIn"];
[[NSUserDefaults standardUserDefaults]synchronize];
[self saveLoginFileToDocDir:dictionary];
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
HomePageVC *vc = [mainStoryboard instantiateViewControllerWithIdentifier:#"HomePageVC"];
[self.navigationController pushViewController:vc animated:YES];
}
else
{
NSLog(#"Unsuccessful, Try again.");
UIAlertView *alertLogin = [[UIAlertView alloc]initWithTitle:#"Error" message:#"Wrong Username Or Password" delegate:self cancelButtonTitle:#"cancel" otherButtonTitles:nil];
[alertLogin show];
}
}
}
- (void)saveLoginFileToDocDir:(NSDictionary *)dictionary
{
NSArray *pListpaths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString *pListdocumentsDirectory = [pListpaths objectAtIndex:0];
NSString *path = [pListdocumentsDirectory stringByAppendingPathComponent:#"Login.plist"];
BOOL flag = [dictionary writeToFile:path atomically:true];
if (flag)
{
NSLog(#"Saved");
}
else
{
NSLog(#"Not Saved");
}
}
- (NSDictionary *)getLoginFileFromDocDir
{
NSArray*pListpaths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString*pListdocumentsDirectory = [pListpaths objectAtIndex:0];
NSString *path = [pListdocumentsDirectory stringByAppendingPathComponent:#"Login.plist"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
return dict;
}
What you need is not to check your controller in AppDelegate.m, even though that is what you're asking.
Your real problem is " how can I access data from two different places ? ".
Right now, you're telling AppDelegate he "knows" your view controllers. It shouldn't.
What you need is one (actually a lot more but you'll learn that with time) new class, that handles the Login calls and state, and all that is login related.
Call that class the... LoginManager.
In that class, you could have some methods, like Login() or Logout(), or anything you would like.
Now you have an external source of data, your login manager knows everything he musts knows about the login. You should even add some properties, like a boolean IsLoggedIn or anything you might need.
And that source of data is what AppDelegate needs to know. Not the controllers. With that kind of architecture, EVERYONE that needs the login information can access it from that class (which could / should be a singleton class, look it up on the internet, its very easy.
In your viewcontroller, you can simply do Loginmanager.login, and in appdelegate, you can check .isloggedin.
That helps you a lot, because you don't have to instantiate view controllers in appdelegate, which is really a lot of work. You're splitting the work and the tasks between classes, which is what a good programmer does. Remember, your class should have only one job, not more, not less. Your VC handles the user inteface, not the webservic calls, not the login, nothing. If it does, it means you need to create another class :)
Once you've implemented all that (read my answer as many times as necessary to make sure you understand), you'll have no problem accessing that kind of data in other place of your app.
Note that you shouldn't abuse singleton classes or static classes (especially static), but again, you'll probably make many mistakes and learn from them, like we all did when we started.
Create your ViewController Object like below,
viewcontroller *objYourVC=[[viewcontroller alloc]init];
Now call method from Appdelegate like below:
[objYourVC functionToBeCalled:nil];
OR
[objYourVC functionToBeCalled:self];
Example,
if(AlreadyLogin){
//call viewcontroller method
viewcontroller *objYourVC=[[viewcontroller alloc]init];
[objYourVC functionToBeCalled:nil];
}

Core Data Set Up, Saving Duplicates and Returning Empty Objects

ANSWERED Q2 by FIXING Q1, FEEL FREE TO EXPLAIN IF YOU KNOW WHY
Full disclosure, I dabble in Objective C so this could be painfully obvious but I could not find similar problems or more accurately answers to similar problems.
I'm writing a very simple app and I am learning about core data and I have a couple of issues
Q.1 Has been answered so here is Q.2
Q2. When fetching from Core Data I retrieve empty objects?
With the Data Controller being the same as the above question I call
- (NSMutableArray*) returnTimers
{
[self initilizeDataLayer];
NSMutableArray *listOfTimers = [[NSMutableArray alloc]init];
NSFetchRequest *request = [[NSFetchRequest alloc]initWithEntityName:#"Timer"];
NSError *error = nil;
NSArray *listOfTimersRaw = [myDataController.managedObjectContext executeFetchRequest:request error:&error];
if (error != nil) {
//Deal with failure
}
else {
//Deal with success
listOfTimers = [NSMutableArray arrayWithArray:listOfTimersRaw];
}
return listOfTimers;
}
This correctly retrieves 2 objects but they are empty?
Again this is experimentation so may be arse ways but I have to learn somehow.....Any help would be greatly appreciated.
BELOW HERE HAS BEEN ANSWERED THANKS
Q1. Saving Objects to Core Data creates a blank row in sqlite db - Why?
I have set up a DataController Object like in the Apple Docs
- (void)initializeCoreData
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"WorkRestWork" withExtension:#"momd"];
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSAssert(mom != nil, #"Error initializing Managed Object Model");
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[moc setPersistentStoreCoordinator:psc];
[self setManagedObjectContext:moc];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentsURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *storeURL = [documentsURL URLByAppendingPathComponent:#"WorkRestWork.sqlite"];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSError *error = nil;
NSPersistentStoreCoordinator *psc = [[self managedObjectContext] persistentStoreCoordinator];
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:_options error:&error];
NSAssert(store != nil, #"Error initializing PSC: %#\n%#", [error localizedDescription], [error userInfo]);
});
}
Then I call this method
- (BOOL) addTimerWithName: (NSString*)timerName
{
[self initilizeDataLayer];
Timer *newTimer = [NSEntityDescription insertNewObjectForEntityForName:#"Timer" inManagedObjectContext:myDataController.managedObjectContext];
newTimer = [NSEntityDescription insertNewObjectForEntityForName:#"Timer" inManagedObjectContext:myDataController.managedObjectContext];
newTimer.name = timerName;
[myDataController.managedObjectContext insertObject:newTimer];
NSError *error = nil;
if ([myDataController.managedObjectContext save:&error] == NO) {
NSAssert(NO, #"Error saving context: %#\n%#", [error localizedDescription], [error userInfo]);
return NO;
}
return YES;
}
And Finally
[cdh addTimerWithName:#"A TEST"];
When I check check the .sqlite file using Base there are 2 rows, one with "A TEST" as the Name and the other empty which is confusing me
Ad Q1.
You are inserting two timers here. However you assign a name just to the second one. First one has no name cause you never set it. Here's your code with some comments:
// Inserts first timer
Timer *newTimer = [NSEntityDescription insertNewObjectForEntityForName:#"Timer" inManagedObjectContext:myDataController.managedObjectContext];
// Inserts second timer
newTimer = [NSEntityDescription insertNewObjectForEntityForName:#"Timer" inManagedObjectContext:myDataController.managedObjectContext];
// Sets name of the second timer
newTimer.name = timerName;

executefetchrequest causes crash

I am trying to implement Core Data in an iOS 7 app (and have successfully done so earlier). However, when I execute the executeFetchRequest method, the app crashes. The most relevant code is added below:
#import "JTHuntingSeasonDB.h"
#implementation JTHuntingSeasonDB
- (id)init {
self = [super init];
if (self) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentsDirectory = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
NSString *documentName = #"JTHuntingDB";
NSURL *url = [documentsDirectory URLByAppendingPathComponent:documentName];
document = [[UIManagedDocument alloc] initWithFileURL:url];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:[url path]];
if (fileExists) {
[document openWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(#"Document opened successfully");
self.allSpecies = [self getSpecies];
self.speciesToday = [self getSpeciesToday];
} else NSLog(#"Failed to open document");
}];
} else {
[document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
NSLog(#"Document created successfully");
self.allSpecies = [self getSpecies];
self.speciesToday = [self getSpeciesToday];
} else NSLog(#"Failed to create document, path: %#", url);
}];
}
}
return self;
}
#pragma mark - Core Data
- (NSArray *)getSpecies {
if (document.documentState == UIDocumentStateNormal) {
context = document.managedObjectContext;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *country = [defaults stringForKey:#"Country"];
NSString *subregion = [defaults stringForKey:#"Subregion"];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"HuntingDates"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedStandardCompare:)];
request.fetchBatchSize = 150;
request.fetchLimit = 150;
request.sortDescriptors = #[sortDescriptor];
if (subregion.length > 0) {
request.predicate = [NSPredicate predicateWithFormat:#"(country IN %#) AND (subregion IN %#)", country, subregion];
} else {
request.predicate = [NSPredicate predicateWithFormat:#"country IN %#", country];
}
NSError *error;
return [context executeFetchRequest:request error:&error];
}
return nil;
}
The line
return [context executeFetchRequest:request error:&error];
causes the app to crash. This is what I have done and discovered so far:
Simulator: Deleted app and reset content and settings
Xcode: Clean (Build folder)
"context" is not nil
NSLog prints out "Document created/opened successfully"
Only "lldb" is printed in the console when the app crashes
Edit:
After following Wain's advice, I turned exception breakpoint off, got a more descriptive error message saying that the predicate was invalid. Problem solved by replacing IN in the predicate with CONTAINS and also changing the variable "country" so that it did not return nil anymore.
I guess it's your predicates, because IN is a collection operator but you're passing strings (should be array / set).
If you're using strings, you should be using CONTAINS in the predicate.
Assuming the CoreData persistent store lives inside the document, you will want to use a UIManagedDocument

check if file has been updated on server

In my application and at startup I check if databases has been changed on server with the copy stored locally. I'm using a synchronous request to the server and I'm doing a check based on the last modified date time fields in the HTTP response
If the last modified data time of the file on the server is > last modified date time of the local file I ask user if he would like to update the database, if he accepts I download the database.
I'm using my machine as server but the problem when I shutdown my machine, the application crash at startup
Thanks for you help
You will find below my code
#import "FirstViewController.h"
#interface FirstViewController ()
#end
#implementation FirstViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// check connectivity
if ([[Reachability reachabilityForInternetConnection] currentReachabilityStatus] == NotReachable) {
[self displayConenctivityAlert];
}else{
[self checkDatabases];
}
}
- (void) checkDatabases {
bool res = [self fileUpdated];
if (res){
// Ask user if he would like to update the databases
}
}
-(void) displayConenctivityAlert{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:[TSLanguageManager localizedString:#"NO_CONNECTED"] delegate:self cancelButtonTitle:[TSLanguageManager localizedString:#"OK"] otherButtonTitles:nil];
[alert show];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(#"Error HTTP ...");
}
- (BOOL)fileUpdated {
NSString *urlString = #"http://192.168.0.10:8888/fuel/stations.db";
NSLog(#"Downloading HTTP header from: %#", urlString);
NSURL *url = [NSURL URLWithString:urlString];
//store locally data into the resource folder.
NSString *documentsDirectory = [Utility documentsPath];
NSString *cachedPath = [documentsDirectory stringByAppendingPathComponent:#"stations.db"];
NSLog(#"Local URL / %#", cachedPath);
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL downloadFromServer = NO;
NSString *lastModifiedString = nil;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"HEAD"];
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error: NULL];
if ([response respondsToSelector:#selector(allHeaderFields)]) {
lastModifiedString = [[response allHeaderFields] objectForKey:#"Last-Modified"];
}
NSDate *lastModifiedServer = nil;
#try {
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.dateFormat = #"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
df.locale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US"];
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:#"GMT"];
lastModifiedServer = [df dateFromString:lastModifiedString];
}
#catch (NSException * e) {
NSLog(#"Error parsing last modified date: %# - %#", lastModifiedString, [e description]);
}
NSLog(#"lastModifiedServer: %#", lastModifiedServer);
NSDate *lastModifiedLocal = nil;
if ([fileManager fileExistsAtPath:cachedPath]) {
NSError *error = nil;
NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:cachedPath error:&error];
if (error) {
NSLog(#"Error reading file attributes for: %# - %#", cachedPath, [error localizedDescription]);
}
lastModifiedLocal = [fileAttributes fileModificationDate];
NSLog(#"lastModifiedLocal : %#", lastModifiedLocal);
}
// Download file from server if we don't have a local file
if (!lastModifiedLocal) {
downloadFromServer = YES;
}
// Download file from server if the server modified timestamp is later than the local modified timestamp
if ([lastModifiedLocal laterDate:lastModifiedServer] == lastModifiedServer) {
downloadFromServer = YES;
}
return downloadFromServer;
}
#end
Your app is crashing because it is taking too long to complete didFinishLaunching the system watchdog kills your app. This is because you are making a synchronous http request in viewDidLoad of your root view controller, which must be completed before you can finish launching. You can resolve this issue multiple ways, either do your HTTP request asynchronously by calling sendAsynchronousRequest:queue:completionHandler on your NSURLConnection. The other option is to move this code out of your launching pipeline, perhaps by moving the code to viewDidAppear of your view controller. This has the side effect of performing the check on every return to the view, not just the initial load.
In short, doing a synchronous HTTP request is a big no no because your UI will hang until the request is complete. In this case it's especially bad because you're forcing your launch to be delayed until the request is complete, which is causing it to fail when the server is down.
Problem solved
I'm using now
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
}
instead of
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error: NULL];

AFNetworking for Image Downloads, Unresponsive UI

I'm using AFNetworking to pull images from a URL, resize, store to disk and log the path in Core Data, then load to a table view and store . When the code executes it freezes my UI. I'm not sure if it's the download or the manipulation that's causing my troubles.
The code I'm using is below
- (void)getPhoto:(NSInteger)type forManagedObject:(MyManagedObject*)object {
// download the photo
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:object.photoUrl]];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request success:^(UIImage *image) {
// MyManagedObject has a custom setters (setPhoto:,setThumb:) that save the
// images to disk and store the file path in the database
object.photo = image;
object.thumb = [image imageByScalingAndCroppingForSize:CGSizeMake(PhotoBlockCellButtonWidth, PhotoBlockCellButtonHeight)];
NSError *nerror;
if (![[DataStore sharedDataStore].managedObjectContext save:&nerror]) {
NSLog(#"Whoops, couldn't save: %#", [nerror localizedDescription]);
return;
}
// notify the table view to reload the table
[[NSNotificationCenter defaultCenter] postNotificationName:#"ReloadTableView" object:nil];
}];
[operation start];
}
And here is a sample code relevant to the setter from my managed object
- (NSString*)uniquePath{
// prepare the directory string
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
// acquire a list of all files within the directory and loop creating a unique file name
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *existingFiles = [fileManager contentsOfDirectoryAtPath:documentsDirectory error:nil];
NSString *uniquePath;
do {
CFUUIDRef newUniqueId = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef newUniqueIdString = CFUUIDCreateString(kCFAllocatorDefault, newUniqueId);
uniquePath = [[documentsDirectory stringByAppendingPathComponent:(__bridge NSString *)newUniqueIdString] stringByAppendingPathExtension:#"png"];
CFRelease(newUniqueId);
CFRelease(newUniqueIdString);
} while ([existingFiles containsObject:uniquePath]);
return uniquePath;
}
- (NSString*)saveImage:(UIImage*)image{
NSString *path = [self uniquePath];
NSData *data = UIImagePNGRepresentation(image);
[data writeToFile:path atomically:YES];
return [NSString stringWithFormat:#"file://%#",path];
}
- (void) setPhoto:(UIImage *)image {
self.photoUrl = [self saveImage:image];
}
I would like to push this to a background thread, but I'm not sure what the implications are with AFNetworking, Core Data, and Messaging in terms of thread safety. Any thought?
AFAIK, the way you are executing your request in incorrect:
[operation start];
you should instead add the operation to an NSOperationQueue:
NSOperationQueue* operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:operation];
(you should correctly memory-manage the queue).
By doing like this, your request will be executed in an async way, it won't block the UI and you will not need to deal with multithreading.
Based on Matt's suggestion, I improved the UI by reworking my call as follows.
- (void)getPhoto:(NSInteger)type forManagedObject:(MyManagedObject*)object {
// download the photo
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:object.photoUrl]];
AFImageRequestOperation *operation = [AFImageRequestOperation
imageRequestOperationWithRequest:request
imageProcessingBlock:^UIImage *(UIImage *image) {
return [image imageByScalingAndCroppingForSize:CGSizeMake(PhotoBlockCellButtonWidth, PhotoBlockCellButtonHeight)];
}
cacheName:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// MyManagedObject has a custom setters (setPhoto:,setThumb:) that save the
// images to disk and store the file path in the database
object.photo = image;
object.thumb = image;
NSError *nerror;
if (![[DataStore sharedDataStore].managedObjectContext save:&nerror]) {
NSLog(#"Whoops, couldn't save: %#", [nerror localizedDescription]);
return;
}
// notify the table view to reload the table
[[NSNotificationCenter defaultCenter] postNotificationName:#"ReloadTableView" object:nil];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error getting photo");
}];
[operation start];
}

Resources