Tuesday, 2 August 2011

NSViewControllers part 3 - NSCollectionView

Previously we looked at NSViewControllers and what they're used for in Appkit. One of the others places they are used is under the hood of NSCollectionView. NSCollectionView is a view that can display a collection of arbitrary views, hooked up to a model. NSCollectionView has a helper class, called NSCollectionViewItem, which it uses to create the necessary views for displaying. Does that sound familiar? Initially, NSCollectionViewItem was a subclass of NSObject, but when NSViewController was added in 10.6, NSCollectionViewItem had most of the same methods as NSViewController, so the class hierarchy was changed to make NSCollectionViewItem a subclass of NSViewController instead, and all of its methods were deprecated.

In writing the code for this article, I came across what appears to be a bug in XCode 4.1, which means that you cannot use XCode's interface designer to bind the data to the views, which is the correct way to do it. In this article, I'm going to be creating the child views manually as that appears to be the only way to get access to the controls in the view.

So let us begin at a look at what can be done with NSCollectionView.

Creating an NSCollectionView by hand


First we will need a model to display in the NSCollectionView. We'll create some simple data objects for the model, with a title string and an integer.

@interface Item: NSObject {
    NSString *title;
    NSInteger option;
}


@property (readwrite, copy) NSString *title;
@property (readwrite) NSInteger option;


- (id)initWithTitle:(NSString *)_title option:(NSInteger)_option;


@end


There's nothing surprising in the Item.m implementation of this object so if you want to see it, you can look at the project file at the end.

Now we have our data item, we need to create a model. In the application delegate, we create an NSMutableArray which will act as our model, and an NSArrayController which will be the controller in the MVC pattern. In this project, I'm actually going to create the array controller in the nib file, and assign it to an outlet in the application delegate. This will allow us to bind to it in the interface designer slightly easier.

The model was created in a very simple manner for testing purposes, in the init method of the application delegate


items = [[NSMutableArray alloc] init];
for (i = 1; i <= 20; i++) {
    [items addObject:[[Item alloc] initWithTitle:[NSString stringWithFormat:@"Item %d", i] option:(i - 1) % 4]];
}

I designed a simple interface and all it has is a NSCollectionView. Once you add an NSCollectionView to a window, XCode automatically adds an NSCollectionViewItem and an new NSView as well. These are going to be the view controller and the view that represents each item in our data model.



Setting up the bindings so that the NSCollectionView can find the data in the model. Firstly we need to tell the NSArrayController where to get its content. We bind the Array controller's contentArray property to the NSMutableArray property of the Application Delegate. I called this property items and so in the bindings panel of the NSArrayController I set it to bind to Application Delegate and the Model Key Path is set to items. 




Next we bind the content property of the NSCollectionView to the NSArrayController's arrangedObjects property. Now the NSCollectionView can see the contents of the NSMutableArray through the NSCollectionView.






NSCollectionViewItem is a subclass of NSViewController, and it has a property called representedObject. When each NSCollectionViewItem is created, this property is set to the object in the model that the NSCollectionViewItem represents. Under normal circumstances we could add controls to the NSView that XCode added for us, and then bind their values to the appropriate property on this representedObject property. For example, if we wanted to bind to the title property that our Item class has, we would select the NSCollectionViewItem as the binding source, and the model key value would be representedObject.title. However, this is what the bug in XCode prevents us from doing, so we need to carry this all out by hand. To do this, we must subclass the objects we want to use and override certain methods.


The first subclass we need is one for the NSCollectionView itself. NSCollectionView has a method 
-(NSCollectionViewItem *)newItemForRepresentedObject:(id)object which is called whenever the collection view needs to display a new object. So we create our subclass (MyCollectionView I called it) and in the interface designer we set the class of the NSCollectionView to be an instance of it in the Identity Inspector.






Then we need to override newItemForRepresentedObject: so that we can do the required binding. We allow the parent class to create the NSCollectionViewItem for us, after which we can do whatever we require with it.


