Skip to content

A Bindable Custom NSView Subclass

2008 November 27

In this post I’ll walk through writing a custom subclass of NSView that can bind to an NSArrayController to display information either from some generic array connected to the array controller, or from a Core Data object model.

The view we’ll be creating is a custom view to display a pie-chart; in the sample app the pie-chart will be drawn from data entered in an NSTableView. The finished application looks like the following:
The finished sample application showing the custom NSView subclass bound to a Core Data entity

If you’ve read previous posts on this site, you just might possibly have realized that I’m quite a big fan of Core Data. Unsurprisingly, therefore, the sample application is a Core Data document-based app. Its project and code can be downloaded here: customnsview.zip.

We’ll be looking at binding to an array controller’s ‘arrangedObjects’ key, and also its ‘selectionIndexes’ key to enable the user to change which object is selected by clicking in our view. The segments on our pie-chart have both a ‘name’ and a numeric ‘value’ so, in the same way that you can bind a model’s keys to different column tables in an NSTableView, we’ll make it possible to bind a model’s keys to the names and values on the pie-chart. This would allow us to bind, for example, a department’s employees’ ‘fullNames’ and their ‘salaries’ to view relative salaries, or in a file-viewing application, ‘fileNames’ and ‘fileSizes’ etc.

Basic Principles

To be able to display our information in the view, the view needs to maintain its own store of the data. We’ll maintain one array of segment names and one array of segment values. When the view is told to display itself, it will sum the values and use the total to determine how big each segment should be relative to the others; the view will then draw each segment using various geometric functions and bezier paths.

Creating our View

We’ll begin by creating a very basic subclass of NSView to which we can add various functionality throughout this article.

The View Subclass

  • Begin by creating a new Xcode ‘Cocoa Core Data Document-Based Application’, called ‘CustomNSView’.
  • In the new project’s browser, right-click (or control-click) on the Classes group and choose Add -> New File…; in the window that appears, chose the ‘Objective-C NSView Subclass’ and click Next.
  • Name the file ‘MyPieChartView.m’ and make sure the ‘Also create “MyPieChartView.h” checkbox is ticked.
  • After clicking ‘Finish’ to create the files, open up ‘MyPieChartView.m’ and change the template drawRect: method with the one below; we’ll also override the ‘isFlipped’ method to return YES so that our coordinate system has {0,0} set to the top left of the view:
    - (void)drawRect:(NSRect)rect
    {
    	[[NSColor blackColor] set];
    	NSRectFill([self bounds]);
    }
     
    - (BOOL)isFlipped
    {
    	return YES;
    }

Next we need to add an instance of our view to the Window of our document object.

  • Open up MyDocument.xib in Interface Builder and delete the placeholder label from the Window.
  • Drag a custom view onto the Window, set it to resize with the window and use the identity inspector to change the class of the view to ‘MyPieChartView’. My interface looks like this:
    The basic interface for the document

If you build and run the application, you should find that each new document opens a window with our black box view showing.

Adding the Arrays and Bindings

At this point, we need to add a couple of arrays to the view that will hold the names and values of each pie segment.

  • Open the MyPieChartView.h header file and add in two NSArrays for the names and values, together with KVC accessors:
    @interface MyPieChartView : NSView
    {
    	NSArray *_segmentNamesArray;
    	NSArray *_segmentValuesArray;
    }
     
    - (NSArray *)segmentNamesArray;
    - (void)setSegmentNamesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentValuesArray;
    - (void)setSegmentValuesArray:(NSArray *)newArray;
     
    @end
  • We’ll be allowing other objects to bind to these arrays so we’ll need to add an initialize: method to our view that exposes them for binding. Whenever the arrays change, we need to get the view to redisplay itself so in the setter methods we need to call [self setNeedsDisplayInRect:]. We also need to release the stored arrays when the view is deallocated. In MyPieChartView.m, implement these methods:
    + (void)initialize
    {
    	[self exposeBinding:@"segmentNamesArray"];
    	[self exposeBinding:@"segmentValuesArray"];
    }
     
    - (NSArray *)segmentNamesArray
    {
    	return [[_segmentNamesArray retain] autorelease];
    }
     
    - (void)setSegmentNamesArray:(NSArray *)newArray
    {
    	[self willChangeValueForKey:@"segmentNamesArray"];
    	[_segmentNamesArray release];
    	_segmentNamesArray = [newArray copy];
    	[self didChangeValueForKey:@"segmentNamesArray"];
     
    	[self setNeedsDisplayInRect:[self visibleRect]];
    }
     
    - (NSArray *)segmentValuesArray
    {
    	return [[_segmentValuesArray retain] autorelease];
    }
     
    - (void)setSegmentValuesArray:(NSArray *)newArray
    {
    	[self willChangeValueForKey:@"segmentValuesArray"];
    	[_segmentValuesArray release];
    	_segmentValuesArray = [newArray copy];
    	[self didChangeValueForKey:@"segmentValuesArray"];
     
    	[self setNeedsDisplayInRect:[self visibleRect]];
    }
     
    - (void)dealloc
    {
    	[_segmentNamesArray release];
    	[_segmentValuesArray release];
    	[super dealloc];
    }

With our arrays setup, we need to create a data model and some objects to which we can bind.

The Data Model

For the purposes of this demonstration, we need only a very simple data model together with an NSTableView in the interface to add and remove objects.

  • Open MyDocument.xcdatamodel and add a new entity called ‘PieSegment’. Add two attributes, a String and a Decimal, called ‘name’ and ‘amount’ respectively. Give the ‘name’ attribute a default string like ‘new segment’ and ‘amount’ a default value of 0.
  • Return to MyDocument.xib in Interface Builder and add a new NSArrayController called ‘segmentsArrayController’ for the ‘PieSegment’ entity, that prepares content. Bind its Managed Object Context to the File’s Owner managedObjectContext.
  • Add an NSTableView and two buttons to the document window; bind the columns of the table view to the ‘name’ and ‘amount’ path of the segmentsArrayController arrangedObjects key.
  • Add a decimal NSNumberFormatter to the ‘amount’ column.
  • Connect the buttons to the array controller’s add: and remove: actions and make any other adjustments as you wish. My interface looks like this:

Build and Run the application to make sure that the table view and buttons work as expected.

Setting up the Bindings and Displaying the Data

