Gloss-Caustic Shading

Posted by Pioneer! Wed, 10 Dec 2008 03:25:45 GMT

Matt Gallagher recently wrote a very useful article about drawing gloss gradients using Core Graphics. In his article, Matt describes how to reproduce the oft-seen glossy gradient effect. Thanks Matt! It’s a nice article. “Cocoa with Love” lovingly provides the working source code. This little article aims to complement Matt’s work.

I’ve also re-factored the software and packaged the result within an Objective-C class called RRGlossCausticShader. This packaging automatically adds support for key-value coding and observing. Bindings then let you easily wrap the class within a little application able to adjust the many parameters interactively.

Show me the money

Before going any further, you might first want to see the results. I know I would! Download the project to play with the shader. Compile and run the application using Xcode. The main window appears as follows. Inspector panels also appear from where you can interactively adjust the many shading parameters.

Gloss Caustic Shader Window

Gloss above, caustic below. Those are the two halves of the shading. Gloss appears in the top half, caustic in the bottom half. Within each half, an exponential function blends a given non-caustic base colour with whiteness above and with matching caustic colour below. The main window displays these basic elements of the shading process: the shading itself (left), non-caustic base colour (top right) and coefficient of the exponent function (bottom right).

Re-factoring

My version of Matt’s work includes some re-factoring. Rather than just the one functional interface, the version that you’ll find within the project involves three modular utility classes.

  • RRGlossCausticShader encapsulates shading
  • RRCausticColorMatcher finds caustic matches for given colours
  • RRExponentialFunction wraps the exponential maths
  • RRLuminanceFromRGBComponents returns luminance from RGB triples

This re-factoring has some benefits. First, it promotes re-use. Other requirements may call for RGB-to-luminance conversion, or a generic exponential function for example; or you might want to sub-class the caustic colour matcher and add some customisation. Separating out the individual sub-requirements adds some extra scope for growth. I think it also makes the source code somewhat easier to follow and therefore comprehend. But that might just be my own personal opinion. Your mileage may vary, as they say!

Anyway, the purpose of this article is to outline some of the differences in implementation and the underlying thinking. Comments welcome, of course.

Twinkle, twinkle

The gloss effect on the upper half of the gradient derives from the base colour’s luminosity. So obtaining luminance from a given Red, Green and Blue is part of the glossing requirement.

As you can see below, I replace Matt’s conversion with the one described at OpenGL. The difference is small, almost non-existent. Only, the comment that “Haeberli notes that [the YIQ colour conversion values used by Matt] are incorrect in a linear RGB colour space” convinced me to make the small change. Just call me fussy.

CGFloat RRLuminanceFromRGBComponents(const CGFloat *rgb)
{
    // 0.3086 + 0.6094 + 0.0820 = 1.0
    return 0.3086f*rgb[0] + 0.6094f*rgb[1] + 0.0820f*rgb[2];
}

Notice a few other things. I’m using CGFloat type for floats because this corresponds exactly with Apple’s usage for colour components. Can we always assume that CGFloat equals float? No. In fact, CGFloat is sometimes double! This happens when 64-bit compiling for instance. Argument const-correctness is another feature.

Caustic nature

The lower half of the gradient blends the base colour into shades of yellow (yellow is the default caustic hue). The lower end of the gradient starts to look more-and-more tinted towards the yellow. However, the precise colour depends upon the non-caustic base colour. For blue colours, for example, the default caustic switches to magenta. Blue looks better fading toward magenta, rather than yellow. In all cases though, the amount of caustic to non-caustic blending varies according to the cosine of the distance between the caustic and non-caustic hues.

So rather than having a function handle the caustic colour matching and implementing the adjustable parameters as manifest constants, the new version implements a “colour matching” class. Interface listed below. The class implements adjustable parameters as read-write class properties with all the necessary setters and getters. This approach buys some advantages. It automatically adds support for key-value coding and observing. In practice, it means that user-interface controls can bind to these properties using Cocoa Bindings.

