tvOS Top Shelf Tutorial: Static and Interactive
In this tvOS Top Shelf tutorial, you’ll learn how to add static and interactive content to the top shelf and give your tvOS app an edge over other apps! By Kelvin Lau.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
tvOS Top Shelf Tutorial: Static and Interactive
20 mins
- Getting Started
- Adding a Static Image
- An Interactive Top Shelf
- Sectioned Top Shelf
- Inset Top Shelf
- Setting Up the Interactive Top Shelf
- TVTopShelfProvider
- Linking Up the Sectioned Layout
- Adding More News Items
- Adding User Interaction
- Event Handling
- Hooking Up the AppDelegate
- Adding the URLs
- Differentiating Between Items
- Where to Go From Here?
Linking Up the Sectioned Layout
In ServiceProvider.swift, make sure the topShelfStyle
variable is returning .sectioned
(it should be returning .sectioned
by default):
var topShelfStyle: TVTopShelfContentStyle {
return .sectioned
}
Modifying the return value for this computed variable lets the app define an inset or sectioned layout.
The topShelfItems
computed variable, as the name implies, will define the UI elements in the top shelf for a given layout.
Modify the topShelfItems
computed variable as follows:
var topShelfItems: [TVContentItem] {
// 1
let breakingNewsIdentifier = TVContentIdentifier(identifier:
"Breaking News", container: nil)!
let breakingNewsSection = TVContentItem(contentIdentifier:
breakingNewsIdentifier)!
breakingNewsSection.title = "Breaking News"
// 2
let martianRiotIdentifier = TVContentIdentifier(identifier:
"Martian Riot", container: nil)!
let martianRiotItem = TVContentItem(contentIdentifier:
martianRiotIdentifier)!
// 3
breakingNewsSection.topShelfItems = [martianRiotItem]
return [breakingNewsSection]
}
Here’s what you are doing above:
- First you define a section where your “Breaking News” items will reside.
- Next you define a single item for your section.
-
Finally you set the
topSelfItems
property of thebreakingNewsSection
to an array that contains the item.
To test your top shelf, you can use the schema generated for you when you created the TV Services Extension. Beside the run/stop button on the top-left corner of Xcode, ensure you have the TopShelf schema selected:
Build and run your app; choose Top Shelf from the “Choose an app to run:” dialog and press Run:
You should see a single cell with a section title above it:
Okay, that takes care of the section title. Now you’ll need an image to go along with it.
Drag sectionedMartians.png from this tutorial’s resources and drop it into your project in the Top Shelf group. Make sure Copy items if needed is checked, and the target is set to TopShelf.
With the asset safely in your project, change to ServiceProvider.swift.
Add the following code just below the line that initializes martianRiotItem
:
let martianURL = Bundle.main.url(forResource:
"sectionedMartians", withExtension: "png")
martianRiotItem.setImageURL(martianURL,
forTraits: .userInterfaceStyleLight)
This tells TVContentItem to look inside your project files to find the image.
Build and run your app; you should see a nice image accompanying your top shelf item in the breaking news category:
Look out, the Martians are rioting! :]
Adding More News Items
A comet in the Milky Way is misbehaving, and an asteroid is traveling at the speed of light, breaking all laws of physics! While these two pieces of news are super-exciting, they don’t quite belong in the “Breaking News” category. You’ll create a new category to house these two pieces of news.
Drag and drop comet.png and asteroid.png from the project resources to the Top Shelf group, making sure Copy items if needed is checked and the target set to TopShelf.
In the topShelfItems computed property, add the following before the return
statement:
// 1
let milkyWayNewsIdentifier = TVContentIdentifier(identifier:
"Milky Way News", container: nil)!
let milkyWaySection = TVContentItem(contentIdentifier:
milkyWayNewsIdentifier)!
milkyWaySection.title = "Milky Way"
// 2
let cometIdentifier = TVContentIdentifier(identifier:
"An odd comet", container: nil)!
let cometItem = TVContentItem(contentIdentifier:
cometIdentifier)!
let cometURL = Bundle.main.url(forResource:
"comet", withExtension: "png")
cometItem.setImageURL(cometURL, forTraits:
.userInterfaceStyleLight)
// 3
let asteroidIdentifier = TVContentIdentifier(identifier:
"Asteroid Light Speed", container: nil)!
let asteroidItem = TVContentItem(contentIdentifier:
asteroidIdentifier)!
let asteroidURL = Bundle.main.url(forResource:
"asteroid", withExtension: "png")
asteroidItem.setImageURL(asteroidURL,
forTraits: .userInterfaceStyleLight)
// 4
milkyWaySection.topShelfItems = [cometItem, asteroidItem]
Take some time to admire the beauty of what you’ve written:
-
You create a new
TVContentItem
intended as a new section; hence, you also give it a title. -
You then create
cometItem
with the intention of adding it tomilkyWaySection
. -
You also create
asteroidItem
to add tomilkyWaySection
as well. -
Finally, you made
cometItem
andasteroidItem
part of themilkyWaySection
by adding them to itstopShelfItems
property.
You’ve created a new section and placed two items inside. Now you need to add this section next to the “Breaking News” section. Update the return
statement to return both the breakingNewsSection
and the milkyWaySection
:
return [breakingNewsSection, milkyWaySection]
Build and run your app; select any top shelf item:
Hmm — nothing happens! Well, the sections look good, but you haven’t yet provided the code to handle events. You’ll do that in the next section.
Adding User Interaction
You have a visually appealing top shelf, so your next step is to make it functional. A top-of-the-line top shelf provides shortcuts to the advertised content, so you’ll need to find a way for the app to recognize that a user has tapped on a given top shelf item. Unfortunately, you can’t use IBAction
event handling here.
So what can you do? How does event handling work for the top shelf?
Event Handling
AppDelegate.swift will call application(_:open:options)
when a user selects anything in the top shelf — provided that the item has a non-nil displayURL
or playURL
. The top shelf can listen to two events on the remote: a press on the touch screen, and a press on the play button.
You’ll start by adding a new value to your app’s Info.plist file. Make sure you’ve opened the plist that’s part of your NewsApp target, not your TopShelf target. Right-click and select Add Row:
Name the row URL types. As you’re typing, Xcode may help you out by offering you an autocomplete. Expand the row you’ve just added and you should see another row named Item 0. Expand that as well. Click the + button next to Item 0 to add another row within the Item 0 dictionary. Name it URL Schemes:
Your new rows should look like this:
Set the value of Item 0 of URL Schemes to newsapp, and URL identifier to NewsApp URL. Your finalized plist entry should look like this:
Hooking Up the AppDelegate
You haven’t provided TVContentItems
with either of these properties yet — that’s your next job.
Adding the URLs
For your news app, you’ll only supply displayURL
. Open ServiceProvider.swift and add the following private method to the bottom of the class:
private func urlFor(identifier: String) -> URL {
var components = URLComponents()
components.scheme = "newsapp"
components.queryItems = [URLQueryItem(name: "identifier",
value: identifier)]
return components.url!
}
You’ll use this private helper method to generate a URL for displayURL
. Your top shelf has three items, so there are three corresponding displayURL
properties you must fill in.
Inside the topShelfItems
computed variable, find where you’ve declared martianRiotItem
. Below that, add the following line:
martianRiotItem.displayURL = urlFor(identifier: "martianRiot")
Similarly, find cometItem
and asteroidItem
and add the following code below the points where you instantiate each of those objects:
cometItem.displayURL = urlFor(identifier: "comet")
asteroidItem.displayURL = urlFor(identifier: "asteroid")
Build and run your app; navigate to your app’s top shelf, select any item, and lo and behold, your app launches!
Right now, your app launches to its initial view controller when any of the top shelf items are selected. What you really want is the items to act as a shortcut to their respective categories. It’s these shortcuts that provide a pleasurable — and convenient — user experience.