Modern SwiftUI Data Architecture: Decoupling Presentation State with Subscripts & Value Sets

Published by malhal on

When building SwiftUI applications, it is easy to default to adding mutable boolean flags onto domain models. On paper, adding var isFavorite: Bool to a Recipe struct feels natural. In practice, it entangles domain content with user presentation state, triggers unnecessary view invalidations, and complicates syncing with persistent storage.

Here is an architectural walkthrough of how shifting from mutable struct mutation to a Set-based subscript pattern cleanly decouples state, optimizes performance, and drastically simplifies SwiftUI bindings.

The Legacy Pattern: Mutating Value Types

In a traditional implementation, favoriting a recipe required mutating the Recipe value struct directly:

// ❌ Legacy Approach: Mutating domain structs for presentation state
struct Recipe: Identifiable {
    let id: UUID
    var title: String
    var isFavorite: Bool // Entangling presentation state with content
}

class RecipeBox: ObservableObject {
    @Published var allRecipes: [Recipe] = []

    func toggleIsFavorite(_ recipe: Recipe) {
        var recipeToUpdate = recipe
        recipeToUpdate.isFavorite.toggle()
        update(recipeToUpdate) // Forces full struct array re-indexing & updates
    }
}

Why This Caused Headaches

  1. Model Mutation Overhead: Toggling a favorite required searching allRecipes for the index, mutating the struct, and replacing it in the array.
  2. Persistence Pollution: Saving a recipe payload to disk or an API required sending or serializing the entire recipe JSON object just to change a boolean flag.
  3. Multi-User / Shared Data Conflicts: If the recipe data is fetched from an external API or shared database, treating isFavorite as an intrinsic property of the recipe breaks down per user.

The Refactored Architecture: Normalized Sets + Subscripts

Instead of mutating Recipe, we decouple “Favorites” into a dedicated set of identifiers on the store (Set<Recipe.ID>). We then expose this collection via a custom model subscript.

final class RecipeBox: ObservableObject {
    @Published var allRecipes: [Recipe] = []
    @Published var favourites: Set<Recipe.ID> = []

    // 💡 O(N) linear filter driven by O(1) Set membership checks
    func favoriteRecipes() -> [Recipe] {
        return allRecipes.filter { favourites.contains($0.id) }
    }
    
    // 💡 Subscript providing direct read/write access to Set membership
    subscript(isFavorite id: Recipe.ID) -> Bool {
        get {
            favourites.contains(id)
        }
        set {
            if newValue {
                favourites.insert(id)
            } else {
                favourites.remove(id)
            }
        }
    }
}

By adding a getter and setter to subscript(isFavorite:), the store becomes the single source of truth for membership, handling insertion and removal cleanly behind a simple boolean interface.

Seamless Integration Across SwiftUI

This single subscript unifying getter/setter access simplifies view implementations across all UI entry points.

1. Sheet Creation & Auto-Categorization

When adding a new recipe while looking at the Favorites context, we assign favorite status immediately after creation using the subscript—without needing to modify the newly created Recipe struct itself:

.sheet(isPresented: $isPresented) {
    RecipeEditor(config: $recipeEditorConfig) {
        // Add recipe to store and receive generated ID
        let recipeID = recipeBox.add(recipeEditorConfig.recipe)
        
        switch selectedSidebarItem {
        case .favorites:
            // 💡 Associate the recipe to favorites cleanly via subscript assignment
            recipeBox[isFavorite: recipeID] = true
        default:
            break
        }
    }
}

2. Swipe Actions in Lists

Inside list rows, toggling favorite status becomes a single inline line of code:

struct RecipeListView: View {
    @Binding var selection: Recipe.ID?
    let selectedSidebarItem: SidebarItem
    @EnvironmentObject private var recipeBox: RecipeBox
    
    var body: some View {
        List(recipes, selection: $selection) { recipe in
            NavigationLink(value: recipe.id) {
                RecipeListItemView(recipe: recipe)
            }
            .swipeActions(allowsFullSwipe: false) {
                Button(role: .destructive) {
                    withAnimation {
                        recipeBox.delete(recipe)
                    }
                } label: {
                    Image(systemName: "trash")
                }
                
                Button {
                    // 💡 Direct toggle on the subscript getter/setter
                    recipeBox[isFavorite: recipe.id].toggle()
                } label: {
                    Image(systemName: "heart")
                }
            }
        }
    }
}

3. Native Toolbar Toggle Bindings

Because SwiftUI projects subscript operations as projections, $recipeBox[isFavorite: recipe.id] produces a native Binding<Bool> for Toggle views without requiring custom Binding(get:set:) wrappers:

struct RecipeDetailToolbar: ToolbarContent {
    @EnvironmentObject var recipeBox: RecipeBox
    @Binding var recipe: Recipe
    @Binding var showDeleteConfirmation: Bool
    let deleteRecipe: () -> Void

    var body: some ToolbarContent {
        ToolbarItem(placement: .primaryAction) {
            // 💡 Directly projects a Binding<Bool> into the Toggle
            Toggle(isOn: $recipeBox[isFavorite: recipe.id]) {
                Image(systemName: recipeBox[isFavorite: recipe.id] ? "heart.fill" : "heart")
            }
        }
    }
}

4. Conditional UI Rendering

Checking favorite status for icons or conditional views requires zero array searches:

Swift

if recipeBox[isFavorite: recipe.id] {
    Image(systemName: "heart")
        .symbolVariant(.fill)
}

Key Benefits of This Architecture

MetricLegacy Mutation PatternRefactored Subscript Pattern
Data IntegrityHigh risk of state duplication/out-of-sync copiesSingle source of truth (Set<Recipe.ID>)
Lookup Efficiency$O(N)$ linear array search to update struct$O(1)$ set membership insertion/deletion
SwiftUI BindingsRequires custom store methods or mutating callsNatural $store[isFavorite: id]projection
PersistenceMust re-encode full recipe modelsPersists lightweight array of string/UUID keys

Conclusion

By extracting presentation state off the Recipe struct into a normalized Set<Recipe.ID> and wrapping it in a custom subscript, we achieved cleaner separation of concerns, higher performance, and expressive SwiftUI bindings across toolbars, swipe actions, and modal sheets.

Categories: SwiftUI