@interface RRCausticColorMatcher : NSObject
{
    CGFloat causticHue;
        // Yellow by default.
    CGFloat graySaturationThreshold;
        // Saturation level at which colours appear grey. Below this level,
        // matcher response snaps to pure caustic.
    CGFloat causticSaturationForGrays;
        // Defines the caustic saturation for grey colours. Grey colours fall
        // below the grey saturation threshold. When saturation drops too low,
        // everything looks grey.
    CGFloat redHueThreshold;
        // Colours at this threshold and above match to default caustics rather
        // than default magenta for blues.
    CGFloat blueHueThreshold;
        // Triggers a switch to magenta caustics. Hues at blue and beyond
        // display magenta-modulated caustics by default.
    CGFloat blueCausticHue;
        // Magenta by default. Magenta caustics for blue colours.
    CGFloat causticFractionDomainFactor;
        // Expands or contracts the caustic fraction's domain. With factor equal
        // to 1, non-caustic and caustic hues blend according to the cosine of
        // their difference. Smaller the difference, greater the amount of
        // caustic hue. Defaults to 1.4 meaning that the point of absolutely no
        // caustic blending occurs at 1/1.4 difference from caustic hue. Try
        // plotting cos(x*pi*1.4) in the -1,1 interval.
    CGFloat causticFractionRangeFactor;
        // Scales the caustic fraction which without a factor outputs a blending
        // fraction between 0 and 1 in favour of the caustic blend. Defaults to
        // 0.6 which scales down the amount of caustic hue-and-brightness by
        // that amount.
}

- (NSColor *)matchForColor:(NSColor *)aColor;
    // Matches the given colour. Answers a matching caustic colour. The result
    // shifts hue and brightness towards yellow. Saturation remains unchanged.
- (void)matchForHSB:(const CGFloat *)hsb caustic:(CGFloat *)outHSB;
    // Does the work.

@property(assign) CGFloat causticHue;
@property(assign) CGFloat graySaturationThreshold;
@property(assign) CGFloat causticSaturationForGrays;
@property(assign) CGFloat redHueThreshold;
@property(assign) CGFloat blueHueThreshold;
@property(assign) CGFloat blueCausticHue;
@property(assign) CGFloat causticFractionDomainFactor;
@property(assign) CGFloat causticFractionRangeFactor;

@end

Exponentially

Shading employs an exponential function. As the gradient progresses from 0 to 0.5, gloss white blending increases exponentially in the 0,1 interval. Progressing through the bottom half of the gradient from 0.5 to 1, caustic blending increases exponentially. The implementation re-factors the exponential function. Interface listing below. It becomes a C-style object class and thereby minimises its dependencies: just plain C types and standard library math.h and hence very suitable for repeated invocations from the bowels of a shading function.

// Encapsulates an optimising generic Exponential Function where
//      y=1-(exp(x*-c)-exp(-c))/(1-exp(-c))
// and where 0<c is a general coefficient describing the exponential
// curvature. The function's input domain lies within the 0..1 interval, its
// output range likewise. The implementation optimises by pre-computing those
// constant terms depending only on the coefficient whenever the coefficient
// changes. Repeated evaluation takes less computing time thereafter.
struct RRExponentialFunction
{
    float coefficient;
    float exponentOfMinusCoefficient;
    float oneOverOneMinusExponentOfMinusCoefficient;
};

typedef struct RRExponentialFunction RRExponentialFunction;

void RRExponentialFunctionSetCoefficient(RRExponentialFunction *f, float c);
float RRExponentialFunctionEvaluate(RRExponentialFunction *f, float x);

Shading

And finally, the “gloss-caustic shader” class brings all the disparate pieces together. The design aims for reusability. Idea is that you instantiate a shader, set up the parameters such as non-caustic colour or any other necessary adjustments to defaults, then re-use it over-and-over whenever you need to draw the shading.

@interface RRGlossCausticShader : NSObject
{
    struct RRGlossCausticShaderInfo *info;
    RRCausticColorMatcher *matcher;
}

- (void)drawShadingFromPoint:(NSPoint)startingPoint toPoint:(NSPoint)endingPoint inContext:(CGContextRef)aContext;

