3

I have an edit view that is presented by a NavigationLink. It takes a recipe, which is held in a manager that has an array of them.

App Code (copy pasta should run):

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

// VIEWS
//
struct ContentView: View {
    @StateObject var recipeManager = RecipeManager()
    
    @State var editingRecipeIndex: Int?
    @State var showEditView = false

    var body: some View {
        NavigationView {
            ZStack {
                if let index = editingRecipeIndex {
                    // THIS LINK SEEMS TO NOT HOOK UP CORRECTLY ***
                    NavigationLink(destination: RecipeEditView(recipe: $recipeManager.recipes[index]), isActive: $showEditView, label: {
                        EmptyView()
                    }).buttonStyle(PlainButtonStyle())
                }
                
                List(recipeManager.recipes, id: \.self) { recipe in
                    NavigationLink(
                        destination: RecipeDetailView(recipe: recipe),
                        label: {
                            Text(recipe.title.isEmpty ? "New Recipe" : recipe.title)
                        })
                }
                .navigationTitle("My Recipes")
                .listStyle(GroupedListStyle())
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: {
                            recipeManager.recipes.append(Recipe())
                            editingRecipeIndex = recipeManager.recipes.count - 1
                            showEditView = true
                        }, label: {
                            Image(systemName: "plus")
                        })
                    }
                }
            }
           
        }
        .environmentObject(recipeManager)
    }
}

struct RecipeDetailView: View {
    var recipe: Recipe
    
    var body: some View {
        VStack {
            Text(recipe.title)
                .font(.title)
                .padding(.top)
            
            Text(recipe.description)
                .fixedSize(horizontal: false, vertical: true)
        }
    }
}

struct RecipeEditView: View {
    @Binding var recipe: Recipe
    
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Form {
            TextField("Enter your recipe title", text: $recipe.title)
            TextField("Enter a description", text: $recipe.description)
            
            Text("Title: \(recipe.title)")
            Text("Description: \(recipe.description)")
            
            Button("Save") {
                presentationMode.wrappedValue.dismiss()
            }
        }
    }
}

// MODELS
//
class RecipeManager: ObservableObject {
    @Published var recipes: [Recipe] = [
        Recipe(title: "one", description: "one-one"),
        Recipe(title: "two", description: "two-two"),
        Recipe(title: "three", description: "three-three")
    ]
}

struct Recipe: Identifiable, Hashable, Equatable {
    let id: UUID
    var imageName: String
    var title: String
    var description: String
    
    var steps: [String]  // [RecipeStep]
    
    static func == (lhs: Recipe, rhs: Recipe) -> Bool {
        return lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    init(id: UUID = UUID(), imageName: String = "croissant", title: String = "", description: String = "", steps: [String] = []) {
        self.id = id
        self.imageName = imageName
        self.title = title
        self.description = description
        self.steps = steps
    }
}

Steps To Reproduce:

  1. Add a new recipe, this should take you to the edit form.
  2. Input a title, followed by a description, note that the binding appears to work for both in this context with the other text fields.
  3. Save, taking you back to the list view.
  4. Tap the new recipe and note that the description is missing.

If I input a description BEFORE the title, both get updated on the model that is bound to the view. However, if I enter the description AFTER the title, only the title is saved. It doesn't seem to matter whether or not I show/hide keyboard, or change the field focus. Even if I add more properties to the Recipe model, the same behavior persists for every field after the title field... help?!

2 Answers 2

2

as you mentioned xcode 13 and the new list binding, try this in ContentView:

            List($recipeManager.recipes, id: \.id) { $recipe in
                NavigationLink(
                    destination: RecipeDetailView(recipe: $recipe),
                    label: {
                        Text(recipe.title.isEmpty ? "New Recipe" : recipe.title)
                    })
            }

and this in RecipeDetailView:

struct RecipeDetailView: View {
     @Binding var recipe: Recipe
     ...
     }

Looks like you are not using ".environmentObject(recipeManager)", so remove it.

Sign up to request clarification or add additional context in comments.

3 Comments

I don't know why everyone suggests this, yet my Xcode 13 does not support providing a binding to List or ForEach (new element syntax). Generic struct 'ForEach' requires that 'Binding<[X]>' conform to 'RandomAccessCollection'
Ask a separate question about your issue, I'm sure we'll be able to help you. Searching SO with the error you describe, should bring up many answers. If you cannot find a suitable answer from those, in your "new" question show us some example code and what type of array you are using in your ForEach. I suspect you are trying to loop over a dictionary.
Xcode 13 RELEASE does not support element binding, despite it being toutet, but Xcode 13 beta 5 does.
0

one key element missing, is your code for the recipe manager. Using the following test code for RecipeManager, I got the TextFields to behave as expected:

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct Recipe: Identifiable, Hashable, Equatable {
    let id: UUID
    var imageName: String
    var title: String
    var description: String
    
    var steps: [String]  // [RecipeStep]
    
    static func == (lhs: Recipe, rhs: Recipe) -> Bool {
        return lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    init(id: UUID = UUID(), imageName: String = "croissant", title: String = "", description: String = "", steps: [String] = []) {
        self.id = id
        self.imageName = imageName
        self.title = title
        self.description = description
        self.steps = steps
    }
}

struct RecipeEditView: View {
    @Binding var recipe: Recipe
    
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Form {
            TextField("Enter your recipe title", text: $recipe.title)
            TextField("Enter a description", text: $recipe.description)
            
            Text("Title: \(recipe.title)")
            Text("Description: \(recipe.description)")
            
            Button("Save") {
                presentationMode.wrappedValue.dismiss()
            }
        }
    }
}

class RecipeManager: ObservableObject {
    @Published var recipes: [Recipe] = [
        Recipe(title: "one", description: "one-one"),
        Recipe(title: "two", description: "two-two"),
        Recipe(title: "three", description: "three-three")
    ]
}

struct ContentView: View {
    @StateObject var recipeManager = RecipeManager()

    var body: some View {
        NavigationView {
            List {
                ForEach(recipeManager.recipes.indices) { index in
                    NavigationLink(recipeManager.recipes[index].title,
                                   destination: RecipeEditView(recipe: $recipeManager.recipes[index]))
                        .tag(index)
                }
            }
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

2 Comments

Thanks! Your code definitely works, I've updated my question with your code, and to include the functionality that I am going for that exhibits the issue. I'm also new to how swiftui wants to handle data, so maybe the way I am trying to add items is just plain incorrect?
After playing with it some more, it also works to just run it in xcode 13 and use the new list binding syntax :\

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.