Upgrading an Xcode Objective-C IOS game with Unity3d based project - objective-c

I have an old Xcode/ObC quiz game that I launched quite a few years ago, pre-swift, that has been, and still is quite successful for me. At least the local version.
I am now at the finish line rewriting this game in Unity3d c#.
Something I have been thinking about lately is how to to maintain the "old" statistics that is saved in a plist file in IOS. I have been google this but I would need some more information to really understand how to proceed.
What will happen with the stats-plist file when I upgrade the current Xcode/ObC with the new Unity-based project, will it still be there and is it possible to easily find it? This particular plist is added when the first player is added and then updated with stats and new players.
Is there a good, and easy, way reading plist from Unity and convert to a normal text file?
To be able to find the file from Unity I am thinking of launching a maintenance release of the ObC based game and only copy this plist file to another directory (Document) to prepare for the new big release. When starting the Unity-based game for the first time I could then read the copied file and process so the player do not lose his/her stats.
The problem I have is that the only time I have updated the actual ObC code the last 5 - 6 years is when I updated the app from 32 to 64 bit so my skills on ObC is very limited at the moment.
I have been thinking of using something like this for the plist:
NSFileManager *filemgr;
filemgr = [NSFileManager defaultManager];
if ([filemgr copyItemAtPath: #"/tmp/myfile.txt" toPath: #"/Users/demo/newfile.txt" error: NULL] == YES)
NSLog (#"Copy successful");
else
NSLog (#"Copy failed");
I would really appreciate some advise how I should process this.

Here is some code you can use to list the doc and app support contents. I guard this with the #define as I do not want this in the final app. I also use it to perform some cleanup (the commented out stuff) that you can use if you need to delete something.
#ifdef DEBUG
// Auxiliary function to list directory contents
+ (void) docList
{
NSFileManager * fileManager = NSFileManager.defaultManager;
NSURL * docUrl = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
NSString * docPath = docUrl.path;
NSLog( #"User document directory" );
NSLog( #"%# listing follows", docPath );
[fmUtil traverse:docPath tab:#"\t" fileManager:fileManager];
}
// Auxiliary function to list application support path
+ (void) supList
{
NSFileManager * fileManager = NSFileManager.defaultManager;
NSURL * docUrl = [NSFileManager.defaultManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask].firstObject;
NSString * docPath = docUrl.path;
NSLog( #"Application support directory" );
NSLog( #"\t%#", docPath );
NSLog( #"\tAppending bundle identifier" );
docPath = [docPath stringByAppendingPathComponent:NSBundle.mainBundle.bundleIdentifier];
NSLog( #"\t%#", docPath );
NSLog( #"%# listing follows", docPath );
[fmUtil traverse:docPath tab:#"\t" fileManager:fileManager];
}
+ (void) traverse:(NSString *)root tab:(NSString *)tab fileManager:(NSFileManager *)fileManager
{
NSArray * dir = [fileManager contentsOfDirectoryAtPath:root error:NULL];
for ( NSString * s in dir )
{
// See if this is a directory or not
NSString * t = [root stringByAppendingPathComponent:s];
BOOL isDir = NO;
[fileManager fileExistsAtPath:t isDirectory:& isDir];
if ( isDir )
{
// Report
NSLog(#"%#%#/",tab,s);
// Traverse
[fmUtil traverse:t tab:[tab stringByAppendingString:#"\t"]
fileManager:fileManager];
}
else
{
// Get size of normal file
NSDictionary * fa = [fileManager attributesOfItemAtPath:t error:NULL];
NSNumber * fz = [fa objectForKey:NSFileSize];
NSDate * fm = [fa objectForKey:NSFileModificationDate];
unsigned long long n = fz.unsignedLongLongValue;
// Some formatting
NSString * f = s;
while ( f.length < 50 )
{
f = [f stringByAppendingString:#" "];
}
NSLog(#"%#%# %15llu bytes (%#)", tab, f, n, [fm descriptionWithLocale:NSLocale.currentLocale] );
// To delete something for test purposes ...
/*
if ( [t.lastPathComponent isEqualToString:#"P5041-1-BuildingStatistics"] && [fileManager removeItemAtPath:t error:NULL] )
{
NSLog( #"%#%# now xxxxxxxxxxxxxxxxxx", tab, f );
}
if ( [t.lastPathComponent isEqualToString:#"index"] && [fileManager removeItemAtPath:t error:NULL] )
{
NSLog( #"%#%# now xxxxxxxxxxxxxxxxxx", tab, f );
}
*/
}
}
}
#endif
Put this in your app delegate e.g. inside application:didFinishLaunchingWithOptions:.

Well, today I started to look back on this "problem" and after testing it turned out not to be a problem at all. I tried initially to find the path via my Objective-C code (this post) but when testing I realised that the files were stored in the (IOS game) Document directory, the same as "Application.persistentDataPath". In Unity I used the following path's:
filePathPlayerData = Application.persistentDataPath + "/playerdata.plist";
filePathPlayerStats = Application.persistentDataPath + "/playerstatistics.plist";
Reading and process the file is easy as well as it is XML structure. However due to the simple format I opened the files as text files wrote my own simple parser in c# (unity), rather than using an XML parser. The whole "problem" turned out not to be any problem.

Related

Objective-c NSXMLParser with Coredata takes too long to parse and store data

I'm not a experience xcode/objective-c programmer so I'm sorry if you might not understand somethings I might say, or somethings I might say wrong.
So everything started with apple rejecting our app because it says:
"Your app did not include sufficient content in the binary for the app to function at launch, and we were required to download or unpack additional resources before we could use it."
Because our app is a transports app, it needs to download dynamic data from services to keep the app with the most accurate and updated data. So basically everytime we open the app we ask the user to download the data (~2.5 MB), but since Apple refused to aprove the app I made an exception and let user enter without downloading any data, but it needs to convert the local XML file into core-data database.
My problem is, this file that is like 2,5 MB space with ~17k lines takes at least 2 minutes to read and store the provided data.
So I tried to see the parser if it was the problem but the code seems fine to me.
I know that this what I'm doing might not be the solution because of what Apple said the "unpacking additional resources" so I think it won't pass the app verification, but still I wanted this to do the parse and store with less time...
This is my code:
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
// <Network LinesNumber="28" ZonesNumber="112" StopsNumber="114">
if ([elementName isEqualToString:#"Network"]) {
int ln = [[attributeDict objectForKey:#"LinesNumber"] intValue];
int zn = [[attributeDict objectForKey:#"ZonesNumber"] intValue];
int sn = [[attributeDict objectForKey:#"StopsNumber"] intValue];
totalItems = (totalItems + ln + sn + zn) *1.0;
updateaction(0);
}
else if ([elementName isEqualToString:#"Stop"]) {
int idStop = [[attributeDict objectForKey:#"Id"] intValue];
NSString* name = [[attributeDict objectForKey:#"Name"] capitalizedString];
NSString* codName = [attributeDict objectForKey:#"CodName"];
int idZona = [[attributeDict objectForKey:#"IdZona"] intValue];
int idDistrito = [[attributeDict objectForKey:#"IdCounty"] intValue];
int idConcelho = [[attributeDict objectForKey:#"IdDistrict"] intValue];
int idFreguesia = [[attributeDict objectForKey:#"IdParish"] intValue];
double latitude = [[attributeDict objectForKey:#"CoordYY"] doubleValue];
double longitude = [[attributeDict objectForKey:#"CoordXX"] doubleValue];
NSString* obs = [attributeDict objectForKey:#"Observation"];
OperatorZone *zone = [Database operatorZoneFromId:idZona];
Stop *stop = [Database createStop:idStop withName:name withCodName:codName withIdZona:idZona withIdDistrito:idDistrito withIdConcelho:idConcelho withIdFreguesia:idFreguesia withLatitude:latitude withLongitude:longitude withObservations:obs withOperatorZone:zone];
[stop setCicID:cicID];
[stop setOperatorID:operatorID];
NSLog(#"Saving stop with Name: %#, cicID: %#, operatorID: %#", name, cicID, operatorID);
[stops_dict setObject:stop forKey: [NSNumber numberWithInt:idStop]];
if (zone != nil) {
[zone addStopsObject:stop];
// [[zone managedObjectContext] MR_saveToPersistentStoreAndWait]; // TIRAR ISTO DAQUI E POR NO FIM DE TUDO
}
itemcount++;
progress = itemcount/totalItems;
updateaction(progress);
}
else if ([elementName isEqualToString:#"Line"]) {
// NSLog(#"Checking line..");
int sid = [[attributeDict objectForKey:#"Id"] intValue];
NSString * name = [attributeDict objectForKey:#"LineName"];
NSString * returnName = [attributeDict objectForKey:#"ReturnLineName"];
NSString * companyID = [attributeDict objectForKey:#"CompanyId"];
int isCircular = [[attributeDict objectForKey:#"IsCircular"] boolValue];
int idOperator = [[attributeDict objectForKey:#"IdOperator"] intValue];
NSString * version = [attributeDict objectForKey:#"Version"];
currentLine = [Database createLine:sid withName:name withReturnName:returnName isCircular:isCircular withOperatorID:idOperator withCompanyID:companyID withVersion:version];
latestLineOpID = idOperator;
[currentLine setCicID:cicID];
[currentLine setOperatorID:operatorID];
lineWithOwnStops = (idOperator == suboperatorid);
itemcount++;
progress = itemcount/totalItems;
updateaction(progress);
}
The XML File data is like this:
<Network CountiesNumber="0" ContactsNumber="2" LinesNumber="326" StopsNumber="3161" ZonesNumber="2866">
<Zones>
<OperatorZone Name="Cavadas (R 25 Abril, 60) Café O Renascer" Id="20274" />
</Zones>
<Stops>
<Stop Id="108591" Name="Setúbal (Avª 22 Dezembro, 25)" CodName="2" IdZona="22793" CoordXX="-8.89310700" CoordYY="38.52755000" Observation="" />
</Stops>
<Lines>
<Line ReturnLineName="Cacilhas - Cristo Rei" LineName="Cristo Rei - Cacilhas" IsCircular="false" CompanyId="101" IdOperator="84" Id="16344" Version="05-08-2019 00:00:00">
<StopLines>
<StopLine StopName="0" OrderPath_I="1" OrderPath_V="0" ZoneId="20435" Id="56356194" IdStop="109346" />
<StopLine StopName="0" OrderPath_I="2" OrderPath_V="0" ZoneId="20423" Id="56356195" IdStop="109838" />
</StopLines>
</Line>
</Lines>
</Network>
EDIT - Example Database Method:
+ (Stop *)createStop:(int)id withName:(NSString*)name withCodName:(NSString *)codName
withIdZona:(int)idZona
withIdDistrito:(int)idDistrito
withIdConcelho:(int)idConcelho
withIdFreguesia:(int)idFreguesia
withLatitude:(double)latitude
withLongitude:(double)longitude
withObservations:(NSString *)observations
withOperatorZone:(OperatorZone *)operator
{
Stop * stop = [Stop MR_createEntity];
stop.ownStop = false;
stop.name = name;
stop.codName = codName;
stop.idZona = [NSNumber numberWithInt:idZona];
stop.idDistrito = [NSNumber numberWithInt:idDistrito];
stop.idConcelho = [NSNumber numberWithInt:idConcelho];
stop.idFreguesia = [NSNumber numberWithInt:idFreguesia];
stop.id = [NSNumber numberWithInt:id];
stop.latitude = [NSNumber numberWithDouble:latitude];
stop.longitude = [NSNumber numberWithDouble:longitude];
if (idDistrito != 0){
NSLog(#"bla bla bla");
}
stop.distrito = [Distrito MR_findFirstByAttribute:#"id" withValue:[NSNumber numberWithInt:idDistrito]];
stop.concelho = [Concelho MR_findFirstByAttribute:#"id" withValue:[NSNumber numberWithInt:idConcelho]];
stop.freguesia = [Freguesia MR_findFirstByAttribute:#"id" withValue:[NSNumber numberWithInt:idFreguesia]];
stop.operatorzone = operator;
//ac [[stop managedObjectContext] MR_saveToPersistentStoreAndWait];
// NSLog(#"Stop %d - %# saved", [stop.id intValue], stop.name);
return stop;
}
Theres also an example of a parser that this most likely looks as mine:
https://gist.github.com/xslim/1020767
Difference is, that hes using NSEntityDescription insertNewObjectForEntityForName: and I'm using MR_createEntity
In general if you want your app to start as fast as it can - don't do the work it needs on startup. Either do it before (at build time) or postpone it and do in background as much as you can.
In this particular example I'd recommend to run that code during the app build to convert XML to sqlite/CoreData, and ship your pre-populated database as a resource in the app bundle.
Note: it is possible to make a macOS tool to do that, and then use that tool for your app target as a Build Phase.
If you want to speed up the updates from the server, try to do the processing in the background. Use profiling tools to see what is the slowest part. During bulk imports into SQL often one of the slowest part is related to updating indexes. Instead of updating an index for each new row, sometimes it is possible to disable them, insert everything, and then build indexes from scratch. That can be much faster, but that requires deeper knowledge of how CoreData uses indexes. Sometimes the slowest part is related to searching. Instead of using the database search one could build a dictionary in memory and use it for faster searching.
I finally found the solution. So I decided to go with a pre-loaded DB instead of filling the data of my database from a local xml file.
Basically what I done was:
After running the app with the local xml file, I let it create the BD then went to the ~Library/Developer/... to get the generated DB files, I had 3 files, but If I opened them with a program called DB Browser for SQLite the 3 files merged into 1 single file.
After that I grabbed the only 1 single DB File, and then moved it into the desired Target of my project. Lets say I called it MyDB.sqlite
And then in my AppDelegate file after didFinishLaunchingWithOptions: I added this code:
// This code is needed to preload the database
NSArray *paths = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
NSURL *documentPath = [paths lastObject];
NSURL *storeURL = [documentPath URLByAppendingPathComponent:#"MyDB.sqlite"];
// Check if the database already exists in the document folder, if it doesn't exist add it to the documents folder
if (![[NSFileManager defaultManager] fileExistsAtPath:[storeURL path]]) {
NSURL *preloadURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"MyDB" ofType:#"sqlite"]];
NSError* err = nil;
if (![[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&err]) {
NSLog(#"Error: Unable to copy preloaded database.");
}
}
// Setup Magical Record in the document path
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreAtURL:storeURL];
For some reason the setupCoreDataStackWithAutoMigratingSqliteStoreNamed wasen't working for me (Maybe because was getting the MyDB.sqlite files from the other targets? I don't know), so I decided to go with setupCoreDataStackWithAutoMigratingSqliteStoreAtURL

Persisting bookmark in core-data

I have an OSX application that is supposed to have a list of files from anywhere in the user's disk.
The first version of the app saves the path to these files in a core-data model.
However, if the file is moved or renamed, the tool loses its purpose and the app can crash.
So I decided to use bookmarks. It seems to be working, but every time I try to recover the data, I get the old path of the files. Why is that? What am I missing?
My core-data entity uses a binary data field to persist the bookmark.
The bookmark itself is done like this:
NSData * bookmark = [filePath bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark
includingResourceValuesForKeys:NULL
relativeToURL:NULL
error:NULL];
And on loading the application, I have a loop to iterate all the table and recover the bookmark like this:
while (object = [rowEnumerator nextObject]) {
NSError * error = noErr;
NSURL * bookmark = [NSURL URLByResolvingBookmarkData:[object fileBookmark]
options:NSURLBookmarkResolutionWithoutUI
relativeToURL:NULL
bookmarkDataIsStale:NO
error:&error];
if (error != noErr)
DDLogCError(#"%#", [error description]);
DDLogCInfo(#"File Path: %#", [bookmark fileReferenceURL]);
}
If I rename the file, the path is null. I see no difference between storing this NSData object and a string with the path. So I am obviously missing something.
Edit:
I also often get an error like this: CFURLSetTemporaryResourcePropertyForKey failed because it was passed this URL which has no scheme.
I appreciate any help, thanks!
I can't find any issues in my code, so I changed it.
After looking for the reason of the "no scheme" message, I came to the conclusion some third-party application is required for this code to work, and that's undesirable.
I am now using aliases. This is how I create them:
FSRef fsFile, fsOriginal;
AliasHandle aliasHandle;
NSString * fileOriginalPath = [[filePath absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
OSStatus status = FSPathMakeRef((unsigned char*)[fileOriginalPath cStringUsingEncoding: NSUTF8StringEncoding], &fsOriginal, NULL);
status = FSPathMakeRef((unsigned char*)[fileOriginalPath cStringUsingEncoding: NSUTF8StringEncoding], &fsFile, NULL);
OSErr err = FSNewAlias(&fsOriginal, &fsFile, &aliasHandle);
NSData * aliasData = [NSData dataWithBytes: *aliasHandle length: GetAliasSize(aliasHandle)];
And now I recover the path like this:
while (object = [rowEnumerator nextObject]) {
NSData * aliasData = [object fileBookmark];
NSUInteger aliasLen = [aliasData length];
if (aliasLen > 0) {
FSRef fsFile, fsOriginal;
AliasHandle aliasHandle;
OSErr err = PtrToHand([aliasData bytes], (Handle*)&aliasHandle, aliasLen);
Boolean changed;
err = FSResolveAlias(&fsOriginal, aliasHandle, &fsFile, &changed);
if (err == noErr) {
char pathC[2*1024];
OSStatus status = FSRefMakePath(&fsFile, (UInt8*) &pathC, sizeof(pathC));
NSAssert(status == 0, #"FSRefMakePath failed");
NSLog(#"%#", [NSString stringWithCString: pathC encoding: NSUTF8StringEncoding]);
} else {
NSLog(#"The file disappeared!");
}
} else {
NSLog(#"CardCollectionUserDefault was zero length");
}
}
However, I am still curious on why my previous code failed. I appreciate any thoughts on that. Thanks!

Is it possible to glob a directory in iOS and have the results cached by the OS?

Many methods of reading from a filesystem using NSFileManager and lower level APIs on iOS involve built-in caching, so reading from directories that haven't changed can be quite fast, even if there's lots of items in the directories.
I have a situation where I want to be able to enumerate files in a directory using a glob:
i.e. the folder has files named like this:
1-1-0.png
1-2-0.png
1-3-0.png
1-3-1.png
2-2-1.png
5-1-1.png
5-1-2.png
5-2-1.png
5-3-0.png
6-1-1.png
...
1501-5-2.png
I might want to get all filenames matching 5-*-1.png, which would give me back 5-1-1.png and 5-2-1.png.
Loading the whole directory listing then doing the globbing in RAM is pretty straightforward, but is there a method for doing this at an OS level that would have caching built-in, so repeated calls to the same glob would give cached (faster) results?
You can use the glob() function as outlined in this gist: https://gist.github.com/bkyle/293959
#include <glob.h>
+ (NSArray*) arrayWithFilesMatchingPattern: (NSString*) pattern inDirectory: (NSString*) directory {
NSMutableArray* files = [NSMutableArray array];
glob_t gt;
NSString* globPathComponent = [NSString stringWithFormat: #"/%#", pattern];
NSString* expandedDirectory = [directory stringByExpandingTildeInPath];
const char* fullPattern = [[expandedDirectory stringByAppendingPathComponent: globPathComponent] UTF8String];
if (glob(fullPattern, 0, NULL, &gt) == 0) {
int i;
for (i=0; i<gt.gl_matchc; i++) {
size_t len = strlen(gt.gl_pathv[i]);
NSString* filename = [[NSFileManager defaultManager] stringWithFileSystemRepresentation: gt.gl_pathv[i] length: len];
[files addObject: filename];
}
}
globfree(&gt);
return [NSArray arrayWithArray: files];
}

Read from iPhoto Library programmatically

I want to create an Application that connects to the iPhoto Library. So now I would like to read the Events and the pictures themselves from the library.
Is there an elegant / easy way to do this or do I have to manually read the Bundle Structure of the iPhoto User Data?
So far I have only found a picture taker: Is there a UIImagePicker for the Mac Desktop
Update: I found another relevant SO post: Selecting iPhoto images within a cocoa application
You can do it with NSAppleScript. This is some copy/paste from my app, hacked up a bit just to show the idea.
NSAppleEventDescriptor d = .. compile this script ..
#"tell application \"iPhoto\" to properties of albums"
for (int i = 0; i < [d numberOfItems]; i++)
{
NSAppleEventDescriptor *albumDesc = [d descriptorAtIndex:i];
// <NSAppleEventDescriptor: 'ipal'{
// 'ID ':4.265e+09,
// 'purl':'utxt'("http://www.flickr.com/photos/..."),
// 'pnam':'utxt'("Vacation"),
// 'alTy':'pubs',
// 'alCh':[ ],
// 'alPx':'msng' }>
NSString *albumName = [[albumDesc descriptorForKeyword:'pnam'] stringValue];
NSString *albumId = [[albumDesc descriptorForKeyword:'ID '] stringValue];
You can do the same thing to find the images
NSString *scp =
[NSString stringWithFormat:#"tell application \"iPhoto\" to properties of photos of album id %#",
[album objectForKey:#"id"]];
NSAppleEventDescriptor *d = ... compile scp ...
// 1 based!?
for (int i = 1; i <= [d numberOfItems]; i++)
{
NSAppleEventDescriptor *photoDesc = [d descriptorAtIndex:i];
// Yes.. this happens. Not sure why?!
if (!photoDesc)
continue;
// <NSAppleEventDescriptor: 'ipmr'{
// 'pnam':'utxt'("IMG_0058.JPG"),
// 'pwid':768,
// 'pdim':[ 768, 1024 ],
// 'alti':1.79769e+308,
// 'filn':'utxt'("3133889525_10975ba071_b.jpg"),
// 'ipth':'utxt'("/Users/lagnat/Pictures/iPhoto Library/Masters/2010/11/10/20101110-002341/3133889525_10975ba071_b.jpg"),
// 'idat':'ldt '($F57C69C500000000$),
// 'rate':0,
// 'titl':'utxt'("IMG_0058.JPG"),
// 'phit':1024,
// 'itpt':'utxt'("/Users/lagnat/Pictures/iPhoto Library/Thumbnails/2010/11/10/20101110-002341/3133889525_10975ba071_b.jpg.jpg"),
// 'ID ':4.295e+09,
// 'lati':'msng',
// 'pcom':'utxt'(""),
// 'opth':'utxt'("/Users/lagnat/Pictures/iPhoto Library/Masters/2010/11/10/20101110-002341/3133889525_10975ba071_b.jpg"),
// 'lngt':'msng',
// 'tiln':'utxt'("3133889525_10975ba071_b.jpg.jpg") }>
NSString *path = [[photoDesc descriptorForKeyword:'ipth'] stringValue];
NSString *imgname = [[photoDesc descriptorForKeyword:'pnam'] stringValue];
If releasing apps on the App Store you are now required now required to use the Sandbox, this stops the previous AppleScript method from working (the iPhoto app launches but an empty set is returned).
iPhoto libraries consist of a directory structure containing photos, databases and XML files. The contents changes with each version of iPhoto so be careful if manually accessing these files.
If you just want the album details you can parse the file AlbumData.xml
If you would like photos you can browse the Masters folder. The files structure follows date rather than by the sets configured in iPhoto.
More information can be found on the internals of the iPhoto library here:
http://www.fatcatsoftware.com/iplm/Help/iphoto%20library%20internals.html
The majority of the databases are in SQLite format and so can be programmatically accessed through Objective C, though again you can expect schema changes between different versions of iPhoto. The main databases of interest are Library.apdb and Properties.apdb in Database/apdb.
If you still want to use the Apple Script method, here's a version of the previous answer with the Apple script execution part included:
NSAppleScript *script = [[NSAppleScript alloc] initWithSource:#"tell application \"iPhoto\" to properties of albums"];
NSAppleEventDescriptor *d = [script executeAndReturnError:nil];
NSLog(#"photo library count: %ld", (long)[d numberOfItems]);
for (int i = 0; i < [d numberOfItems]; i++)
{
NSAppleEventDescriptor *albumDesc = [d descriptorAtIndex:i];
NSString *albumName = [[albumDesc descriptorForKeyword:'pnam'] stringValue];
NSLog(#"%#", albumName);
}

How do I create a temporary file with Cocoa?

Years ago when I was working with C# I could easily create a temporary file and get its name with this function:
Path.GetTempFileName();
This function would create a file with a unique name in the temporary directory and return the full path to that file.
In the Cocoa API's, the closest thing I can find is:
NSTemporaryDirectory
Am I missing something obvious or is there no built in way to do this?
A safe way is to use mkstemp(3).
[Note: This applies to the iPhone SDK, not the Mac OS SDK]
From what I can tell, these functions aren't present in the SDK (the unistd.h file is drastically pared down when compared to the standard Mac OS X 10.5 file). I would use something along the lines of:
[NSTemporaryDirectory() stringByAppendingPathComponent: [NSString stringWithFormat: #"%.0f.%#", [NSDate timeIntervalSinceReferenceDate] * 1000.0, #"txt"]];
Not the prettiest, but functional
Apple has provided an excellent way for accessing temp directory and creating unique names for the temp files.
- (NSString *)pathForTemporaryFileWithPrefix:(NSString *)prefix
{
NSString * result;
CFUUIDRef uuid;
CFStringRef uuidStr;
uuid = CFUUIDCreate(NULL);
assert(uuid != NULL);
uuidStr = CFUUIDCreateString(NULL, uuid);
assert(uuidStr != NULL);
result = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:#"%#-%#", prefix, uuidStr]];
assert(result != nil);
CFRelease(uuidStr);
CFRelease(uuid);
return result;
}
LINK :::: http://developer.apple.com/library/ios/#samplecode/SimpleURLConnections/Introduction/Intro.html#//apple_ref/doc/uid/DTS40009245
see file :::AppDelegate.m
I created a pure Cocoa solution by way of a category on NSFileManager that uses a combination of NSTemporary() and a globally unique ID.
Here the header file:
#interface NSFileManager (TemporaryDirectory)
-(NSString *) createTemporaryDirectory;
#end
And the implementation file:
#implementation NSFileManager (TemporaryDirectory)
-(NSString *) createTemporaryDirectory {
// Create a unique directory in the system temporary directory
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:guid];
if (![self createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:nil]) {
return nil;
}
return path;
}
#end
This creates a temporary directory but could be easily adapted to use createFileAtPath:contents:attributes: instead of createDirectoryAtPath: to create a file instead.
If targeting iOS 6.0 or Mac OS X 10.8 or higher:
NSString *tempFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
Swift 5 and Swift 4.2
import Foundation
func pathForTemporaryFile(with prefix: String) -> URL {
let uuid = UUID().uuidString
let pathComponent = "\(prefix)-\(uuid)"
var tempPath = URL(fileURLWithPath: NSTemporaryDirectory())
tempPath.appendPathComponent(pathComponent)
return tempPath
}
let url = pathForTemporaryFile(with: "blah")
print(url)
// file:///var/folders/42/fg3l5j123z6668cgt81dhks80000gn/T/johndoe.KillerApp/blah-E1DCE512-AC4B-4EAB-8838-547C0502E264
Or alternatively Ssswift's oneliner:
let prefix = "blah"
let url2 = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(prefix)-\(UUID())")
print(url2)
You could use mktemp to get a temp filename.
The modern way to do this is FileManager's url(for:in:appropriateFor:create:).
With this method, you can specify a SearchPathDirectory to say exactly what kind of temporary directory you want. For example, a .cachesDirectory will persist between runs (as possible) and be saved in the user's library, while a .itemReplacementDirectory will be on the same volume as the target file.
Don't use legacy APIs like NSTemporaryDirectory, get a proper URL instead from FileManager.
let tmpURL = FileManager
.default
.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
You'd still have to create the directory.
You could use an NSTask to uuidgen to get a unique file name, then append that to a string from NSTemporaryDirectory(). This won't work on Cocoa Touch. It is a bit long-winded though.
Adding to #Philipp:
- (NSString *)createTemporaryFile:(NSData *)contents {
// Create a unique file in the system temporary directory
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:guid];
if(![self createFileAtPath:path contents:contents attributes:nil]) {
return nil;
}
return path;
}