Sidebar #1

A 60-Second Introduction to Objective-C

Objective-C is a hybrid of the languages Smalltalk and C. It starts with full C compatibility, and then adds a new data type and a way to define classes.

The new data type is called id; it is a pointer to any kind of object, the way C's void * is a pointer to any kind of structure.

The new syntax for defining classes looks like this:

@implementation ASillyClass				/* starts a class definition */
		
- (int) add: (int) aNumber to: (int) anotherNumber { 
	return aNumber + anotherNumber;
}

- print: (int) someNumber { 
	printf("%d\n", someNumber);
	return self;			/* self is the object that was sent the message */
}

@end								/* ends a class definition */

Individual methods begin with a dash (ª-º). The first method takes two numbers and returns the result of them added together. The second method prints the value of its argument and ªreturns self,º a common idiom.

Use a pair of square brackets to send a message to an Objective-C object. Here is an example, using the above class:

id  obj = [[ASillyClass alloc] init];	/* make an object of class ASillyClass */
int x=3, y=4, z;
	
z = [obj add: x to: y];
printf("%d plus %d is ", x, y);
[obj print: z];

In the first line above, a message was sent to the result of the previous message; this is a common shorthand (e.g., [[self doThis] doThat]). You can also embed Objective-C messages inside regular C expressions:

z = 14 / [obj add: x to: y];

By convention, Objective-C programmers use capital letters to begin the names of classes and use lower-case letters to begin the names of messages and variables.

Although there is much more to Objective-C, this small introduction should be enough to get you started reading Listings 1-4.

Sidebar #2

Extending Ergo, or How to Get Your Own Copy

Face it: Ergo needs work. The game should display the number of white- and black-filled squares after each move, detect the end of the game, have a clock to limit the total time taken by each player, have sound effects and music, and more. Each of these extensions is extremely easy to add to the existing structure of Ergo, if you think in an object-oriented way.

Some interesting extensions to the game have already been included in the versions of Ergo you can get below. These include an Interface Builder palette -based version of Ergo, in which new boards can be created in seconds from a palette of pieces, and a version that allows multiple boards to be loaded at once. All the versions of Ergo below come with additional comments and the full sources (all the .m, .h, .nib, etc. files).

If you have ftp access to the Internet, Ergo is available in all of the main archive sites (cs.orst.edu and sonata.cc.purdue.edu are two). If you have only E-mail access, send a message with the subject "help" to the address archive-server@cc.purdue.edu and follow the instructions mailed back. If this fails, send a message asking for Ergo to ergo@nextworld.com and we will send you a copy. Floppy disks will be available on a limited basis: write to Ergo Floppy Offer, NeXTWORLD Magazine, 501 Second St., San Francisco, CA 94107. If all else fails, you can type in the source code included in Listings 1±4. It is a complete listing of Ergo (except for a few trivial .h files that are mirrors of the .m files). Use Figure 2 to help you create the .nib file.

Listing 1 ± ErgoApp, a Subclass of Application

Each Class.m file looks the same: Import the necessary .h files, define a sequence of implementations for public messages in alphabetical order, then the private messages. Key messages in Ergo's protocol will appear in bold, while comments will appear in italics. Good object-oriented programming is convoluted by its very nature, so expect to search back and forth across these listings as you read through the tutorial. A good object-oriented environment makes following these links between methods trivial, but the linear layout of text here prevents us from doing so.

File: ErgoApp.m

#import "ErgoApp.h"
#import <appkit/View.h>
#import <objc/List.h>

@implementation ErgoApp

- highlightedSquare {
	return highlightedSquare;
}

- (BOOL) isSomeoneSelected {
	return isSomeoneSelected;
}

- (BOOL) isWhiteMove {
	return isWhiteMove;
}

- letOtherColorMove {
	[self setIsWhiteMove: ![self isWhiteMove]];
	return [boardWindow display];
}

- makeSquaresPerform: (SEL) aMessage {
	return [[self viewList] makeObjectsPerform: aMessage];
}

- makeSquaresPerform: (SEL) aMessage with: sender {
	return [[self viewList] makeObjectsPerform: aMessage with: sender];
}

- setHighlightedSquare: aSquare {
	highlightedSquare = aSquare;
	return self;
}

- setIsSomeoneSelected: (BOOL) state {
	isSomeoneSelected = state;
	return self;
}

- setIsWhiteMove: (BOOL) state {
	isWhiteMove = state;
	return self;
}

/* Private methods: */

- appDidInit: sender {		/* our return value is ignored, so any is OK */
	isWhiteMove = YES;
	return [self makeSquaresPerform: @selector(appDidInit)];
}

- viewList {
	return [[boardWindow contentView] subviews];
}

@end

Listing 2 ± AbstractSquare, a Subclass of View