Obviously, nothing is happening yet with our custom NSView since we haven’t bound anything to our names and values arrays, nor have we modified the view’s drawRect: method. We’ll rectify that now:

  • Open the MyDocument.h header file and add in IBOutlets for the array controller and view:
    @interface MyDocument : NSPersistentDocument
    {
    	IBOutlet NSArrayController *segmentsArrayController;
    	IBOutlet NSView *pieChartView;
    }
     
    @end
  • Connect these outlets to the relevant view and controller in Interface Builder.
  • In MyDocument.m, modify the windowControllerDidLoadNib: method so that we bind the view to the array controller:
    - (void)windowControllerDidLoadNib:(NSWindowController *)windowController
    {
    	[super windowControllerDidLoadNib:windowController];
     
    	[pieChartView bind:@"segmentNamesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.name" options:nil];
    	[pieChartView bind:@"segmentValuesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.amount" options:nil];
    }
  • In MyPieChartView.m, modify the drawRect: method with some very simple text display code that will just output the two arrays side-by-side:
    - (void)drawRect:(NSRect)rect
    {
    	[[NSColor blackColor] set];
    	NSRectFill([self bounds]);
     
    	NSPoint midPoint = NSMakePoint(NSMidX([self bounds]), NSMidY([self bounds]));
     
    	NSString *namesText = [[self segmentNamesArray] description];
    	NSString *valuesText = [[self segmentValuesArray] description];
     
    	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys: [NSColor whiteColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:10], NSFontAttributeName, nil];
     
    	NSSize namesSize = [namesText sizeWithAttributes:textAttributes];
    	NSSize valuesSize = [valuesText sizeWithAttributes:textAttributes];
     
    	NSPoint namesTextPoint = NSMakePoint(midPoint.x - namesSize.width - 2, midPoint.y - (namesSize.height / 2));
    	NSPoint valuesTextPoint = NSMakePoint(midPoint.x + valuesSize.width + 2, midPoint.y - (valuesSize.height / 2));
     
    	[namesText drawAtPoint:namesTextPoint withAttributes:textAttributes];
    	[valuesText drawAtPoint:valuesTextPoint withAttributes:textAttributes];
    }

If you build and run the application, you’ll find that values added to the table view will show up in columns in the pie-chart view:

Displaying the Data as a Pie-Chart

Whilst at this point we have a nicely-bound custom NSView subclass, we still need to use the provided data to display a pie-chart. There are many different ways to go about displaying such a chart; what follows is the path I decided to take. You may have different suggestions, in which case please feel free to make comments. The methods I use to calculate the sizes and shapes of the segments may not be the fastest, or least memory-intensive, but should be fairly easily understood by anyone with basic mathematical and geometric knowledge.

We’ll draw the segments of the ‘pie’ using NSBezierPaths. Since it isn’t only the changing of data that can cause a view to be told to draw itself, we’ll ‘cache’ these paths in an array held by the view, updating them only if the data changes. To make it easy to color each segment we’ll maintain a static class array of colors, generated randomly when needed, such that additional instances of our class can also use the same colors.

Adding Drawing Information to our View

