App Tour : Part 4: Mimicking instance variables for a category

by Andy

This is the final part of a four part series. The first part discussed dynamically injecting video into a UIWebView and followed on in the second part with how to detect scrolling to the end of the HTML page. The third part covered how to handle full screen video on an iPad.

The previous post in this series discussed how to handle full screen video when that video is launched from a popover on the iPad. A popover is not a standard part of the view hierarchy, so when the video is made full screen the popover remains on top of the video, which of course is not a desired effect. The solution is to recognize when the video enters full screen and hide the popover temporarily and restore it when the video exits.

In my application there are currently two different view controllers that present a popover with the potential to show video. Therefore I wanted to wrap the hide and show effects into a nice neat code unit that I could reuse whenever necessary. In Objective-C that usually means a category. I could encapsulate the new hidePopover: and showPopover: methods in a UIViewController category and import that wherever it was required.

Hiding any old popover is easy, but what I wanted to do was to restore the popover to the exact same state as when it was hidden. As my popover contained a UINavigationController and several UITableViewControllers, I needed to return to a view stack several layers deep. The easiest way to do this is to retain a reference to the popover’s content before dismissing it and using that reference when the popover is recreated.

// Temporarily hide a popover
- (void)hidePopover:(NSNotification *)notification {
self.hiddenPopoverContent = nil;
if (self.hiddenPopoverController) {
// Save some details of the current state
// Holding on to the content view means we can go back to the same state
self.hiddenPopoverContent = [self.hiddenPopoverController contentViewController];
self.hiddenPopoverContentSize = [self.hiddenPopoverController popoverContentSize];
id <UIPopoverControllerDelegate> popoverDelegate = [self.hiddenPopoverController delegate];
self.hiddenPopoverDelegate = popoverDelegate;
[self.hiddenPopoverController dismissPopoverAnimated:YES];
self.hiddenPopoverHidingInProgress = YES;
if (popoverDelegate) {
[popoverDelegate popoverControllerDidDismissPopover:self.hiddenPopoverController];
}
self.hiddenPopoverHidingInProgress = NO;
self.hiddenPopoverController = nil;
}
}
// Show a popover that had previously been hidden
- (void)showPopover:(NSNotification *)notification {
if (self.hiddenPopoverContent) {
UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:self.hiddenPopoverContent];
if ([popover respondsToSelector:@selector(popoverBackgroundViewClass)]) {
[popover setPopoverBackgroundViewClass:[ZingPopoverBackgroundView class]];
}
CGSize popSize = self.hiddenPopoverContentSize;
[popover setPopoverContentSize:popSize animated:YES];
[popover setDelegate:self.hiddenPopoverDelegate];
if (self.hiddenPopoverPresentingButton) {
[popover presentPopoverFromBarButtonItem:self.hiddenPopoverPresentingButton permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
} else {
[popover presentPopoverFromRect:self.hiddenPopoverPresentingRect inView:[self view] permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
}
self.hiddenPopoverController = popover;
}
}
view raw gistfile1.m hosted with ❤ by GitHub

The hidePopover: grabs the popover’s contentViewController before dismissal so that the content is not released, and when showPopover: recreates the popover, it reuses this content. Because the category retains the contentViewController, it’s state is left untouched and the user is returned to the same place in the navigation hierarchy.

Note that hidePopover: also retains information about the size and placement of the popover. Once we have this information then the category does not have to go back to the hosting view controller and ask it to re-present the popover again. As far as the hosting view controller is concerned. nothing has happened.

But the fly in the ointment here, is that categories cannot define new ivars, only methods. So how do we store new information just for the category, without having to extend the main view controller? If we have to modify the base class then that destroys the drop-in effect of the category.

Well, this is were we can drop down and exploit the Objective-C run-time.

With iOS 4.0, Apple introduced associated objects to the Objective-C run-time, providing a mechanism to link two objects together in an ad-hoc manner, without the need to explicitly declare variables. Effectively an object maintains a dictionary to a set of arbitrary objects.

The two main associated object functions are:

#import 

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
id objc_getAssociatedObject(id object, void *key)

The association policy is similar to the standard semantic – OBJC_ASSOCIATION_ASSIGN, OBJC_ASSOCIATION_RETAIN_NONATOMIC, OBJC_ASSOCIATION_COPY_NONATOMIC, OBJC_ASSOCIATION_RETAIN, OBJC_ASSOCIATION_COPY. Therefore an object can retain a reference to another object.

So let’s see how we can wrap this into a neat package for our category.

First we’ll declare a set of private properties and keys for our associations:

static char const * const HiddenPopoverControllerKey = "HiddenPopoverController";
static char const * const HiddenPopoverContentKey = "HiddenPopoverContent";
static char const * const HiddenPopoverDelegateKey = "HiddenPopoverDelegate";
static char const * const HiddenPopoverPresentingRectKey = "HiddenPopoverPresentingRect";
static char const * const HiddenPopoverPresentingButtonKey = "HiddenPopoverPresentingButton";
static char const * const HiddenPopoverArrowDirectionKey = "HiddenPopoverArrowDirection";
static char const * const HiddenPopoverAnimatedKey = "HiddenPopoverAnimated";
static char const * const HiddenPopoverContentSizeKey = "HiddenPopoverContentSize";
static char const * const HiddenPopoverHidingInProgessKey = "HiddenPopoverHidingInProgress";
@interface UIViewController (RBPopoverPrivate)
@property (nonatomic) UIPopoverController *hiddenPopoverController;
@property (nonatomic) UIViewController *hiddenPopoverContent;
@property (nonatomic) id <UIPopoverControllerDelegate> hiddenPopoverDelegate;
@property (nonatomic) CGRect hiddenPopoverPresentingRect;
@property (nonatomic) UIBarButtonItem *hiddenPopoverPresentingButton;
@property (nonatomic) UIPopoverArrowDirection hiddenPopoverArrowDirection;
@property (nonatomic) BOOL hiddenPopoverAnimated;
@property (nonatomic) CGSize hiddenPopoverContentSize;
@property (nonatomic) BOOL hiddenPopoverHidingInProgress;
@end
view raw gistfile1.m hosted with ❤ by GitHub

The properties allow an easy interface to the data in the other methods in the category, e.g. self.hiddenPopoverController and self.hiddenPopoverContent.

In the implementation, we provide our own accessor functions that fetch and store the data as associated objects, with a retain policy, to the base object.

@implementation UIViewController (RBPopoverPrivate)
- (UIPopoverController *)hiddenPopoverController {
return objc_getAssociatedObject(self, HiddenPopoverControllerKey);
}
- (void)setHiddenPopoverController:(UIPopoverController *)hiddenPopoverController {
objc_setAssociatedObject(self, HiddenPopoverControllerKey, hiddenPopoverController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIViewController *)hiddenPopoverContent {
return objc_getAssociatedObject(self, HiddenPopoverContentKey);
}
- (void)setHiddenPopoverContent:(UIViewController *)hiddenPopoverContent {
objc_setAssociatedObject(self, HiddenPopoverContentKey, hiddenPopoverContent, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// plus many more ...
@end
view raw gistfile1.m hosted with ❤ by GitHub

Note, assigning nil to an associated object will release it and remove the association.

Now we have the basics for our category to handle the presentation of a popover when there is a chance that we might need to temporarily hide it due to full screen video.

Here is the full code for the category.

@implementation UIViewController (RBPopover)
- (void)startTrackingHidePopover {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(hidePopover:)
name:NOTIFICATION_POPOVER_HIDE
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showPopover:)
name:NOTIFICATION_POPOVER_SHOW
object:nil];
}
- (void)stopTrackingHidePopover {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NOTIFICATION_POPOVER_HIDE
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NOTIFICATION_POPOVER_SHOW
object:nil];
}
- (void)presentPopover:(UIPopoverController *)popover fromRect:(CGRect)rect permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections animated:(BOOL)animated {
self.hiddenPopoverController = popover;
self.hiddenPopoverPresentingRect = rect;
self.hiddenPopoverPresentingButton = nil;
self.hiddenPopoverArrowDirection = arrowDirections;
self.hiddenPopoverAnimated = animated;
if ([popover delegate]) {
self.hiddenPopoverDelegate = [popover delegate];
}
[popover presentPopoverFromRect:rect inView:[self view] permittedArrowDirections:arrowDirections animated:animated];
}
- (void)presentPopover:(UIPopoverController *)popover fromBarButtonItem:(UIBarButtonItem *)button permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections animated:(BOOL)animated {
self.hiddenPopoverController = popover;
self.hiddenPopoverPresentingButton = button;
self.hiddenPopoverArrowDirection = arrowDirections;
self.hiddenPopoverAnimated = animated;
[popover presentPopoverFromBarButtonItem:button permittedArrowDirections:arrowDirections animated:animated];
}
- (void)dismissedPopover {
// Don't destroy any data if we are in the middle of the hiding process
if (self.hiddenPopoverHidingInProgress) {
return;
}
self.hiddenPopoverDelegate = nil;
self.hiddenPopoverContent = nil;
self.hiddenPopoverController = nil;
self.hiddenPopoverPresentingButton = nil;
}
#pragma mark - Popover notifications
// Temporarily hide a popover
- (void)hidePopover:(NSNotification *)notification {
self.hiddenPopoverContent = nil;
if (self.hiddenPopoverController) {
// Save some details of the current state
// Holding on to the content view means we can go back to the same state
self.hiddenPopoverContent = [self.hiddenPopoverController contentViewController];
self.hiddenPopoverContentSize = [self.hiddenPopoverController popoverContentSize];
id <UIPopoverControllerDelegate> popoverDelegate = [self.hiddenPopoverController delegate];
self.hiddenPopoverDelegate = popoverDelegate;
[self.hiddenPopoverController dismissPopoverAnimated:YES];
self.hiddenPopoverHidingInProgress = YES;
if (popoverDelegate) {
[popoverDelegate popoverControllerDidDismissPopover:self.hiddenPopoverController];
}
self.hiddenPopoverHidingInProgress = NO;
self.hiddenPopoverController = nil;
}
}
// Show a popover that had previously been hidden
- (void)showPopover:(NSNotification *)notification {
if (self.hiddenPopoverContent) {
UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:self.hiddenPopoverContent];
if ([popover respondsToSelector:@selector(popoverBackgroundViewClass)]) {
[popover setPopoverBackgroundViewClass:[ZingPopoverBackgroundView class]];
}
CGSize popSize = self.hiddenPopoverContentSize;
[popover setPopoverContentSize:popSize animated:YES];
[popover setDelegate:self.hiddenPopoverDelegate];
if (self.hiddenPopoverPresentingButton) {
[popover presentPopoverFromBarButtonItem:self.hiddenPopoverPresentingButton permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
} else {
[popover presentPopoverFromRect:self.hiddenPopoverPresentingRect inView:[self view] permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
}
self.hiddenPopoverController = popover;
}
}
@end
view raw gistfile1.m hosted with ❤ by GitHub

To use it, a view controller should call startTrackingHidePopover and use either of the presentPopover: replacements to initially show a popover. When the popover is finally dismissed, call dismissedPopover, and stopTrackingHidePopover when you leave the view.

  • Part 1: Dynamically injecting video into a UIWebView
  • Part 2: Detecting scrolling to the end of a UIWebView page
  • Part 3: Handling full screen video