Streaming is available in most browsers,
and in the Developer app.
-
Build SwiftUI apps for tvOS
Add a new dimension to your tvOS app with SwiftUI. We'll show you how to build layouts powered by SwiftUI and customize your interface with custom buttons, provide more functionality in your app with a context menu, check if views are focused, and manage default focus. To get the most out of this session, you should be comfortable with SwiftUI. For a primer, watch “Introducing SwiftUI: Building Your First App” and “SwiftUI On All Devices.”
Resources
- CardButtonStyle
- Human Interface Guidelines: Designing for tvOS
- isFocused
- Learn to Make Apps with SwiftUI
- prefersDefaultFocus(_:in:)
- Supporting Multiple Users in Your tvOS App
- SwiftUI
Related Videos
WWDC20
WWDC19
-
Download
Hello, and welcome to WWDC. Hello, everyone. My name is Tanu Singhal, and I'm an engineer on the tvOS team. Today, we'll talk about building SwiftUI apps for Apple TV. We'll introduce new APIs, we'll discuss best practices, and we'll go over some examples that'll help you create experiences that look and feel familiar to TV users. First, we'll discuss button styles and context menus that are new on tvOS 14.
Next, we'll talk about managing focus in our apps.
And finally, we'll learn to create layouts that look good on the largest screen in the home. There are some button styles that are unique to TV. Let's look at an example. Say we're building an app for streaming music on Apple TV.
Here's a mock-up that we received from our design team.
They'd like us to create these buttons that have a movement effect when we drag on the Siri Remote, just like the one you see under "Albums" here.
To create such buttons in SwiftUI, we can use the new Card-Button-Style.
A Card-Button creates a platter that raises and looks highlighted when it's focused. It also adds directional movement to the button when you drag on the Siri Remote.
To create a Card-Button, you simply add the button style modifier to any button, and set it to Card-Button-Style.
Card-Button-Styles can really enhance the appearance of your buttons. However, there might be times when you don't want the default highlighting and focus effects that we offer with the preexisting button styles. In such a case, you can create your own button style. This adds no existing effects for pressing and focused states. And a custom button style is really easy to configure and customize.
To create your own button style, you first conform to the Button-Style protocol...
then in the makeBody method, you can leverage the configuration to return any view.
Once the button style is set up, it can be added to any button using the button style modifier. Now we've set up the buttons in our app, but we've received another feature request. When we long-press on an album button, we want to present some quick actions, like the ones you see here.
These can be created easily in SwiftUI using context menus. Context menus can be added to any button or view and they're invoked after the long-press gesture.
We can add actions to the context menu using regular buttons.
Here's some code showing how you can create a context menu. We simply add the context menu modifier, and then we can add some buttons within it. As you've seen, it's really easy to use the new button styles and context menus. We think these will look great in your apps.
Next, we'll discuss focus. Focus is the primary way to interact with a TV app. And it's incredibly important to be able to focus on views, as well as determine if a view is focused.
To learn more about focus, let's look at another example from our music app.
Here we have the "Now Playing" screen.
We want to focus on the currently playing song. And for this focused song, we would like to display the name of the artist, as well as the name of the album, along with some music emoji.
For the songs that are not focused, we simply display the name of the artist without the name of the album.
To implement this, first, we need to be able to focus on the Song-View. One way to do this is by using the focusable modifier. The focusable modifier creates a focusable wrapper on top of your existing view.
Note that this modifier is not meant for intrinsically focusable views. So, if you have a button or list, or a UIKit view that manages focus in UIKit, then it's best to not have a focusable modifier on that, because this modifier will add another focusable wrapper on top of your existing view. To manage the focused and non-focused state, in tvOS 13, you would have to use the onFocusChange callback for the focusable modifier.
However, new in tvOS 14, we're introducing the isFocused environment variable.
This lets you check whether or not a view is in focus, even if the view itself is not focusable.
The isFocused environment variable will return true if the nearest focusable ancestor of your view is focused.
So now, we'll look at the code for the Song-View that we saw before.
This just has an image followed by some text labels.
We've refactored the text labels into a Details-View.
Within the Details-View, we can simply use the environment variable for isFocused to check whether or not this view is focused.
So if the Song-View that's the parent of the Details-View is focused, then our isFocused variable would be true.
We'll use this isFocused variable to display either the artist and the album name, along with the emojis, or simply the artist name when the view is not focused.
Notice, in our Song-View, we are using a button so we don't need to use the focusable modifier at all, because the button is focusable already.
Using a button also has the advantage of giving us selection and accessibility for free.
We've added the button style that's our own button style, because we didn't want the default highlighting and focus effect offered by the existing button styles.
Now, at this point, our app is mostly set up, and it's ready to start streaming music.
But, we also want to add a paid version of this app, so users can listen to premium content.
To create this paid version, we are first setting up a sign in screen.
This screen simply has a username and password field followed by a log in button. Notice that the username field is what's focused by default. tvOS will geometrically compute the view that should be focused on load. This is typically the topmost or leading focusable view on the screen.
In this case, it makes sense for the username to be focused. However, there are times when we already know the username and password, and we want to focus on the log in button instead.
To implement this in SwiftUI, we can use the new default focus APIs.
We're introducing the prefersDefaultFocus modifier that allows you to specify the view that prefers to be focused by default. Now, we want to make sure that you don't accidentally change focus for an entire global view hierarchy when you're only working on a small custom view. To support this, we have created the focus-Scope modifier, which allows you to limit default focus preferences just to a specific view. We'll look at the code for the log in screen that we saw. This is simply a VStack with a TextField, a Secure-Field and a button. Now I'll add some focus management code to this.
Don't worry about reading all of it at once. We'll go over it line by line. We have a state variable to figure out if the credentials are filled. Now, we add the prefersDefaultFocus modifier to the username TextField, and the first parameter of this modifier is a Boolean that should be true if this view prefers default focus. In this case, the username prefers default focus when the credentials are not filled. We'll also add the same modifier to the log in button, but this prefers default focus when the credentials are filled. Next, we want to limit our focus preferences just to the VStack that we're working on. To do this, we create a namespace. The namespace is a dynamic property that provides us with a unique ID which can be used to identify any view.
We'll add this namespace to the focus-Scope modifier that we've added to our VStack.
Next, we'll pass the same namespace to the prefersDefaultFocus modifiers as the second parameter. By doing this, we have set up our default focus preferences in a way that they apply only to this VStack. So if focus was meant to be within the VStack, then our username or log in button would be focused depending on whether or not the credentials are filled. However, if focus was supposed to be somewhere else in the view hierarchy, then our modifiers will not impact global focus, which is what we want. In addition to setting default focus, we also need to reset focus sometimes. This can be done using the new reset-Focus environment action. This environment action resets focus back to default and, again, the focus updates stay scoped to the namespace that you provide. So, in the sign in screen example that we saw earlier, let's say we also want to add a clear button, which clears the username and password and resets focus back to the username. To implement that, we'll use the reset-Focus environment action. Then, when the clear button is triggered, we call reset-Focus for the same namespace that we had in our focus-Scope.
Since the credentials have been cleared at this point, our focus will get reset back to the username. In this section, we learned about the isFocus environment variable, as well as the new modifiers and environment actions that can be used to control default focus. We think you'll find these really useful as you create your SwiftUI apps.
Finally, we'll learn to build layouts that are commonly seen on Apple TV. Here's a view that we saw earlier from the music streaming app. You may have seen similar layouts across various Apple TV apps.
This is made up of shelves that scroll horizontally.
To implement this screen, we can use the new Lazy Grids.
Lazy Grids arrange child views in a grid container that scrolls vertically or horizontally.
The grid items can specify properties such as size, alignment, and spacing, that help with building the layout. To learn more about grids, you can check out the following two sessions: In this session, though, we'll learn to leverage a Lazy Grid to build the layout that we just saw.
In our Shelf-View, we have created a Scroll-View that scrolls horizontally.
Within the horizontal Scroll-View, we have added a Lazy-H-Grid.
Now, we can add a lot of items to this Lazy-H-Grid, and they won't all get initialized at once. They'll get loaded as and when they're needed, while we scroll.
For the Lazy Grid, we also set up Grid-Items. Grid-Items can be a flexible or fixed size and we can customize the spacing between Grid-Items.
Within our Lazy Grid, we simply add the content that we want to present in our cells.
Now, our shelf is ready, and we can stack together multiple shelves in a VStack along with a few text labels to create the layout that we saw before.
It's as easy as that to create beautiful Grid layouts on Apple TV. So, to recap, we encourage you to try out the new Card-Button Styles and context menus on tvOS 14. They're really easy to use, and they will look great in your apps.
The new focus APIs can help you manage focus better in your apps, and we think you'll find them really useful.
Finally, we hope you'll leverage Lazy Grids to quickly build layouts for tvOS. We cannot wait to see what you build next with SwiftUI. Thank you, and have an amazing WWDC.
-
-
1:42 - CardButtonStyle
Button(albumLabel, action: playAlbum) .buttonStyle(CardButtonStyle())
-
2:24 - Custom Button Styles
struct MyNewButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(configuration.isPressed ? … : …) // Custom styling } } Button(albumLabel, action: playAlbum) .buttonStyle(MyNewButtonStyle())
-
3:19 - Context Menus
AlbumView() .contextMenu { Button("Add to Favorites", action: addAlbumToFavorites) Button("View Artist", action: viewArtistPage) Button("Discover Similar Albums", action: viewSimilarAlbums) }
-
5:47 - isFocused Environment Variable
struct SongView: View { var body: some View { Button(action: playSong) { VStack { Image(albumArt) DetailsView(...) } }.buttonStyle(MyCustomButtonStyle()) } } struct DetailsView: View { ... @Environment(\.isFocused) var isFocused: Bool var body: some View { VStack { Text(songName) Text(isFocused ? artistAndAlbum : artistName) } } }
-
8:42 - Login Screen (Default Focus)
var body: some View { VStack { TextField("Username", text: $username) SecureField("Password", text: $password) Button("Log In", action: logIn) } }
-
8:51 - Default Focus
@Namespace private var namespace @State private var areCredentialsFilled: Bool var body: some View { VStack { TextField("Username", text: $username) .prefersDefaultFocus(!areCredentialsFilled, in: namespace) SecureField("Password", text: $password) Button("Log In", action: logIn) .prefersDefaultFocus(areCredentialsFilled, in: namespace) } .focusScope(namespace) }
-
11:12 - Reset Focus
@Namespace private var namespace @State private var areCredentialsFilled: Bool @Environment(\.resetFocus) var resetFocus var body: some View { VStack { TextField("Username", text: $username) .prefersDefaultFocus(!areCredentialsFilled, in: namespace) SecureField("Password", text: $password) Button("Log In", action: logIn) .prefersDefaultFocus(areCredentialsFilled, in: namespace) Button("Clear", action: { username = ""; password = "" areCredentialsFilled = false resetFocus(in: namespace) }) } .focusScope(namespace) }
-
12:45 - Lazy Grids
struct ShelfView: View { var body: some View { ScrollView([.horizontal]) { LazyHGrid(rows: [GridItem()]) { ForEach(playlists, id: \.self) { playlist in Button(action: goToPlaylist) { Image(playlist.coverImage) .resizable() .frame(…) } .buttonStyle(CardButtonStyle()) } } } } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.