- (void)update;
    // Send -update after changing one or more parameters. Setters do not
    // automatically update the shader. This is by design. It applies to the
    // caustic colour matcher too. Change anything? Send an update. Otherwise,
    // if the setters automatically update, multiple changes trigger unnecessary
    // multiple updates. It's a small optimisation.
    // Updating follows the dependency chain. Caustic colour depends on
    // non-caustic colour along with all the caustic colour matcher's tuneable
    // configuration settings. Updating also re-computes the gloss. Gloss
    // derives from non-caustic colour luminance, among other things.

//---------------------------------------------------------------------- setters

- (void)setExponentialCoefficient:(float)c;
- (void)setNoncausticColor:(NSColor *)aColor;
    // Converts aColor to device RGB colour space. The resulting colour
    // components become the new non-caustic colour. This setter, like all
    // others, does not automatically readjust the dependencies. Invoke -update
    // after adjusting one or more settings.
- (void)setGlossReflectionPower:(CGFloat)powerLevel;
    // Assigns a new power level to the gloss reflection.
- (void)setGlossStartingWhite:(CGFloat)whiteLevel;
    // White levels range between 0 and 1 inclusive. Gloss starting white levels
    // typically have higher values compared to ending white level.
- (void)setGlossEndingWhite:(CGFloat)whiteLevel;

//---------------------------------------------------------------------- getters

- (float)exponentialCoefficient;
- (NSColor *)noncausticColor;
    // Returns the non-caustic colour.
- (CGFloat)glossReflectionPower;
- (CGFloat)glossStartingWhite;
- (CGFloat)glossEndingWhite;

// Key-value coding automatically gives access to colour matching for caustic
// colours. Special note though, changing caustic matcher thresholds does not
// (repeat not) automatically adjust the shader's caustic colour. You need to
// update the shader when ready.
// Currently, the matcher property offers read-only access. You cannot set the
// matcher! However, perhaps future versions will allow setting in order to
// override the default caustic matching behaviour. Developers might want to
// customise the colour matching algorithmically as well as by tweaking
// parameters.
@property(readonly) RRCausticColorMatcher *matcher;

@end

The sample project explains how to use the shader. I hope it’s clear enough. The sources have an MIT license. I haven’t tried it out on iPhone or pre-Leopard Mac, so can’t make any comments about portability except to say that porting should be fairly straightforward. But no doubt you’ve heard that before!


Outline view, tree controller and itemForPersistentObject

Posted by Pioneer! Wed, 10 Sep 2008 12:42:56 GMT

This is the scenario: your user interface comprises an outline view and a hierarchical data model. You want the outline view to display the hierarchy and remember the expansion state automatically in preferences. Hence when the user re-runs the application, the items that were expanded are still expanded, and vice versa: what was collapsed remains so. Outline view, tree controller, hierarchical model, bindings. That’s the recipe.

According to some, storing expansion state of an outline view when used with a tree controller is not just difficult: it’s impossible! But is it? No, is the simple answer. It’s actually quite easy. In this article I introduce a new helper class called RROutlineViewExpandedItemsAutosaver! Original aren’t I? It does not involve sub-classing or access to private methods. The solution presented uses only documented interfaces and only requires a small stateless class instance for handling all outline view auto-saving technicalities. It does assume Core Data use for modelling. But you can easily adapt the technique for other data-model implementations.

It’s impossible?

At first it seems so. For a start, if you read the documentation for setAutosaveExpandedItems: you can see that you also need to implement two data-source methods: outlineView:itemForPersistentObject: and outlineView:persistentObjectForItem:. Goes without saying, you also need a data source implementing those methods. Otherwise it does not work. In fact, it’s not exactly clear what these two necessary methods are supposed to do. The document talks about translating to and from an “archived object.” What does that mean exactly?

You can find a number of posts at CocoaBuilder on this issue. Keith Blount started a thread in 2005; Nick Briggs asked about it again in 2006. But no answers forthcoming! Some discussion exists and some solutions have been posted at CocoaDev (OutlineViewCoreDataBindings and NSOutlineViewStateSavingWithNSTreeController). But there is nothing definitive. Plus, the solution supplied relies on private methods and sub-classing. Messy!

Start with a test

In the spirit of Agile, let’s start with a test that fails.