We need to add an array pointer to MyPieChartView that will hold the segment paths. We also need a method to access this paths array, and a method that will regenerate it when necessary. We need a method that will produce a random color, and another method that will supply a color from a static colors array if given a zero-based index, generating it if necessary.

  • Change the MyPieChartView.h header file by adding the new array pointer and methods:
    @interface MyPieChartView : NSView
    {
    	NSArray *_segmentNamesArray;
    	NSArray *_segmentValuesArray;
     
    	NSMutableArray *_segmentPathsArray;
    }
     
    - (NSArray *)segmentNamesArray;
    - (void)setSegmentNamesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentValuesArray;
    - (void)setSegmentValuesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentPathsArray;
    - (void)generateDrawingInformation;
     
    - (NSColor *)randomColor;
    - (NSColor *)colorForIndex:(unsigned)index;
     
    @end
  • In MyPieChartView.m implement the segmentPathsArray: method and modify the dealloc: method to release the array:
    - (NSArray *)segmentPathsArray
    {
    	return _segmentPathsArray;
    }
     
    - (void)dealloc
    {
    	[_segmentNamesArray release];
    	[_segmentValuesArray release];
     
    	if( _segmentPathsArray )
    		[_segmentPathsArray release];
     
    	[super dealloc];
    }
  • Implement the randomColor: and colorForIndex: methods like this:
    - (NSColor *)randomColor
    {
    	float red = (random()%1000)/1000.0;
    	float green = (random()%1000)/1000.0;
    	float blue = (random()%1000)/1000.0;
    	float alpha = (random()%1000)/1000.0;
    	return [NSColor colorWithCalibratedRed:red green:green blue:blue alpha:alpha];
    }
     
    - (NSColor *)colorForIndex:(unsigned)index
    {
    	static NSMutableArray *colorsArray = nil;
     
    	if( colorsArray == nil )
    	{
    		colorsArray = [[NSMutableArray alloc] init];
    	}
     
    	if( index >= [colorsArray count] )
    	{
    		unsigned currentNum = 0;
    		for( currentNum = [colorsArray count]; currentNum <= index; currentNum++ )
    		{
    			[colorsArray addObject:[self randomColor]];
    		}
    	}
     
    	return [colorsArray objectAtIndex:index];
    }
  • The generateDrawingInformation: method is quite complicated. It keeps hold of its own pointer to the segmentValuesArray, and uses this to generate the paths array. Here I’m also using #define to set a value for the padding around the pie-chart.
    - (void)generateDrawingInformation
    {
    	// Keep a pointer to the segmentValuesArray
    	NSArray *cachedSegmentValuesArray = [self segmentValuesArray];
     
    	// Get rid of any existing Paths Array
    	if( _segmentPathsArray )
    	{
    		[_segmentPathsArray release];
    		_segmentPathsArray = nil;
    	}
     
    	// If there aren't any values to display, we can exit now
    	if( [cachedSegmentValuesArray count] < 1 )
    		return;
     
    	// Get the sum of the amounts and exit if it is zero
    	float sumOfAmounts = 0;
    	for( NSNumber *eachAmountToSum in cachedSegmentValuesArray )
    		sumOfAmounts += [eachAmountToSum floatValue];
     
    	if( sumOfAmounts == 0 )
    		return;
     
    	_segmentPathsArray = [[NSMutableArray alloc] initWithCapacity:[cachedSegmentValuesArray count]];
     
    #define PADDINGAROUNDGRAPH 20.0
     
    	NSRect viewBounds = [self bounds];
    	NSRect graphRect = NSInsetRect(viewBounds, PADDINGAROUNDGRAPH, PADDINGAROUNDGRAPH);
     
    	// Make the graphRect square and centred
    	if( graphRect.size.width > graphRect.size.height )
    	{
    		double sizeDifference = graphRect.size.width - graphRect.size.height;
    		graphRect.size.width = graphRect.size.height;
    		graphRect.origin.x += (sizeDifference / 2);
    	}
     
    	if( graphRect.size.height > graphRect.size.width )
    	{
    		double sizeDifference = graphRect.size.height - graphRect.size.width;
    		graphRect.size.height = graphRect.size.width;
    		graphRect.origin.y += (sizeDifference / 2);
    	}
     
    	// Calculate how big a 'unit' is
    	float unitSize = (360.0 / sumOfAmounts);
     
    	if( unitSize > 360 )
    		unitSize = 360;
     
    	float radius = graphRect.size.width / 2;
     
    	NSPoint midPoint = NSMakePoint( NSMidX(graphRect), NSMidY(graphRect) );
     
    	// cycle through the segmentValues and create the bezier paths
    	float currentDegree = 0;
    	unsigned currentIndex;
    	for( currentIndex = 0; currentIndex < [cachedSegmentValuesArray count]; currentIndex++ )
    	{
    		NSNumber *eachValue = [cachedSegmentValuesArray objectAtIndex:currentIndex];
     
    		float startDegree = currentDegree;
    		currentDegree += ([eachValue floatValue] * unitSize);
    		float endDegree = currentDegree;
     
    		NSBezierPath *eachSegmentPath = [NSBezierPath bezierPath];
    		[eachSegmentPath moveToPoint:midPoint];
     
    		[eachSegmentPath appendBezierPathWithArcWithCenter:midPoint radius:radius startAngle:startDegree endAngle:endDegree];
    		[eachSegmentPath closePath]; // close path also handles the lines from the midPoint to the start and end of the arc
    		[eachSegmentPath setLineWidth:2.0];
     
    		[_segmentPathsArray addObject:eachSegmentPath];
    	}
    }
  • The paths array should be recalculated whenever the bound arrays change, so modify the two setter methods:
    - (void)setSegmentNamesArray:(NSArray *)newArray
    {
    	[self willChangeValueForKey:@"segmentNamesArray"];
    	[_segmentNamesArray release];
    	_segmentNamesArray = [newArray copy];
    	[self didChangeValueForKey:@"segmentNamesArray"];
     
    	[self generateDrawingInformation];
    	[self setNeedsDisplayInRect:[self visibleRect]];
    }
     
    - (void)setSegmentValuesArray:(NSArray *)newArray
    {
    	[self willChangeValueForKey:@"segmentValuesArray"];
    	[_segmentValuesArray release];
    	_segmentValuesArray = [newArray copy];
    	[self didChangeValueForKey:@"segmentValuesArray"];
     
    	[self generateDrawingInformation];
    	[self setNeedsDisplayInRect:[self visibleRect]];
    }
  • Finally, we can now replace the drawRect: method so that it displays the paths:
    - (void)drawRect:(NSRect)rect
    {
    	[[NSColor whiteColor] set]; // white background
    	NSRectFill([self bounds]);
     
    	NSArray *pathsArray = [self segmentPathsArray];
    	unsigned count;
    	for( count = 0; count < [pathsArray count]; count++ )
    	{
    		NSBezierPath *eachPath = [pathsArray objectAtIndex:count];
     
    		// fill the path with the drawing color for this index
    		[[self colorForIndex:count] set];
    		[eachPath fill];
     
    		// draw a black border around it
    		[[NSColor blackColor] set];
    		[eachPath stroke];
    	}
    }

When you build and run the project, you’ll find that segments added to the table view with non-zero amounts will show up as a basic, colored pie-chart in the view:

Displaying the Text

