Streaming is available in most browsers,
and in the Developer app.
-
The SwiftUI cookbook for focus
The SwiftUI team is back in the coding "kitchen" with powerful tools to shape your app's focus experience. Join us and learn about the staple ingredients that support focus-driven interactions in your app. Discover focus interactions for custom views, find out about key-press handlers for keyboard input, and learn how to support movement and hierarchy with focus sections. We'll also go through some tasty recipes for common focus patterns in your app.
Chapters
- 1:44 - What is focus
- 3:18 - Ingredients
- 3:35 - Ingredients: Focusable views
- 6:04 - Ingredients: Focus state
- 7:03 - Ingredients: Focused values
- 8:54 - Ingredients: Focused sections
- 10:45 - Recipes
- 11:21 - Recipes: Controlling focus
- 14:36 - Recipes: Custom focusable control
- 18:04 - Recipes: Grid view
Resources
Related Videos
WWDC23
-
Download
♪ ♪ Cody: Hi, and welcome to "The SwiftUI cookbook for focus." I'm Cody, and today I'll be discussing some of the things that you can do with focus APIs in SwiftUI to cook up a really great user experience.
In this video, I'll serve up a three-course meal from a fixed menu of tasty API details paired with a series of fine code examples. As an appetizer, I'll spend some time reviewing the very basics of focus: what is it, and what job does it do? For the first course, whet your palate with a look at the ingredients that make the focus experience. With those ingredients laid out, I can really start cooking. For the main course, I'll dig in to some recipes for controlling focus appearance, observing focus movement, and responding to keyboard input with custom controls. So, what is focus? Focus is a tool for deciding how to respond when someone presses a key on a keyboard, swipes on an Apple TV Remote, or turns the Digital Crown on their watch. These input methods have one important detail in common. On their own, they don't provide enough information to identify which on-screen control their input is intended for. Compare this with things like mice, and trackpads, and touch screens. When you use a mouse or trackpad, the on-screen cursor associates your clicks with screen coordinates that the system uses to find the target of the interaction. Focus provides the extra information that the system needs to direct input without a pointer cursor. When a view has focus, the system will use it as a starting point for responding to input from keyboards, Apple TV remotes, and Apple Watch Digital Crowns.
Focus isn't just an implementation detail. It's equally important to the people using your app, which is why the focused view is presented with special emphasis.
macOS automatically adds a border around the focused view to show that it will receive keyboard input. watchOS draws a green border around controls to signal that the control's value can be changed by rotating the Digital Crown. And on tvOS, the focused view receives a hover effect that lifts it above the plane of other controls.
Emphasizing the focus view helps people in a couple of ways. They can anticipate where their input will go when they type on a keyboard or swipe on the remote. And in a complex or detailed layout, it reminds people at a glance which part of the app they're interacting with. Focus behaves a lot like a special kind of cursor. Instead of tracking a point on screen like a mouse cursor does, it tracks which part of your UI is the target for focus input. For this reason, I like to think of focus as a cursor for the user's attention. Now that you know a little about what focus is and how it appears in your app, I can lay out the first course, a look at the basic ingredients that go into every app's focus experience: focusable views, focus state, focused values, and focus sections. The main ingredient to consider when cooking with focus is the focused view itself. This is the view the system uses as its starting point when responding to focus input. Different controls are focusable under different circumstances, and for different reasons. Compare text fields and buttons on macOS and iPadOS. Text fields are always focusable, whether you tap on them or you press the Tab key to move focus from a previous control. This kind of control supports focus for editing, because its role is to capture continuous focus input.
Buttons are different. Their job is to handle clicks and taps. macOS and iPadOS don't give focus to buttons when you tap them, and the only way to reach them with the Tab key is to turn on keyboard navigation system-wide. If you aren't familiar with this setting, you can find it in the Keyboard settings pane of macOS System Settings. It's the switch labeled "Keyboard navigation". After flipping that switch, I can press the Tab key to focus on buttons, and press the Space bar to activate them.
Buttons support focus for activation. These controls don't require focus to do their job, but they'll take focus if the system allows it, in order to support focus-driven alternatives to clicking and tapping. In iOS 17 and macOS Sonoma, there are new APIs for custom controls to participate in the focus system. When you apply the "focusable" view modifier, you can now fine-tune the resulting behavior by specifying the kinds of focus interactions your control supports. For controls that use focus to update state over time, specify edit interactions. For controls that use focus as an alternative to direct pointer activation, specify activate interactions.
If you don't provide any arguments at all, the system gives the control focus for all interactions. Prior to macOS Sonoma, the focusable modifier only supported activation semantics. If you're already using the focusable modifier in your macOS code, verify that the new behavior suits your use case. You may need to update your code by adding an "interactions" argument. The next ingredient relates to the state of the focus system from moment to moment. This ingredient is aptly named "FocusState". The system keeps track of which view has focus, and the app can use that information in its logic to determine how to handle input and how to style views. To observe the state of the system, you create bindings that associate values that you provide with focus being on a particular view. Views can read these bindings to get notified when focus changes, such as a view becoming focused, or when focus is dismissed. Focus state properties with Boolean values will tell you if a single view is focused, as shown here. For more complex cases, you can also use custom data types. Later, I'll discuss an example of this, and show how focus state can be changed programmatically.
Next up, the Focused Values API. The Focused Values API solves the problem of how to build data dependencies that link remote parts of your user interface. Use this API to update your app's commands based on what's happening in the active scene. Focused values enable data flow between these different elements. I'll define a custom one and use it to build my main menu content. Creating and using focused values is similar to creating and using custom environment keys and objects. You define a new key using the "FocusedValueKey" protocol, and then extend "FocusedValues" with a computed property that uses the new key to get and set values. The data you use comes from your scene's views, and can be a value, a binding, or an observable object. In any case, you use a family of view modifiers to associate the data with focus being in that part of the view hierarchy. As with environment values, you access your focused values by declaring a dynamic property. In this example, my focused value is a binding, so I use the "@FocusedBinding" property wrapper and provide it with my custom key path. "@FocusedBinding" looks at the focused view and its ancestors to see if there's currently a binding associated with the key. The property wrapper automatically unwraps the binding so I can work with the bound value directly. The only other thing I need to do is use my new property in the view's body. Over time, as focus moves between different controls and different windows become active, the system will update the view to reflect the values it finds in the new context. The last ingredient is the Focus sections API. Focus sections give you a way to influence how focus moves when someone swipes on an Apple TV Remote or presses the Tab key on a keyboard. By default, focus starts on the top-most control that's closest to the leading edge of the screen. From there, pressing Tab moves focus from one control to the next, following the layout order of the current locale. When you reach the last control on the screen, pressing Tab again will restart the sequence. Focus movement with an Apple TV remote is directional. You can swipe up, down, left, and right to move focus between controls. Directional movement only works between adjacent targets. In this example, I can swipe right from the Creme Brûlée button to one of the other desserts. But if I want to add creme brûlée fixings to my grocery list, I can't swipe down. That button isn't directly beneath the creme brûlée button, so my gesture fails. To make these focus targets line up, I'll mark the bottom button's container as a focus section. Focus sections become targets for movement gestures, but they don't become focusable. Instead, they guide focus towards the nearest focusable content. To be effective, the focus sections have to take up more space than their contents. In this case, I'll add spacers before and after the button to make the stack grow to fit the width of the screen. With the larger focus target in place, I can now swipe down from anywhere to reach the bottom button. I can taste that creme brûlée already! I'll take you through some recipes that combine the staple ingredients I just described to polish the look and feel of custom controls and remove friction from common tasks.
Recently, I've been using a cookbook app made by my fellow chef Curt. You may recognize it from his WWDC22 video. The recipes in this section are based on some new features I've been working on that will benefit from some attention to focus behavior. For instance, I've added an in-app grocery list to help me remember what to get on my next trip to the grocery store.
This first recipe shows how a dash of programmatic focus movement can make editing my grocery list a delightful experience.
When the grocery list sheet appears, it always has an empty item at its end. Tapping on the empty item brings up the keyboard so I can describe what I need to buy. Adding groceries is a frequent task, so I'd like to save myself a tap by putting focus on the empty item automatically whenever the list appears. Earlier, I showed how to use the Focus State API to observe and update which view has focus. I'll use the same API here. The previous example used a flag to signal whether a single view had focus. In the case of my grocery list, there are any number of text fields to observe. FocusState's value can be any Hashable type for cases like this. Each ingredient I add on this screen has a unique ID, and I can keep track of focus by storing the ID associated with the focused text field. I'll use the "focused(_:equals:)" modifier to make the link between each text field and its ingredient. I need to provide this modifier with two arguments: a binding to my "focusedItem" property and the ingredient ID that the binding should be updated with, when focus is in that text field. I can now run the app and verify that the "focusedItem" property is updated with different ID values when I tap around the grocery list.
With my focus state binding in place, I have what I need to programmatically move focus to the text field when the grocery list first appears on screen. I do this by adding the "defaultFocus(_:_:)" view modifier to my list, which is now also available in iOS 17. When the system is evaluating focus for the first time on this screen, it will try to update my binding with the ID of the last grocery list item.
With these changes, adding to my grocery list is now a two-step process. Tap the toolbar button to present the sheet, and then start typing. There is no step three. As my shopping list grows, I notice that tapping the Add button in the toolbar creates a new empty list item, but focus remains where it was. I have to tap the empty item to give it focus. This is another case where I want the app to move focus programmatically so that I can start typing as soon as the new item appears. The difference is that now I want to be in control of the timing of the change.
Happily, I can use the same focus state binding that I created for setting default focus. In my GroceryListView, I have an "addEmptyItem" method that adds a new item in my model. And since I've already associated the new item's TextField with the "currentItemID" property, I just need to update the property with the new ID as part of my toolbar button action.
Voilà! Now when I want to start or update my grocery list, I don't have to tap anything to put focus where it needs to be, I can just start typing.
Next up, let's use some more of the ingredients to improve the focus interactions for a custom control that I've created.
At this point, I've cataloged a lot of recipes. As I try each one out, I want to remember which ones come out well and which might need some re-thinking, or at least a little more salt. To help with this, I've built a custom picker control with emoji to capture the highs and lows of my culinary journey. I can rate each recipe by tapping on an emoji, but as someone who lives the keyboard navigation lifestyle, I'd really like to be able to focus on the control with the Tab key and use the arrow keys to change the selection. Let's make that happen. Here's the basic structure of my emoji picker: The first thing I need to do is make the control focusable. I start out by adding the "focusable" modifier with no arguments. This makes my control focusable when I press the Tab key, but I notice some additional behavior that I don't see with other buttons and similar controls. My control, for example, gets focus on click, whereas buttons and segmented controls don't. Those controls require "Keyboard navigation" in order to be focused. Mine should, too. To get that behavior, I'll specify my control as being focusable for activation. Controls that are focusable for activation don't get focus on click, and they need "Keyboard navigation" turned on to receive focus with the keyboard. The next thing I notice is that the focus ring that macOS draws around my control is rectangular. For a more polished look, I want the focus ring to follow the path of the capsule-shaped background. Focus rings always follow the content shape of a view, which is a rectangle by default in my case. I'll use the "contentShape" modifier and pass in the same Capsule shape that I'm using to visually clip the view. Now that my control is focusable, the next step is to have it handle key presses. I want to be able to use the left and right arrow key to change the chosen rating. Using the "onMoveCommand" modifier, I can provide an action to perform in response to a platform-appropriate move command, like when arrow keys are pressed on a Mac keyboard, or the directional edges are tapped on an Apple TV remote. The system calls the action with the direction of movement, so I'll move the rating selection left or right based on that. Control content should flip horizontally for folks using a right-to-left language like Arabic and Hebrew. Make sure your move command action uses the Environment's "layoutDirection" to account for this.
One of the neat things about implementing focus behavior is that I can take the same control and get a great result in my Apple Watch app.
To handle focus input on watchOS, I use the "digitalCrownRotation" modifier instead of the "onMoveCommand" modifier. And I'll use the isFocused environment value to draw the familiar green border around the control when it has focus.
With just these few modifiers, I was able to take a simple control and add support for keyboards and the Digital Crown. The last recipe is for a focusable grid view that I've been building to showcase the pictures I've been taking of my finished results. I'm building this as a lazy grid, and I've already implemented some selection behavior. So clicking on an image selects it and double-clicking goes to the recipe's detail view. Now I need to consider how it should handle focus interactions. Specifically, I want the grid to be focusable when I press the Tab key. When focused, I want the arrow keys to update the selection, and I want the Return key to take me to the selected recipe's details. I'll use several of the ingredients I talked about before, plus a few extra ones to help me handle key presses and customize how the grid appears when it has focus.
As in previous examples, the first step is to make the grid focusable. In this case, I don't need to specify any interactions. By default, the grid will take focus when I click on it and when I use the keyboard to tab to it, whether or not "Keyboard navigation" is enabled. This is exactly what I want. Now that I've made the grid focusable, the system automatically draws a focus ring around it. For containers of selectable content, the effect is redundant. The colored border I add around the selected recipe already communicates whether the grid has focus. I can use the "focusEffectDisabled" modifier to turn off the automatic focus ring. Use "SelectionShapeStyle" for borders and other indicators that a view is selected. It automatically adapts to my chosen accent color, and it turns gray when none of its ancestor views have focus, like when focus moves from the grid to the sidebar. The next thing I want to do is hook up a main menu command for marking the selected recipe as a favorite. I'll use the Focused Values API for this and I'll pass in a binding to my selection for my menu commands to update as needed. To support arrow key selection, I'll use the onMoveCommand modifier. And when the system calls, I'll use the movement direction to update my grid's selected recipe. Lastly, I want a way to act on the selection and navigate to it when the Return key is pressed. I can do this with the "onKeyPress" modifier, which is new in macOS Sonoma and iOS 17. This modifier takes a set of keys or characters and an action to perform when any of those keys are pressed on a connected hardware keyboard. Return "ignored" if the action didn't handle the press and dispatch should continue up the view hierarchy. And for a bonus feature, I'll also use "onKeyPress" to implement Type Selection, so I can quickly scroll to and select a recipe by typing the first letter of its name.
Now that I've built a great keyboard experience for my grid on macOS, let's turn to the grid on tvOS. On tvOS, each cell in the grid is focusable, so that as focus is moved in different directions with the remote, the cell in that direction becomes focus and visually lifts above the others. The system uses the "lift" hover effect by default with Buttons and NavigationLinks. And this effect is appropriate for views with text or those combining text and images. However, these recipe photos would benefit from a different effect. New in tvOS 17, I can apply the highlight hover effect to my focusable views. This effect adds a perspective shift and specular shine to the focused item as I swipe the remote, and it looks great with artwork and photos, like my recipe thumbnails. And as a cherry on top for my tvOS app, I'll add focus sections. The grid is next to a list of buttons, and I'll often need to navigate between these two groupings. When using the app, I notice a familiar problem. When focus is on one of the lower rows in the grid, I can't swipe left to move the category buttons, because the focus targets aren't adjacent to one another. I'll place the category list in a focus section that spans the full height of the layout. Now as I swipe left from the Creme Brûlée, focus moves to the categories, just as I would expect. And the grid is complete.
Bellisimo! I covered a lot of ground in this video. It's time to gather up your focus ingredients and see what you can make. Test your macOS and iPadOS apps with keyboard navigation enabled. Place default focus where it will be most useful. Organize your controls in focus sections to help guide movement through irregular layouts. Thank you, and bon appétit!
-
-
5:05 - Focusable views
// Focusable views struct RecipeGrid: View { var body: some View { LazyVGrid(columns: [GridItem(), GridItem()]) { ForEach(0..<4) { _ in Capsule() } } .focusable(interactions: .edit) } } struct RatingPicker: View { var body: some View { HStack { Capsule() ; Capsule() } .focusable(interactions: .activate) } }
-
6:12 - Focus state
// Focus state struct GroceryListView: View { @FocusState private var isItemFocused @State private var itemName = "" var body: some View { TextField("Item Name", text: $itemName) .focused($isItemFocused) Button("Done") { isItemFocused = false } .disabled(!isItemFocused) } }
-
7:32 - Focused values
// Focused values struct SelectedRecipeKey: FocusedValueKey { typealias Value = Binding<Recipe> } extension FocusedValues { var selectedRecipe: Binding<Recipe>? { get { self[SelectedRecipeKey.self] } set { self[SelectedRecipeKey.self] = newValue } } } struct RecipeView: View { @Binding var recipe: Recipe var body: some View { VStack { Text(recipe.title) } .focusedSceneValue(\.selectedRecipe, $recipe) } } struct RecipeCommands: Commands { @FocusedBinding(\.selectedRecipe) private var selectedRecipe: Recipe? var body: some Commands { CommandMenu("Recipe") { Button("Add to Grocery List") { if let selectedRecipe { addRecipe(selectedRecipe) } } .disabled(selectedRecipe == nil) } } private func addRecipe(_ recipe: Recipe) { /* ... */ } } struct Recipe: Hashable, Identifiable { let id = UUID() var title = "" var isFavorite = false }
-
10:03 - Focus sections
// Focus sections struct ContentView: View { @State private var favorites = Recipe.examples @State private var selection = Recipe.examples.first! var body: some View { VStack { HStack { ForEach(favorites) { recipe in Button(recipe.name) { selection = recipe } } } Image(selection.imageName) HStack { Spacer() Button("Add to Grocery List") { addIngredients(selection) } Spacer() } .focusSection() } } private func addIngredients(_ recipe: Recipe) { /* ... */ } } struct Recipe: Hashable, Identifiable { static let examples: [Recipe] = [ Recipe(name: "Apple Pie"), Recipe(name: "Baklava"), Recipe(name: "Crème Brûlée") ] let id = UUID() var name = "" var imageName = "" }
-
11:29 - Controlling focus
struct GroceryListView: View { @State private var list = GroceryList.examples @FocusState private var focusedItem: GroceryList.Item.ID? var body: some View { NavigationStack { List($list.items) { $item in HStack { Toggle("Obtained", isOn: $item.isObtained) TextField("Item Name", text: $item.name) .onSubmit { addEmptyItem() } .focused($focusedItem, equals: item.id) } } .defaultFocus($focusedItem, list.items.last?.id) .toggleStyle(.checklist) } .toolbar { Button(action: addEmptyItem) { Label("New Item", systemImage: "plus") } } } private func addEmptyItem() { let newItem = list.addItem() focusedItem = newItem.id } } struct GroceryList: Codable { static let examples = GroceryList(items: [ GroceryList.Item(name: "Apples"), GroceryList.Item(name: "Lasagna"), GroceryList.Item(name: "") ]) struct Item: Codable, Hashable, Identifiable { var id = UUID() var name: String var isObtained: Bool = false } var items: [Item] = [] mutating func addItem() -> Item { let item = GroceryList.Item(name: "") items.append(item) return item } } struct ChecklistToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle.dashed") .foregroundStyle(configuration.isOn ? .green : .gray) .font(.system(size: 20)) .contentTransition(.symbolEffect) .animation(.linear, value: configuration.isOn) } .buttonStyle(.plain) .contentShape(.circle) } } extension ToggleStyle where Self == ChecklistToggleStyle { static var checklist: ChecklistToggleStyle { .init() } }
-
15:25 - Custom focusable control
struct RatingPicker: View { @Environment(\.layoutDirection) private var layoutDirection @Binding var rating: Rating? #if os(watchOS) @State private var digitalCrownRotation = 0.0 #endif var body: some View { EmojiContainer { ratingOptions } .contentShape(.capsule) .focusable(interactions: .activate) #if os(macOS) .onMoveCommand { direction in selectRating(direction, layoutDirection: layoutDirection) } #endif #if os(watchOS) .digitalCrownRotation($digitalCrownRotation, from: 0, through: Double(Rating.allCases.count - 1), by: 1, sensitivity: .low) .onChange(of: digitalCrownRotation) { oldValue, newValue in if let rating = Rating(rawValue: Int(round(digitalCrownRotation))) { self.rating = rating } } #endif } private var ratingOptions: some View { ForEach(Rating.allCases) { rating in EmojiView(rating: rating, isSelected: self.rating == rating) { self.rating = rating } } } #if os(macOS) private func selectRating( _ direction: MoveCommandDirection, layoutDirection: LayoutDirection ) { var direction = direction if layoutDirection == .rightToLeft { switch direction { case .left: direction = .right case .right: direction = .left default: break } } if let rating { switch direction { case .left: guard let previousRating = rating.previous else { return } self.rating = previousRating case .right: guard let nextRating = rating.next else { return } self.rating = nextRating default: break } } } #endif } private struct EmojiContainer<Content: View>: View { @Environment(\.isFocused) private var isFocused private var content: Content #if os(watchOS) private var strokeColor: Color { isFocused ? .green : .clear } #endif init(@ViewBuilder content: @escaping () -> Content) { self.content = content() } var body: some View { HStack(spacing: 2) { content } .frame(height: 32) .font(.system(size: 24)) .padding(.horizontal, 8) .padding(.vertical, 6) .background(.quaternary) .clipShape(.capsule) #if os(watchOS) .overlay( Capsule() .strokeBorder(strokeColor, lineWidth: 1.5) ) #endif } } private struct EmojiView: View { var rating: Rating var isSelected: Bool var action: () -> Void var body: some View { ZStack { Circle() .fill(isSelected ? Color.accentColor : Color.clear) Text(verbatim: rating.emoji) .onTapGesture { action() } .accessibilityLabel(rating.localizedName) } } } enum Rating: Int, CaseIterable, Identifiable { case meh case yummy case delicious var id: RawValue { rawValue } var emoji: String { switch self { case .meh: return "😕" case .yummy: return "🙂" case .delicious: return "🥰" } } var localizedName: LocalizedStringKey { switch self { case .meh: return "Meh" case .yummy: return "Yummy" case .delicious: return "Delicious" } } var previous: Rating? { let ratings = Rating.allCases let index = ratings.firstIndex(of: self)! guard index != ratings.startIndex else { return nil } let previousIndex = ratings.index(before: index) return ratings[previousIndex] } var next: Rating? { let ratings = Rating.allCases let index = ratings.firstIndex(of: self)! let nextIndex = ratings.index(after: index) guard nextIndex != ratings.endIndex else { return nil } return ratings[nextIndex] } }
-
18:50 - Grid view
struct ContentView: View { @State private var recipes = Recipe.examples @State private var selection: Recipe.ID = Recipe.examples.first!.id @Environment(\.layoutDirection) private var layoutDirection var body: some View { LazyVGrid(columns: columns) { ForEach(recipes) { recipe in RecipeTile(recipe: recipe, isSelected: recipe.id == selection) .id(recipe.id) #if os(macOS) .onTapGesture { selection = recipe.id } .simultaneousGesture(TapGesture(count: 2).onEnded { navigateToRecipe(id: recipe.id) }) #else .onTapGesture { navigateToRecipe(id: recipe.id) } #endif } } .focusable() .focusEffectDisabled() .focusedValue(\.selectedRecipe, $selection) .onMoveCommand { direction in selectRecipe(direction, layoutDirection: layoutDirection) } .onKeyPress(.return) { navigateToRecipe(id: selection) return .handled } .onKeyPress(characters: .alphanumerics, phases: .down) { keyPress in selectRecipe(matching: keyPress.characters) } } private var columns: [GridItem] { [ GridItem(.adaptive(minimum: RecipeTile.size), spacing: 0) ] } private func navigateToRecipe(id: Recipe.ID) { // ... } private func selectRecipe( _ direction: MoveCommandDirection, layoutDirection: LayoutDirection ) { // ... } private func selectRecipe(matching characters: String) -> KeyPress.Result { // ... return .handled } } struct RecipeTile: View { static let size = 240.0 static let selectionStrokeWidth = 4.0 var recipe: Recipe var isSelected: Bool private var strokeStyle: AnyShapeStyle { isSelected ? AnyShapeStyle(.selection) : AnyShapeStyle(.clear) } var body: some View { VStack { RoundedRectangle(cornerRadius: 20) .fill(.background) .strokeBorder( strokeStyle, lineWidth: Self.selectionStrokeWidth) .frame(width: Self.size, height: Self.size) Text(recipe.name) } } } struct SelectedRecipeKey: FocusedValueKey { typealias Value = Binding<Recipe.ID> } extension FocusedValues { var selectedRecipe: Binding<Recipe.ID>? { get { self[SelectedRecipeKey.self] } set { self[SelectedRecipeKey.self] = newValue } } } struct RecipeCommands: Commands { @FocusedBinding(\.selectedRecipe) private var selectedRecipe: Recipe.ID? var body: some Commands { CommandMenu("Recipe") { Button("Add to Grocery List") { if let selectedRecipe { addRecipe(selectedRecipe) } } .disabled(selectedRecipe == nil) } } private func addRecipe(_ recipe: Recipe.ID) { /* ... */ } } struct Recipe: Hashable, Identifiable { static let examples: [Recipe] = [ Recipe(name: "Apple Pie"), Recipe(name: "Baklava"), Recipe(name: "Crème Brûlée") ] let id = UUID() var name = "" var imageName = "" }
-
21:28 - Focusable grid on tvOS
struct ContentView: View { var body: some View { HStack { VStack { List(["Dessert", "Pancake", "Salad", "Sandwich"], id: \.self) { NavigationLink($0, destination: Color.gray) } Spacer() } .focusSection() ScrollView { LazyVGrid(columns: [GridItem(), GridItem()]) { RoundedRectangle(cornerRadius: 5.0) .focusable() } } .focusSection() } } }
-
-
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.