tvOS: How to get TVApplicationController to play nice with native view controllers?

1.4k views Asked by At

I'm trying to build a tvOS app that contains both native and TVML content. Ideally, I'd like to present TVML content inside a container view; failing that, I'd be happy if I could present a native UIViewController modally over the TVML content. I have so far failed in both approaches.

The closest I've got so far is presenting modally, but my problem is that the modal view controller always appears blurred, behind the TVML content. The code for this is below (it was based on a snippet from Ray Wenderlich's site):-

// URL and context
NSString *baseURL = @"http://localhost:9001/";
NSString *bootURL = [NSString stringWithFormat:@"%@js/application.js", baseURL];
TVApplicationControllerContext *context = [[TVApplicationControllerContext alloc] init];
context.javaScriptApplicationURL = [NSURL URLWithString:bootURL];
context.launchOptions = @{@"BASEURL": baseURL};

// only seems to work with a full-screen window
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

// instantiate TVApplicationController
self.tvAppController = [[TVApplicationController alloc] initWithContext:context window:window delegate:self];

// get a simple test view controller from storyboard and present modally
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"testVC"];
[self.tvAppController.navigationController pushViewController:controller animated:YES];

There are a couple of things I don't understand here.

Firstly, I need to allocate a brand new, full-size window for this to work. Trying to use the existing self.view.window, or trying to use a smaller window both fail, i.e. no TVML is shown. So I'm assuming the new window is getting added into the view hierarchy at a pretty high level and getting in front of everything else? This seems pretty limited - am I missing something?

Secondly, Apple's docs suggest that the window can be omitted to give more control over how the TVML view will appear:-

If no window is provided, the navigation controller can be presented and dismissed manually within the binary app.

However, I can make little practical sense from that statement. If I set the window parameter to nil, the TVML content simply does not appear. How do they want me to "present" the read-only navigation controller? (I've tried embedding it in a container view but I get exceptions.)

I've tried just about everything I can think of here. Has anyone managed to get mixed TVML and tvOS native views working at the same time?

2

There are 2 answers

2
Alex Ozun On

When you pass self.window to [TVApplicationController alloc] initWithContext:context window:self.window delegate:self], TVApplicationController automatically sets it's navigationController as a rootViewController of provided self.window. This means that self.tvAppController.navigationController will be the initial controller of your app. But you are free to push controllers instantiated inside your xcode app to self.tvAppController.navigationController at any moment. However, the reason why your code presents TVApplicationController above UIViewController is because it takes more time for TVApplicationController to init, then it does for UIViewController, which results in an asynch problem. You can verify this with a simple dispatch_after. If you want a UIViewController to be an initial controller of your app, you will have to pass nil instead of self.window to initWithContext:window:delegate: and then present a TVApplicationController with [self.window.rootViewController presentViewController:self.appController.navigationController animated:YES completion:nil]; Of course, at this point, instead of presenting TVApplicationController modally you can do the same thing as initWithContext:window:delegate: does, which is [self.window setRootViewController:self.appController.navigationController]; and proceed pushing UIViewControllers to self.appController.navigationController, but it seems less flexible to me. Consider this example:

...
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
[self.window makeKeyAndVisible];

self.appController = [[TVApplicationController alloc] initWithContext:appControllerContext window:nil delegate:self];

UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"testVC"];

[self.window setRootViewController:viewController];
//Note that you don't have to init window and UIViewController if you have your storyboard set as Main in info.plist and the storyboard has an initial controller selected. In this case, you'll just need the following code:

[self.window.rootViewController presentViewController:self.appController.navigationController animated:YES completion:nil];
4
bluedome On

If you would like to show TVML over native views, create TVAppController without window and present it just like this.

// your viewController

- (IBAction)buttonAction:(id)sender {
    TVApplicationControllerContext *context = ...;
    self.tvAppController = [[TVApplicationController alloc] initWithContext:context window:nil delegate:self];

    [self presentViewController:self.tvAppController.navigationController animated:YES completion:nil];
}