Back to blog

Engineering article

Bringing Apple Foundation Models into FoodieCalc

How FoodieCalc uses Apple's Foundation Models framework for private, structured, on-device nutrition help without turning food logging into a black box.

FoodieCalcFoundation ModelsApple IntelligenceSwiftUIOn-device AI
Foundation Models Framework graphic

Apple Foundation Models in FoodieCalc

Foundation Models Framework graphic.

FoodieCalc started as a nutrition app I wanted for myself: fast logging, native iOS feel, real data, clean design, and the least possible friction between “I ate this” and “my diary is correct.”

Apple Foundation Models changed the shape of what I could build.

Not because I wanted to add AI for the sake of adding AI. I wanted FoodieCalc to understand the kind of sentence a person actually writes:

2 eggs, 200g rice, and a coffee

That is not a search query. It is a messy human shortcut. A normal nutrition app often makes you split that sentence into separate searches, pick results, choose servings, adjust grams, repeat. FoodieCalc can now use Apple’s Foundation Models framework to turn that sentence into structured food items, then let the real app do the serious work.

The decision FoodieCalc uses Apple Foundation Models as a local language layer, not as the source of truth. The model helps parse, estimate, and explain. The app still owns matching, nutrition data, review, and saving.

Why Foundation Models felt right for this app

Food logging is personal. It contains routines, goals, weight plans, and daily habits. I did not want the first version of FoodieCalc’s AI features to depend on sending that text to a random online AI service.

Apple’s Foundation Models framework was a better fit for the product I wanted to build because it gives iOS apps a native Swift API for language tasks, built around Apple Intelligence availability and privacy-oriented system behavior.

Foundation Models network mark on a white background. Apple Intelligence style mark on a colorful gradient.

The big product advantages were:

  • Private by default: FoodieCalc’s AI features are designed around Apple’s local Foundation Models path when available.
  • Native Swift integration: The implementation lives in a Swift Package and uses typed responses instead of fragile JSON strings.
  • Availability aware: The app checks whether the system model is actually ready before showing or using AI features.
  • No extra AI backend for this layer: The app can offer smart parsing and insights without introducing a separate hosted LLM service for user food text.

That last point matters. FoodieCalc still uses real nutrition data sources where they belong, including FatSecret through the app’s nutrition data path. But natural-language understanding does not need to leak into an external AI provider.

The architecture: one Swift Package for on-device AI

I did not put Foundation Models calls directly inside random screens.

FoodieCalc has a dedicated base package:

FCOnDeviceAI
  Availability
  Models
  Session

That package owns:

  • FCAIAvailability
  • FCAIFoodParser
  • FCAINutritionInsightService
  • FCAINutritionEstimationService
  • the @Generable response models
  • the model session lifecycle

This boundary made the rest of the app cleaner. Feature packages can ask for an AI parser or estimator, but they do not need to know how LanguageModelSession is built, prewarmed, reset, or released.

It also makes the feature easier to reason about later. If I improve prompts, change session strategy, add more structured fields, or gate a capability differently, the work stays in one place.

Availability comes first

The app never assumes Apple Intelligence is ready.

FoodieCalc wraps the system check in FCAIAvailability:

public static var isAvailable: Bool {
    SystemLanguageModel.default.isAvailable
}

It also maps the unavailable reasons:

case appleIntelligenceNotEnabled
case deviceNotEligible
case modelNotReady
case unknown

That sounds small, but it changes the product behavior. AI is not a promise the app cannot keep. If the model is unavailable, the feature can stay hidden, disabled, or fail with a useful message instead of letting the user hit a broken path.

The most important rule: the model only parses

The AI Quick Log flow begins with FCAIFoodParser.

Its job is intentionally narrow:

parse(_ text: String) async throws -> [FCAIParsedFoodItem]

The parser does not log food. It does not decide calories. It does not convert portions into nutrition. It does not invent missing ingredients.

It extracts this structure:

@Generable(description: "One food the person explicitly mentioned eating")
public struct FCAIParsedFoodItem: Sendable {
    public var mention: String
    public var name: String
    public var quantity: Double
    public var unit: FCAIFoodUnit
}

That mention field is one of my favorite details. It copies the user’s original words into the generated structure. That gives the app a grounding field: the model has to connect each output back to text the user actually wrote.

There is also a strict instruction in the parser:

Only include foods the person explicitly mentioned.
Never add, infer, guess, or invent a food, side, drink, or ingredient.
Do not convert amounts to grams.

That is a deliberate product decision. I want AI to remove friction, not silently make nutrition decisions for the user.

Greedy parsing for consistency

For the parser, FoodieCalc uses greedy sampling:

private static let generationOptions = GenerationOptions(sampling: .greedy)

In a creative writing feature, randomness can be fun.

In a nutrition logging feature, randomness is a bug.

If the user writes the same meal twice, FoodieCalc should not parse it differently just because the model felt more imaginative the second time. Greedy sampling keeps the extraction path more predictable and reduces run-to-run variation.