So far we haven’t done anything at all with the segment names; we still need to display these on our pie-chart. We’ll maintain an array of dictionaries containing information to display the text. We’ll modify the generateDrawingInformation: method so that it creates this array of dictionaries, and change the drawRect: method to display the text on the pie-chart.

  • Open the MyPieChartView.h header file and add in an array for the text information, along with an accessor:
    @interface MyPieChartView : NSView
    {
    	NSArray *_segmentNamesArray;
    	NSArray *_segmentValuesArray;
     
    	NSMutableArray *_segmentPathsArray;
    	NSMutableArray *_segmentTextsArray;
    }
     
    - (NSArray *)segmentNamesArray;
    - (void)setSegmentNamesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentValuesArray;
    - (void)setSegmentValuesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentPathsArray;
    - (NSArray *)segmentTextsArray;
    - (void)generateDrawingInformation;
     
    - (NSColor *)randomColor;
    - (NSColor *)colorForIndex:(unsigned)index;
     
    @end
  • Add the accessor method to MyPieChartView.m and change dealloc: to release it:
    - (NSArray *)segmentTextsArray
    {
    	return _segmentTextsArray;
    }
     
    - (void)dealloc
    {
    	[_segmentNamesArray release];
    	[_segmentValuesArray release];
     
    	if( _segmentPathsArray )
    		[_segmentPathsArray release];
     
    	if( _segmentTextsArray )
    		[_segmentTextsArray release];
     
    	[super dealloc];
    }
  • Now modify the generateDrawingInformation: method so that it stores necessary information in this texts array. We draw each segment arc using two ‘half’ arcs rather than one in order to find out the midpoint from which we decide where to put the text. We also check to see in which ‘quarter’ of the pie-chart view that midpoint falls and offset it by a #defined TEXTPADDING value accordingly, also making sure that the resultant point doesn’t fall outside of the view’s bounds:
    - (void)generateDrawingInformation
    {
    	// Keep pointers to the segmentValuesArray and segmentNamesArray
    	NSArray *cachedSegmentValuesArray = [self segmentValuesArray];
    	NSArray *cachedSegmentNamesArray = [self segmentNamesArray];
     
    	// Get rid of any existing Paths Array
    	if( _segmentPathsArray )
    	{
    		[_segmentPathsArray release];
    		_segmentPathsArray = nil;
    	}
     
    	// Get rid of any existing Texts Array
    	if( _segmentTextsArray )
    	{
    		[_segmentTextsArray release];
    		_segmentTextsArray = nil;
    	}
     
    	// If there aren't any values to display, we can exit now
    	if( [cachedSegmentValuesArray count] < 1 )
    		return;
     
    	// Get the sum of the amounts and exit if it is zero
    	float sumOfAmounts = 0;
    	for( NSNumber *eachAmountToSum in cachedSegmentValuesArray )
    		sumOfAmounts += [eachAmountToSum floatValue];
     
    	if( sumOfAmounts == 0 )
    		return;
     
    	_segmentPathsArray = [[NSMutableArray alloc] initWithCapacity:[cachedSegmentValuesArray count]];
    	_segmentTextsArray = [[NSMutableArray alloc] initWithCapacity:[cachedSegmentValuesArray count]];
     
    #define PADDINGAROUNDGRAPH 20.0
    #define TEXTPADDING 5.0
     
    	NSRect viewBounds = [self bounds];
    	NSRect graphRect = NSInsetRect(viewBounds, PADDINGAROUNDGRAPH, PADDINGAROUNDGRAPH);
     
    	// Make the graphRect square and centred
    	if( graphRect.size.width > graphRect.size.height )
    	{
    		double sizeDifference = graphRect.size.width - graphRect.size.height;
    		graphRect.size.width = graphRect.size.height;
    		graphRect.origin.x += (sizeDifference / 2);
    	}
     
    	if( graphRect.size.height > graphRect.size.width )
    	{
    		double sizeDifference = graphRect.size.height - graphRect.size.width;
    		graphRect.size.height = graphRect.size.width;
    		graphRect.origin.y += (sizeDifference / 2);
    	}
     
    	// get NSRects for the different quarters of the pie-chart
    	NSRect topLeftRect, topRightRect;
    	NSDivideRect(viewBounds, &topLeftRect, &topRightRect, (viewBounds.size.width / 2), NSMinXEdge );
    	NSRect bottomLeftRect, bottomRightRect;
    	NSDivideRect(topLeftRect, &topLeftRect, &bottomLeftRect, (viewBounds.size.height / 2), NSMinYEdge );
    	NSDivideRect(topRightRect, &topRightRect, &bottomRightRect, (viewBounds.size.height / 2), NSMinYEdge );
     
    	// Calculate how big a 'unit' is
    	float unitSize = (360.0 / sumOfAmounts);
     
    	if( unitSize > 360 )
    		unitSize = 360;
     
    	float radius = graphRect.size.width / 2;
     
    	NSPoint midPoint = NSMakePoint( NSMidX(graphRect), NSMidY(graphRect) );
     
    	// Set the text attributes to be used for each textual display
    	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys: [NSColor whiteColor], NSBackgroundColorAttributeName, [NSColor blackColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:12], NSFontAttributeName, nil];
     
    	// cycle through the segmentValues and create the bezier paths
    	// Also add the text details (note we expect the texts' indexes to tie up with the values' indexes)
    	float currentDegree = 0;
    	unsigned currentIndex;
    	for( currentIndex = 0; currentIndex < [cachedSegmentValuesArray count]; currentIndex++ )
    	{
    		NSNumber *eachValue = [cachedSegmentValuesArray objectAtIndex:currentIndex];
     
    		float startDegree = currentDegree;
    		currentDegree += ([eachValue floatValue] * unitSize);
    		float endDegree = currentDegree;
    		float midDegree = startDegree + ((endDegree - startDegree) / 2);
     
    		NSBezierPath *eachSegmentPath = [NSBezierPath bezierPath];
    		[eachSegmentPath moveToPoint:midPoint];
     
    		[eachSegmentPath appendBezierPathWithArcWithCenter:midPoint radius:radius startAngle:startDegree endAngle:midDegree clockwise:NO];
     
    		NSPoint textPoint = [eachSegmentPath currentPoint];
     
    		[eachSegmentPath appendBezierPathWithArcWithCenter:midPoint radius:radius startAngle:midDegree endAngle:endDegree clockwise:NO];
     
    		[eachSegmentPath closePath]; // close path also handles the lines from the midPoint to the start and end of the arc
    		[eachSegmentPath setLineWidth:2.0];
     
    		[_segmentPathsArray addObject:eachSegmentPath];
     
    		// Get the text to be displayed, if it exists, and see how big it is
    		NSString *eachText = @"";
    		if( [cachedSegmentNamesArray count] > currentIndex )
    			eachText = [cachedSegmentNamesArray objectAtIndex:currentIndex];
     
    		NSSize textSize = [eachText sizeWithAttributes:textAttributes];
     
    		// Offset it by TEXTPADDING in direction suitable for whichever quarter of the view it is in
    		if( NSPointInRect(textPoint, topLeftRect) )
    		{
    			textPoint.y -= (textSize.height + TEXTPADDING);
    			textPoint.x -= (textSize.width + TEXTPADDING);
    		}
    		else if( NSPointInRect(textPoint, topRightRect) )
    		{
    			textPoint.y -= (textSize.height + TEXTPADDING);
    			textPoint.x += TEXTPADDING;
    		}
    		else if( NSPointInRect(textPoint, bottomLeftRect) )
    		{
    			textPoint.y += TEXTPADDING;
    			textPoint.x -= (textSize.width + TEXTPADDING);
    		}
    		else if( NSPointInRect(textPoint, bottomRightRect) )
    		{
    			textPoint.y += TEXTPADDING;
    			textPoint.x += TEXTPADDING;
    		}
     
    		// Make sure the point isn't outside the view's bounds
    		if( textPoint.x < viewBounds.origin.x )
    			textPoint.x = viewBounds.origin.x;
     
    		if( (textPoint.x + textSize.width) > (viewBounds.origin.x + viewBounds.size.width) )
    			textPoint.x = viewBounds.origin.x + viewBounds.size.width - textSize.width;
     
    		if( textPoint.y < viewBounds.origin.y )
    			textPoint.y = viewBounds.origin.y;
     
    		if( (textPoint.y + textSize.height) > (viewBounds.origin.y + viewBounds.size.height) )
    			textPoint.y = viewBounds.origin.y + viewBounds.size.height - textSize.height;
     
    		// Finally add the details as a dictionary to our segmentTextsArray.
    		// We include here the textAttributes lest we decide later to e.g. color the texts the same color as the segment fill
    		[_segmentTextsArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:textPoint.x], @"textPointX", [NSNumber numberWithFloat:textPoint.y], @"textPointY", eachText, @"text", textAttributes, @"textAttributes", nil]];
    	}
    }
  • Modify the drawRect: method to display the text:
    - (void)drawRect:(NSRect)rect
    {
    	[[NSColor whiteColor] set]; // white background
    	NSRectFill([self bounds]);
     
    	NSArray *pathsArray = [self segmentPathsArray];
    	unsigned count;
    	for( count = 0; count < [pathsArray count]; count++ )
    	{
    		NSBezierPath *eachPath = [pathsArray objectAtIndex:count];
     
    		// fill the path with the drawing color for this index
    		[[self colorForIndex:count] set];
    		[eachPath fill];
     
    		// draw a black border around it
    		[[NSColor blackColor] set];
    		[eachPath stroke];
    	}
     
    	NSArray *textsArray = [self segmentTextsArray];
     
    	for( count = 0; count < [textsArray count]; count++ )
    	{
    		NSDictionary *eachTextDictionary = [textsArray objectAtIndex:count];
    		NSPoint textPoint = NSMakePoint( [[eachTextDictionary valueForKey:@"textPointX"] floatValue], [[eachTextDictionary valueForKey:@"textPointY"] floatValue] );
     
    		NSDictionary *textAttributes = [eachTextDictionary valueForKey:@"textAttributes"];
     
    		NSString *text = [eachTextDictionary valueForKey:@"text"];
    		[text drawAtPoint:textPoint withAttributes:textAttributes];
    	}
    }