Apple’s Developer Connection provides sample code. Their AbstractTree demonstrates Core Data, use of bindings along with an NSTreeController. Download the sample here as a ZIP file. It contains two subdirectories: AbstractTree and Tutorial. The tutorial is pretty good. It should help you get up-to-speed if you need some familiarity with the basic technology. Build and run the project living under the AbstractTree folder. It presents you with a window comprising an empty outline view and two buttons.

Empty outline view with two buttons

Click Add a few times. You get a series of nested nodes named “untitled node”. Quit the application, re-run and you find that, although data saved, the expansion state of the outline view resets. You have to manually expand each item. Of course, you can Option-click the disclosure triangles to auto-expand an item and all its sub-items. But that’s not really what we want. We want the expand or collapse state of each element displayed in the outline view to persist! That’s the goal.

So far so good. We have a test case that fails. Just what we wanted.

Outline view’s auto-save name

Open the sample’s MainMenu.xib. Select the NSOutlineView. Set its Autosave name to (say) Nodes. Exact value does not matter. This is just a little piece of text for constructing the preferences key. The application’s defaults will contain an array containing the expanded items. The key will become “NSOutlineView Items Nodes” where Nodes corresponds the the specified Autosave name.

Autosave name of Nodes

Leave the Autosave Expanded Items check box unchecked! Strange but important. If you check this box, the framework resolves the expand-collapse question straightaway. It needs to wait until bindings have been fully established and the data store has been added. The application will instead enable this option programmatically when the application finishes launching. Not before.

Autosave Expanded Items check box unchecked

Add the new class

Add the interface header, RROutlineViewExpandedItemsAutosaver.h, to the project. Contents as follows. Download header and module sources here.

#import <AppKit/AppKit.h>

//------------------------------------------------------------------------------
// RROutlineViewExpandedItemsAutosaver
//------------------------------------------------------------------------------
// This class acts as a "dummy" outline-view data source. Dummy because it does
// not source any data. But you wire it up as though it did. In reality, its
// sole purpose is to enable automatic saving of an outline view's expanded
// items. It sounds counter-intuitive, but you can apply bindings through a tree
// controller and at the very same time hook up a data source (such as one of
// these) in order to provide the necessary persistence translations.
//
// Connect an outline view to an instance of this class. Note, you can connect
// multiple outlines views to the same instance! The instance carries no
// state. Message interactions with outline views pass the outline view
// instance. So no state needed.
//
// The implementation makes a number of assumptions. It assumes you connect
// using bindings to a Core Data model. Hence for every item, it sends [[item
// representedObject] objectID] which assumes that the represented object
// responds to -objectID. Core Data's managed objects respond with a unique
// object identifier.

@interface RROutlineViewExpandedItemsAutosaver : NSObject
@end

Add the source module RROutlineViewExpandedItemsAutosaver.m. Contents follow.

#import "RROutlineViewExpandedItemsAutosaver.h"

@implementation RROutlineViewExpandedItemsAutosaver

//------------------------------------------------------------------------------
#pragma mark Outline View Data Source
//------------------------------------------------------------------------------

// Although linked to the core-data context via bindings and a tree controller,
// the outline view also has an instance of this object connected to the outline
// view's data source.

- (id)outlineView:(NSOutlineView *)outlineView itemForPersistentObject:(id)object
{
    // Iterate all the items. This is not straightforward because the outline
    // view items are nested. So you cannot just iterate the rows. Rows
    // correspond to root nodes only. The outline view interface does not
    // provide any means to query the hidden children within each collapsed row
    // either. However, the root nodes do respond to -childNodes. That makes it
    // possible to walk the tree.
    NSMutableArray *items = [NSMutableArray array];
    NSInteger i, rows = [outlineView numberOfRows];
    for (i = 0; i < rows; i++)
    {
        [items addObject:[outlineView itemAtRow:i]];
    }
    for (i = 0; i < [items count] && ![object isEqualToString:[[[[[items objectAtIndex:i] representedObject] objectID] URIRepresentation] absoluteString]]; i++)
    {
        [items addObjectsFromArray:[[items objectAtIndex:i] childNodes]];
    }
    return i < [items count] ? [items objectAtIndex:i] : nil;
}