Here we show both the .h and .m files for AbstractSquare. Note that several commonly used files are imported in the .h file. This allows all subclasses of this abstract class to import them automatically without thinking about it, since each subclass will ultimately import AbstractSquare.h. Note, too, the declaration of some local state information for holding the square's row, column, etc.  These are instance variables, which can hold different values in each instance (object) of the AbstractSquare class (or any of its subclasses).  AbstractSquare is the most general of the classes implementing Ergo squares.

File: AbstractSquare.h

#import "ErgoApp.h"
#import <appkit/View.h>
#import <appkit/nextstd.h>
#import <dpsclient/psops.h>

@interface AbstractSquare : View {
	short		row, column;
	BOOL	isHighlighted;
	int		trackingRectNumber;
}

- (short) column;
- (short) distanceTo: otherSquare;
- drawSelf: (const NXRect *) rects : (int) rectCount;
- (BOOL) isHighlighted;
- mouseExited: (NXEvent *) theEvent;
- (short) row;
- setIsHighlighted: (BOOL) state;

/* Private methods: */

- appDidInit;
- appDidInitInside: (BOOL) state;
- become: newSquareClass;
- capture: sender;
- checkAdjacencyWith: sender;
- free;
- freeTrackingRect;
- highlight: (const NXRect *) aRect;
- initFrame: (const NXRect *) frameRect;
- setTrackingRect: (const NXRect *) aRect inside: (BOOL) state;

@end

File: AbstractSquare.m

#import "AbstractSquare.h"

@implementation AbstractSquare

- (short) column {
	return column;
}

- (short) distanceTo: otherSquare {
	return	MAX(ABS([self  row ]	 - [otherSquare  row ]),
			ABS([self column]	 - [otherSquare column]));
}

- drawSelf: (const NXRect *) rects : (int) rectCount {
	[super drawSelf: rects : rectCount];
	NXDrawGrayBezel(&bounds, rects);
	if ([self isHighlighted])
		[self highlight: rects];
	return self;
}

- (BOOL) isHighlighted {
	return isHighlighted;
}

- mouseExited: (NXEvent *) theEvent {
	if ([self isHighlighted])
		[self setIsHighlighted: NO];
	return self;
}

- (short) row {
	return row;
}

- setIsHighlighted: (BOOL) state {
	if (isHighlighted = state)
		[NXApp setHighlightedSquare: self];
	return [self display];
}

/* Private methods: */

- appDidInit {
	return [self appDidInitInside: NO];
}

- appDidInitInside: (BOOL) state {
	return [self setTrackingRect: &frame inside: state];
}

- become: newSquareClass {
	BOOL	wasHighlighted = [self isHighlighted];
	id		theSuperView = [self superview];
	id		newSquare = [[newSquareClass alloc] initFrame: &frame];

	[[[self freeTrackingRect] removeFromSuperview] free];
	[theSuperView addSubview: newSquare];
	return [newSquare appDidInitInside: wasHighlighted];
}

- capture: sender {
	return self;
}

- checkAdjacencyWith: sender {
	return self;
}

- free {
	if ([self isHighlighted])
		[NXApp setHighlightedSquare: nil];
	return [super free];
}

- freeTrackingRect {
	if (trackingRectNumber > 0)
		[[self window] discardTrackingRect: trackingRectNumber];
	return self;
}

#define INSET	2
#define WIDTH	7

- highlight: (const NXRect *) aRect {
	NXRect	theRect = *aRect;

	NXInsetRect(&theRect, INSET, INSET);
	PSsetgray(NX_DKGRAY);
	NXFrameRectWithWidth(&theRect, WIDTH);
	return self;
}

- initFrame: (const NXRect *) frameRect {
	[super initFrame: frameRect];	/* the below only works with small */
	row = frameRect->origin.y / frameRect->size.height; 	/* borders */
	column = frameRect->origin.x / frameRect->size.width;
	return self;
}

- setTrackingRect: (const NXRect *) aRect inside: (BOOL) state {
	static int	rectNumber = 1;
	NXRect	winRect = *aRect;

	trackingRectNumber = rectNumber;
	[[self superview] convertRect: &winRect toView: nil];
	[[self window] setTrackingRect: &winRect
			    inside: state	/* cursor is inside/outside rect */
			    owner: self tag: rectNumber++ left: NO right: NO];
	return self;
}

@end

Listing 3 ± AbstractFilledSquare, a Subclass of AbstractSquare

This class is an important one.  Most mouse-event-driven messages are dispatched into this class, and all legal moves begin here.

File: AbstractFilledSquare.m

#import "AbstractFilledSquare.h"
#import "EmptySquare.h"		/* for foundAdajacent message decl. */

@implementation AbstractFilledSquare

- (BOOL) acceptsFirstMouse {
	return YES;
}

- (float) color {
	return [self subclassResponsibility: _cmd], 0;	/* ", 0" for -Wall */
}

#define FRACTION	0.6