The view should now display text on the chart:

Indicating the Selected Segment(s)

In order to display which segment is selected, we need to bind to the array controller’s ‘selectionIndexes'; the view needs to keep its own local copy of these indexes, and expose binding for them:

  • Add an NSIndexSet for the selectionIndexes to the MyPieChartView.h header file, along with a getter and setter:
    @interface MyPieChartView : NSView
    {
    	NSArray *_segmentNamesArray;
    	NSArray *_segmentValuesArray;
     
    	NSMutableArray *_segmentPathsArray;
    	NSMutableArray *_segmentTextsArray;
     
    	NSIndexSet *_selectionIndexes;
    }
     
    - (NSArray *)segmentNamesArray;
    - (void)setSegmentNamesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentValuesArray;
    - (void)setSegmentValuesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentPathsArray;
    - (NSArray *)segmentTextsArray;
    - (void)generateDrawingInformation;
     
    - (NSColor *)randomColor;
    - (NSColor *)colorForIndex:(unsigned)index;
     
    - (NSIndexSet *)selectionIndexes;
    - (void)setSelectionIndexes:(NSIndexSet *)newIndexes;
     
    @end
  • Implement these accessor methods and expose the binding:
    - (NSIndexSet *)selectionIndexes
    {
        return [[_selectionIndexes retain] autorelease];
    }
     
    - (void)setSelectionIndexes:(NSIndexSet *)newIndexes
    {
    	if ((_selectionIndexes != newIndexes) && (![_selectionIndexes isEqualToIndexSet:newIndexes]))
    	{
    		[self willChangeValueForKey:@"selectionIndexes"];
    		[_selectionIndexes release];
    		_selectionIndexes = [newIndexes copy];
    		[self didChangeValueForKey:@"selectionIndexes"];
     
    		[self generateDrawingInformation];
    		[self setNeedsDisplayInRect:[self visibleRect]];
    	}
    }
     
    + (void)initialize
    {
    	[self exposeBinding:@"segmentNamesArray"];
    	[self exposeBinding:@"segmentValuesArray"];
    	[self exposeBinding:@"selectionIndexes"];
    }
  • In MyDocument.m, bind the selectionIndexes to the array controller:
    - (void)windowControllerDidLoadNib:(NSWindowController *)windowController
    {
    	[super windowControllerDidLoadNib:windowController];
     
    	[pieChartView bind:@"segmentNamesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.name" options:nil];
    	[pieChartView bind:@"segmentValuesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.amount" options:nil];
    	[pieChartView bind:@"selectionIndexes" toObject:segmentsArrayController withKeyPath:@"selectionIndexes" options:nil];
    }
  • We also need to modify the generateDrawingInformation: method to offset the selected segment (and its text) by a #defined amount:
    - (void)generateDrawingInformation
    {
    	// Keep pointers to the segmentValuesArray and segmentNamesArray
    	NSArray *cachedSegmentValuesArray = [self segmentValuesArray];
    	NSArray *cachedSegmentNamesArray = [self segmentNamesArray];
     
    	// Get rid of any existing Paths Array
    	if( _segmentPathsArray )
    	{
    		[_segmentPathsArray release];
    		_segmentPathsArray = nil;
    	}
     
    	// Get rid of any existing Texts Array
    	if( _segmentTextsArray )
    	{
    		[_segmentTextsArray release];
    		_segmentTextsArray = nil;
    	}
     
    	// If there aren't any values to display, we can exit now
    	if( [cachedSegmentValuesArray count] < 1 )
    		return;
     
    	// Get the sum of the amounts and exit if it is zero
    	float sumOfAmounts = 0;
    	for( NSNumber *eachAmountToSum in cachedSegmentValuesArray )
    		sumOfAmounts += [eachAmountToSum floatValue];
     
    	if( sumOfAmounts == 0 )
    		return;
     
    	_segmentPathsArray = [[NSMutableArray alloc] initWithCapacity:[cachedSegmentValuesArray count]];
    	_segmentTextsArray = [[NSMutableArray alloc] initWithCapacity:[cachedSegmentValuesArray count]];
     
    	NSIndexSet *selectionIndexes = [self selectionIndexes];
    	BOOL shouldOffsetSelectedSegment = ([selectionIndexes count] > 0) ? YES : NO;
     
    #define PADDINGAROUNDGRAPH 20.0
    #define TEXTPADDING 5.0
    #define SELECTEDSEGMENTOFFSET 5.0
     
    	NSRect viewBounds = [self bounds];
    	NSRect graphRect = NSInsetRect(viewBounds, PADDINGAROUNDGRAPH, PADDINGAROUNDGRAPH);
     
    	// Make the graphRect square and centred
    	if( graphRect.size.width > graphRect.size.height )
    	{
    		double sizeDifference = graphRect.size.width - graphRect.size.height;
    		graphRect.size.width = graphRect.size.height;
    		graphRect.origin.x += (sizeDifference / 2);
    	}
     
    	if( graphRect.size.height > graphRect.size.width )
    	{
    		double sizeDifference = graphRect.size.height - graphRect.size.width;
    		graphRect.size.height = graphRect.size.width;
    		graphRect.origin.y += (sizeDifference / 2);
    	}
     
    	// get NSRects for the different quarters of the pie-chart
    	NSRect topLeftRect, topRightRect;
    	NSDivideRect(viewBounds, &topLeftRect, &topRightRect, (viewBounds.size.width / 2), NSMinXEdge );
    	NSRect bottomLeftRect, bottomRightRect;
    	NSDivideRect(topLeftRect, &topLeftRect, &bottomLeftRect, (viewBounds.size.height / 2), NSMinYEdge );
    	NSDivideRect(topRightRect, &topRightRect, &bottomRightRect, (viewBounds.size.height / 2), NSMinYEdge );
     
    	// Calculate how big a 'unit' is
    	float unitSize = (360.0 / sumOfAmounts);
     
    	if( unitSize > 360 )
    		unitSize = 360;
     
    	float radius = graphRect.size.width / 2;
     
    	NSPoint midPoint = NSMakePoint( NSMidX(graphRect), NSMidY(graphRect) );
     
    	// Set the text attributes to be used for each textual display
    	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys: [NSColor whiteColor], NSBackgroundColorAttributeName, [NSColor blackColor], NSForegroundColorAttributeName, [NSFont systemFontOfSize:12], NSFontAttributeName, nil];
     
    	// cycle through the segmentValues and create the bezier paths
    	// Also add the text details (note we expect the texts' indexes to tie up with the values' indexes)
    	float currentDegree = 0;
    	unsigned currentIndex;
    	for( currentIndex = 0; currentIndex < [cachedSegmentValuesArray count]; currentIndex++ )
    	{
    		NSNumber *eachValue = [cachedSegmentValuesArray objectAtIndex:currentIndex];
     
    		float startDegree = currentDegree;
    		currentDegree += ([eachValue floatValue] * unitSize);
    		float endDegree = currentDegree;
    		float midDegree = startDegree + ((endDegree - startDegree) / 2);
     
    		NSBezierPath *eachSegmentPath = [NSBezierPath bezierPath];
    		[eachSegmentPath moveToPoint:midPoint];
     
    		[eachSegmentPath appendBezierPathWithArcWithCenter:midPoint radius:radius startAngle:startDegree endAngle:midDegree clockwise:NO];
     
    		NSPoint textPoint = [eachSegmentPath currentPoint];
     
    		[eachSegmentPath appendBezierPathWithArcWithCenter:midPoint radius:radius startAngle:midDegree endAngle:endDegree clockwise:NO];
     
    		[eachSegmentPath closePath]; // close path also handles the lines from the midPoint to the start and end of the arc
    		[eachSegmentPath setLineWidth:2.0];
     
    		// Check to see whether we should offset this segment if it's currently selected in the array controller
    		if( shouldOffsetSelectedSegment && [selectionIndexes containsIndex:currentIndex] )
    		{
    			float differenceRatio = (SELECTEDSEGMENTOFFSET / radius) + (SELECTEDSEGMENTOFFSET / (endDegree - startDegree));
     
    			float diffY = (textPoint.y - midPoint.y) * differenceRatio;
    			float diffX = (textPoint.x - midPoint.x) * differenceRatio;
     
    			NSAffineTransform *transform = [NSAffineTransform transform];
     
    			[transform translateXBy:diffX yBy:diffY];
    			[eachSegmentPath transformUsingAffineTransform: transform];
     
    			textPoint = [transform transformPoint:textPoint];
    		}
     
    		[_segmentPathsArray addObject:eachSegmentPath];
     
    		// Get the text to be displayed, if it exists, and see how big it is
    		NSString *eachText = @"";
    		if( [cachedSegmentNamesArray count] > currentIndex )
    			eachText = [cachedSegmentNamesArray objectAtIndex:currentIndex];
     
    		NSSize textSize = [eachText sizeWithAttributes:textAttributes];
     
    		// Offset it by TEXTPADDING in direction suitable for whichever quarter of the view it is in
    		if( NSPointInRect(textPoint, topLeftRect) )
    		{
    			textPoint.y -= (textSize.height + TEXTPADDING);
    			textPoint.x -= (textSize.width + TEXTPADDING);
    		}
    		else if( NSPointInRect(textPoint, topRightRect) )
    		{
    			textPoint.y -= (textSize.height + TEXTPADDING);
    			textPoint.x += TEXTPADDING;
    		}
    		else if( NSPointInRect(textPoint, bottomLeftRect) )
    		{
    			textPoint.y += TEXTPADDING;
    			textPoint.x -= (textSize.width + TEXTPADDING);
    		}
    		else if( NSPointInRect(textPoint, bottomRightRect) )
    		{
    			textPoint.y += TEXTPADDING;
    			textPoint.x += TEXTPADDING;
    		}
     
    		// Make sure the point isn't outside the view's bounds
    		if( textPoint.x < viewBounds.origin.x )
    			textPoint.x = viewBounds.origin.x;
     
    		if( (textPoint.x + textSize.width) > (viewBounds.origin.x + viewBounds.size.width) )
    			textPoint.x = viewBounds.origin.x + viewBounds.size.width - textSize.width;
     
    		if( textPoint.y < viewBounds.origin.y )
    			textPoint.y = viewBounds.origin.y;
     
    		if( (textPoint.y + textSize.height) > (viewBounds.origin.y + viewBounds.size.height) )
    			textPoint.y = viewBounds.origin.y + viewBounds.size.height - textSize.height;
     
    		// Finally add the details as a dictionary to our segmentTextsArray.
    		// We include here the textAttributes lest we decide later to e.g. color the texts the same color as the segment fill
    		[_segmentTextsArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:textPoint.x], @"textPointX", [NSNumber numberWithFloat:textPoint.y], @"textPointY", eachText, @"text", textAttributes, @"textAttributes", nil]];
    	}
    }
  • We’ll also modify the drawRect: method so that it fills selected segments with blue rather than the usual colour:
    - (void)drawRect:(NSRect)rect
    {
    	[[NSColor whiteColor] set]; // white background
    	NSRectFill([self bounds]);
     
    	NSArray *pathsArray = [self segmentPathsArray];
    	unsigned count;
    	for( count = 0; count < [pathsArray count]; count++ )
    	{
    		NSBezierPath *eachPath = [pathsArray objectAtIndex:count];
     
    		// fill the path with the drawing color for this index unless it's selected
    		if( [[self selectionIndexes] containsIndex:count] )
    			[[NSColor blueColor] set];
    		else
    			[[self colorForIndex:count] set];
     
    		[eachPath fill];
     
    		// draw a black border around it
    		[[NSColor blackColor] set];
    		[eachPath stroke];
    	}
     
    	NSArray *textsArray = [self segmentTextsArray];
     
    	for( count = 0; count < [textsArray count]; count++ )
    	{
    		NSDictionary *eachTextDictionary = [textsArray objectAtIndex:count];
    		NSPoint textPoint = NSMakePoint( [[eachTextDictionary valueForKey:@"textPointX"] floatValue], [[eachTextDictionary valueForKey:@"textPointY"] floatValue] );
     
    		NSDictionary *textAttributes = [eachTextDictionary valueForKey:@"textAttributes"];
     
    		NSString *text = [eachTextDictionary valueForKey:@"text"];
    		[text drawAtPoint:textPoint withAttributes:textAttributes];
    	}
    }