The resolver is where the app takes control

After parsing, FoodieCalc does not immediately save anything.

It resolves every parsed item through a priority chain:

FoodieCalc AI Quick Log flow from natural text to review.

The resolver checks:

  • local custom foods
  • recipes
  • saved meals
  • the nutrition database
  • an on-device AI estimate only if no match is found

That priority is important. Your own library should beat the web. A saved recipe should beat a generic guess. Real nutrition data should beat model estimation. AI estimation is useful as a fallback, but it should not override known data.

The code expresses that clearly in FCAIQuickLogResolver: library first, database next, on-device estimate last.

Review before save

This might be the most important user-experience decision in the whole feature.

AI Quick Log has two phases:

1. Analyze the description and present reviewable items.
2. Save only after the user confirms the reviewed items.

The first phase parses and resolves:

state.aiStep = .analyzing
parsed = try await foodParser.parse(text)
resolved.append(await resolver.resolve(item))
state.aiStep = .review

The second phase logs only the reviewed items:

state.aiStep = .logging
// save food, recipe, meal, or quick-log estimate
state.aiStep = .done(count: loggedCount)

This creates a much better relationship between the user and the model. The model can accelerate the flow, but the user keeps the final say.

They can edit amounts, change units, remove unwanted items, and decide whether an estimated item is good enough.

That is the FoodieCalc version of “trust, but review.”

AI insights on the dashboard

Foundation Models are not only used for logging.

FoodieCalc also has a daily nutrition insight service:

generateInsight(
    caloriesConsumed: Double,
    calorieTarget: Double,
    proteinConsumed: Double,
    proteinTarget: Double,
    carbsConsumed: Double,
    carbsTarget: Double,
    fatConsumed: Double,
    fatTarget: Double
) async throws -> FCAINutritionInsight

The model receives a compact summary of the day:

Today's nutrition: 1873/2200 cal,
158/160g protein,
106/220g carbs,
89/70g fat.
Provide one brief insight.

The output is typed:

@Generable(description: "A brief nutrition insight with emoji, title, and body")
public struct FCAINutritionInsight: Sendable {
    public var reasoning: String
    public var emoji: String
    public var title: String
    public var body: String
}

Again, this is not free-form text dumped into the UI. The app asks for a small, shaped result that fits the dashboard: emoji, title, body.

That keeps the final experience native and calm instead of feeling like a chatbot was bolted onto a nutrition app.

Estimation is useful, but clearly bounded

The third AI service is FCAINutritionEstimationService.

It estimates nutrition per 100g for a described food:

estimate(description: String) async throws -> FCAINutritionEstimate

This is useful for homemade dishes, quick entries, and foods that do not exist in the user’s library or nutrition database.

But the app treats it as an estimate. It is not the preferred path when better data exists. In AI Quick Log, estimation comes after local and database matching. In food creation, it can pre-fill fields so the user starts from a reasonable baseline and can still edit the final values.

That balance feels right for nutrition. AI should help the user move faster, but the interface should never pretend an estimate is the same as verified package or database data.

Session lifecycle became a performance feature

One of the most practical lessons was memory.

LanguageModelSession is powerful, but it should not be created casually and kept alive forever. FoodieCalc creates sessions lazily:

private func makeSessionIfNeeded() -> LanguageModelSession {
    if let session { return session }
    let session = LanguageModelSession(instructions: Self.instructions)
    self.session = session
    return session
}

Each service also exposes:

prewarm()
releaseSession()
resetSession()

That gave me a better mental model for AI inside an iOS app. It is not just an API call. It is a resource that affects memory, latency, and screen lifecycle.

For FoodieCalc, the dashboard insight service is lazy. The food parser can be prewarmed before the user needs it. Sessions can be released when the app no longer needs that model context.

This is the kind of detail that makes the feature feel native instead of heavy.

The final user experience

The user does not see most of this architecture.

They see a simple flow:

  1. Tap AI Quick Log.
  2. Type a natural sentence.
  3. Review the foods FoodieCalc found.
  4. Adjust anything that needs fixing.
  5. Log the meal.

That is exactly where I wanted the feature to land.

The AI is there, but it does not take over the product. It sits behind a fast native workflow and helps with the part computers are finally getting good at: turning messy human language into useful structure.

The rest is still FoodieCalc: SwiftUI, local data, CloudKit sync, FatSecret-powered nutrition lookup, custom foods, recipes, meals, widgets, and a UI designed around daily use.

What I learned

This feature made me think differently about AI in apps.

The strongest version was not “let the model do everything.”

The strongest version was:

  • give the model a narrow job,
  • ask for typed output,
  • keep deterministic code in charge,
  • check availability,
  • manage session memory,
  • keep the user in the loop,
  • and make the UI feel like part of the app, not a demo pasted on top.

That is the Foundation Models lesson I am taking forward.

For an iOS engineer, this is exciting territory. We can now build intelligent features that still feel native, private, and product-shaped. FoodieCalc is my first serious step into that kind of app design.

References