Shared NSURLCache and UIWebView on iOS 8 - uiwebview

In iOS 7, I was able to set a shared URL cache to a subclass of NSURLCache and any UIWebViews I created would automatically use that shared cache for each request.
// Set the URL cache and leave it set permanently
ExampleURLCache *cache = [[ExampleURLCache alloc] init];
[NSURLCache setSharedURLCache:cache];
However, now in iOS 8 it doesn't seem like UIWebView pulls from the shared cache and cachedResponseForRequest never gets called.
Has anyone found documentation for this change, or a workaround?

I had same problem today. It was ok on ios7 and broken on ios8.
The trick is to create your own cache as the first thing you do in didFinishLaunchingWithOptions.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// IMPORTANT: call this line before anything else. Do not call [NSURLCache sharedCache] before this because that
// creates a reference and then we can't create the new cache.
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
...
You can see this being done in other apps:
https://github.com/AFNetworking/AFNetworking/blob/master/Example/AppDelegate.m
This site, while old, has more info on why you shouldn't even call [NSURLCache sharedInstance] before the above code:
http://inessential.com/2007/02/28/figured_it_the_heck_out

Related

UIDocumentPickerViewController NewBox App Hangs

I am referring WWDC 2014 sample app NewBox for document provider extension.
I am using following code from NeBox app, to import a document from Document Provider to my app.
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
BOOL startAccessingWorked = [url startAccessingSecurityScopedResource];
NSURL *ubiquityURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
NSLog(#"ubiquityURL %#",ubiquityURL);
NSLog(#"start %d",startAccessingWorked);
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init];
NSError *error;
[fileCoordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
NSData *data = [NSData dataWithContentsOfURL:newURL];
NSLog(#"error %#",error);
NSLog(#"data %#",data);
}];
[url stopAccessingSecurityScopedResource];
}
App totally hangs for coordinateReadingItemAtURL method.
Any inputs will be helpful.
I noticed this problem in NewBox app as well, and decided to trace it. So, there are two extensions in this app: Document Picker and File Provider. To make long story short, there is a race condition between the two when they try to access files within app's document storage folder.
In my opinion, the easiest method to trace down a problem is to put NSLog() in a bunch of locations. The problem is, however, that the debugging output generated by extension won't be visible in Xcode console. The good news is that you can open console in iOS Simulator app by clicking to Debug -> Open System Log menu. This will show all kinds of debugging messages, including those generated by extensions. You can find more about extension debugging here.
By using this method one can easily realize that execution gets stuck in File Provider's startProvidingItemAtURL method. More specifically, the following line causes a deadlock:
[self.fileCoordinator coordinateWritingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
Why is that? Take a look at documentation for coordinateWritingItemAtURL:
If the url parameter specifies a file:
This method waits for other readers and writers of the exact same file to finish in-progress actions.
Function documentPicker that you mentioned calls a read operation, which in its turn triggers a write operation. This is a deadlock. I guess the easiest way to fix it would be to avoid using coordinateWritingItemAtURL in File Provider.
As per documentation:
Each of these methods wait synchronously on the same thread they were invoked on before invoking the passed-in accessor block on the same thread, instead of waiting asynchronously and scheduling invocation of the block on a specific queue.
Apple recommends that you not use file coordination inside this method. The system already guarantees that no other process can access the file while this method is executing. That's the sole reason for this deadlock.
Please refer to this documentation for more details.
You can use block also. Block works too fast, hang problem will get resolve.
Step 1: Take global variable of
UIDocumentPickerViewController *documentPicker;
also decalre
typedef void(^myCompletion)(BOOL);
Step 2: Write a method where allocation takes place and can send callback on completion
-(void) allocateDocumentPicker:(myCompletion) compblock{
//do stuff
documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:#[#"public.content"]
inMode:UIDocumentPickerModeImport];
documentPicker.delegate = self;
documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
compblock(YES);
}
Step 3: Call the method where allocation is taking place every time you want to open the composer but present it on receiving completion as YES.
-(IBAction)attachmentButtonClicked:(id)sender{
[self allocateDocumentPicker:^(BOOL finished) {
if(finished){
[self.parentScreen presentViewController:documentPicker animated:YES completion:nil];
}
}];
}
Simple Syntax to create own block, take reference from this link
Custom completion block for my own method

GCD network requests failing on iOS 4

I have the following code in my application to load some data from my API. It works fine, great in fact in iOS 5 but on iOS 4 I am getting so many responses with status 204.
This only happens on iOS 4, this could have been treated as an API error, but it works great in the browser, on Rested.app, on iOS 5 etc... only iOS 4 fails, it fails in the simulator and on the device (iPhone 4).
I am calling this code each time I load a cell into a table view. I have a core data object with a load state, set to no initially, if it's not loaded I perform this code, if it's loaded, I skip this code. In the mean time I display a spinner inside the cell on the table view.
I am sure it's a problem with multiple requests in GCD on iOS 4.
Can anyone spot anything wrong with my code snippet?
-(void)myFunction{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// query users participations (Network)
NSError * _urlError = nil;
NSString * url = [NSString stringWithFormat:#"my api url"];
NSMutableURLRequest * loginHTTPRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
[loginHTTPRequest setValue:#"application/json" forHTTPHeaderField:#"Accept"];
NSLog(#"Description: %#", [loginHTTPRequest description]);
NSHTTPURLResponse * _responseHeaders = nil;
NSData * responseData = [NSURLConnection sendSynchronousRequest:loginHTTPRequest
returningResponse:&_responseHeaders
error:&_urlError];
if(_urlError != nil){
dispatch_async( dispatch_get_main_queue(), ^{
// alert network connection error
});
return;
}
NSString *json_string = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary * jsonData = [NSDictionary dictionaryWithDictionary:[parser objectWithString:json_string]];
[json_string release];
[parser release];
dispatch_async( dispatch_get_main_queue(), ^{
// here [_responseHeaders statusCode] keeps returning 204 and there is nothing in responseData
// do some Core Data stuff
});
});
}
UPDATE
Note this code is working fine if called even with a for loop repeatedly, the issue is when I invoke this method from tableView:cellForRowAtIndexPath:
I have Core Data objects with a property "isLoaded" set to NO and changed to YES upon remote load. When my tableview's datasource loads the cells for each object, the tableView:cellForRowAtIndexPath: method calls this function if the object's "isLoaded" property is NO.
I suspected the problem may be because there 2 or more simultaneous calls to the API happening when the table is loaded and reloaded. Each successful load from the api invokes reloadData for that tableview.
This lets me have a pre filled tableview with spinners and asynchronously load in my data as I need it on screen which is nice because I can efficiently use NSFetchedResultsController with lazy loading my objects core data.
(I have an endpoint for all my objects returning an array of object id's - I create Core Data objects with only the ID's, all rest of data, name, date etc etc... is not loaded until it's needed).
When I start scrolling around the new cells which are created/reused call this method and they always get a 200 response with the data. it's only the first loading which causes this "block".
I think I found the problem, I was performing synchronous requests on an asynchronous GCD thread, and for some reason timeouts were occurring, but only on requests in iOS 4, maybe the headers are sent slightly differently from iOS 4 which is causing the API to take longer to respond ? Or maybe multiple (as in simultaneous to the millisecond) requests sent from different threads synchronously on an asynchronous thread were clashing in the system before being sent ?
Anyhow... this didn't seem to be the case calling google.com or even my own private server, so it must be something to do with the headers and multiple requests...
I am using asi http from github and it's working a lot more efficiently, now I am not using GCD for these requests, just an ASI queue.
Any final thoughts on iOS 4 synchronous requests performing on an asynchronous GCD thread with possible timeouts not being respected and returning early with a status 204 ?
Ok - if it is https, then you probably can't get away with using sendSynchronousRequest. The documentation states that things like [NSURLConnection connection:didReceiveAuthenticationChallenge:] won't call some key things, i.e.:
If authentication is required in order to download the request, the required credentials must be specified as part of the URL. If authentication fails, or credentials are missing, the connection will attempt to continue without credentials.
I'm still surprised it's working on iOS5, to be honest. I think you'll have to use asynchronous methods to at least debug it to find out what is going on.

How do you handle a UIManagedDocument?

First off, I should mention that this is my first post on this site. I am trying to teach myself to program iOS and in my google searches for answers I find that I'm constantly directed here. So thank you to all who have contribute here. You have help me a ton already.
I have been going through the Stanford CS193P class and LOVE it. But I'm stuck right now and not sure where to turn.
My problem has been with the UIManagedDocument.
I tired to make a simple app to test my new skills. This is what it does:
A simple accounting app that tracks individual contributions to a fundraising event.
I have a UITabBar that on each tab allows you to:
1. Track the participants (Players) - This will connect to the address book and allow you to add them or just keep them in this app.
2. Manage the events (Events) - You can add, edit or delete events that you will then add participants to and then be able to add what they brought (Bank) in on that event.
3. Settings. - I've added some buttons just to help me figure stuff out now including a reset button that clears all data and a "dummy data" button.
I have three coreData Entities. Players, Events and Bank each with relationships with the other two.
When I first tried to make this app (pre iOS5) I used the appDelegate to create my ManagedObjectContext and pass it around to my viewControllers. That worked. But now I'm supposed to use the UIManagedObjectDocument and not use the AppDelegate. I believe I understand the principle and the integration to iCloud. (I could be wrong)
Using the examples from the class and what I could find online I made a helper class that will provide the first of each of my ViewControllers within my UINavigationControllers the ManagedDocument.
+ (UIManagedDocument *)sharedManagedDocument
{
static UIManagedDocument *sharedDocument = nil;
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"DefaultAppDatabase"];
// url is "<Documents Directory>/<DefaultAppDatabase>"
// Create the shared instance lazily upon the first request.
if (sharedDocument == nil) {
sharedDocument = [[UIManagedDocument alloc] initWithFileURL:url];
}
if (sharedDocument.fileURL != url) {
UIManagedDocument *newDocument = [[UIManagedDocument alloc] initWithFileURL:url];
sharedDocument = newDocument;
}
NSLog(#"SharedDocument: %#", sharedDocument);
return sharedDocument;
}
I then had that first ViewController open the document and perform the fetch.
From there I pass whatever NSManagedObject is selected to the next ViewController through the segue.
The problems are when I get to adding or reseting the data. (I'm assuming that if I can get it to work with the "dummy data" button I can get it to work on an individual entry) When I press the "Reset" or "Dummy" button my logs tell me that it was pushed but I don't see any change in the data until I restart the app. Then it shows up perfectly. My guess is I'm not saving the file correctly or I'm not refreshing the tableViews correctly. I made a small attempt at using NSNotification but didn't go to far into it since I couldn't get it to respond to anything. I'm happy to go back down that road if I need to.
This is my save method... pretty much just copied from the default coreData appDelegate.
- (void)saveContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.appDatabase.managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
[self.appDatabase saveToURL:self.appDatabase.fileURL
forSaveOperation:UIDocumentSaveForOverwriting
completionHandler:^(BOOL success){
if(!success) NSLog(#"failed to save document %#", self.appDatabase.localizedName);
if(success) NSLog(#"Success: save document %#", self.appDatabase.localizedName);
}];
}
Paul, the instructor from CS193P, suggested in Assignment 6 to use a helper method to pass around the UIManagedDocument through a block. I get the theory behind blocks but haven't completely wrapped my head around them so I haven't ruled out that my answer may lie there as well.
Thank you so much for any help or pointing me in the right direction to do more research. Sorry this post is so long, I was trying to be as clear as I can.
I was over thinking the whole thing. Alan asked my question perfectly in this post:
How do I create a global UIManagedDocument instance per document-on-disk shared by my whole application using blocks?
The question and the answers cleared everything up.
Thanks

iOS - using multiple connections

I've got a simple login view that when you successfully log in, it goes and fetches some json from a server and inserts it into a database. So im using:
// NSURLRequest
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// appending data
}
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
// insert into db
}
I want a second connection now to download some documents, how can I utilise the methods above to pass it another URL and handle data in a different manner (not json) for the 2nd NSURLRequest in the same view?
Well, the first thing you need to do is set the delegate of your new NSURLConnection to this class again, so the methods get called (but you knew that). If you're doing these one at a time, store a pointer to the active NSURLRequest, something like:
#property (nonatomic, assign) NSURLRequest *activeURLRequest;
Then check the active request's URL to differentiate the requests:
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *urlPath = [[activeURLRequest URL] path];
if([urlPath isEqualToString:#"http://my.url.string/1"])
// insert into DB
else if([urlPath isEqualToString:#"http://my.url.string/2"])
// do the other thing
}
Note you can also compare the NSURL object directly with a stored version of it using isEqual: (or the == operator).
If you're doing more than one at a time, you need another way to differentiate. I recommend using ASIHTTPRequest or AFNetworking to make this easier, but if you want to do it using Apple's libraries then you'll need to spin off multiple threads, keep a record of which thread number is handling which request URL, and use that information in your connectionDidFinishLoading: method.

donloading gif giles in iphone - improving performance

I have a table view showing records
When user taps ona record I need to show the details of that record
The detail shown also includes an image that I need to download from awebserver ( GIF ) file
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection release];
[request release];
NSData *imageData = [NSData dataWithContentsOfURL:url];
The problem is the last line where it takes some time to download the image
I am wondering what the possible options should be / can be done
I know the initWithRequest is aynchronous - but the call to dataWithContentsofUrl is right in the main path - so user experiences a wait time of more than a second
Is there a way that I could possibly have the dataWithContentsofUrl pushed into some other method that is called asynchronously
If thats possible I could look at showing a spinner or "please wait...." while the image loads
The other option I am thinking of is :
Should I have the images saved on the server in a zipped format ?
and then unzip ?
will it be faster ?
has anyone tried it before ?
or does anyone have a better option ?
I am not looking for spoon feeding but would like to hear your ideas please!

Resources