Caluru

Caluru - Architecture Guide

This document describes the high-level architecture, design patterns, and key data flows in Caluru.


๐Ÿ“ Architecture Overview

Caluru uses MVVM with SwiftData + CloudKit for persistence and Supabase for Auth and AI proxy. Entry and shell:

Layers:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         SwiftUI Views                           โ”‚
โ”‚  MainTabView, Home, Plan, Scan, Recipes, ShoppingList,          โ”‚
โ”‚  Assistant, Profile, Onboarding (Duolingo-style quiz flow)              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     ViewModels (@Observable)                    โ”‚
โ”‚  Home, Plan, Scan, Recipes, ShoppingList, Assistant,           โ”‚
โ”‚  Profile, Onboarding                                            โ”‚
โ”‚  โ€ข State & navigation                                            โ”‚
โ”‚  โ€ข Data load/save via ModelContext                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Services Layer                          โ”‚
โ”‚  NutritionCalculator, SeedDataService, ShoppingListBuilder      โ”‚
โ”‚  AIAssistantService (AssistantResponseService) โ†’ OpenAIProxy   โ”‚
โ”‚  OpenFoodFactsService, FoodRecognitionService (โ†’ OpenAIProxy)   โ”‚
โ”‚  AuthService, TargetCalculator, MealPlanGenerator              โ”‚
โ”‚  ProgressiveProfilingService, NotificationService               โ”‚
โ”‚  AccountResetService, RecipeService, OnboardingAnalytics       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ               โ–ผ               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ SwiftData        โ”‚ โ”‚ Supabase     โ”‚ โ”‚ External APIs            โ”‚
โ”‚ + CloudKit       โ”‚ โ”‚ Auth +       โ”‚ โ”‚ Open Food Facts         โ”‚
โ”‚ (local + sync)   โ”‚ โ”‚ openai-proxy โ”‚ โ”‚ (barcode)                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐ŸŽจ Design Patterns

1. MVVM (Model-View-ViewModel)

Implementation:

Example:

// View (HomeView.swift)
struct HomeView: View {
    @State private var viewModel = HomeViewModel()
    // View reads from viewModel, calls viewModel methods
}

// ViewModel (HomeViewModel.swift)
@Observable
class HomeViewModel {
    var dailyLog: DailyLog?
    func loadData(context: ModelContext) { ... }
    func addFoodItem(_ food: FoodItem, ...) { ... }
}

2. Dependency Injection via Environment

SwiftDataโ€™s ModelContext is injected via SwiftUIโ€™s environment:

// App entry point
@main
struct CaluruApp: App {
    var body: some Scene {
        WindowGroup {
            MainTabView()
        }
        .modelContainer(sharedModelContainer)  // Injects ModelContext
    }
}

// View accesses it
struct HomeView: View {
    @Environment(\.modelContext) private var modelContext
}

3. Static Services (Utility Pattern)

Services are implemented as classes with static methods (no instances needed):

class NutritionCalculator {
    static func calculate(from log: DailyLog) -> NutritionSummary { ... }
    static func suggestRecipe(...) -> Recipe? { ... }
}

4. Onboarding Gate (CloudKit-Driven)

5. Codable Nested Data

Complex nested data is stored as JSON Data in SwiftData models:

@Model
final class DailyLog {
    var entriesData: Data?  // Stored as JSON
    
    var entries: [MealEntry] {
        get { /* decode from entriesData */ }
        set { /* encode to entriesData */ }
    }
}

๐Ÿ—บ Component Diagram

graph TB
    subgraph App["Caluru App"]
        subgraph Views["Views Layer"]
            MainTab[MainTabView]
            Home[HomeView]
            Plan[WeeklyPlanView]
            Scan[ScanView]
            Recipes[RecipesView]
            RecipeDetail[RecipeDetailView]
            Shopping[ShoppingListView]
            Assistant[AssistantView]
            AddFood[AddFoodSheet]
            Profile[ProfileView]
            Onboarding[OnboardingFlowView]
        end
        
        subgraph ViewModels["ViewModels Layer"]
            HomeVM[HomeViewModel]
            PlanVM[WeeklyPlanViewModel]
            ScanVM[ScanViewModel]
            RecipesVM[RecipesViewModel]
            ShoppingVM[ShoppingListViewModel]
            AssistantVM[AssistantViewModel]
            ProfileVM[ProfileViewModel]
            OnboardingVM[OnboardingViewModel]
        end
        
        subgraph Services["Services Layer"]
            NutritionCalc[NutritionCalculator]
            ShoppingBuilder[ShoppingListBuilder]
            AIAssistant[AIAssistantService]
            OpenAIProxy[OpenAIProxyService]
            OpenFoodFacts[OpenFoodFactsService]
            FoodRecognition[FoodRecognitionService]
            AuthSvc[AuthService]
            TargetCalc[TargetCalculator]
            MealPlanGen[MealPlanGenerator]
            ProgressiveProf[ProgressiveProfilingService]
            NotifSvc[NotificationService]
            SeedData[SeedDataService]
        end
        
        subgraph Data["SwiftData Layer"]
            UserProfile[(UserProfile)]
            FoodItem[(FoodItem)]
            Recipe[(Recipe)]
            DailyLog[(DailyLog)]
            WeeklyPlan[(WeeklyPlan)]
            ShoppingList[(ShoppingList)]
            WeightEntry[(WeightEntry)]
        end
        
        subgraph DesignSystem["Design System"]
            Colors[AppColors]
            Typography[AppTypography]
            Components[UI Components]
        end
    end
    
    MainTab --> Home
    MainTab --> Plan
    MainTab --> Scan
    MainTab --> Recipes
    MainTab --> Profile
    MainTab --> Onboarding
    
    Home --> HomeVM
    Plan --> PlanVM
    Scan --> ScanVM
    Recipes --> RecipesVM
    Shopping --> ShoppingVM
    Assistant --> AssistantVM
    
    HomeVM --> NutritionCalc
    HomeVM --> DailyLog
    HomeVM --> UserProfile
    
    PlanVM --> WeeklyPlan
    PlanVM --> Recipe
    
    ShoppingVM --> ShoppingBuilder
    ShoppingVM --> ShoppingList
    ShoppingVM --> WeeklyPlan
    
    AssistantVM --> AIAssistant
    AIAssistant --> OpenAIProxy
    AssistantVM --> DailyLog
    AssistantVM --> Recipe
    
    ScanVM --> OpenFoodFacts
    ScanVM --> FoodRecognition
    FoodRecognition --> OpenAIProxy
    ScanVM --> FoodItem
    
    ProfileVM[ProfileViewModel] --> AuthSvc
    ProfileVM --> WeightEntry
    OnboardingVM[OnboardingViewModel] --> TargetCalc
    OnboardingVM --> UserProfile
    
    Views --> Components
    Views --> Colors
    Views --> Typography

๐Ÿ”„ Key Data Flows

Flow 1: Adding a Meal (Food Logging)

sequenceDiagram
    participant User
    participant HomeView
    participant HomeVM as HomeViewModel
    participant AddSheet as AddFoodSheet
    participant DailyLog
    participant SwiftData as ModelContext
    
    User->>HomeView: Tap "+" on Desayuno row
    HomeView->>HomeVM: openAddFood(for: .desayuno)
    HomeVM->>HomeVM: showAddFoodSheet = true
    HomeView->>AddSheet: Present sheet
    
    User->>AddSheet: Search & select "Huevo"
    AddSheet->>HomeVM: onAddFood(foodItem, 1.0)
    HomeVM->>HomeVM: Create LoggedItem from FoodItem
    HomeVM->>DailyLog: addItem(loggedItem, to: .desayuno)
    DailyLog->>DailyLog: Encode entries to JSON Data
    HomeVM->>SwiftData: context.save()
    
    Note over HomeView: SwiftUI observes @Observable
    HomeView->>HomeView: UI updates with new totals
    
    Note over HomeView: Progress ring animates<br/>Macro cards update

Flow 2: Generating Shopping List

sequenceDiagram
    participant User
    participant ShoppingView as ShoppingListView
    participant ShoppingVM as ShoppingListViewModel
    participant Builder as ShoppingListBuilder
    participant WeeklyPlan
    participant ShoppingList
    
    User->>ShoppingView: Tap "Actualizar"
    ShoppingView->>ShoppingVM: regenerateFromPlan()
    ShoppingVM->>WeeklyPlan: Fetch current plan
    ShoppingVM->>Builder: build(from: plan)
    
    loop For each day in plan
        Builder->>Builder: Get breakfast/lunch/dinner recipes
        Builder->>Builder: Extract ingredients
        Builder->>Builder: Aggregate by name+unit
    end
    
    Builder-->>ShoppingVM: Return [ShoppingListItem]
    ShoppingVM->>ShoppingList: Update items
    ShoppingVM->>ShoppingView: UI refreshes

๐Ÿงญ Navigation Architecture

Tab-Based Navigation

The app uses a custom TabView with 5 tabs. Shopping List is reached from Plan (e.g. cart icon) or can be a separate tab depending on implementation.

Tab View Icon
Inicio HomeView house.fill
Plan WeeklyPlanView calendar
Scan ScanView (fullScreenCover) camera.fill
Recetas RecipesView book.fill
Perfil ProfileView person.fill

Each tab uses NavigationStack for internal navigation:

struct RecipesView: View {
    var body: some View {
        NavigationStack {
            // Grid of recipes
            .navigationDestination(isPresented: $viewModel.showRecipeDetail) {
                RecipeDetailView(recipe: viewModel.selectedRecipe!)
            }
        }
    }
}

Used for:


๐Ÿ’พ Persistence Strategy

SwiftData Configuration

// CaluruApp.swift
let schema = Schema([
    UserProfile.self,
    FoodItem.self,
    Recipe.self,
    DailyLog.self,
    WeeklyPlan.self,
    ShoppingList.self,
    WeightEntry.self
])

let modelConfiguration = ModelConfiguration(
    schema: schema,
    isStoredInMemoryOnly: false,
    cloudKitDatabase: .private("iCloud.com.agarcia.caluru")
)

Data Storage Location

Seeding Strategy

On first launch, SeedDataService.seedIfNeeded() checks for existing UserProfile:


๐Ÿ” State Management

@Observable Pattern (iOS 17+)

ViewModels use the new @Observable macro:

@Observable
class HomeViewModel {
    var selectedDate: Date = Date()      // Observed by Views
    var dailyLog: DailyLog?              // Observed by Views
    var showAddFoodSheet: Bool = false   // Controls sheet presentation
}

State Ownership

| State Type | Owner | Example | |โ€”โ€”โ€”โ€”|โ€”โ€”-|โ€”โ€”โ€”| | UI State | ViewModel | showAddFoodSheet, selectedCategory | | Domain Data | SwiftData | DailyLog, Recipe | | Navigation | View (@State) | selectedTab, showScanView | | Environment | SwiftUI | modelContext |


๐Ÿ”Œ Dependency Boundaries

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          Views                                  โ”‚
โ”‚  โ€ข Import: SwiftUI, SwiftData                                  โ”‚
โ”‚  โ€ข Depend on: ViewModels, DesignSystem, Models                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        ViewModels                               โ”‚
โ”‚  โ€ข Import: Foundation, SwiftData, SwiftUI                      โ”‚
โ”‚  โ€ข Depend on: Models, Services                                 โ”‚
โ”‚  โ€ข Do NOT import: Views, DesignSystem                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Services                                โ”‚
โ”‚  โ€ข Import: Foundation, SwiftData (where needed)                โ”‚
โ”‚  โ€ข Depend on: Models                                           โ”‚
โ”‚  โ€ข Do NOT import: Views, ViewModels, DesignSystem              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          Models                                 โ”‚
โ”‚  โ€ข Import: Foundation, SwiftData                               โ”‚
โ”‚  โ€ข Depend on: Nothing (pure data)                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โšก Performance Considerations

Lazy Loading

JSON Encoding Overhead

Nested data (entries, ingredients, steps) is encoded/decoded as JSON. For large datasets, consider:

Image Placeholders

Currently uses SF Symbols as placeholders. For real images:


๐Ÿ”ฎ Future Architecture Considerations

Backend Integration (Current)

Modularization

Current structure supports splitting into Swift packages: