App Tour : Part 3 – Handling full screen video

by Andy

This is the third 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.

Back in the first part of this series, I discussed how to dynamically insert a video into a HTML page hosted in a UIWebView control. By utilizing HTML5 video tags, we can offload the actual playback to the UIWebView control, with it providing all of the playback controls and chrome. On an iPhone any HTML5 video will automatically play in full screen, which is just perfect – we are showing video of using the iPhone version of the app, so showing it full screen is exactly what we want.

But on an iPad, the video is constrained by the size of the UIWebView. In my app, the iPad version of the tour is presented in a popover so the video is only 320px wide. Part of the video player chrome includes a full screen button, which does indeed expand the video to full screen, obscuring everything in the main view hierarchy, however the popover remains visible on top of the video because it is not part of the standard view hierarchy. This is definitely not what we want. So our next challenge in the app tour is how to hide the popover while a video is playing full screen and restore that popover when the video is done.

The video playback is actually controlled by the UIWebView object and not a MPMoviePlayerController. This means that you cannot observe any of the standard MPMoviePlayer* movie player notifications (well you can observe them, you just won’t receive any notifications!). However, when the UIWebView control plays video, it generates equivalent UIMoviePlayerController* notifications, and so there are UIMoviePlayerControllerDidEnterFullscreenNotification and UIMoviePlayerControllerDidExitFullscreenNotification (note the lowercase ‘s’ in screen) notifications that we can observe and receive notifications for.

In the tour view controller that is being presented in the popover, I can be told when the user decides to go full screen and react accordingly:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoEnterFullScreen:) name:@"UIMoviePlayerControllerDidEnterFullscreenNotification" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(videoExitFullScreen:) name:@"UIMoviePlayerControllerDidExitFullscreenNotification" object:nil];

Once I receive the notifications, I need to pass on that fact to the view controller that initially presented the popover so that it can dismiss or re-present the popover. I do that through another set of application specific notifications. I use two sets of notifications because the decision to hide/show a popover might come from more scenarios than just full screen video playback. The tour view controller knows it is going to be showing video, so that is what it looks for. But the main view controller has no idea what is going on in the popover’s content, so it just looks for generic instructions to show and hide the popover.

- (void)videoEnterFullScreen:(NSNotification *)notification {
// Need to inform whoever is listening to hide my popover
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_POPOVER_HIDE object:nil];
}
- (void)videoExitFullScreen:(NSNotification *)notification {
// Need to inform whoever is listening to show my popover
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_POPOVER_SHOW object:nil];
}
view raw gistfile1.m hosted with ❤ by GitHub

Once the main view controller receives the NOTIFICATION_POPOVER_HIDE notification, then it can dismiss the popover via something like [self._myPopover dismissPopoverAnimated:YES] and the video can play full screen unencumbered by any overlays.

The next challenge comes about when the video is finished and the video exits full screen. Again, the main view controller receives a notification to show the popover again, which it can do easily, but I would really like to return to the same state that the user last saw in the popover. That is, I want to return to a view potentially several layers deep in a view hierarchy. I can do that if I retain a reference to the content view before dismissing the popover, and using that reference when I present the popover again.

// 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: method is called as a result of receiving NOTIFICATION_POPOVER_HIDE, it retains some useful information about the popover before dismissing it. The showPopover: method uses those retained values and a couple of others that were saved when the popover was initially presented, to show the popover again. Because the popover’s content view was retained, it’s state has been preserved and the user sees the exact same view from before the video playback.

I actually implemented the hidePopover: and showPopover: in a category on UIViewController, but the details of that implementation will be the subject of the fourth part of this series.

  • Part 1: Dynamically injecting video into a UIWebView
  • Part 2: Detecting scrolling to the end of a UIWebView page
  • Part 4: Mimicking instance variables for a category