5

Currently, any app that I make for iPhone/iPad can be mirrored to the Apple TV via AirPlay. However, even in landscape mode, it only takes up the center part of the screen, with black on left and right sides. What all is involved with getting it to AirPlay full-screen, in the way that apps like Real Racing HD have done?

EDIT: Per a suggestion, I have added in all the code I am using, and instead of telling the secondWindow to use same root view controller as normal, I set up a new VC with a different color to see if mechanics were set up right. They are NOT, as it still just does the normal mirroring, even when telling it to use a different VC.

Here is AppDelegate.h

#import <UIKit/UIKit.h>
@class MainView;
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate> {
    UIWindow *window;
    UINavigationController *tabBarController;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *tabBarController;
@property (nonatomic, retain) UIWindow *secondWindow;
@end

And relevant parts of AppDelegate.m

- (void)checkForExistingScreenAndInitializeIfPresent {
    if ([[UIScreen screens] count] > 1) {
        // Get the screen object that represents the external display.
        UIScreen *secondScreen = [[UIScreen screens] objectAtIndex:1];
        // Get the screen's bounds so that you can create a window of the correct size.
        CGRect screenBounds = secondScreen.bounds;

        self.secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
        self.secondWindow.screen = secondScreen;

        // Set up initial content to display...
        NSLog(@"Setting up second screen: %@", secondScreen);
        ViewController *mainView = [[ViewController alloc] init];
        self.secondWindow.rootViewController = mainView;
        [self.secondWindow makeKeyAndVisible];

        // Show the window.
        //        self.secondWindow.hidden = NO;
    }
    NSLog(@"Screen count too low");
}

- (void)setUpScreenConnectionNotificationHandlers {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver:self selector:@selector(handleScreenDidConnectNotification:)
                   name:UIScreenDidConnectNotification object:nil];
    [center addObserver:self selector:@selector(handleScreenDidDisconnectNotification:)
                   name:UIScreenDidDisconnectNotification object:nil];
}

- (void)handleScreenDidConnectNotification:(NSNotification*)aNotification {
    UIScreen *newScreen = [aNotification object];
    CGRect screenBounds = newScreen.bounds;

    if (!self.secondWindow) {
        NSLog(@"Initializing secondWindow/screen in notification");
        self.secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
        self.secondWindow.screen = newScreen;

        // Set the initial UI for the window.
        ViewController *mainView = [[ViewController alloc] init];
        self.secondWindow.rootViewController = mainView;
    } else {
        NSLog(@"Second window already initialized.");
    }
}

- (void)handleScreenDidDisconnectNotification:(NSNotification*)aNotification {
    if (self.secondWindow) {
        // Hide and then delete the window.
        self.secondWindow.hidden = YES;
        self.secondWindow = nil;
    }
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[window setRootViewController:tabBarController];
    [self setUpScreenConnectionNotificationHandlers];
    [self checkForExistingScreenAndInitializeIfPresent];
return YES;
}
user717452
  • 33
  • 14
  • 73
  • 149
  • The OS must think your app is still in portrait mode. – StilesCrisis Jun 02 '15 at 21:08
  • @StilesCrisis no, that's the behavior for any app, regardless of being in landscape, it will show a centered scaled view. I know it is possible to make it go fullscreen on the Apple TV, just not sure how. – user717452 Jun 02 '15 at 21:18

4 Answers4

3

What all is involved with getting it to AirPlay full-screen...

If you want to dive right in, here is where you start learning about Windows and Screens in the iOS world: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/WindowAndScreenGuide/UsingExternalDisplay/UsingExternalDisplay.html

If you want to take a broader overview first, here is the aggregation page with videos and other tutorials/documentation:

https://developer.apple.com/airplay/

[EDIT] Here's an excerpt of code from an app of mine where I'm setting up the second display. I've taken much of this from the links I gave you. NOTE: "main" is the for the device and "remote" is for the TV.

NOTE: This code is not production-complete; there are still state changes it does not respond to. Connect to an AirPlay Receiver and turn on Mirroring before running this.

I have this:

@interface AppDelegate () {
    SFCManagerMainViewController *_mainVC;
    SFCRemoteMonitorViewController *_remoteVC;
}

@end

The header:

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UIWindow *secondWindow;

and this:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

[self setUpScreenConnectionNotificationHandlers];
[self checkForExistingScreenAndInitializeIfPresent];

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
_mainVC = [[SFCManagerMainViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = _mainVC;
[self.window makeKeyAndVisible];
return YES;
}

- (void)checkForExistingScreenAndInitializeIfPresent {
    if ([[UIScreen screens] count] > 1) {
        // Get the screen object that represents the external display.
        UIScreen *secondScreen = [[UIScreen screens] objectAtIndex:1];
        // Get the screen's bounds so that you can create a window of the correct size.
        CGRect screenBounds = secondScreen.bounds;

        self.secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
        self.secondWindow.screen = secondScreen;

        // Set up initial content to display...
        NSLog(@"Setting up second screen: %@", secondScreen);
        _remoteVC = [[SFCRemoteMonitorViewController alloc] initWithNibName:nil bundle:nil];
        self.secondWindow.rootViewController = _remoteVC;
        [self.secondWindow makeKeyAndVisible];

        // Show the window.
        self.secondWindow.hidden = NO;
    }
}

- (void)setUpScreenConnectionNotificationHandlers {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver:self selector:@selector(handleScreenDidConnectNotification:)
                   name:UIScreenDidConnectNotification object:nil];
    [center addObserver:self selector:@selector(handleScreenDidDisconnectNotification:)
                   name:UIScreenDidDisconnectNotification object:nil];
}

