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

Multiple virtual pages in a UIScrollView with just 2 child views

The UIScrollView and UIPageControl in Cocoa Touch allow for user interfaces with multiple panning pages. The sample project that Apple provides (PageControl) keeps all child views for every page in a lazily loaded array. I'll show you how you can implement this using just two child views, no matter how many virtual pages you wish to represent.

First, something completely different...

Last week, Dan Grigsby of Mobile Orchard interviewed me about a post I wrote on Automated user interface testing on the iPhone. He posted the podcast of the interview online earlier this week, so I recommend you go listen if you're interested in the Director's Commentary for that earlier post.

Back to this post...

This week, I'm presenting the following application:

pagingscrollview.png

The black region is a UIScrollView with pagingEnabled set to YES. The dots at the bottom are a UIPageControl indicating the number of pages in the UIScrollView.

Apple's sample application "PageControl" presents a similar user interface that loads a different UIViewController and UIView for each of the pages in the UIScrollView. I'll show you how you can create the same effect using exactly two child UIViewControllers and UIViews — a "current" and the "next" view which leap around out-of-view to fill in each new page as the scrolling reaches it.

Fast display, slow content

The approach I will display is optimized for views that can redisplay quickly, once their data is loaded. The data may or may not be slow-to-load data so it is important that it is not loaded as part of the view.

Data is therefore loaded and cached in a separate, data-specific class. In the sample app, it will be a class named DataSource that holds an array of page titles and page text — simple in this case but it's just a sample app.

Holding your data in a view-independent class is basic program design — as good programmers, I'm sure you always do this and never, ever have your UIViewControllers directly load and store your data. Ever.

Moving child views to maintain the illusion

The trick in this post is handling an arbitrary array of child pages using just two views. It will work as follows:

  1. Initially, the displayed view will be the currentPage view. The next view will be configured to display the cached data for Page 1
  2. As the user begins scrolling to the right, the nextPage is quickly moved into the location for the next page in the scrolling direction. At the same time as it is positioned, nextPage is configured with the data for Page 2. Neither of these configuration changes will be visible to the user.
  3. As the scroll operation ends, the pointers for currentPage and nextPage are swapped so that currentPage now points to the view displaying Page 2 and nextPage now points to Page 1
  4. If the next scroll is to the left, nextPage is already configured and in position. If the next scroll is to the right, nextPage will be moved and configured as it was during the first scroll.

The code that chooses how to move the views as the scroll view scrolls (step 2 in the above description) looks like this:

- (void)scrollViewDidScroll:(UIScrollView *)sender
{
    CGFloat pageWidth = scrollView.frame.size.width;
    float fractionalPage = scrollView.contentOffset.x / pageWidth;
    
    NSInteger lowerNumber = floor(fractionalPage);
    NSInteger upperNumber = lowerNumber + 1;
    
    if (lowerNumber == currentPage.pageIndex)
    {
        if (upperNumber != nextPage.pageIndex)
        {
            [self applyNewIndex:upperNumber pageController:nextPage];
        }
    }
    else if (upperNumber == currentPage.pageIndex)
    {
        if (lowerNumber != nextPage.pageIndex)
        {
            [self applyNewIndex:lowerNumber pageController:nextPage];
        }
    }

I've left off the final condition for size but it is a rarely invoked path for when very fast scrolling leaves the currentPage out of position and it needs to configure both pages.

The exchange of pointers at the end of scrolling (step 3 in the above description) is handled in the scrollViewDidEndScrollingAnimation: method:

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)newScrollView
{
    CGFloat pageWidth = scrollView.frame.size.width;
    float fractionalPage = scrollView.contentOffset.x / pageWidth;
    NSInteger nearestNumber = lround(fractionalPage);

    if (currentPage.pageIndex != nearestNumber)
    {
        PageViewController *swapController = currentPage;
        currentPage = nextPage;
        nextPage = swapController;
    }

    pageControl.currentPage = currentPage.pageIndex;
}