Now the view should display the selected segment as desired, updating when the user clicks on a different line in the table view:

If you wish, go into Interface Builder and change the selection type of the table view to ‘Multiple’. You should find that you can shift-click multiple items in the table view and they will show as desired in the chart view.

Resizing Issues

Since we cache the array of segment paths, only updating it when the arrays of data are changed, you might have noticed that the view doesn’t behave very well when we resize it. Assuming you set the view to resize with its window, whilst the white background will enlarge, the actual paths won’t change. This is easy to rectify:

  • Simply check in the drawRect: method whether we are in the middle of a ‘liveResize’ and, if so, update the drawing information arrays:
    - (void)drawRect:(NSRect)rect
    {
    	[[NSColor whiteColor] set]; // white background
    	NSRectFill([self bounds]);
     
    	if( [self inLiveResize] )
    	{
    		[self generateDrawingInformation];
    	}
     
    	NSArray *pathsArray = [self segmentPathsArray];
    	unsigned count;
    	for( count = 0; count < [pathsArray count]; count++ )
    	{
    		NSBezierPath *eachPath = [pathsArray objectAtIndex:count];
     
    		// fill the path with the drawing color for this index unless it's selected
    		if( [[self selectionIndexes] containsIndex:count] )
    			[[NSColor blueColor] set];
    		else
    			[[self colorForIndex:count] set];
     
    		[eachPath fill];
     
    		// draw a black border around it
    		[[NSColor blackColor] set];
    		[eachPath stroke];
    	}
     
    	NSArray *textsArray = [self segmentTextsArray];
     
    	for( count = 0; count < [textsArray count]; count++ )
    	{
    		NSDictionary *eachTextDictionary = [textsArray objectAtIndex:count];
    		NSPoint textPoint = NSMakePoint( [[eachTextDictionary valueForKey:@"textPointX"] floatValue], [[eachTextDictionary valueForKey:@"textPointY"] floatValue] );
     
    		NSDictionary *textAttributes = [eachTextDictionary valueForKey:@"textAttributes"];
     
    		NSString *text = [eachTextDictionary valueForKey:@"text"];
    		[text drawAtPoint:textPoint withAttributes:textAttributes];
    	}
    }

