Cocoa Touch added API for presenting a view controller in a popup bubble in iPhone OS 3.2, the responsible class is named UIPopoverController
. One would guess that this new class is a subclass of UIViewController
, just like UINavigationController
is, but that is not the case. One would also guess that in functionality many ideas for displaying a view controller as modal controller would have been moved over to displaying a view controller as a popover controller, but that is not the case either.
Modal View Controllers are Easy
Displaying a modal view controller is quite straight forward as this small example shows:
1 2 3 4 5 |
-(void)showDetails:(CWDetails*)details { UIViewController* vc = [[CWDetailsViewController alloc] initWithDetails:details]; [self presentModalViewController:vc animated:YES]; [vc release]; } |
Popover View Controllers Can be Complex
Displaying the same details view controller in a popover on iPad is not quite as straight forward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-(void)showDetails:(CWDetails*)details fromBarButtonItem:(UIBarButtonItem*)item { UIViewController* vc = [[CWDetailsViewController alloc] initWithDetails:details]; self.popoverController = [[[UIPopoverController alloc] initWithContentViewController:vc] autorelease]; self.popoverController.delegate = self; [self.popoverController presentPopoverFromBarButtonItem:item permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; [vc release]; } -(void)popoverControllerDidDismissPopover:(UIPopoverController*)popoverController { self.popoverController = nil; } |
As you see presenting a popover requires an extra object instance, this instance is not automatically retained while presenting the popover, like a modal view controller is, so the instance must be saved somewhere. In this example I saved the instance as a property. Notice that I also register as a delegate for the popover in order to remove this instance when the popover is dismissed. Here is another detail that is not obvious; this delegate callback is only called if user action dismisses the popover, not if you programmatically dismiss the popover. In the end using popovers quite quickly introduces a lot of code, and also coupling between view controllers for no other reason but to manage the popover instance.
Would it not be nice if you could instead do like this, and be done with it:
1 2 3 4 5 |
-(void)showDetails:(CWDetails*)details fromBarButtonItem:(UIBarButtonItem*)item { UIViewController* vc = [[CWDetailsViewController alloc] initWithDetails:details]; [self presentPopoverViewController:vc fromBarButtonItem:item animated:YES]; [vc release]; } |
In fact what we want is to have these methods added to the public API of UIViewController
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@interface UIViewController (CWPopover) @property (nonatomic, retain, readonly) NSSet* visiblePopoverControllers; -(void)presentPopoverViewController:(UIViewController*)controller fromBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated; -(void)presentPopoverViewController:(UIViewController*)controller fromView:(UIView *)view animated:(BOOL)animated; -(void)dismissPopoverController:(UIViewController*)controller animated:(BOOL)animated; -(void)dismissAllPopoverControllersAnimated:(BOOL)animated; -(void)setContentSize:(CGSize)size forViewInPopoverAnimated:(BOOL)animated; @end |
But That Requires Rewriting Existing Classes?
Yes it does, in an ideal world we want new methods and properties on the existing UIViewController
class, and all subclasses, just as if Apple had put them there. This can easily be done thanks to the dynamic nature of Objective-C.
The view controller that is going to present other view controllers in a popover needs to have access to all UIPopoverController
instances currently visible. The view controllers that are going to be presented in a popover need to have access to their container UIPopoverController
, and for consistency with modal view controller its parent view controller.
Categories can only add and replace methods on an existing class, not modify instance variables. So we need to store this information in another way. I choose to tuck them away in a helper class called CWViewControllerPopoverHelper
. With this small interface:
1 2 3 4 5 6 7 8 9 10 11 |
@interface CWViewControllerPopoverHelper : NSObject { @private UIViewController* parentViewController; UIPopoverController* containerPopoverController; NSMutableSet* visiblePopoverControllers; } @property (nonatomic, assign) UIViewController* parentViewController; @property (nonatomic, assign) UIPopoverController* containerPopoverController; @property (nonatomic, retain) NSMutableSet* visiblePopoverControllers; @end |
The implementation is just synthesized properties, so no need to duplicate that one here.
So now all we do is to stuff one instance of CWViewControllerPopoverHelper
into a global map using the UIViewController
as key for each view controller that needs these extra instance information.
Fetching the Popover Helper
Let’s implement a helper method that lazily creates a CWViewControllerPopoverHelper
instance and puts it into the global map when needed. This way any view controller that do not need to support popovers will never be bothered, and those who do manage popovers gets one instance with no fuzz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static NSMutableDictionary* popoverHelpers = nil; -(CWViewControllerPopoverHelper*)popoverHelper; { NSValue* key = [NSValue valueWithPointer:self]; if (popoverHelpers == nil) { popoverHelpers = [[NSMutableDictionary alloc] initWithCapacity:16]; } CWViewControllerPopoverHelper* helper = [popoverHelpers objectForKey:key]; if (helper == nil) { helper = [[[CWViewControllerPopoverHelper alloc] init] autorelease]; [popoverHelpers setObject:helper forKey:key]; } return helper; } |
Proper Memory Management
There is one problem with storing the extra information in a global map; no view controller would ever be released from memory, since the map will hold on to the references. No problem, lets not use the actual view controller instance as a key, but a NSValue
with the pointer to the instance. This solves the problem of releasing objects, but also means that the map will contain dangling pointers to released objects. We need to remove the dangling pointers whenever a UIViewController
is deallocated.
Subclassing is not an option, since it would defeat our purpose of requiring the public API. It turns out we can replace the -[UIViewController dealloc]
method with our own, and still call the original implementation. Each class and category that is loaded into the Objective-C run-time will call their own method +[? load]
method, it is the public hook for further modifying a class or category before use. We want to use this hook to replace the default deallocation method with our own:
1 2 3 4 5 6 7 8 9 |
+(void)load; { Method m1 = class_getInstanceMethod(self, @selector(dealloc)); Method m2 = class_getInstanceMethod(self, @selector(cwPopoverDealloc)); method_exchangeImplementations(m1, m2); m1 = class_getInstanceMethod(self, @selector(parentViewController)); m2 = class_getInstanceMethod(self, @selector(cwPopoverParentViewController)); method_exchangeImplementations(m1, m2); } |
As a bonus we also replace -[UIViewController parentViewController]
, to return the parent as would be expected for anyone who has ever presented a modal view controller. The implementations of our overrides are just as short and sweet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-(void)cwPopoverDealloc; { [self dismissAllPopoverControllersAnimated:NO]; NSValue* key = [NSValue valueWithPointer:self]; [popoverHelpers removeObjectForKey:key]; [self cwPopoverDealloc]; } -(UIViewController*)cwPopoverParentViewController; { UIViewController* parent = [self popoverHelper].parentViewController; if (parent == nil) { parent = [self cwPopoverParentViewController]; } return parent; } |
Wrapping Up
From here on it is just a tedious work to implement all 50 more lines of code needed to actually present the popovers, the first method as an example:
1 2 3 4 5 6 7 8 9 10 11 |
-(void)presentPopoverViewController:(UIViewController*)controller fromBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated; { UIPopoverController* pc = [[[UIPopoverController alloc] initWithContentViewController:controller] autorelease]; [self addPopoverViewController:pc passthroughViews:views]; [pc presentPopoverFromBarButtonItem:item permittedArrowDirections:UIPopoverArrowDirectionAny animated:animated]; } |
The rest of the implementation, and a few other nice to have methods can be downloaded here.
Conclusions
The dynamic nature of Objective-C means that you can rewrite any existing API to suit your needs, without having access to the original source code. Sometimes just to add a new utility function where it belongs; a “sort by first name” method should probably be added to the actual people collection class, not as a spurious utility class.
Or as shown in this example to make a public API more convenient to use.
it seems that my question isn’t related to your topic, but I wonder if it is possible to get cocoa touch API and using it on windows environment as I am working on project that aims to develop iPhone apps on windows environment
so could I get and use cocoa touch API?
@R CSD: Short answer is no, Mac OS X is a requirement for iOS development. You can get it to work with vmware, but I would not recommend trying to go down that rout; there be dragons.