iOS: parse a URL into segments - iphone

What's an efficient way to take an NSURL object such as the following:
foo://name/12345
and break it up into one string and one unsigned integer, where the string val is 'name' and the unsigned int is 12345?
I'm assuming the algorithm involves converting NSURL to an NSString and then using some components of NSScanner to finish the rest?

I can only add an example here, the NSURL class is the one to go. This is not complete but will give you a hint on how to use NSURL:
NSString *url_ = #"foo://name.com:8080/12345;param?foo=1&baa=2#fragment";
NSURL *url = [NSURL URLWithString:url_];
NSLog(#"scheme: %#", [url scheme]);
NSLog(#"host: %#", [url host]);
NSLog(#"port: %#", [url port]);
NSLog(#"path: %#", [url path]);
NSLog(#"path components: %#", [url pathComponents]);
NSLog(#"parameterString: %#", [url parameterString]);
NSLog(#"query: %#", [url query]);
NSLog(#"fragment: %#", [url fragment]);
output:
scheme: foo
host: name.com
port: 8080
path: /12345
path components: (
"/",
12345
)
parameterString: param
query: foo=1&baa=2
fragment: fragment
This Q&A NSURL's parameterString confusion with use of ';' vs '&' is also interesting regarding URLs.

NSURL has a method pathComponents, which returns an array with all the different path components. That should help you get the integer part. To get the name I'd use the host method of the NSURL. The docs say, that it should work if the URL is properly formatted, might as well give it a try then.
All in all, no need to convert into a string, there seems to be plenty of methods to work out the components of the URL from the NSURL object itself.

Actually there is a better way to parse NSURL. Use NSURLComponents. Here is a simle example:
Swift:
extension URL {
var params: [String: String]? {
if let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: true) {
if let queryItems = urlComponents.queryItems {
var params = [String: String]()
queryItems.forEach{
params[$0.name] = $0.value
}
return params
}
}
return nil
}
}
Objective-C:
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
NSArray *queryItems = [components queryItems];
NSMutableDictionary *dict = [NSMutableDictionary new];
for (NSURLQueryItem *item in queryItems)
{
[dict setObject:[item value] forKey:[item name]];
}

Thanks to Nick for pointing me in the right direction.
I wanted to compare file urls but was having problems with extra slashes making isEqualString useless. You can use my example below for comparing two urls by first de-constructing them and then comparing the parts against each other.
- (BOOL) isURLMatch:(NSString*) url1 url2:(NSString*) url2
{
NSURL *u1 = [NSURL URLWithString:url1];
NSURL *u2 = [NSURL URLWithString:url2];
if (![[u1 scheme] isEqualToString:[u2 scheme]]) return NO;
if (![[u1 host] isEqualToString:[u2 host]]) return NO;
if (![[url1 pathComponents] isEqualToArray:[url2 pathComponents]]) return NO;
//check some properties if not nil as isEqualSting fails when comparing them
if ([u1 port] && [u2 port])
{
if (![[u1 port] isEqualToNumber:[u2 port]]) return NO;
}
if ([u1 query] && [u2 query])
{
if (![[u1 query] isEqualToString:[u2 query]]) return NO;
}
return YES;
}

Related

Send NSURLConnection request multiple times with different url's in a for loop

I have an array of addresses that I need to convert to Lat/Long using Google's Geocode api. I am feeding an address and the city into the Google Geocode URL, which forms a correct connection url.
Basically I want to be able to use a for loop to create multiple NSURLConnection requests, returning multiple responses.
-(void)setString{
for (int i = 0; i < [businessArray count]; i ++)
{
NSString *address = [addressArray objectAtIndex:0];
NSString *city = [locationDict valueForKey:#"city"];
NSString *geocodeURL = [NSString stringWithFormat:#"http://maps.googleapis.com/maps/api/geocode/json?address=%#,+%#,&sensor=true", address, city];
geocodeURL = [geocodeURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:geocodeURL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10];
NSLog(#"%#", request);
geoCodeConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
if (geoCodeConnection)
{
responseData = [NSMutableData data];
connectionIsActive = YES;
NSLog(#"connection active");
} else {
NSLog(#"connection failed");
connectionIsActive = NO;
}
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
NSError *jsonError = nil;
SBJsonParser *json = [[SBJsonParser alloc] init];
NSDictionary *parsedJSON = [json objectWithString:responseString error:&jsonError];
NSString *lat= [[[[parsedJSON valueForKey:#"results"] valueForKey:#"geometry"] valueForKey:#"location"] valueForKey:#"lat"];
NSString *lng= [[[[parsedJSON valueForKey:#"results"] valueForKey:#"geometry"] valueForKey:#"location"] valueForKey:#"lng"];
NSLog(#"lat = %# long= %#", lat, lng);
connectionIsActive = NO;
[geoCodeLatArray addObject:lat];
[geoCodeLngArray addObject:lng];
NSLog(#"geoCodeArrayLat: %#", geoCodeLatArray);
}
Right now the code returns only the last address' lat and long. How can I send multiply requests and return multiply responses with JSON?
Try this I am using this,
for(int i=0;i< businessArray.count;i++)
{
NSString *address = [addressArray objectAtIndex:i];
NSString *city = [locationDict valueForKey:#"city"];
NSString *address = [NSString stringWithFormat:#"%#,%#", address, city];
CLLocationCoordinate2D location = [self geoCodeUsingAddress:address];
// then here store the location.latitude in lat array and location.longitude in long array.
}
- (CLLocationCoordinate2D) geoCodeUsingAddress:(NSString *)address
{
NSString *esc_addr = [address stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *req = [NSString stringWithFormat:#"http://maps.google.com/maps/api/geocode/json?sensor=false&address=%#", esc_addr];
NSDictionary *googleResponse = [[NSString stringWithContentsOfURL: [NSURL URLWithString: req] encoding: NSUTF8StringEncoding error: NULL] JSONValue];
NSDictionary *resultsDict = [googleResponse valueForKey: #"results"];
NSDictionary *geometryDict = [resultsDict valueForKey: #"geometry"];
NSDictionary *locationDict = [geometryDict valueForKey: #"location"];
NSArray *latArray = [locationDict valueForKey: #"lat"];
NSString *latString = [latArray lastObject];
NSArray *lngArray = [locationDict valueForKey: #"lng"];
NSString *lngString = [lngArray lastObject];
CLLocationCoordinate2D location;
location.latitude = [latString doubleValue];
location.longitude = [lngString doubleValue];
return location;
}
Update to the above function:
- (CLLocationCoordinate2D) geoCodeUsingAddress:(NSString *)address
{
double latitude = 0, longitude = 0;
NSString *esc_addr = [address stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *req = [NSString stringWithFormat:#"http://maps.google.com/maps/api/geocode/json?sensor=false&address=%#", esc_addr];
NSString *result = [NSString stringWithContentsOfURL:[NSURL URLWithString:req] encoding:NSUTF8StringEncoding error:NULL];
if (result) {
NSScanner *scanner = [NSScanner scannerWithString:result];
if ([scanner scanUpToString:#"\"lat\" :" intoString:nil] && [scanner scanString:#"\"lat\" :" intoString:nil]) {
[scanner scanDouble:&latitude];
if ([scanner scanUpToString:#"\"lng\" :" intoString:nil] && [scanner scanString:#"\"lng\" :" intoString:nil]) {
[scanner scanDouble:&longitude];
}
}
}
CLLocationCoordinate2D location;
location.latitude = latitude;
location.longitude = longitude;
return location;
}
This worked for me.
You might approach the problem using an asynchronous method that performs the request and has a completion block which will be called when the result is available. This completion block provides a parameter result which is the result of the connection request.
This method may be declared as follows:
typedef void (^completion_block_t) (id result);
- (void) fetchGeoCoordinateForAddress:(NSString*)address
completionHandler:(completion_block_t)completionHandler;
Say, if the request succeeds the parameter result in the block is a JSON representation of the response data. Otherwise, result is an NSError object indicating the error. But the exact details depend on how you implement the method fetchGeoCoordinateForAddress:completionHandler:.
Now you can setup the loop as follows:
for (NSString* address in addresses)
{
[self fetchGeoCoordinateForAddress:address completionHandler:^(id result) {
if (![result isKindOfError:[NSError class]]) // check if result is an error
{
// Note: result is not nil and is a NSDictionary representation of JSON.
// Retrieve the "location" from the response:
NSDictionary* location = result[#"results"][#"geometry"][#"location"];
// Multiple request can occur at the same time! Thus, we need to
// synchronize access to the result array "myLocations" through
// accessing it *exclusively and everywhere* on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self.myLocations addObject:location];
});
}
else {
// got error
DebugLog(#"ERROR: %#", result);
}
}
}
Note: your actual code may differ slightly depending on the actual JSON and other details.
Regarding the implementation of method fetchGeoCoordinateForAddress:completionHandler: you have a few options:
Use a third party library and implement a simple convenience wrapper fetchGeoCoordinateForAddress:completionHandler:.
Create your own "MyHTTPConnectionOperation" class that encapsulates a NSURLConnection and the response data and couple of other useful state info in a dedicated class. This class executes the request asynchronously via start method and has a completion handler. Basically, all third party network libraries will use this approach. Then implement the wrapper.
Use NSURLConnection's asynchronous convenient method if it is sufficient and works in your context. This is the fastest to implement, but least flexible approach and may not work in all cases and may also work only suboptimal.
Edit:
A couple of hints:
If possible, use NSJSONSerialization for parsing JSON and creating a Foundation representation. Other third party libraries only offer a slight advantage if you have special requirements, e.g. you need "chunked parsing with NSData objects" - which is useful when you want to download and parse simultaneously. Or you need to create other representations than Foundation - say a C++ container or you want directly create a Model with SAX style parsing. Or, you need better performance and lower memory food print since you are receiving ultra large strings which you want to save to disk. NSJSONSerialization became quite fast recently, so "performance" alone shouldn't be an argument today.
The timeout for the request shall be not that low as 10 seconds. In a cellular connection, this is too less. Leave it at the default.
If you plan to implement your own "HTTPConnectionOperation" class, I've put a very limited sample on gist here which can give you a jump start.
I think you have to start with AFNetworking
AFNetworking1
AFNetworking2
because AFNetworking gives a lot of power and flexibility in terms of scheduling and queueing requests as well as pausing and cancelling requests.

UIWebView URL from String without http://

So I check if the string starts with "http://" using the code below, and then I want to add "http://" so that I'm able to open the page in UIWebView.
NSString *firstString = [NSString stringWithFormat:#"%#", URL.text];
NSString *check = [NSString stringWithFormat:#"%#", #"http://"];
if (firstString != check) {
NSString *newString = [NSString stringWithFormat:#"http://%#", URL.text];
newString = [newString substringWithRange:NSMakeRange(7, newString.length - 7)];
URL.text = newString;
}
[WebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:URL.text]]];`
This doesn't work for some reason.
Do you know why?
Somebody posted as I was writing, but either way, here's an answer:
You are making this too difficult. You simply need to use hasPrefix to check for "http". As an example, I use this for my unified search/url bar.
- (IBAction)go:(id)sender {
NSString *inputString = [searchField stringValue];
NSString *outputString = [inputString stringByReplacingOccurrencesOfString:#" " withString:#"+"];
if ([inputString hasPrefix:#"http://"]) {
//Has Prefix
[[webView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:inputString]]];
}
else
{
//Does not have prefix. Do what you want here. I google it.
NSString *googleString = #"http://google.com/search?q=";
NSString *searchString = [NSString stringWithFormat:#"%#%#", googleString, outputString];
[[webView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:searchString]]];
}
}
That is for googling instead, you could keep using NSString *newString = [NSString stringWithFormat:#"http://%#", URL.text];
You could add a few more checks as well if you wanted to. Good luck!
firstString != check checks to see if both objects point to the same location in memory. [firstString isEqualToString:check] checks if the two strings are equal. However, what you most likely want to do is if(![firstString hasPrefix:check]). This will check to make sure firstString does not start with check, then you can append check to the start of it. Alternatively, you can do firstString = [firstString stringByReplacingOccurrencesOfString:#"http://" withString:#""];, and then you know it will never start with #"http://"
Try printing what URL.text is before the loadRequest line via NSLog(#"%#",URL.text);
Also the preferred check condition would be if (![firstString isEqualToString:check]) {}

How to get find and get URL in a NSString in iPhone?

I have a text with http:// in NSString. I want to get that http link from the NSString. How can i get the link/url from the string? Eg: 'Stack over flow is very useful link for the beginners https://stackoverflow.com/'. I want to get the 'https://stackoverflow.com/' from the text. How can i do this? Thanks in advance.
I am not sure what you exactly mean by link but if you want to convert your NSString to NSURL than you can do the following:
NSString *urlString = #"http://somepage.com";
NSURL *url = [NSURL URLWithString:urlString];
EDIT
This is how to get all URLs in a given NSString:
NSString *str = #"This is a grate website http://xxx.xxx/xxx you must check it out";
NSArray *arrString = [str componentsSeparatedByString:#" "];
for(int i=0; i<arrString.count;i++){
if([[arrString objectAtIndex:i] rangeOfString:#"http://"].location != NSNotFound)
NSLog(#"%#", [arrString objectAtIndex:i]);
}
Rather than splitting the string into an array and messing about that way, you can just search for the substring beginning with #"http://":
NSString *str = #"Stack over flow is very useful link for the beginners http://stackoverflow.com/";
// get the range of the substring starting with #"http://"
NSRange rng = [str rangeOfString:#"http://" options:NSCaseInsensitiveSearch];
// Set up the NSURL variable to hold the created URL
NSURL *newURL = nil;
// Make sure that we actually have found the substring
if (rng.location == NSNotFound) {
NSLog(#"URL not found");
// newURL is initialised to nil already so nothing more to do.
} else {
// Get the substring from the start of the found substring to the end.
NSString *urlString = [str substringFromIndex:rng.location];
// Turn the string into an URL and put it into the declared variable
newURL = [NSURL URLWithString:urlString];
}
try this :
nsstring *str = #"Stack over flow is very useful link for the beginners http://stackoverflow.com/";
nsstring *http = #"http";
nsarray *arrURL = [str componentsSeparatedByString:#"http"];
this will give two objects in the nsarray. 1st object will be having:Stack over flow is very useful link for the beginners and 2nd will be : ://stackoverflow.com/ (i guess)
then you can do like:
NSString *u = [arrURL lastObject];
then do like:
nsstring *http = [http stringByAppendingFormat:#"%#",u];
Quite a lengthy,but i think that would work for you. Hope that helps you.

NSURL with Curly Braces

NSURL does not support the use of curly braces (i.e. {}) in URLs. My application needs to talk to a server that requires the use of curly braces in the URL. I'm considering using bridges to write the networking code in Python or C++, or even rewriting the C code for NSURL to make it accept curly braces. Percent escapes are not accepted by my remote server.
Do I have any other good alternatives?
EDIT: Explained why addingPercentEscapesUsingEncoding and the like don't work for me here.
Will it work for you if you escape the braces?
This code:
// escape {} with %7B and %7D
NSURL *url = [NSURL URLWithString:#"http://somesite.com/%7B7B643FB915-845C-4A76-A071-677D62157FE07D%7D.htm"];
NSLog(#"%#", url);
// {} don't work and return null
NSURL *badurl = [NSURL URLWithString:#"http://somesite.com/{7B643FB915-845C-4A76-A071-677D62157FE07D}.htm"];
NSLog(#"%#", badurl);
Outputs:
2011-11-30 21:25:06.655 Craplet[48922:707] http://somesite.com/%7B7B643FB915-845C-4A76-A071-677D62157FE07D%7D.htm
2011-11-30 21:25:06.665 Craplet[48922:707] (null)
So, escaping seems to work
Here's how you can programmatically escape the url:
NSString *escapedUrlString = [unescaped stringByAddingPercentEscapesUsingEncoding: NSASCIIStringEncoding];
EDIT:
In your comment below, you said your server won't accept encoded braces and was there any other alternatives. First, I would try and get the server fixed. If that's not possible ... I haven't tried this with braces etc... but the layer below NS networking classes is CFNetworking.
See this:
http://developer.apple.com/library/ios/#documentation/Networking/Conceptual/CFNetwork/CFHTTPTasks/CFHTTPTasks.html#//apple_ref/doc/uid/TP30001132-CH5-SW2
From that doc:
CFStringRef url = CFSTR("http://www.apple.com");
CFURLRef myURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
CFStringRef requestMethod = CFSTR("GET");
....
Once again, haven't tried it and I'm running out. might try it later but if a layer isn't working your first options are to move down the stack.
Bryan's answer is perfectly great (+1 to him!), but another solution would be to use NSString's stringByAddingPercentEscapesUsingEncoding: method.
Like this:
NSString * originalURLAsString = [NSString stringWithString: #"http://somesite.com/{7B643FB915-845C-4A76-A071-677D62157FE07D}.htm"];
NSString * properlyEncodedString = [originalURLAsString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
NSURL * url = [NSURL URLWithString: properlyEncodedString];
I hope this info helps you out!
To call using CFNetwork can use the follwoing
-(void)getDataFromUrl{
CFStringRef tempURLA = CFSTR("http://my.test.server/iostest/index.html?{\"a\":\"b\"}");
CFStringRef tempUrlSting = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)tempURLA,CFSTR("{}"), CFSTR("\""), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
CFURLRef myURL = CFURLCreateWithString(kCFAllocatorDefault, tempUrlSting, NULL);
CFStringRef requestMethod = CFSTR("GET");
CFHTTPMessageRef myRequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, myURL,kCFHTTPVersion1_1);
CFStringRef headerFieldName = CFSTR("Accept");
CFStringRef headerFieldValue = CFSTR("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
CFHTTPMessageSetHeaderFieldValue(myRequest, headerFieldName, headerFieldValue);
[self performHTTPRequest:myRequest];
}
-(void)performHTTPRequest:(CFHTTPMessageRef)request {
CFURLRef gotdatab = (__bridge CFURLRef)(CFBridgingRelease(CFHTTPMessageCopyRequestURL(request)));
// NSLog(#"(CFHTTPMessageRef request %#",gotdatab);
CFReadStreamRef requestStream = CFReadStreamCreateForHTTPRequest(NULL, request);
CFReadStreamOpen(requestStream);
NSMutableData *responseBytes = [NSMutableData data];
NSError *error;
while (TRUE) {
if (CFReadStreamHasBytesAvailable(requestStream)) {
UInt8 streambuffer[1024];
int readBytes = CFReadStreamRead (requestStream,streambuffer,sizeof(streambuffer));
NSLog(#"Read: %d",readBytes);
[responseBytes appendBytes:streambuffer length:readBytes];
}
if (CFReadStreamGetStatus(requestStream) == kCFStreamStatusError) {
error = (NSError*)CFBridgingRelease(CFReadStreamCopyError (requestStream));
if ([error code] == 61) {
// connection refused
NSLog(#"Error occured: %d",[error code]);
}
break;
}
if (CFReadStreamGetStatus(requestStream) == kCFStreamStatusAtEnd) {
NSLog(#"Stream reached end!");
error = nil;
break;
}
}//
CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreamCopyProperty(requestStream, kCFStreamPropertyHTTPResponseHeader);
if (response==NULL) {
NSLog(#"response is null");
return;
}
}
The above was done using examples from here and here
But even the above method has the same issue. That is: if {} are not encoded the URL doesn't get generated. If the {} are encoded the server doesn't return a proper value.
Any other suggestions to resolve this posts issue?

How can you read a files MIME-type in objective-c

I am interested in detecting the MIME-type for a file in the documents directory of my iPhone application. A search through the docs did not provide any answers.
It's a bit hacky, but it should work, don't know for sure because I'm just guessing at it
There are two options:
If you just need the MIME type, use the timeoutInterval: NSURLRequest.
If you want the data as well, you should use the commented out NSURLRequest.
Make sure to perform the request in a thread though, since it's synchronous.
NSString* filePath = [[NSBundle mainBundle] pathForResource:#"imagename" ofType:#"jpg"];
NSString* fullPath = [filePath stringByExpandingTildeInPath];
NSURL* fileUrl = [NSURL fileURLWithPath:fullPath];
//NSURLRequest* fileUrlRequest = [[NSURLRequest alloc] initWithURL:fileUrl];
NSURLRequest* fileUrlRequest = [[NSURLRequest alloc] initWithURL:fileUrl cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:.1];
NSError* error = nil;
NSURLResponse* response = nil;
NSData* fileData = [NSURLConnection sendSynchronousRequest:fileUrlRequest returningResponse:&response error:&error];
fileData; // Ignore this if you're using the timeoutInterval
// request, since the data will be truncated.
NSString* mimeType = [response MIMEType];
[fileUrlRequest release];
Add MobileCoreServices framework.
Objective C:
#import <MobileCoreServices/MobileCoreServices.h>
NSString *fileExtension = [myFileURL pathExtension];
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
Swift:
import MobileCoreServices
func mimeType(fileExtension: String) -> String? {
guard !fileExtension.isEmpty else { return nil }
if let utiRef = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as CFString, nil) {
let uti = utiRef.takeUnretainedValue()
utiRef.release()
if let mimeTypeRef = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType) {
let mimeType = MIMETypeRef.takeUnretainedValue()
mimeTypeRef.release()
return mimeType as String
}
}
return nil
}
The accepted answer is problematic for large files, as others have mentioned. My app deals with video files, and loading an entire video file into memory is a good way to make iOS run out of memory. A better way to do this can be found here:
https://stackoverflow.com/a/5998683/1864774
Code from above link:
+ (NSString*) mimeTypeForFileAtPath: (NSString *) path {
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
return nil;
}
// Borrowed from https://stackoverflow.com/questions/5996797/determine-mime-type-of-nsdata-loaded-from-a-file
// itself, derived from https://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)[path pathExtension], NULL);
CFStringRef mimeType = UTTypeCopyPreferredTagWithClass (UTI, kUTTagClassMIMEType);
CFRelease(UTI);
if (!mimeType) {
return #"application/octet-stream";
}
return [NSMakeCollectable((NSString *)mimeType) autorelease];
}
Prcela solution did not work in Swift 2. The following simplified function will return the mime-type for a given file extension in Swift 2:
import MobileCoreServices
func mimeTypeFromFileExtension(fileExtension: String) -> String? {
guard let uti: CFString = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as NSString, nil)?.takeRetainedValue() else {
return nil
}
guard let mimeType: CFString = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() else {
return nil
}
return mimeType as String
}
I was using the answer provided by slf in a cocoa app (not iPhone) and noticed that the URL request seems to be reading the entire file from disk in order to determine the mime type (not great for large files).
For anyone wanting to do this on the desktop here is the snippet I used (based on Louis's suggestion):
NSString *path = #"/path/to/some/file";
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath: #"/usr/bin/file"];
[task setArguments: [NSArray arrayWithObjects: #"-b", #"--mime-type", path, nil]];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file = [pipe fileHandleForReading];
[task launch];
[task waitUntilExit];
if ([task terminationStatus] == YES) {
NSData *data = [file readDataToEndOfFile];
return [[[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding] autorelease];
} else {
return nil;
}
If you called that on a PDF file it would spit out: application/pdf
Based on the Lawrence Dol/slf answer above, I have solved the NSURL loading the entire file into memory issue by chopping the first few bytes into a head-stub and getting the MIMEType of that. I have not benchmarked it, but it's probably faster this way too.
+ (NSString*) mimeTypeForFileAtPath: (NSString *) path {
// NSURL will read the entire file and may exceed available memory if the file is large enough. Therefore, we will write the first fiew bytes of the file to a head-stub for NSURL to get the MIMEType from.
NSFileHandle *readFileHandle = [NSFileHandle fileHandleForReadingAtPath:path];
NSData *fileHead = [readFileHandle readDataOfLength:100]; // we probably only need 2 bytes. we'll get the first 100 instead.
NSString *tempPath = [NSHomeDirectory() stringByAppendingPathComponent: #"tmp/fileHead.tmp"];
[[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil]; // delete any existing version of fileHead.tmp
if ([fileHead writeToFile:tempPath atomically:YES])
{
NSURL* fileUrl = [NSURL fileURLWithPath:path];
NSURLRequest* fileUrlRequest = [[NSURLRequest alloc] initWithURL:fileUrl cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:.1];
NSError* error = nil;
NSURLResponse* response = nil;
[NSURLConnection sendSynchronousRequest:fileUrlRequest returningResponse:&response error:&error];
[[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];
return [response MIMEType];
}
return nil;
}
On Mac OS X this would be handled through LaunchServices and UTIs. On the iPhone these are not available. Since the only way for data to get into your sandbox is for you to put it there, most apps have intrinsic knowledge about the data of any file they can read.
If you have a need for such a feature you should file a feature request with Apple.
I'm not sure what are the practices on iPhone, but if you're allowed to, I'd make use of UNIX philosophy here: use program file, which is the standard way to detect filetype on an UNIX operating system. It includes a vast database of magic markers for filetype detection. Since file is probably not shipped on iPhone, you could include it in your app bundle. There might be a library implementing file's functionality.
Alternatively, you could trust the browser. Browsers send the MIME type they guessed somewhere in the HTTP headers. I know that I can easily grab the MIME type information in PHP. That of course depends if you're willing to trust the client.
Make sure are you import the coreservices
import <CoreServices/CoreServices.h>
in your file.