-
The SwiftUI cookbook for navigation
The recipe for a great app begins with a clear and robust navigation structure. Join the SwiftUI team in our proverbial coding kitchen and learn how you can cook up a great experience for your app. We'll introduce you to SwiftUI's navigation stack and split view features, show you how you can link to specific areas of your app, and explore how you can quickly and easily restore navigational state.
Ressources
- List
- Migrating to new navigation types
- NavigationSplitView
- NavigationStack
- Bringing robust navigation structure to your SwiftUI app
Vidéos connexes
Tech Talks
WWDC22
-
Rechercher dans cette vidéo…
-
-
6:05 - Pushable Stack
import SwiftUI // Pushable stack struct PushableStack: View { @State private var path: [Recipe] = [] @StateObject private var dataModel = DataModel() var body: some View { NavigationStack(path: $path) { List(Category.allCases) { category in Section(category.localizedName) { ForEach(dataModel.recipes(in: category)) { recipe in NavigationLink(recipe.name, value: recipe) } } } .navigationTitle("Categories") .navigationDestination(for: Recipe.self) { recipe in RecipeDetail(recipe: recipe) } } .environmentObject(dataModel) } } // Helpers for code example struct RecipeDetail: View { @EnvironmentObject private var dataModel: DataModel var recipe: Recipe var body: some View { Text("Recipe details go here") .navigationTitle(recipe.name) ForEach(recipe.related.compactMap { dataModel[$0] }) { related in NavigationLink(related.name, value: related) } } } class DataModel: ObservableObject { @Published var recipes: [Recipe] = builtInRecipes func recipes(in category: Category?) -> [Recipe] { recipes .filter { $0.category == category } .sorted { $0.name < $1.name } } subscript(recipeId: Recipe.ID) -> Recipe? { // A real app would want to maintain an index from identifiers to // recipes. recipes.first { recipe in recipe.id == recipeId } } } enum Category: Int, Hashable, CaseIterable, Identifiable, Codable { case dessert case pancake case salad case sandwich var id: Int { rawValue } var localizedName: LocalizedStringKey { switch self { case .dessert: return "Dessert" case .pancake: return "Pancake" case .salad: return "Salad" case .sandwich: return "Sandwich" } } } struct Recipe: Hashable, Identifiable { let id = UUID() var name: String var category: Category var ingredients: [Ingredient] var related: [Recipe.ID] = [] var imageName: String? = nil } struct Ingredient: Hashable, Identifiable { let id = UUID() var description: String static func fromLines(_ lines: String) -> [Ingredient] { lines.split(separator: "\n", omittingEmptySubsequences: true) .map { Ingredient(description: String($0)) } } } let builtInRecipes: [Recipe] = { var recipes = [ "Apple Pie": Recipe( name: "Apple Pie", category: .dessert, ingredients: Ingredient.fromLines(applePie)), "Baklava": Recipe( name: "Baklava", category: .dessert, ingredients: []), "Bolo de Rolo": Recipe( name: "Bolo de rolo", category: .dessert, ingredients: []), "Chocolate Crackles": Recipe( name: "Chocolate crackles", category: .dessert, ingredients: []), "Crème Brûlée": Recipe( name: "Crème brûlée", category: .dessert, ingredients: []), "Fruit Pie Filling": Recipe( name: "Fruit Pie Filling", category: .dessert, ingredients: []), "Kanom Thong Ek": Recipe( name: "Kanom Thong Ek", category: .dessert, ingredients: []), "Mochi": Recipe( name: "Mochi", category: .dessert, ingredients: []), "Marzipan": Recipe( name: "Marzipan", category: .dessert, ingredients: []), "Pie Crust": Recipe( name: "Pie Crust", category: .dessert, ingredients: Ingredient.fromLines(pieCrust)), "Shortbread Biscuits": Recipe( name: "Shortbread Biscuits", category: .dessert, ingredients: []), "Tiramisu": Recipe( name: "Tiramisu", category: .dessert, ingredients: []), "Crêpe": Recipe( name: "Crêpe", category: .pancake, ingredients: []), "Jianbing": Recipe( name: "Jianbing", category: .pancake, ingredients: []), "American": Recipe( name: "American", category: .pancake, ingredients: []), "Dosa": Recipe( name: "Dosa", category: .pancake, ingredients: []), "Injera": Recipe( name: "Injera", category: .pancake, ingredients: []), "Acar": Recipe( name: "Acar", category: .salad, ingredients: []), "Ambrosia": Recipe( name: "Ambrosia", category: .salad, ingredients: []), "Bok l'hong": Recipe( name: "Bok l'hong", category: .salad, ingredients: []), "Caprese": Recipe( name: "Caprese", category: .salad, ingredients: []), "Ceviche": Recipe( name: "Ceviche", category: .salad, ingredients: []), "Çoban salatası": Recipe( name: "Çoban salatası", category: .salad, ingredients: []), "Fiambre": Recipe( name: "Fiambre", category: .salad, ingredients: []), "Kachumbari": Recipe( name: "Kachumbari", category: .salad, ingredients: []), "Niçoise": Recipe( name: "Niçoise", category: .salad, ingredients: []), ] recipes["Apple Pie"]!.related = [ recipes["Pie Crust"]!.id, recipes["Fruit Pie Filling"]!.id, ] recipes["Pie Crust"]!.related = [recipes["Fruit Pie Filling"]!.id] recipes["Fruit Pie Filling"]!.related = [recipes["Pie Crust"]!.id] return Array(recipes.values) }() let applePie = """ ¾ cup white sugar 2 tablespoons all-purpose flour ½ teaspoon ground cinnamon ¼ teaspoon ground nutmeg ½ teaspoon lemon zest 7 cups thinly sliced apples 2 teaspoons lemon juice 1 tablespoon butter 1 recipe pastry for a 9 inch double crust pie 4 tablespoons milk """ let pieCrust = """ 2 ½ cups all purpose flour 1 Tbsp. powdered sugar 1 tsp. sea salt ½ cup shortening ½ cup butter (Cold, Cut Into Small Pieces) ⅓ cup cold water (Plus More As Needed) """ struct PushableStack_Previews: PreviewProvider { static var previews: some View { PushableStack() } } -
10:40 - Multiple Columns
import SwiftUI // Multiple columns struct MultipleColumns: View { @State private var selectedCategory: Category? @State private var selectedRecipe: Recipe? @StateObject private var dataModel = DataModel() var body: some View { NavigationSplitView { List(Category.allCases, selection: $selectedCategory) { category in NavigationLink(category.localizedName, value: category) } .navigationTitle("Categories") } content: { List( dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in NavigationLink(recipe.name, value: recipe) } .navigationTitle(selectedCategory?.localizedName ?? "Recipes") } detail: { RecipeDetail(recipe: selectedRecipe) } } } // Helpers for code example struct RecipeDetail: View { var recipe: Recipe? var body: some View { Text("Recipe details go here") .navigationTitle(recipe?.name ?? "") } } class DataModel: ObservableObject { @Published var recipes: [Recipe] = builtInRecipes func recipes(in category: Category?) -> [Recipe] { recipes .filter { $0.category == category } .sorted { $0.name < $1.name } } } enum Category: Int, Hashable, CaseIterable, Identifiable, Codable { case dessert case pancake case salad case sandwich var id: Int { rawValue } var localizedName: LocalizedStringKey { switch self { case .dessert: return "Dessert" case .pancake: return "Pancake" case .salad: return "Salad" case .sandwich: return "Sandwich" } } } struct Recipe: Hashable, Identifiable { let id = UUID() var name: String var category: Category var ingredients: [Ingredient] var related: [Recipe.ID] = [] var imageName: String? = nil } struct Ingredient: Hashable, Identifiable { let id = UUID() var description: String static func fromLines(_ lines: String) -> [Ingredient] { lines.split(separator: "\n", omittingEmptySubsequences: true) .map { Ingredient(description: String($0)) } } } let builtInRecipes: [Recipe] = { var recipes = [ "Apple Pie": Recipe( name: "Apple Pie", category: .dessert, ingredients: Ingredient.fromLines(applePie)), "Baklava": Recipe( name: "Baklava", category: .dessert, ingredients: []), "Bolo de Rolo": Recipe( name: "Bolo de rolo", category: .dessert, ingredients: []), "Chocolate Crackles": Recipe( name: "Chocolate crackles", category: .dessert, ingredients: []), "Crème Brûlée": Recipe( name: "Crème brûlée", category: .dessert, ingredients: []), "Fruit Pie Filling": Recipe( name: "Fruit Pie Filling", category: .dessert, ingredients: []), "Kanom Thong Ek": Recipe( name: "Kanom Thong Ek", category: .dessert, ingredients: []), "Mochi": Recipe( name: "Mochi", category: .dessert, ingredients: []), "Marzipan": Recipe( name: "Marzipan", category: .dessert, ingredients: []), "Pie Crust": Recipe( name: "Pie Crust", category: .dessert, ingredients: Ingredient.fromLines(pieCrust)), "Shortbread Biscuits": Recipe( name: "Shortbread Biscuits", category: .dessert, ingredients: []), "Tiramisu": Recipe( name: "Tiramisu", category: .dessert, ingredients: []), "Crêpe": Recipe( name: "Crêpe", category: .pancake, ingredients: []), "Jianbing": Recipe( name: "Jianbing", category: .pancake, ingredients: []), "American": Recipe( name: "American", category: .pancake, ingredients: []), "Dosa": Recipe( name: "Dosa", category: .pancake, ingredients: []), "Injera": Recipe( name: "Injera", category: .pancake, ingredients: []), "Acar": Recipe( name: "Acar", category: .salad, ingredients: []), "Ambrosia": Recipe( name: "Ambrosia", category: .salad, ingredients: []), "Bok l'hong": Recipe( name: "Bok l'hong", category: .salad, ingredients: []), "Caprese": Recipe( name: "Caprese", category: .salad, ingredients: []), "Ceviche": Recipe( name: "Ceviche", category: .salad, ingredients: []), "Çoban salatası": Recipe( name: "Çoban salatası", category: .salad, ingredients: []), "Fiambre": Recipe( name: "Fiambre", category: .salad, ingredients: []), "Kachumbari": Recipe( name: "Kachumbari", category: .salad, ingredients: []), "Niçoise": Recipe( name: "Niçoise", category: .salad, ingredients: []), ] recipes["Apple Pie"]!.related = [ recipes["Pie Crust"]!.id, recipes["Fruit Pie Filling"]!.id, ] recipes["Pie Crust"]!.related = [recipes["Fruit Pie Filling"]!.id] recipes["Fruit Pie Filling"]!.related = [recipes["Pie Crust"]!.id] return Array(recipes.values) }() let applePie = """ ¾ cup white sugar 2 tablespoons all-purpose flour ½ teaspoon ground cinnamon ¼ teaspoon ground nutmeg ½ teaspoon lemon zest 7 cups thinly sliced apples 2 teaspoons lemon juice 1 tablespoon butter 1 recipe pastry for a 9 inch double crust pie 4 tablespoons milk """ let pieCrust = """ 2 ½ cups all purpose flour 1 Tbsp. powdered sugar 1 tsp. sea salt ½ cup shortening ½ cup butter (Cold, Cut Into Small Pieces) ⅓ cup cold water (Plus More As Needed) """ struct MultipleColumns_Previews: PreviewProvider { static var previews: some View { MultipleColumns() } } -
14:10 - Multiple Columns with a Stack
import SwiftUI // Multiple columns with a stack struct MultipleColumnsWithStack: View { @State private var selectedCategory: Category? @State private var path: [Recipe] = [] @StateObject private var dataModel = DataModel() var body: some View { NavigationSplitView { List(Category.allCases, selection: $selectedCategory) { category in NavigationLink(category.localizedName, value: category) } .navigationTitle("Categories") } detail: { NavigationStack(path: $path) { RecipeGrid(category: selectedCategory) } } .environmentObject(dataModel) } } struct RecipeGrid: View { @EnvironmentObject private var dataModel: DataModel var category: Category? var body: some View { if let category = category { ScrollView { LazyVGrid(columns: columns) { ForEach(dataModel.recipes(in: category)) { recipe in NavigationLink(value: recipe) { RecipeTile(recipe: recipe) } } } } .navigationTitle(category.localizedName) .navigationDestination(for: Recipe.self) { recipe in RecipeDetail(recipe: recipe) } } else { Text("Select a category") } } var columns: [GridItem] { [GridItem(.adaptive(minimum: 240))] } } struct RecipeDetail: View { @EnvironmentObject private var dataModel: DataModel var recipe: Recipe var body: some View { Text("Recipe details go here") .navigationTitle(recipe.name) ForEach(recipe.related.compactMap { dataModel[$0] }) { related in NavigationLink(related.name, value: related) } } } struct RecipeTile: View { var recipe: Recipe var body: some View { VStack { Rectangle() .fill(Color.secondary.gradient) .frame(width: 240, height: 240) Text(recipe.name) .lineLimit(2, reservesSpace: true) .font(.headline) } .tint(.primary) } } class DataModel: ObservableObject { @Published var recipes: [Recipe] = builtInRecipes func recipes(in category: Category?) -> [Recipe] { recipes .filter { $0.category == category } .sorted { $0.name < $1.name } } subscript(recipeId: Recipe.ID) -> Recipe? { // A real app would want to maintain an index from identifiers to // recipes. recipes.first { recipe in recipe.id == recipeId } } } enum Category: Int, Hashable, CaseIterable, Identifiable, Codable { case dessert case pancake case salad case sandwich var id: Int { rawValue } var localizedName: LocalizedStringKey { switch self { case .dessert: return "Dessert" case .pancake: return "Pancake" case .salad: return "Salad" case .sandwich: return "Sandwich" } } } struct Recipe: Hashable, Identifiable { let id = UUID() var name: String var category: Category var ingredients: [Ingredient] var related: [Recipe.ID] = [] var imageName: String? = nil } struct Ingredient: Hashable, Identifiable { let id = UUID() var description: String static func fromLines(_ lines: String) -> [Ingredient] { lines.split(separator: "\n", omittingEmptySubsequences: true) .map { Ingredient(description: String($0)) } } } let builtInRecipes: [Recipe] = { var recipes = [ "Apple Pie": Recipe( name: "Apple Pie", category: .dessert, ingredients: Ingredient.fromLines(applePie)), "Baklava": Recipe( name: "Baklava", category: .dessert, ingredients: []), "Bolo de Rolo": Recipe( name: "Bolo de rolo", category: .dessert, ingredients: []), "Chocolate Crackles": Recipe( name: "Chocolate crackles", category: .dessert, ingredients: []), "Crème Brûlée": Recipe( name: "Crème brûlée", category: .dessert, ingredients: []), "Fruit Pie Filling": Recipe( name: "Fruit Pie Filling", category: .dessert, ingredients: []), "Kanom Thong Ek": Recipe( name: "Kanom Thong Ek", category: .dessert, ingredients: []), "Mochi": Recipe( name: "Mochi", category: .dessert, ingredients: []), "Marzipan": Recipe( name: "Marzipan", category: .dessert, ingredients: []), "Pie Crust": Recipe( name: "Pie Crust", category: .dessert, ingredients: Ingredient.fromLines(pieCrust)), "Shortbread Biscuits": Recipe( name: "Shortbread Biscuits", category: .dessert, ingredients: []), "Tiramisu": Recipe( name: "Tiramisu", category: .dessert, ingredients: []), "Crêpe": Recipe( name: "Crêpe", category: .pancake, ingredients: []), "Jianbing": Recipe( name: "Jianbing", category: .pancake, ingredients: []), "American": Recipe( name: "American", category: .pancake, ingredients: []), "Dosa": Recipe( name: "Dosa", category: .pancake, ingredients: []), "Injera": Recipe( name: "Injera", category: .pancake, ingredients: []), "Acar": Recipe( name: "Acar", category: .salad, ingredients: []), "Ambrosia": Recipe( name: "Ambrosia", category: .salad, ingredients: []), "Bok l'hong": Recipe( name: "Bok l'hong", category: .salad, ingredients: []), "Caprese": Recipe( name: "Caprese", category: .salad, ingredients: []), "Ceviche": Recipe( name: "Ceviche", category: .salad, ingredients: []), "Çoban salatası": Recipe( name: "Çoban salatası", category: .salad, ingredients: []), "Fiambre": Recipe( name: "Fiambre", category: .salad, ingredients: []), "Kachumbari": Recipe( name: "Kachumbari", category: .salad, ingredients: []), "Niçoise": Recipe( name: "Niçoise", category: .salad, ingredients: []), ] recipes["Apple Pie"]!.related = [ recipes["Pie Crust"]!.id, recipes["Fruit Pie Filling"]!.id, ] recipes["Pie Crust"]!.related = [recipes["Fruit Pie Filling"]!.id] recipes["Fruit Pie Filling"]!.related = [recipes["Pie Crust"]!.id] return Array(recipes.values) }() let applePie = """ ¾ cup white sugar 2 tablespoons all-purpose flour ½ teaspoon ground cinnamon ¼ teaspoon ground nutmeg ½ teaspoon lemon zest 7 cups thinly sliced apples 2 teaspoons lemon juice 1 tablespoon butter 1 recipe pastry for a 9 inch double crust pie 4 tablespoons milk """ let pieCrust = """ 2 ½ cups all purpose flour 1 Tbsp. powdered sugar 1 tsp. sea salt ½ cup shortening ½ cup butter (Cold, Cut Into Small Pieces) ⅓ cup cold water (Plus More As Needed) """ struct MultipleColumnsWithStack_Previews: PreviewProvider { static var previews: some View { MultipleColumnsWithStack() } } -
18:12 - Use Scene Storage
import SwiftUI import Combine import Foundation // Use SceneStorage to save and restore struct UseSceneStorage: View { @StateObject private var navModel = NavigationModel() @SceneStorage("navigation") private var data: Data? @StateObject private var dataModel = DataModel() var body: some View { NavigationSplitView { List( Category.allCases, selection: $navModel.selectedCategory ) { category in NavigationLink(category.localizedName, value: category) } .navigationTitle("Categories") } detail: { NavigationStack(path: $navModel.recipePath) { RecipeGrid(category: navModel.selectedCategory) } } .task { if let data = data { navModel.jsonData = data } for await _ in navModel.objectWillChangeSequence { data = navModel.jsonData } } .environmentObject(dataModel) } } // Make the navigation model Codable class NavigationModel: ObservableObject, Codable { @Published var selectedCategory: Category? @Published var recipePath: [Recipe] = [] enum CodingKeys: String, CodingKey { case selectedCategory case recipePathIds } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory) try container.encode(recipePath.map(\.id), forKey: .recipePathIds) } init() {} required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.selectedCategory = try container.decodeIfPresent( Category.self, forKey: .selectedCategory) let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds) self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] } } var jsonData: Data? { get { try? JSONEncoder().encode(self) } set { guard let data = newValue, let model = try? JSONDecoder().decode(NavigationModel.self, from: data) else { return } self.selectedCategory = model.selectedCategory self.recipePath = model.recipePath } } var objectWillChangeSequence: AsyncPublisher<Publishers.Buffer<ObservableObjectPublisher>> { objectWillChange .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest) .values } } struct RecipeGrid: View { var category: Category? @EnvironmentObject private var dataModel: DataModel var body: some View { if let category = category { ScrollView { LazyVGrid(columns: columns) { ForEach(dataModel.recipes(in: category)) { recipe in NavigationLink(value: recipe) { RecipeTile(recipe: recipe) } } } } .navigationTitle(category.localizedName) .navigationDestination(for: Recipe.self) { recipe in RecipeDetail(recipe: recipe) } } else { Text("Select a category") } } var columns: [GridItem] { [GridItem(.adaptive(minimum: 240))] } } struct RecipeDetail: View { @EnvironmentObject private var dataModel: DataModel var recipe: Recipe var body: some View { Text("Recipe details go here") .navigationTitle(recipe.name) ForEach(recipe.related.compactMap { dataModel[$0] }) { related in NavigationLink(related.name, value: related) } } } struct RecipeTile: View { var recipe: Recipe var body: some View { VStack { Rectangle() .fill(Color.secondary.gradient) .frame(width: 240, height: 240) Text(recipe.name) .lineLimit(2, reservesSpace: true) .font(.headline) } .tint(.primary) } } class DataModel: ObservableObject { @Published var recipes: [Recipe] = builtInRecipes static var shared: DataModel { // Just instantiate each time for the example. A real app would need to // persist the data model as well. DataModel() } func recipes(in category: Category?) -> [Recipe] { recipes .filter { $0.category == category } .sorted { $0.name < $1.name } } subscript(recipeId: Recipe.ID) -> Recipe? { // A real app would want to maintain an index from identifiers to // recipes. recipes.first { recipe in recipe.id == recipeId } } } enum Category: Int, Hashable, CaseIterable, Identifiable, Codable { case dessert case pancake case salad case sandwich var id: Int { rawValue } var localizedName: LocalizedStringKey { switch self { case .dessert: return "Dessert" case .pancake: return "Pancake" case .salad: return "Salad" case .sandwich: return "Sandwich" } } } struct Recipe: Hashable, Identifiable { let id: UUID var name: String var category: Category var ingredients: [Ingredient] var related: [Recipe.ID] = [] var imageName: String? = nil } struct Ingredient: Hashable, Identifiable { let id = UUID() var description: String static func fromLines(_ lines: String) -> [Ingredient] { lines.split(separator: "\n", omittingEmptySubsequences: true) .map { Ingredient(description: String($0)) } } } let builtInRecipes: [Recipe] = { var recipes = [ "Apple Pie": Recipe( id: UUID(uuidString: "E35A5C9C-F1EA-4B3D-9980-E2240B363AC8")!, name: "Apple Pie", category: .dessert, ingredients: Ingredient.fromLines(applePie)), "Baklava": Recipe( id: UUID(uuidString: "B95B2D99-F45D-4B74-9EC4-526914FFC414")!, name: "Baklava", category: .dessert, ingredients: []), "Bolo de Rolo": Recipe( id: UUID(uuidString: "E17C729D-1E09-48F6-99E2-5BB959F5AE70")!, name: "Bolo de Rolo", category: .dessert, ingredients: []), "Chocolate Crackles": Recipe( id: UUID(uuidString: "89202A12-2B04-4EFE-ADC5-D1ECE7A25389")!, name: "Chocolate Crackles", category: .dessert, ingredients: []), "Crème Brûlée": Recipe( id: UUID(uuidString: "412EA92A-40B5-4CFE-9379-627A1C80FFE1")!, name: "Crème Brûlée", category: .dessert, ingredients: []), "Fruit Pie Filling": Recipe( id: UUID(uuidString: "4792C8AE-9596-4502-A9CB-806E2DFEA408")!, name: "Fruit Pie Filling", category: .dessert, ingredients: []), "Kanom Thong Ek": Recipe( id: UUID(uuidString: "331C25F6-4FED-4DA5-980E-7E619855DE92")!, name: "Kanom Thong Ek", category: .dessert, ingredients: []), "Mochi": Recipe( id: UUID(uuidString: "1EAA5288-8D2B-4969-AF97-ED591796B456")!, name: "Mochi", category: .dessert, ingredients: []), "Marzipan": Recipe( id: UUID(uuidString: "416F4F5A-A81C-40FD-87F1-060B0F57DE6D")!, name: "Marzipan", category: .dessert, ingredients: []), "Pie Crust": Recipe( id: UUID(uuidString: "D0820C1A-1AFB-4472-97DA-39A475304048")!, name: "Pie Crust", category: .dessert, ingredients: Ingredient.fromLines(pieCrust)), "Shortbread Biscuits": Recipe( id: UUID(uuidString: "3D9FEA8C-B38E-4739-8B4B-424885D76926")!, name: "Shortbread Biscuits", category: .dessert, ingredients: []), "Tiramisu": Recipe( id: UUID(uuidString: "586B9A4C-410A-40D2-AE40-BC32351A5C08")!, name: "Tiramisu", category: .dessert, ingredients: []), "Crêpe": Recipe( id: UUID(uuidString: "9BD6C3B2-30CB-425E-8D60-7F07D0BA720C")!, name: "Crêpe", category: .pancake, ingredients: []), "Jianbing": Recipe( id: UUID(uuidString: "117E5CD4-8FF9-43FB-ACAE-53C35A648F6F")!, name: "Jianbing", category: .pancake, ingredients: []), "American": Recipe( id: UUID(uuidString: "4584B877-E482-4FF2-824E-FC667BFAD271")!, name: "American", category: .pancake, ingredients: []), "Dosa": Recipe( id: UUID(uuidString: "5666FEB6-90DB-4CD2-91FA-D6F00986E90E")!, name: "Dosa", category: .pancake, ingredients: []), "Injera": Recipe( id: UUID(uuidString: "752DAEB8-123E-4C48-A190-79742AA56869")!, name: "Injera", category: .pancake, ingredients: []), "Acar": Recipe( id: UUID(uuidString: "F0D54AF2-04AD-4F08-ACE4-7886FCAE1F7B")!, name: "Acar", category: .salad, ingredients: []), "Ambrosia": Recipe( id: UUID(uuidString: "F7FD59E8-F1AE-4331-8667-D5534817F7E7")!, name: "Ambrosia", category: .salad, ingredients: []), "Bok L'hong": Recipe( id: UUID(uuidString: "3DE38C07-F985-4E05-810C-1108A777766B")!, name: "Bok L'hong", category: .salad, ingredients: []), "Caprese": Recipe( id: UUID(uuidString: "055D963C-0546-4578-AF18-6FBEE249EF35")!, name: "Caprese", category: .salad, ingredients: []), "Ceviche": Recipe( id: UUID(uuidString: "50B62AF4-89AF-4D00-9832-E200FEC01279")!, name: "Ceviche", category: .salad, ingredients: []), "Çoban Salatası": Recipe( id: UUID(uuidString: "87AD6B33-FFD2-4E5C-BC4B-59769F7AC7E3")!, name: "Çoban Salatası", category: .salad, ingredients: []), "Fiambre": Recipe( id: UUID(uuidString: "8A9BC0D5-A931-4381-BDA8-713DF6389FE7")!, name: "Fiambre", category: .salad, ingredients: []), "Kachumbari": Recipe( id: UUID(uuidString: "E9497D38-49E0-4A18-939B-63A3F2C7C0B4")!, name: "Kachumbari", category: .salad, ingredients: []), "Niçoise": Recipe( id: UUID(uuidString: "DE9F7106-4D0C-4EAC-B44C-A8D8ECD81087")!, name: "Niçoise", category: .salad, ingredients: []) ] recipes["Apple Pie"]!.related = [ recipes["Pie Crust"]!.id, recipes["Fruit Pie Filling"]!.id ] recipes["Pie Crust"]!.related = [recipes["Fruit Pie Filling"]!.id] recipes["Fruit Pie Filling"]!.related = [recipes["Pie Crust"]!.id] return Array(recipes.values) }() let applePie = """ ¾ cup white sugar 2 tablespoons all-purpose flour ½ teaspoon ground cinnamon ¼ teaspoon ground nutmeg ½ teaspoon lemon zest 7 cups thinly sliced apples 2 teaspoons lemon juice 1 tablespoon butter 1 recipe pastry for a 9 inch double crust pie 4 tablespoons milk """ let pieCrust = """ 2 ½ cups all purpose flour 1 Tbsp. powdered sugar 1 tsp. sea salt ½ cup shortening ½ cup butter (Cold, Cut Into Small Pieces) ⅓ cup cold water (Plus More As Needed) """ struct UseSceneStorage_Previews: PreviewProvider { static var previews: some View { UseSceneStorage() } } -
25:33 - Biscuits
import SwiftUI struct Biscuits: View { @State private var step = 0 @ScaledMetric private var fontSize = 18 var body: some View { VStack(alignment: .leading) { HStack { Spacer() VStack { Text("Biscuits") .font(.headline) Text(subtitle) .font(.subheadline) } .padding(16) Spacer() } Spacer() Text(LocalizedStringKey(steps[step])) .font(.system( size: fontSize, weight: .semibold, design: .serif)) .padding(16) .lineLimit(1...) Spacer() HStack { Button { withAnimation { step -= 1 } } label: { Label("Previous", systemImage: "chevron.backward") } .disabled(step - 1 < 0) Spacer() Button { withAnimation { step += 1 } } label: { Label("Next", systemImage: "chevron.forward") } .disabled(step + 1 >= steps.count) } .buttonStyle(CarouselButtonStyle()) .padding(16) } .foregroundStyle(Color.white) .background(gradient) .ignoresSafeArea(edges: .bottom) } var subtitle: LocalizedStringKey { if step == 0 { return "Ingredients" } return "Step \(step)" } var gradient: AngularGradient { AngularGradient( colors: colors, center: UnitPoint(x: 0.5, y: 1.0), angle: .degrees(180 * Double(step) / Double(steps.count - 1))) } } struct CarouselButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { ZStack { Circle() .fill(.ultraThinMaterial.shadow(.inner( radius: configuration.isPressed ? 3 : 0))) .frame(width: 44, height: 44) configuration.label .labelStyle(.iconOnly) .foregroundStyle(isEnabled ? .black : .secondary) .opacity(configuration.isPressed ? 0.3 : 0.8) } } } let steps = [ """ 2 cups all-purpose flour ¼ teaspoons coarse salt 1 cup (2 sticks) unsalted butter, room temperature ¾ cup confectioners' sugar """, "Sift flour and salt, mix into bowl and set aside.", "Mix butter on high speed until fluffy (3 to 5 minutes).", "Gradually add sugar slowly, continuing to mix until pale and fluffy.", "Add flour all at once and mix until combined.", "Butter a square pan.", "Pat and roll shortbread into pan no more than 1/2-inch thick.", "Refrigerate for at least 30 minutes.", "Preheat oven to 300 F.", "Cut chilled shortbread into squares.", """ Bake until golden and make sure the middle is firm. \ Approximately 45 to 60 minutes. """, "Cool completely. Re-slice them, if necessary, and serve.", ] let colors = [Color.yellow, .red, .purple] struct Biscuits_Previews: PreviewProvider { static var previews: some View { Biscuits() } }
-