This document describes the high-level architecture, design patterns, and key data flows in Caluru.
Caluru uses MVVM with SwiftData + CloudKit for persistence and Supabase for Auth and AI proxy. Entry and shell:
ModelContainer (SwiftData + CloudKit private iCloud.com.agarcia.caluru); injects .modelContainer; applies preferredColorScheme from appTheme; runs AuthService.restoreSession() and setupNotificationsIfNeeded(); uses AppDelegate for UNUserNotificationCenter (foreground presentation, tap handling).MainTabView() (used if you need a single root; otherwise app can point directly to MainTabView).hasCompletedOnboarding is false (derived from profiles.first?.hasCompletedOnboarding after a short CloudKit wait); shows progressive profiling overlay when ProgressiveProfilingService.shared.currentCard is non-nil; runs SeedDataService.seedIfNeeded and profiling triggers on appear.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) โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ
Implementation:
/Views/ folder@Observable classes in /ViewModels/ folder@Model classes in /Models/ folderExample:
// 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, ...) { ... }
}
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
}
Services are implemented as classes with static methods (no instances needed):
class NutritionCalculator {
static func calculate(from log: DailyLog) -> NutritionSummary { ... }
static func suggestRecipe(...) -> Recipe? { ... }
}
@Query private var profiles: [UserProfile] and hasCompletedOnboarding = profiles.first?.hasCompletedOnboarding == true.hasCheckedCloudKit so an existing profile can appear and skip onboarding.OnboardingState); final result is written to UserProfile in SwiftData and onboardingCompletedAt set.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 */ }
}
}
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
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
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
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:
AddFoodSheet - Adding food to mealsRecipePickerSheet - Selecting recipe for meal planAddShoppingItemSheet - Manual item entryAssistantView - Chat interface// 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")
)
~/Library/Application Support/default.store).On first launch, SeedDataService.seedIfNeeded() checks for existing UserProfile:
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 Type | Owner | Example |
|โโโโ|โโ-|โโโ|
| UI State | ViewModel | showAddFoodSheet, selectedCategory |
| Domain Data | SwiftData | DailyLog, Recipe |
| Navigation | View (@State) | selectedTab, showScanView |
| Environment | SwiftUI | modelContext |
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
LazyVGrid for efficient scrollingNested data (entries, ingredients, steps) is encoded/decoded as JSON. For large datasets, consider:
Currently uses SF Symbols as placeholders. For real images:
AsyncImage with cachingAIAssistantService uses OpenAI GPT-4o-mini via Supabase Edge Function openai-proxy; API key server-side only.OpenFoodFactsService; photo via FoodRecognitionService (GPT-4o Vision through same proxy).MockFoodRecognitionService remains available as fallback/offline.Current structure supports splitting into Swift packages:
CaluruCore - Models, ServicesCaluruUI - DesignSystem, ComponentsCaluruApp - Views, ViewModels