Search code examples
swiftuiswiftui-navigationlinkswiftui-form

SwiftUI Binding to a list element only updates values input in the right order


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?!


Solution

  • 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.