The only remaining component to reveal is exactly how the pages are positioned and configured in applyNewIndex:pageController:

- (void)applyNewIndex:(NSInteger)newIndex pageController:(PageViewController *)pageController
{
    NSInteger pageCount = [[DataSource sharedDataSource] numDataPages];
    BOOL outOfBounds = newIndex >= pageCount || newIndex < 0;

    if (!outOfBounds)
    {
        CGRect pageFrame = pageController.view.frame;
        pageFrame.origin.y = 0;
        pageFrame.origin.x = scrollView.frame.size.width * newIndex;
        pageController.view.frame = pageFrame;
    }
    else
    {
        CGRect pageFrame = pageController.view.frame;
        pageFrame.origin.y = scrollView.frame.size.height;
        pageController.view.frame = pageFrame;
    }

    pageController.pageIndex = newIndex;
}

You can see here that if a page is given an out-of-bounds index, it is placed below the bottom of the scroll view (invisible). I chose not to use setHidden: on the view because this resulted in "pop-in" when making the view visible again.

The actual configuration of the view for the new page index happens in the setter method for pageController.pageIndex. This method fetches the data for the page out of the DataSource and configures the view for displaying that page.

- (void)setPageIndex:(NSInteger)newPageIndex
{
    pageIndex = newPageIndex;
    
    if (pageIndex >= 0 &&
        pageIndex < [[DataSource sharedDataSource] numDataPages])
    {
        NSDictionary *pageData =
            [[DataSource sharedDataSource] dataForPage:pageIndex];
        label.text = [pageData objectForKey:@"pageName"];
        textView.text = [pageData objectForKey:@"pageText"];
    }
}

Notice that I've made this method tolerant of out-of-bounds indices. This is because while out-of-bounds indices are invalid for data from the DataSource, they can be valid locations for views in the scroll view (representing an offscreen position) so these positions must be permitted.

Offscreen UITextViews don't update correctly

If you take a look at the code for this sample project, you'll notice a method that I've added named updateTextViews: and a value I track named textViewNeedsUpdate.

These parts of the program exist because UITextView (used for the "Some text for Page X" display) don't update if they are offscreen (in this case: in an offscreen page of the UIScrollView). This behavior isn't a problem when the UITextView remains offscreen but becomes especially annoying when it is brought onscreen and still doesn't update.

I have addressed this in 2 ways:

  1. When an update is applied, check if the UITextView is offscreen. If it is, set textViewNeedsUpdate to YES. I check this value regularly during a scroll, to see if the UITextView has appeared and update the view when it does.
  2. In some fast scrolling cases (mostly when using the UIPageControl instead of the UIScrollView to page) this still doesn't work — so I always force one update to the currentPage at the end of scrolling. This case can result in a visible update if your eyes are quick but it is an uncommon case.

Conclusion

You can download the complete Xcode 3.1 project for PagingScrollView (31kB).

It is possible to handle a paging scroll view using just 2 child views, no matter how many virtual pages you wish to support.

This approach assumes the views can be reconfigured to display a new virtual page quickly (normally a safe assumption). Remember that the data displayed need not be fast to load for the pages to reconfigure quickly because the data is loaded and cached in a separate object, independent of the paging.

The bug in UITextView is annoying. I've presented a means of getting around it, although I'd be happy to see a better approach.

Demystifying NSApplication by recreating it

