2022 EssentialsSwiftUI & UI Frameworks
WWDC22 · 26 min · Essentials / SwiftUI & UI Frameworks
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.
Watch at developer.apple.com ↗Code shown on screen · 5 snippets
Pushable Stack
import SwiftUI
// Pushable stack
struct PushableStack: View {
private var path: [Recipe] = []
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 {
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 {
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()
}
} Multiple Columns
import SwiftUI
// Multiple columns
struct MultipleColumns: View {
private var selectedCategory: Category?
private var selectedRecipe: Recipe?
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 {
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()
}
} Multiple Columns with a Stack
import SwiftUI
// Multiple columns with a stack
struct MultipleColumnsWithStack: View {
private var selectedCategory: Category?
private var path: [Recipe] = []
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 {
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 {
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 {
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()
}
} Use Scene Storage
import SwiftUI
import Combine
import Foundation
// Use SceneStorage to save and restore
struct UseSceneStorage: View {
private var navModel = NavigationModel()
("navigation") private var data: Data?
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 {
var selectedCategory: Category?
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?
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 {
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 {
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()
}
} Biscuits
import SwiftUI
struct Biscuits: View {
private var step = 0
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 {
(\.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()
}
} Resources
Related sessions
-
34 min -
18 min -
14 min -
24 min -
1 min