Where can a sandboxed Mac app save files? - objective-c

My Mac app is sandboxed and I need to save a file. Where do I save this file? I can't seem to find the specific place where this is allowed without using an open panel. This is how I do it on iOS:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];
What is the equivalent for the sandboxed directory on Mac?

That code snippet works on the Mac regardless of whether your application is sandboxed.
In a non-sandboxed Mac app, path will refer to the Documents folder in your home: e.g. /Users/username/Documents.
In a sandboxed app, it refers to a folder within the app sandbox: e.g. /Users/username/Library/Containers/com.yourcompany.YourApp/Documents
See the docs for details.

Apple's Sandboxing guide is very useful, found here.
You basically have a folder dedicated for your app, as described by theAmateurProgrammer in reply to my question here.
~/Library/Container/com.yourcompany.yourappname/
Here is what I have so far, I will improve it later:
//Create App directory if not exists:
NSFileManager* fileManager = [[NSFileManager alloc] init];
NSString* bundleID = [[NSBundle mainBundle] bundleIdentifier];
NSArray* urlPaths = [fileManager URLsForDirectory:NSApplicationSupportDirectory
inDomains:NSUserDomainMask];
NSURL* appDirectory = [[urlPaths objectAtIndex:0] URLByAppendingPathComponent:bundleID isDirectory:YES];
//TODO: handle the error
if (![fileManager fileExistsAtPath:[appDirectory path]]) {
[fileManager createDirectoryAtURL:appDirectory withIntermediateDirectories:NO attributes:nil error:nil];
}

Converting #Mazyod's answer into Swift (5.1):
var appPath: URL? {
//Create App directory if not exists:
let fileManager = FileManager()
let urlPaths = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)
if let bundleID = Bundle.main.bundleIdentifier, let appDirectory = urlPaths.first?.appendingPathComponent(bundleID,isDirectory: true) {
var objCTrue: ObjCBool = true
let path = appDirectory.path
if !fileManager.fileExists(atPath: path,isDirectory: &objCTrue) {
do {
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
} catch {
return nil
}
}
return appDirectory
}
return nil
}
However, the directory has changed and I am not sure that the additonal repetition of the bundle ID is needed as the path is
"/Users/**user name**/Library/Containers/**bundleID**/Data/Library/Application Support/**bundleID**".
But it seems to work.

Is is even easier. For sandboxed apps on macOS the function NSHomeDirectory gives you the path where you have read and write access and can save all your files. It will be a path like this
/Users/username/Library/Containers/com.yourcompany.YourApp

Related

How to get the path of user's special folder in macOS?