In this post I will recreate code that is normally concealed between the NSApplicationMain call (invoked in a Cocoa application's main function) and the sendEvent: method (which distributes the events to windows and views from the main run loop). By recreating this code for you, I hope to explain the steps that occur between program startup and the dispatch of events to your code — so you can gain greater understanding of what NSApplication does on your behalf.

Introduction

The default Cocoa Application template provided by Xcode contains just one line of code, in one function:

int main(int argc, char *argv[])
{
    return NSApplicationMain(argc,  (const char **) argv);
}

Somehow, this one line of code is enough to create a menubar, put a window onscreen and then handle user events (mouse clicks and menu selections) until the user quits the program, at which point the application gracefully exits.

What we know

Scattered throughout the Cocoa documentation are hints about what must happen in NSApplicationMain:

  • Read from the "Info.plist" file to determine:
    • The name of the "MainMenu" NIB file to load automatically on startup
    • The name of the application's principal class (the class of the application object)
  • Construct the application object
  • Load the "MainMenu" NIB file
  • Begin the "Run Loop" (which handles the rest of the program)

I will recreate all of these steps by implementing my own version of NSApplicationMain — named MyApplicationMain — and by creating my own NSApplication subclass that implements the run method for itself.

MyApplicationMain

Create the application object

The first step in the MyApplicationMain function is to read the name of the "Principal class" and construct the application object from this class.

NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
Class principalClass =
    NSClassFromString([infoDictionary objectForKey:@"NSPrincipalClass"]);
NSAssert([principalClass respondsToSelector:@selector(sharedApplication)],
    @"Principal class must implement sharedApplication.");
NSApplication *applicationObject = [principalClass sharedApplication];

The infoDictionary method returns the "Info.plist" for a bundle (in this case, our application's bundle) converted into an NSDictionary for easy access. All we need to do is get the "NSPrincipalClass" string and fetch the class identified by that string.

Since the application is a singleton, we construct it by calling the sharedApplication method on the class. If we try to construct the object using standard alloc and init calls, the NSApp singleton instance won't be set correctly and an exception will be thrown at a later point when a second application is created.

For more information on singletons, you can see my earlier post about singletons or visit Apple's page on Creating a Singleton Instance.
Load the contents of the MainMenu NIB file

Loading the "MainMenu" NIB file is a similar task: get the name from the infoDictionary and then load it.

NSString *mainNibName = [infoDictionary objectForKey:@"NSMainNibFile"];
NSNib *mainNib =
    [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle mainBundle]];
[mainNib instantiateNibWithOwner:applicationObject topLevelObjects:nil];

I briefly considered implementing the NSNib code too, to show how that works for loading objects in a NIB file, but since the format of a NIB file is not publicly declared, it wasn't possible. Suffice it to say that the instantiateNibWithOwner:topLevelObjects method performs the following steps:

  • Allocates all the objects in the NIB file and initializes them with one of the following methods:
    • initWithFrame: (used for NSView objects)
    • initWithCoder: (used for most other objects in the Interface Builder library)
    • init (used for all other objects)
  • Sets the IBOutlet pointers on objects as specified in the NIB file and establishes all bindings
  • Invokes awakeFromNib on all objects that implement this method
Start the run loop

It is extremely important that the application's main loop runs on the main thread. For this reason, we don't directly invoke run but instead make sure it is performed on the main thread.

if ([applicationObject respondsToSelector:@selector(run)])
{
    [applicationObject
        performSelectorOnMainThread:@selector(run)
        withObject:nil
        waitUntilDone:YES];
}

I also check that the applicationObject will respond to the method before trying to invoke it, just in case the "Principal Class" from the Info.plist file is not a valid NSApplication or NSApplication-like object.

Implementing our own run loop

The remaining task required so that you can trace the application's execution from startup to dispatch of events to your code is the implementation of the run loop.

Despite Apple's comment in the NSApplication documentation that run is one of the "Methods to Override", my brief search failed to uncover anyone who had actually done it. My suspicion is this: it is not a method to override and that document is woefully out-of-date. The only reason to implement your own run method now is curiosity, not functionality.

In that spirit, here's a reimplementation of run and a corresponding terminate method:

- (void)run
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    [self finishLaunching];

    shouldKeepRunning = YES;
    do
    {
        [pool release];
        pool = [[NSAutoreleasePool alloc] init];

        NSEvent *event =
            [self
                nextEventMatchingMask:NSAnyEventMask
                untilDate:[NSDate distantFuture]
                inMode:NSDefaultRunLoopMode
                dequeue:YES];

        [self sendEvent:event];
        [self updateWindows];
    } while (shouldKeepRunning);

    [pool release];
}