- (NSCollectionViewItem *)newItemForRepresentedObject:(id)object
{
    // Let the parent class create the item
    NSCollectionViewItem *item = [super newItemForRepresentedObject:object];
    // Now we need the view so we can create bindings between it and the object.


    NSView *view = [item view];


    [view bind:@"title" toObject:object withKeyPath:@"title" options:nil];
    [view bind:@"option" toObject:object withKeyPath:@"option" options:nil];


    return item;
}


The bind:toObject:withKeyPath:options: message is binding the title property on the created view to the same property on the object that it represents. Obviously we need to create a subclass for the view, which we'll call ItemView.


As we couldn't create the view in XCode we need to create the view by hand in the initWithCoder: method. We also need to add properties for the title and option bindings that we want to display from our model item. We use the initWithCoder: method instead of initWithFrame: because the view is still being defined in the nib file, we're just filling in some other bits.



- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    
    NSRect frame = [self frame];
    float controlHeight = (frame.size.height - 30) / 2;
    
    segmentedControl = [[NSSegmentedControl alloc] initWithFrame:NSMakeRect(10.0, 10.0, frame.size.width - 20, controlHeight)];
    [segmentedControl setSegmentCount:4];
    
    NSInteger i;
    for (i = 0; i < 4; i++) {
        [segmentedControl setLabel:[NSString stringWithFormat:@"Option %d", i]
                        forSegment:i];
    }
    
    [self addSubview:segmentedControl];
    titleField = [[NSTextField alloc] initWithFrame:NSMakeRect(10.0, controlHeight + 15, frame.size.width - 20, controlHeight)];
    [titleField setEditable:NO];
    [titleField setBordered:NO];
    [titleField setDrawsBackground:NO];
    
    [self addSubview:titleField];
    
    return self;
}


We create a segmented control to display the option property and a text field to display the title property. Then we need to update the controls whenever the property changes which we can do in the set methods:



- (void)setTitle:(NSString *)_title
{
    if (_title == title) {
        return;
    }
    
    [title release];
    title = [_title copy];
    
    [titleField setStringValue:title];
}


- (void)setOption:(NSInteger)_option
{
    if (option == _option) {
        return;
    }
    
    option = _option;
    [segmentedControl setSelectedSegment:option];
}


These are just standard setter functions, except they update the controls.


Finally for this section, we need to tell XCode that the class of the NSView is actually our ItemView by setting it in the Custom Class section of the Identify Inspector. 






The size of the ItemView is also set in the nib file, so if you want to specify a size thats where to do it.




Handling Selection In NSCollectionView

NSCollectionViewItem had a selected property, but as of 10.7 this has been deprecated so any new code shouldn't use it. This makes handling selections slightly more awkward but it is still possible.

The first thing to do is to make the NSCollectionView selectable in the Attributes Inspector

and to bind the NSCollectionView's Selection Indexes to the selectionIndexes property on the array controller


This means that other controls can use the array controller to listen to changes in the selection. As an example, we'll add an NSTextField which will display the selected item's title without any code needing to be written.

Add the NSTextField to the main window. Then in the Bindings Inspector we need to bind the value property of the NSTextField to the Items Controller. The controller key is selection and the Model Path Key is title.



If you build this now and select an item, the title of the selected item will appear in the text field, but the item doesn't look like it is selected. This is because we need to tell the item that it is selected, and we need to draw the NSView differently when it is selected.

To our ItemView class we add a selected property and when the property is changed we tell our view to redraw itself.

- (void)setSelected:(BOOL)_selected
{
    if (_selected == selected) {
        return;
    }
    
    selected = _selected;
    [self setNeedsDisplay:YES];
}

We can override the drawRect: method of our view to draw the background in the selected colour when the item is selected.

- (void)drawRect:(NSRect)dirtyRect
{
    if (selected) {
        [[NSColor selectedControlColor] set];
        NSRectFill(dirtyRect);
    }
}

Finally, to complete this, we need to tell the items when they are or are not selected. Initially I thought about doing this with Key Value Observing (KVO) on the selectedIndexes property of the array controller. The problem with this is that allow the KVO notifications are called correctly when the selection changes the changes dictionary is always empty. So I approached this from a different way. I'm not sure this is as "correct" as the KVO method would have been but it still works.

