Turn Your Ingredients Into Delicious Meals
Recipefy is an iOS app that uses AI-powered image recognition to identify ingredients from photos and generate personalized recipe suggestions. Simply snap a photo of your fridge, pantry, or ingredients, and let Recipefy do the rest. This app is developed for 67-443 Mobile App Development at Carnegie Mellon University, Fall 2025.
- πΈ Multi-Image Scanning β Capture up to 5 photos per session (fridge, pantry, countertop)
- π€ AI Ingredient Recognition β Powered by Google's Gemini 2.5 Flash for accurate identification
- βοΈ Ingredient Management β Add, edit, or delete ingredients manually after scanning using intuitive IOS UI. Or start with a blank slate and add ingredients manually.
- π½οΈ Smart Recipe Generation β Get 3 unique recipes based on your available ingredients initially; request more batches as needed
- π Recipe Detail View β Tabbed interface showing Ingredients, Steps, and Nutrition breakdown
- β€οΈ Favorites β Save and organize your favorite recipes
- π€ User Profiles β Edit display name, email, and password; track usage stats
- Diet Types β Vegetarian, Vegan, Pescatarian, Gluten-Free, Dairy-Free, Low-Carb
- Allergen Management β Peanuts, Tree Nuts, Shellfish, Fish, Eggs, Dairy, Gluten, Soy, Sesame
- Food Dislikes β Exclude specific ingredients you don't enjoy
- Cooking Time Limits β Set maximum preparation time for recipes
- Email/Password β Traditional account creation with password reset
- Sign in with Apple β Native Apple authentication with secure nonce
- Google Sign-In β OAuth-based Google account integration
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | SwiftUI (iOS 26) | Declarative, modern iOS interface |
| Language | Swift 5.0 | Type-safe, modern programming language |
| Architecture | MVVM + Controllers | Separation of concerns, testability |
| AI/ML | Firebase AI (Gemini 2.5 Flash) | Ingredient recognition, recipe generation |
| Authentication | Firebase Auth | Multi-provider auth (Email, Apple, Google) |
| Database | Cloud Firestore | Real-time NoSQL document storage |
| Storage | Firebase Storage | Image hosting for scanned ingredients |
| Testing | Swift Testing Framework | Modern, declarative test assertions |
firebase-ios-sdk 12.4.0 β Core Firebase services
GoogleSignIn-iOS 9.0.0 β Google authentication
swift-protobuf 1.32.0 β Protocol buffer support
CryptoKit (native) β SHA256 for Apple Sign-In nonce
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SwiftUI Views β
β (HomeView, ScanView, RecipeView, SettingsView, etc.) β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β @EnvironmentObject
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββ
β Controllers β
β AuthController β ScanController β IngredientController β β
β β RecipeController β NavigationState β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β Protocol-based DI
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββ
β Service Layer β
β GeminiService β FirebaseFirestoreService β StorageService β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββ
β Firebase Backend β
β Firestore β Auth β Storage β AI (Gemini) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All external services are abstracted behind protocols, enabling:
- Testability β Mock implementations for unit testing
- Flexibility β Swap implementations without changing business logic
- Decoupling β Controllers don't depend on concrete Firebase classes
protocol GeminiServiceProtocol {
func analyzeIngredients(image: UIImage) async throws -> [Ingredient]
func getRecipe(ingredients: [String]) async throws -> [Recipe]
}
protocol FirestoreServiceProtocol {
func saveIngredients(scanId: String, ingredients: [Ingredient]) async throws -> [Ingredient]
func loadRecipes(userId: String) async throws -> (recipes: [Recipe], scanId: String?)
// ... more operations
}All controllers and view-related classes are marked with @MainActor to ensure UI updates happen on the main thread:
@MainActor
final class RecipeController: ObservableObject {
@Published var currentRecipes: [Recipe]?
@Published var isRetrieving = false
// ...
}Rationale: Swift's strict concurrency model requires explicit main-thread guarantees for @Published properties. Using @MainActor at the class level eliminates race conditions and makes the code safer.
Controllers are shared across the app using SwiftUI's environment:
@main
struct RecipefyApp: App {
@StateObject private var authController = AuthController()
@StateObject private var scanController = ScanController(...)
@StateObject private var ingredientController = IngredientController(...)
@StateObject private var recipeController = RecipeController(...)
var body: some Scene {
WindowGroup {
NavigationBarView()
.environmentObject(authController)
.environmentObject(scanController)
.environmentObject(ingredientController)
.environmentObject(recipeController)
}
}
}Rationale: This ensures a single source of truth across all views and tabs, enabling seamless data flow when navigating between screens.
The app maintains data association between scans and their generated content:
Scan (images) β ScanController.currentScanId
β
Ingredients β IngredientController.currentScanId
β
Recipes β RecipeController.lastGeneratedScanId
Rationale: This ensures recipes are never mixed from different scanning sessions, preventing confusing user experiences.
AI responses can be unpredictable. The IngredientCategory enum includes fuzzy matching:
static func from(string: String) -> IngredientCategory {
let normalized = string.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
switch normalized {
case "vegetable", "vegetables", "veggie", "veggies", "veg":
return .vegetables
case "protein", "proteins", "meat", "meats":
return .proteins
// ... more variations
default:
return .other
}
}Rationale: Gemini might return "Veggies" instead of "Vegetables". Graceful fallback prevents crashes and ensures all ingredients are categorized.
Multiple images are analyzed concurrently using Swift's structured concurrency:
let allIngredients = try await withThrowingTaskGroup(of: [Ingredient].self) { group in
for image in images {
group.addTask {
try await self.geminiService.analyzeIngredients(image: image)
}
}
var results: [Ingredient] = []
for try await ingredients in group {
results.append(contentsOf: ingredients)
}
return results
}Rationale: Users can scan fridge + pantry + countertop. Processing them in parallel reduces wait time from ~30s to ~10s for 3 images.
User preferences are injected directly into AI prompts:
func toPromptString() -> String {
var prompt = "\n\nUSER DIETARY PREFERENCES:\n"
if !dietTypes.isEmpty {
prompt += "- Diet Types: \(dietTypes.map { $0.displayName }.joined(separator: ", "))\n"
}
if !allergies.isEmpty {
prompt += "- ALLERGIES (CRITICAL - MUST AVOID): \(allergies.map { $0.rawValue }.joined(separator: ", "))\n"
}
prompt += "\nIMPORTANT: Generate recipes that strictly respect these dietary constraints.\n"
return prompt
}Rationale: This approach leverages Gemini's instruction-following capabilities rather than post-filtering recipes, resulting in more relevant suggestions.
Development on simulators is enabled through mock camera support:
if SimulatorCameraSupport.isRunningOnSimulator {
capturedImage = SimulatorCameraSupport.generateMockIngredientImage()
return
}Rationale: Camera hardware is unavailable on simulators. Mock images allow full flow testing without a physical device.
Cold start latency is reduced by warming up Firestore on app launch:
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
FirebaseApp.configure()
// Pre-warm Firestore connection
Task {
let db = Firestore.firestore()
_ = try? await db.collection("_warmup").document("ping").getDocument()
}
return true
}Rationale: First Firestore request can take 2-3 seconds for connection setup. Pre-warming hides this latency behind the splash screen.
Ingredients are saved using batched writes for atomicity and performance:
func saveIngredients(scanId: String, ingredients: [Ingredient]) async throws -> [Ingredient] {
let batch = db.batch()
for ingredient in ingredients {
let docRef = ingredientsCollection.document()
batch.setData(ingredientData, forDocument: docRef)
}
try await batch.commit()
return ingredientsWithIds
}Rationale: A single batch commit is faster and ensures all-or-nothing saves, preventing partial data states.
Recipes are generated in batches of 3 to minimize wait time:
func loadMoreRecipesIfNeeded() async {
guard !isRetrieving, !isLoadingMore, !lastFormattedIngredients.isEmpty else { return }
isLoadingMore = true
let moreRecipes = try await geminiService.getRecipe(ingredients: lastFormattedIngredients)
currentRecipes?.append(contentsOf: moreRecipes)
isLoadingMore = false
}Rationale: AI recipe generation takes time. Generating 3 recipes initially provides fast results, and users who want more variety can request additional batches on demand rather than waiting upfront for a large set.
Automatic ingredient deduplication was initially planned but intentionally omitted after testing.
Rationale: Testing revealed that automatic deduplication was overly aggressive β unique items with that happened to be in the same scan upload were incorrectly merged. Users found it easier to quickly swipe-to-delete duplicates than to re-add ingredients that were incorrectly removed. This trade-off prioritizes data accuracy over convenience.
Recipefy/
βββ Recipefy/
β βββ RecipefyApp.swift # App entry point & DI setup
β βββ AppDelegate.swift # Firebase initialization
β β
β βββ Models/
β β βββ User.swift # AppUser with auth providers
β β βββ Recipe.swift # Recipe with nutrition data
β β βββ Ingredient.swift # Ingredient with categories
β β βββ Scan.swift # Image scan metadata
β β βββ DietaryPreferences.swift # Diet types, allergies, dislikes
β β βββ MeasurementUnit.swift # Standardized units
β β
β βββ Controllers/
β β βββ AuthController.swift # Authentication state & operations
β β βββ ScanController.swift # Image capture & upload
β β βββ IngredientController.swift # AI analysis & CRUD
β β βββ RecipeController.swift # Recipe generation & favorites
β β βββ NavigationState.swift # Tab selection state
β β
β βββ Data/
β β βββ GeminiService.swift # AI ingredient/recipe analysis
β β βββ GeminiServiceProtocol.swift
β β βββ FirebaseFirestoreService.swift
β β βββ FirestoreServiceProtocol.swift
β β βββ FirebaseStorageService.swift
β β βββ StorageService.swift # Protocol for storage
β β βββ FirebaseScanRepository.swift
β β βββ ScanRepository.swift # Protocol for scan persistence
β β βββ FireStorePaths.swift # Firestore path constants
β β
β βββ Views/
β β βββ LandingView.swift # Onboarding screen
β β βββ AuthView.swift # Login/signup UI
β β βββ HomeView.swift # Dashboard with quick actions
β β βββ NavigationBarView.swift # Tab bar container
β β βββ ScanView.swift # Camera interface
β β βββ ReviewScansView.swift # Photo review before analysis
β β βββ IngredientListView.swift # Detected ingredients
β β βββ IngredientFormView.swift # Add/edit ingredient
β β βββ RecipeView.swift # Recipe generation trigger
β β βββ RecipeCardsView.swift # Swipeable recipe cards
β β βββ RecipeDetailView.swift # Full recipe view
β β βββ FavoriteRecipesView.swift # Saved recipes
β β βββ SettingsView.swift # Profile & preferences
β β βββ EditProfileView.swift # Name, email, password
β β βββ PreferencesView.swift # Dietary settings
β β βββ EmptyStateView.swift # Reusable empty state
β β βββ Camera/
β β β βββ CameraManager.swift # AVFoundation wrapper
β β β βββ CameraPreviewView.swift
β β β βββ CameraOverlays.swift # Grid overlay
β β β βββ SimulatorCameraSupport.swift
β β βββ ... (Help, Privacy, Terms views)
β β
β βββ Helpers/
β β βββ AppleSignInHelper.swift # Nonce generation, SHA256
β β
β βββ Assets.xcassets/ # App icons, images
β
βββ RecipefyTests/ # Unit tests
β βββ RecipeControllerTests.swift
β βββ IngredientControllerTests.swift
β βββ AuthControllerErrorTests.swift
β βββ DietaryPreferencesTests.swift
β βββ CameraTests.swift
β βββ ... (18 test files)
β
βββ RecipefyUITests/ # UI tests
βββ RecipefyUITests.swift
βββ RecipefyUITestsLaunchTests.swift
- Xcode 26.0+
- iOS 26.0+ device or simulator
- Firebase project with:
- Authentication (Email, Apple, Google)
- Cloud Firestore
- Firebase Storage
- Firebase AI (Gemini API enabled)
-
Clone the repository
git clone https://github.com/your-org/Recipefy.git cd Recipefy -
Open the Xcode project
open Recipefy/Recipefy.xcodeproj
-
Configure Firebase
- Create a Firebase project at console.firebase.google.com
- Download
GoogleService-Info.plist - Replace the existing plist in
Recipefy/Recipefy/ - Enable Authentication providers (Email, Apple, Google)
- Enable Cloud Firestore and Storage
- Enable Gemini API in Firebase AI
-
Configure Google Sign-In
- Add the reversed client ID from
GoogleService-Info.plistto URL schemes inInfo.plist
- Add the reversed client ID from
-
Configure Sign in with Apple
- Add "Sign in with Apple" capability in Xcode
- Configure in Apple Developer portal
-
Build and Run
- Select your target device or simulator
- Press
βRto build and run
The project uses Swift's modern Testing framework with 240+ unit tests across 18 test files, implementing mock-based dependency injection for fast, deterministic testing.
Test your code, not the SDK. We trust that Firebase and Gemini work β our tests verify our business logic.
All tests are:
- Fast β No network calls; all tests run in under 5 seconds
- Deterministic β Same input always produces same output
- Independent β Each test can run alone without dependencies
| Category | Coverage | Files |
|---|---|---|
| Models | 95-100% | Recipe, Ingredient, Scan, User, DietaryPreferences, MeasurementUnit |
| Controllers | 75-85% | RecipeController, IngredientController, ScanController, NavigationState |
| Helpers/Utils | 95-100% | AppleSignInHelper, FireStorePaths |
| View Logic | 80-90% | Validation logic, data formatting, share text generation |
| Camera State | 90%+ | CameraManager state machine, simulator detection |
Why these are tested:
- Models contain core business logic used throughout the app
- Controller business logic is tested via dependency injection with mocks
- Pure helper functions are easy to test with high value
- View validation logic is critical for data integrity
| Category | Reason |
|---|---|
| Firebase SDK Calls | Would require mocking entire Firebase SDK; slow and brittle; Firebase is already tested by Google |
| Gemini AI API Calls | Non-deterministic AI responses; costs money per call; requires API keys; response parsing IS tested with mock data |
| SwiftUI View Rendering | Unit tests don't render UI; this is covered by UI tests and manual QA |
| App Lifecycle | Thin wrappers around Firebase/SwiftUI initialization; rarely changes |
Controllers accept protocols, not concrete types, enabling mock injection in tests:
// Protocol defines the contract
protocol GeminiServiceProtocol {
func analyzeIngredients(image: UIImage) async throws -> [Ingredient]
func getRecipe(ingredients: [String]) async throws -> [Recipe]
}
// Production uses real service
let controller = RecipeController(
geminiService: GeminiService(), // Real AI
firestoreService: FirebaseFirestoreService() // Real database
)
// Tests use mock service
let controller = RecipeController(
geminiService: MockGeminiService(), // Returns test data
firestoreService: MockFirestoreService() // No network calls
)class MockGeminiService: GeminiServiceProtocol {
var mockRecipes: [Recipe] = []
var shouldThrowError = false
func getRecipe(ingredients: [String]) async throws -> [Recipe] {
if shouldThrowError { throw GeminiError.noResponse }
return mockRecipes
}
}RecipefyTests/
βββ Models
β βββ RecipeTests.swift (14 tests)
β βββ IngredientTests.swift (12 tests)
β βββ ScanTests.swift (11 tests)
β βββ AppUserTests.swift (8 tests)
β βββ DietaryPreferencesTests.swift (33 tests)
β βββ MeasurementUnitTests.swift (10 tests)
βββ Controllers
β βββ RecipeControllerTests.swift (27 tests)
β βββ IngredientControllerTests.swift (17 tests)
β βββ ScanControllerTests.swift (12 tests)
β βββ NavigationStateTests.swift (12 tests)
β βββ AuthControllerErrorTests.swift (10 tests)
βββ Views
β βββ HomeViewTests.swift (6 tests)
β βββ AuthViewTests.swift (8 tests)
β βββ IngredientFormViewTests.swift (9 tests)
β βββ RecipeDetailViewTests.swift (11 tests)
βββ Helpers
β βββ AppleSignInHelperTests.swift (15 tests)
β βββ FirestorePathsTests.swift (5 tests)
βββ Camera
βββ CameraTests.swift (22 tests)
In Xcode, press βU to run all tests, or use the Test Navigator (β6) to run individual test files or methods.
| Scope | Target |
|---|---|
| Models | 95-100% |
| Business Logic | 85-95% |
| Overall | 85-90% |
Note: 100% coverage is not the goal β untested Firebase/Gemini wrappers are intentional. Testing SDK calls adds complexity without value.
| Model | Key Properties | Purpose |
|---|---|---|
AppUser |
uid, email, displayName, authProvider, photoURL | Firebase Auth user with provider tracking |
Ingredient |
name, quantity, unit, category | Scanned ingredient with categorization |
IngredientCategory |
vegetables, proteins, grains, dairy, seasonings, oil, other | Enum for ingredient classification |
Recipe |
title, ingredients, steps, calories, protein, carbs, fat, fiber, sugar, favorited | AI-generated recipe with full nutrition |
Scan |
userId, imagePaths, status, createdAt | Image upload metadata and processing status |
DietaryPreferences |
dietTypes, allergies, dislikes, maxCookingTime | User dietary constraints for AI prompts |
users/
βββ {userId}/
βββ displayName, email, photoURL, authProvider
βββ preferences/
βββ dietary/
βββ dietTypes: [String]
βββ allergies: [String]
βββ dislikes: [String]
βββ maxCookingTime: Int
scans/
βββ {scanId}/
βββ userId, imagePaths, status, createdAt
βββ ingredients/
βββ {ingredientId}/
βββ name, quantity, unit, category
recipes/
βββ {recipeId}/
βββ title, description, ingredients, steps
βββ calories, servings, cookMin
βββ protein, carbs, fat, fiber, sugar
βββ createdBy, sourceScanId, favorited
βββ createdAt
- Color Theme β Green accent (
#5CB85C) representing fresh, healthy cooking - Dark & Light Mode β Full support for both system appearances
- Typography β SF Pro with semantic sizing for hierarchy
- Navigation β 5-tab structure (Home, Ingredients, Scan, Recipes, Settings)
- Empty States β Informative messages with relevant icons
- Loading States β Progress indicators with status text
- Cards β Rounded corners, subtle shadows, gesture-friendly
- Streak Honey
- Jonass Oh
- Abdallah Abdaljalil
- Yuqi Zou