- (void)handleScreenDidConnectNotification:(NSNotification*)aNotification {
    UIScreen *newScreen = [aNotification object];
    CGRect screenBounds = newScreen.bounds;

    if (!self.secondWindow) {
        NSLog(@"Initializing secondWindow/screen in notification");
        self.secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
        self.secondWindow.screen = newScreen;

        // Set the initial UI for the window.
        _remoteVC = [[SFCRemoteMonitorViewController alloc] initWithNibName:nil bundle:nil];
        self.secondWindow.rootViewController = _remoteVC;
    } else {
        NSLog(@"Second window already initialized.");
    }
}

- (void)handleScreenDidDisconnectNotification:(NSNotification*)aNotification {
    if (self.secondWindow) {
        // Hide and then delete the window.
        self.secondWindow.hidden = YES;
        self.secondWindow = nil;
    }
}
Brad Brighton
  • 2,179
  • 1
  • 13
  • 15
  • I followed instructions from the first link exactly, and got a blank screen on my Apple TV. – user717452 Jun 21 '15 at 21:34
  • That's part of what I would expect. In the first link, you're getting an additional screen (akin to a second monitor, not a duplication of the existing monitor) which is where you would put the content that can be resized to the full width/height. If your interface is using size classes/autolayout, you should be able to add your view controller to the second screen and see more of what you're expecting. – Brad Brighton Jun 21 '15 at 21:37
  • Alright, so following Apple's instructions, and adding in EXACTLY the code they have listed in the link, how do I then DUPLICATE what is on my iPad screen to the second screen? – user717452 Jun 21 '15 at 21:41
  • Take a look at the answer edit I made to include code. – Brad Brighton Jun 21 '15 at 21:44
  • Ok, so if I wanted to mirror the display exactly, and my main screen in `didFinishLaunching` looked like `[window setRootViewController:tabBarController];` then shouldn't I be able to make the other code `[self.secondWindow.rootViewController = tabBarController];`? I tried this, and got the blank screen on Apple TV – user717452 Jun 21 '15 at 21:50
  • I do get `2015-06-21 16:57:18.741 PriceIsRightVBS[1692:444063] Initializing secondWindow/screen in notification ` in my Console when I turn on AirPlay mirroring, but the screen stays same, with bars on left and right. – user717452 Jun 21 '15 at 21:53
  • As a general statement, I would say yes, you can do that. As a specific statement, I think it's probably time for you to edit your question to include actual code of how you're implementing so that folks can possibly spot omissions, etc. Another thing I would highly encourage is for you to take this code of mine (or similar samples), put a *different* VC (even if it's just a colored background) onto the TV to make sure the mechanics are correct, then focus on duplicating the content. – Brad Brighton Jun 21 '15 at 21:55
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/81136/discussion-between-user717452-and-brad-brighton). – user717452 Jun 21 '15 at 22:07
1

You just need to set the overscan compensation:

UIScreen* secondScreen = (...);
secondScreen.overscanCompensation = UIScreenOverscanCompensationInsetApplicationFrame;

If you want full code to test it, just put the following code somewhere in your AppDelegate.m and on application:didFinishLaunchingWithOptions: call [self setupAirplay];

-(void)setupAirplay{
    // self.windows is a NSMutableArray property on AppDelegate
    self.windows = [[NSMutableArray alloc] init];

    NSArray* screens = [UIScreen screens];
    for (UIScreen *_screen in screens){
        if (_screen == [UIScreen mainScreen])
            continue;

        NSNotification* notification = [[NSNotification alloc] initWithName:UIScreenDidConnectNotification object:_screen userInfo:nil];
        [self screenDidConnect:notification];
    }

    // Register for notifications
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(screenDidConnect:)
                                                 name:UIScreenDidConnectNotification
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(screenDidDisconnect:)
                                                 name:UIScreenDidDisconnectNotification
                                               object:nil];
}

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

    // Do we already have a window for this screen?
    for (UIWindow *window in self.windows){
        if (window.screen == screen){
            aWindow = window;
        }
    }
    // Still nil? Create a new one.
    if (aWindow == nil){
        aWindow = [[UIWindow alloc] initWithFrame:[screen bounds]];
        [aWindow setScreen:screen];
        [self.windows addObject:aWindow];
    }

    return aWindow;
}