To do it, I override the setSelectionIndexes: method of MyCollectionView, which is called every time the selection changes as well. NSCollectionView has the method itemAtIndex: which we use to get the item for the selection. We also need to store the selected row so that we know which row to unselect when it is changed.

- (void)setSelectionIndexes:(NSIndexSet *)indexes
{
    [super setSelectionIndexes:indexes];

    NSCollectionViewItem *item = [self itemAtIndex:currentSelection];
    ItemView *view = (ItemView *)[item view];
    [view setSelected:NO];
    
    item = [self itemAtIndex:[indexes firstIndex]];
    view = (ItemView *)[item view];
    [view setSelected:YES];
    
    currentSelection = [indexes firstIndex];
}

Now when we compile and run it we should see that the item is clearly selected.


I think thats a fairly comprehensive look at NSCollectionView and friends, there's bound to be other things and hopefully Apple will get the binding bug fixed in the next version of XCode so using it won't be such a hassle.

(Code for the project can be found at http://www.mediafire.com/?n29lgi9wl22dsgf)

References:


Links to the other parts of the NSViewController articles
Part 1:- http://comelearncocoawithme.blogspot.com/2011/07/nsviewcontrollers.html
Part 2:- http://comelearncocoawithme.blogspot.com/2011/07/nsviewcontrollers-part-2.html
Part 4:- http://comelearncocoawithme.blogspot.com/2011/08/nscollectionview-redux.html


Thursday, 21 July 2011

NSViewControllers (Part 2)

Following on from the last post which was really the setting up of an NSViewController and we used it to switch views in and out of the window. And really we didn't need very much code to do it. This time we're going to subclass the NSViewController to set some properties in them.

When you create a subclass (using File->New->New File, or apple+N) and you tell Xcode that the parent class is NSViewController it also creates a nib file for that controller. As I was following on from the previous project, I already had the two nib files, so I deleted the newly created ones. Then in the TestView1.xib and TestView2.xib files I had to change the class of the File's Owner to our new NSViewController subclasses (which in my case were TestView1Controller and TestView2Controller) in the Identity Inspector pane on the right. Remember that we also created two NSViewControllers in the MainMenu nib file, so we need to tell Xcode that it should use our new subclasses for those as well. Again just set that in the Identity Inspector pane. Compiling and running showed that everything worked as it did before.

One of the properties that an NSViewController has is a title for the view. This is designed to be displayed somewhere in the UI. I'm going to put it in the title of the NSBox for want of anywhere else to put it. First we need to set the title for each of the controllers.

In the TestView1Controller.m file Xcode has already given us the initWithNibName:bundle: method and that seems like the best place to put it. But if we put code into there we discover that it never gets called. This is because when an object is created from the nib file a different init message is sent: initWithCoder:. We can either put the code in this method or into the awakeFromNib method.


- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    [self setTitle:@"Test view 2"];
    
    return self;
}


Obviously because this is an init method we need to call up to the superclass. Or if we put it into awakeWithNib then all we need is to set the title.



- (void)awakeFromNib
{
    [self setTitle:@"Test View 1"];
}


Finally all we need to do is to set the box title. Back in the action methods we just need to send the setTitle: message to the box like so.



- (IBAction)switchView1:(id)sender
{
    NSView *view = [testView1Controller view];
    
    [box setContentView:view];
    [box setTitle:[testView1Controller title]];
}


