What would be the best way to always ensure the file saved to disk (or iCloud) contains the default file extension for our document format in an NSDocument based Cocoa app?
Background:
Users can load legacy files into the app that still use Type-Creator-Codes.
With auto-saving enabled in our app we need to make sure the file always has the file extension added as soon as it's written back to the disk (following any kind of changes) with our Cocoa app - or the app won't be able to open it (with now neither the type-creator-code nor the file extension).
If I got it right I'd overwrite NSDocumentController's open method
- (void)openDocumentWithContentsOfURL:(NSURL *)url
display:(BOOL)displayDocument
completionHandler:(void (^)(NSDocument *document,
BOOL documentWasAlreadyOpen,
NSError *error))completionHandler {
if(!url.pathExtension isEqualToString:#"XYZ")
url = url URLByAppendingPathExtension:#"XYZ"];
[super openDocumentWithContentsOfURL:url
display:displayDocument
completionHandler:completionHandler];
}
NOTE: written inline
Related
I have a sandboxed document-based (NSDocument) app. When saving files, I get weird sandboxing errors:
[scoped] handle 0: sandbox_extension_release error [22: Invalid argument]
[scoped] <0x600001278f00 file:///Users/username/Testi%203.exampletext>: internal sandbox error for <StopAccessing>
I'm also fetching file attributes for recent files using recentDocumentURLs in document controller, and display them in NSOutlineView. When I've accessed enough files, I start getting a new sandbox error, sandbox_extension_consume returned 12, and after that sandbox sometimes blocks the app from accessing any files.
This even happens when creating a minimal document-based app from the Xcode template. Things get saved correctly, and I can access the files I want, but errors start stacking up.
Here is a sample project which reproduces the behavior:
https://www.dropbox.com/s/9v0v65jbqkjb7ra/Sandboxed%20Doc%20App.zip?dl=1
Create a new document (window only shows the basic template, to keep it minimal) and save it. Sandbox error is immediately logged.
Document subclass in the sample is as bare bones as possible:
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError {
return [_string dataUsingEncoding:NSUTF8StringEncoding];
}
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
_string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return YES;
}
AFAIK, NSDocumentController should support sandboxing without any subclassing. Also, trying to manually stop accessing scoped bookmark of the document URL (after saving or on document close) produces an error.
Similar sandbox bug is referred to on Twitter, but I'm hardly accessing couple of files, and not over 4000.
What am I doing wrong, or is this a macOS Big Sur bug? And if not, how can it be present even in the document-based app template?
The issue here was that my app was requesting the list of recent files from NSDocumentController, and checking the edited dated by once again requesting that same list.
There seems to be a limit for how many files the app can rapidly access outside its sandbox, even if it has the permission to do so, and sandbox quickly runs out of extensions.
To fix the issue, check your code for anything that runs through a list of files and accesses them.
I am letting user record audio from mic to .acc file
at the moment they are saved to default app document folder
in plist i enabled "supports opening documents in place"
Now in iPhones File App i can see my apps folder in Documents and also see and listen to the recorded .acc file
great... but...
Now the user can also see and then edit / mess with ini files and other files i dont really want them o see or edit etc...
So now this isnt a good solution.
So where do i save recorded audio so that users can still see them, find them on their phone, listen and share them with friends (without being to see the apps other files)?
For any files that don't need to change, just keep them as resources in your app bundle and load them from there at runtime.
For files that are generated at runtime (not at build-time) or config files that change, which you don't want to be exposed, put them in Application Support instead of in Documents.
In Objective-C:
NSArray * appSupportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
inDomains:NSUserDomainMask];
In Swift:
let appSupportURLs = FileManager.default.urls(for: .applicationSupportDirectory,
in: .userDomainMask)
This Apple Developer video pretty much covers it: https://developer.apple.com/videos/play/tech-talks/204/
I'm writing a WatchKit extension and I'd like to read a file out of the host application's [NSBundle mainBundle]. I've tried [NSBundle bundleWithIdentifier:] but that just returns nil.
I have several potential workarounds, but nothing that would be as simple as "just read what you need from the host's mainBundle".
Is there a way of doing this?
The host app and your WatchKit extension can share files in only one of two ways, as far as I know:
Shared app group
Including a file in both targets
They run in separate processes and aren't accessible to each other outside of approved methods.
I ran into a similar problem like yours. The main host app has a particular pList that I needed to read, and I couldn't read from watch extension because they are isolated.
So in the watch I invoked the openParentApplication method
and in the main application my handler was something along the lines of
-(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
NSString *request = [userInfo objectForKey:#"request"];
if ([request isEqualToString:ReadFile])
{
//read the file. and then i like to put it into a NSDictionary
NSDictionary *responseDictionary = //whatever
reply(responseDictionary);
}
else{ reply(nil); }
}
And then the contents were returned to me in the callback closure on the watch of the openParentApplication. Seems to work. Though your situation could be different in which case this method might not be viable.
From the Apple WatchKit programming guide:
To share preferences data between apps, create an NSUserDefaults object using the identifier of the shared group. The initWithSuiteName: method of NSUserDefaults creates an object that allows access to the shared user defaults data. Both processes can access this data and write changes to it.
Your main app can write a NSDictionary/NSArray to the shared prefs, and then the watch kit can pull it out, without starting the main app - however, the main app will have to be run at least once to update the shared prefs.
I am developing a mac app and I want to put my settings (array of strings) into a file. If that file exists in the same folder as the app it is read and it overwrites default settings.
No problems writing the file but when reading with code:
NSArray* settingsInput = [[NSArray alloc] initWithContentsOfFile:#"./SettingsFile"];
something strange happens. When running the app from XCode (the settings file is in the build/debug folder next to the app) settings are read without a problem. When I archive the app and run it from the desktop, the file cannot be loaded. Even when I copy the app from the build folder to the desktop it does not work.
What could be the reason for this kind of behaviour? How can I read this file?
It may be a better Idea to use the normal prefence system. NSUserDefaults.
There a couple of ways you can do it.
But the idea is to give your app a set of default preference which are registered for you in the correct domain and always with a fresh app.
Using the registerDefaults: from NSUserDefaults.
See Apples documentation NSUserDefaults and its #registerDefaults
But the one I would use is :
Copy a plist file into the supporting files in you Xcode project.
Making sure "Copy files into destination group's folder" is checked. And the "Add to targets is check also"
The plist file should contain your array of strings.
(I created mine. By duplicating another plist in my user preferences. Renaming it. Copying it to the project. Selecting it and editing it to how I needed it. Making sure I use the file menu ->'Save' to save the changes. )
Declare a NSUserDefaults * prefs;
Now in the - (id)init method for the app. you register the contents of the file as your default preferences.
- (id)init
{
self = [super init];
if (self) {
prefs = [NSUserDefaults standardUserDefaults] ;
NSString *registerDefaultsPlistFile= [[NSBundle mainBundle] pathForResource:#"registerDefaults" ofType:#"plist"];
[prefs registerDefaults:[NSDictionary dictionaryWithContentsOfFile: registerDefaultsPlistFile]];
}
return self;
}
You can later make a call to read these preferences.
NSLog(#" arrayOfStrings = %#", [prefs objectForKey:#"arrayOfStrings" ]);
These default preferences are NOT written to file/out unless you make a change to them. By written to file I mean to the applications preference file. Once you do make a change to them then they will be written out into the users preferences and those are what will be used from then on.
You should not rely on the current directory of the app. You should either read from the app bundle (see NSBundle class for get the correct path) or the app's document directory (see NSSearchPathForDirectoriesInDomains(NSDocumentDirectory ...).
The UNIX concept of the current working directory is not commonly used in Mac desktop applications. When launching an app through the Finder it's usually set to the root directory of the boot volume.
You should find another way to determine locations for your settings files. Good spots would be ~/Library/Preferences or ~/Library/Application Support/MyApp. You can get the absolute path to these directories using:
NSString *path = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomain, YES)[0];
I have a simple UITableView, where each cell has an thumbnail picture that the user may have taken with its iOS device camera.
If iCloud is enabled the image is saved in it. However I was wondering if some sort of caching happens when loading the image, because I have notice a slowness on first loading then, even if the piece of code is called again when cell display on screen, the image show quite fast.
This is the relevant code fragment, I have omitted the logic for building the cell, I think it is not relevant, because the question is about other aspects:
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// logic for retrieve data and build cell
NSURL *ubiquityUrl = [fm URLForUbiquityContainerIdentifier:nil];
NSURL *docURL = [ubiquityUrl
URLByAppendingPathComponent:[NSString stringWithFormat:#"P_%#_%#.jpg",imgId,#"thumbnail"]
isDirectory:NO];
// this a custom object extending UIDocument
IP2DataDocument *dataDocument = [[IP2DataDocument alloc] initWithFileURL:docURL];
[dataDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(#"iCloud document opened");
// logic for filling table cell picture
} else {
NSLog(#"failed opening document from iCloud");
}
}];
// returning cell
}
I can see "iCloud document opened" in Xcode, each time the cell display.
If some sort of caching occurs, can you point out where and how ?
The Document-based App Programming Guide for iOS says:
When you run a metadata query to learn about an application’s iCloud documents, the query results are placeholder items (NSMetadataItem objects) for document files. The items contain metadata about the file, such as its URL and its modification date. The document file is not in the iCloud container directory.
The actual data for a document is not downloaded until one of the following happens:
Your application attempts to open or access the file, such as by calling openWithCompletionHandler:.
Your application calls the NSFileManager method startDownloadingUbiquitousItemAtURL:error: to download the data explicitly.
To sum it up: The first time you open your document, it may not have been downloaded from iCloud yet, which is why it'll take longer. Afterwards, there is a local copy of the file that is obviously faster to read.
It is your docURL or specially
NSURL *ubiquityUrl = [fm URLForUbiquityContainerIdentifier:nil];
You don't actual access directly to iCloud. You access an equivalent local location (ubiquityUrl) that is iCloud enabled. If the file that you try to get has not been downloaded locally yet, then the local iCloud daemon will download it at the time you are accessing. Once this occurs, it is stored in your device locally. So the next time, you try to access it again using the docURL, it is much quicker because of this. Also the iCloud daemon is responsible for syncing version your document. So you don't need to worry about that if someone else has updated a newer version of the same document from another device.