ios 8: Bundle path changes - ios

I have an iOS app that stores the absolute path of files in a database and in generated html documents. I just recently updated my iPhone to iOS 8 and now when ever I run the app it seems that the app is installed in a different directory every re-compile. For example on the first build/run [[NSBundle mainBundle] bundlePath] returns something different on the next build/run. What is going on? Is this a new feature of Apple?
Update: A bug report was created
Code example:
If I run the following line over multiple build/runs then I will get a different result each time.
#define kOLD_PATH #"oldPath"
NSString* newPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
NSString* oldPath = [[NSUserDefaults standardUserDefaults] objectForKey:kOLD_PATH];
NSLog(#"New Path: %#", newPath);
NSLog(#"Old Path: %#", oldPath);
NSLog(#"Result: %#", [oldPath isEqualToString:newPath] ? #"Same" : #"Changed");
[[NSUserDefaults standardUserDefaults] setObject:newPath forKey:kOLD_PATH];
[[NSUserDefaults standardUserDefaults] synchronize];
The output looks like this over multiple runs
New Path: /var/mobile/Containers/Data/Application/4FFCE2CB-580D-409A-90CB-EF2B8A1FB653/Library
Old Path: /var/mobile/Containers/Data/Application/B038B2DA-F85D-4E18-A5F1-8635834EC454/Library
Result: Changed
Full Disclosure: In my app the user imports a web page (ePub) that has resources. The resources are stored with the web page. The web page also accesses resources that are part of the app bundle. To achieve this when I load the web page the base url is set to the directory the web page is in and the bundle resources are accessed via absolute file paths. Now that file paths change on every update this is broken. I tried creating symbolic links to the bundle resources but that also fails un subsequent updates.

In iOS 8, The file system layout of app containers has changed. Applications and their content are no longer stored in one root directory.
From the iOS 8 Release Notes:
The file system layout of app containers has changed on disk. Rather
than relying on hard-coded directory structure, use the
NSSearchPathForDirectoriesInDomains function or the
URLForDirectory:inDomain:appropriateForURL:create:error: method of the
NSFileManager class. See Accessing Files and Directories in File
System Programming Guide.
This is not a bug. Make sure you use the recommended APIs (from the above quote) and you won't have a problem.
So, If you are trying to access a bundled resource you added to the project, you would use:
[[NSBundle mainBundle] pathForResource:#"resourceName" ofType:#"extension"];
But if you want to use something that you put in the documents directory, you would use:
[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:#"resourceName.extension"];

Refer Technical Note 2406 by Apple
The breaking change is
Beginning in iOS 8, the Documents and Library directories are no
longer siblings of your application's bundle.
Don't store full path/URL to your documents. Store the file name and always generate full path/URL with recommended approach.
Get the DocumentsDirectory URL
// Returns the URL to the application's Documents directory.
- (NSURL *)applicationDocumentsDirectory {
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
Then you get path out of url and append the file name to generate full path.

don't know if you solved your problem, but this link is possible the answer.
https://developer.apple.com/library/prerelease/ios/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/AccessingFilesandDirectories/AccessingFilesandDirectories.html#//apple_ref/doc/uid/TP40010672-CH3-SW10
Locating Files Using Bookmarks
A few lines before this section in the page is this text:
"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."
Good bye.

I think the different path for each build and run is the intended way of things happening in iOS simulator. It is not an issue.
/var/mobile/Containers/Data/Application/4FFCE2CB-580D-409A-90CB-EF2B8A1FB653/Library
/var/mobile/Containers/Data/Application/B038B2DA-F85D-4E18-A5F1-8635834EC454/Library
I found even if you use the recommended way
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];
}
The results are same. Different path for each build & run.

Related

How to store file with local path in iOS

I am a React native developer and I was trying to integrate a native ios code.
One of the instance takes NSURL * which should be a local path I guess
https://github.com/uber/startup-reason-reporter/blob/master/StartupReasonReporter/StartupReasonReporterPriorRunInfo/UBApplicationStartupReasonReporterPriorRunInfo.h#L21
+ (nonnull instancetype)priorRunAtDirectoryURL:(nullable NSURL *)directoryURL;
I am not sure what does localPath Url looks like in IOS, like what should I pass? for example?
Ps: intentionally including swift tag as well because I think swift developers could also answer it.
Based on description of that function:
Returns the prior run information stored to disk at the given directory URL.
#param directoryURL The directory to use to to store the startup reason data.
#return the previous startup reason data if it was present on disk, or empty startup reason object.
*/
you need to provide a directory they can write into. So Apps's document directory would be the best (as a root) + whatever folder you want (which, based on their code, they will even create for you).
So:
NSURL* docs = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSURL* myCrashes = [docs URLByAppendingPathComponent:#"myCrashes" isDirectory:TRUE];
it will look something like:
file:///some/path/to/app/sandbox/data/Documents/myCrashes
Try looking up documentation for
- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory
inDomains:(NSSearchPathDomainMask)domainMask;`
That will get you the URL and check out FileManager.SearchPathDirectory enum for all the viable options:
Here's an example for getting the caches directory
NSURL* url = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject];`
It could be any one of the 25-26 options in SearchPathDirectory depending on where they put that stuff.

Large files downloaded to Documents and backed up to iCloud

I have an iOS app in the app store that can download relatively large files that need to stay on the device for offline use. Those files are currently stored in the app's Documents folder but I'm just now reading that the Documents folder is backed up and should really only be used for user-generated content. This Apple technical Q&A states that the NSURLIsExcludedFromBackupKey should be set to prevent backup. This states that an app's /Library/Caches is the right place to put these kinds of files although further reading suggests that the folder may be cleared when the device is low on storage which is unacceptable for this app. I believe /Library/Application Support/ is then the best location for them -- does this sound right?
Unfortunately, this mistake got through the app review process. What are some best practices for fixing this now that people are using the app and already have some files persisted to the Documents folder and to their backups? It seems I need to move all the existing files and set their NSURLIsExcludedFromBackupKey on app update. How do I guarantee that this is done exactly once and that it isn't interrupted? Is moving the files out of the Documents folder important or could I leave them there? Will changing the files' backup status remove them from existing backups?
I'm using Swift 2.1.1 and targeting iOS 8.0+.
As stated in the technical Q&A, you best bet could be create a subdirectory in the Documents, and exclude that subdirectory once.
I don't believe you can write a 'do it once and be sure it is done' routine, since you can't guarantee your app doesn't crash while it is running. You certainly could set a completion flag when you are sure it is done so that once it is done you don't have to run it again.
Exclude your directory from backup, not the individual files.
From Xcode:
You can use this property to exclude cache and other app support files which are not needed in a backup. Some operations commonly made to user documents cause this property to be reset to false; consequently, do not use this property on user documents.
Here is the strategy I have used with good results
(sorry, its in objective-c -- I'm not a swift guy. Hopefully it will give you the idea):
- (BOOL)moveAllFiles{
// Searches through the documents directory for all files ending in .data
BOOL success = true;
NSString *myNewLocation = #"/store/my/files/here/now";
// Get the documents directory
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
// Get all files ending in .data (whatever your file type is)
NSArray *dataFilesArray = [NSArray arrayWithArray:[NSBundle pathsForResourcesOfType:#"data" inDirectory:documentDirectory]];
// If you have multiple resource types, use this line once each for each resource type, then append the arrays.
// Iterate the found files
NSString *fileName = [NSString string];
for (int i=0; i<[dataFilesArray count]; i++) {
fileName = [[dataFilesArray objectAtIndex:i] lastPathComponent];
// Move file, set success to false if unsuccessful move
if (![[NSFileManager defaultManager] moveItemAtPath:[dataFilesArray objectAtIndex:i]
toPath:[myNewLocation stringByAppendingPathComponent:fileName]
error:nil]) {
success = false; // Something went wrong
}
}
return success;
}
Now use the value of success to set a key in the user defaults file. Check for that key on startup. If it is false (or absent), run this routine (again).
This example is with file paths. You can do the same thing with file URLs if you wish.

Obtain the path of app Group from FileManager

I am trying to update my app for WatchKit and I save a NSKeyedArchiver file to the NSDocumentsDirectory normally.
With updating to app groups I need to store it in the app groups folder. The issue I am having is I cant figure out how to just get the path, and not have it referenced as a file I am looking for.
The way it is set up now is to find the file it gives the path as a NSString
/Users/ME/Library/Developer/CoreSimulator/Devices/43F/data/Containers/Data/Application/5E/Documents/fav
but when I store to app groups, no matter which way I access the folder, it is returned
file:///Users/ME/Library/Developer/CoreSimulator/Devices/43F/data/Containers/Data/Application/5E/Documents/fav
What is the best way to just obtain the path to the shared group, rather than have the app looking for the direct file?
So coffee deprived me had forgotten about the .path for filemanager.
NSURL *fileManagerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com"];
NSString *tmpPath = [NSString stringWithFormat:#"%#", fileManagerURL.path];
NSString *finalPath = [NSString stringWithFormat:#"%#",[string stringByAppendingString:#"/Favourites2"]];
I was running into the same problem. I was going through the whole process of building a string to my save location and now I'm switching over to app groups and using the
NSURL *fileManagerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupID];
Well, the problem is, now instead of a string to the location that starts with "/Users/yourname/Library..." you get "file:///Users/yourname/Library..."
Here's what I did. I created the NSURL. Then I called absoluteString on it.
NSURL groupPath = [[fileManager containerURLForSecurityApplicationGroupIdentifier:groupID] absoluteString];
I now have a string that I need to strip off the first 7 characters of, then my old code works just fine, except now instead of being in the Documents directory, it's in a shared app group that can be accessed by both my old code and my new watchkit extension.
Here's the code to strip off the first 7 characters (index 6 since you start with 0), you should be able to use either method...
NSString *newGroupPath = [groupPath substringFromIndex:6];
or
NSString *newGroupPath = [groupPath substringWithRange:NSMakeRange(6, [str length]-6)];
This just removes the "file://" from the absoluteString that was made from the NSURL and gives you back your older string path the starts "/Users/YourName/Library/Developer/yada yada yada"
Hope that helps you, I have spent 4 hours figuring it out.
It seems to work for me on the simulator, I haven't tried it on the Watch yet. But at least my app is now working the way it was before, just saving the data in a shared app group. (I have a singleton that manages all of my data throughout my app, and I want that same singleton to provide data to my watch app).

Xcode 6 iPhone Simulator Application Support location

In Xcode 6, I have an app I'm using Core Data in, but there is no folder in Application Support for the iOS 8 iPhone Simulator. Where are my files and Core Data sqlite database being stored?
The simulator directory has been moved with Xcode 6 beta to...
~/Library/Developer/CoreSimulator
Browsing the directory to your app's Documents folder is a bit more arduous, e.g.,
~/Library/Developer/CoreSimulator/Devices/4D2D127A-7103-41B2-872B-2DB891B978A2/data/Containers/Data/Application/0323215C-2B91-47F7-BE81-EB24B4DA7339/Documents/MyApp.sqlite
I would suggest that you use SimPholders to find your Simulator files. It is a menu bar item that tracks your simulator apps and lets you go directly to their folders and content. It's awesome.
I found SimulatorManager application very useful. It takes you directly to the application folder of installed simulators. I have tried with 7.1, 8.0 and 8.1 simulators.
SimulatorManager resides as an icon in the system tray and provides an option to "Launch At Login".
Note: This works only with Xcode 6 (6.1.1 in my case) and above.
Hope that helps!
To know where your .sqlite file is stored in your AppDelegate.m add the following code
- (NSURL *)applicationDocumentsDirectory
{
NSLog(#"%#",[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]);
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
now call this method in AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//call here
[self applicationDocumentsDirectory];
}
This worked for me in swift:
let dirPaths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
println("App Path: \(dirPaths)")
I wrestled with this for some time. It became a huge pain to simply get to my local sqlite db. I wrote this script and made it a code snippet inside XCode. I place it inside my appDidFinishLaunching inside my appDelegate.
//xcode 6 moves the documents dir every time. haven't found out why yet.
#if DEBUG
NSLog(#"caching documents dir for xcode 6. %#", [NSBundle mainBundle]);
NSString *toFile = #"XCodePaths/lastBuild.txt"; NSError *err = nil;
[DOCS_DIR writeToFile:toFile atomically:YES encoding:NSUTF8StringEncoding error:&err];
if(err)
NSLog(#"%#", [err localizedDescription]);
NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:#"CFBundleName"];
NSString *aliasPath = [NSString stringWithFormat:#"XCodePaths/%#", appName];
remove([aliasPath UTF8String]);
[[NSFileManager defaultManager] createSymbolicLinkAtPath:aliasPath withDestinationPath:DOCS_DIR error:nil];
#endif
This creates a simlink at the root of your drive. (You might have to create this folder yourself the first time, and chmod it, or you can change the location to some other place) Then I installed the xcodeway plugin https://github.com/onmyway133/XcodeWay
I modified it a bit so that it will allow me to simply press cmd+d and it will open a finder winder to my current application's persistent Documents directory. This way, not matter how many times XCode changes your path, it only changes on run, and it updates your simlink immediately on each run.
I hope this is useful for others!
Open finder>Library>Developer>CoreSimulator>Devices
Then Change Arrangement icon from finder select Date Added
Select your app >data>Container>data>Applications>
choose your app >Documents>Here is your db file
In my case:
/Users/pluto/Library/Developer/CoreSimulator/Devices/A75107D2-A535-415A-865D-978B2555370B/data/Containers/Data/Application/265A12BC-FF5B-4235-B5EF-6022B83754B4/Documents/dboPhotoBucket.sqlite
Otherwise do this :
NSLog(#"app dir: %#",[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]);
- It will print the full path to data folder.
Swift:
let dirPaths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
println("App Path: \(dirPaths)")
Use Finder-->go to folder and enter given basepath to reach application folders
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
NSLog(#"%#",basePath);
The simulator puts the file in ~/Library/Developer/CoreSimulator/Devices/... but the path after /Devices is different for everyone.
Use this handy method. It returns the path of the temporary directory for the current user and takes no argument.
NSString * NSTemporaryDirectory ( void );
So in my ViewController class I usually put this line in my viewDidLoad just for a reference when I need to grab my CoreData stored file. Hope this helps.
NSLog(#"FILE PATH :%#", NSTemporaryDirectory());
(Note: to go to the path, from the finder menu click on Go and type ~/Library to open hidden directory then in the Finder Window you can click on the path shown on your console.)
This location has, once again, changed, if using Swift, use this to find out where the folder is (this is copied from the AppDelegate.swift file that Apple creates for you so if it doesn't work on your machine, search in that file for the right syntax, this works on mine using Xcode 6.1 and iOS 8 simulator):
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
println("Possible sqlite file: \(urls)")
The simulators are located under:
~/Library/Developer/CoreSimulator/
Here, they are listed as directories with UUID names. Use sort by 'Date modified' to find the latest one. Inside navigate to:
/data/Containers/Data/Application/
Here you will get a list of all the applications on that device. You can again sort this to get the latest app.
NOTE: Xcode changes the directory name every time you run the app, so don't rely on making alias/short cuts on desktop.
The easiest way is to use the app here, which does everything automatically.
I created this script that will zip the latest app built for any simulator and zip it to the desktop.
https://gist.github.com/Gerst20051/8ca49d5afbf09007b3696fab6e9f425c
#!/bin/bash
DESTINATION_DIR="$HOME/Desktop"
APP_PATH=$(find ~/Library/Developer/CoreSimulator/Devices/*/data/Containers/Bundle/Application/*/*.app -type d -maxdepth 0 -print0 | xargs -0 ls -td | head -n1)
APP_DIRNAME=$(dirname "$APP_PATH")
APP_BASENAME=$(basename "$APP_PATH")
FILE_NAME="${APP_BASENAME%.*}"
cd "$APP_DIRNAME"
zip -qr "$DESTINATION_DIR/$FILE_NAME.zip" "$APP_BASENAME"
In Swift 4 or Swift 5 you can use NSHomeDirectory().
The easiest way in Xcode 10 (or Xcode 11) is to pause your app (like when it hits a breakpoint) and run this line in the debugger console:
po NSHomeDirectory()
po stands for print object and prints most things
1. NSTemporaryDirectory() gives this:
/Users/spokaneDude/Library/Developer/CoreSimulator/Devices/1EE69744-255A-45CD-88F1-63FEAD117B32/data/Containers/Data/Application/199B1ACA-A80B-44BD-8E5E-DDF3DCF0D8D9/tmp
2. remove "/tmp" replacing it with "/Library/Application Support/<app name>/" --> is where the .sqlite files reside
With Swift 4, you can use the code below to get your app's home directory. Your app's document directory is in there.
print(NSHomeDirectory())
I think you already know that your app's home directory is changeable, so if you don't want to add additional code to your codebase, SimPholder is a nice tool for you.
And further more, you may wonder is there a tool, that can help you save time from closing and reopening same SQLite database every time after your app's home directory be changed. And the answer is yes, a tool I know is SQLiteFlow. From it's document, it says that:
Handle database file name or directory changes. This makes SQLiteFlow can work friendly with your SQLite database in iOS simulator.
Here is the sh for last used simulator and application. Just run sh and copy printed text and paste and run command for show in finder.
#!/bin/zsh
lastUsedSimulatorAndApplication=`ls -td -- ~/Library/Developer/CoreSimulator/Devices/*/data/Containers/Data/Application/*/ | head -n1`
echo $lastUsedSimulatorAndApplication

Accessing assets in iOS7 with NSBundle

I am having problems updating an app to iOS7 SDK. Before I've used iOS 6 SDK and accessed my mp3 file using a NSURL for the folder like this:
NSURL *folderURL = [[NSBundle mainBundle] URLForResource:#"" withExtension:#"" subdirectory:#"AudioGuide"];
Now, using the iOS 7 SDK I always get nil as the value for folderURL and my audio guide doesn't find the mp3's anymore.
I've already looked into the generated .app-Bundle for the simulator (in ~/Library/Application Support/...), and I can see the "AudioGuide" folder in the root. So it's definitly there.
I am not that iOS guru and didn't really follow iOS7 updates. Has there been any changes made on how to access own assets in an app? How do I access my files?
Apple doc for - (NSURL *)URLForResource:(NSString *)name withExtension:(NSString *)extension subdirectory:(NSString *)subpath
Returns the file URL for the resource file identified by the specified
name and extension and residing in a given bundle directory.
what that means is you can get the file URL with specific type in a given subdirectory, so you should use this method call with your file name and extension.
If you are trying to read the list of files under that directory, what you can do is bundle all of your audio files in a single zip or gzipped file and you extract it in your Documents or Application Support directory when it is accessed for the very first time, then you can read it using the code below.
NSArray *directories = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *directory = [directories objectAtIndex:0];
NSString *audioDirectoryPath = [directory stringByAppendingPathComponent:#"AudioGuide"];
NSArray *audioFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:audioDirectoryPath];
Hope this helps!

Resources