- (id)outlineView:(NSOutlineView *)outlineView persistentObjectForItem:(id)item
{
    // "Persistent object" means a unique representation of the item's object,
    // representing the objects identity, not its state. Outline view writes
    // this to user defaults as soon as the item expands. That's when it asks
    // for the persistent object, sending -outlineView:persistentObjectForItem:
    // and execution arrives here. A minor problem arises when adding new
    // items. The new item represents a new unsaved managed object. The managed
    // object only has a temporary object identifier. It will receive a
    // permanent one when saved. So, if the objectID answers a temporary one,
    // ask the context to save and re-request the objectID. The second request
    // gives a permanent identifier, assuming saving succeeds. Don't worry about
    // committing unsaved edits at this point.
    NSManagedObject *object = [item representedObject];
    NSManagedObjectID *objectID = [object objectID];
    if ([objectID isTemporaryID])
    {
        if (![[object managedObjectContext] save:NULL])
        {
            return nil;
        }
        objectID = [object objectID];
    }
    return [[objectID URIRepresentation] absoluteString];
}

@end

Add this header and module to the project, under the Classes group. An instance of this class will become the new data source for the outline view.

Add the new data-source

Add a new instance of RROutlineViewExpandedItemsAutosaver to the nib and connect the new instance to the NSOutlineView as the outline view’s dataSource. Use Interface Builder. Drag an Object from the palette to the nib. Change its class in the Identity Inspector panel (Command-6). Then connect the NSOutlineView to it as the new data source. It replaces the pre-existing connection between outline view and app delegate.

After this, the nib is ready.

App delegate

Apple have used the standard Core Data application delegate. It builds a managed object model, persistent store co-ordinator and managed object context. The classic Core Data stack! Our changes need to add a message-send for enabling auto-save expanded items to the outline view. But it’s not the only change required.

There is a subtle problem. It concerns bindings and Core Data. The important requirement is that when the outline view tries to restore auto-saved expanded items, the outline view has its items already loaded. Otherwise how can it translate “persistent objects” to items. Persistent objects, in this context, refers to keys used in the application defaults to identify the set of expanded items; everything not expanded defaults to collapsed.

Moving where the application adds its persistent store

When the application loads, it automatically loads MainMenu nib. All the object instances therein become instantiated. Indirectly, the managed object context and its other Core Data stack components come to life at this time. Therein lies the subtle problem. The Cocoa framework will not immediately load the Core Data objects if the context already has its store when nib-loading establishes the bindings. That’s a mouthful. Basically, the order must go:

  1. Establish bindings. Do this wholesale.
  2. Add persistent store. At this point, the outline view grabs its contents through bindings to the tree controller.
  3. Enable auto-save expanded items.

Simplify the -persistentStoreCoordinator implementation to:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (persistentStoreCoordinator == nil) {
        persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
            initWithManagedObjectModel:[self managedObjectModel]];
    }
    return persistentStoreCoordinator;
}

And second, add a new method:

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    NSFileManager *fileManager;
    NSString *applicationSupportFolder = nil;
    NSURL *url;
    NSError *error;

    fileManager = [NSFileManager defaultManager];
    applicationSupportFolder = [self applicationSupportFolder];
    if (![fileManager fileExistsAtPath:applicationSupportFolder isDirectory:NULL]) {
        [fileManager createDirectoryAtPath:applicationSupportFolder attributes:nil];
    }

    url = [NSURL fileURLWithPath:[applicationSupportFolder
        stringByAppendingPathComponent:@"AbstractTree.xml"]];
    if (![[self persistentStoreCoordinator]
        addPersistentStoreWithType:NSXMLStoreType
                     configuration:nil
                               URL:url
                           options:nil
                             error:&error]) {
        [[NSApplication sharedApplication] presentError:error];
    }    
    // Watch out for this! Make sure that the data is available before enabling
    // auto-save for expanded items. You can switch it on within the nib. Problem
    // though is that the data store must be available at nib awaking time!
    // Otherwise, how can the auto-saving of expanded items compare against
    // existing items in order to determine whether or not they should be restored
    // as expanded or not.
    //
    // There's another caveat. You need to let the bindings make the necessary
    // connections first, before connecting the Core Data context and persistent
    // store coordinator to the store. In other words, the store must be added
    // last. Here is a good place. Application-did-finish-launching occurs after
    // all nib awaking methods, after bindings have been established. Hence, when
    // the store gets added, the outline view immediately sees the contents
    // through its bindings. Otherwise the outline-view expanded items auto-saver
    // cannot see the items at all.
    [outlineView setAutosaveExpandedItems:YES];
}

