top of page

Developing for Apple TV

We recently debuted our newest app, Houzz for Apple TV®, bringing home design to the living room. In these blog posts I will share some of the lessons we learned during the development process.

The Big Picture Developing an app for Apple TV is quite different from developing an app for iPhone or iPad, in several ways. To start, an iPhone / iPad user is typically a single user, interacting with the screen from a close distance. The TV experience, on the other hand, is often more communal, with several people sitting together watching the screen, and watching it from a 10-foot distance. This should greatly dictate how you design your app; you want to have a bigger screen presence, and short texts. In general, we found the screen “smaller” than that of an iPhone in terms of the number of items we could show at one time. With the TV screen, we had to show larger items, and have a focused item on screen at all times, yet leave room for the focus engine to do its zoom magic (more on the focus engine later).

The user interaction model is also quite different in that you don’t touch the screen as you do on iOS, you manage the focus with the remote, moving from one UI element to another. An element always needs to be in focus on screen to receive the touch events from the remote.

You interact on tvOS with the same gestures you’re familiar with from iOS. Single finger gestures are supported, and the remote supports both a touch and a click. The remote also has an accelerometer and gyro just like an iPhone. The remote has two buttons, which are used to navigate content: the menu button, typically used to navigate back through the view hierarchy; and the play/pause button, used for playing content.

Another difference is that there is no persistent storage on the device. You can write to the caches directory or the NSTemporaryDirectory and expect it to be available while your app is in the foreground, but once moved to the background, the system may purge it and there is no guarantee it will be there on your next app session. Instead, if you need persistent storage, you will need to use iCloud storage. You can also use NSUserDefaults for persistence of key/values, but note NSUserDefaults storage is limited to 500 KB max. iCloud key/value storage is limited to 1 MB max, otherwise you would need to use CloudKit or you can always store things on your own server.

This brings me to another mindset change: the Apple TV is an always-connected device. Wifi is likely to be available, and at a good connection speed, the user will want to stream movies.

Another consideration, which didn’t impact us at Houzz since our bundle size is small, is that the app bundle on Apple TV is limited to 200 MB and you have to use iOS 9 on demand resources if you need more.

tvOS doesn’t support UIWebView or its friends, SFSafariWebViewController and WKWebView. No workaround is available – you just don’t do web content in a native app. You have the option of doing a TVML app, but we didn’t take that route. And at this point, it’s important to mention that the Javascript/ObjC bridge, Core Javascript, is also not available on tvOS.

We also had to strip everything related to social from our TV app. The social frameworks are not available on the Apple TV, including Twitter, MessageUI and the Accounts frameworks, as well as Facebook.

Architecture One of the issues we faced was how to setup our code and project architecture to share code between our iOS app and tvOS app. It was clear most of the UI would have to be re-written for  Apple TV, but we could reuse a lot of our model and communication infrastructure. What helped us to get things rolling quickly was that our code was already structured into a Core Houzz framework that included all of the communication with our server and our data model since we support app extensions on iOS 8. It was a simple matter of recompiling that framework for tvOS. The first thing we encountered is that Xcode doesn’t let you link a framework compiled with iOS to a tvOS app, and that there is no way to have one configuration compiled with the iOS SDK, and one with the tvOS SDK. We ended up duplicating our CoreHouzz framework target to a CoreHouzzTV target that was compiled using the tvOS SDK.

If your project is built similarly, it should be easy to create a framework for the tvOS. Again we encountered some iOS/tvOS differences we had to reconcile. In the code, we used a lot of the pre-compile predicate:

   #if TARGET_OS_TV
   #endif

libsqlite is not available on tvOS, so we had to #if it out. It was used for some persistent storage, which we were able to do without, with some minimal iCloud usage. The UIInterfaceOrientation enum is not defined for tvOS (obviously since no one turns their TV on its side!), though I wish Apple would have left that and given it a value of landscape. All of the rest of the status bar orientation related methods are also unavailable on tvOS.