Is there any API to get the user's special folder, such as the Downloads or Documents folder in macOS?
As per my exprience I am generally use to get Home directory path as per below example.
1st Way :
NSArray * paths = NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES);
NSString * desktopPath = [paths objectAtIndex:0];
For Desktop : NSDesktopDirectory
For Document : NSDocumentDirectory
For Downloads : NSDownloadsDirectory
2nd Way:
Not the best way but we can get path by this method also.
Use Localization for different languages.
$Home Directory of Current User:
[NSURL fileURLWithPath:NSHomeDirectory()];
$Home/Desktop
[NSURL fileURLWithPath:[NSHomeDirectory() stringByAppendingPathComponent:#"Desktop"]];
$Home/Documents :
[NSURL fileURLWithPath:[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"]];
$Home/Downloads :
[NSURL fileURLWithPath:[NSHomeDirectory() stringByAppendingPathComponent:#"Downloads"]];
Hope this will help you to find special folder path.
As already mentioned by others, a more modern approach is to retrieve a URL by using NSFileManager:
NSURL *desktopUrl = [NSFileManager.defaultManager URLsForDirectory:NSDesktopDirectory inDomains:NSUserDomainMask].firstObject;
desktop for Swift 5.x:
var DesktopDir : String = {
let paths = NSSearchPathForDirectoriesInDomains(.desktopDirectory, .userDomainMask, true)
return paths[0]
}()

how to get /Users/username/Downloads path in a sandboxed app?

what I've done is setup the com.apple.security.files.downloads.read-write to true, and have looked up the Apple Sandbox related docs, but I can't figure out how to get the downloads folder path, what I get is still container path like this: /Users/username/Library/Containers/com.errpro.Snell/Data/Downloads
, the method I use is NSSearchPathForDirectoriesInDomains. I've seen someone use getpwent to get the path, but seems not appropriate. Any help would be appreciated.
You should use the method URLForDirectory. This method find the name of the current user and insert it in the URL path.
NSFileManager *fm = [NSFileManager defaultManager];
NSURL *downloadsURL;
downloadsURL = [fm URLForDirectory:NSDownloadsDirectory
inDomain:NSUserDomainMask appropriateForURL:nil
create:YES error:nil];
Swift 5
do {
let url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
} catch{
print(error)
}

LSCopyApplicationURLsForURL always returns null

When I try to log all available editors on my system for my temporary file (which is "toString" in this code) it always returns null, although I have many applications installed on my system.
NSArray *appUrls = (NSArray*)LSCopyApplicationURLsForURL((CFURLRef)[NSURL URLWithString:toString], kLSRolesViewer | kLSRolesEditor);
toString is containing the following file path:
/var/folders/pl/tcc5k3fd6tj2__9dprg9dm1m0000gp/T/tempFile
What should be the problem here?
[NSURL URLWithString:toString]
expects a complete URL string including scheme, such as "file://var/folders/...".
Use
[NSURL fileURLWithPath:toString]
instead to get a file URL with the specified path.
Another problem could be that your file name does not have any file extension (e.g. ".txt"), because Launch Services uses the extension (or file type/creator) to find a suitable application.
I was struggling with the this and I wanted to get all Bundles that could open a determined path/file extension.
If you have a file extension, you can get all bundles that can edit it by the following:
//All Bundle Ids
NSString *pathExtension = #"docx";
CFArrayRef utisRef = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension,(__bridge CFStringRef) pathExtension,nil);
NSLog( #"UTI: utisRef %#", utisRef);
NSArray *utis = CFBridgingRelease(utisRef);
NSMutableSet *mutableSet = [[NSMutableSet alloc] init];
for (NSString *uti in utis) {
CFArrayRef bundleIDsRef = LSCopyAllRoleHandlersForContentType((__bridge CFStringRef) uti,kLSRolesEditor);
[mutableSet addObjectsFromArray:CFBridgingRelease(bundleIDsRef)];
}
NSLog( #"bundleIDs: %#", mutableSet);
If you have a path of file and you want to get all apps location that can edit it, you can use the following:
//Location of apps
NSString *str = #"/Users/ricardoanjos/Library/Developer/Xcode/DerivedData/EgnyteDrive-hforbniifiojczefbnwanzxakvlr/Build/Products/Debug/1.pdf";
NSURL* url = [[NSURL alloc] initFileURLWithPath:str];
CFURLRef urlRef = (__bridge CFURLRef)url;
CFArrayRef appUrlsRef = LSCopyApplicationURLsForURL(urlRef, kLSRolesEditor);
NSArray *appUrls = CFBridgingRelease(appUrlsRef);
NSLog(#"appUrls: %#", appUrls);
I hope it will help.

Create a Folder (bundle) in Cocoa

I'm trying to programmatically create a folder with Cocoa.
I've written an NSString category and we've got the following function there :
- (void)createAsFolder
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError* err = nil;
[fileManager createDirectoryAtPath:self withIntermediateDirectories:YES attributes:nil error:&err];
if (err)
{
NSLog(#"ERROR : %#",err);
}
}
So, in a few words, let's say we have an NSString* path = #"/some/path/is/here";, we can create it simply by :
[path createAsFolder];
The thing is, although it works PERFECTLY for normal folders, it does NOT when the path specified is a bundle (that is : WITH an extension). E.g.
NSString* path = #"/this/is/a/path/to/some/bundle.bun";
[path createAsFolder];
The above does NOT work.
Any ideas on how to fix that?
OK, here's the answer (thanks to #thundersteele), if you want to copy a full file tree from on place to another :
NSFileWrapper* w = [[NSFileWrapper alloc] initWithPath:initialPath];
[w writeToFile:destinationPath atomically:YES updateFilenames:YES];
And yep : it has ABSOLUTELY no problem whether the subfolders are packages/bundles or whatever. Not that hard, huh? Just 2 lines... lol
Try NSFileWrapper instead. I think it can do what you want to do.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSFileWrapper_Class/Reference/Reference.html

Embedding XML to iOS application

I'm making my first game in iOS and I need to load some levels that are stored as XML. Level files are stored locally and everything works fine from emulator. However, since XML is loaded in the runtime when i try to test my game on an actual device it can't find the XML files since they are not actually part of the app.
I'm coming from Flash background so I might have a wrong idea how this is done on iOS but I need to somehow bundle those XML files with the app, or embed it in the code somehow? Is this possible?
Thanks a lot :)
Well The code to look up your app bundle for the specified file is
NSString *path = [[NSBundle mainBundle] pathForResource:#"fileName.xml" ofType:nil]
NSURL *xmlURL = [NSURL fileURLWithPath:path];
[[NSXMLParser alloc] initWithContentsOfURL: xmlURL]
Hope this helps you...
You could add the XML file to your project and run time read it up with something like this
NSArray *myPathList = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *myPath = [myPathList objectAtIndex:0];
NSError **err;
myPath = [myPath stringByAppendingPathComponent:fileName];
if([[NSFileManager defaultManager] fileExistsAtPath:myPath])
text = [NSString stringWithContentsOfFile:myPath encoding:NSUTF8StringEncoding error:err];
You could consider using JSON in instead of XML. At least to my knowledge the available XML parsers are not nearly as simply to use as SBJson for instance (https://github.com/stig/json-framework/)
Here is my solution in Swift 3.0. In addition to the code, you'll need to add your xml file as a data set in your Assets.xcassets. Do this by creating a new Data Set, giving it any name, and then dragging the xml file from the finder to your newly created data set. When you reference the file in your code, you'll use the file name of the file, and not the name of the data set.
var parseResults = false
if let path = Bundle.main.path(forResource: fileName, ofType: ".xml") {
let url = URL(fileURLWithPath: path)
if let xmlParser = XMLParser(contentsOf: urlToFile) {
xmlParser.delegate = self
parseResults = xmlParser.parse()
}
}