Understanding NSURL, in the context of retrieving a persistent store document - cocoa-touch

Learning to use coreData. Currently looking at Stanford's CS193P Lecture 14, which is very helpful. I have successfully set up a working app with core data persistence, using a ManagedDocument.
This code below is run every time the app starts. My confusion is: how do we know that the url for the document is correct? How does it know that "lastObject" will always be the URL for the saved document?
if (!myManagedDocument) {
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"theDatabase"];
myManagedDocument = [[UIManagedDocument alloc]initWithFileURL:url];
}
This code below will open the document, or create/save it if it has not already been saved previously.
if (![[NSFileManager defaultManager] fileExistsAtPath:[myManagedDocument.fileURL path]]) {
[myManagedDocument saveToURL:myManagedDocument.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL sucess) {
[self getInfoFromDatabase];
}];
} else if (myManagedDocument.documentState == UIDocumentStateClosed) {
[myManagedDocument openWithCompletionHandler:^(BOOL sucess) {
[self getInfoFromDatabase];
}];
} else if (myManagedDocument.documentState == UIDocumentStateNormal) {
[self getInfoFromDatabase];
}

Depending on the directory and domainMask argument, URLsForDirectory can return
an array of several URLs. For example, on OS X,
NSArray *urls = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSAllDomainsMask];
returns
(
file://localhost/Users/<user>/Library/Application%20Support/,
file://localhost/Library/Application%20Support/,
file://localhost/Network/Library/Application%20Support/
)
But in your case, on iOS,
NSArray *urls = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
returns an array of exactly one URL, which is the document directory inside the
application sandbox. On the simulator, this would look like
(
file://localhost/Users/&ltuser>/Library/Application%20Support/iPhone%20Simulator/5.0/Applications/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/Documents/
)
So it does not matter if you take the first or last object of that array.
The code just assumes that the managed document is saved in the document directory
of the application sandbox.

Related

Unable to rename the file while moving from temporary directorary

