External Screen Tutorial

External Screen Tutorial

We all heard about the games that use an TV that connects both players, or applications for DJ's that show something else on the big screen via an external cable (VGA/HDMI).
I'm gonna show you in short how to accomplish this, let's say you want to make a Presentation application, so you have the full screen presentation on screen and on your iphone/ipad you can see some extra things like timers, short notes etc.

Basically, what I did for this tutorial was creating an SecondScreenManager, which contains all of the property's and extensions for creating and maintaining this second screen.

Next you need the manager to listen to some UINotifications that your OS will fire off within your application. You will need to listen to the next NSNotifications:

NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(screenDidConnect:) name:UIScreenDidConnectNotification object:nil]; [center addObserver:self selector:@selector(screenDidDisconnect:) name:UIScreenDidDisconnectNotification object:nil]; [center addObserver:self selector:@selector(screenModeDidChange:) name:UIScreenModeDidChangeNotification object:nil];

Now our code is listening to these notifications we can define in our manager. First of all, I'd run a setup when initing this class like:

@property (nonatomic, strong) NSMutableArray *windows;

- (void) setup {
    UIWindow *localWindow = nil;
    NSArray *screens = nil;

    self.windows = [[NSMutableArray alloc] init];

    screens = [UIScreen screens];
    NSLog(@"screens: %@", screens);
    for (int i = 0; i < [[UIScreen screens] count]; i++){
        UIScreen *_screen = [[UIScreen screens] objectAtIndex:i];
        if (i >= 1) {
            SetFullscreenViewController *_viewController = nil;

            _viewController = [[SetFullscreenViewController alloc] init];

            localWindow = [self createWindowForScreen:_screen];
            [self addViewController:_viewController toWindow:localWindow];

            _viewController = nil;

            // If you don't do this here, you will get the "Applications are expected to have a root view controller" message.
            if (_screen == [UIScreen mainScreen]){
                [localWindow makeKeyAndVisible];
            }
        }

    }
}

#pragma mark -
#pragma mark Private methods

- (UIWindow *) createWindowForScreen:(UIScreen *)screen {
    UIWindow *localWindow    = nil;

    // Do we already have a window for this screen?
    for (UIWindow *window in self.windows){
        if (window.screen == screen){
            localWindow = window;
        }
    }

    // Still nil? Create a new one.
    if (localWindow == nil){
        localWindow = [[UIWindow alloc] initWithFrame:[screen bounds]];
        [localWindow setScreen:screen];
        [self.windows addObject:localWindow];
    }

    return localWindow;
}

- (void) addViewController:(UIViewController *)controller toWindow:(UIWindow *)window {
    [window setRootViewController:controller];
    [window setHidden:NO];
}

- (void) screenDidConnect:(NSNotification *) notification {
    UIScreen *screen = nil;
    UIWindow *localWindow = nil;
    SetFullscreenViewController *_viewController    = nil;

    UIAlertView *valert = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"Screen connected" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
    [valert show];
    screen = [notification object];

    // Get a window for it
    localWindow = [self createWindowForScreen:screen];

    // Add the view controller to it
    // This view controller does not do anything special, just presents a view that tells us
    // what screen we're on
    [self addViewController:_viewController toWindow:localWindow];
}

- (void) screenDidDisconnect:(NSNotification *) notification {
    UIScreen *_screen = nil;

    UIAlertView *valert = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"Screen disconnected" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
    [valert show];
    _screen = [notification object];

    // Find any window attached to this screen, remove it from our window list, and release it.
    for (UIWindow *loopWindow in self.windows){
        if (loopWindow.screen == _screen){
            NSUInteger windowIndex = [self.windows indexOfObject:loopWindow];
            [self.windows removeObjectAtIndex:windowIndex];
        }
    }
    return;
}

So basically what we have now, is our manager listening to the notifications, if they connect, a screenDidConnect & screenDidDisconnect are fired. In the screenDidConnect a SetFullscreenViewController is created (a ViewController we made which will show something else then on our iPad).
A new window is created and it will take the bounds of the screen, because some TV's/projectors have a different resolution.
Then add the new controller to the window & for each screen a new window + controller will be created.
Once that is done, you can start updating it from another view controller, let's say you start a new presentation, you can call "updateOutput" on the layer manager, add this method in the header file so it's available via the shared manager like so.

- (void)updateOutput:(UIImage*)theImage {
    NSLog(@"updating output");
    for (int i = 0; i < [self.windows count]; i++) {
        UIWindow *window = [self.windows objectAtIndex:i];
        SetFullscreenViewController *setController = (SetFullscreenViewController*)window.rootViewController;
        [setController updateOutput:theImage];
    }
}

Basically, now you can call

[[SecondScreenManager manager] updateOutput:[UIImage imageNamed:@"blahBIG.png"]];

in any viewController you want, and it will update your fullscreen viewController with that image! (if you made sure your viewController supports this updating).
That's it in short, you can download some example files here. I have made this blog with the help of tutorials I found on my own and experimenting with it. For any questions or extended explanations you can contact me via this blog!