SwiftUI Composable Architecture Not Updating Views

871 Views Asked by At

I am using The Composable Architecture for a SwiftUI app, and I am facing issues with the view not updating when my store's state changes.

My screen flow is as follows:

RootView -> RecipeListView -> RecipeDetailView --> RecipeFormView (presented as a sheet).

The RecipeListView has a list of recipes. Tapping on the list item shows the RecipeDetailView.

This detail view can present another view (as a sheet), which allows you to edit the recipe.

The issue is that after editing the recipe, the view does not update when I dismiss the sheet and go back. None of the views in the hierarchy update show the updated recipe item.

I can confirm that the state of the store has changed. This is the feature code:

import Combine
import ComposableArchitecture

struct RecipeFeature: ReducerProtocol {
    
    let env: AppEnvironment
    
    struct State: Equatable {
        var recipes: [Recipe] = []
    }
    
    enum Action: Equatable {
        case onAppear
        case recipesLoaded([Recipe])
        case createOrUpdateRecipe(Recipe)
    }
    
    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case.onAppear:
            let recipes = env.recipeDB.fetchRecipes()
            return .init(value: .recipesLoaded(recipes))
            
        case .recipesLoaded(let recipes):
            state.recipes = recipes
            return .none
            
        case .createOrUpdateRecipe(let recipe):
            if let index = state.recipes.firstIndex(of: recipe) {
                state.recipes[index].title = recipe.title
                state.recipes[index].caption = recipe.caption
            }
            else {
                state.recipes.append(recipe)
            }
            return .none
        }
    }
}

typealias RecipeStore = Store<RecipeFeature.State, RecipeFeature.Action>

The createOrUpdateRecipe action does not work in the case of editing a recipe. When adding a new recipe, the views update successfully.

If I assign a new ID as follows:

            if let index = state.recipes.firstIndex(of: recipe) {
                state.recipes[index].id = UUID() // <-- this will trigger a view update
                state.recipes[index].title = recipe.title
                state.recipes[index].caption = recipe.caption
            }

The RecipeListView will update, but the RecipeDetailView` still does not update. Also, I don't want to assign a new ID just to trigger an update, since this action is simply updating the recipe info for an existing recipe object.

Edit:

This is the view place where the action is invoked in RecipeFormView (lots of UI not shown to save space):

struct RecipeFormView: View {
    
    let store: RecipeStore
    
    // Set to nil if creating a new recipe
    let recipe: Recipe?
    
    @StateObject var viewModel: RecipeFormViewModel = .init()
    
    @EnvironmentObject var navigationVM: NavigationVM
    
    var body: some View {
        WithViewStore(store) { viewStore in
            VStack {
                titleContent
                    .font(.title)
                ScrollView {
                    VStack(alignment: .leading, spacing: SECTION_SPACING) {
                        // LOTS OF OTHER UI.....
                        
                        // Done button
                        ZStack(alignment: .center) {
                            Color.cyan
                            VStack {
                                Image(systemName: "checkmark.circle.fill")
                                    .resizable()
                                    .frame(width: 25, height: 25)
                                    .foregroundColor(.white)
                            }
                            .padding(10)
                        }
                        .onTapGesture {
                            let recipe = viewModel.createUpdatedRecipe()
                            viewStore.send(RecipeFeature.Action.createOrUpdateRecipe(recipe))
                            navigationVM.dismissRecipeForm()
                        }
                    }
                    .padding(16)
                }
                .textFieldStyle(.roundedBorder)
            }
        }
        .onAppear() {
            self.viewModel.setRecipe(recipe: self.recipe)
        }
    }
}

The view model just does some validation and creates a new Recipe object with the updated properties.

This is the Recipe model:

struct Recipe: Identifiable, Equatable, Hashable
{
    var id: UUID = UUID()
    var title: String
    var caption: String
    var ingredients: [Ingredient]
    var directions: [String]
    
    static func == (lhs: Recipe, rhs: Recipe) -> Bool {
        return lhs.id == rhs.id
    }
}
1

There are 1 best solutions below

1
On BEST ANSWER

The view doesn't update because your Recipe is (as of per your definition) still equal to the old one. You're just comparing the id.

Change the Recipe struct like this:

struct Recipe: Identifiable, Equatable, Hashable {
    var id: UUID = UUID()
    var title: String
    var caption: String
    var ingredients: [Ingredient]
    var directions: [String]

    static func == (lhs: Recipe, rhs: Recipe) -> Bool {
        return lhs.id == rhs.id 
        && lhs.title == rhs.title
        && lhs.caption == rhs.caption
    }
}