The view will now ‘live resize’ itself as would be expected.

Changing the Selection Indexes when the user clicks a Segment

We have already set up the view to display a segment ‘selected’ in the table view, but it would be nice if the user could also click on a segment to select it. Given that we store an array of segment paths inside the view, it should be pretty easy to ‘hit-test’ each path to determine which segment was clicked.

  • To receive clicks in our view, we need to override the acceptsFirstResponder: method for our view to return YES so add the following to MyPieChartView.m:
    - (BOOL)acceptsFirstResponder
    {
    	return YES;
    }
  • Next we’ll implement a method to determine which segment was clicked so add the method declaration to MyPieChartView.h:
    - (int)objectIndexForPoint:(NSPoint)thePoint;
  • And implement it in MyPieChartView.m:
    - (int)objectIndexForPoint:(NSPoint)thePoint
    {
    	NSArray *cachedPathsArray = [self segmentPathsArray];
     
    	int count;
    	for( count = 0; count < [cachedPathsArray count]; count++ )
    	{
    		NSBezierPath *eachPath = [cachedPathsArray objectAtIndex:count];
     
    		if( [eachPath containsPoint:thePoint ] )
    		{
    			return count;
    		}
    	}
     
    	// if control reaches here, no segment contained the point so return -1
    	return -1;
    }
  • We also need to implement a mouseUp: method that will be called when the user has clicked on a segment. Expected behavior varies depending on whether the user is holding down a modifier key at the time the segment was clicked; holding down the command key should either add or remove the clicked segment to the current selection; holding down the shift key and clicking should select a range of segments between the existing selection and the clicked segment:
    - (void)mouseUp:(NSEvent *)theEvent
    {
    	int index = [self objectIndexForPoint:[self convertPoint:[theEvent locationInWindow] fromView:nil]];
     
    	NSMutableIndexSet *newSelectionIndexes = [[self selectionIndexes] mutableCopy];
     
    	if ( [theEvent modifierFlags] & NSCommandKeyMask )
    	{
    		// Add or remove the clicked segment
    		if ( [newSelectionIndexes containsIndex:index] )
    		{
    			[newSelectionIndexes removeIndex:index];
    		}
    		else
    		{
    			[newSelectionIndexes addIndex:index];
    		}
    	}
    	else if ( [theEvent modifierFlags] & NSShiftKeyMask )
    	{
    		// Add range to selection
    		if ([newSelectionIndexes count] == 0)
    		{
    			[newSelectionIndexes addIndex:index];
    		}
    		else
    		{
    			unsigned int origin = (index < [newSelectionIndexes lastIndex]) ? index :[newSelectionIndexes lastIndex];
    			unsigned int length = (index < [newSelectionIndexes lastIndex]) ? [newSelectionIndexes lastIndex] - index : index - [newSelectionIndexes lastIndex];
     
    			length++;
    			[newSelectionIndexes addIndexesInRange:NSMakeRange(origin, length)];
    		}
    	}
    	else // the user just clicked without modifier keys so simply select the segment
    	{
    		[newSelectionIndexes removeAllIndexes];
    		if( index >= 0 )
    			[newSelectionIndexes addIndex:index];
    	}
     
    	[self setSelectionIndexes:newSelectionIndexes];
    	[newSelectionIndexes release];
    }
  • Finally we need to bind the selectionIndexes of our view to the selectionIndexes of the array controller so that it updates the table view as well. Add this last binding in MyDocument.m:
    - (void)windowControllerDidLoadNib:(NSWindowController *)windowController
    {
    	[super windowControllerDidLoadNib:windowController];
     
    	[pieChartView bind:@"segmentNamesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.name" options:nil];
    	[pieChartView bind:@"segmentValuesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.amount" options:nil];
    	[pieChartView bind:@"selectionIndexes" toObject:segmentsArrayController withKeyPath:@"selectionIndexes" options:nil];
    	[segmentsArrayController bind:@"selectionIndexes" toObject:pieChartView withKeyPath:@"selectionIndexes" options:nil];
    }

