The top shelf is a content showcase area above the top row of apps on the Apple TV Home screen. The user can decide which apps are in the top row. When one of these apps is brought into focus, the top shelf displays featured content specific to that app.
It’s a unique opportunity for your app to highlight new, featured, or useful content and let the user jump directly to it. For example, when the iTunes TV Shows app is in focus, people see a row of featured shows. Focusing on and clicking a TV show goes right to the related product page in the app. Pressing the Play/Pause button on the remote while a show is in focus begins media playback for the show.
To begin, you’ll already need a tvOS app. Top Shelf is an extension to the tvOS app similar to a Today Widget and Apple Watch app to an iOS app, they are bundled together. You can follow the steps in this article on how to build an tvOS video app: tvOS Tutorial: Make a Video App in Swift. We’ll take this app and add the featured movies as our top shelf.
Static Image
At a minimum, every app must supply a single, static top shelf image that can be displayed when your app is in the top row of the Home screen and in focus. This image needs to be 1920px by 720px.
With the Assets folder, drag your image into the Top Shelf Image location, as seen below.
Sectioned Content Row
This layout style shows a single labeled row of sectioned content. It’s commonly used to highlight recently viewed content, new content, or favorites.
Content in the row is focusable, letting the user scroll through it at the desired speed. A label appears when an individual piece of content comes into focus, and small movements on the remote’s touch surface bring the focused image to life. A sectioned content row can be configured to show multiple labels, if desired.
This layout is templated and cannot be customized any further. It must contain a title, image and the title for the video will appear once the user has put it in focus. When a user taps on it it should deep link directly to the content within your app.
To create a sectioned content row, follow the steps below.
- Click File > New > Target
- Select “Application Extension” under tvOS and choose the “TV Services Extension”, and click “Next”.
- Name the extension, “TopShelf” is good.
- The Bundle Identifier is important. Since this is an extension it should be the same as your parent app. You can only add one more period “.” to the end to make it unique, i.e. com.brianjcoleman.Video-App.TopShelf and click “Finish”.
- A popup will appear, “Activate “TopShelf” scheme?”, you can click “Activate” on this. It will make the TopShelf target active so you can test it within Xcode.
- You’ll notice we now have a new folder in the Project Navigator with the ServiceProvider.swift and Info.plist file.
- To ensure we can access web servers for grabbing content, be sure to go into the Info.plist and add the following keys.
Enter the following code into ServiceProvider.swift
import Foundation import TVServices class ServiceProvider: NSObject, TVTopShelfProvider { override init() { super.init() } var topShelfStyle: TVTopShelfContentStyle { // Return desired Top Shelf style. return .Sectioned } var topShelfItems: [TVContentItem] { // Create an array of TVContentItems. let wrapperID = TVContentIdentifier(identifier: "shelf-wrapper", container: nil)! let wrapperItem = TVContentItem(contentIdentifier: wrapperID)! var ContentItems = [TVContentItem]() for (var i = 0; i < 8; i++) { let identifier = TVContentIdentifier(identifier: "VOD", container: wrapperID)! let contentItem = TVContentItem(contentIdentifier: identifier )! if let url = NSURL(string: "http://www.brianjcoleman.com/code/images/feature-\(i).jpg") { contentItem.imageURL = url contentItem.imageShape = .HDTV contentItem.title = "Movie Title" contentItem.displayURL = NSURL(string: "VideoApp://video/\(i)"); contentItem.playURL = NSURL(string: "VideoApp://video/\(i)"); } ContentItems.append(contentItem) } // Section Details wrapperItem.title = "Featured Movies" wrapperItem.topShelfItems = ContentItems return [wrapperItem] } }
Notice that we are defining the type of content to display in the topShelfStyle method. You can return either .Sectioned to display multiple items in the row on the screen at once (i.e. iTunes Movies) or .Inset which will display one large banner similar to the App Store.
The topShelfItems method should return the array of items, they are of type TVContentItem, which contain the following properties:
- imageURL - A URL giving the location of the image to be displayed for this content item.
- imageShape - The intended aspect ratio or shape of the content image.
Below are the three sizes available for .Sectioned content.
- title - The localized string title of the item.
- displayURL - A URL that causes the app which created this content item to display a description screen for the item. (type is NSURL)
- playURL - A URL that causes the app which created this content item to begin playing the item at the user's current position. (type is NSURL)
Lastly set the returned wrapper TVContentItem title to what you want the section header to be named.
Deep Linking
Top shelf layouts are a path to content that people care about most. Help them get to it quickly. Let users click a focused image to open your app and go right to the related content, or let them use the Play button on the remote to immediately start media playback or enter gameplay.
First setup your URL Scheme within the info.plist of your tvOS parent app.
The deep link URL will now be the URL scheme, plus any parameters you wish to pass through: VideoApp://video/1
You must provide data for both playURL and displayURL. Failing to provide both means that selecting a top shelf item does nothing. As soon as you provide a value for displayURL, selecting a video will immediately start opening your app. Just like iOS and Watch OS apps the first method that will get called in your App Delegate is openURL.
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool { if url.host == nil { return true } let urlString = url.absoluteString let queryArray = urlString.componentsSeparatedByString("/") let query = queryArray[2] if query.rangeOfString("video") != nil { let videoIndexPathRow = queryArray[3] let deepLinkVideoIndexPathRow = videoIndexPathRow if deepLinkVideoIndexPathRow != "" { if let tabBarController = self.window!.rootViewController as? UITabBarController { let svc = tabBarController.viewControllers![0] as! MoviesViewController tabBarController.selectedIndex = 0 svc.setDeepLink(deepLinkVideoIndexPathRow) } } } return true }
The openURL method above reads in the URL passed from the top shelf. It then checks the query string for "video" as the key identifier to let us know that we want to play a video. This is helpful if you have multiple deep links for different features your app. Next we parse the rest of the URL to look for the row number selected so we know what video to play (this can be any unique identifier to let you know what video was selected). Then we direct the app to go to the MoviesViewController and call a new method named setDeepLink where we pass in the unique identifier and play the video.
func setDeepLink(videoIndexPathRow: String) { let playerVC = PlayerViewController() playerVC.playVideo() [self.presentViewController(playerVC, animated: true, completion: nil)] }
You'll notice that I am passing in the videoIndexPathRow but am not actually using it since this sample app doesn't have any actual data feeds, but you should pass something so you will need to key on a unique ID so you know what video to play. As soon as this method is called it starts the video player and plays the selected video.
You can grab the full source code for this tutorial.
Note: Created using Xcode 7.1 GM (tvOS & Swift 2).