Now, if you compile&run the box displays the title that was set in the view controller.
(code for this project available to download http://www.mediafire.com/?h0810n5s808c0u5 Again with the mediafire, fingers crossed...)


We can also set the title with bindings. If we add an NSViewController iVar to the ViewControllerExampleAppDelegate called currentController, then in the bindings inspector pane in Interface Builder we can bind the NSBox's title property to the ViewControllerExampleAppDelegate with keypath being currentController.title. All we have to do to set the box's title is to set currentController to the NSViewController we want displayed. In the new example, I've refactored the duplicated code in the action methods into a method of its own, and now it all looks like this.

- (void)setController:(NSViewController *)controller
{
    NSView *view = [controller view];
    
    [box setContentView:view];
    [self setCurrentController:controller];
}

- (void)awakeFromNib
{
    [self setController:testView1Controller];
}

- (IBAction)switchView1:(id)sender
{
    [self setController:testView1Controller];
}

- (IBAction)switchView2:(id)sender
{
    [self setController:testView2Controller];
}

I had hoped that we could have set the box's contentView via bindings as well, but apparently not.
(code for example 3 available here: http://www.mediafire.com/?k797b2y2uuqjz6c )

There's still some more things to look at on NSViewControllers, I'm working out how to bind things inside the view (if thats possible, I think it is) and I want to look at how NSCollectionView uses view controllers.


Links to the other parts of the NSViewController articles
Part 1:- http://comelearncocoawithme.blogspot.com/2011/07/nsviewcontrollers.html
Part 3:- http://comelearncocoawithme.blogspot.com/2011/08/nsviewcontrollers-part-3.html
Part 4:- http://comelearncocoawithme.blogspot.com/2011/08/nscollectionview-redux.html

NSViewControllers (Part 1)

I seem to see a lot of people asking questions about NSViewControllers on Stack Overflow and I wondered about them as well for a quite some time. They are being used more frequently in newer API on Cocoa, and you can't program for iOS without them.

There are a few functions the NSViewController allows you to do:

  1. Create new views on the fly
  2. Provide a title for the view for displaying in a tab or popup menu
  3. Enable you to bind to a value in the view, before the view is actually created.
Lets look at each one of these separately.

Creating a view on the fly is used quite a lot, think of a complex application like iTunes. The left hand column has a list of different options and when you select one, it shows a different view in the right hand side of the window. Now I have no idea how iTunes does it, but this is the one of the UI designs that NSViewController is designed to help with.

NSViewController has a method - (NSView *)view which creates a view and returns it. The simplest use of NSViewController is like so

NSViewController *viewController = [[NSViewController alloc] initWithNibName:@"customView" bundle:nil];
NSView *view = [viewController view];


[parentView addSubview:view];


This simply creates a new NSViewController and tells it that the definition for the NSView is found in the customView.nib (or customView.xib) file in the main bundle. Then it creates a new NSView and puts that view into parentView. But really, using NSViewController in this way doesn't really add much over just creating the view directly. Where NSViewController is really useful is when you need to create the views on the fly. Lets assume, for the sake of argument, that iTunes does use NSViewController. When you're looking at the Music view you may not want the Movies view loaded into memory. NSViewController does this by not creating the view until you call -(NSView *)view or -(void)loadView. This allows you to create the view controller and hook things up to it, without having the view loaded and taking up memory until it's needed. Lets look at a more full example.


In our application delegate (called ViewControllerExample1AppDelegate in the example project), we have some iVars that get filled from the MainWindow.nib.




@interface ViewControllerExample1AppDelegate : NSObject <NSApplicationDelegate> {
@private
    NSWindow *window;
    NSBox *box;
    NSViewController *testView1Controller;
    NSViewController *testView2Controller;
}


@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet NSBox *box;
@property (assign) IBOutlet NSViewController *testView1Controller;
@property (assign) IBOutlet NSViewController *testView2Controller;


- (IBAction)switchView1:(id)sender;
- (IBAction)switchView2:(id)sender;



In the MainWindow.nib we've created a window with 2 NSButtons, and an empty NSBox. The two buttons are hooked up to the switchView1 and switchView2 methods and we'll look at what they do later on. The NSBox is connected to the box iVar. In the nibfile we've created two NSViewControllers and connected them to the appropriate iVars. The ultimate aim for this is that clicking on the first button will switch the view contained in the NSBox to the view from the first view controller, and the second button with switch it to the second view controller's view. A simple but common UI pattern.


We also need to create two nib files that the NSViewControllers use to generate their views. I just did this by creating a new file, selecting UI in the type column, and selecting View in the type. That gives a nib file that only has an NSView in it, and I just dragged whatever controls I wanted in the view. After these nib files have been created, we need to tell the NSViewControllers what nib file to do, and to do that you just need to set the Nib Name property on the NSViewController.


Whenever NSViewController is sent the loadView or the view message, it creates the view from the nib file and it sets the File's Owner object to the receiver, which is our NSViewController and this it uses the File's Owner object's view outlet to find out what NSView should be returned. This means that we have to connect it up in our nib files. The first part of this process is to set the type of the File's Owner object. By default it is of type NSObject, which isn't very useful for us, so in Interface Builder we change the class type property by selecting the File's Owner object, and in the inspector pane's Identity Inspector changing where it says the class is NSObject to NSViewController. Now if we try to ctrl-drag from the File's Owner placeholder object to the NSView, it will allow us to set that as the view outlet.


Finally, all we need to do is to make the buttons switch the views and that simply means creating the view and setting it in the box. We do this in the switchView1 and switchView2 actions, which look like this:




- (IBAction)switchView1:(id)sender
{
    NSView *view = [testView1Controller view];
    
    [box setContentView:view];
}


- (IBAction)switchView2:(id)sender
{
    NSView *view = [testView2Controller view];
    
    [box setContentView:view];
}

The controllers are simply asked for their views, and the NSBox is told to show it. The returned view is set to be autoreleased, so if you want to keep it around then you need to send it the retain message. In this instance the setContentView message sent to box does the retaining for us so when the content view is replaced the old view is released.

We can make the program slightly more complete by displaying a box when it initially starts by doing the same thing in the awakeFromNib message sent to the application delegate.



- (void)awakeFromNib
{
    NSView *view = [testView2Controller view];
    
    [box setContentView:view];
}


I'll look into the other things you can do with NSViewController in the next part as this seems to be getting quite long.


The project for this example can be downloaded from http://www.mediafire.com/?c3n3wob0sdppp9l (sorry its on Mediafire, hopefully it won't be taken down)
Apple's NSViewController documentation for Cocoa: http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/NSViewController_Class/Introduction/Introduction.html


Links to the other parts of the NSViewController articles
Part 2:- http://comelearncocoawithme.blogspot.com/2011/07/nsviewcontrollers-part-2.html
Part 3:- http://comelearncocoawithme.blogspot.com/2011/08/nsviewcontrollers-part-3.html
Part 4:- http://comelearncocoawithme.blogspot.com/2011/08/nscollectionview-redux.html

Wednesday, 13 July 2011

An Introduction

Everyone seems to start one of these blogs when they're learning Cocoa, so I thought I'd start one too. Partly it'll help me to remember what I've worked out, maybe it'll help someone else too.

I've been programming in C/C++ on Unix for about 13 years now and I thought I'd expand my skill set. As such its unlikely that this blog will cover much about learning objective C or C and I'll probably take knowledge of those things as assumed. Maybe not, I've not written anything yet so who knows.

Recently I've been playing around with Controllers, ArrayControllers, ObjectControllers and ViewControllers, so I think I'll start with something like that. I also have an interest in audio and MIDI and graphics programming, maybe something will come from that.

Some books that I can recommend that I've found useful are two from Big Nerd Ranch:
* iOS Programming: The Big Nerd Ranch Guide (2nd Edition) (Big Nerd Ranch Guides)
* Cocoa® Programming for Mac® OS X (3rd Edition)

Two very good tutorial style books that get the reader up to speed on beginning to program iOS and Cocoa, starting with explaining the basics of Objective C and using the developer tools Apple provide.

* Core Animation for Mac OS X and the iPhone: Creating Compelling Dynamic User Interfaces (Pragmatic Programmers)

This book on Core Animation is quite good as a reference and introduction to Core Animation concepts, but it isn't a tutorial book and assumes quite a high level of knowledge of Objective C and Cocoa technologies.

* Programming with Quartz: 2D and PDF Graphics in Mac OS X (The Morgan Kaufmann Series in Computer Graphics)

Programming with Quartz is a huge guide and reference to drawing with Quartz. I'd say its essential if you're planning on doing any custom drawing in your applications.