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.
Related
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).
Is it possible to force the Xcode complier to verify that files referenced in code are valid?
There are multiple points in Cocoa development when you naturally reference a file programmatically via an NSString:
[UINib nibWithNibName:#"MyNib" bundle:nil];
[UIImage imageNamed:#"MyImage"];
[[UIViewController alloc] initWithNibName:#"MyNib" bundle:nil];
Is there any way at compile time to check is these file references are valid?
Often times after using above methods, I end up changing the name of the referenced file but forget to change the name in code. Everything complies without a problem and it is only when you happen to go to the portion of the app that accesses this file that the bug will reveal itself.
Is there another approach or technique that people use to avoid this sort of error?
Referencing a file name via a string feels very fragile.
Warning: This answer is mostly outdated. The general idea is fine but better solutions exist now (e.g. Image assets with a SwiftGen script to generate an enum).
Nibs usually have a class with the same name as the file, e.g.
[[MyViewController alloc] initWithNibName:NSStringFromClassName([MyViewController class]) bundle:nil];
I usually hide it into the controller's init method as [self class].
For image loading, compile-time checks are difficult. Help yourself with macros, first replace the loading method by a simple macro, e.g.
#define LOAD_IMAGE(__IMAGE_NAME__) [UIImage imageNamed:__IMAGE_NAME__]
First thing you should do is to put an assert into this macro and always check that the image was successfully loaded. It's not a compile-time check but it helps to find missing resources.
The second thing is to write a ruby/python/shell/(any scripting language) script that will search your source files for LOAD_IMAGE and check if the file (between parenthesis) exists. As a shell script, it will be very simple (e.g. using grep). You can add this script into your xcode project to be run when compiling.
Don't forget to check images referenced by xibs.
However, often you have to create the image name dynamically, e.g. NSString* imageName = [NSString stringWithFormat:#"image_%i", index]. There's no way how you can check this at compile time.
Also don't forget to do the reverse check - don't include image files which are not used anywhere.
AutoComplete for [UIImage imageNamed:] by Kent Sutherland.
This provides code completion support within Xcode - a brilliant piece of code. This is working for me in Xcode 4.6:
Currently this project does not have support for strings other than imageNamed:. To support those, I will try to write a compile time script. Or maybe I will become bold and try to extend Mr. Sutherland's spectacular work.
Xcode doesn't support this, but if this problem is really biting you then you could use the following hack:
Give every in-bundle file a unique prefix (e.g. app__)
When you add a file to your project, make sure you first rename it to add this prefix.
Your compile time (pre-distribution) check then has two parts: 1) Search through all .m files and enumerate strings that begin with the prefix. You shouldn't have to check if the string is quoted since your prefix is unique. 2) grep project.pbxproj for each string to check if it is included in the bundle.
With some effort, this process can be mostly automated and also optimized, but the above recipe ought to work.
here is a bash script that we use that lists all images on disk but NOT referenced in code.
https://gist.github.com/3750087
it would likely be easy to reverse this to check for non-exting images and xibs.
Anyways, the script should be a good starting point
I have an AppleScript that I am trying to convert to ScriptingBridge. Since my application is a C++/Obj-C application, ScriptingBridge is much easier to use and quite a bit faster (not to mention I hate dynamically building AppleScripts).
The AppleScript sends a message to Photoshop to open a file. The file parameter is sent as an alias, but ScriptingBridge imports the parameter as an id. I don't know what Obj-C object I should pass in?
I've tried passing an NSURL and an NSString (probably incorrectly :-P), but to no avail. Any suggestions on what I should be passing for the file alias?
The short answer is that you can't open documents in Photoshop with Scripting Bridge.
Apple's docs really spell it out like it is. All classes must have a container, which is a mutable array, that they need to be added to before they can be acted upon, as shown in the generated header...
#interface photoshopCS4Application : SBApplication
- (SBElementArray *) documents;
- (SBElementArray *) fonts;
- (SBElementArray *) notifiers;
... and that is the complete list of top-level containers available to us. The open command requires a photoshopCS4OpenOptions to be generated and populated. Because the API doesn't expose the array to store the newly created photoshopCS4OpenOptions object, we can't use a newly created photoshopCS4OpenOptions object. Therefore we can't make a target document and by extensions can't use the open command in Scripting Bridge. The same can be said of all the commands that require some kind of options object.
The only workaround that I have sorted out is to either open a document with native Applescript called from Cocoa or objc-appscript, and then parse the documents array looking for the one just opened. It's not ideal, but then neither is Scripting Bridge because it requires application developers write their scripting APIs in a very specific way that is not native to the OSA framework.
If your program is such that opening a Photoshop document can be executed outside your AppleScript script/Scripting Bridge code, Cocoa provides a method to open files with a specific application:
[[NSWorkspace sharedWorkspace] openFile:#"/Users/bavarious/Desktop/test.psd" withApplication:#"Adobe Photoshop CS4"];
or, if you want to use the default application that handles that file type, you can drop the application name altogether:
[[NSWorkspace sharedWorkspace] openFile:#"/Users/bavarious/Desktop/test.psd"];
Consider Appscript. http://appscript.sourceforge.net/
Here's the code using that:
APApplication *adobePhotoshopCs4 = [APApplication applicationWithName: #"Adobe Photoshop CS4"];
id result = [[adobePhotoshopCs4 open_] send];
(Note, I'm not a Cocoa programmer - I mainly use Appscript with Python but Appscript comes with ASTranslate which translates Applescript into Python, Ruby or Obj-C and that's the output - but I've found there are subtle mistakes in the past sometimes with the translator)
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.
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"];