When the user clicks on a segment in our chart view, that segment should become selected and also change the selection in the table view; modifier keys should also affect the way that segments become selected.

One Last Binding

Just in case you haven’t had enough bindings yet, we’ll add one final feature to the view – the ability to rotate the chart, perhaps so the user can better fit the related text or place one segment in the middle etc.

  • We’ll start by adding a ‘rotationAmount’ attribute to our view, along with the necessary accessors in MyPieChartView.h:
    @interface MyPieChartView : NSView
    {
    	NSArray *_segmentNamesArray;
    	NSArray *_segmentValuesArray;
     
    	NSMutableArray *_segmentPathsArray;
    	NSMutableArray *_segmentTextsArray;
     
    	NSIndexSet *_selectionIndexes;
     
    	float _rotationAmount;
    }
     
    - (NSArray *)segmentNamesArray;
    - (void)setSegmentNamesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentValuesArray;
    - (void)setSegmentValuesArray:(NSArray *)newArray;
     
    - (NSArray *)segmentPathsArray;
    - (NSArray *)segmentTextsArray;
    - (void)generateDrawingInformation;
     
    - (NSColor *)randomColor;
    - (NSColor *)colorForIndex:(unsigned)index;
     
    - (NSIndexSet *)selectionIndexes;
    - (void)setSelectionIndexes:(NSIndexSet *)newIndexes;
     
    - (int)objectIndexForPoint:(NSPoint)thePoint;
     
    - (void)setRotationAmount:(NSNumber *)value;
    - (NSNumber *)rotationAmount;
     
    @end
  • Expose the binding for the rotationAmount attribute and add the accessor methods to MyPieChartView.m:
    + (void)initialize
    {
    	[self exposeBinding:@"segmentNamesArray"];
    	[self exposeBinding:@"segmentValuesArray"];
    	[self exposeBinding:@"selectionIndexes"];
    	[self exposeBinding:@"rotationAmount"];
    }
     
    - (void)setRotationAmount:(NSNumber *)value
    {
    	if( [value floatValue] != _rotationAmount )
    	{
    		[self willChangeValueForKey:@"rotationAmount"];
    		_rotationAmount = [value floatValue];
    		[self didChangeValueForKey:@"rotationAmount"];
     
    		[self generateDrawingInformation];
    		[self setNeedsDisplayInRect:[self visibleRect]];
    	}
    }
     
    - (NSNumber *)rotationAmount
    {
    	return [NSNumber numberWithFloat:_rotationAmount];
    }
  • To make use of the rotation amount, we simply need to modify our drawing code to set the ‘currentDegree’ to the rotation amount so find and change this in generateDrawingInformation::
    	// cycle through the segmentValues and create the bezier paths
    	// Also add the text details (note we expect the texts' indexes to tie up with the values' indexes)
    	float currentDegree = [[self rotationAmount] floatValue];
    	unsigned currentIndex;
  • Next we need to have a control to rotate the graph so open up MyDocument.xib in InterfaceBuilder and add a horizontal slider control; set its minimum and maximum values to 0.0 and 360.0, and its current value to 0.0; tick the ‘Continuous’ box in the inspector. My interface looks like this:
  • Back in Xcode, add an IBOutlet for the new slider to MyDocument.h:
    @interface MyDocument : NSPersistentDocument
    {
    	IBOutlet NSArrayController *segmentsArrayController;
    	IBOutlet NSView *pieChartView;
    	IBOutlet NSSlider *sliderRotationControl;
    }
     
    @end
  • Connect the outlet to the control in Interface Builder.
  • In MyDocument.m, bind the rotation control’s value to the rotationAmount on the chart view:
    - (void)windowControllerDidLoadNib:(NSWindowController *)windowController
    {
    	[super windowControllerDidLoadNib:windowController];
     
    	[pieChartView bind:@"segmentNamesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.name" options:nil];
    	[pieChartView bind:@"segmentValuesArray" toObject:segmentsArrayController withKeyPath:@"arrangedObjects.amount" options:nil];
    	[pieChartView bind:@"selectionIndexes" toObject:segmentsArrayController withKeyPath:@"selectionIndexes" options:nil];
    	[segmentsArrayController bind:@"selectionIndexes" toObject:pieChartView withKeyPath:@"selectionIndexes" options:nil];
    	[sliderRotationControl bind:@"value" toObject:pieChartView withKeyPath:@"rotationAmount" options:nil];
    }

That’s It!

At this point you should have a pretty functional, bindable pie-chart view. For such a view it would make sense to create an Interface Builder palette so that the bindings can be setup visually in Interface Builder rather having to do it programmatically; this may well be a topic for a future post!

Share
6 Responses leave one →
  1. December 10, 2008

    Really great tutorial. This is a problem that I tried to work out a few weeks ago and decided to go on another route because it seemed better at the time. This tutorial might just make me have another go though. You rock.

  2. December 22, 2008

    Excellent tutorial. Thanks so much.

  3. March 5, 2009

    This is truely amazing stuff, thank for for the awesome work. You’ve covered so much in the area’s of my interest. Custom Views, bindings, drawing. This is one blog that I will keep coming back to.

  4. Christiaan permalink
    April 8, 2009

    Very nice tutorial, thanks. One fix you should consider though is to make the selectionIndexes binding two-way. That is the way bindings like this are supposed to work. Bindings should always go from view to controller and from controller to data, never the other way around. That avoids infinite update loops and retain cycles. The way to do that is to add the following at the end of mouseUp:

    NSDictionary *info = [self infoForBinding:@”selectionIndexes”];
    [[info objectForKey:NSObservedObjectKey] setValue:[self selectionIndexes] forKeyPath:[info objectForKey:NSObservedKeyPathKey]];

  5. April 24, 2009

    Once again :) Two perfect hits in way day. That really speaks for your tutorials.

  6. Ian Fairchild permalink
    April 25, 2009

    What a fantastic tutorial! I learned more in the last half hour than in the last month. Thank you, thank you.

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS