Modern SwiftUI Data Architecture: Decoupling Presentation State with Subscripts & Value Sets
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
- Model Mutation Overhead: Toggling a favorite required searching
allRecipesfor the index, mutating the struct, and replacing it in the array. - 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.
- Multi-User / Shared Data Conflicts: If the recipe data is fetched from an external API or shared database, treating
isFavoriteas 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
| Metric | Legacy Mutation Pattern | Refactored Subscript Pattern |
| Data Integrity | High risk of state duplication/out-of-sync copies | Single source of truth (Set<Recipe.ID>) |
| Lookup Efficiency | $O(N)$ linear array search to update struct | $O(1)$ set membership insertion/deletion |
| SwiftUI Bindings | Requires custom store methods or mutating calls | Natural $store[isFavorite: id]projection |
| Persistence | Must re-encode full recipe models | Persists 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.