How can I suppress the autosave “The file has been changed by another application” alert? - objective-c

I have a NSDocument subclass that presents a text document from disk. I’m trying to make it refresh automatically on detecting file changes on disk. I’ve overridden -presentedItemDidChange like this:
- (void)presentedItemDidChange
{
[super presentedItemDidChange];
// Ignoring bundles and error-handling for the moment.
NSData *newData = [NSData dataWithContentsOfURL:self.presentedItemURL];
self.textView.string = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
}
The UI refreshes fine when the file is changed in another application. The problem is, I get this dialog when I try to save the document in my application after it is modified by another app:
I kind of have an idea why this happens (not sure whether it’s correct): The modification time of the document is later (because it’s modified by another application) than the latest saved version in my app. But can I notify the autosaving system that I have done something with it and let it go away? Or am I doing things wrong when I refresh the document, and I should do it some other way to handle document versions correctly? I need to consider both external applications support or do not support autosave.
Thanks in advance.

#uranusjr's answer pointed me in the right direction -- only revertDocumentToSaved: wasn't exactly the right place.
override func presentedItemDidChange() {
dispatch_async(dispatch_get_main_queue()) {
self.reloadFromFile()
}
}
func reloadFromFile() {
guard let fileURL = self.fileURL else { return }
do {
try revertToContentsOfURL(fileURL, ofType: "YOUR TYPE HERE IF NECESSARY")
} catch {
// TODO handle error
print(error)
}
}
This simply reloads the file. readFromURL(url:, ofType:) (or the NSData/file wrapper based variants) is called and you can re-create your data structures from there.

Stumbled across the solution today (finally). You can “cheat” OS X into not warning about this by reverting the document (but not the file itself) before actually updating the internal data structure:
// Somehow read the updated data.
NSString *content = ...;
// Revert the document.
// This will discard any user input after the last document save,
// so you might want to present some UI here, like an NSAlert.
[self revertDocumentToSaved:self];
// Update the internal state.
self.content = content;
Now OS X will be happy when you save the document.

Related

Mac OSX app : Issue Related to NSArrayController NSTableView Core data Adding Record

I have using NSArrayController NSTableView and Core data binding.
I have take one button and connect add: method of NSArrayController to its action.
On Adding new record
TableView added and shows new record.
NSArrayController's add: method called
Problem :
Value is not added into core data (Sqlite type).
On application relaunching shows old data.
This is an apple example code. Basically it tries to save the context before app is terminated. Depending on your specific case you might move the functionality to somewhere else.
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
// Save changes in the application's managed object context before the application terminates.
let context = persistentContainer.viewContext
if !context.commitEditing() {
NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing to terminate")
return .terminateCancel
}
if !context.hasChanges {
return .terminateNow
}
do {
try context.save()
} catch {
let nserror = error as NSError
// Customize this code block to include application-specific recovery steps.
let result = sender.presentError(nserror)
if (result) {
return .terminateCancel
}
let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message")
let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info");
let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title")
let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title")
let alert = NSAlert()
alert.messageText = question
alert.informativeText = info
alert.addButton(withTitle: quitButton)
alert.addButton(withTitle: cancelButton)
let answer = alert.runModal()
if answer == .alertSecondButtonReturn {
return .terminateCancel
}
}
// If we got here, it is time to quit.
return .terminateNow
}
You're probably missing setting the managedObjectContext and entityName on NSArrayController.

Having Trouble Getting the UIDocumentBrowserController to open docs in a Document based app

