I'm making an app and I would like to check if my computer has already installed an app (Example.app, com.example.test) and be able to send the app to the Mac App Store.
I tried several things, like:
NSString *script = #"try\r tell application \"Finder\" to get application file id \"com.example.test\"\r set appExists to true\r on error\r set appExists to false\r end try";
NSAppleEventDescriptor *result = [self executeScript:script];
return [result booleanValue];
This works perfectly, but I've been reading that Apple doesn't allow temporary exceptions for Finder in the entitlements file in order to keep the app secure.
I also tried something similar but avoiding the use of Finder:
NSString *script = #"set appID to id of application \"Example\"\r set msg to exists application id appID\r tell application \"Example\" to quit\r return msg";
NSAppleEventDescriptor *result = [self executeScript:script];
return [result booleanValue];
This works only if the user has the app, if not, it will prompt a dialog asking for the app location. (and shows the Example icon in the dock for a few milliseconds)
I also been trying some more hacky solutions like:
NSTask *task = [NSTask new];
[task setLaunchPath:#"/bin/bash"];
[task setArguments:#[#"if ls /Applications/Example.app >/dev/null 2>&1; then echo FOUND; else echo NOT FOUND; fi"]];
[task launch];
[task waitUntilExit];
int status = [task terminationStatus];
if (status == 0)
NSLog(#"Task succeeded.");
else
NSLog(#"Task failed.");
But the task always fails, I think command line stuff will never work (if so, nonsense sandboxing).
I've been thinking to put a button (checkbox), that prompts a dialog in order to select the path of the App, and check if the app name is equal to Example, if it is, check the checkbox, and uncheck if it's not. But I don't know how to prompt that dialog. (And I would like to avoid this solution if it's possible)
My questions are:
Is it possible to know if the app exists (without open it) following the rules of sandboxing?
Could I set Finder as a temporary exception and Apple will approve it? (Explaining what's the intention)
Thanks
Have you tried the following using NSWorkspace?:
NSURL *appURL = [[NSWorkspace sharedWorkspace]
URLForApplicationWithBundleIdentifier:#"com.example.test"];
If the result is nil, you can consider that as Launch Services not "knowing about" the application, wherever it might be located, otherwise it will return the NSURL. (Launch Services, part of the CoreServices umbrella framework, is the framework that handles dealing with applications and document bindings). Using Launch Services is usually a better approach than checking at a specific path, since app bundles can be moved around the file system but still be present.
The AppleScript code you posted is probably about the equivalent of having the Finder call the code shown above: it's simply calling into Launch Services to find an app with that bundle identifier.
I think (according to the Automation conferences at the WWDC in 2012) you should use the NSUserScriptTask. To be exactly you need the subclass NSUserAppleScriptTask and and you're be able to fire an user defined AppleScript outside the sandbox. What it will do is executing the script using an XPC-service and where it run the script using the command line utility osascript. Your application is sandboxed so the only allowed place where the script may be installed is at NSApplicationScriptsDirectory which is located at ~/Library/Application Scripts/<bundle-id>/
So be aware, the application context it not self targeted by default, so when you need to script your application you need to explicitly tell your application to perform the action. So it doesn't run like NSAppleScript which will be executed by the Application itself, NSUserScriptTask and it's subclasses will run outside the application (sandbox).
Related
In macOS apps, if you need to get the path of your app or any resources in its bundle, you can of course use the mainBundle static method of NSBundle:
NSBundle *bundle = [NSBundle mainBundle];
NSString *appPath = [bundle bundlePath];
NSString *resourceFolderPath = [bundle resourcePath];
... etc ...
However, if the user moves your app's bundle to a different location on disk while the app is running, then this method will not work. All of the above functions will still return the bundle's old paths from before the user moved it.
How can you get the current path of your bundle or the path of any resource inside of it regardless of whether the user has moved it on disk?
So far, I've tried using NSRunningApplication to get its current location like so:
NSRunningApplication *thisApp = [NSRunningApplication runningApplicationWithProcessIdentifier:getpid()];
NSLog(#"bundle URL: %#", [thisApp bundleURL]);
...but that too returns the old path of the bundle!
Moving an application that's already running typically isn't supported. If that's a scenario you really need to support then you can do the following:
When the app is first launched, get the app's current path as you would normally and then create an NSURL bookmark for that path.
Whenever you need to know the app's current path you can resolve the NSURL bookmark you created at launch. The bookmark will resolve to app's current path, even if the app has been moved.
More info on NSURL bookmarks is available here: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/AccessingFilesandDirectories/AccessingFilesandDirectories.html#//apple_ref/doc/uid/TP40010672-CH3-SW10
While macOS doesn't care if you move documents around as they're open, it does care if you move applications around. This is a long-recognized issue in macOS development.
Instead of trying to fight it, app developers tend to either not care, or ask users if they want to move the application to the Applications directory at launch (since this is the most common move destination for apps). LetsMove is a project that demonstrates how you would do that.
How do you change the desktop picture in cocoa/objective-c? I've tried using defaults but had many errors.
NSArray *args=[NSArray arrayWithObjects:#"write",#"com.apple.desktop", #"Background", #"'{default = {ImageFilePath = \"~/desktop.jpg\";};}'", nil];
NSTask *deskTask=[[NSTask alloc] init];
[deskTask setArguments: args];
[deskTask setLaunchPath:#"/usr/bin/defaults"];
[deskTask launch];
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:#"com.apple.desktop" object:#"BackgroundChanged"];
The command works successfully in terminal. I don't need anyone to tell me exactly what to do but I would like some insight.
EDIT: My OS is 10.4.11
I think the canonical way is to use scripting with System Events. The Applescript version is something like:
tell application "System Events"
tell current desktop
set picture to (whatever)
end tell
end tell
You can use the Scripting Bridge to do it from Objective-C.
When you use a tilde-compressed path in the shell, the shell expands the tilde for you, so when you run the command in the shell, you set the desktop-picture path to the expanded path (/path/to/desktop.jpg). There is no shell at work when you use NSTask, so the code you showed sets it to the tilde-compressed path. Very few things expect such a path; they don't expand the tilde, so it doesn't work.
To make that code work, you need to expand the tilde itself using the appropriate method of the NSString object, or construct the path by appending to the path returned by NSHomeDirectory().
That said, talking to System Events as Chuck suggested is a much better way to implement this. Note his comment telling you how to do it without requiring Leopard.
I found there are at least three ways to launch an app with Mac OS X from an application.
NSTask. I can give parameters, but it seems that it's not for an Cocoa App, but an UNIX style binary.
system function (system()) just the same way as C does. I don't know the reason why but it seems that nobody recommends this method.
NSWorkspace, but I can't find a way to pass parameters to this function.
Questions
Q1 : Is there any other way to launch an App (from an App) other than three methods?
Q2 : What's the pros and cons for each method?
Q3 : What's the preferable way for launching an App (from an App)?
Q4 : What's the preferable way for launching an App with parameters (from an App)?
Q5 : What's the preferable way to open a document (from an App)?
ADDED
NSWorkspace openFile:withApplication: : For running "TextMate README.txt", based on Roadmaster's answer and this code, I could make it as follows.
But, I can't give the parameters to the App.
NSString * path = #"/Users/smcho/Desktop/README.txt";
NSURL * fileURL = [NSURL fileURLWithPath: path];
NSWorkspace * ws = [NSWorkspace sharedWorkspace];
[ws openFile:[fileURL path] withApplication:#"TextMate"];
NSWorkspace launchApplicationAtURL:options: : It works with 10.6 or later, you can get an example from this question.
NSURL * bURL = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:#"com.macromates.textmate"];
NSWorkspace * ws = [NSWorkspace sharedWorkspace];
[ws launchApplicationAtURL:bURL options:NSWorkspaceLaunchDefault configuration:nil error:nil];
NSTask : This is the working code. I need to give the correct binary path, and it doesn't look like a Cocoa way, as it's for running binary, not bundle. Though, it's possible to give more parameters than just a file name.
[NSTask launchedTaskWithLaunchPath:#"/Applications/TextMate.app/Contents/MacOS/TextMate" arguments:[NSArray arrayWithObjects:#"hello.txt", nil]];
system() : With the shell, I could run "system(open -a ABC --args hello.txt)", just like I do with the command line. It seems that this is the easiest way to go.
In 10.6 and later, NSWorkspace has a method launchApplicationAtURL:options:configuration:error: that can be used to pass arguments to the app.
There are also Launch Services functions such as LSOpenItemsWithRole.
You could also send an AppleEvent to the Finder asking it to open something.
EDIT TO ADD: "best" is subjective, but I'd say if you can use NSWorkspace, use it. If you can't, e.g., you need to pass command-line parameters and you need to support Leopard, then use Launch Services.
By using Scripting Bridge, you can use the method activate to launch a cocoa app. See: Scripting Bridge.
could you tell me please - which object and which method can i use to fetch information about installed applications on OSX with objective c or macruby?
You can just call the Apple command-line tool system_profiler, e.g.
% system_profiler SPApplicationsDataType
For C or Objective-C you could use system() to do this.
For more info:
% man system
% man system_profiler
If you meant to list all of GUI apps, you can use Launch Services. For the list of functions, see the reference.
Do not manually list .app bundles! It's already done by the system, and you just have to query it.
There is a command-line program called lsregister at
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
which can be used to get the detailed dump of the Launch Service database. This tool is sometimes helpful,
but don't depend on that in a shipping program,
OS X doesn't really have a notion of "installing an app". OS X applications are self contained bundles that appear in the Finder as a single virtual file. There's no central registry, just a convention that applications are placed in /Applications or ~/Applications for a per user "install".
About the best you can do will be enumerate those directories for their contents, and every ".app" directory you find is an application. It will not be a exhaustive (I often run small apps from e.g. my Desktop if I've just downloaded them).
I moved in a following way:
execute command system_profiler SPApplicationsDataType -xml
from code. Key SPApplicationsDataType means that only application data is required. -xml means that I expect to see xml result for easier parsing.
parse result array
Here you can find nice example regarding command execution from code: Execute a terminal command from a Cocoa app
Total code example is looked like following:
NSStirng * commangString = #"system_profiler SPApplicationsDataType -xml";
NSData * resultData = [CommandLineTool runCommand:commangString];
NSArray * appArray = [PlistManager objectFromData:resultData];
// parse array of NSDictionaries here ....
// method from PlistManager
+ (id) objectFromData: (NSData *) data {
NSString *errorString = nil;
NSPropertyListFormat format;
id object = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListMutableContainers format:&format errorDescription:&errorString];
if (!errorString) {
return object;
} else {
NSLog(#"Error while plist to object conversion: %#", errorString);
return nil;
}
}
// runCommand method was used from post I mentioned before
Use Spotlight via NSMetadataQuery.
You can view the results you'll get using mdfind at the command line:
mdfind "kMDItemContentType == 'com.apple.application-bundle'"
Works like a charm and very very fast.
[[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:#"com.google.Chrome"];
This would return a path to Google Chrome app if it is installed, nil otherwise.
If you don't know the BundleID, there are two options to find it:
1) Open .plist file of the app by right-clicking the app icon and choosing Show Package Contents option.
Default path to Chrome .plist is /Applications/Google\ Chrome.app/Contents/Info.plist
2) Use lsregister alongside with grep.
Try typing the following into the Terminal app, you will find the BundleID there:
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump | grep -i chrome
As it is a unix-like OS there is no real way of telling what you have installed (unless of course you have control of the other app - then there is a lot of ways of doing this for obvious reasons).
Generally apps get put into /Applications or ~/Applications BUT you can run it from anywhere and not those folders (just like a unix machine)
AppleScript was too slow, so I tried ScriptingBridge to open System Preferences.app and set the current pane, which is also too slow. Is there a faster way to do it? properly
A more direct method than using the file system path is to use the appropriate resource URL for the preference pane with NSWorkspace as shown:
NSString *urlString = #"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
where the urlString was taken from a list of some of the possible URL strings https://macosxautomation.com/system-prefs-links.html
Use Launch Services or NSWorkspace to open the prefpane bundle. That's the programmatic version of the open(1) command.
No brainer:
system("open -a System\\ Preferences");
And to choose which Pane to open:
open /System/Library/PreferencePanes/Internet.prefPane
open /System/Library/PreferencePanes/DateAndTime.prefPane
...
Provided you found, with a little trial and error, the right file in /System/Library/PreferencePanes/ first.
I'm sure there's a more cocoa way to do this last trick, still... this one works with every language.
Also: you may want to check these paths
/Library/PreferencePanes/
~/Library/PreferencePanes/
...as that's where third party apps install their *.prefPane files
How exactly did you use the Scripting Bridge?
I tried with this code and I think it performs reasonably well:
SystemPreferencesApplication *SystemPreferences = [SBApplication applicationWithBundleIdentifier:#"com.apple.systempreferences"];
#try {
[SystemPreferences activate];
SystemPreferences.currentPane = [SystemPreferences.panes objectWithID:#"com.apple.preference.security"];
} #catch (NSException *exception) {
NSLog(#"%#", [exception description]);
}
Here is another option just for fun which is Cocoa, but not documented at all (and only works with system preference panes). You may use it to compare performances, but don't use it in production code.
id bezelServicesTask = [NSConnection rootProxyForConnectionWithRegisteredName:#"com.apple.BezelServices" host:nil];
[bezelServicesTask performSelector:#selector(launchSystemPreferences:) withObject:#"Security.prefPane"];