Default behaviour of UISplitViewController collapseSecondaryViewController

The documentation for UISplitViewControllerDelegate collapseSecondaryViewController says:

When you return NO, the split view controller calls the collapseSecondaryViewController:forSplitViewController: method of the primary view controller, giving it a chance to do something with the secondary view controller’s content. Most view controllers do nothing by default but the UINavigationControllerclass responds by pushing the secondary view controller onto its navigation stack.

Similarily, for separateSecondaryViewControllerFromPrimaryViewController it says:

When you return nil from this method, the split view controller calls the primary view controller’s separateSecondaryViewControllerForSplitViewController: method, giving it a chance to designate an appropriate secondary view controller. Most view controllers do nothing by default but the UINavigationControllerclass responds by popping and returning the view controller from the top of its navigation stack.

I thought it might be interesting to try to implement this magic behaviour to help understand what is going on and thus provide a starting point for customisation.

- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
    
    if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]] && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)) {
        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;
    } else {
        
        // push the secondary view controller onto the primary's navigation stack
        UINavigationController *nav = (UINavigationController *)primaryViewController;
        [nav pushViewController:secondaryViewController animated:NO];
        return YES;

    }
}

- (nullable UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController{

    // respond by popping and returning the view controller from the top of its navigation stack
    UINavigationController *nav = (UINavigationController *)primaryViewController;
    return [nav popViewControllerAnimated:NO];

}

It’s strange it is possible to push the secondary navigation controller , since usually that exceptions with “Pushing a navigation controller is not supported”. Turns out within the push method it checks a private property _allowNestedNavigationControllers to allow it to pass in this case, and it must have been set by the split view controller at some point. These kind of tricks are really annoying because gives inconsistent behaviour and thus lowers developer confidence in the APIs.
It’s interesting to call: [(UINavigationController *)secondaryViewController setViewControllers:nil]; before pushing because it throws an exception that proves that nested navigation controllers are being used: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot display a nested UINavigationController with zero viewControllers

I find using the plus iPhone simulator useful for testing the split controller because in portrait it is compact width but rotating to landscape it moves to regular width.

ObjC Delegation and Notifications

I was researching the different ways people are implementing the part of MVC where the model notifies the controller about new data. A limitation of the delegate pattern is only one class can be the delegate, for other classes to listen then they need to use Notification Center notifications (or use a custom multi-cast delegate). Normally when the model class is called to say, insert an object, it would use the standard routine to check the delegate responds to the selector, and then call the delegate method as defined on the protocol. Then when reading some Apple’s delegations article I noticed something rather interesting:

Delegation and Notifications
The delegate of most Cocoa framework classes is automatically registered as an observer of notifications posted by the delegating object. The delegate need only implement a notification method declared by the framework class to receive a particular notification message. Following the example above, a window object posts an NSWindowWillCloseNotification to observers but sends a windowShouldClose: message to its delegate.

The interesting point here is that the delegate is registered as an observer, which is different from calling the delegate method directly. To check this was a correct statement I went to the gold mine that is the GNUStep source code for NSWindow (Github Mirror) and found this very nifty piece of code:

- (void) setDelegate: (id)anObject
{
  if (_delegate)
    {
      [nc removeObserver: _delegate name: nil object: self];
    }
  _delegate = anObject;
#define SET_DELEGATE_NOTIFICATION(notif_name) \
if ([_delegate respondsToSelector: @selector(window##notif_name:)]) \
[nc addObserver: _delegate \
selector: @selector(window##notif_name:) \
name: NSWindow##notif_name##Notification object: self]

SET_DELEGATE_NOTIFICATION(DidBecomeKey);
SET_DELEGATE_NOTIFICATION(DidBecomeMain);
SET_DELEGATE_NOTIFICATION(DidChangeScreen);
SET_DELEGATE_NOTIFICATION(DidDeminiaturize);
SET_DELEGATE_NOTIFICATION(DidExpose);
SET_DELEGATE_NOTIFICATION(DidMiniaturize);
SET_DELEGATE_NOTIFICATION(DidMove);
SET_DELEGATE_NOTIFICATION(DidResignKey);
SET_DELEGATE_NOTIFICATION(DidResignMain);
SET_DELEGATE_NOTIFICATION(DidResize);
SET_DELEGATE_NOTIFICATION(DidUpdate);
SET_DELEGATE_NOTIFICATION(WillClose);
SET_DELEGATE_NOTIFICATION(WillMiniaturize);
SET_DELEGATE_NOTIFICATION(WillMove);
}

So sure enough, it adds the delegate as an observer, and only doing so if it responds to the selector. It’s is a pretty good optimisation because now at each event only the notification needs to be posted. Previously, the delegate performing the selector would be checked every time (still done this way for delegate methods that contain data). I also really like the macro. The only problem is the notifications are rather primitive in that they contain no data about the reason, they only contain the object self. In seeing this implementation it sparked my interest to investigate if Apple implemented it the same way, which could be tested by removing the delegate as an observer and then seeing if the delegate method is no longer called, don’t think I’ll bother just now though.

NSArray @property backed by a NSMutableArray

Saw a bunch of posts today on Stack Overflow asking how to implement this:

NSArray @property backed by a NSMutableArray

Hiding privately mutable properties behind immutable interfaces in Objective-C (my answer)

Protect from adding object to NSMutableArray in public interface

Having readonly nsarray property and nsmutable array not readonly with the same name and _ in xcode 4.2 vs 4.5

How to expose an NSMutableArray as an NSArray as a return type from a method

The main reason is to expose an array as immutable in the public interface, to convey that it shouldn’t be modified externally, but can be mutated internally. This is a typical thing you would want to do in a Model class, e.g. having an insertObject method and then notifying controllers via a delegate and notifications. One of the stumbling blocks I noticed is people were trying to implement is solely with their knowledge of properties, where you can redefine a readonly property as readwrite internally using a category, unfortunately you cannot change its type. With some understanding of ObjC before automatic synthesis the solution is to manually implement the ivar that backs the property, like this:

.h file:

@interface Model : NSObject

@property (nonatomic, strong) NSArray *objects;

-(void)insertObject:(id)object

// todo: delegate
@end

.m file:

@implementation Model{
    NSMutableArray* _objects;
}

-(void)insertObject:(id)object{
    if(!_objects){
        _objects = [[NSMutableArray alloc] init];
    }
    [_objects addObject:object];
}

@end

Just one of the many reasons developers have such a difficulty extracting their model from the view controller in Apple’s default project templates.