Streaming is available in most browsers,
and in the Developer app.
-
Migrate your TVML app to SwiftUI
SwiftUI helps you build great apps on all Apple platforms and is the preferred toolkit for bringing your content into the living room with tvOS 18. Learn how to use SwiftUI to create familiar layouts and controls from TVMLKit, and get tips and best practices.
Chapters
- 0:00 - Introduction
- 3:54 - Lockups
- 5:12 - Shelves
- 7:35 - Catalogs
- 11:07 - Search
Resources
Related Videos
WWDC24
-
DownloadArray
-
-
4:18 - Borderless button lockup
Button {} label: { Image("discovery_landscape") .resizable() .frame(width: 250, height: 375) Text("Borderless Portrait") } .buttonStyle(.borderless)
-
5:38 - Standard content shelf
ScrollView(.horizontal) { LazyHStack(spacing: 40) { ForEach(Asset.allCases) { asset in Button {} label: { asset.portraitImage .resizable() .aspectRatio(250/375, contentMode: .fit) .containerRelativeFrame(.horizontal, count: 6, spacing: 40) Text(asset.title) } } } } .scrollClipDisabled() .buttonStyle(.borderless)
-
8:19 - Card button
ScrollView(.horizontal) { LazyHStack(spacing: 48) { ForEach(Asset.allCases) { asset in Button {} label: { HStack(alignment: .top, spacing: 10) { asset.landscapeImage .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 12)) VStack(alignment: .leading) { Text(asset.title) .font(.body) Text("Subtitle text goes here, limited to two lines") .font(.caption2) .foregroundStyle(.secondary) .lineLimit(2) Spacer(minLength: 0) HStack(spacing: 4) { ForEach(1..<4) { _ in Image(systemName: "ellipsis.rectangle.fill") } } .foregroundStyle(.secondary) } } .padding([.leading, .top, .bottom], 12) .padding(.trailing, 20) .frame(maxWidth: .infinity) } .containerRelativeFrame(.horizontal, count: 3, spacing: 48) } } } .scrollClipDisabled() .buttonStyle(.card)
-
8:39 - Landing page
ScrollView(.vertical) { LazyVStack(alignment: .leading, spacing: 26) { VStack(alignment: .leading) { Text("tvOS with SwiftUI") .font(.largeTitle).bold() Spacer(minLength: 300) HStack { Button("Show") {} Button(“More Info…”) {} Spacer() } .padding(.bottom, 100) Spacer() } .onScrollVisibilityChange { visible in withAnimation { belowFold = !visible } } Section("Movie Shelf") { MovieShelf() } Section("TV and Music Shelf") { TVMusicShelf() } Section("Content Cards") { CardShelf() } } .scrollTargetLayout() } .scrollClipDisabled() .background(alignment: .top) { if !belowFold { Image("beach_landscape") .resizable() .aspectRatio(contentMode: .fill) .ignoresSafeArea() .mask { LinearGradient(stops: [ .init(color: .black, location: 0.0), .init(color: .black, location: 0.45), .init(color: .black.opacity(0), location: 0.8) ], startPoint: .top, endPoint: .bottom) } } } .scrollTargetBehavior(.viewAligned)
-
11:13 - Tab view
TabView { Tab("Stack", systemImage: "line.3.horizontal") { StackView() } // Other Tabs... Tab("Search", systemImage: "magnifyingglass") { SearchView() } }
-
11:50 - Search page
@State var searchTerm: String = "" let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: 40), count: 4) ScrollView(.vertical) { LazyVGrid(columns: columns) { ForEach(sortedMatchingAssets) { asset in Button {} label: { asset.landscapeImage .resizable() .aspectRatio(16 / 9, contentMode: .fit) Text(asset.title) } } } } .scrollClipDisabled() .buttonStyle(.borderless) .searchable(text: $searchTerm) .searchSuggestions { ForEach(suggestedSearchTerms, id: \.self) { suggestion in Text(suggestion) } }
-
14:59 - Sidebar adaptable tab view style
TabView { Tab("Stack", systemImage: "line.3.horizontal") { StackView() } // Other Tabs... Tab("Search", systemImage: "magnifyingglass") { SearchView() } } .tabViewStyle(.sidebarAdaptable)
-
-
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.