- (void)terminate:(id)sender
{
    shouldKeepRunning = NO;
}

It's as simple as you might expect: take the next event out of the queue for your application and route it appropriately using sendEvent: which handles the task of determining which window wants the event and invokes sendEvent: on the window to further route the event to a specific view.

For those interested: I discovered all the methods that need to be invoked in the run method in the most naive way: by searching the NSApplication documentation for "loop" to find all methods that claimed they were invoked in the main run loop.

The specific function of finishLaunching method is a bit of a mystery. All I know is that menus, menu updates and a host of other features won't work if it isn't invoked. It is possible that it sets up the responder chain for some actions. It is possible it adds observers to the run loop. I don't really know.

nextEventMatchingMask:untilDate:inMode:dequeue: is the biggest reason why I chose, when "reimplementing" NSApplication for this post, to make my implementation a subclass of NSApplication instead of a completely separate class. This method (or something it contains) converts notifications from the operating system and hardware drivers into NSEvent objects. A principal class is not required to be a subclass of NSApplication but I wouldn't know where to begin reimplementing this method, so I had to use NSApplication to do it.

Conclusion

You can download an implementation of the default Cocoa Application Xcode 3.1 project that uses this code: RecreatingNSApplication.zip (60kB)

This recreation of NSApplicationMain and NSApplication's run does not do everything that the real implementations do (I've deliberately kept it simple for clarity) but I think it shows that the key steps involved are straightforward and easy to understand.

There are a few steps that I haven't reimplemented: loading NIB files, retrieving events and routing events. Unfortunately, these remain black boxes to me, so I can't shed further light on them. You can find out some things by setting breakpoints (in your constructors for objects created in NIBs or in your event methods) and reading the names of Apple functions invoked to bring these commands to you.

Beyond this, you can also read how GNUStep implements the same methods. Apple doesn't necessarily do the same thing but GNUStep at least represents an observable solution to the same problems.

Multiple row selection and editing in a UITableView

By default, UITableView only supports single-row selection. In this post, I'll show you how to implement multi-row selection, similar to the message edit interface in Mail.

Introduction

The target behavior for this post is an editing mode for a UITableView which allows the selection of multiple rows and presents a button to perform an action on the selection rows.

The following is a screenshot of the sample application running:

multirow.png

When not editing, neither the column of circles and check marks nor the bottom toolbar is visible. When the "Edit" button is clicked (located in place of the cancel button above), the "Edit" button is replaced by the "Cancel" button and the circles and check marks column and the bottom toolbar animate in.

Requirements for the implementation

UITableView does not support multiple selection. We will use the method tableView:didSelectRowAtIndexPath: to detect touches in rows but the selected state will need to be stored separately (we cannot rely on the UITableView's selection).

We will also need a background view for displaying the selection color and a UIImageView for displaying the not-selected/selected indicator. Since the UIImageView will be hidden while not editing and the label for the row needs to move left or right when it is shown or hidden, we will also need to implement some form of layout for the UITableViewCell.

Other required behaviors include switching the "Edit"/"Cancel" buttons between modes, showing/hiding the toolbar at the bottom and tracking the number of selected rows to display in the button in the toolbar.

Implementation

The implementation begins with Apple's default "Navigation-based Application" template.

I then changed the RootViewController to be a subclass of the GenericTableViewController implementation that I presented in my previous Heterogeneous cells in a UITableViewController post. In that post, this class was presented to aid handling of different cell types in one table. I use it again here with only one cell type because in this case, the CellController provides a convenient object in which to store the "selected" state for each row and allows me to keep each file smaller and narrower in focus because it keeps "row" behavior out of the table controller.

Toolbar

The first addition I made was that of the toolbar. This is initially hidden but needs to animate onto the screen when the edit mode is entered.

The toolbar is constructed in the viewDidLoad implementation:

actionToolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 416, 320, 44)];
actionButton =
    [[[UIBarButtonItem alloc]
        initWithTitle:@"No Action"
        style:UIBarButtonItemStyleBordered
        target:self
        action:@selector(noAction:)]
    autorelease];