- (void) screenDidConnect:(NSNotification *) notification {

    UIScreen* screen = [notification object];
    NSLog(@"Screen connected: %.0f x %.0f", screen.bounds.size.width, screen.bounds.size.height);

    // Create a view controller for the new window (you should use a Storyboard / XIB)
    UIViewController* airplay = [[UIViewController alloc] init];
    airplay.view = [[UIView alloc] initWithFrame:screen.bounds];
    [airplay.view setBackgroundColor:[UIColor whiteColor]];

    UILabel* label = [[UILabel alloc] initWithFrame:screen.bounds];
    [label setTextAlignment:NSTextAlignmentCenter];
    [label setFont:[UIFont systemFontOfSize:40.0f]];
    [label setText:@"AirPlay Screen"];

    [airplay.view addSubview:label];

    // Comment this line and you'll get the four black borders:
    screen.overscanCompensation = UIScreenOverscanCompensationInsetApplicationFrame;

    UIWindow* aWindow = [self createWindowForScreen:screen];

    // Add the view controller to the window
    [aWindow setRootViewController:airplay];
    [aWindow setHidden:NO];
}

- (void) screenDidDisconnect:(NSNotification *) notification {

    NSLog(@"Screen disconnected");
    UIScreen* screen = [notification object];

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

I didn't get 1080p resolution in my Apple TV 3rd generation while testing with my iPad Mini 2, but I got 720p scaled up to full screen. I'm not sure if that's what's expected by the whole AirPlay concept or if something here isn't fast enough, but I wish I could get 1080p.

Let me know if you need any further help with setting AirPlay up, but it should work as is.

B.R.W.
  • 1,566
  • 9
  • 15
  • This works for adding a different view to show on the AirPlay device, but I simply want to mirror what is on my app on the Apple TV. – user717452 Jun 21 '15 at 20:54
  • If I add no code whatsoever, it mirrors to the Apple TV but with bars on left and right side. If I add code, I get a blank screen. – user717452 Jun 21 '15 at 21:33
  • Oh, I see. Have you tried to set the overscan compensation on the main screen then? (I'm not at home now, so I can't check that) – B.R.W. Jun 22 '15 at 01:44
0

Firstly, is it possible that the TV itself is overriding some overscan settings?

There are a few APIs for UIScreen that you may want to check out, specifically:

@property(nonatomic) UIScreenOverscanCompensation overscanCompensation 
@property(nonatomic,retain) UIScreenMode *currentMode
@property(nonatomic,readonly,copy) NSArray *availableModes

Otherwise the other option is to disable mirroring and do your drawing into a view that is the size of the screen:

//Be sure to check for the screen's existence first :)
UIScreen *secondScreen = [UIScreen screens][1];

//Create a window that is the size of the second screen
self.externalWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, secondScreen.bounds.size.width, secondScreen.bounds.size.height)];

//Set to a view controller that should be displayed
self.externalWindow.rootViewController = [[MyViewController alloc] init]; 

//Move the new window to the second screen
self.externalWindow.screen = secondScreen;
self.externalWindow.hidden = NO;
Max Chuquimia
  • 7,494
  • 2
  • 40
  • 59
  • 1
    If I add no code whatsoever, it mirrors to the Apple TV but with bars on left and right side. If I add code, I get a blank screen. – user717452 Jun 21 '15 at 21:33
0

This works for me on both old and the new Apple TV with an iPad Air. The second window is displayed full screen on the TV while your normal UI is displayed on the iPad.

Insert in your AppDelegate.m:

@property UIWindow *secondWindow;

...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  if ([UIScreen screens].count > 1) {
    [self setUpSecondWindowForScreen:[UIScreen screens][1]];
  }

  NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  [center addObserver:self selector:@selector(handleScreenDidConnectNotification:)
                 name:UIScreenDidConnectNotification object:nil];
  [center addObserver:self selector:@selector(handleScreenDidDisconnectNotification:)
                 name:UIScreenDidDisconnectNotification object:nil];

  return YES;
}

- (void)handleScreenDidConnectNotification:(NSNotification*)notification {
  if (!self.secondWindow) {
    [self setUpSecondWindowForScreen:[notification object]];
  }
}

- (void)handleScreenDidDisconnectNotification:(NSNotification*)notification {
  if (self.secondWindow) {
    self.secondWindow = nil;
  }
}

- (void)setUpSecondWindowForScreen:(UIScreen*)screen {
  self.secondWindow = [[UIWindow alloc] init];
  self.secondWindow.screen = screen;
  self.secondWindow.screen.overscanCompensation = UIScreenOverscanCompensationNone;

  UIViewController *viewController = [[UIViewController alloc] init];
  viewController.view.backgroundColor = [UIColor redColor];
  self.secondWindow.rootViewController = viewController;

  [self.secondWindow makeKeyAndVisible];
}

Code inspired on: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/WindowAndScreenGuide/UsingExternalDisplay/UsingExternalDisplay.html

Tomasz
  • 3,476
  • 1
  • 22
  • 13