SwiftUI on tvOS
Build your own tvOS app while brushing up your SwiftUI skills. Get hands-on practice with tvOS lazy views and the Focus Engine. By Jordan Osterberg.
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
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
Lazy Views
Sometimes, your app needs to display a lot of data. Imagine you have thousands of pictures to display, for example. Your app would perform poorly if you tried to load all those photos into views at once.
Build and run. Navigate to the Lots of Videos tab and try to scroll down.
Notice that the simulator becomes very slow, if not unresponsive. That’s because the video list isn’t lazy.
A lazy view is a view that only loads or renders right as it is about to display. This improves performance because your app doesn’t need to render thousands of views at once. It just renders the ones that are currently displayed. Fortunately, some of the key views used in apps, like Stacks and Grids, have lazy counterparts.
Open CategoryListView.swift. In the body
, scroll to right below else
in body
. Replace the VStack
line with the following:
LazyVStack(alignment: .leading) {}
Build and run.
Navigate to the Lots of Videos tab. You’ll notice a significant performance improvement. Fantastic!
Using the Focus Engine
The Focus Engine is tvOS’s tool to show the user what’s highlighted, or focused, on the screen. Generally speaking, this means the content grows slightly and appears to float away from the background.
Users will expect your app to follow this behavior. In the starter project, you may have noticed that there is no focus behavior for any of the videos.
Open CategoryRow.swift and look for the NavigationLink
view. NavigationLink
is a button that brings the user to a new view in your app.
In this case, the link opens the selected video’s VideoDetailView
.
Normally, using a NavigationLink
in tvOS provides a default button style. RickTV disables this, because it doesn’t look great for the video browser’s use case.
Check out what SwiftUI provides by default by temporarily removing this line of code:
.buttonStyle(PlainNavigationLinkButtonStyle())
Make sure to add buttonStyle
‘s’ code back into the app before moving on.
buttonStyle
changes the style of the NavigationLink
to a custom style named PlainNavigationLinkButtonStyle
. This custom style doesn’t currently support the Focus Engine out of the box.
Using Environment Values
To add the focus, open PlainNavigationLinkButtonStyle.swift, paying attention to PlainNavigationLinkButton
.
At the top of the struct, above the configuration
property, add this line of code:
@Environment(\.isFocused) var focused: Bool
This is an environment value, which SwiftUI provides to give you information about the current context of a view. There are other environment values, like the current theme (i.e., dark or light mode) of the user’s device, that can modify the state of a view based on that context.
In this case, you’re using isFocused
. This property is true
when tvOS focuses on this view, and false
otherwise.
Next, inside the body
of PlainNavigationLinkButton
, add scaleEffect
to configuration.label
:
.scaleEffect(focused ? 1.1 : 1)
This ensures that the button — the video, in this case — will grow when the user focuses on it.
Finally, after adding scaleEffect
, add focusable
to configuration.label
:
.focusable(true)
This informs tvOS that the user can focus on PlainNavigationLinkButton
.
Build and run, then scroll down to a video.
Everything is in focus!
Reusing Views
Inside the app, select a video.
This page is good — but wouldn’t it be awesome if you included recommended videos on the detail page?
One of the most powerful SwiftUI capabilities is the option to easily reuse the views you create in multiple places. For example, you can just embed a CategoryRow
inside the video detail page to recommend other videos from the same category. You’ll implement this now.
Open VideoDetailView.swift. Below the Stack
that contains the Play and Favorite buttons, add this code right after .padding(.bottom, 50)
:
if !categoryWithCurrentVideoRemoved.videos.isEmpty {
}
You can use if
statements inside SwiftUI views. This one checks to see if there are any videos other than the currently displayed one inside the current video’s category.
If there are other videos to display, you show a CategoryRow
with that category and a custom title named Related Videos, add this inside the if
:
CategoryRow(
category: categoryWithCurrentVideoRemoved,
customTitle: "Related Videos")
Build and run, then click a video that has more than one video in the same category.
Notice the Related Videos section at the bottom. Scroll down and click that video, and a new VideoDetailView
opens, again with the CategoryRow
at the bottom containing the other videos in the category. Bravo! :]
Playing Videos
Now that users have multiple ways to find videos, it’s time to actually play the video they’re looking for.
Open PlayerView.swift. Notice it’s essentially an empty view except for the text that says Video Player.
SwiftUI has a built-in VideoPlayer
view, but it needs some changes to work with your app. First, add these two properties to PlayerView
:
@State private var player: AVQueuePlayer?
@State private var videoLooper: AVPlayerLooper?
These properties come from AVKit
, which is the Apple framework that allows developers to play video.
Next, replace the Text
view inside the body
with VideoPlayer
:
VideoPlayer(player: player)
Build and run. Navigate to the player view by selecting a video and pressing Play:
This isn’t quite right. Where’s the video?
A few things are happening here:
-
player
is nil, which means there isn’t a source to play from. - You haven’t told the
player
which video to play. - You haven’t started playing the video when the
PlayerView
appears on screen.
To fix this, while in PlayerView.swift, add the onAppear
modifier to the VideoPlayer
you added above:
.onAppear {
}
This block is executed every time the view appears on screen.
Inside .onAppear
, add the following:
// 1
if player == nil {
// 2
let templateItem = AVPlayerItem(
url: Bundle.main.url(forResource: "rick", withExtension: "mp4")!)
// 3
player = AVQueuePlayer(playerItem: templateItem)
// 4
videoLooper = AVPlayerLooper(player: player!, templateItem: templateItem)
}
Here’s what’s going on in the code above:
- The code checks if the player has been created.
- If there’s no player, it creates a new player item that using
AVPlayerItem
This item references the actual video you wish to play. - Next, it must create a queue of items to play.
- Finally, it creates
AVPlayerLooper
, which will handle looping your video when it ends.
Outside of this if
statement, you need to play the video if it hasn’t started playing:
if player?.isPlaying == false { player?.play() }
Build and run, then play a video:
Great! Everything works… but why is the video squished like that?
That’s because of the safe area. The safe area ensures that your content stays readable, no matter which device — the TV screen, in this case — it’s running on.
The video player should ignore the safe area and let the video play in full screen. To implement this, add edgesIgnoringSafeArea
to the end of the VideoPlayer
view:
.edgesIgnoringSafeArea(.all)
Build and run and view a video:
Looks great!