I'm trying to use the ASIWebPageRequest to load a local html file so that I can use the built-in caching in ASIWebPageRequest. I know this might sound a bit pointless however I want to use remote images in my local files that only get updated once a week or so. If that makes sense.
Here is what I'm doing in code:
//OUTPUT: file:///Users/ledixonuk/Library/Application%20Support/iPhone%20Simulator/4.3.2/Applications/FDF21331-CC80-4ECB-9A33-16AEC073D117/Documents/ItemOne.html
NSURL *fileUrl = [[NSURL alloc] initFileURLWithPath:[FileSystemHelper dataFilePathInDocuments:#"ItemOne.html"]];
// Assume request is a property of our controller
// First, we'll cancel any in-progress page load
[[self webRequest] setDelegate:nil];
[[self webRequest] cancel];
[self setWebRequest:[ASIWebPageRequest requestWithURL:fileUrl]];
[[self webRequest] setDelegate:self];
[[self webRequest] setDidFailSelector:#selector(webPageFailed:)];
[[self webRequest] setDidFinishSelector:#selector(webPageFinished:)];
// Tell the request to replace urls in this page with local urls
//[[self webRequest] setUrlReplacementMode:ASIReplaceExternalResourcesWithLocalURLs];
// Tell the request to embed external resources directly in the page
[[self webRequest] setUrlReplacementMode:ASIReplaceExternalResourcesWithData];
// It is strongly recommended you use a download cache with ASIWebPageRequest
// When using a cache, external resources are automatically stored in the cache
// and can be pulled from the cache on subsequent page loads
[[self webRequest] setDownloadCache:[ASIDownloadCache sharedCache]];
// Ask the download cache for a place to store the cached data
// This is the most efficient way for an ASIWebPageRequest to store a web page
[[self webRequest] setDownloadDestinationPath:[[ASIDownloadCache sharedCache] pathToStoreCachedResponseDataForRequest:[self webRequest]]];
[[self webRequest] startAsynchronous];
And here is the error I'm getting:
404 Not Found
Not Found
The requested URL /Users/ledixonuk/Library/Application Support/iPhone Simulator/4.3.2/Applications/FDF21331-CC80-4ECB-9A33-16AEC073D117/Documents/ItemOne.html was > not found on this server.
Obviously the ASIWebPageRequest can not find the ItemOne.html file in the application document folder, however I've double checked and the file is definitely there.
Has anyone else had a similar issue to this? It's driving me mad trying to sort it out!
ASIWebPageRequest is a subclass of ASIHTTPRequest, and unfortunately ASIHTTPRequest doesn't support fetching file: URLs - only http and https ones.
Basically you have all the code you need, you just need to find a way to plumb it together. You could create a subclass of ASIHTTPRequest that can load file: URLs (essentially provide your own implement of startAsyncronous that instead just calls back the delegates after setting up the right data files), or create a subclass of ASIWebPageRequest that does it. I've not yet thought about it in detail so I have no idea which way would be best.
(I think you're seeing the 404 page as ASIHTTPRequest is managing to contact a web server somewhere - perhaps it's trying to fetch from http://127.0.0.1/Users/ledixonuk/Library/....)
See below the simple implementation I have added to my subclass of ASIHTTPRequest in order to support file:// based URLs. It's by no means complete and possibly missing some properties that should be set and it will not invoke all the delegates but for my own purposes this was enough.
- (void)startRequest
{
if ([url isFileURL])
{
// ASIHTTPRequest does not support handling file:// URLs, this is my own simple implementation here
if ([self isCancelled]) {
return;
}
[self performSelectorOnMainThread:#selector(requestStarted) withObject:nil waitUntilDone:[NSThread isMainThread]];
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
NSString *filePath = [url path];
BOOL isDirectory = NO;
if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory] && !isDirectory)
{
responseStatusCode = 200;
[self setRawResponseData:[NSData dataWithContentsOfFile:filePath]];
[self setContentLength:rawResponseData.length];
[self setTotalBytesRead:[self contentLength]];
}
else
{
responseStatusCode = 404;
[self setContentLength:0];
[self setError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIFileManagementError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:#"Cannot open file at path '%#'",filePath],NSLocalizedDescriptionKey,error,NSUnderlyingErrorKey,nil]]];
}
complete = YES;
downloadComplete = YES;
[self requestFinished];
[self markAsFinished];
}
else
{
// let the original implementation deal with all the other URLs
[super startRequest];
}
}
Related
I have a view where I show an UIImageView which complimentary internet, I use to bring data NSXMLparser which are loaded correctly, the problem was that I use to make the parser NSOperationQueue background so then I refresh the image in my main view. image which form no refresh them in any way
I leave here the code below
- (void)viewDidLoad
{
[Base64 initialize];
[super viewDidLoad];
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(loadDataWithOperation)
object:nil];
[queue addOperation:operation];
[operation release];
}
- (void) loadDataWithOperation {
getData=NO;
NSURL *url1 = [ NSURL URLWithString:[url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSData *xmlData = [NSData dataWithContentsOfURL:url1];
NSXMLParser *parser = [[NSXMLParser alloc]initWithData:xmlData];
[parser setDelegate:self];
[parser parse];
[self performSelectorOnMainThread:#selector(showImage)withObject:nil waitUntilDone:NO];
}
-(void)showImage
{
NSArray *imagen =[[NSArray alloc] initWithArray:xml];
NSString *hola1 =[[imagen objectAtIndex:5]objectForKey:#"imagen"];
NSData * dataa = [Base64 decode:hola1];
img = [UIImage imageWithData:dataa];
self.images.image = img;
[images setImage:img];
[img release];
}
What am I doing wrong? appreciate your help please
I don't see anything obvious (though obviously we don't see the building of the xml array nor do we know precisely what the XML itself looks like). You should NSLog (or set a breakpoint and manually inspect) the xml as well as hola1 and dataa results in showImage, to identify at precisely what's going on at each step.
There are two possible problems with background operations and downloading XML. Neither of these seem applicable here, but I'll mention it just in case:
But there's nothing that would prevent the downloading and parsing of your XML like you have demonstrated here. If you were using NSURLConnection to download the XML (perhaps you simplified it here for the purposes of demonstration), there are issues in using NSURLConnection in a background queue. But if you use dataWithContentsOfURL (or better, NSXMLParser method initWithContentsOfURL) that wouldn't be an issue.
It looks like you're downloading a single XML, but if you were downloading multiple XML sources simultaneously, you should appreciate that many servers impose a limit as to how many concurrent connections they'll allow from a single client. You can use NSOperationQueue property maxConcurrentOperationCount to mitigate that problem. Four is a typical value.
Unrelated, but there are a couple of minor memory management things you might want to look at:
if you already have xml you don't need to create a new imagen;
you should probably release the queue object after adding the operation, or if you need to keep it around to reuse it, you should make it a class property or instance variable;
you could get rid of operation completely if you did:
[queue addOperationWithBlock:^{
[self loadDataWithOperation];
}];
you should release the parser object when you're done with it;
if you keep imagen in your code you should also release it when done; and
you should not release the img object since imageWithData returns an autorelease object
The routine memory management stuff probably would be pointed out to you if you did a static analysis (choose "Analyze" from the "Product" menu).
One final observation:
I notice that you have:
NSData *xmlData = [NSData dataWithContentsOfURL:url1];
NSXMLParser *parser = [[NSXMLParser alloc]initWithData:xmlData];
Clearly that xmlData is not necessary because you could have also just have done:
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url1];
I am using NSURLConnection to load data from a response. It works as it should, the delegate method connectionDidFinishLoading has the connection instance with the data I need. The problem is that I want to pass some information along with the request so that I can get it when the connection finishes loading:
User wants to share the content of a URL via (Facebook, Twitter,
C, D).
NSURLConnection is used to get the content of the URL
Once I have the content, I use the SL framework
SLComposeViewController:composeViewControllerForServiceType and need
to give it the service type
At this point I don't know what service the user selected in step 1. I'd like to send that with the NSURLConnection.
Can I extend NSURLConnection with a property for this? That seems very heavy-handed. There must be a "right way" to do this.
Many Thanks
Assuming you don't need the delegate-based version of the NSURLConnection process for some other reason, this is a good use case for the block-based version:
- (void)shareContentAtURL:(NSURL *)shareURL viaService:(NSString *)service
{
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:shareURL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] == 0 && error == nil) {
// handle empty response
} else if (error != nil) {
// handle error
} else {
// back to the main thread for UI stuff
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// do whatever you do to get something you want to post from the url content
NSString *postText = [self postTextFromData:data];
// present the compose view
SLComposeViewController *vc = [SLComposeViewController composeViewControllerForServiceType:service];
[vc setInitialText:postText];
[self presentViewController:vc animated:YES];
}];
}
}];
}
Since blocks can capture variables from their surrounding scope, you can just use whatever context you already had for the user's choice of service inside the NSURLConnection's completion block.
If you're still wed to the delegate-based NSURLConnection API for whatever reason, you can always use an ivar or some other piece of state attached to whatever object is handling this process: set self.serviceType or some such when the user chooses a service, then refer back to it once you get your content from the NSURLConnectionDelegate methods and are ready to show a compose view.
You could check the URL property of an NSURLConnection instance and determine the service by parsing the baseURL or absoluteString property of the URL with something like - (ServiceType)serviceTypeForURL:(NSURL *)theURL;
All the NSURLConnectionDelegate methods pass the calling NSURLConnection object-so you could get it from
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
or
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
First of all the questions are failry simiple.. if you just want to see what they are skip to the bottom of this post and you will see them in bold.. for more detail then you can read the rest of this post...
I am just trying to iron out my NSURLConnection so that its working smoothly and I understand this properly. There is a profound lack of example/tutorials for Asynchronous connections on the internet or not any that I can find that explaine what is going on with any level of depth other than getting the connection up and running which after working on it seems pretty simple. Hopefully this question can full the void that I feel is out there for other users.
So, in my .h file i have imported the foundations headers and declared the methods required for the received or lack of received data (errors etc).
.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h> //add foundations
//.. other headers can be imported here
#interface MyViewController: UITableViewController {
//Im not setting any delegates to access the methods because Is all happening in the same
//place so I just use the key word 'self' when accessing the methods declared below
//I'm not sure if this is the best thing to do but I wasn't able to get my head around declaring the delegate or how it would help me with the way I have set up my request etc.
}
- (IBAction)setRequestString:(NSString *)string; //this method sets the request and connection methods
//these methods receive the response from my async nsurlconnection
- (void)receivedData:(NSData *)data;
- (void)emptyReply;
- (void)timedOut;
- (void)downloadError:(NSError *)error;
So thats my header file.. pretty simple not much explaining needed.
.m
//call setRequestString from some other method attached to a button click or something
[self setRequestString:#"rss.xml"];
//..
- (IBAction)setRequestString:(NSString *)string
{
//Set database address
NSMutableString *databaseURL = [[NSMutableString alloc] initWithString:#"http:www.comicbookresources/feeds/"]; // address not real jsut example
//append the string coming in to the end of the databaseURL
[databaseURL appendString:string];
//prepare NSURL with newly created string
NSURL *url = [NSURL URLWithString:databaseURL];
//AsynchronousRequest to grab the data
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil){
[self receivedData:data];
}else if ([data length] == 0 && error == nil){
[self emptyReply];
}else if (error != nil && error.code == NSURLErrorTimedOut){ //used this NSURLErrorTimedOut from foundation error responses
[self timedOut];
}else if (error != nil){
[self downloadError:error];
}
}];
}
now set up the methods that were initialized in the .h file and called in the if statement above
- (void)receivedData:(NSData *)data
{
NSString* newStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", newStr); //logs recived data
//now you can see what data is coming in on your log
//do what you want with your data here (i.e. start parsing methods
}
- (void)emptyReply
{
//not sure what to do here yet?
}
- (void)timedOut
{
//also not sure what to do here yet?
}
- (void)downloadError:(NSError *)error
{
NSLog(#"%#", error);
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:#"Error!" message:#"A connection failure occurred." delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[errorAlert show];
}
Cool so that pretty much the basics of what I have done right there.. now the questions I have are as follows.
Question one:
Where I call NSURLConnection like so
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
What is happening here what is the ^ for is that executing that whole block (including the if statements) on a different thread or something? because it looks alot like grand central dispatch formatting but slightly different.
Question two:
what should I be doing inside emptyReply & timedOut methods?
Question three:
How would I incorporate caching into this? I would like to cache the responses I get back from different requests. i.e. with my setRequestString you will see there is a string input parameter, so i can request different rss feeds with the same method.. I need to figure out how to cache these responses into individual caches.. but im not sure where to start with it.
Finally
If you have made it this far, thank you very much for reading my question. Hopefully with your responses we can get a pretty nice solution going here.. that other people can use for themselves and pick and choose the bits and peices they need that works for there own solution..
Anyway thank you very much for reading and I look forward to your replies.. even if they are just refrences to tutorials or examples you think might help me.. anything is good I just want to fully understand whats going on and whats a good solution.
Read about blocks in Apple documentation. Its new. Or you can read here
You can show errors such as request timed out etc. You don't really have to handle them separately than the error one unless you have special logic.
Try this for caching
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:TIMEOUT_INTERVAL];
A brief over view of what I am trying to do.
I am using the tableView:didSelectRowAtIndexPath: method inside my UITableViewController subclass which is catching a row selection from that view like so...
//..... inside tableView:didSelectRowAtIndexPath:
if (indexPath.section == 0) {
//--- Get the subview ready for use
VehicleSearchResponseTableViewController *vehicleSearchResponseTableViewController = [[VehicleSearchResponseTableViewController alloc] initWithNibName:#"VehicleSearchResponseTableViewController" bundle:nil];
// ...
//--- Sets the back button for the new view that loads
self.navigationItem.backBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:#"Back" style: UIBarButtonItemStyleBordered target:nil action:nil] autorelease];
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:vehicleSearchResponseTableViewController animated:YES];
if(indexPath.row == 0) {
vehicleSearchResponseTableViewController.title = #"Mans";
EngineRequests *engineRequest = [[EngineRequests alloc] init];
[engineRequest getMans];
[engineRequest release];
}
if(indexPath.row == 1) {
//.... etc etc
As you can see in this method I set up a few things, pushing the new view onto the viewstack and changing the back buttons text, then I go into catching the different rows and then initiating a method in a subclass of nsobject where I want to have all my connection/request stuff going on.
Inside my NSObject I have several different methods for the different cells that you can select on the UITableViewController, basicly they specify different strings that will then initialize my ASIHTTPRequest wrapper to make a connection to the php script and catch all the data that will come back from the database.. NSObject looks like this.
//.... NSObject.m
- (IBAction) getMans
{
NSString *mansString = [[NSString alloc] initWithString:#"mans.php"];
[self grabURLInBackground:mansString];
[manusString release];
}
//....cont....
//--- Connect to server and send request ---------------->>
- (IBAction)grabURLInBackground:(NSString *)setUrlString
{
NSString *startURL = [NSString stringWithFormat:#"http://127.0.0.1:8888/CodeTest/%#", setUrlString];
NSURL *url = [NSURL URLWithString:startURL];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
- (void)requestFinished:(ASIHTTPRequest *)request
{
NSString *responseString = [request responseString]; //Pass request text from server over to NSString
NSData *responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding]; //Create NSData object for Parser Delegate and load with responseString
NSLog(#"stuff %#",responseData);
}
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
NSLog(#"%#", error);
}
From here I would like to pass the data I am getting from the requestFinished method back over to the newly pushed UITableView.. However I have an error before I am able to get this far that I need to solve... if I run the simulator and click back and forth between the views (the main UITableViewController with the cells and then the newly popped view where I want to put the data) the application falls over and pops up an error in main.m Thread 1: program receive signal EXC_BAD_ACCESS.. I just don;t know whats causing because from what I can tell my code is not so bad.
Also when I debug my application I notice that once grabURLInBackground method has finished it bounces out back to the getMans method then goes back over to the UITableViewController and continues through the if statements, completely neglecting the requestFinished and requestFailed methods, and I just cannot figure out why.
I guess I am not sure if I am calling the methods and functions I need to use in the right places so if you have any suggestions or answers on how I can improve or if you know where my error is coming form that would be greatly appreciated.
There's a few issues with the code above but I'd guess that your bad access exception is due to the handling of your EngineRequests and use of AsiHttpRequest.
The code here
EngineRequests *engineRequest = [[EngineRequests alloc] init];
[engineRequest getMans];
[engineRequest release];
effectively creates an object then deallocates as soon as getMans has finished running.
Then inside the engineRequest object this code
[request setDelegate:self];
[request startAsynchronous];
requests that AsiHttpRequest notify the almost certainly released object once the request has completed.
There may be other issues at work here but I'd start by restructuring to try to keep this object around until at least after it's received the response from AsiHttpRequest.
Hard to tell from the brief overview, but generally when you bad_access and end up in the main application method, it's usually because you autoreleased something, then released it, and it craps out when the autorelease pool is drained. Might want to turn on NSZombiesEnabled and look for memory problems.
Who does receive your request?
The sender (and receiver) object is engineRequest.
But you release Engine Request in that very moment after you issued the async request (by mens of the getMans Method.
I would suggest that you
1. move the code
vehicleSearchResponseTableViewController.title = #"Mans";
EngineRequests *engineRequest = [[EngineRequests alloc] init];
[engineRequest getMans];
[engineRequest release];
from your UITableViewController's didSelectRowAtIndexPath method to your vehicleSearchResponseTableViewController's viewDidLoad method.
2. to retain your EngineRequests object and keep it in some instance variable within vehicleSearchResponseTableViewControllerand do not release it before the request is completely processed, either successfully or in error.
Originally I needed the ability to use the search API with twitter. I did this using Matt Gemmell's great MGTwitterEngine. That code was very very simple and looked something like this:
- (void)viewDidLoad {
[super viewDidLoad];
tweetArrays = nil;
tweetNameArray = nil;
NSString *username = #"<username>";
NSString *password = #"<password>";
NSString *consumerKey = #"<consumerKey>";
NSString *consumerSecret = #"<consumerSecret>";
// Most API calls require a name and password to be set...
if (! username || ! password || !consumerKey || !consumerSecret) {
NSLog(#"You forgot to specify your username/password/key/secret in AppController.m, things might not work!");
NSLog(#"And if things are mysteriously working without the username/password, it's because NSURLConnection is using a session cookie from another connection.");
}
// Create a TwitterEngine and set our login details.
twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
[twitterEngine setUsesSecureConnection:NO];
[twitterEngine setConsumerKey:consumerKey secret:consumerSecret];
// This has been undepreciated for the purposes of dealing with Lists.
// At present the list API calls require you to specify a user that owns the list.
[twitterEngine setUsername:username];
[twitterEngine getSearchResultsForQuery:#"#HelloWorld" sinceID:0 startingAtPage:1 count:100];
}
This would end up calling the function:
- (void)searchResultsReceived:(NSArray *)searchResults forRequest:(NSString *)connectionIdentifier
And then I could do what I wanted with the searchResults. This required me to include the yajl library.
I then wanted to expand my code to allow users to tweet. I downloaded Ben Gottlieb's great code Twitter-OAuth-iPhone
So there's only one problem. The getSearchResultsForQuery returns a requestFailed with the following error:
Error Domain=HTTP Code=400 "The operation couldn’t be completed. (HTTP error 400.)"
To call this code I simply took the demo project in Twitter-OAuth-iPhone and added a call to getSearchResultsForQuery as seen here:
- (void) viewDidAppear: (BOOL)animated {
if (_engine) return;
_engine = [[SA_OAuthTwitterEngine alloc] initOAuthWithDelegate: self];
_engine.consumerKey = kOAuthConsumerKey;
_engine.consumerSecret = kOAuthConsumerSecret;
UIViewController *controller = [SA_OAuthTwitterController controllerToEnterCredentialsWithTwitterEngine: _engine delegate: self];
if (controller)
[self presentModalViewController: controller animated: YES];
else {
[_engine getSearchResultsForQuery:#"HelloWorld"];
// [_engine sendUpdate: [NSString stringWithFormat: #"Already Updated. %#", [NSDate date]]];
}
}
This as stated above returns a 400 error. Every other twitter API call I add here does work such as:
- (NSString *)getRepliesStartingAtPage:(int)pageNum;
Am I doing anything wrong? Or does getSearchResultsForQuery no longer work? The two code bases seem to use different versions of MGTwitterEngine, could that be causing the problem?
Thanks!
The problem is that you have to instantiate the twitter engine as an instance of SA_OAuthTwitterEngine, instead of MGTwitterEngine. When you call getSearchResultsForQuery, it uses a call to _sendRequestWithMethod to actually send the search request. SA_OAuthTwitterEngine overrides the MGTwitterEngine's implementation, but its version of that function doesn't work with getSearchResultsForQuery.
So, you need to go to getSearchResultsForQuery and make it use the MGTwitterEngine's version of the function.