I am developing a zip extractor app i followed the algorithm that CRD explained #Here but i stuck at third step i am unable to rename the unzipped file which is at temporary directorary.
here is my code
NSURL *tempDir = [NSURL fileURLWithPath:destinationPath];
NSError *error;
NSURL *tmpDirectory = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tempDir create:YES error:&error];
if (error) {
return ;
}
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
NSLog(#"temp dir %#",tmpDirectory);
NSLog(#"temp path %#",tmpDirectory.path);
[SSZipArchive unzipFileAtPath:zipFilePath toDestination:tmpDirectory.path];
NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory.path error:nil];
NSLog(#"dir file %#",dirFiles);
for (NSString *string in dirFiles) {
NSArray *dirDestinationFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:destinationPath error:nil];
NSLog(#"dir destination file %#",dirDestinationFiles);
[dirDestinationFiles enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error;
if ([string isEqualToString:obj]) {
NSLog(#"Already present");
BOOL isMoved = [fm moveItemAtPath:tmpDirectory.path toPath:[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#-1",string]] error:&error];
if (isMoved) {
NSLog(#"Moved");
}else{
NSLog(#"errorL %#", error);
NSLog(#"Not moved");
}
[fm removeItemAtPath:tmpDirectory.path error:&error];
[self moveFileToTrash:zipFilePath];
[self openExtractedFolderWithZipPath:zipFilePath toDestinationPath:destinationPath];
}
}];
}
Any Suggestions..
Thanks in Advance !
Let's just review your code to hopefully help you on your way.
It may seem minor, but pick good variable names:
NSURL *tempDir = [NSURL fileURLWithPath:destinationPath];
NSURL *tmpDirectory = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tempDir create:YES error:&error];
Two names which are semantically similar for different things, that is just confusing. How about, say, destinationURL instead of tempDir?
Next, when constructing/pulling apart/etc. pathnames or URLs you will be better off being consistent. Both NSURL and NSString provide similar methods for these operations, in one place you use them:
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
but then restort to direct string manipulation using a path separator which may, or may not, be correct:
[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#-1",string]]
The routines provided by NSURL and NSString abstract away from the details of path separators and how to, say, find the extension on the last path component (which you might find useful when renaming to avoid clashes).
Going back to:
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
There is no reason for you to do this. The temporary directory is created for you and you should delete it after using it. So there is no need to create a subdirectory extracts within it, and by reassigning to the same variable you've lost the URL you need to delete the temporary directory.
Now something less obvious, in my comment above I wrote:
To move each item you must handle name clashes, to do this try the move and if you get an error indicating a name clash modify the destination name however you like and re-try the move, repeating until you succeed or you until reach some limit of tries (determined by you).
I didn't explain why you should do it this way and you have tackled the problem a different way: for each item you are going to move you check for names clashes before attempting the move by iterating over the names in the destination directory.
If you read Apple's documentation on the file system you will find they often recommend you try an operation and then examine any error returned instead of trying to predict whether an error will occur and avoid it. The reason for this is the file system is dynamic, other processes can be modifying it, so if you try to avoid an error you may still get one. In pseudocode you are better of doing something like:
moveDone = false
attemptCount = 0
while not moveDone and attemptCount < MAX_ATTEMPTS
move object
if object exists error
modify destination URL
increment attemptCount
else
moveDone = true
end
end
if not moveDone then handle error
Following this outline and using a simple count and the NSString/NSURL path routines will produce you a much simpler and more reliable solution than the one you have now posted as an answer.
HTH
Here is the code working for me.
NSURL *tempDir = [NSURL fileURLWithPath:destinationPath];
NSError *error;
NSURL *tmpDirectory = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tempDir create:YES error:&error];
if (error) {
return ;
}
tmpDirectory = [tmpDirectory URLByAppendingPathComponent:#"extracts"];
NSLog(#"temp dir %#",tmpDirectory);
NSLog(#"temp path %#",tmpDirectory.path);
[SSZipArchive unzipFileAtPath:zipFilePath toDestination:tmpDirectory.path];
NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory.path error:nil];
NSLog(#"dir file %#",dirFiles);
for (NSString *string in dirFiles) {
NSArray *dirDestinationFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:destinationPath error:nil];
NSLog(#"dir destination file %#",dirDestinationFiles);
NSMutableArray *folderCount = [[NSMutableArray alloc] init];
NSMutableArray *folderNumCount = [[NSMutableArray alloc] init];
[dirDestinationFiles enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj containsString:string]){
[folderNumCount addObject:obj];
}
if ([string isEqualToString:obj]) {
[folderCount addObject:string];
}
}];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error;
if (folderCount.count == 0) {
NSLog(#"First time extract");
BOOL isMoved = [fm moveItemAtPath:tmpDirectory.path toPath:[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#",string]] error:&error];
if (isMoved) {
NSLog(#"Moved");
}else{
NSLog(#"errorL %#", error);
NSLog(#"Not moved");
}
[fm removeItemAtPath:tmpDirectory.path error:&error];
// [self moveFileToTrash:zipFilePath];
// [self openExtractedFolderWithZipPath:zipFilePath toDestinationPath:destinationPath];
}else if (folderCount.count > 0){
NSLog(#"Already present");
BOOL isMoved = [fm moveItemAtPath:tmpDirectory.path toPath:[destinationPath stringByAppendingString:[NSString stringWithFormat:#"/%#-%lu",string,folderNumCount.count-1]] error:&error];
if (isMoved) {
NSLog(#"Moved");
}else{
NSLog(#"errorL %#", error);
NSLog(#"Not moved");
}
[fm removeItemAtPath:tmpDirectory.path error:&error];
// [self moveFileToTrash:zipFilePath];
// [self openExtractedFolderWithZipPath:zipFilePath toDestinationPath:destinationPath];
}
}

Objective C can't read text file when running on iPhone

I've made an app that is relying on reading and writing a plist-file. This works well when I'm running the app in the iPhone simulator, but doesn't work at all when I'm testing it on my iPhone. I've also made a pre made text file in .txt format with demo data. The app works when I'm running this file.
All the reading and writing is done in a class that looks like this:
-(void)saveArray:(NSMutableArray *)inputArray
{
albumArray = inputArray;
NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFolder = [path objectAtIndex:0];
NSString *filePath = [documentFolder stringByAppendingFormat:#"albums.plist"];
[albumArray writeToFile:filePath atomically:YES];
}
Update: Changed the string from "stringByAppendingFormat" to "stringByAppendingPathComponent" and it seems to work now. Thanks a lot! You guys made my day made.
Are you sure, that the folders already exist?
Here is a function i'm using to get the path to my file:
- (NSString*) pathToSavedAlbums
{
NSURL *applicationSupportURL = [self applicationDataDirectory];
if (! [[NSFileManager defaultManager] fileExistsAtPath:[applicationSupportURL path]])
{
NSError *error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:[applicationSupportURL path]
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (error)
{
NSLog(#"error creating app support dir: %#", error);
}
}
NSString *path = [[applicationSupportURL path] stringByAppendingPathComponent:#"albums.plist"];
return path;
}
Check the spelling of the plist name as well as the case, device is case sensitive for docs but simulator isn't. Also try deleting the app from the device and reinstalling it ?
writeToFile:atomically: returns a bool, so check that to see if it fails to even write to the path. Check the file path string and ensure this is where you want it to go.

Writing to /tmp folder iPad

I´m writing certain values to a file. See Write Operations below.
This works fine when using iPad 6.1 Simulator.
When trying the same thing on my iPad it fails. I think it´s something with sandboxing. I haven´t found out yet which path is best on iOS Devices to write stuff for internal use.
Any ideas?
#pragma mark Write Operations to Tmp Folder
- (BOOL) psWriteFileWithName: (NSString*) fileName
withString:(NSString*) string {
NSString *fileName = #"artistNumber";
NSError * error = NULL;
NSString *filePath = [NSString stringWithFormat:#"/tmp/%#.txt",fileName];
[string writeToFile:filePath
atomically:YES
encoding: NSUTF8StringEncoding
error:&error];
return YES;
}
You cannot write to /tmp since this is outside of your app sandbox.
However your app also has a temp directory, which can be referenced with the NSTemporaryDirectory() function:
Which works like:
NSString *tempfilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
Here is you method with the correct NSTemporaryDirectory() implementation, also edit some error handling:
#pragma mark Write Operations to Tmp Folder
- (BOOL) psWriteFileWithName: (NSString*) fileName
withString:(NSString*) string {
NSString *fileName = #"artistNumber";
NSError *error = nil;
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
if (![string writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error] ) {
NSLog(#"Error writing file: %#", error);
return NO;
}
return YES;
}

Using an existing SQLite database in MagicalRecord

I've created a SQLite database that contains records from some JSON using this tutorial, and I want to use MagicalRecord to query it.
MagicalRecord sees the NSManagedObject (BlogPost) and can create records, but it doesn't see the prepopulated records, so I'm guessing it's not seeing the SQLite file I've added. I've verified that the SQLite database does indeed contain rows using a SQLite client.
In application:didFinishLaunchingWithOptions:, I've put:
[MagicalRecord setupCoreDataStackWithStoreNamed:#"DBG.sqlite"];
And in applicationWillTerminate::
[MagicalRecord cleanUp];
When I call [BlogPost MR_findAll] in a controller, it returns an empty set. DBG.sqlite is at the root of the project directory, and I've tried putting it in "Copy Bundle Resources", but blogPosts still returns an empty set.
Any ideas?
The issue ended up being that the preloaded SQLite db needs to be copied to the default path of the application's db:
NSArray *paths = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
NSURL *documentPath = [paths lastObject];
NSURL *storeURL = [documentPath URLByAppendingPathComponent:#"DBG.sqlite"];
if (![[NSFileManager defaultManager] fileExistsAtPath:[storeURL path]]) {
NSURL *preloadURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"DBG" ofType:#"sqlite"]];
NSError* err = nil;
if (![[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&err]) {
NSLog(#"Error: Unable to copy preloaded database.");
}
}
This should be placed just before [MagicalRecord setupCoreDataStackWithStoreNamed:#"DBG.sqlite"];.

Last Opened Documents using Cocoa

Is there a way to get the last opened files of an application? I know with LSSharedFileListCreate you can get an array of the global recent documents of the user but I was wondering if it was possible to get the recent documents for a specific application, such as Xcode, using cocoa or objective-c.
I don't think there is a public API for getting recent documents of apps other than your own. The recent files are saved as plists in ~/Library/Preferences though, so you could just read those directly.
Note however that the format of these plists might change with future OS upgrades, so you should ideally not rely on this.
Here's an example for reading the recent documents of Xcode:
NSString *bundleID = #"com.apple.dt.Xcode";
NSString *prefPath = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:#"Preferences"];
NSString *recentFilesFileName = [bundleID stringByAppendingPathExtension:#"LSSharedFileList.plist"];
NSString *recentFilesPath = [prefPath stringByAppendingPathComponent:recentFilesFileName];
NSArray *recentDocumentItems = [[[NSDictionary dictionaryWithContentsOfFile:recentFilesPath] objectForKey:#"RecentDocuments"] objectForKey:#"CustomListItems"];
for (NSDictionary *recentDocumentItem in recentDocumentItems) {
NSString *name = [recentDocumentItem objectForKey:#"Name"];
NSData *bookmarkData = [recentDocumentItem objectForKey:#"Bookmark"];
NSError *error = nil;
NSURL *bookmarkURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithoutMounting | NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:NULL error:&error];
if (bookmarkURL) {
NSLog(#"File name: %#", name);
NSLog(#"URL: %#", bookmarkURL);
} else {
NSLog(#"Could not resolve URL for file %#: %#", name, error);
}
}
This won't work in a sandboxed app of course.
You can use -[NSDocumentController recentDocumentURLs], which retrieves an array of URLs of the most recently opened documents:
NSArray *array = [[NSDocumentController sharedDocumentController] recentDocumentURLs];