DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on Originally published at mvpfactory.io

Wiring Apple's On-Device Foundation Models API to SwiftUI

---
title: "Ship On-Device AI with Apple Foundation Models + SwiftUI"
published: true
description: "Wire Apple's on-device Foundation Models to SwiftUI: streaming with AsyncSequence, memory constraints, and latency tradeoffs for iOS 18.4+."
tags: [swift, ios, mobile, architecture]
canonical_url: https://mvpfactory.co/blog/apple-foundation-models-swiftui
---
Enter fullscreen mode Exit fullscreen mode

What You Will Build

By the end of this tutorial you will have a SwiftUI view that streams private, on-device AI inference using Apple's Foundation Models framework — no server, no API key, no data leaving the device. You will also have a production-ready hardware-gated fallback path for older devices, because shipping means handling the whole device matrix.

Prerequisites: Xcode 16+, iOS 18.4+ simulator or device (A17 Pro or M-series chip), Swift concurrency fundamentals.


Step 1 — Understand the Session API

Let me show you a pattern I use in every project. Foundation Models exposes inference through LanguageModelSession. The surface area is deliberately small — do not let that fool you into thinking it is limited.

import FoundationModels

let session = LanguageModelSession(
    instructions: "You are a concise summarization assistant."
)

let response = try await session.respond(
    to: "Summarize this note in one sentence.",
    options: .init(temperature: 0.7)
)
print(response.content)
Enter fullscreen mode Exit fullscreen mode

Inference never leaves the device. For health, finance, or productivity apps handling sensitive personal data — think break-reminder apps like HealthyDesk that sit on top of personal usage patterns — this eliminates an entire privacy risk class and removes server inference costs entirely.


Step 2 — Stream Responses into SwiftUI

Synchronous respond(to:) works for short completions. For anything the user reads in real time, use streaming. The docs do not always make this obvious, but AsyncSequence + the task modifier is the idiomatic path here — not Combine.

@State private var output = ""

var body: some View {
    ScrollView { Text(output).padding() }
        .task {
            let session = LanguageModelSession()
            for try await partial in session.streamResponse(to: prompt) {
                output += partial.text
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

No third-party dependencies. No manual dispatch queue management. Most teams reach for Combine out of habit — resist that. AsyncSequence + task is already there and composes exactly right for this pattern.


Step 3 — Gate on Hardware and Build the Fallback Path

Here is the minimal setup to get this working across your real device matrix.

Apple's model sits at approximately 3 billion parameters (per WWDC 2025 "Explore the Foundation Models framework"). Memory headroom varies significantly:

Chip Tier Neural Engine Unified Memory Context Headroom
A17 Pro 16-core 8 GB ~4 GB usable for ML
A18 / A18 Pro 16-core (2nd gen) 8 GB Higher throughput
M1 / M2 (iPad, Mac) 16–32 core 8–16 GB Lowest fallback risk
A16 and below Not supported Always route to server

Build the fallback as a first-class path — not an afterthought:

func runInference(prompt: String) async throws -> String {
    guard LanguageModelSession.isSupported else {
        return try await serverInference(prompt: prompt)
    }

    let session = LanguageModelSession(
        instructions: "You are a concise assistant."
    )
    let response = try await session.respond(to: prompt)
    return response.content
}

func serverInference(prompt: String) async throws -> String {
    var request = URLRequest(url: serverEndpointURL)
    request.httpMethod = "POST"
    request.httpBody = try JSONEncoder().encode(["prompt": prompt])
    let (data, _) = try await URLSession.shared.data(for: request)
    return try JSONDecoder().decode(InferenceResponse.self, from: data).text
}
Enter fullscreen mode Exit fullscreen mode

Note: Verify the exact symbol name — LanguageModelSession.isSupported or equivalent — against the current SDK docs before shipping. This API surface was evolving through the iOS 18.x cycle.


Gotchas

Prompt Guard runs on every call and you cannot disable it. It screens for prompt injection and policy violations before the model processes input. Apple has not published per-device latency figures for this layer — treat any specific numbers in circulation with skepticism until you measure on your target hardware. The practical implication: do not build flows that depend on sub-100ms first-token latency on the initial session call.

Skipping representative prompt testing is how you discover your fallback path in production. Build a small evaluation suite of realistic prompts early. Run it on A17 Pro, M-series, and an unsupported device before App Store review. Evaluation is part of shipping.

Do not reach for CoreML just because it feels more engineered. Foundation Models wins on time-to-production for general language tasks. CoreML wins when you need domain-specific accuracy or deterministic sub-50ms inference. The integration and maintenance cost of a custom CoreML pipeline is real and ongoing — choose it deliberately.


Conclusion

Three things to internalize before you ship:

  1. Gate on hardware availability from day one and ship the fallback path alongside it — device capability is a first-class architectural input.
  2. Use AsyncSequence + task for all streaming UI — it is idiomatic Swift concurrency and requires zero extra dependencies.
  3. Run your prompt suite across the full device tier matrix before you hit App Store review.

The session API is minimal by design. The privacy guarantee is real. The memory constraints are real too — knowing where that line sits for your prompts is the engineering work that separates a shipped app from a demo.

Resources:

Top comments (0)