That’s it!

Successful test

Build and go. This time, when you expand nodes, quit then re-run, the sample correctly expands the previously-expanded items. Great. You can view the sample application’s user defaults by typing, in Terminal:

defaults read com.apple.dts.AbstractTreeApp

It will list the saved expanded items array, resembling something along these lines:

{
    "NSOutlineView Items Nodes" =     (
        "x-coredata://68A35129-6EA4-45E7-B3FB-F1C15FDC02D9/Node/p104",
        "x-coredata://68A35129-6EA4-45E7-B3FB-F1C15FDC02D9/Node/p102"
    );
}

Download the complete sample project here.


Organising view controllers, continued

Posted by Pioneer! Mon, 01 Sep 2008 21:59:24 GMT

The last article on this subject (Organising view controllers) started looking at an example. Apple’s ViewController sample illustrates basic use of multiple view controllers. This article completes the work.

Download the source if you would rather skip ahead. Sometimes it’s easier that way. You can see the code in its full context. Snippets and extracts don’t always tell you everything you need to know.

Missing pieces

Where did we leave off? Application delegate and window controller exist but don’t do much. The window controller has a stub for the pop-up action. It does nothing. Of course, the application needs some custom view controllers for handling the view-specific behaviours. The image view has an Open push-button for example. Click this and the controller presents an Open Panel letting the user change the view’s image. Lake Don Pedro looks nice, but perhaps you fancy a change! The table view initialises some names, etc. Custom view controllers will add the view-specific controller behaviour.

However, the biggest missing piece: the view meta-controller itself. The project needs a class for meta-controlling view controllers.

Start simple

What is the most basic requirement? Our test application (View Controller Mark II) wants to say “load a view” given by @"CustomImageView", a string. The meta-control architecture then loads the corresponding nib, constructs an instance of CustomImageViewController and then hangs the result in a graph of meta-controllers. At any time thereafter, any object method with access to the meta-control graph can access the view controller (i.e. [vmc viewControllerForViewPath:@"CustomImageView"] where vmc means the view meta-controller) and, from there, its view, represented object and title. Note, if there were nested sub-controllers, the view-path would read @"CustomImageView.SomeOtherName". SomeOtherName stands for a nested tier of control. In this case though, requirements only need non-nested controllers. Pity.

Let’s take the NSViewController class and bolt on a meta-controller. The new class associates with one view controller (its subordinate controller) and multiple subordinate meta-controllers by name. See diagram below.

View Meta Controller class diagram

The diagram indicates unidirectional associations using directed arrows. The viewController association from meta-controller to view controller is unidirectional, in that direction. Likewise from name to subordinate meta-controller. Names are unique strings identifying lower levels of meta-control. This model forms a graph or tree: a hierarchy of named meta-controllers.

It’s a simple model. Note, no reverse navigation from view controller to meta-controller. Hence no need for new NSViewController subclasses. The hierarchy lives outside the pre-existing view controller framework. Of course, the model supports view controller subclasses, but not for maintaining the meta-control graph.

You might have noticed the diamond shape at the name-end of the meta-controller association; it marks an aggregation. In fact, in this case, it marks a self-aggregation, also known as circular. Some sources say that self-aggregation does not make sense. I’m not so sure. In this instance, meta-controllers are “parts of” other meta-controllers. Aggregation is not a precise concept. Suffices to say there is a strong association between meta-controller parts.

Overlaying some methods

Of course, the diagram above shows RRViewMetaController without methods. So not a very useful class, at present.

