Weather.Co is now free for every pilot — always free for CFIs and flight schools.
Lab · Guides · Foundation Models on-device
Lab guide · Apple Intelligence

Apple Foundation Models, on-device.

Generate structured output from a local LLM that runs entirely on the user's phone. The pattern behind Outpost.Co's Discover, Upsee's weekly briefing, and tmpo's natural-language insights — no cloud, no API key, no user data leaving the device.

Apple's Foundation Models framework (iOS 18.2+) exposes the on-device LLM that powers Apple Intelligence. For developers, the most useful capability isn't chat — it's schema-driven generation: ask the model for a value matching a Swift type, and it returns a validated instance you can use directly, no JSON parsing or prompt-shape gymnastics.

Before you start
  • iOS 18.2+ deployment target.
  • A device that supports Apple Intelligence (iPhone 15 Pro / 16 family, M-series iPad, M-series Mac).
  • Apple Intelligence enabled in Settings — the user controls this.
  • import FoundationModels in your Swift target.

Step 1Check availability first

The framework is always importable, but the model isn't always usable — old device, region restriction, Apple Intelligence disabled, model still downloading. Guard at the top of any feature that needs it.

import FoundationModels

enum AICapability {
  static var isAvailable: Bool {
    SystemLanguageModel.default.availability == .available
  }
  static var reasonUnavailable: String? {
    switch SystemLanguageModel.default.availability {
    case .available: return nil
    case .unavailable(.deviceNotEligible): return "Needs Apple Intelligence hardware."
    case .unavailable(.appleIntelligenceNotEnabled): return "Turn on Apple Intelligence in Settings."
    case .unavailable(.modelNotReady): return "Model is still downloading."
    @unknown default: return "Unavailable."
    }
  }
}

Step 2Define your output schema

The Swift compiler does the heavy lifting via @Generable. Annotate the type and its properties; the framework gives the model a schema that matches.

@Generable
struct CampSuggestion {
  @Guide(description: "Park or area name")
  let name: String

  @Guide(description: "Why this fits the user's request")
  let rationale: String

  @Guide(description: "Drive time in hours from the user's location", .range(0.5...6.0))
  let driveTimeHours: Double

  @Guide(description: "Best season for this kind of trip", .anyOf(["spring", "summer", "fall", "winter"]))
  let bestSeason: String
}

@Generable
struct CampSuggestionList {
  @Guide(description: "3 distinct candidates, ranked best-to-worst")
  let suggestions: [CampSuggestion]
}

Step 3Run a session

func suggestCamps(for prompt: String) async throws -> CampSuggestionList {
  let session = LanguageModelSession(instructions: Instructions {
    "You are a camping-trip recommender for users in the central US."
    "Recommend state parks and national forests. Be specific and brief."
  })
  let response = try await session.respond(to: prompt, generating: CampSuggestionList.self)
  return response.content
}

That's it. response.content is a typed CampSuggestionList. The framework handles prompting the model with the schema, parsing its output, retrying on validation failure, and giving you a clean value. No JSON, no manual prompt engineering.

Step 4Stream the response

For longer outputs, stream so the UI updates as the model generates.

let stream = session.streamResponse(to: prompt, generating: CampSuggestionList.self)
for try await partial in stream {
  await MainActor.run { self.partial = partial }
}

partial is an evolving CampSuggestionList.PartiallyGenerated — fields appear as the model fills them. Bind it to a SwiftUI @State and the list grows in real time.

Step 5Tool calls (let the model query your data)

Sometimes the model needs to look up something your app knows. Define a Tool and the model decides when to invoke it.

struct CurrentWeatherTool: Tool {
  let name = "current_weather"
  let description = "Get the current weather at a park."

  @Generable
  struct Arguments {
    @Guide(description: "Park name")
    let park: String
  }

  func call(arguments: Arguments) async throws -> ToolOutput {
    let w = try await WeatherService.shared.current(for: arguments.park)
    return .init("Currently \(w.tempF)°F, \(w.conditions).")
  }
}

let session = LanguageModelSession(tools: [CurrentWeatherTool()]) { ... }
Tip — keep prompts boring

Treat the on-device model the way you'd treat a junior employee with strong instincts and no context. Give it the goal in plain English, the constraints, and ideally an example. Resist the urge to engineer the prompt — schema-driven generation handles most of the formatting work for you.

Step 6Graceful fallback

Anything that uses Foundation Models needs a non-AI fallback because most iOS devices still don't support Apple Intelligence. The right pattern: have the AI feature enhance an experience that already works without it.

if AICapability.isAvailable {
  // AI-powered Discover with natural-language prompts
  AIDiscoverView()
} else {
  // Filter-based Discover — works on every device
  FilteredDiscoverView()
}

This is the rule that's saved the most pain: the app must do its job without the AI. AI is the enhancement, not the foundation.

Common gotchas
  • Works on your device, fails everywhere else. You're testing on hardware with Apple Intelligence and the user isn't. Always test the .unavailable branch.
  • Model returns nonsense for novel categories. Add an example in your Instructions block. The on-device model is smaller than cloud LLMs and benefits more from in-context guidance.
  • Schema validation fails repeatedly. Your @Guide hints are conflicting (e.g. range + anyOf that don't intersect). Simplify the schema.
  • First call is slow. The model warms up. Pre-warm in the background after first launch (_ = LanguageModelSession()).
  • The user disabled Apple Intelligence between launches. Re-check availability before every session — don't cache the result for the app lifetime.

The argument for on-device AI isn't just privacy (though it's that). It's that you can ship features the cloud-LLM approach simply can't — features that respect the user's data enough to be on by default, without account creation, without latency, without sending the user's prompts to anyone. Outpost's Discover, Upsee's weekly briefing, and tmpo's natural-language insights all exist because of this. None of them would have shipped if they required sending your data to my server.

Sources: Apple Developer — FoundationModels framework, LanguageModelSession, Generable; Outpost.Co + Upsee project memory. Requires iOS 18.2+ and Apple Intelligence-eligible hardware.