title: "WWDC22: The SwiftUI cookbook for navigation | Apple"
Youtube_Thumbnail: https://img.youtube.com/vi/6Rp71CvKIKs/hqdefault.jpg
sources: https://www.youtube.com/watch?v=6Rp71CvKIKs
media_link: https://www.youtube.com/watch?v=6Rp71CvKIKs
contentPublished: 2024-09-24
noteCreated: 2025-07-11
description: 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 ...
tags:
- clippings
- video
takeaways:
subjects:
Status: ✅ Read
publish: true
Youtube_Duration: 26:06The 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 ...
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()
}
}
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()
}
}
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()
}
}
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()
}
}
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()
}
}