App Tour : Part 4: Mimicking instance variables for a category
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
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.
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.
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:
The properties allow an easy interface to the data in the other methods in the category, e.g.
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.
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.
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
stopTrackingHidePopover when you leave the view.