Once we had our core framework compiled for tvOS, the rest of the work entailed creating the view controllers and views for our TV app. We did share some very basic views between our iOS and tvOS – views like showing a gradient, loading an image from a URL, displaying a rating, etc.

Get Focused As I’ve mentioned, a big part of the development pertains to managing the focus, as there is always one focused element on screen. You can find out the focused view by:

    UIScreen.mainScreen().focusedView

You can choose the initial focus and the focus engine will move the focus from item to item on the screen based on the user touch input on the remote. You can be notified of these changes. The key is a new protocol, UIFocusEnvironment, that is conformed by UIView and UIViewController. This protocol lets these objects manage the focus in the view hierarchy they control.

Only UIViews and subclasses of UIView can become focused.

    override func canBecomeFocused() -> Bool {
      return true
    }

To indicate a view can get the focus, override the method canBecomeFocused:

To set the initial focus, the focus engine traverses the responder chain, starting from the UIWindow, and asks for the preferredFocusedView, so you need to override it as such:

    override var preferredFocusedView: UIView? {
    
       return theViewToFocusFirst
    }

This, of course, can be done in either UIView subclasses or UIViewController. In the case of UICollectionViewDelegate or UITableViewDelegate there is a different method you can use:


    func tableView(tableView: UITableView, 
                   canFocusItemAtIndexPath ip: NSIndexPath) -> Bool
    func indexPathForPreferredFocusedViewInTableView(tableView: UITableView)  -> NSIndexPath?

    func collectionView(collectionView: UICollectionView, 
    canFocusItemAtIndexPath indexPath: NSIndexPath) -> Bool
    func indexPathForPreferredFocusedViewInCollectionView(collectionView: UICollectionView) -> NSIndexPath?

To get notified of focus changes, you implement the didUpdateFocusInContext function from the UIFocusEnvironment protocol:


func didUpdateFocusInContext(context: UIFocusUpdateContext,
      withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)

The focus update context is an object that tells you the current focused view and the previous focused view. The UICollectionViewDelegate and UITableViewDelegate provide index path based variations of these functions:


   func collectionView(collectionView: UICollectionView, 
        didUpdateFocusInContext context: UICollectionViewFocusUpdateContext,            
        withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)

The UICollectionViewFocusUpdateContext is the same as the focus context object, except it provides index paths for the views.

Finally, each UIView has a focused property you can observe using KVO to be notified when the view focus state changes.

If you need to update the focus programmatically, you can use setNeedsFocusUpdate() and updateFocusIfNeeded(). Any object that controls the currently focused view, i.e. a parent view or a controlling UIViewController, can ask to update the focus. The focus engine then asks the responder chain for their preferredFocusedView as in the initial focus set. UIKit will automatically ask to update the focus on certain events – the focused view is removed from the view hierarchy, a view controller is presented or dismissed, etc.

One case where we needed to update the focus programmatically demonstrates the difference in managing the focus on an Apple TV versus managing user interaction on an iPhone. The scenario was that we wanted to present some information to the user and have them be able to proceed on a tap anywhere. We proceeded like we would on iPhone: we added a clear view on top of our UIViewController’s root view, added a UITapGestureRecognizer to it, and expected it to call us when it detected a tap. The problem was that taps continued to be detected in the views that were underneath our clear view. When we added our clear view, the focus environment didn’t change and the focused view remained the same view that was there before we added it to the view hierarchy. The solution was our view controller called setNeedsFocusUpdate() after adding our clear overlay view and returned it in its preferredFocusedView. Now the new view was the focused view (it had to also override canBecomeFocused on that view) and it received the touch events.

Summary We’ve covered in this post the main differences between iOS and tvOS and what you need to consider when developing for tvOS. Apart from the technical challenges in setting up your Xcode project for both iOS and tvOS, the main difference is in the focus-based navigation and in designing for a 10-foot viewing experience. In the next post we’ll cover some more programming challenges and examples.

9 views0 comments

Recent Posts

See All

Scaling Data Science

Being the first Data Scientist (DS) at a startup is exciting, yet comes with a myriad of challenges from navigating data infrastructure and data engineering staffing to balancing proper modeling again

bottom of page