Application Technologies • 49:08
Learn how to create an Automator Action using Objective C, Cocoa, and Cocoa Bindings in this hands-on session. You will learn everything you need to know to create your own Automator Actions using Cocoa.
Speaker: Kerry Hazelgren
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Good afternoon everybody. Would you please welcome Kerry Hazelgren, Senior Apple Script Automator Engineer, who will be giving your talk today. Good afternoon. Welcome to the last session before the beer bash. I hope we can get through this quickly, right? Today we're going to talk about writing Cocoa Automator Actions. As you see, my name's Kerry Hazelgren, as mentioned. Let's have a quick show of hands. How many people have attempted to or successfully written an Automator Action? All right, pretty good.
And how many people knew that the robot's name was Otto? Okay. It's Otto, O-T-T-O, not A-U-T-O. We're going to start to review what we're going to learn today. We'll go over the steps in the hands-on session. This is supposed to be a hands-on session, but I have to apologize. It's going to be more like a guided tour due to a glitch in the sample code. You'll be able to follow along with me, but won't be able to build any of the projects. I apologize about that.
That should be fixed shortly after the conference. You'll be able to download the corrected source code. Then we'll talk about a little background information on what Automator is and what it does and its pieces and parts, and why you should create Automator Actions, what's in it for you.
So what we're going to learn, we're going to talk about how to design actions, some of the guidelines to be followed when designing Automator Actions, how to create them, meaning physically or in Xcode, how to create a project, How to add your source code, how to design the UI, how to build it, install it, etc. How to create conversion actions. We'll have to talk about those and explain what those are, but we will go through and we'll actually create conversion action. And for those of you who put bugs in your code, we'll talk about how to debug actions as well.
So what is Automator? This is a quote from Matt Newberg. End users, I suspect, will quickly discover that Automator is the fulfillment of a dream. At long last, anyone can program the Mac. And that's really true. This is for everyone. Automator, as best as I can describe it, is workflow automation for the masses. It's for anyone to use. It's very simple. Just through a drag and drop interface. And it's flexible so that it can be tailored to your needs.
It's also extensible. As Steve mentioned in his keynote, there's something like 550 actions which are shipped so far, plus the 200 that were shipped with Tiger. And that's just in the first six weeks or so. And we're hoping that this conference will give it another boost. And there will be many more to come.
You're great actions. So workflows are what we create with Automator. Workflows are what we create with Automator. Workflows can be saved as individual applications. You don't need to have Automator running in order to run these workflows. They can be saved as plugins to the Finder, to the script menu, to the print panel, to other things as well. It can really tie in to Tiger.
So what is an Action? Focused functionality in a small package. It can have a large code base behind it, but the UI is small, and it presents a very focused, Very focused purpose. We can talk to -- Actions can talk to frameworks. Generally, Cocoa Actions will talk to frameworks. Or they can talk to applications. Generally, that would be AppleScript Actions talking to applications to get at their data models, object models, and get data out of them.
We support technologies such as Cocoa, AppleScript, and new in Xcode 2.1 is the Shell Scripting Action. So you can write with your favorite scripting language, whatever it be, or command line tool. Why should you create Automator Actions? Well, it can be very easy since these are small. They don't take a lot of effort into designing them. The tools you use are familiar, Xcode, Interface Builder. It can be fast because you can reuse your code. You can take the code that you've already worked on over the years, tested, debugged, etc., put it right into an action, and put a new UI on it.
Give users a new way to use your technology or your product, and then you've found a new market. You become part of their daily workflow, and you'll be cool. In the Mac OS X State of the Union speech, Scott Forstall went over his 10 best practices, and one of them was adopting new technologies.
Automator is a new technology. And users like that. So you can generate attention for your product as well. To mention BBEdit, which released an update the day before Tiger was released and came out with 19 Automator Actions. They got a lot of press for that. They're finding new users, people who don't normally use BBEdit possibly, but would use a BBEdit action in a workflow, and suddenly they have new markets.
So when we go to design an action, the first question we ask is, what type of action do we want? Well, that kind of depends what our target is. Is our target a scriptable application? If so, then that would lead to an AppleScript action. Is our target a framework or a technology, a library, et cetera? Then a Cocoa action would be best. Or are we targeting a command line tool? Something like that. Then a shell script action would fit your purpose best.
Also, we want to keep it small. We don't want any Swiss Army actions, something that does everything and more. We want these to be modular pieces that users can put together in the order that they want, and that allows them to really focus in on what they want to do.
Also, we want to keep it small. We don't want any Swiss Army actions, something that does everything and more. We want these to be modular pieces that users can put together in the order that they want, and that allows them to really focus in on what they want to do.
Also, we want to keep it small. We don't want any Swiss Army actions, something that does everything and more. We want these to be modular pieces that users can put together in the order that they want, and that allows them to really focus in on what they want to do.
Also, choose meaningful data types. When you design an action, you have to specify which type of data comes in, which type of data goes out. I will talk more about this in a little bit, but there are several predefined data types for you. If you can't use one of those, you can define one for yourself.
So I'm going to go and show you what we're going to be building today, two actions plus a conversion action. I'm going to show you the finished product first since as we go through we can't really run the full thing until all the pieces are in place. Could I get demo two, please?
[Transcript missing]
So here's our workflow that we're going to be building. Get specified finder items is an existing shipping action. So is copy finder items, which we've put in there because we're going to be modifying the files that we work on. We're going to be building this Unsharp Mask Action and the later Save Image to File Action.
A couple things about these two actions. You'll notice that we have defined a custom data type for them called images. That's not image files, it's actually image data in memory. So we're going to have a conversion action between Copy Finder Items and Unsharp Mask to take the image files that are passed in, get the image data out of them, and then pass them to Unsharp Mask. Unsharp Mask is an action which uses the Core Image Framework.
Core Image acts kind of like Automator does in that it allows you to apply successive actions or filters on an image, and the resulting image isn't actually computed until it's rendered. So that's why we have it set up this way. We have an Unsharp Mask, and you could easily create additional Core Image type actions. And chain them together, and then the final one would be a Save Image to File, where it takes that image, processes all the filters that have been acted upon it, and then saves the resulting data to disk.
Okay. Back to the slides, please. So that's what we're going to be building. We're going to start with the unshark mask action, and we're going to build the user interface. So a couple things to keep in mind when you build the user interface for your action. One, keep it simple. We've already talked about that.
Follow the published guidelines. There are Aqua Human Interface Guidelines, but there are also Automator Action Human Interface Guidelines. I don't think they're called that. It's a long title. They basically say things like, use small fonts, small controls in your action, spaces that are premium, so minimize your vertical space so that you don't end up having really long, you don't have to scroll too much.
Use Cocoa Bindings when possible. It's not required, but it makes things a lot easier, and it's all about being easy to do. And you can use shipping actions as examples. There are a lot out there. If you look at the screenshot of that action, it uses small controls. There's a text 10-pixel border around the edges, which we'll show, and that's an example of an action interface.
Now, so let's go build the Unchart Mask user interface. Demo 2. Now I'm just going to start by opening a new project. And in Xcode, if you select-- can you read that? Okay. In Xcode, if you open a new project and select Cocoa Automator Action, we'll call it Unsharp Mask.
Okay, a quick look at our project. The template gives us code classes based on the name of the project. It gives us a main.nib file, Info.plist, and a target and an executable. I hope I'm not making you dizzy with that. Target and an executable. And we're just going to open up the main.nib file.
And we're going to build that user interface that we saw just a minute ago. So I'm going to grab a label here. And I'll zoom in when I'm done so you can see it, but that's hard to do when I'm trying to build this. Call the first one intensity.
Oops, grab a slider. Open up the inspector because we need this to be small. Text field. Make it small. Make it even smaller. and line it up. Oh, and I wanted that to continuously update. Okay. Then I'm just going to copy and paste and change the second label to radius.
and line it up. Oh, and I wanted that to continuously update. Okay. Then I'm just going to copy We can arrange it so that we have a 10 pixel border around the edges. I can just resize the window while I do this. It can be kind of tricky.
I actually tried doing this on my PowerBook on a moving train. It was a lot trickier. OK. So we've got all the elements in place. Now what we need to do is bind it using Cocoa Bindings. And every nib file created for an action with the template comes with an object controller named Parameters already on the back end hooked up to our data, our actions data, the Parameters dictionary. So what we need to do is just hook it up on the front end to our objects. So we can select the Parameters object controller.
Then in the inspector, I add a couple keys, one called Intensity That's how I do it. One called intensity and the other called radius. You can add these here, or you can also add them in the Bindings Inspector. But I thought I'd show you one of two ways. OK, so if I select the slider, the top one, then I'm going to select the value. Hopefully you can see this.
Problem with that. Okay, so we're going to select the Model Key Path, Intensity, bind that to the Intensity, and the same for the Text field. Now, something that is useful when you're binding text fields is to select the Continuously Updates value. That's so that if you don't actually return or tab out of the text field, it still will get the value that you or the user has entered. Okay, then we'll just do the same thing with the radius key for the lower text field and slider. and we're done with our user interface. We can go back to Xcode, build and run it, and see what it looks like.
It might take a minute. And it should launch Automator, load our action, But since we haven't configured any of the other properties, it'll just come up in this strange primary application default key. But there's our action, Unchart Mask. If we add it to the workflow, we can see, well, we don't have any default data because we didn't set any up yet. But we can see that the bindings are working because the slider and the text field are corresponding. Not much use, but we can see that we can do a little bit yet, or so far.
Okay, so the next step, if we can go back to the slides, once that's done, is to configure the basic properties of the action. The name, the category it goes in, the input and output types, you know, any description information. And we can use the Action Target Inspector to do this. This is new in Xcode. It's built into Xcode 2.1.
The way that we get to that, and I'll show you this, is you can select your target, get the info on it, open the inspector, and then select the properties tab. Tab. Tab view. Tab in the tab view. There's a pop-up, a collection pop-up that will allow us to set the desired properties that we want and configure them to our heart's content. If we don't have Xcode 2.0, we can edit the Info.plist directly because that's all this is doing. It's editing the Info.plist for you because that's not very fun.
Now what I said about data types. I mentioned data types earlier. Automator uses UTIs to define data types. That stands for Uniform Type Identifier. And they're very common on Mac OS X Tiger. They can describe a type of a file or they can describe objects in memory. They can describe really anything you want. Automator has a lot of predefined data types for you. For Cocoa types such as Cocoa.String. For AppleScript types, AppleScript Object is one. And they're all documented in the documentation. So you can go there, pick one that's best for your data type or your action. or your action.
and use it. Use it if you can. Use one of those if possible. But if not, you can declare a data type for your own object. And the way you would do that is there's something called a .definition file. In your sample code is an example of a .definition file called Image Definition.
And it's, there's nothing to compile in it. It's basically an Info.plist with a declaration. And you declare the UTI using reverse namespace format, such as com.yourcompany.yourframework or yourapp.yourobject. And you also can provide a human readable name so that users don't actually ever see the UTI type. They see the name that you've given it.
Um, .definition files can be installed anywhere in the /library/automator domains. or in your project's bundle. If it's in there, along with some Automator Actions, you can just drag and drop "Install an application," and it'll see it, and Automator will be able to use it. So let's go ahead and do that. Back to Demo 2.
Okay, as I said about using the Target Inspector, our target here is called Unsharp Mask, the same as our project by default. So we can select that, and then click Get Info to open the Inspector. Select the Properties tab. Now the top portion you probably don't even need to modify.
Most of that's filled out for you. The bottom portion is the real Action Inspector here. There's a collection pop-up with a lot of different types of collections. So you can change things like we can tell it that the application is going to be in the preview application group. Should use the preview icon.
and input types are selected here. Now as I said, lists or single items are supported. You can select either of those. You can make the input type optional if you want. I'm going to change the UTI type to be the UTI type that I defined in -- for this action, for the image type, which is -- and again, users never see this. Quartz Core, because that's the framework, and Core Image. That's the image type I'm using. Instead of filling those all out, I'm going to redo the InfoP list all at once, and then I'll go back and show you the rest of the changes that I've made.
So if I save that, the Target Inspector should be updated. It is. Good. So some of the things-- I'll just show you the things we changed. I haven't modified a lot of the settings, but I have set the name, Unchart Mask, the application, the category, and the icon name. We've changed the input type.
and the output type to be the same thing. They're both passing in-- or it's accepting and providing this core image type. And we've added two parameters. And these parameters match what we added to the object controller in our nib. We have an intensity value, which is a real or a float. And we can supply a default value for it. We also have a radius parameter to go along with it.
Now I've added, I've changed the default warning level. That should be irreversible rather. You can specify an action as being safe, means it doesn't modify any of the data. It modifies it in a reversible fashion or an irreversible fashion. And in this case, that's what we have. So we've specified that it's irreversible.
And when you add that to your workflow, Automator will prompt the user to add, this is the bundle identifier for the copy finder items action, it will prompt the user to add that before your action. So we added that and we also changed the description, provided a summary, and provided some information on the options for this action.
And that's all we've done to the InfoP list. We also need to-- since there is a strings file provided, it would pick up this information if we don't modify it. So this is using the Info.plist strings, you can put localized keys in here to localize your action. It'll actually, if the user relaunches in French, your action UI will also be in French if you have localized keys.
So you can provide things like the action name, a localized action name, names for your default parameters, your descriptions, et cetera. Put that in. And so we've just supplied localized names for the action. The application, the category, options and summary, etc. and the Warning Level Description. This comes up in a dialogue when you add your action.
So now, now we should be able to build and run it, and it should look correct and behave like any other action, only "It won't do anything because we haven't added any code to do anything. The default behavior is just to pass through your input to your output." Okay, so it should be on preview. Unsharp mask. And there's our warning. I'm gonna say no for now. But there's our action. It has our default values in there. and our UTI type, our image type, with the human readable name. So, looks pretty good. Okay, let's go back to the slides. Need this.
So our next step is to actually add code. And the first thing we do is add our existing code, or our frameworks, to the action, just like you normally would in an Xcode project. The method that we override is runWithInput, fromAction, error. And I'll show you the class that belongs in the AM bundle action class. I'll show you that in a second. But that method is what gets called when your action is run. It's where everything happens.
Tiger Ships with Automator Framework on the system, and it defines these classes for using in Automator Actions. AM Action is the base class. It's just an abstract base class which you won't use. AM Bundle Action is the one that Cocoa Actions use. It supports loading of nibs and copying and coding
[Transcript missing]
Do not present custom error dialogs. If an error occurs in your Action, don't put up a warning or display a sheet. Leave that to Automator so the Automator knows it needs to stop the workflow. So in the run with input from Action error, there's that error parameter that gives you a dictionary, and you can fill out the keys and just return that, return from your Action, and Automator will see that an error occurred and treat that properly.
Access your Actions Bundle, if you need to, by calling self-bundle, or it's a method on am-bundle-action. If you try and call ns-bundle main-bundle, you'll get Automator's Bundle, because Automator is the one executing this. So that's something to keep in mind. Actions run in a secondary thread. So if you need to do any special UI, like put up a window, update a text field, something like that, you'll need to call into the primary thread. And you can do that with this method, perform selector on main thread with object, wait until done. That'll get you the right behavior.
Some commonly overridden methods in Automator Actions. There's the opened method. This is called When your Action is added to a workflow, but just before it's made visible. And you'll want to do this to update or to, rather, to initialize the user interface of your Action. activated is called whenever the workflow window is made frontmost. So if the user goes away and does something, comes back, this is called. This is really handy if you need to keep your Actions UI in sync with other stuff that's going on in the system.
Parameters Updated is called when the Parameters Dictionary is changed behind the scenes and you need to update your user interface if necessary. And then you can call update parameters to tell Automator that your parameters have changed. For example, if you're not using Cocoa Bindings, then you need to update that Parameters Dictionary in memory. And remember to call super on all these methods, because the super classes do things which need to be done. And so you need to call super on those.
Now when it comes time to build an action, One thing you need to do is configure the target. The product name and extension are generally filled out for you and are correct, but for conversion actions, which I'll show you, you should need to change the extension. AMLint is a tool that is added by the template as a build phase, so it gets run whenever you build your action, and it will look for common errors and mistakes in the process of creating an action.
And you'll see those errors and warnings in the standard build pane. You can run PLUtil. If you've edited your InfoP list by hand and want to make sure that you haven't made any mistakes, It will check the format of your InfoP list. That's real handy because sometimes if you've made a mistake in your InfoP list, it can be really tricky to find that that's where the mistake was made.
And again, installing your Action, you can install it anywhere in the /library/automator domains or your applications bundle. And it is not necessary to install it before you debug it. It just--you can, but it takes a lot of extra time, and it's a pain doing it that way. So let's add some code to our Unsharp Mask Action.
Here we are. Okay, so looking at our source code files, we have a header file called Unchart Mask, and it is a subclass of AMBundleAction. And we have the runWithInput from action error method here. It's declared by the superclass. The template gives it to us, so we'll just leave it. And as I said, the default behavior is just to return your input to your output. And so we're going to modify that. I'm going to remove this. Drag in the new.
As I said, we're using the Core Image Framework. We're also using this extended Core Image Framework that is included in your sample code. Now, the reason for that is so that when an image comes in, or an image file comes into our workflow, we convert it to an image object, then later on we want to save it back to the file, but we've got to preserve the file name. So we use this extended Core Image Framework, which adds a category on Core Image, so that we can save the original path to that image in the image object, and then we'll have it later on. That's what that's there for.
So in our runWithInput method, let's look at the code. The first thing we do is create a mutable array called output. And we're just going to use this. Every time we get a piece of output, we're going to stick it in there. We're going to return it at the end of our method, and that's going to be the output for our action.
So what we're going to do-- is get a Core Image Filter Object, initialize it with defaults, And then the way that we get the parameters that the user has customized is to call self parameters, object for key, and then the key that we're interested in. And that's how we access our parameters dictionary. Our default parameters are the ones that the user has customized, and we're just going to take those, and we're going to pass them to that filter object.
Then, what we're going to do is we're going to iterate over the input. So these should be image objects. And for each image object, we're going to pass the filter the image object. Then get the resulting output image. And as I said, we're trying to save the original file URL, so the new image is different from the previous one, so we just pass that along. And then we add the resulting CI image object.
[Transcript missing]
and we have no nib in here. It has no UI. And if you present, if you have no nib in your action, you'll still get in the Automator workflow, you'll still get the title bar and the status bar, but just nothing in between. It'll just be an empty action. There's a lot of actions like that. So all we have here is code. And here, let me replace what we need to replace.
Tada! Okay. And here we have our runWithInput method. And it does a lot of core image stuff that we don't need to go into, but we'll go over it somewhat. We get a graphics context. Remember I said in this step we have to actually render our core image object to a graphic. We have to render it, which we do into a graphics context. So we get a graphics context, and then we iterate over the input. Again, we create a mutable array to hold our output. We iterate over the input.
So for each image that comes into this action, we're going to get the data for image, and that calls into a function which I have below here. We're going to get the data for that image, then we're going to get the path that that image was originally stored into, and and we're going to write the data to the file. Now, if an error happens there, This is where we return an error.
We create an error string saying, sorry, we couldn't do that. And then in the error info dictionary, We set that error string for this NSAppleScript error message key. AppleScript seems a little out of place here, but Cocoa methods, it's all AppleScript thing, right? Or at least it was originally, yeah. It's Cocoa AppleScript, shell scripting, it's everything. So that's how we report errors. and there's the data for image method you can look at on your source code. It's nothing particular to Automator, it's just all core image stuff.
Back to the slides, please. Okay, we went through Save Image to File. And now we need to create the Conversion Action. Now, what is a Conversion Action? Conversion Actions are really an essential part of Automator. They're completely invisible, but Automator really wouldn't work very well without Conversion Actions.
They handle the flow of data between the actions. So they can convert data from completely different types, or they can sort of act as filter, filtering out unwanted types of data. They have no UI. In fact, you don't supply a nib, and you shouldn't try and put any UI up in a conversion action.
And they're executed automatically by Automator. Automator will look at, well, for example, in this example, getCurrentSong returns iTunes Songs. That's not the files, but that's actually the iTunes, that's a specifier to the object type in iTunes memory. Then it passes that to an action which takes files and folders. Well, there's a conversion action in there that's actually performing that translation.
It's using AppleScript to get the iTunes song object, getting the location property out of that object, and then creating a list of these paths and returning that or passing that along to the next action. And so Automator will determine when this needs to be done. It'll find the correct conversion action. It'll even string together a series of conversion actions if necessary to perform the required conversion.
So how do you create a Conversion Action? It's almost exactly identical to creating a regular Action, but you need to change the product extension to .cAction instead of .action. and you delete the main nib file. That's probably even optional. I think Automator wouldn't even load it if you had one.
So you just need to follow those steps and we'll do that right now. If we can go to the demo machine. So I'm going to open up step one. The completed portion of this part is in step two, if you want to see that. But I'm going to open up step one and make the changes from there.
Okay, so if we select our target and open up the inspector, what we want to do is click on the Build tab and scroll down until we find... The wrapper extension. Boop. Right there. So it says -action by default. We want to change that to C action. Pretty easy, and we don't have a nib. I've already pre-configured the rest.
Let's look at this to show you the input and output type. So the input type is a Cocoa path, so we're going to be getting paths to files, and we're going to output our data type, Quartz Core, Core Images. Now to write the code for that, wrong button. Again we have a runWithInput method.
So again, the first thing we do is create a mutable array to hold the output. Starting to see a pattern here. Then we iterate over the input. Again, the same thing. And we get the file path, the current file path, and we're going to create a URL in it so that we can put that into our image that we create.
And we're going to create an image, a CI image object from the URL. and put that object in our output. It's that simple. Most conversion actions are really simple. It's just a way of, like, of getting an object and then just getting a property out of it to convert the type or something like that.
So... So that's what there is to creating a conversion action. And in a moment, we should be able to build and run the entire workflow. Can I go back to the slides? Okay, and I'll show you that as part of debugging an action. So, to debug an action, There is an executable that the template will create this executable
[Transcript missing]
is the name of the Action, and it will load that Action as Automator launches. So that's a way that you can build and run and debug your Action without having to install it. And we will show you that. And then you can debug as usual.
Also in the workflow, if you want to modify or monitor and inspect what your action is, what's coming into it, what's going out of it, the View Results Action is very handy for that. It'll show you all the information. And then there's also a workflow log which will show you actions that are being run at what time, what conversions are happening. et cetera. So let's debug an action. Whoops. We can go back to demo machine. Alright, so I'm going to go back to the action that we created, which was this one. And I need to do two things. I just need to import our custom framework.
Library Frameworks. That's not it. Sorry, wrong disk. Okay, Library Frameworks, and there's our custom framework. Yes. And import the Quartz. Core Image Framework in System Library Frameworks. Quartz Core. Okay, I'm going to set a breakpoint on the runWithInput method. and let me just make sure of one property. Okay.
[Transcript missing]
should launch... Oh, sorry, one mistake. Automator was already running. I don't want to run a second copy. Here we go. Launching Automator.
Okay, and I have a preset workflow here that we're going to test out. Now what this does is we're gonna get an image file. We're going to copy it to the desktop. We're going to run the Unsharp Mask filter on it, and I've got the settings turned way up so that we can see-- because it's kind of a subtle action-- so that we can see the effects of it. Then it's going to save that to file.
And I've got this other step so that we can see the before and after. We're going to get the original image again, and it's going to add it. So we've got the modified and the original one, and then we're going to open them both in preview. But it should break on Unsharp Mask in the run with action. There we are.
and we can step through it if we want. There aren't any bugs here that I know about, but we can step through it. and look and see. There we've created an image. We have a resulting image. We're gonna add it to the output. Return the output. and should launch preview and there we can see it really sharpened the heck out of it. There we go.
Okay, back to the slides. Okay, so what did we learn? We learned guidelines and designing actions, how to create actions and conversion actions, How to debug them. It's all really very similar to what you're used to doing in regular Objective C apps. And just how easy it is. Each of these pieces is just small, and you can quickly create a whole slew of them. That's probably why there's 550 Automator Actions out there already.
So, more information. We've got the standard ADC website for documentation, sample code. and related sessions. Tomorrow morning is a lab for Automator Action development. Please come by and give us your questions. We'd love to help you out. Also, tomorrow afternoon is the Building Automator Actions for System Administrators session.
I believe most of that will be the shell script action, but it should be very interesting. Who to contact? Sal, who you all know. Inside Joke, sorry. And Todd Fernandez, the Engineering Manager. Also, there's the Automator Dev mailing list. Get on that. It's very interesting reading. And with that, we'll invite John Montbreon for the Q&A.