I want to cache an image I download from a URL in the local filesystem.
What I don't know, is how can I create from the URL a NSString that is compatible with the characters the iOS file system supports. For instance, if I try to create a file with a : in the name, it will fail.
What steps should I follow to create this NSString? Is a simple hash the best way to go? If so, what hash routine is available in iOS that I can use?
I'd use hashing:
it'll make for much more readable file names
you avoid problems with file name length
md5 should be perfectly fine for your purposes. Unfortunately, the ios5-sdk contains only a C-String function for this:
CC_MD5(in, in_len, out);
The function is contained in <CommonCrypto/CommonDigest.h>, there are other hash functions there, too.
You can find infos on how to wrap this up in a function that takes/returns an NSString here.
The following code belongs to ZDS_Shared. resolveLocalURLForRemoteURL accepts a remote URL and returns a URL pointing to the file on the iOS filesystem. The filename will be an alphanumeric string that doesn't resemble the original URL, but that shouldn't be a problem.
https://github.com/ZarraStudios/ZDS_Shared/blob/master/ZSAssetManager.m#L185
- (NSURL*)resolveLocalURLForRemoteURL:(NSURL*)url
{
if (!url) return nil;
NSString *filename = [[url absoluteString] zs_digest];
NSString *filePath = [[self cachePath] stringByAppendingPathComponent:filename];
return [NSURL fileURLWithPath:filePath];
}
https://github.com/ZarraStudios/ZDS_Shared/blob/master/NSString%2BZSAdditions.m#L38
// NSString category
- (NSString*)zs_digest
{
const char *cstr = [self cStringUsingEncoding:NSASCIIStringEncoding];
return [[NSData dataWithBytes:cstr length:strlen(cstr)] zs_digest];
}
https://github.com/ZarraStudios/ZDS_Shared/blob/master/NSData%2BZSAdditions.m#L38
// NSData category
- (NSString*)zs_digest
{
uint8_t digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1([self bytes], [self length], digest);
NSMutableString* outputHolder = [[NSMutableString alloc] initWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) {
[outputHolder appendFormat:#"%02x", digest[i]];
}
NSString *output = [outputHolder copy];
MCRelease(outputHolder);
return [output autorelease];
}
Related
-- a question about how to make an object that is saved to the documents directory persist on the drive and be recoverable after the iDevice is rebooted.
Here's my problem. I make a data object with NSCoding and fill it with data. I write it to the documentsDirectory each time the data in the object are updated. I stop the app and start the app again, and my data object persists, with all of its data. But if I reboot the iPhone the code I wrote to recover and read the data object fails.
The code I wrote originally used only a NSString for the file path. It worked well under ios7 but it fails under ios8.
Reading up on things, I found this clue from the Apple documentation:
"Important: Although they are safe to use while your app is running, file reference URLs are not safe to store and reuse between launches of your app because a file’s ID may change if the system is rebooted. If you want to store the location of a file persistently between launches of your app, create a bookmark as described in Locating Files Using Bookmarks."
So I rewrote my ios7 file open and file close methods so they no longer use strings or urls but get their strings and urls from a bookmark that is saved using NSUserDefaults. Same problem: everything works fine so long as I do not power off the phone, but all is lost once I do. I am not able to solve this.
Here is my current series of steps. First I either determine (or if it already exists in NSUsrDefaults, I recover) the absolute path to the documentsDirectory, using a bookmark:
+ (NSString*) getGeoModelAbsolutePath
{
NSString *path;
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSURL *documentsDirectoryBookmarkURL;
NSData* documentsDirectoryBookmark = [userDefaults objectForKey:#"documentDirectoryBookmark"];
if(documentsDirectoryBookmark == nil)
{
documentsDirectoryBookmarkURL = [self getDocumentsDirectoryURL];
documentsDirectoryBookmark = [self bookmarkForURL:documentsDirectoryBookmarkURL];
}
documentsDirectoryBookmarkURL = [self urlForBookmark:documentsDirectoryBookmark];
path = documentsDirectoryBookmarkURL.path;
path = [path stringByAppendingString:#"/Model.mod"];
return path;
}
using methods modified from my ios7 code (which used only the getDocumentsDirectory method):
+ (NSString *)getDocumentsDirectory
{
NSURL *directory = [self getDocumentsDirectoryURL];
NSString * documentsDirectory = directory.path;
return documentsDirectory;
}
And
+ (NSURL *)getDocumentsDirectoryURL
{
NSURL *directory = [[[NSFileManager defaultManager]
URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask]
lastObject];
return directory;
}
And
+ (NSData*)bookmarkForURL:(NSURL*)url {
NSError* theError = nil;
NSData* bookmark = [url bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&theError];
if (theError || (bookmark == nil)) {
// Handle any errors.
return nil;
}
return bookmark;
}
So now I have a NSString path with the model filename that I can use to get to the GeoModel
- (GeoModel*) openGeoModel
{
GeoModel *geoModel;
NSString* documentsDirectoryGeoModel =[FileManager getGeoModelAbsolutePath];
if([FileManager fileExistsAtAbsolutePath:documentsDirectoryGeoModel])
{
NSData* data = [NSData dataWithContentsOfFile: documentsDirectoryGeoModel]; //]documentsDirectoryGeoModel];
geoModel = [NSKeyedUnarchiver unarchiveObjectWithData: data];
NSString *unarchivedGeoModelVersion = geoModel.geoModel_VersionID;
if(![unarchivedGeoModelVersion isEqual: currentGeoModelVersion])
{
[FileManager deleteFile:documentsDirectoryGeoModel];
geoModel = [GeoModel geoModelInit];
[Utilities setGeoProjectCounter:0];
}
}
else
{
geoModel = [GeoModel geoModelInit];
}
[FileManager saveGeoModel];
return geoModel;
}
Which I then can save to the documentsDirectory as follows:
+ (BOOL)saveGeoModel
{
NSError *error = nil;
NSString *path = [self getGeoModelAbsolutePath];
[NSKeyedArchiver archiveRootObject:appDelegate.currentGeoModel toFile:path];
NSData* encodedData = [NSKeyedArchiver archivedDataWithRootObject: appDelegate.currentGeoModel];
BOOL success = [encodedData writeToFile: path options:NSDataWritingAtomic error:&error];
return success;
}
Which is always successful -- but is persistent only if I do not turn off the device! I am not making any progress with this: Any help would be much appreciated!
Thanks in advance
Tim Redfield
There. I think it is answered -- unless someone else has a comment on how to improve the above listings, they DO work as they ought to!
I trying to record a video and I'm getting the error like
Cannot record to URL <#file url> because it is not a file URL.
I define the destination url as follows:
NSString *Path = [[NSString alloc] init];
Path = #"/Users/me/Documents/My fols/recording_try/newMovie.mov";
NSURL *dest = [[NSURL alloc] initWithString:[Path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
Then after creating the session, input and output objects. I tried recording like this.
mMovieFileOutput = [[AVCaptureMovieFileOutput alloc] init] ;
[mSession addOutput:mMovieFileOutput];
[mMovieFileOutput startRecordingToOutputFileURL:dest recordingDelegate:self];
I have started running the session, tried using begin and comitconfiguration, etc. But every time I run I get an error like:
[AVCaptureMovieFileOutput startRecordingToOutputFileURL:recordingDelegate:] - Cannot record to URL /Users/me/Documents/My%20fols/recording_try/newMovie.mov because it is not a file URL.
I don't know where I'm going wrong... Could someone please help???
Thanks in Advance...
Just change your NSURL configuration to conform to file URL type
Something like this:
NSURL *dest = [[NSURL alloc] initFileURLWithPath:<#(NSString *)#>]
Try something like this:
// variable names should start with lower case letters
// also, let's do as much as we can with auto-released objects
// so we don't have to worry about leaking (if we're not using ARC)
NSString *path = [NSString stringWithString: #"/Users/me/Documents/My fols/recording_try"];
NSURL *dest = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
if(dest == nil)
{
NSLog( #"does not appear to be a valid NSURL object" );
return;
}
NSError * error = nil;
if([[NSFileManager defaultManager] createDirectoryAtURL: dest withIntermediateDirectories: YES attributes: nil error: &error] == YES)
{
dest = [dest URLByAppendingPathComponent: #"newMovie.mov"];
// now you can create the session plus input and output objects
// within this block
} else {
NSLog( #"was not able to create the directory which contains path %# - error is %#", path, [error localizedDescription] );
}
I'm hoping to find a method to pass certain information in to my app when I launch it during testing, so that I can perform special debug tasks. Xcode has a section "Arguments Passed on Launch", and I assumed they would show up in my UIApplicationDelegate's application:didFinishLaunchingWithOptions: but the dictionary that's passed in is always nil.
Am I going about this the wrong way?
You can access them using NSProcessInfo object like this,
NSArray * arguments = [[NSProcessInfo processInfo] arguments];
Another easier way is to use the NSUserDefaults.
http://perspx.com/archives/parsing-command-line-arguments-nsuserdefaults/
From the article:
Command line arguments that can be parsed and used by the
NSArgumentDomain must take the format:
-name value
The argument is stored as a default with key of name and value of
value.
At this point accessing values passed in on the command line is the
same process for accessing any other defaults.
For example running an application as such:
MyApplication -aString "Hello, World" -anInteger 10
allows the command line arguments to be retrieved as such:
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
NSString *aString = [standardDefaults stringForKey:#"aString"];
NSInteger anInteger = [standardDefaults integerForKey:#"anInteger"];
For those who stumbled to this question like me :)
I wanted to have a logLevel for my static lib. The way I did is,
static NSUInteger logLevel = 1;
/** This argument should be passed from XCode's build scheme configuration option, Arguments passed on launch */
static const NSString *kIdcLogLevelArgument = #"-com.mycompany.IDCLogLevel";
#implementation IDCLogger
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
+(void)initialize
{
logLevel = 1;
NSArray *arguments = [[NSProcessInfo processInfo] arguments];
NSUInteger value = 0;
if ([arguments containsObject:kIdcLogLevelArgument]) {
NSUInteger index = [arguments indexOfObject:kIdcLogLevelArgument];
if (arguments.count > index) {
NSString *valueStr = [arguments objectAtIndex:index + 1];
NSCharacterSet* notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
if ([valueStr rangeOfCharacterFromSet:notDigits].location == NSNotFound)
{
value = [valueStr integerValue];
logLevel = value;
}
}
}
NSLog(#"%#:logLevel = %lu", [self class], (unsigned long)logLevel);
}
+ (void)setLogLevel:(NSUInteger)l
{
logLevel = l;
NSLog(#"[%#]: Log level set to: %lu", [self class], (unsigned long)l);
}
In addition to scalars, command line arguments can be an NSData, NSArray, or NSDictionary references. Apple's documentation on "Old-Style ASCII Property Lists" tells how to do it. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html#//apple_ref/doc/uid/20001012-BBCBDBJE
For example, this syntax should decode into an NSDictionary:
MyApplication -aLocation "{ latitude = 37.40089; longitude = -122.109428; }"
I am building an app that allows users to select a file and/or folder either locally or across the network and list the contents of that selection in a NSTableView after some filtering (no hidden files, only accepting .tif, .eps). The user can then select a file name from the list and then have the files metadata shown to them. At least that is what I want to happen. Right now I am getting null returned for the metadata. Here's my code:
- (void)tableViewSelectionDidChange:(NSNotification *)notif {
NSDictionary* metadata = [[NSDictionary alloc] init];
//get selected item
NSString* rowData = [fileList objectAtIndex:[tblFileList selectedRow]];
//set path to file selected
NSString* filePath = [NSString stringWithFormat:#"%#/%#", objPath, rowData];
//declare a file manager
NSFileManager* fileManager = [[NSFileManager alloc] init];
//check to see if the file exists
if ([fileManager fileExistsAtPath:filePath] == YES) {
//escape all the garbage in the string
NSString *percentEscapedString = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)filePath, NULL, NULL, kCFStringEncodingUTF8);
//convert path to NSURL
NSURL* filePathURL = [[NSURL alloc] initFileURLWithPath:percentEscapedString];
NSError* error;
NSLog(#"%#", [filePathURL checkResourceIsReachableAndReturnError:error]);
//declare a cg source reference
CGImageSourceRef sourceRef;
//set the cg source references to the image by passign its url path
sourceRef = CGImageSourceCreateWithURL((CFURLRef)filePathURL, NULL);
//set a dictionary with the image metadata from the source reference
metadata = (NSDictionary *)CGImageSourceCopyPropertiesAtIndex(sourceRef,0,NULL);
NSLog(#"%#", metadata);
[filePathURL release];
} else {
[self showAlert:#"I cannot find this file."];
}
[fileManager release];
}
I'm guessing the problem here is the CFURLREF in CGImageSourceCreateWithURL. Instead of NSURL should I be using something else?
Thanks
Here's the path I am passing (logged from filePathURL): file://localhost/Volumes/STORAGE%20SVR/Illustration-Wofford/Illustration%20Pickup/Archive/AL013111_IL_Communication.eps
I can't tell where the "localhost" part in your file URL comes from, but I think that's the culprit. A file url doesn't usually contain a "localhost" part. In your example, it should look like this:
file:///Volumes/STORAGE%20SVR/Illustration-Wofford/Illustration%20Pickup/Archive/AL013111_IL_Communication.eps
But I'm pretty sure you've figured this out by now :)
Update: I stand corrected by Mike's comment: file://localhost/... is the same thing as file:///...!
I am currently trying to put together an URL where I specify some GET parameters. But I want to use japanese or other characters too in this URL.
Is there a way to convert a NSString to a string containing the HTML entities for the 'special' characters in my NSString?
I am currently using the following code, which seems to work, except for 'special characters' like chinese and japanese:
NSString* url = #"/translate_a/t?client=t&sl=auto&tl=";
url = [url stringByAppendingString:destinationLanguage];
url = [url stringByAppendingString:#"&text="];
url = [url stringByAppendingString:text];
NSURL* nsurl = [[NSURL alloc] initWithScheme:#"http" host:#"translate.google.com" path:url];
NSError* error;
NSString* returnValue = [[NSString alloc] initWithContentsOfURL:nsurl encoding:NSUTF8StringEncoding error:&error];
To properly URL encode your parameters, you need to convert each name and value to UTF-8, then URL encode each name and value separately, then join names with values using '=' and name-value pairs using '&'.
I generally find it easier to put all the parameters in an NSDictionary, then build the query string from the dictionary. Here's a category that I use for doing that:
// file NSDictionary+UrlEncoding.h
#import <Cocoa/Cocoa.h>
#interface NSDictionary (UrlEncoding)
-(NSString*) urlEncodedString;
#end
// file NSDictionary+UrlEncoding.m
#import "NSDictionary+UrlEncoding.h"
// private helper function to convert any object to its string representation
static NSString *toString(id object) {
return [NSString stringWithFormat: #"%#", object];
}
// private helper function to convert string to UTF-8 and URL encode it
static NSString *urlEncode(id object) {
NSString *string = toString(object);
return [string stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
}
#implementation NSDictionary (UrlEncoding)
-(NSString*) urlEncodedString {
NSMutableArray *parts = [NSMutableArray array];
for (id key in self) {
id value = [self objectForKey: key];
NSString *part = [NSString stringWithFormat: #"%#=%#",
urlEncode(key), urlEncode(value)];
[parts addObject: part];
}
return [parts componentsJoinedByString: #"&"];
}
#end
The method build an array of name-value pairs called parts by URL encoding each key and value, then joining them together with '='. Then the parts in the parts array are joined together with '&' characters.
So for your example:
#import "NSDictionary+UrlEncoding.h"
// ...
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
[parameters setValue: #"t" forKey: #"client"];
[parameters setValue: #"auto" forKey: #"sl"];
[parameters setValue: destinationLanguage forKey: #"tl"];
[parameters setValue: text forKey: #"text"];
NSString *urlString = [#"/translate_a/t?" stringByAppendingString: [parameters urlEncodedString]];
NSString has a method -stringByAddingPercentEscapesUsingEncoding:
Here's NSString extension you can find over internet
http://code.google.com/p/wimframework/source/browse/trunk/WimFramework/Classes/Helpers/WimAdditions.m
The decode part has some error in mapping array index to actual entity number. But since you only need encoding, it's fine to use it.
For simple URL encoding of strings, many of the solutions I have seen, while technically correct, look a lot less easy to use than I would like. So I came up with the following NSString category:
#interface NSString (MLExtensions)
- (NSString *)urlencode;
#end
NSString *_mlfilterChars = #";/?:#&=+$,";
#implementation NSString (MLExtensions)
- (NSString *)urlencode
{
return [[NSString stringWithString: (NSString *)
CFURLCreateStringByAddingPercentEscapes(
NULL,
(CFStringRef)self,
NULL,
(CFStringRef)_mlfilterChars,
kCFStringEncodingUTF8)]
stringByReplacingOccurrencesOfString: #"%20" withString: #"+"];
}
#end
I'm kind of in a hurry with some other stuff I'm working on, so I kind of cheated with the %20 => + conversion step, but it all seems to work great and I've been using it for a while now with a good number of URLs in my app.
Usage is blessfully easy:
- (NSString *)URLForSearch: (NSString *)searchFor
{
return [#"http://example.org/search?query="
stringByAppendingString: [searchFor urlencode]];
}