It's been a year since I last played with Cocoa and it seems a lot has changed.
I am trying to run an open dialog and retrieve the file path. This used to be very simple but now...
The code is:
-(NSString *)getFileName{
NSOpenPanel* panel = [NSOpenPanel openPanel];
__block NSString *returnedFileName;
// This method displays the panel and returns immediately.
// The completion handler is called when the user selects an
// item or cancels the panel.
[panel beginWithCompletionHandler:^(NSInteger result){
if (result == NSFileHandlingPanelOKButton) {
NSURL* theDoc = [[panel URLs] objectAtIndex:0];
// Open the document.
returnedFileName = [theDoc absoluteString];
}
}];
return returnedFileName;
}
-(IBAction)openAFile:(id)sender{
NSLog(#"openFile Pressed");
NSString* fileName = [self getFileName];
NSLog(#"The file is: %#", fileName);
}
(The indentation has been screwed up in the post but it's correct in the code)
My problem is that the final NSLog statement is being executed as soon as the open dialog opens and not waiting until the dialog closes. That leaves the fileName variable null which is what the final NSLog reports.
What is causing this?
Thanks.
There is a similar question to yours:
How do I make my program wait for NSOpenPanel to close?
Maybe
[openPanel runModal]
helps you. It waits until the user closes the panel
The stuff I had written a year ago used runModal so on Christoph's advice I went back to that.
It would appear that the beginWithCompletionHandler block is unnecessary, at least in this case. Removing it also had the advantage of removing the necessity to use the __block identifier.
The following now works as required
-(NSString *)getFileName{
NSOpenPanel* panel = [NSOpenPanel openPanel];
NSString *returnedFileName;
// This method displays the panel and returns immediately.
// The completion handler is called when the user selects an
// item or cancels the panel.
if ([panel runModal] == NSModalResponseOK) {
NSURL* theDoc = [[panel URLs] objectAtIndex:0];
// Open the document.
returnedFileName = [theDoc absoluteString];
}
return returnedFileName;
}
And well done Apple for deprecating the obvious and easy and replacing it with increased complexity.
Related
So I'm new to Cocoa and Objective-C programming, but I've read enough to be able to modify this one program I'm working on almost completely to my satisfaction...with the exception of one particular use case.
The program lets a user manually specify a path for where they want the files downloaded to reside, which is then saved into user defaults dictionary through binding the label's value to user defaults controller. The label's stringValue is then updated through the IBAction attached to the 'Open in Finder' button (by modifying the label outlet's stringValue inside the Push button's IBAction) which gets triggered whenever the user clicks the button and chooses a new path.
My problem is that when it comes time to download a file, if the path that the user has chosen no longer exists or is not valid, it will default to the user's desktop. But when this is the case, I do not see the desktop path being reflected in the label. This makes sense too, because there is no code to directly modify the label's stringValue in that case (no access to the label's outlet from that file). I've been trying to come up with a solution to fix this little thing that's been bugging me, but I haven't found a solution that's worked yet. Any advice or tips would be greatly welcome! I've included snippets of what the relevant pieces of my code look like below.
"PreferencesController.m"
- (IBAction)onOpenDownloadsPath:(id)sender {
NSOpenPanel* panel = [NSOpenPanel openPanel];
[panel setCanChooseFiles:NO];
[panel setCanChooseDirectories:YES];
[panel setAllowsMultipleSelection:NO];
[panel beginWithCompletionHandler:^(NSInteger result){
if (result == NSFileHandlingPanelOKButton) {
NSURL* theDir = [[panel URLs] objectAtIndex:0];
NSString* thePath = theDir.path;
[Preferences setDownloadsPath:thePath];
self->_labelDisplay.stringValue = [Preferences getDownloadsPath];
}
}];}
"Preferences.m"
+ (void)setDownloadsPath:(NSString *)value {
NSUserDefaults* ud = [NSUserDefaults standardUserDefaults];
[ud setObject:value forKey:#"Preferences.downloadsPath"];
}
+ (NSString*)getDownloadsPath {
NSUserDefaults* ud = [NSUserDefaults standardUserDefaults];
return [ud stringForKey:#"Preferences.downloadsPath"];
}
Solved. I used an NSNotification observer/listener to listen for when the set function would get called, then updated the stringValue in the label accordingly.
I received a warning from XCode during the execution of my program :
2016-01-21 03:19:26.468 IsoMetadonnees[1975:303] An instance 0x1004eefd0 of class NSVBOpenPanel was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:
<NSKeyValueObservationInfo 0x608000444710> (
<NSKeyValueObservance 0x6080000d5310: Observer: 0x100592cf0, Key path: level, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x6080004486a0>
)
The problem occurs while the application presents an NSOpenPanel to select some files that will be asynchronously loaded. The application does not crash and file are correctly loaded...
I don't create any value observer, so I imagine that the observer is created by NSOpenPanel, but I don't know any procedure to remove observer that I have not created...
Despite of this warning, I have made multiple loads without notice any crash. I use my application since many years without any problems, but I recently switch to ARC; may be the problem appeared (or is detected) at this time.
Here is a simplified version of my code :
- (IBAction)ajoutFichier:(id)sender {
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
// Here some configurations of openPanel
if ([openPanel runModal] == NSOKButton) {
tmp_listeURLFichiers = [openPanel URLs];
}
//[openPanel close]; // I add this code unsuccessfully
openPanel = nil; // I add this code unsuccessfully
// I call a task in back ground to load my files
if ((tmp_listeURLFichiers != nil) && ([tmp_listeURLFichiers count]>0))
[self performSelectorInBackground:#selector(ajouteListeFichiers:) withObject:tmp_listeURLFichiers];
}
// Load files in background
-(BOOL) ajouteListeFichiers:(NSArray *)listeDesFichierAAjouter {
#autoreleasepool {
// Some stuff to show a progress bar
// Loop to load selected files
for (id tmpCheminVersMonImage in listeDesFichierAAjouter) {
// Load files
}
} // <========== THE WARNING OCCURS AT THIS POINT, WHEN autoreleasepool is cleaned
return (YES);
}
I try adding
[openPanel close];
and
openPanel = nil;
to force releasing openPanel from memory (and thus observers) before starting background task, but that doesn't change anything...
Do you have any idea ?
Thank you for your help !
I could fix the problem using the following trick :
I declare a variable in my view controller :
__strong NSOpenPanel *prgOpenPanel;
Then I use it in my code
//NSOpenPanel *prgOpenPanel = [NSOpenPanel openPanel];
self.prgOpenPanel = nil;
self.prgOpenPanel = [NSOpenPanel openPanel];
// Here some configurations of openPanel
if ([prgOpenPanel runModal] == NSOKButton) {
tmp_listeURLFichiers = [prgOpenPanel URLs];
if ((tmp_listeURLFichiers != nil) && ([tmp_listeURLFichiers count]>0))
[self performSelectorInBackground:#selector(ajouteListeFichiers:) withObject:tmp_listeURLFichiers];
}
No more warnings !
So I currently have this bit of code to get a dir:
-(NSString *)get {
NSOpenPanel *gitDir = [NSOpenPanel openPanel];
NSInteger *ger = [gitDir runModalForTypes:nil];
NSString *Directory = [gitDir directory];
return Directory;
}
But it gives me errors and says it has now been depreciated.
Is there a better way for OSX 10.7?
This is a supplement to sosborn's answer, not a replacement.
runModalForTypes: is deprecated, and the correct replacement is runModal (or setAllowedFileTypes: followed by runModal, but in this case you're passing nil for the types).
directory is also deprecated, and the correct replacement is directoryURL. (If you actually must return an NSString path rather than an NSURL, just return [[gitDir directoryURL] path].)
However, what you're doing is asking the user to select a file, and then returning the directory that file is in, when what you really want is to ask the user to select a directory. To do that, you want to call setCanChooseFiles to NO and setCanChooseDirectories to YES, and then call URLs to get the directory the user selected.
Also, you're ignoring the result of runModal (or runModalForTypes:). I'm sure the compiler is warning you about the unused variable "ger", and you shouldn't just ignore warnings. If the user cancels the panel, you're going to treat that as clicking OK, and select whatever directory she happened to be in when she canceled.
Here's a better implementation, which will return the URL of the selected directory, or nil if the user canceled (or somehow managed to not select anything). Again, if you need an NSString, just add a "path" call to the return statement:
-(NSURL *)get {
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
if ([panel runModal] != NSModalResponseOK) return nil;
return [[panel URLs] lastObject];
}
Whenever you see a deprecation warning you should go straight to the official documentation. In this case, the docs for NSOpenPanel say:
runModalForTypes: Displays the panel and begins a modal event loop
that is terminated when the user clicks either OK or Cancel.
(Deprecated in Mac OS X v10.6. Use runModal instead. You can set
fileTypes using setAllowedFileTypes:.)
I adapted the code by abarnert for swift. tx for the code just what I needed.
func askUserForDirectory() -> NSURL? {
let myPanel:NSOpenPanel = NSOpenPanel()
myPanel.allowsMultipleSelection = false
myPanel.canChooseDirectories = true
myPanel.canChooseFiles = false
if ( myPanel.runModal() != NSFileHandlingPanelOKButton ) {
return nil
}
return myPanel.URLs[0] as? NSURL
}
I'm having a problem saving a string file with NSSavePanel after sandboxing the app for the Mac App Store. I set com.apple.security.files.user-selected.read-write to YES and the NSOpenPanel is working as it should.
When I try to save a new file, though, it seems that everything is working fine but then there is no saved file where it should be....
This is the code I am using to save the file:
NSSavePanel *save = [NSSavePanel savePanel];
long int result = [save runModal];
if (result == NSOKButton)
{
NSString *selectedFile = [save filename];
NSString *fileName = [[NSString alloc] initWithFormat:#"%#.dat", selectedFile];
NSString *arrayCompleto = [[NSString alloc]initWithFormat:#"bla bla bla"];
[arrayCompleto writeToFile:fileName
atomically:NO
encoding:NSUTF8StringEncoding
error:nil];
}
First of all, the -[NSSavePanel filename] selector has been deprecated. Use -[NSSavePanel URL] instead. Second, the way that the -[NSString writeToFile:atomically:encoding:error] tells you what you're doing wrong is with the error:(NSError**) argument.
You should also handle errors for file I/O in particular, because even if your code is 100% correct, there still might be errors on the user's system (insufficient privileges, etc.) and presenting the error to the user will allow them to see it failed (and have some idea why). Handling the error in code will also allow your app to recover. For instance, if you tried to read in the file below the code you pasted (after writing it to disk), but the user tried writing it to a network share they didn't have access to, your app might crash. If you know the write failed, you can proceed accordingly (perhaps prompting for a different save location).
In this case, though, I believe the following line is your problem:
NSString *fileName = [[NSString alloc] initWithFormat:#"%#.dat", selectedFile];
When your app is sandboxed, the user needs to give you permission for either a specific file or a specific directory through the open/save panels to bring them into your sandbox. What you're doing is taking the file the user gave you permission to write and saying "that's great, but I want to save a different file", which violates the sandbox. What you should do instead is set the extension in the Save Panel. The complete fixed solution would be:
NSSavePanel *save = [NSSavePanel savePanel];
[save setAllowedFileTypes:[NSArray arrayWithObject:#"dat"]];
[save setAllowsOtherFileTypes:NO];
NSInteger result = [save runModal];
if (result == NSOKButton)
{
NSString *selectedFile = [[save URL] path];
NSString *arrayCompleto = #"bla bla bla";
NSError *error = nil;
[arrayCompleto writeToFile:selectedFile
atomically:NO
encoding:NSUTF8StringEncoding
error:&error];
}
if (error) {
// This is one way to handle the error, as an example
[NSApp presentError:error];
}
If in the future something else is wrong, you can check the value of error at runtime. While debugging, set a breakpoint inside the if (error) statement to check error object's value (do a po error in Xcode's debugger). That should help you figure out what's wrong.
I'm converting my Lion app to use the App Sandbox. I'm trying to make use of the security-scoped bookmarks feature introduced in 10.7.3 to allow persistent access to a folder. The code I have below returns a nil bookmark, and produces the following log message: XPC couldn't look up the Mach service for scoped bookmarks agent.
I set the User Selected File Access entitlement to Read/Write Access, and also tried with and without the surrounding ..AccessingSecurityScopedResource calls.
I think I'm doing everything right according to the documentation, so I'd appreciate any pointers. The code was working to retrieve a plain URL before I began sandboxing the app.
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[openPanel setCanChooseFiles:NO];
[openPanel setCanChooseDirectories:YES];
[openPanel setAllowsMultipleSelection:NO];
NSInteger result = [openPanel runModal];
if( result == NSFileHandlingPanelCancelButton ) {
return;
}
NSArray *urls = [openPanel URLs];
if( urls != nil && [urls count] == 1 ) {
NSURL *url = [urls objectAtIndex:0];
NSData *bookmark = nil;
NSError *error = nil;
bookmark = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil // Make it app-scoped
error:&error];
if (error) {
NSLog(#"Error creating bookmark for URL (%#): %#", url, error);
[NSApp presentError:error];
}
NSLog(#"bookmark: %#", bookmark);
}
Update (x3)
Now that I got it working, I can verify that the calls to -startAccessingSecurityScopedResource and -stopAccessingSecurityScopedResource are not necessary in the code above, since the Powerbox grants access to the resource after the user selects it in the NSOpenPanel.
If you're creating a bookmark from another security-scoped URL, such as making a document-scoped bookmark from an app-scoped bookmark created in another app session, then you need to get access to the file first.
It turns out I was missing a crucial entitlement, not listed in the UI, but listed in the documentation:
com.apple.security.files.bookmarks.app-scope
Update 12/18/2018
According to this Twitter thread, this entitlement may not be required anymore. Thanks #pkamb for alerting me to this.