[actionToolbar setItems:[NSArray arrayWithObject:actionButton]];

but cannot be easily added to the view hierarchy at this time. Instead, we wait until the viewDidAppear: method is invoked and add it as a child of the table's parent (the UINavigationController's content frame):

- (void)viewDidAppear:(BOOL)animated
{
    [self.view.superview addSubview:actionToolbar];
}

The initial location of the toolbar is below the bottom of the screen, so when editing begins, we need to move it up onto the screen. When editing ends, it is moved back again. This frame animation occurs in the showActionToolbar: method.

Edit Mode

Standard edit modes for UITableViews are started by calling setEditing:animated: on the UITableView. We are not going to use any of the standard UITableViewCellEditingStyles, so invoking this method is not strictly required but it will propagate a notification to the UITableViewCells and allow us to query the state at a later time so we will use it anyway.

The edit: and cancel: methods switch us into and out of "Edit" mode respectively.

- (void)edit:(id)sender
{
    [self showActionToolbar:YES];
    
    UIBarButtonItem *cancelButton =
        [[[UIBarButtonItem alloc]
            initWithTitle:@"Cancel"
            style:UIBarButtonItemStyleDone
            target:self
            action:@selector(cancel:)]
        autorelease];
    [self.navigationItem setRightBarButtonItem:cancelButton animated:NO];
    [self updateSelectionCount];
    
    [self.tableView setEditing:YES animated:YES];
}

- (void)cancel:(id)sender
{
    [self showActionToolbar:NO];

    UIBarButtonItem *editButton =
        [[[UIBarButtonItem alloc]
            initWithTitle:@"Edit"
            style:UIBarButtonItemStylePlain
            target:self
            action:@selector(edit:)]
        autorelease];
    [self.navigationItem setRightBarButtonItem:editButton animated:NO];

    NSInteger row = 0;
    for (MultiSelectCellController *cellController in
        [tableGroups objectAtIndex:0])
    {
        [cellController clearSelectionForTableView:self.tableView
            indexPath:[NSIndexPath indexPathForRow:row inSection:0]];
        row++;
    }

    [self.tableView setEditing:NO animated:YES];
}

Here you can see the "Edit"/"Cancel" buttons being swapped, the toolbar being shown/hidden and setEditing:animated: being invoked. I also implement tableView:canEditRowAtIndexPath: to always return yes, since all rows may be edited in this table.

Showing/hiding the check mark column

When setEditing:animated: is invoked on the table, the table in turn invokes the setEditing:animated: on all visible UITableViewCells, allowing each row to update for editing.

In response to this, we need to show/hide the check mark column. We handle this in a UITableViewCell subclass where the setEditing:animated: is implemented to call setNeedsLayout and the layoutSubviews method is overridden to handle different layouts for the "Edit" an "Not Editing" modes.

When editing, the cell's contentView is shifted to the right, otherwise it is layed out flush against the left side. This is all we'll need to display the extra column because the check mark column is always present in the cell. Outside of editing mode, it is layed out off the left of screen (so you can't see it). When the contentView is shifted right by 35 pixels during editing, the check mark column (which is located at the contentView's origin minus 35 pixels horizontally) becomes visible.

- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
    [self setNeedsLayout];
}

- (void)layoutSubviews
{
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationBeginsFromCurrentState:YES];
        
    [super layoutSubviews];

    if (((UITableView *)self.superview).isEditing)
    {
        CGRect contentFrame = self.contentView.frame;
        contentFrame.origin.x = EDITING_HORIZONTAL_OFFSET;
        self.contentView.frame = contentFrame;
    }
    else
    {
        CGRect contentFrame = self.contentView.frame;
        contentFrame.origin.x = 0;
        self.contentView.frame = contentFrame;
    }

    [UIView commitAnimations];
}

The setEditing:animated: implementation ensures that re-layout occurs every time edit mode is entered/exited.