Adding methods can prove challenging. It takes judgement, experience, skill, even art. There is never one “right” way. It’s almost like pouring custard over your pudding! You don’t want a thick blob at one side; a smooth even coating works best. Yes, I know, that carries it too far. But I’m sure you know what I mean: behaviour needs judicious placement within a class structure. Methods are little packets of behaviour. Placing them, naming them is as important as implementing them, because in the long run mistakes can have an important impact on maintenance. When the custard goes cold, it gets harder to spread. I’ll shut up about custard now (I must be hungry though I just ate).

Meta-control will need methods for the following function groups.

Initialising and de-allocating

The usual housekeeping work.

  • -init initialises an instance
  • -dealloc de-allocates an instance

Accessing properties

Every meta-controller controls a subordinate view controller. Other objects need access to this.

  • -viewController answers the associated view controller
  • -view answers the view controller’s view

Adding and removing from super-views

Every meta-controller directly and indirectly manages a triplet: a view controller, a nib and a view hierarchy. The view hierarchy does not automatically enter any given window undirected. The design requires methods for adding and removing the view, along with any sub-views.

  • -addToSuperview:aSuperview adds the meta-controlled view to aSuperview
  • -removeFromSuperview removes the meta-controlled view from its super-view

Naming and removing meta-controllers

Accessing meta-controllers’ names, removing them if necessary.

  • -namesOfMetaControllers gives all the names of the subordinate meta-controllers
  • -nameOfMetaController:aMetaController answers the name of aMetaController
  • -removeMetaController:aMetaController removes aMetaController by identity

Resolving and removing view paths

Given view-path strings, meta-controller instances answer corresponding controllers, loading view controllers and views as necessary.

  • -resolveViewPath:aViewPath resolves a view path answering an array of meta-controllers
  • -removeViewPath:aViewPath removes the meta-controller identified by aViewPath
  • -viewMetaControllerForViewPath:aViewPath answers the view meta-controller identified by aViewPath
  • -viewControllerForViewPath:aViewPath shortcuts the view controller accessor, answering a view controller for a given view path

Adding these to the class diagram:

View Meta Controller class diagram (ii)

Notice the method signatures reflect C++ and Java language styling. Method name comes before arguments and arguments appear in parentheses. Of course, in real life, Objective-C intermingles signature and arguments. Hence you see addToSuperview: as the name, with aSuperView : NSView as the argument-and-type pair. I’ve also skipped the pointer asterisk as implied. I hope it makes sense. Note also, plus prefix means public in this context, not a class method.

Interface, RRViewMetaController.h

Turning the requirements into Objective-C:

#import <AppKit/AppKit.h>

// Meta refers to a position beyond, of a higher or second-order kind. Thus, a
// meta-controller controls controllers! Hence, the view meta-controller class
// controls view controllers.
// Notice, the class does not implement a back-reference to the parent
// meta-controller. True, even though they form a tree. View paths specify leaf
// nodes. The class resolves the path by walking from root to leaf, recording
// the journey at each step. This approach obviates references to parent nodes.
// Effectively, the graph of meta-controllers "hovers" above the view
// controllers, retaining them and their view hierarchies. You address leaves on
// the meta-graph using view paths where the names of the path elements
// correspond to the nib names. That implies that you can have the same nib by
// the same name. You can but not at the same tier in the hierarchy unless you
// use a colon to add more context to the name without adding to the nib
// name. In short, name strings of the form myNib:blahBlah loads the nib file
// named myNib but uses myNib:blahBlah to uniquely identify the meta-controller
// within the graph.
@interface RRViewMetaController : NSObject
{
    NSMutableDictionary *metaControllersByName;
    NSViewController *viewController;
}

@property(retain) NSViewController *viewController;

//------------------------------------------------- Initialisers & De-Allocators

- (void)releaseViewController;
    // Releases the associated view controller and by implication also removes
    // the view controller's view from its super-view.

//-------------------------------------------------------------------- Accessors

- (NSView *)view;
    // Every meta-controller stands in the shadow of a view controller, an
    // instance of NSViewController. Every view controller associates with a
    // view. The meta-controller's -view method (this method) answers the
    // view. If not already loaded from its nib, this method also indirectly
    // does so.

//---------------------------------------- Adding to & Removing from Super-Views

- (void)addToSuperview