UIWindow: When A UIViewController Just Won’t Cut It

When writing Huvi, I stumbled on a technique for when you need an Absolutely-Modal-And-I-Mean-It message to appear. There are a few cases where we need to make sure we know exactly what’s on the top of the view hierarchy, and that the user won’t go messing around with UI underneath.

huvi-internet-connection

One example is when the user doesn’t have an active internet connection. In that case, Huvi can’t do anything useful (it’s a streaming video app, after all), so we basically want to shut the user out of the app and say “go get some internet” when the connection is inactive. The trouble is, we can’t make any assumptions about what the user will be doing when the connection status changes. They could literally be anywhere in our app, and we need to be able to gracefully lock them out, then gracefully let them back in when the connection returns, placing them right where they left off.

Having each View Controller be responsible for this monitoring is one possible solution, but it would be really easy to forget in one instance or another, and end up with a lot of duplicated code. We could use NSNotificationCenter to broadcast connection changes, and that at least puts the responsibility for monitoring the connection in one place, but then we still have a zillion places that are responsible for the UI side of shit. Ideally we would create a listener class singleton, which would monitor the connection, and when the internet went bye-bye, it could force a view to the top, regardless of what the user is doing, and disable any user interaction with the app. And that’s what I did.

UIWindow turned out to be the key to this. Ultimately, when you display a view, you’re adding it to the current view hierarchy somewhere, and the typical Cocoa design pattern dictates you have a View Controller for the current screen, so you just attach it to the appropriate place in that View Controller’s view. But displaying a dialog from a random singleton sitting outside the app’s normal flow would then require you to know something about the current screen’s view hierarchy. Maybe it’s just one view controller managing one view. Or maybe it’s a UINavigationController managing a stack of UIViewControllers. Or maybe some split screen. Or maybe iOS is currently displaying a modal sheet or dialog. You just don’t know, and even if you did, what’s the appropriate way to display a full-screen modal view in each of those cases?It’s messy to even contemplate.

So HVNetworkStatus, which is listening for connection changes creates its own window, sets a HVNetworkStatusOverlay as the root view controller, and then calls makeKeyAndVisible on its own window. As a result, we no longer really have to care what’s going on in the app’s main view hierarchy – it’s all hidden behind our fullscreen view, and we’re gobbling up any user interaction in our own window’s responder chain. Even other modal dialogs in Huvi (say, responding to a timeout from our backend due to our newly disabled network connection), will end up being displayed underneath the window we’ve just presented.

Removing our fullscreen overlay has an additional bit of cleanup, as we want to make sure the user ends up in the same place they were before the connection disappeared. So before we make our window the key window, we grab a reference to the current key window, and restore it on the way out.

The HVNetworkStatus listener has methods for showing and hiding the overlay, and managing its window:

-(void)displayOverlay {
    if (!self.window.isKeyWindow) {
        self.lastKeyWindow = [UIApplication sharedApplication].keyWindow;
        [self.window makeKeyAndVisible];
        [self startRecheckTimer];
        HVNetworkStatusOverlay* overlay = (HVNetworkStatusOverlay*)self.window.rootViewController;
        [overlay show];
    }
}

-(void)dismissOverlay {
    if (self.window.isKeyWindow) {
        HVNetworkStatusOverlay* overlay = (HVNetworkStatusOverlay*)self.window.rootViewController;
        [overlay hide].then(^{
            [self.lastKeyWindow makeKeyAndVisible];
        });
    }
}

And the network status overlay has a hide and a show method as well, to handle the pretty pretty animation:

-(AnyPromise*)show {
    AnyPromise* result = self.showPromise;
    if (!result) {
        result = [UIView promiseWithDuration:kFadeDuration delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
            self.view.alpha = 1.0;
            self.containerView.transform = CGAffineTransformIdentity;
        }].then(^{
            self.showPromise = nil;;
        });
        self.showPromise = result;
    }
    return result;
}

-(AnyPromise*)hide {
    AnyPromise* result = self.hidePromise;
    if (!result) {
        result = [UIView promiseWithDuration:kFadeDuration delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
            self.view.alpha = 0.0;
            self.containerView.transform = CGAffineTransformMakeScale(0.8, 0.8);
        }].then(^{
            self.hidePromise = nil;;
        });
        self.hidePromise = result;
    }
    return result;
}

One additional (potential) pitfall is that we might leave the app in an unusable state if we never hide the overlay. One fear I had when designing the class this way is that we would somehow miss a notification that the network status had changed, so I set up a timer to periodically check the connection status when the overlay is displayed.

Part of designing great software is considering all the edge cases. In this case, using the UIWindow class to make SURE that we know what the user is seeing allowed me to handle a huge swath of potential edge cases with a single, elegant solution.