Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

Key Value Information

NSKeyValueCoding is used throughout Cocoa (Bindings, Core Data, Collections and more) but despite being a dynamic way of connecting to an object, it doesn't provide dynamic information about its own operation — which keys an object supports or the custom methods used to get and set values. This post will look at how NSKeyValueCoding works and show you how to get supported keys and their methods for any class or object.

Previously on "Fundamentals of Cocoa Programming"...

Key Value Coding is a way of getting and setting data on an object. Key Value Coding means that you use a generic method on the object to ask it to get or set a value for a specific name (the "key"). By using a generic method on the object, Key Value Coding abstracts away the mechanics of how the value for the "key" is obtained or set on the object.

Key Value Coding is about connection logic; it does not normally perform the work of retrieving or changing values (normal getter and setter methods to do this). Instead, it defines how to invoke the getter and setter methods in an abstract way.

It is particularly useful in at least 5 situations situations:

  • It reduces the appearance of an object to that of simple data storage — whether or not it is actually simple under the hood (e.g. NSManagedObject).
  • It provides a simple interface for flexibly dealing with variable numbers of named values (e.g. NSDictionary).
  • It removes the need to know the specific object type that you're dealing with — such as the generic connecting of objects in user-interface bindings.
  • Allows easy chaining of access operations through "key paths" and provides operators for dealing with collections of data accessed in this way.
  • Generic access to data makes possible other services such as NSKeyValueObserving.

It does (most) of this through two basic methods:

- (id)valueForKey:(NSString *)key
- (void)setValue:(id)value forKey:(NSString *)key

which are defined in the NSKeyValueCoding informal protocol (implicitly implemented by all NSObjects).

How does it work?

Once you start asking how anything works, you immediately plunge into a gray area. The documentation at least tells us this:

  • The default implementation will invoke the -(id)<key> or -(void)set<key>: methods, if they exist, to perform the work. There are a few other method names tried if these can't be found.
  • If no method is found, any instance variable with the same name as the key will be automatically accessed (unless this feature is explicitly disabled for the class).
  • If no matching instance variable is found either, the -(id)valueForUndefinedKey:(NSString *)key method is invoked which raises an NSUndefinedKeyException by default.

Distilling this information down, you can (in most cases) presume that key value coding will work for any "key" on any class that supports the methods -(id)<key> and -(void)set<key>:, or has an attribute named "key". Sometimes overrides valueForUndefinedKey: will support other keys too but that is highly implementation specific. Most other key and object combinations will throw an NSUndefinedKeyException.

Definitive answers

Key Value Coding is all about dynamic connections, simplifying and abstracting — so it seems reasonable that you may eventually find a situation where you want to get a value from an object but you need to check at runtime if it supports the key for the value you want.

Unfortunately, the many fallback options for key lookup and the possibility of overrideable methods like valueForUndefinedKey: mean that there is no specific property you can read that will reveal if a key is supported.

In the most general sense, there is only one way of determining if an object supports a given key:

BOOL supportsSomeKey = YES;
@try
{
    [object valueForKey:somekey];
}
@catch (NSException *e)
{
    if ([[e name] isEqualTo:NSUndefinedKeyException])
    {
        supportsSomeKey = NO;
    }
}

If it throws an NSUndefinedKeyException, then it's not happy with the key.

More detailed but less definitive answers

If you consider deliberately triggering exceptions to be a crime against programming or you wanted different information about how the value is accessed for a given key, then a different approach may be required.

Depending on your circumstances, you may simply want to perform the lookups that NSKeyValueCoding itself likely performs — i.e. build the different accessor method names from the "key" name, convert this to a SEL and ask the class or object if it handles the selector.

This is exactly the approach I took in a piece of code I was playing with recently. My code used Core Data's NSManagedObject and wanted to inspect generic NSManagedObjects to see if I had written custom accessor methods (as distinct from the automatically generated accessor methods) for specific keys. I was able to use the following method:

+ (SEL)getterSelectorForKey:(NSString *)key
{
    NSString *capitalizedKey =
        [[[key substringToIndex:1] uppercaseString]
            stringByAppendingString:[key substringFromIndex:1]];

    NSString *getString = [@"get" stringByAppendingString:capitalizedKey];
    SEL getSelector = NSSelectorFromString(getString);
    if ([self instancesRespondToSelector:getSelector])
    {
        return getSelector;
    }
    
    SEL plainSelector = NSSelectorFromString(key);
    if ([self instancesRespondToSelector:plainSelector])
    {
        return plainSelector;
    }
    
    NSString *isString = [@"is" stringByAppendingString:capitalizedKey];
    SEL isSelector = NSSelectorFromString(isString);
    if ([self instancesRespondToSelector:isSelector])
    {
        return isSelector;
    }
    
    return nil;
}

in an NSManagedObject category to lookup any custom written accessor for a specified key — this method simply follows the lookup path described in the documentation to find an accessor method if it exists.

The above method is able to differentiate between a custom accessor and Core Data's automatically generated accessors because it is invoked on the class. Core Data's automatically generated accessors only exist on object instances (the class doesn't have them in its list of instance methods), so the above method will only return the custom compiled methods for which I was searching.

You could use a similar approach to get the other types of method used in key value coding, i.e.:

+ (SEL)setterSelectorForKey:(NSString *)key;
+ (SEL)addObjectSelectorForKey:(NSString *)key;
+ (SEL)removeObjectSelectorForKey:(NSString *)key;
+ (SEL)countOfSelectorForKey:(NSString *)key;
+ (SEL)objectInAtIndexForKey:(NSString *)key;

Similarly, if you were interested in key value coding that may access instance variables directly, you could use class_getClassVariable to get any instance variables with the same name as the key.

Read more...

Better integration for NSViewController and NSView

NSViewController simplifies loading an NSView from a NIB file. But it has some limitations compared to NSWindowController (its window loading/managing equivalent). In this post, I'll explain the limitations and present options for overcoming them.

Window and View Controllers

In a clean Model-View-Controller application design (I know, it's never clean but let's pretend), the views in your windows know how to do their own work but are ignorant of any greater application context. The context for views comes from higher level objects (controllers) that configure the view state and provide data connections and notifications.

NSWindowController has provided this "higher level context" for windows and their contents since the beginning of Mac OS X. Its primary function is to act as a window-from-NIB loader. Through this role, NSWindowController and the NIB loader provide most of the context to view objects simply through IBOutlet connections and bindings. In addition, NSWindowController is normally the window's delegate and resides in the responder chain immediately after the window — this allows it to perform further arranging actions and higher-level responses to user actions within the window.

From Mac OS X 10.5, NSViewController began offering similar from-NIB loading convenience for NSViews. This allows higher level context and dynamic configuration of regions of a window, rather than purely at the coarse level of the whole window.

View-Controller integration

NSWindow was written to integrate well with NSWindowController. You can access the NSWindowController from the NSWindow via the windowController method. The NSWindow ensures that the NSWindowController is included in the responder chain shortly after the NSWindow itself. When also the delegate of the NSWindow, NSWindowController is sent messages when certain actions occur, including windowWillClose: and windowDidBecomeMain:, in addition to NSWindowController methods windowDidLoad and windowWillLoad.

Sadly, none of these features exist to integrate an NSView with its NSViewController. This likely derives from the fact that an NSView cannot directly access its NSViewController.

Adding integration

Adding the same level of integration to NSView and NSViewController requires that the NSView can access its NSViewController.

Where possible, this is best done by using an NSView subclass for the view loaded by the NSViewController. This subclass should have the following member:

    IBOutlet NSViewController *viewController;

which should be connected to the NSViewController in Interface Builder (the File's Owner object).

Once this is done, we can use this value to achieve responder chain integration with methods in the NSView subclass as follows:

- (void)setViewController:(NSViewController *)newController
{
    if (viewController)
    {
        NSResponder *controllerNextResponder = [viewController nextResponder];
        [super setNextResponder:controllerNextResponder];
        [viewController setNextResponder:nil];
    }

    viewController = newController;
    
    if (newController)
    {
        NSResponder *ownNextResponder = [self nextResponder];
        [super setNextResponder: viewController];
        [viewController setNextResponder:ownNextResponder];
    }
}

- (void)setNextResponder:(NSResponder *)newNextResponder
{
    if (viewController)
    {
        [viewController setNextResponder:newNextResponder];
        return;
    }
    
    [super setNextResponder:newNextResponder];
}

The NIB loader will kindly invoke the setViewController: method for us when it establishes the connection from the NIB. Together with the setNextResponder: method, these ensure that any time the viewController is set on the NSView, it is inserted in the responder chain immediately after the NSView.

It can also be useful to override viewDidMoveToSuperview in the NSView subclass and use the override to send a notification to an NSViewController subclass method. This allows delegate-like behavior for the NSViewController similar to that available for windows.

Other considerations

These changes use a subclass of NSView. For NSWindow, integration with the NSWindowController came for free, without needing a subclass.

With a subclass comes the disadvantage of temptation. Once the NSView is subclassed to integrate with NSViewController, the temptation exists to insert code that is properly controller code into the NSView instead of into the NSViewController where it belongs.

There is also the disadvantage that existing NSView subclasses would all need to be separately subclassed to add this functionality.

These disadvantages could be overcome by inserting the NSView subclass functionality directly into NSView itself. Instead of a per-view instance variable pointing to the view's controller, a global NSMapTable relating any NSViewController to its respective NSView could be used. Similarly, all required methods could be swizzled or patched into NSView and NSViewController directly. I don't do this in my own code because its messier and harder to maintain — but you could do it if you needed.

Read more...

CoreGraphics curves and lines: a sample app

This is a small application which shows the basic CoreGraphics line drawing primitives. Includes mouse editable control points to manipulate the primitives graphically.

CurveDrawing

This post is a departure from my normal style. It's really just an example that you can play with to get a feel for how basic controls and drawing operate in a Cocoa application.

The project and code for this post can be downloaded here:

This project is XCode 3.0 Universal.
If you have an Intel Mac and haven't installed the PPC SDKs, you may get a "warning... missing required architecture PPC". Either disable the PPC build (Project->Project Settings->Build->Architecures) or choose "Debug" instead of "Release" from the "Active Build Configuration" to avoid.

The code for the application shows:

  • NSView subclassing
  • Using an NSSegmentedControl to switch between options
  • Line and rectangle drawing using CoreGraphics C functions
  • Getting the CGContextRef for the current NSGraphicsContext using
    -[NSGraphicsContext graphicsPort]
  • Mouse tracking and dragging using
    -[NSWindow nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)]
  • Using an NSSlider an configuring it for continuous updates using
    -[NSControl setContinuous:YES]

Once compiled, the result will look like this:

samplescreenshot.png

The program doesn't implement any kind of Model-View-Controller architecture (the "model" is really just 4 NSPoint values — too trivial for an abstracted layer). It also doesn't show all the CoreGraphics lines drawing primitives — I've left out a couple that can't easily be described with 4 control points.

Understanding the program

The program always shows exactly 4 control points as small gray squares in the window. Each of these can be clicked and dragged around with the mouse

The control points are used to draw the current primitive (selected in the segmented control at the top of the window).

"ArcToPoint" draws using the function CGContextAddArcToPoint. This primitive is typically used for drawing round rectangles, though it can also be used to draw curve segments between lines of any angle (chamfers and fillets). The radius of the curve is controlled by the slider at the bottom of the window.

"CurveToPoint" draws using the function CGContextAddCurveToPoint. It uses all four points to draw a cubic Bézier.

"EllipseInRect" draws using the function CGContextAddEllipseInRect. It draws an ellipse in the rectangle that encloses all four control points.

"Lines" draws using the function CGContextAddLineToPoint. It draws line segments through all the control points.

"QuadCurve" draws using the function CGContextAddQuadCurveToPoint. It uses the first three points to draw a quadratic Bézier.

"Rect" draws using the function CGContextAddRect. It draws a rectangle that encloses all four control points.

Understanding the code

The only code for the program is in the "CurveView.m" source file. It consists of the following methods:

  • initWithFrame:
    Invoked as the NIB is loaded. Initializes the array of points to their initial values.
  • awakeFromNib
    Invoked by the NIB loader after the CurveView has been connected to the slider. Sets the slider to continuous (so that we receive updates while the slider is being adjusted) and sets the initial "radius" value to the midpoint of the slider.
  • drawRect:
    An implementation of the default NSView drawing method. Invoked by Cocoa automatically when the view needs to be drawn in the window or when the view is deliberately updated. It draws:
    • background as a white square framed in black
    • a line from point 0 to 1, 1 to 2 and 2 to 3
    • the currently selected drawing primitive using the control points
    • a square to represent each of the four control points
  • mouseDown:
    Determines if a click in the view occurred in one of the control point squares and drags the control point while the mouse remains down.
  • setRadius:
    Invoked automatically by the binding set up in the NIB file between the slider and the "radius" value on the NSObjectController in the NIB. Since the "content" of the NSObjectController is set to the "CurveView", this has the effect of binding the slider to the CurveView's radius.
    We implement this method (instead of letting the binding set the value directly) to trigger a view redraw after changing the "radius".
  • changeSegment:
    Invoked as the target of the NSSegmentedControl's action when clicked. We change the selected index and redraw.
  • resetControlPoints:
    Invoked from the "initWithFrame:" method and by the button in the window. Sets the points in the array to their default values.
  • controlPointRectForPoint:
    Invoked from "drawRect:" and "mouseDown:". Returns the bounds of the control point rectangle corresponding to a point.
  • boundsOfAllControlPoints
    Calculates and returns a rectangle enclosing all the control points.

Understanding the MainMenu.xib file

The XIB file is the new XML version of the NIB file. It is still compiled into a .nib file when the project is built.

Most of the objects in the application are created from the NIB file. These include:

  • All menus in the menu bar and all their contents (these are the default Cocoa Application menus)
  • The window and all its contents, including the CurveView (modelled as a Custom NSView in InterfaceBuilder)
  • An NSObjectController which only exists to connect the NSSlider in the window to the "radius" value of the CurveView via bindings

Connections made between objects:

  • "Reset Control Points" button has its action connected to the CurveView's resetControlPoints: method to allow the button to work.
  • "ArcToPoint radius" slider has its value binding connected to the "radius" value of the "Object Controller" to announce changes to the slider.
  • "Object Controller" NSObjectController has its "content" set to the CurveView to allow the controller to keep things (specifically the slider) in sync with CurveView values.
  • The NSSegmentedControl has its action set to the CurveView's changeSegment: method.
  • The CurveView has its "sliderControl" set to the "ArcToPoint radius" slider.
Read more...

NSMapTable: more than an NSDictionary for weak pointers

NSMapTable is collection class that was introduced in Mac OS X 10.5 (Leopard). At first glance, it would appear to be most useful as an NSDictionary replacement that can choose between "strong" and "weak" pointers. In this post, I'll show you why it is also useful outside garbage collection and how it can do things that NSDictionary can't (or shouldn't) do.

More Cocoa for your Leopard

Cocoa added a few new collections classes in Mac OS X 10.5 (Leopard). These included:

NSPointerArray is completely new but most of NSHashTable and NSMapTable's functionality was previously available through the opaque Foundation C structs of the same names.

In some respects, these new classes work like NSMutableArray, NSMutableSet and NSMutableDictionary respectively but give the option of working with "weak" garbage collected pointers. If you're using Objective-C 2.0 with Garbage Collection, you should know what "weak" means with respect to your pointers, so the advantage of using this option should be clear.

NSPointerArray can also be used for pure pointers (pointers that are not necessarily Objective-C classes) but the NSHashTable and NSMutableArray classes both require their contents to be Objective-C objects.

In a general sense though, NSPointerArray and NSHashTable fill the same design role as NSMutableArray and NSMutableSet (an ordered array and an unordered set respectively).

NSMapTable is different because it can fill roles in your design that NSMutableDictionary couldn't (or shouldn't).

Limitations of NSDictionary

NSDictionary provides a key-to-object mapping. In essence, NSDictionary stores the "objects" in locations indexed by the "keys".

Since the objects are stored at specific locations, NSDictionary requires that the key's value doesn't change (otherwise the object would suddenly be at the wrong location for its key). To ensure this requirement is maintained, NSDictionary always copies the keys to its own private location.

This key copying behavior is fundamental to how NSDictionary works but is also a limitation: you can only use an Objective-C object as a key in an NSDictionary if it supports the NSCopying protocol. Further, the key should be small and efficient enough that copying is does not burden CPU or memory.

This means that NSDictionary is really only suited to "value"-type objects as keys (eg. small strings and numbers). It is not ideal for modelling mappings from fully fledged objects to other objects.

Object to Object mappings

NSMapTable (as the name implies) is more suited to mapping in a general sense. Depending on how it is constructed, NSMapTable can handle the "key-to-object" style mapping of an NSDictionary but it can also handle "object-to-object" mappings — also known as an "associative array" or simply a "map".

For example, an NSMapTable constructed as follows:

NSMapTable *keyToObjectMapping =
    [NSMapTable
        mapTableWithKeyOptions:NSMapTableCopyIn
        valueOptions:NSMapTableStrongMemory];

will work much the same as an NSMutableDictionary, copying its "key" values and retaining its "object" values.

A pure object-to-object mapping could be constructed as follows:

NSMapTable *objectToObjectMapping =
    [NSMapTable mapTableWithStrongToStrongObjects];

An object-to-object behavior could previously be emulated using an NSDictionary if all the keys were NSNumbers containing the memory address of the source object in the mapping (don't laugh, I've seen it done) but outside of this run-around, NSMapTable offers a true object-to-object mapping for the first time in a Cocoa collection class.

Options for NSMapTable

The options provided to NSMapTable are composed of three parts: a "memory option", a "personality option" and the "copy in" flag. You may use one option for each part (there are default behaviors that will be used if no option is provided for the part). The parts are all bit flags (binary "or" them together to combine parts).

Officially, NSMapTable allows the following options:

  • NSMapTableStrongMemory (a "memory option")
  • NSMapTableWeakMemory (a "memory option")
  • NSMapTableObjectPointerPersonality (a "personality option")
  • NSMapTableCopyIn (a "copy option")

NSMapTableStrongMemory is the default "memory option". However, the default "personality option" and the default "copy in" behaviors don't have names so these two values can be considered implicitly included in the list.

Memory options

Since "strong" and "weak" are terms associated with Garbage Collection in Objective-C, it may not be obvious that these options can be used outside of Garbage Collected code (manually managed memory as Apple calls it).

Outside garbage collection, the following definitions apply:

  • strong: use retain and release
  • weak: don't use retain and release

NSMapTable only permits NSPointerFunctionsOptions "personality options" that correspond to Objective-C objects. There are other NSPointerFunctionsOptions "personality options" where the behavior for "strong" pointers does not include retain and release but these options are not permitted for NSMapTable.

A warning about using "weak" outside of garbage collection:
The pointer will not be zeroed as in a garbage collected environment so you must be careful not to dereference the pointer if it is released.
Personality options

The NSMapTableObjectPointerPersonality option is used to control whether the isEqualTo: and hash methods on the object are used when adding the object to the collection.

  • NSMapTableObjectPointerPersonality specified
    The object's pointer value is used for direct comparison and bit-shifted hash generation (isEqualTo: and hash methods are not used).
  • NSMapTableObjectPointerPersonality not specified (default behavior)
    The hash and isEqualTo: methods will invoked on the key to determine a storage location in the NSMapTable. The return values of these methods should not change (be immutable) for the duration that the key is used in the NSMapTable.

Both behaviors imply that the content implements the NSObject protocol, so methods in this protocol may also be invoked on the keys and objects. In particular, the description method may be invoked on NSMapTable contained keys and objects regardless of the personality option used. The NSMapTable will only support NSCoding if all keys and objects implement the NSCoding protocol too.

Copy options

If NSMapTableCopyIn is specified, the NSMapTable makes its own copies of data using the NSCopying protocol when they are added. If you don't specify this option (default behavior) no copy will be made.

Read more...