- drawSelf: (const NXRect *) rects : (int) rectCount {
	double	cx = NX_MIDX(&bounds), cy = NX_MIDY(&bounds);

	[super drawSelf: rects : rectCount];
	PSsetgray([self color]);
	PSarc(cx, cy, FRACTION * bounds.size.width / 2, 0, 360);
	PSfill();
	if ([self isSelected])
		[self select: cx : cy];
	return self;
}

- (BOOL) isMyMove {
	return [NXApp isWhiteMove] == [self isWhite];
}

- (BOOL) isSelected {
	return isSelected;
}

- (BOOL) isWhite {
	return [self subclassResponsibility: _cmd], NO;
}

- mouseDown: (NXEvent *) theEvent {
	if ([self isMyMove])
		[self setIsSelected: YES];
	return self;
}

- mouseEntered: (NXEvent *) theEvent {	/* below could check if ANY */
	if ([self isSelected] || ([self isMyMove]	/* move is a legal one */
						&& ![NXApp isSomeoneSelected]))
		[self setIsHighlighted: YES];
	return self;
}

- mouseUp: (NXEvent *) theEvent {
	id	emptySquare = [NXApp highlightedSquare]; /* is EmptySquare */

	if (self != emptySquare && [self isSelected]	   /* emptySquare != nil */
						&& [emptySquare isHighlighted]) {
		id	emptyClass = [emptySquare class];
		short	jump = [self distanceTo: emptySquare];

		[NXApp makeSquaresPerform: @selector(capture:)
					with: [emptySquare become: [self class]]];
		if (jump > 1)
			[self become: emptyClass];
		else
			[self setIsSelected: NO];
		return [NXApp letOtherColorMove];  /* any non-nil return OK */
	}
	return [self setIsSelected: NO];
}

- setIsSelected: (BOOL) state {
	[NXApp setIsSomeoneSelected: isSelected = state];
	return [self display];
}

/* Private methods: */

- capture: sender {
	if ([self class] != [sender class] && [self distanceTo: sender] == 1)
		return [self become: [sender class]];
	return self;
}

- checkAdjacencyWith: sender {	/* can extend here for a toroidal board */
	if ([self isSelected] && [self distanceTo: sender] <= 2)
		[sender foundAdjacent];
	return self;
}

- free {
	if ([self isSelected])
		[NXApp setIsSomeoneSelected: NO];
	return [super free];
}

#define X_OFFSET	9	/* should really be a % of width */
#define X_WIDTH	4

- select: (double) cx : (double) cy {
	PSsetgray(NX_DKGRAY);
	PSsetlinewidth(X_WIDTH);
	PSmoveto(	cx - X_OFFSET,	cy + X_OFFSET	);
	PSlineto(	cx + X_OFFSET,	cy - X_OFFSET	);
	PSmoveto(	cx + X_OFFSET,	cy + X_OFFSET	);
	PSlineto(	cx - X_OFFSET,	cy - X_OFFSET	);
	PSstroke();
	return self;
}

@end

Listing 4 ± EmptySquare, BlockedSquare, Black/WhiteFilledSquare

Here are all the peripheral classes that implement Ergo squares. They inherent almost all their behaviour from their superclasses, and are the real examples of the power of inheritance. Almost nothing needs to be done in their implementations; the class hierarchy of Ergo does all the work.

File: EmptySquare.m

#import "EmptySquare.h"

@implementation EmptySquare

- mouseEntered: (NXEvent *) theEvent {
	[NXApp makeSquaresPerform: @selector(checkAdjacencyWith:)
					 with: self];
	return self;
}	/* above, don't do return [...]; someone may depend on return self */

/* Private methods: */

- foundAdjacent {	/* we were found to be adjacent by the above */
	return [self setIsHighlighted: YES];
}

@end

File: BlockedSquare.m

#import "BlockedSquare.h"

@implementation BlockedSquare

#define FRACTION	0.5
#define BORDER		((1 - FRACTION) / 2)

- drawSelf: (const NXRect *) rects : (int) rectCount {
	NXRect block = {	bounds.origin.x + BORDER * bounds.size.width,
				bounds.origin.y + BORDER * bounds.size.height,
				FRACTION * bounds.size.width,
				FRACTION * bounds.size.height};

	[super drawSelf: rects : rectCount];
	PSsetgray(NX_DKGRAY);
	NXRectFill(&block);
	return self;
}

/* Private methods: */

- appDidInit {
	return self;		/* does nothing (sets no tracking rectangle) */
}

@end

File: BlackFilledSquare.m

#import "BlackFilledSquare.h"

@implementation BlackFilledSquare

- (float) color {
	return NX_BLACK;
}

- (BOOL) isWhite {
	return NO;
}

@end

File: WhiteFilledSquare.m

#import "WhiteFilledSquare.h"

@implementation WhiteFilledSquare

- (float) color {
	return NX_WHITE;
}

- (BOOL) isWhite {
	return YES;
}

@end