Notice that no custom drawing happens here in the UITableViewCell. I've seen many people override UITableViewCell for custom drawing but I don't think it's a good idea. The UITableViewCell is really just a layout container and that should be the only way you use it. Custom drawing should go in the backgroundView, selectionView or contentView that the UITableViewCell contains.

Drawing the cell

I use a UILabel for the text rather than setting the text property of the cell because it makes it easier to get a transparent background for the text (which I'll need to see the blue selection color).

I set cell.selectionStyle = UITableViewCellSelectionStyleNone because I don't want to use the standard selection view at all (it is limited to single rows). Instead, I achieve a selection color by creating a backgroundView for the cell and setting its background color to white or pale blue as appropriate.

The selection indicator is just a UIImageView. As previously indicated, it is layed out 35 pixels left of the contentView which places it offscreen. When the contentView is shifted right during editing, it will become visible.

The only other important behavior is that the CellController must invoke updateSelectionCount on the RootViewController when selected/deselected so that the selection count can be updated when the selection changes. I implement this in a lazy fashion by recounting all selected rows — you should probably implement this in a more efficient fashion.

Conclusion

You can download the complete MultiRowSelect Xcode 3.1 project (40kB).

The final result is a few hundred lines of code. This is not a giant mountain of code by any means but still a considerable volume given how simple "multi-row selection" might seem as a description. I think this serves to show that user-interface implementations can be very time consuming when the desired functionality is not provided by the default libraries.

None of the code is particularly complex but it still involves a lot of coordination between the table, table controller and cell so I hope that this sample implementation simplifies the task for anyone else who needs to implement it in the future.

Serving an NSManagedObjectContext over an NSConnection

In this post, I'll show you how you can serve a Core Data document over a network using NSConnection. This arrangement will never be as efficient or safe as writing your own code to communicate the data over the network but the promise of transparent and automatic networking seemed too tempting to pass up.

NSConnection and NSManagedObjectContext

NSConnection is an often forgotten tool in the Foundation toolbox which allows messages to be sent from local objects to remote objects (in another process or on another machine). Once set up, it works almost transparently, especially when you keep the data sent over the NSConnection small and simple.

NSManagedObjectContext manages the data graph read from a Core Data persistent store (normally an SQLite database). It handles huge amounts of complex data.

Other than their shared presence in Cocoa, these classes don't have much common ground. However, they seem like they should produce the Holy Grail of network computing: complex object management transparently served and managed over a network.

Of course, it won't be easy: these APIs aren't likely to be sympathetic to each other. In fact, it's probably just a bad idea.

The sample app

I'm going to present a variation on Apple's Core Data Stickies sample app (the original is installed by the Developer Tools at /Developer/Examples/Core Data/Stickies/).

Stickies look like this:

stickies.png

Download the finished ClientServerStickies Xcode 3.1 project (92kB).

There are two versions of the program: a client and a server. The server will host the NSManagedObjectContext and any number of client may connect to it.

Both client and server will continue to look like the original Stickies app, the only difference is that the server contains the only copy of the data and the note information is shared to each client over a network connection.

Since the position of each note is shared, if you run the client and server on the same machine, the windows will be positioned on top of each other. Hide either the client or the server to check the other.

Setup

I've taken the Stickies project and duplicated the target, splitting it into a Server and Client target. The Server has the C preprocessor macro STICKIES_SERVER defined, so code differences between the two will be wrapped in #ifdefs using this macro. They also use different Info.plist files (to keep their CFBundleIdentifiers different) but are otherwise the same.

To run the project, you must build both targets and always start the server before the client.

Getting started: sharing the NSManagedObjectContext

The first step is to get the client and the server using the same NSManagedObjectContext. For this, we modify the managedObjectContext method in StickiesAppDelegate.m

The server version remains the same until immediately before the return line, we add:

NSSocketPort *port =
    [[[NSSocketPort alloc] initWithTCPPort:STICKIES_SERVER_PORT] autorelease];

NSConnection *theConnection =
    [[NSConnection connectionWithReceivePort:port sendPort:nil] retain];
[theConnection setDelegate:self];
[theConnection setRootObject:managedObjectContext];

This is all we need to share the NSManagedObjectContext.

For the client, we remove all the context creation code, leaving:

@try
{
    NSSocketPort *port =
        [[[NSSocketPort alloc]
            initRemoteWithTCPPort:STICKIES_SERVER_PORT host:@"127.0.0.1"]
        autorelease];

    NSConnection *theConnection =
        [[NSConnection connectionWithReceivePort:nil sendPort:port] retain];
    [theConnection setDelegate:self];
    managedObjectContext =
        (NSManagedObjectContext *)[[theConnection rootProxy] retain];
}
@catch (NSException *e)
{
    [[NSAlert
        alertWithMessageText:@"Connection error"
        defaultButton:nil
        alternateButton:nil
        otherButton:nil
        informativeTextWithFormat:
            @"Server not found. Application will terminate."]
    runModal];
    
    [NSApp terminate:self];
}

This connects to the vended NSManagedObjectContext and uses it remotely.

You can see here that I've hard-coded 127.0.0.1 (the localhost). If you want to actually use over a network, you can change this or publish using NSNetService if you're properly ambitious. Clearly, I'm not.

That was great! And so simple. Does it work? Of course not.

What is NSKnownKeysDictionary1?

To work over an NSConnection an object must be encodeable by an NSPort. Almost everything can be encoded by the default NSPortCoder. Of course, I use the word "almost" because there are some things which can't be encoded. Generally, void * and pointers to pointers can't be encoded. In your own code, you can implement substitutions for the port coder to make it work.

But Foundation is not my code, so when the return value from entitiesByName refuses to encode because its subclass of NSDictionary is too weird to be encoded, I have to take different measures. The following delegate method replaces this weird return with something more normal that will journey over the NSConnection.

- (BOOL)connection:(NSConnection *)conn
    handleRequest:(NSDistantObjectRequest *)doReq
{
    NSInvocation *invocation = [doReq invocation];
    [invocation invoke];

    if ([invocation selector] == @selector(entitiesByName))
    {
        id retVal;
        [invocation getReturnValue:&retVal];
        
        NSDictionary *rebuilt = [NSDictionary dictionaryWithDictionary:retVal];
        [invocation setReturnValue:&rebuilt];
    }

    [doReq replyWithException:nil];

    return YES;
}

This is a delegate method for the NSConnection. In this case, it will intercept the return results from entitiesByName (when this message is sent to an object on the server-side) and replace it with a clean and normal NSDictionary, not the unfriendly NSKnownKeysDictionary1 oddball.

Don't have the model create the view. Really.

If you run the program using only the changes listed so far, the client won't show any windows.

This is because the windows for the Stickies are created in Sticky.m in methods that are part of the Sticky class. This is the NSManagedObject subclass for the Sticky entity in the model. Like everything in the NSManagedObjectContext, it only exists on the server — so the server will have windows but the client wont.

I'm not sure what the poor, overworked code-monkey that wrote this was thinking but you shouldn't write a program like this. The model should never create its view; a controller should.

The NSArrayController fetches the Stickies for each client. To fix the creation of windows, we need something that watches the NSArrayController and creates the windows correctly on the client side when new objects are added to its arrangedObjects.

All of the code that was in Sticky.m is pulled out and put into StickyWindowController.m. We can then have multiple window controllers for the same underlying data object — one window controller per client and one for the server.

I've put the code that creates the window controllers into the StickiesAppDelegate. This isn't the most sensible place for it but the NSArrayController is already connected to this class in the sample so I'm working with what I have.

Replacing the old applicationDidFinishLaunching: and adding synchronization methods gives:

- (void)applicationDidFinishLaunching:(NSNotification *)notification 
{
    stickyWindowControllers = [[NSMutableArray alloc] init];
    [stickiesController
        addObserver:self
        forKeyPath:@"arrangedObjects"
        options:NSKeyValueObservingOptionNew
        context:nil];
}

- (void)syncWithStickiesController 
{
    NSSet *newArrangedObjects =
        [NSSet setWithArray:[stickiesController arrangedObjects]];
    NSSet *oldArrangedObjects =
        [NSSet setWithArray:[stickyWindowControllers valueForKey:@"sticky"]];

    NSMutableSet *createdObjects = [NSMutableSet setWithSet:newArrangedObjects];
    [createdObjects minusSet:oldArrangedObjects];
    
    for (NSManagedObject *sticky in createdObjects)
    {
        [stickyWindowControllers addObject:
            [[[StickyWindowController alloc]
                initWithSticky:sticky]
            autorelease]];
    }
    for (StickyWindowController *stickyWindowController in
        [NSArray arrayWithArray:stickyWindowControllers])
    {
        if ([oldArrangedObjects containsObject:[stickyWindowController sticky]] &&
            ![newArrangedObjects containsObject:[stickyWindowController sticky]])
        {
            [stickyWindowController close];
            [stickyWindowControllers removeObject:stickyWindowController];
        }
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
    change:(NSDictionary *)change context:(void *)context 
{
    if ([keyPath isEqualTo:@"arrangedObjects"])
    {
        [self syncWithStickiesController];
    }
}

Two problems with NSArrayController remain

The last two issues I had to address were related to the NSArrayController not actually being in the same process as the NSManagedObjectContext to which it is connected.

The NSArrayController lives on each client but it is asked to create new Sticky entities, which must live on the server. When it tries to allocate them on the client, it fails. In a more general sense: how do we create new NSManagedObjects on the server from the client side?

The second problem is that NSArrayController listens for new or deleted objects in the context through NSNotifications. The NSManagedObjectContext only sends these to the NSNotificationCenter on the server. So the NSArrayController isn't receiving change notifications for the Stickies.

To solve the first problem, I extend NSManagedObject on the client to intercept attempts to create objects and bounce these client-side requests to be performed on the server.

On the server, I listen for the same notifications that NSArrayController needs and forward them to each of the clients.

All of this is bundled up in the following code:

#ifdef STICKIES_SERVER

@implementation NSManagedObjectContext (ServerRedirection)

- (id)remotelyAllocateWithEntity:(NSEntityDescription *)entity
{
    NSEntityDescription *localEntity = [entity valueForKey:@"self"];
    return [[NSManagedObject alloc]
        initWithEntity:localEntity insertIntoManagedObjectContext:self];
}

- (void)forwardContextChangeNotificationsTo:(id)receiver
{
    [[NSNotificationCenter defaultCenter]
        addObserver:receiver
        selector:@selector(forwardNotification:)
        name:@"_NSObjectsChangedInManagingContextPrivateNotification"
        object:self];
}

@end

#else

@implementation NSManagedObject (ServerRedirection)

- (id)initWithEntity:(NSEntityDescription *)entity
    insertIntoManagedObjectContext:(NSManagedObjectContext *)context
{
    return [context remotelyAllocateWithEntity:entity];
}

@end

#endif

To make the forwarding work, the client needs to invoke forwardContextChangeNotificationsTo: on the remote NSManagedObjectContext object, passing in itself, and then implement the forwardNotification: method to send the notification method to the local NSNotificationCenter.

Conclusion

You can download the finished ClientServerStickies Xcode 3.1 project (92kB).

I hope this post has shown some of the quirks associated with vending arbitrary objects over an NSConnection to achieve networking.

There are at least a few (possibly many) issues remaining. One of these is that if a client quits without explicitly calling removeObserver:forKeyPath: on any observed server objects, the server will crash if it later tries to send the observation. To fix this, you'd need to track all observations established over the NSConnection and remove them yourself in the case of abrupt client disconnection.

This setup is also verbose, with lots of small messages getting sent forwards and backwards. Not a problem when runnning on the local host or over a local network but I don't think you'd want to run this over the broader Internet.