I'm trying to make a Mac OS X application that asks the user for a directory. I'm using an NSOpenPanel that gets triggered when the user presses a "Browse" button.
The problem is, [NSOpenPanel filenames] was deprecated, so now I'm using the URLs function. I want to parse out the stuff that's url related to just get a normal file path. So I tried fileName = [fileName stringByReplacingOccurrencesOfString:#"%%20" withString:#" "];, but that gave me an error:
-[NSURL stringByReplacingOccurrencesOfString:withString:]: unrecognized selector sent to instance 0x100521fa0
Here's the entire method:
- (void) browse:(id)sender
{
int i; // Loop counter.
// Create the File Open Dialog class.
NSOpenPanel* openDlg = [NSOpenPanel openPanel];
// Enable the selection of files in the dialog.
[openDlg setCanChooseFiles:NO];
// Enable the selection of directories in the dialog.
[openDlg setCanChooseDirectories:YES];
// Display the dialog. If the OK button was pressed,
// process the files.
if ( [openDlg runModal] == NSOKButton )
{
// Get an array containing the full filenames of all
// files and directories selected.
NSArray* files = [openDlg URLs];
// Loop through all the files and process them.
for( i = 0; i < [files count]; i++ )
{
NSString* fileName = (NSString*)[files objectAtIndex:i];
NSLog(#"%#", fileName);
// Do something with the filename.
fileName = [fileName stringByReplacingOccurrencesOfString:#"%%20" withString:#" "];
NSLog(#"%#", fileName);
NSLog(#"Foo");
[oldJarLocation setStringValue:fileName];
[self preparePopUpButton];
}
}
}
Interestingly enough, "Foo" never gets outputted to that console. It's like the method aborts at the stringByReplacigOccurencesOfString line.
If I remove that one line, the app will run and fill my text box with the string, just in URL form, which I don't want.
Your problem is that the NSArray returned by [NSOpenPanel URLs] contains NSURL objects, not NSString objects. You're doing the following cast:
NSString* fileName = (NSString*)[files objectAtIndex:i];
Since NSArray returns an id, there isn't any compile-time checking to make sure your cast makes sense, but you do get a runtime error when you try to send an NSString selector to what is actually an NSURL.
You could convert the NSURL objects to NSString and use your code mostly as-is, but there's no need for you to handle the URL decoding yourself. NSURL already has a method for retrieving the path portion which also undoes percent-encoding: path.
NSString *filePath = [yourUrl path];
Even if your code was dealing with just a percent-encoded NSString, theres stringByReplacingPercentEscapesUsingEncoding:.
Related
There's a similar question for iOS, but I found that the proprosed solutions do not work on macOS in all cases.
On a Mac, there are many possible Trash folders:
/.Trashes
~/.Trash
~/Library/Mobile Documents/com~apple~CloudDocs/.Trash – this one is from iCloud
/Users/xxx/.Trash – any other user's trash
/Volumes/xxx/.Trashes
This code should work but doesn't for the case of the iCloud trash:
NSURL *theURL = ...;
NSURLRelationship relationship = NSURLRelationshipOther;
NSError *error = nil;
[NSFileManager.defaultManager
getRelationship: &relationship
ofDirectory: NSTrashDirectory
inDomain: 0
toItemAtURL: theURL
error: &error];
BOOL insideTrash = !error && (relationship == NSURLRelationshipContains);
If the URL points to any iCloud folder (including the Trash folder shown above), I get this error:
Error Domain=NSCocoaErrorDomain Code=3328
"The requested operation couldn’t be completed because the feature is not supported."
Curiously, even the header file of "NSFileManager" in the 10.15 SDK suggests to use this same code:
/* trashItemAtURL:resultingItemURL:error: [...]
To easily discover if an item is in the Trash, you may use
[fileManager getRelationship:&result ofDirectory:NSTrashDirectory
inDomain:0 toItemAtURL:url error:&error]
&& result == NSURLRelationshipContains.
*/
There also seems to be an issue with trashItemAtURL: on iCloud-synched folders.
So, how do I solve this? If the Finder can detect the iCloud trash, I should be, too.
(Note: The app I use for testing this is not even sandboxed)
More findings: Fails with dead symlinks, too
The officially suggested method of using getRelationship: also fails with an error if the url points to a symlink whose target doesn't exist.
So, basically, this function is quite broken (verified in 10.13.6, 10.15.7 and 11.0.1).
Here's code to demonstrate the bug, which I've filed with Apple under FB8890518:
#import <Foundation/Foundation.h>
static void testSymlink (NSString* symlinkName, NSString* symlinkTarget)
{
NSString *path = [[NSString stringWithFormat:#"~/.Trash/%#", symlinkName] stringByExpandingTildeInPath];
NSURL *url = [NSURL fileURLWithPath:path];
symlink (symlinkTarget.UTF8String, path.UTF8String);
NSLog(#"created symlink at <%#> pointing to <%#>", url.path, symlinkTarget);
NSURLRelationship relationship = -1;
NSError *error = nil;
[NSFileManager.defaultManager getRelationship:&relationship ofDirectory:NSTrashDirectory inDomain:0 toItemAtURL:url error:&error];
NSString *rel = #"undetermined";
if (relationship == 0) rel = #"NSURLRelationshipContains";
if (relationship == 1) rel = #"NSURLRelationshipSame";
if (relationship == 2) rel = #"NSURLRelationshipOther";
NSLog(#"result:\n relationship: %#\n error: %#", rel, error);
}
int main(int argc, const char * argv[])
{
#autoreleasepool {
testSymlink (#"validSymlink", #"/System");
testSymlink (#"brokenSymlink", #"/nonexisting_file");
}
return 0;
}
With the realization that [NSFileManager getRelationship:] even fails for broken symlinks, I conclude that this is a bug in macOS that's been existing undetected for years.
I came up with the following work-around:
Use the getRelationship: operation, then check the returned error first:
If there's no error, then check if relationship == NSURLRelationshipContains, and use that as my result.
Else, in case of any error, check whether the path contains "/.Trash/" or "/.Trashes/" - if so, assume the item is inside the Trash folder.
NSURL *theURL = ...;
NSURLRelationship relationship = NSURLRelationshipOther;
NSError *error = nil;
[NSFileManager.defaultManager
getRelationship: &relationship
ofDirectory: NSTrashDirectory
inDomain: 0
toItemAtURL: theURL
error: &error];
BOOL insideTrash = !error && (relationship == NSURLRelationshipContains)
|| error && (
[theURL.path containsString:#"/.Trash/"]
|| [theURL.path containsString:#"/.Trashes/"]
)
);
This question already has an answer here:
Use NSOpenpanel and NSfilemanager to find contents of directory
(1 answer)
Closed 10 years ago.
I have a button in an app that when clicked should cause a dialog box to open. The user then chooses a folder, clicks OK, and then the app displays the number of PDF files in that folder.
I have the following code implemented below. How do I scan a folder for the amount of PDF files in it?
- (IBAction)selectPathButton:(NSButton *)sender {
// Loop counter.
int i;
// Create a File Open Dialog class.
NSOpenPanel* openDlg = [NSOpenPanel openPanel];
// Set array of file types
NSArray *fileTypesArray;
fileTypesArray = [NSArray arrayWithObjects:#"pdf", nil];
// Enable options in the dialog.
[openDlg setCanChooseFiles:YES];
[openDlg setAllowedFileTypes:fileTypesArray];
[openDlg setAllowsMultipleSelection:TRUE];
// Display the dialog box. If the OK pressed,
// process the files.
if ( [openDlg runModal] == NSOKButton ) {
// Gets list of all files selected
NSArray *files = [openDlg URLs];
// Loop through the files and process them.
for( i = 0; i < [files count]; i++ ) {
}
NSInteger payCount = [files count];
self.payStubCountLabel.stringValue = [NSString stringWithFormat:#"%ld", (long)payCount];
}
}
get files and directories under path
[NSFileManager]- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
get files and directories under path and subpath
[NSFileManager]- (NSArray *)subpathsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
then filter out pdf files.
NSMutableArray *pdfFiles = [NSMutableArray array];
for(NSString *fileName in files) {
NSString *fileExt = [fileName pathExtension];
if([fileExt compare:#"pdf" options:NSCaseInsensitiveSearch] == NSOrderSame) {
[pdfFiles addObject:fileName];
}
}
now what you want is in pdfFiles.
NSUInteger pdfFilesCount = [pdfFiles count];
if you only want count of pdf files, just using a variable with forin loop.
I am trying to use NSFileManager copyItemAtURL:toURL:error: to move a UNIX executable file (a command line program) from one directory to another but I always get an error that says the URL type is unsupported. I assume this is because without an extension on the file it is being viewed as a directory but I'm not sure. Is it possible to move this type of file with NSFileManager?
Edit:
Here is my code
#define SAVE_DIR [#"~/Library/Prog" stringByExpandingTildeInPath]
#define PROG_PATH [SAVE_DIR stringByAppendingString:#"/ProgCom"]
#define RESOURCES [[NSBundle mainBundle] resourcePath]
#define LOCAL_PROG [RESOURCES stringByAppendingString:#"/ProgCom"]
-(void)moveProg
{
NSError *error = nil;
NSURL *fromURL = [NSURL URLWithString:LOCAL_PROG];
NSURL *toURL = [NSURL URLWithString:PROG_PATH];
NSLog(#"%#", [fromURL path]);
NSLog(#"%#", [toURL path]);
if ([fMan fileExistsAtPath:[fromURL path]]) {
[fMan copyItemAtURL:fromURL
toURL:toURL
error:&error];
if (error)
[NSApp presentError:error];
}
}
The error I receive:
The file couldn't be opened because the specified URL type isn't supported.
And finally what gets logged:
fromURL = /Users/Nick/Library/Developer/Xcode/DerivedData/Prog-dpnblqaraeuecyadjgizbinfrtcm/Build/Products/Debug/Prog.app/Contents/Resources/ProgCom
toURL = /Users/Nick/Library/Prog/ProgCom
The problem is you're using +[NSURL URLWithString:]. This is producing an invalid URL, since you're not actually giving it one. What you want is +[NSURL fileURLWithPath:], which will produce a file:///Users/Nick/... URL.
Is it possible to predefine constant or variable for an AppleScript in cocoa application?
in other words is the function "addConstantToAppleScript" (used in the following code) definable?
addConstantToAppleScript("myText", "Hello!");
char *src = "display dialog myText";
NSString *scriptSource = [NSString stringWithCString:src];
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:scriptSource];
NSDictionary *scriptError = [[NSDictionary alloc] init];
[appleScript executeAndReturnError:scriptError];
thanks.
If you want to prepend an NSDictionary of key/value pairs to the beginning of an NSString containing AppleScript you could use something like the following function. Personally I would do this as a category on NSString but you have asked for a function.
NSString *addConstantsToAppleScript(NSString *script, NSDictionary *constants) {
NSMutableString *constantsScript = [NSMutableString string];
for(NSString *name in constants) {
[constantsScript appendFormat:#"set %# to \"%#\"\n", name, [constants objectForKey:name]];
}
return [NSString stringWithFormat:#"%#%#", constantsScript, script];
}
This function converts the key/value pairs to AppleScript statements of the form set <key> to "<value>". These statements are then added to the front of the supplied script string. The resulting script string is then returned.
You would use the above function as follows:
// Create a dictionary with two entries:
// myText = Hello\rWorld!
// Foo = Bar
NSDictionary *constants = [[NSDictionary alloc ] initWithObjectsAndKeys:#"Hello\rWorld!", #"myText", #"Bar", #"Foo", nil];
// The AppleScript to have the constants prepended to
NSString *script = #"tell application \"Finder\" to display dialog myText";
// Add the constants to the beginning of the script
NSString *sourceScript = addConstantsToAppleScript(script, constants);
// sourceScript now equals
// set Foo to "Bar"
// set myText to "Hello\rWorld!"
// tell application "Finder" to display dialog myText
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:sourceScript];
I'm using an OpenPanel to get a file path URL. This works:
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
NSURL *pathToFile = nil;
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
This doesn't, resulting in an 'assignment of read-only variable' error:
NSURL *pathToFile = nil;
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
return pathToFile;
In general, any attempt to extract pathToFile from the context of oPanel has failed. This isn't such a big deal for small situations, but as my code grows, I'm forced to stuff everything -- XML parsing, core data, etc -- inside an inappropriate region. What can I do to extract pathToFile?
Thanks.
This doesn't, resulting in an 'assignment of read-only variable' error:
NSURL *pathToFile = nil;
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
return pathToFile;
Yes, because you're trying to assign to the copy of the pathToFile variable that gets made when the block is created. You're not assigning to the original pathToFile variable that you declared outside the block.
You could use the __block keyword to let the block assign to this variable, but I don't think this will help because beginSheetModalForWindow:completionHandler: doesn't block. (The documentation doesn't mention this, but there's no reason for the method to block, and you can verify with logging that it doesn't.) The message returns immediately, while the panel is still running.
So, you're trying to have your completion-handler block assign to a local variable, but your method in which you declared the local variable will probably have returned by the time block runs, so it won't be able to work with the value that the block left will leave in the variable.
Whatever you do with pathToFile should be either in the block itself, or in a method (taking an NSURL * argument) that the block can call.
you can also runModal after you begin the sheet you just need to make sure you end the sheet later. This way you don't have to bend to apple's will, it isn't deprecated and it should still work perfectly.
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[openPanel beginSheetModalForWindow:window completionHandler:nil];
NSInteger result = [openPanel runModal];
NSURL *url = nil;
if (result == NSFileHandlingPanelOKButton)
{
url = [openPanel URL];
}
[NSApp endSheet:openPanel];
It seems a little bit like black magic coding but it does work.