I've been working on a new Document-based app, and was super glad about the new UIDocumentBrowserController...trying to roll my own solution for the document browser UI was tricky!
I'm having some trouble getting the browser to open documents after they've been created.
What happens now is that when I choose to create a new document in the document browser, the document is created and opened as expected, although an error message is logged. However, after the doc is closed, I cannot reopen the file, either immediately or upon subsequent launches, even though the document is displayed. However, a weird clue here is that if I stop running the app after creating the document, but without adding new information to it (triggering the save cycle), and run the project again, I can open the file correctly. Whuch makes me think that there's something in the way the files are being saved that is the issue.
(Note: At this phase, I'm working on getting the local, non/icloud implentation working, before I move on to the icloud implementation.)
Here is the error message at any point in the code whenthe document is saved to disk (or at least most of the time!):
2017-06-20 13:21:58.254938-0500 Sermon Design 2 iOS[22454:5000138] [default] [ERROR] Could not get attribute values for item file:///Users/stevenhovater/Library/Developer/CoreSimulator/Devices/9A4364F2-B3A1-4AD9-B680-FB4BC876C707/data/Containers/Data/Application/DD534ED8-C4A3-40FE-9777-AED961976878/Documents/Untitled-9.sermon. Error: Error Domain=NSFileProviderInternalErrorDomain Code=1 "The reader is not permitted to access the URL." UserInfo={NSLocalizedDescription=The reader is not permitted to access the URL.}
I suspect that the issue lies somewher in my document types plists, which I've tried to set up by imitating the setup in the video for wwdc 2017 session 229.
My docs are encapuslated by an NSData object, using what I take to be a pretty standard subclass implentation of UIDocument. (I'm omitting the code to generate the thumbnails)
override func contents(forType typeName: String) throws -> Any {
print("Saving Document Changes")
if sermon != nil {
let newData = NSKeyedArchiver.archivedData(withRootObject: sermon!)
return newData
} else {
let newData = NSKeyedArchiver.archivedData(withRootObject: Sermon())
return newData
}
}
override func fileAttributesToWrite(to url: URL, for saveOperation: UIDocumentSaveOperation) throws -> [AnyHashable : Any] {
let thumbnail:UIImage = self.createThumbnail()
let thumbnaildict = [URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey : thumbnail]
let dict = [URLResourceKey.thumbnailDictionaryKey:thumbnaildict]
return dict
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let newSermon:Sermon = NSKeyedUnarchiver.unarchiveObject(with: contents as! Data) as? Sermon else{
throw documentErrors.invalidFile
}
self.sermon = newSermon
}
In my subclass of UIDocumentBrowserViewController, Here is my code for getting a local filename and for creating the new document.
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
var newDocumentURL: URL? = nil
print("creating new local document")
guard let target = self.newLocalFilename() else {
return
}
let targetSuffix = target.lastPathComponent
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory() + targetSuffix)
let newDocument:SDDocument = SDDocument(fileURL: tempURL)
newDocument.sermon = Sermon()
/
newDocument.save(to: tempURL, for: .forCreating) { (saveSuccess) in
/
guard saveSuccess else {
/
importHandler(nil, .none)
return
}
/
newDocument.close(completionHandler: { (closeSuccess) in
/
guard closeSuccess else {
/
importHandler(nil, .none)
return
}
/
importHandler(tempURL, .move)
})
}
}
func newLocalFilename() -> URL? {
let fileManager = FileManager()
guard let baseURL = self.localDocumentsDirectoryURL.appendingPathComponent("Untitled")
else {return nil}
var target = baseURL.appendingPathExtension(DocumentBrowserViewController.documentExtension)
var nameSuffix = 2
while fileManager.fileExists(atPath: target.path) {
target = URL(fileURLWithPath: baseURL.path + "-\(nameSuffix).\(DocumentBrowserViewController.documentExtension)")
nameSuffix += 1
}
let targetSuffix = target.lastPathComponent
print("Target name: \(targetSuffix)")
print("new url: \(target)")
return target
}
After four or five hours of work banging my head against this problem, I discovered a simple solution: don't test in the Simulator. I switched to testing on my device and instantly everything started working as advertised.
[I can't speak from experience here, but it may be that the "doesn't work in the Simulator" problem is confined to Sierra, but that the Simulator does work in High Sierra. This would explain why some users see this issue and others don't, and especially why Apple seems blissfully unaware of it in the WWDC video.]
I had exactly the same issue when I was trying to save to NSTemporaryDirectory().
If you instead save to the documents directory ([[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject]), it appears to work fine!
Update: it looks like this issue is fixed in iOS 11 beta 3, and you can now save newly created documents to NSTemporaryDirectory() correctly.
Here is my current theory.
This error
Error Domain=NSFileProviderInternalErrorDomain Code=1 "The reader is not permitted to access the URL."
shows up when first creating a UIDocument at a new URL using -initWithFileURL. It's basically saying "this URL doesn't exist yet," but in a way that makes it sound more like a permissions issue.
As far as I can tell, it doesn't prevent you from saving, opening, editing, or closing the file. So I think it's just a superfluous error that Apple should have taken out.
I found that the error happens on simulator when LSSupportsOpeningDocumentsInPlace property is set to YES in the info.plist.
Turn this property to NO, then it starts working, in my case.
On the real device, it works anyway without having error.
Had similar problem, and tried the method for providing a default Core Location in the Schemes settings and it works now. The method was mentioned in this answer: IOS 9 Error Domain=kCLErrorDomain Code=0 "(null)"

CoreText CopyFontsForRequest received mig IPC error

I've been working on a BIG project (there's no point of showing any actual code anyway) and I've notice that the following message appears in the logs:
CoreText CopyFontsForRequest received mig IPC error (FFFFFFFFFFFFFECC) from font server
The error pops up as soon as a WebView has finished loading. And I kinda believe it's the culprit behind a tiny lag.
Why is that happening? What can I do to fix this?
P.S. Tried the suggested solution here to check whether it was something system-specific, but it didn't work.
More details:
The error appears when using the AMEditorAppearance.car NSAppearance file, from the Appearance Maker project. Disabling it (= not loading it all) makes the error go away.
I don't really care about the error message, other than that it creates some weird issues with fonts. E.g. NSAlert panels, with input fiels, show a noticeable flicker and the font/text seems rather messed up, in a way I'm not sure I can accurately describe. (I could post a video with that if that'd help)
This is probably related to system font conflicts and can easily be fixed:
Open Font book
Select all fonts
Go to the file menu and select "Validate fonts"
Resolve all font conflicts (by removing duplets).
Source: Andreas Wacker
Answer by #Abrax5 is excellent. I just wanted to add my experience with this problem and could not fit it into a comment:
As far as I can tell, this error is raised only on the first failed attempt to initialise an NSFont with a font name that is not available. NSFont initialisers are failable and will return nil in such a case at which time you have an opportunity to do something about it.
You can check whether a font by a given name is available using:
NSFontDescriptor(fontAttributes: [NSFontNameAttribute: "<font name>"]).matchingFontDescriptorWithMandatoryKeys([NSFontNameAttribute]) != nil
Unfortunately, this also raises the error! The following method does not, but is deprecated:
let fontDescr = NSFontDescriptor(fontAttributes: [NSFontNameAttribute: "<font name>"])
let isAvailable = NSFontManager.sharedFontManager().availableFontNamesMatchingFontDescriptor(fontDescr)?.count ?? 0 > 0
So the only way I found of checking the availability of a font of a given name without raising that error is as follows:
public extension NSFont {
private static let availableFonts = (NSFontManager.sharedFontManager().availableFonts as? [String]).map { Set($0) }
public class func available(fontName: String) -> Bool {
return NSFont.availableFonts?.contains(fontName) ?? false
}
}
For example:
NSFont.available("Georgia") //--> true
NSFont.available("WTF?") //--> false
(I'm probably overly cautious with that optional constant there and if you are so inclined you can convert the returned [AnyObject] using as! [String]...)
Note that for the sake of efficiency this will not update until the app is started again, i.e. any fonts installed during the app's run will not be matched. If this is an important issue for your particular app, just turn the constant into a computed property:
public extension NSFont {
private static var allAvailable: Set<String>? {
return (NSFontManager.sharedFontManager().availableFonts as? [String]).map { Set($0) }
}
private static let allAvailableAtStart = allAvailable
public class func available(fontName: String) -> Bool {
return NSFont.allAvailable?.contains(fontName) ?? false
}
public class func availableAtStart(fontName: String) -> Bool {
return NSFont.allAvailableAtStart?.contains(fontName) ?? false
}
}
On my machine available(:) takes 0.006s. Of course, availableAtStart(:) takes virtually no time on all but the first call...
This is caused by calling NSFont fontWithFamily: with a family name argument which is not available on the system from within Chromium's renderer process. When Chromium's sandbox is active this call triggers the CoreText error that you're observing.
It happens during matching CSS font family names against locally installed system fonts.
Probably you were working on a Chromium-derived project. More info can be found in Chromium Bug 452849.

Open file with own application written in objective-c

I would like to know how can I open a file with my OS X application, which I wrote in Objective-C. I registered the file types in Info.plist and I have application:openFile: in my code. I did everything by this post, which was marked as solved.
The problem is that this works only if I drag and drop my file on my application while it is running. But it doesn't work if I just double click on my file. It starts my application, but not as it would start if I would drag and drop. So the code which is in application:openFile: doesn't run when double-clicked, but only when I drag and drop my file.
EDIT:
Some more detail about my code, and what I am trying to achieve.
I created a wrapper application for an other app. Let's call the other app the "HelperApp.app". This HelperApp is inside the /Contents/ folder of my wrapper app. With the wrapper app I specified a new file type, let's call it ".ha" in the Info.plist file. This file contains some argument commands. What I try to achieve, that when a user clicks on a file which is a ".ha" file, then my wrapper app reads in the argument from this file and sets it for the HelperApp, then starts the HelperApp. This HelperApp is opening different things depending on the argument it gets. Below you can check my code.
I have an AppDelegate.h and an AppDelegate.mm by default how the newest Xcode creates it. I added this line to my AppDelegate.h, just before the "#end":
- (BOOL)processFile:(NSString *)file;
I have these functions in my AppDelegate.mm:
#import "AppDelegate.h"
#import "ArgumentParser.h"
#implementation AppDelegate
- (void)dealloc
{
[super dealloc];
}
- (BOOL)application:(NSApplication *)WrapperApp openFile:(NSString *)filename
{
return [self processFile:filename];
}
- (BOOL)processFile:(NSString *)file
{
NSLog(#"The following file has been dropped or selected: %#",file);
std::string path = [file cStringUsingEncoding:[NSString defaultCStringEncoding]];
ArgumentParser parser = ArgumentParser();
parser.getArgumentfromFile(path);
parser.setArgumentinFile(); // <== This is the "corrupted" function
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *helperAppPath = [[mainBundle bundlePath]
stringByAppendingString:#"/Contents/HelperApp.app"];
[[NSWorkspace sharedWorkspace] launchApplication:helperAppPath];
return YES;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
}
The corrupted function - setArgumentinFile():
void ArgumentParser::setArgumentinFile() {
std::string content = ""; // The file content
std::fstream file;
file.open("HelperApp.app/Contents/Wrapper/HelperApp.app/Contents/Info.plist");
// Open the file and modify the arguments
if(file.is_open()) {
std::stringstream stream;
stream << file.rdbuf();
std::string line = "";
bool isIt = false;
while(getline(stream, line)) {
// This line is the argument list, which needs to be modifyed
if(isIt) {
int index = (int)line.find_last_of("<");
std::string start = line.substr(0, index);
std::string end = line.substr(index, std::string::npos);
std::string argument_list = start + " " + _argument + end;
content += argument_list + '\n';
isIt = false;
}
// Save the rest of the file so we can overwrite it
else {
content += line + '\n';
}
// Next line is the argument list
if(line.find("WineProgramArguments") != std::string::npos) {
isIt = true;
}
}
file.flush();
file.close();
}
else {
file.flush();
file.close();
throw std::runtime_error("File isn't opened");
}
file.open("HelperApp.app/Contents/Wrapper/HelperApp.app/Contents/Info.plist", std::ios::out);
// Open the file and overwrite it with the modifyed argument
if(file.is_open()) {
file << content;
file.flush();
file.close();
}
else {
file.flush();
file.close();
throw std::runtime_error("File isn't opened");
}
}
If I comment out the above function from the processFile function in AppDelegate, then everything works "smoothly". I mean the wrapper app starts and it starts the HelperApp with default arguments. So here should be the error...
If you've implemented -application:openFile:, it should be called when you double-click a file of the type that you've registered. You say that the app launches, so the OS is trying to use your app to open the file. Here's a useful note from the documentation:
If the user started up the application by double-clicking a file, the
delegate receives the application:openFile: message before receiving
applicationDidFinishLaunching:. (applicationWillFinishLaunching: is
sent before application:openFile:.)
So, if you're doing anything in -applicationDidFinishLaunching: that has to be done before you open any files, that could be your problem. Consider moving your app initialization code to -applicationWillFinishLaunching:.
I've figured it out. When you double-click on a file icon, the application will launch itself, other things done correctly. But the application that responds to your action is not necessarily the one that you built for the last time. Probably, an old copy of your application is responding. Take a look at Library > Developer > Xcode > DrivedData. You should see many folders for your application. You can locate your application folders by right-clicking and choosing Shown In Finder after build one. Trash them all, and build a new application. Then double-click and see what happens now.
The problem was, that I gave the wrong path in my function. This path worked if I started the app from Xcode, but did not if I started the app by itself.
Here is the post which solved my problem!
right-click vs. double-click to open a file behave differently!
Apple Docs:
If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message before receiving applicationDidFinishLaunching:. (applicationWillFinishLaunching: is sent before application:openFile:.)
The Apple Docs leave out a vital piece of info...
I had assumed that a right-click -> 'Open With'
operation in Finder would be the same as a double-click.
Its NOT!
application:openFile: happens AFTER applicationDidFinishLaunching: in this case!
Was scratching my head for an hour on this one.

Change Authorization Dialog shown by AuthorizationCreate()

Looking through Apples BetterAuthorizationSample and further Derivatives( http://www.stevestreeting.com/2011/11/25/escalating-privileges-on-mac-os-x-securely-and-without-using-deprecated-methods/ )
I am trying to make a small change to the application and gain better understanding of the whole Security & ServiceManagement framework.. Therefore I proceeded to add an a button which removes the installed Job through the inverse of SMJobBless - SMJobRemove(). Straightforward however the AuthorizationCreate() call displays a dialog that states and requests permission to install a helper and not remove it.
That's the dialog I get (by using kSMRightModifySystemDaemons). As you can see it says that my app tries to add a new helper tool. Which will confuse my users because the app actually tries to remove the installed helper tool.
I'm seeking to find knowledge on how this dialog is changed to reflect my actual action (Job Removal), There are also several other apps which seem to completely customize the dialog - showing their own Custom Label and Buttons..
BOOL doRemoveSystemTool(NSString* label, NSError** error)
{
BOOL result = NO;
AuthorizationItem authItem = { kSMRightModifySystemDaemons, 0, NULL, 0 };
AuthorizationRights authRights = { 1, &authItem };
AuthorizationFlags flags = kAuthorizationFlagDefaults |
kAuthorizationFlagInteractionAllowed |
kAuthorizationFlagPreAuthorize |
kAuthorizationFlagExtendRights;
AuthorizationRef authRef = NULL;
//Obtain authorization
OSStatus status = AuthorizationCreate(&authRights, kAuthorizationEmptyEnvironment, flags, &authRef);
if (status != errAuthorizationSuccess)
{
NSLog(#"Failed to create AuthorizationRef, return code %ld", (long)status);
} else
{
//We have authorization so proceed with removing the Job via SMJobRemove
result = SMJobRemove(kSMDomainSystemLaunchd, (CFStringRef)label, authRef, YES, (CFErrorRef *)error);
}
AuthorizationFree(authRef, kAuthorizationFlagDefaults);
return result;
}
I have experimented with the authItem changing to kSMRightModifySystemDaemons from kSMRightBlessPrivilegedHelper but all this did was change the dialogue to display 'Add' instead of 'Install'
Would greatly appreciate some assistance here...
I haven't used this before but found your question interesting so I did a little reading of Apple's documentation and based on that I wonder if setting up the environment with a kAuthorizationEnvironmentPrompt would do what you want?
From AuthorizationTags.h:
The name of the AuthorizationItem that should be passed into the environment
when specifying a invocation specific additional text. The value should be a
localized UTF8 string.
You'd create an AuthorizationItem with this and then an AuthorizationItemSet containing that, and then pass the set into the AuthorizationCreate call for the environment: parameter.
I'd try that.
The other idea I had reading the documentation was to have a command line tool that does the remove and authorize the execution of the command line tool ("SomethingSomethingHelper") which might be less confusing to the user (so using AuthorizationExecuteWithPrivileges or kAuthorizationRightExecute or whatever).