swiftswiftuiswift-composable-architecture

SwiftUI Composable Architecture Not Updating Views


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
    }
}

Solution

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