written by Brian Christensen
Writing a screen saver module is surprisingly simple using Mac OS X's screen saver framework. The ScreenSaverView class provides us with an interface for animating the screen saver, drawing the preview view in the System Preferences pane, and displaying a configuration sheet.
I originally wrote this article in 2001 when it was still necessary to jump through a few configuration hoops just to create a new screen saver project. An entire section was dedicated to "Preparing the Project." Thankfully, these steps are no longer required. Ever since the advent of Xcode Apple has included a default screen saver project template to make our lives significantly easier.
Having said that, this is not just a mere recycling of the old article. There is a considerable amount of fresh material which I hope will improve upon the original article.
First, we'll need to create a new project. Launch Xcode and choose File > New Project. The window depicted below will appear:
From the list of choices you are presented with at this point select Screen Saver located under the Standard Apple Plug-ins category. Proceed to name the project MyScreenSaver (or any other name you prefer) and save it in your desired location. Once you have completed these steps, the project window will open.
Xcode automatically creates two files for us: MyScreenSaverView.h and MyScreenSaverView.m. Take a few minutes to poke around MyScreenSaverView.m and take note of the various methods that are provided for our use.
I have summarized some of these methods below:
ScreenSaverView: Key Methods
-initWithFrame: isPreview: |
initializes the view with the given frame rect |
-setAnimationTimeInterval: |
sets the animation rate in seconds |
-startAnimation: |
invoked by the screen saver engine when animation should begin |
-stopAnimation: |
invoked by the screen saver engine when animation should stop |
-drawRect: |
draws the view |
-animateOneFrame |
advances the animation by one frame at the rate set by the preceding method (this can be used for drawing instead of drawRect:) |
-hasConfigureSheet |
returns true if the module has a configure sheet |
-configureSheet |
returns the module's associated configure sheet |
We want to draw a bunch of shapes of random type, size, color, and location. This drawing magic must occur either in the drawRect: or animateOneFrame method. Since animateOneFrame is slightly easier to use and better suits our purpose, this is the method we will be using.
Open the "MyScreenSaverView.m" file to edit it. Scroll to the animateOneFrame method and add the following code:
MyScreenSaverView.m
- (void) animateOneFrame
{
NSBezierPath *path;
NSRect rect;
NSSize size;
NSColor *color;
float red, green, blue, alpha;
int shapeType;
First of all, we want to obtain the screen's boundaries for later use. To do this, we use the bounds method. This returns an NSRect from which we will obtain the size property.
size = [self bounds].size;
Since we want our shapes to have random sizes, we can use the specially supplied SSRandomFloatBetween() function for this. For those who are wondering, it is automatically seeded by the screen saver framework, so there is no need to manually deal with that.
// Calculate random width and height
rect.size = NSMakeSize( SSRandomFloatBetween( size.width / 100.0,
size.width / 10.0 ),
SSRandomFloatBetween( size.height / 100.0,
size.height / 10.0 ));
Now we want to calculate a random origin point for our shape. To do this, we use the handy SSRandomPointForSizeWithinRect() function, which will do all the work for us.
// Calculate random origin point
rect.origin = SSRandomPointForSizeWithinRect( rect.size, [self bounds] );
In simple terms, an NSBezierPath object lets you draw paths consisting of straight and curved line segments. When put together these can form shapes such as rectangles, ovals, arcs, and glyphs. We will be using the first three in our code.
We want to randomly decide whether to create our NSBezierPath object as a rectangle, oval, or arc. In order to provide for greater flexibibility should you choose to invent additional shape types at a later time, we will utilize a switch statement.
// Decide what kind of shape to draw
shapeType = SSRandomIntBetween( 0, 2 );
switch (shapeType)
{
case 0: // rect
path = [NSBezierPath bezierPathWithRect:rect];
break;
case 1: // oval
path = [NSBezierPath bezierPathWithOvalInRect:rect];
break;
The arc case is a bit more complicated, so we will go through it step-by-step. An arc consists of a center, a radius, a starting angle, an ending angle, and a direction. First, we will select random starting and ending angles between 0 and 360 degrees. (Naturally, as you can see in the code below, it wouldn't make sense for the ending angle to start before the starting angle. Hence, it will not necessarily fall between 0 and 360 degrees. It is actually chosen to be somewhere between the starting angle value and the starting angle value plus 360.)
case 2: // arc
default:
{
float startAngle, endAngle, radius;
NSPoint point;
startAngle = SSRandomFloatBetween( 0.0, 360.0 );
endAngle = SSRandomFloatBetween( startAngle, 360.0 + startAngle );
Similarly, we now choose a random radius. Since we already did a calculation earlier to determine a random rectangle size, we will simply use either the width or the height of that size, whichever is the smallest. In addition, we also calculate a center point using previously generated origin and size values.
// Use the smallest value for the radius (either width or height)
radius = rect.size.width <= rect.size.height ?
rect.size.width / 2 : rect.size.height / 2;
// Calculate our center point
point = NSMakePoint( rect.origin.x + rect.size.width / 2,
rect.origin.y + rect.size.height / 2 );
Finally, we are ready to construct our path. Unlike the above rectangle and oval cases, here we must first create an empty bezier path and then append the arc path to it.
// Construct the path
path = [NSBezierPath bezierPath];
[path appendBezierPathWithArcWithCenter: point
radius: radius
startAngle: startAngle
endAngle: endAngle
clockwise: SSRandomIntBetween( 0, 1 )];
}
break;
}
Naturally, we want to use random colors for our shapes, too. Take special note of the alpha parameter, which will create a nice transparency effect.
// Calculate a random color
red = SSRandomFloatBetween( 0.0, 255.0 ) / 255.0;
green = SSRandomFloatBetween( 0.0, 255.0 ) / 255.0;
blue = SSRandomFloatBetween( 0.0, 255.0 ) / 255.0;
alpha = SSRandomFloatBetween( 0.0, 255.0 ) / 255.0;
color = [NSColor colorWithCalibratedRed:red
green:green
blue:blue
alpha:alpha];
Last, but not least, use the set method to set the color used to draw our shape to the screen. In keeping with the tradition we have established so far, we will again randomly determine whether to draw a filled shape or an outlined shape (using either fill or stroke).
[color set];
// And finally draw it
if (SSRandomIntBetween( 0, 1 ) == 0)
[path fill];
else
[path stroke];
}
It's time to test the screen saver. Select Build > Build. When the process is finished, go to the Finder and open the build folder located inside of your MyScreenSaver project folder. You should see a MyScreeSaver.saver file in there.
To test your screen saver simply double-click the file. This will open System Preferences and present you with a dialog to allow you to automatically install the screen saver.
If you make changes in the code and rebuild your project later, the next time you double-click the file System Preferences will conveniently ask you if you want to replace the old version. (Please note that you may need to quit and relaunch System Preferences for it to reload your screen saver module.)
Although we could actually stop here since we now have a fully functioning screen saver, we, of course, will not. We want to make our screen saver even better by providing a few customization options through the use of a so-called "configure sheet." To save a bit of work, I have prepared the NIB file.
Download this file, and double-click it to decompress it.
Drag the ConfigSheet.nib file into the Xcode project's
Resource group. Accept the default settings for adding the file to the project.
This NIB file has a bunch of outlets which we need to let our screen saver view know about. Edit MyScreenSaverView.h and add the following code:
MyScreenSaverView.h
@interface MyScreenSaverView : ScreenSaverView
{
IBOutlet id configSheet;
IBOutlet id drawFilledShapesOption;
IBOutlet id drawOutlinedShapesOption;
IBOutlet id drawBothOption;
}
To let the "Desktop & Screen Saver" system preference pane know that it should enable the "Options" button for our screen saver, we need to change our hasConfigureSheet method to return YES.
MyScreenSaverView.m
- (BOOL) hasConfigureSheet
{
return YES;
}
Furthermore, we need to load our NIB file and return a pointer to our sheet, which we do by implementing the following code in our configureSheet method.
- (NSWindow *)configureSheet
{
if (!configSheet)
{
if (![NSBundle loadNibNamed:@"ConfigureSheet" owner:self])
{
NSLog( @"Failed to load configure sheet." );
NSBeep();
}
}
return configSheet;
}
Before we can do a quick sanity check test run, we need to add one more method.
- (IBAction)cancelClick:(id)sender
{
[[NSApplication sharedApplication] endSheet:configSheet];
}
To make sure that everything has gone smoothly so far, build the project and double-click the resulting MyScreenSaver.saver file to install it. Once System Preferences has opened, click the "Options" button for our screen saver and the configure sheet should appear.
Now that we have a working configure sheet, we'd like it to actually do something useful. Fortunately, implementing preferences in screen savers is not significantly different than in any other Cocoa application. We will use the ScreenSaverDefaults class, a subclass of NSUserDefaults. This is a convenient and simple way of dealing with user preferences.
The first step is to register our default values, the factory settings. First, add this constant somewhere towards the top of our ".m" file (directly under @implementation, for instance, would be fine):
MyScreenSaverView.m
static NSString * const MyModuleName = @"com.yournamehere.MyScreenSaver";
Now, add the missing lines to the initWithFrame:isPreview: method so that it looks like this:
- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
self = [super initWithFrame:frame isPreview:isPreview];
if (self)
{
ScreenSaverDefaults *defaults;
defaults = [ScreenSaverDefaults defaultsForModuleWithName:MyModuleName];
// Register our default values
[defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
@"NO", @"DrawFilledShapes",
@"NO", @"DrawOutlinedShapes",
@"YES", @"DrawBoth",
nil]];
[self setAnimationTimeInterval:1/30.0];
}
return self;
}
Trudging on, let's implement the okClick: method. This is the method that will be invoked when the user clicks "OK" in the configuration sheet, so what we need to do is simply update the defaults to reflect the settings selected in the sheet.
- (IBAction) okClick: (id)sender
{
ScreenSaverDefaults *defaults;
defaults = [ScreenSaverDefaults defaultsForModuleWithName:MyModuleName];
// Update our defaults
[defaults setBool:[drawFilledShapesOption state]
forKey:@"DrawFilledShapes"];
[defaults setBool:[drawOutlinedShapesOption state]
forKey:@"DrawOutlinedShapes"];
[defaults setBool:[drawBothOption state]
forKey:@"DrawBoth"];
// Save the settings to disk
[defaults synchronize];
// Close the sheet
[[NSApplication sharedApplication] endSheet:configSheet];
}
Lastly, we want the sheet to reflect the values in the preferences file when it is displayed. Navigate to the configureSheet method and update it as necessary to contain the following code:
- (NSWindow *)configureSheet
{
ScreenSaverDefaults *defaults;
defaults = [ScreenSaverDefaults defaultsForModuleWithName:MyModuleName];
if (!configSheet)
{
if (![NSBundle loadNibNamed:@"ConfigureSheet" owner:self])
{
NSLog( @"Failed to load configure sheet." );
NSBeep();
}
}
[drawFilledShapesOption setState:[defaults
boolForKey:@"DrawFilledShapes"]];
[drawOutlinedShapesOption setState:[defaults
boolForKey:@"DrawOutlinedShapes"]];
[drawBothOption setState:[defaults boolForKey:@"DrawBoth"]];
return configSheet;
}
Now that we have the defaults set up and ready to go, we want to actually utilize them during our animation drawing. As you will see, doing so is quite simple. Scroll to the animateOneFrame method and add the following line to the list of local method variables:
MyScreenSaverView.m
ScreenSaverDefaults *defaults;
Continue scrolling down to the last section of code in the same method, which should be this:
if ( SSRandomIntBetween( 0, 1 ) == 0 )
[path fill];
else
[path stroke];
Replace the above block with the following:
defaults = [ScreenSaverDefaults defaultsForModuleWithName:MyModuleName];
if ([defaults boolForKey:@"DrawBoth"])
{
if (SSRandomIntBetween( 0, 1 ) == 0)
[path fill];
else
[path stroke];
}
else if ([defaults boolForKey:@"DrawFilledShapes"])
[path fill];
else
[path stroke];
That's it. Hit build and take your new screen saver module for a test run.
You've written your first screen saver for Mac OS X. Certainly you must be hungry for more, so take a look at Write a Screen Saver: Part II, which covers more advanced topics including the use of OpenGL to do the drawing. It also explains important issues regarding symbol clashes and what conventions to use to prevent such problems (of which you should be aware before you release a screen saver into the wild).
As always, let us know what you think about the tutorial.
Further Reading