Fastest way to programmatically open prefpane? - objective-c

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"];

Related

Opening OS X Apple Maps with specific coordinates from Cocoa command line tool

I am trying to launch the native Maps app and show a specific coordinate in OS X from my command line tool.
I've found three options:
Use [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
Use the open command and pass a formatted URL and let the system figure it out.
Use NSTask to launch Maps.app with the URL.
I tried the first one:
NSString *url = [NSString stringWithFormat:#"http://maps.apple.com/?q=%#&ll=%f,%f", user[#"realName"], geo.latitude, geo.longitude];
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
(where it results in a perfectly valid string)
But it doesn't work. It just does nothing. Nothing opens, no errors, no warnings. Then I went to the second option. I couldn't figure out how to do the second, so I've turned up to NSTask. I've found this answer: https://stackoverflow.com/a/412573/811405 and I've passed this URL as argument: http://maps.apple.com/?q=Example&ll=12.345,23.456 but I'm getting:
How can I open maps with a specific point from a Cocoa command line tool?
Apparently, your URL string was not valid because it had a space in it. Therefore, [NSURL URLWithString:url] was returning nil.
Do not use string formatting to build URLs. Don't build potentially-invalid URL strings and then try to fix it by calling -stringByAddingPercentEscapesUsingEncoding:. You can't properly percent-escape encode whole URL strings. Each component has different valid/invalid characters that need to be escaped.
The best approach is to use NSURLComponents to build your URL from parts.
If you can't do that, use -stringByAddingPercentEncodingWithAllowedCharacters: on each component of the URL before concatenating them together. For the query component, you would use URLQueryAllowedCharacterSet.
Here's some documentation about Map Links that Apple provides.
And when I typed in http://maps.apple.com/?ll=12.345,23.456, the maps app opens up with a blank beige screen (because the coordinates appears to be in the middle of a desert in Sudan).
From the command line, I was able to do open "http://maps.apple.com/?q=Cupertino and that works great.

Code to check if an external app exists without breaking the sandbox

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).

Save in plist to a program directory

i need to save a .plist file NOT to documents, but to the core of program.
For example my program called "123" and if i save data, then send my app to my friend and he opens this app he could see saved data, no matter where he puts this program. I can't find solution to this problem, please help me.
I'm making mac app.
and i save plist with
[NSKeyedArchiver archiveRootObject:FBCover1.text=
[NSString stringWithFormat:#"%#",Cover1.attributedStringValue]
toFile:#"/Users/admin/FBCover1.plist"];
General answer:
If you're trying to do this on iPhone (you didn't tag this for iOS or MacOS), this isn't going to work as this will break your code signing.
If you're doing this on MacOS and you're using code signing, you'll have the same problem.
There may be places where you could save and share data, such as Game Center or DropBox or Box or some other cloud storage mechanism, but you'll need to pick up and make use of some additional API's or frameworks.
Specific answer just for you:
Instead of:
[NSKeyedArchiver archiveRootObject:FBCover1.text=[NSString stringWithFormat:#"%#",Cover1.attributedStringValue] toFile:#"/Users/admin/FBCover1.plist"];
which is big and ugly and I don't know what the heck it's doing, why not save your string this way?
NSString * stringToSave = [NSString stringWithFormat:#"%#",Cover1.attributedStringValue];
if(stringToSave)
{
NSError * error = nil;
BOOL success = [stringToSave writeToFile: #"/Users/admin/FBCovert1.txt" atomically: YES encoding: NSUTF8StringEncoding error: #error];
if(!success)
{
NSLog( #"error in saving - %#", [error localizedDescription]);
}
}
This saves the raw string into a file.
If you want to do it as a plist, then create a NSDictionary and save your string as the value with some appropriate key.
Preamble: this is an awful idea. What you should do is create a document-based application and pass your document backwards and forwards.
Literal answer:
You can use NSBundle to get the path of the resources folder within your application bundle with something like:
[[NSBundle mainBundle] resourcePath]
The resources folder is where application resources, such as plists, are meant to go. You're supposed to consider your application bundle as read-only in general but that's as good a choice as any if you want to hack away.

Comparing different 'the launching Cocoa app with parameters' methods

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.

How can I discover what apps are installed on OS X using Objective C or Macruby?

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)