Le framework Foundation Models d'Apple donne à chaque application un accès direct au même grand modèle de langage exécuté sur l'appareil qui alimente Apple Intelligence. Aucune clé API, aucun aller-retour réseau, aucune facturation par jeton. L'inférence s'exécute entièrement sur l'appareil de l'utilisateur — un Mac, un iPhone ou un iPad à puce Apple silicon — ce qui signifie que les données de l'utilisateur ne quittent jamais l'appareil et que vos fonctionnalités continuent de fonctionner en mode avion.
Dans ce tutoriel, vous allez créer une fonctionnalité Notes intelligentes : vous collez un bloc de notes de réunion brutes et le modèle renvoie un résumé propre ainsi qu'une liste de tâches structurée et typée — chacune avec un responsable et une priorité. En chemin, vous découvrirez les quatre piliers du framework : la vérification de disponibilité, la génération de texte simple, la génération guidée avec @Generable, le streaming et l'appel d'outils.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Xcode 26 ou une version ultérieure — le framework est livré avec le dernier SDK
- Un appareil ou simulateur exécutant iOS 26+, iPadOS 26+ ou macOS 26+
- Un appareil à puce Apple silicon avec Apple Intelligence activé (Réglages → Apple Intelligence et Siri)
- Une aisance avec Swift et les bases de SwiftUI
- Environ 4 Go d'espace libre — le modèle sur l'appareil se télécharge automatiquement lors de sa première utilisation
Aucun compte Anthropic, OpenAI ou Google n'est requis. Tout ici s'exécute localement.
Ce que vous allez créer
À la fin, vous disposerez d'un écran SwiftUI avec un éditeur de texte et un bouton « Résumer ». Un appui appelle le modèle sur l'appareil, qui renvoie :
- Un résumé concis d'un paragraphe des notes
- Un tableau typé de tâches, chacune avec un
task, unowneret une énumérationpriority
Comme la sortie est fortement typée, vous pouvez l'afficher directement dans SwiftUI sans analyse de chaîne fragile. Vous ajouterez également un outil qui permet au modèle de rechercher des membres de l'équipe, ainsi qu'un mode streaming qui affiche le résumé jeton par jeton.
Comprendre le framework Foundation Models
Un modèle mental rapide avant d'écrire du code.
Le modèle sur l'appareil
Le framework expose un modèle de langage compact — environ trois milliards de paramètres — optimisé pour s'exécuter sur Apple silicon avec quantification. Ce n'est pas un modèle de pointe comme Claude ou Gemini. Il excelle dans les tâches ciblées sur l'appareil : résumé, classification, extraction, réécritures courtes et génération structurée. Pour ces travaux, il est rapide, gratuit et totalement privé.
Les types principaux
| Type | Rôle |
|---|---|
SystemLanguageModel | Représente le modèle sur l'appareil ; expose .availability |
LanguageModelSession | Une conversation avec état ; vous appelez respond(to:) dessus |
@Generable | Macro qui transforme un type Swift en schéma que le modèle remplit |
@Guide | Ajoute des descriptions en langage naturel et des contraintes aux champs |
Tool | Protocole pour les fonctions que le modèle peut appeler pendant la génération |
Pourquoi la génération guidée est essentielle
La plupart des intégrations de LLM vous forcent à demander du JSON dans un prompt, puis à analyser une chaîne en espérant qu'elle soit valide. Foundation Models inverse cette logique : vous annotez une structure Swift avec @Generable, vous remettez le type à la session, et le framework contraint le décodage du modèle pour que la sortie corresponde forcément à votre schéma. Vous récupérez une vraie valeur Swift typée — jamais une chaîne brute à démêler.
Étape 1 : Importer le framework et vérifier la disponibilité
Ne supposez jamais que le modèle est prêt. Un appareil peut ne pas être éligible, Apple Intelligence peut être désactivé, ou le modèle peut encore être en téléchargement. Vérifiez toujours availability d'abord.
import FoundationModels
import SwiftUI
struct SmartNotesView: View {
// Une référence au modèle système sur l'appareil.
private let model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
NotesEditor()
case .unavailable(.deviceNotEligible):
ContentUnavailableView(
"Non pris en charge",
systemImage: "cpu",
description: Text("Cet appareil ne peut pas exécuter l'IA sur l'appareil.")
)
case .unavailable(.appleIntelligenceNotEnabled):
ContentUnavailableView(
"Activez Apple Intelligence",
systemImage: "sparkles",
description: Text("Activez Apple Intelligence dans les Réglages pour utiliser les Notes intelligentes.")
)
case .unavailable(.modelNotReady):
ContentUnavailableView(
"Téléchargement du modèle",
systemImage: "arrow.down.circle",
description: Text("Le modèle est encore en téléchargement. Réessayez sous peu.")
)
case .unavailable(let other):
ContentUnavailableView(
"Indisponible",
systemImage: "exclamationmark.triangle",
description: Text("L'IA sur l'appareil est indisponible : \(String(describing: other))")
)
}
}
}Le modèle se télécharge automatiquement selon l'état du réseau, le niveau de batterie et la charge système — c'est pourquoi modelNotReady existe. Gérez ce cas avec élégance plutôt que d'afficher une erreur.
Étape 2 : Votre première génération
Une LanguageModelSession représente une conversation en cours. Créez-la avec des instructions facultatives — un prompt système qui façonne le ton et le comportement — puis appelez respond(to:).
import FoundationModels
func quickSummary(of notes: String) async throws -> String {
let session = LanguageModelSession(
instructions: """
Tu es un assistant de réunion concis. Résume les notes en un \
court paragraphe. N'invente pas de détails absents.
"""
)
let response = try await session.respond(
to: "Résume ces notes :\n\n\(notes)"
)
return response.content
}respond(to:) est async throws. La valeur renvoyée expose .content pour le texte généré. La session conserve sa propre transcription, donc les appels suivants sur la même session se souviennent des tours précédents — pratique pour un chat, mais pour un résumé ponctuel, une session fraîche par requête convient.
Étape 3 : Génération guidée avec @Generable
Le texte simple convient pour un résumé, mais les tâches ont besoin de structure. Définissez la forme souhaitée sous forme de types Swift annotés @Generable. Utilisez @Guide pour décrire chaque champ afin que le modèle le remplisse correctement.
import FoundationModels
@Generable
struct NotesDigest {
@Guide(description: "Un résumé concis d'un paragraphe des notes.")
let summary: String
@Guide(description: "Chaque tâche actionnable mentionnée dans les notes.")
let actionItems: [ActionItem]
}
@Generable
struct ActionItem {
@Guide(description: "Une description courte et impérative de la tâche.")
let task: String
@Guide(description: "La personne responsable, ou 'Non assigné' si peu clair.")
let owner: String
let priority: Priority
}
@Generable
enum Priority {
case low
case medium
case high
}Demandez maintenant à la session de générer ce type exact. Passez le type comme argument generating: et le framework contraint le décodage à correspondre à votre schéma.
func makeDigest(from notes: String) async throws -> NotesDigest {
let session = LanguageModelSession(
instructions: """
Extrais un résumé et une liste de tâches concrètes à partir des \
notes de réunion. N'inclus que les tâches explicitement énoncées.
"""
)
let response = try await session.respond(
to: "Analyse ces notes :\n\n\(notes)",
generating: NotesDigest.self
)
// response.content est un NotesDigest entièrement typé — sans analyse.
return response.content
}response.content est un vrai NotesDigest. Le champ priority est une véritable énumération Priority, pas une chaîne à valider. C'est le plus grand gain d'ergonomie du framework.
Contraindre davantage les champs
@Guide accepte plus que des descriptions. Vous pouvez borner des quantités, imposer des motifs, ou restreindre une chaîne à un ensemble de valeurs autorisées.
@Generable
struct TaggedNote {
@Guide(description: "Le corps de la note.")
let body: String
// Force le modèle à renvoyer entre 1 et 3 étiquettes.
@Guide(description: "Étiquettes de sujets pertinentes.")
@Guide(.count(3))
let tags: [String]
}Ces contraintes sont appliquées pendant le décodage, donc le modèle ne peut physiquement pas renvoyer quatre étiquettes quand vous en avez demandé trois.
Étape 4 : L'intégrer dans SwiftUI
Voici un écran d'édition compact qui exécute le résumé et affiche le résultat typé. Notez la garde isWorking — une session traite une requête à la fois.
import FoundationModels
import SwiftUI
struct NotesEditor: View {
@State private var notes = ""
@State private var digest: NotesDigest?
@State private var isWorking = false
@State private var errorText: String?
var body: some View {
Form {
Section("Notes brutes") {
TextEditor(text: $notes)
.frame(minHeight: 160)
}
Button("Résumer") {
Task { await run() }
}
.disabled(notes.isEmpty || isWorking)
if let digest {
Section("Résumé") {
Text(digest.summary)
}
Section("Tâches") {
ForEach(Array(digest.actionItems.enumerated()), id: \.offset) { _, item in
VStack(alignment: .leading, spacing: 4) {
Text(item.task).font(.headline)
Text("\(item.owner) · \(String(describing: item.priority))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
if let errorText {
Text(errorText).foregroundStyle(.red)
}
}
.overlay {
if isWorking { ProgressView() }
}
}
private func run() async {
isWorking = true
errorText = nil
defer { isWorking = false }
do {
digest = try await makeDigest(from: notes)
} catch {
errorText = error.localizedDescription
}
}
}Compilez et exécutez sur un appareil pris en charge, collez un paragraphe de notes désordonnées, et appuyez sur Résumer. Vous obtiendrez un résumé propre et une liste de tâches typée affichée nativement — le tout calculé sur l'appareil.
Étape 5 : Diffuser la réponse en streaming
Pour tout ce qui dépasse une phrase, le streaming rend l'interface vivante. Au lieu de respond, appelez streamResponse, qui renvoie une séquence asynchrone d'instantanés progressivement complets.
func streamSummary(of notes: String, onUpdate: @escaping (String) -> Void) async throws {
let session = LanguageModelSession(
instructions: "Résume les notes de réunion de façon claire et brève."
)
let stream = session.streamResponse(
to: "Résume ces notes :\n\n\(notes)"
)
for try await partial in stream {
// Chaque instantané est l'intégralité du texte généré jusqu'ici.
onUpdate(partial.content)
}
}La génération guidée se diffuse aussi. Quand vous diffusez un type @Generable, chaque instantané est une version partiellement générée de votre structure, avec des champs remplis au fur et à mesure — vous pouvez donc afficher un résumé pendant que les tâches sont encore en cours de production.
let stream = session.streamResponse(
to: "Analyse ces notes :\n\n\(notes)",
generating: NotesDigest.self
)
for try await partial in stream {
// partial.content.summary peut être rempli avant que actionItems soit complet.
await MainActor.run { self.livePartial = partial.content }
}Étape 6 : Ajouter un outil
Parfois le modèle a besoin d'informations en direct qu'il ne possède pas — l'effectif actuel de votre équipe, un enregistrement de base de données, la météo. Les outils permettent au modèle d'appeler votre code Swift en cours de génération. Conformez-vous au protocole Tool : donnez-lui un name, une description, un type Arguments annoté @Generable et une méthode call.
import FoundationModels
struct TeamDirectoryTool: Tool {
let name = "lookupTeamMember"
let description = "Trouver le nom complet et le rôle d'un membre d'équipe à partir d'un nom partiel."
@Generable
struct Arguments {
@Guide(description: "Un nom partiel ou complet à rechercher.")
var query: String
}
// Un petit substitut pour une vraie source de données.
private let roster = [
"sara": "Sara Ben Ali — Cheffe de produit",
"youssef": "Youssef Trabelsi — Ingénieur backend",
"lina": "Lina Haddad — Designer"
]
func call(arguments: Arguments) async throws -> String {
let key = arguments.query.lowercased()
if let match = roster.first(where: { key.contains($0.key) }) {
return match.value
}
return "Aucun membre d'équipe ne correspond à '\(arguments.query)'."
}
}Enregistrez l'outil lors de la création de la session. Dès lors, le modèle décide lui-même quand l'appeler, et le framework achemine l'appel vers votre méthode call, réinjecte le résultat, puis poursuit la génération.
let session = LanguageModelSession(
tools: [TeamDirectoryTool()],
instructions: """
Assigne les tâches à de vrais membres d'équipe. Quand une note \
mentionne une personne par un nom partiel, utilise l'outil de \
recherche pour résoudre son nom complet.
"""
)
let response = try await session.respond(
to: "Analyse ces notes :\n\n\(notes)",
generating: NotesDigest.self
)Si vos notes disent « Sara doit livrer la page de tarification », le modèle appelle lookupTeamMember avec query: "Sara", reçoit « Sara Ben Ali — Cheffe de produit », et remplit le champ owner avec le nom résolu. Vous pouvez inspecter chaque étape via session.transcript, qui énumère instructions, prompts, appels d'outils, sorties d'outils et réponses — inestimable pour le débogage.
Étape 7 : Régler la génération et les performances
Deux petites touches font une vraie différence en production.
Contrôlez l'aléatoire avec GenerationOptions. Baissez la température pour les tâches d'extraction où vous voulez une sortie déterministe et fidèle.
let response = try await session.respond(
to: prompt,
generating: NotesDigest.self,
options: GenerationOptions(temperature: 0.3)
)Préchauffez la session quand vous savez qu'une requête arrive — par exemple quand l'utilisateur commence à taper — pour que le modèle soit résident en mémoire au moment où il appuie sur le bouton.
let session = LanguageModelSession(instructions: "…")
session.prewarm() // Charge les ressources avant la première vraie requête.Tester votre implémentation
Pour vérifier la fonctionnalité de bout en bout :
- Exécutez sur un appareil physique Apple silicon avec Apple Intelligence activé — la prise en charge du simulateur varie, et le matériel réel reflète la vraie latence.
- Collez des notes mentionnant une tâche et une personne, par ex. « Discussion du lancement Q3. Sara finalise la tarification d'ici vendredi. Youssef doit corriger le bug de connexion — urgent. »
- Confirmez que le résumé est fidèle et que le tableau tâches contient deux entrées avec les bons responsables et priorités.
- Activez le mode avion et relancez — cela doit toujours fonctionner, prouvant que l'inférence est entièrement sur l'appareil.
- Ouvrez
session.transcriptdans le débogueur pour confirmer que l'outil a été invoqué.
Dépannage
unavailable(.modelNotReady) ne se résout jamais. Le modèle se télécharge de façon opportuniste. Assurez-vous que l'appareil est en Wi-Fi, chargé et inactif quelques minutes, puis relancez.
La génération guidée renvoie des valeurs étranges. Affinez vos descriptions @Guide. Le modèle les traite comme des instructions — des descriptions vagues produisent des champs vagues. Ajoutez des contraintes comme .count(_:) là où la structure compte.
La session lève une exception à un deuxième appel concurrent. Une session traite une requête à la fois. Protégez avec un indicateur isResponding, ou créez des sessions distinctes pour le travail parallèle.
La sortie semble générique ou hallucinée. C'est un modèle compact de classe 3 milliards, pas un modèle de pointe. Gardez les tâches ciblées, baissez la température, et demandez-lui explicitement de n'utiliser que les informations présentes dans l'entrée.
Prochaines étapes
- Explorez les énumérations
@Generableavec valeurs associées pour modéliser des arbres de décision plus riches. - Combinez les outils avec WeatherKit, EventKit ou HealthKit pour ancrer la génération dans les données réelles de l'appareil.
- Ajoutez un chemin de repli vers un modèle cloud (Claude ou Gemini) pour les tâches qui dépassent la portée du modèle sur l'appareil, en gardant le chemin privé par défaut.
- Lisez nos guides connexes : Créer des agents IA avec le Claude Agent SDK et l'API Prompt intégrée de Chrome avec Gemini Nano pour l'IA sur l'appareil dans le web.
Conclusion
Vous avez créé une fonctionnalité d'IA complète, privée et sur l'appareil en Swift sans une seule clé API. Vous avez vérifié la disponibilité du modèle, généré du texte simple, extrait des données fortement typées avec @Generable, diffusé les résultats dans SwiftUI, appelé votre propre code Swift via un outil, et réglé le modèle pour la fiabilité. Le framework Foundation Models transforme l'IA sur l'appareil en une API Swift ordinaire — typée, rapide, gratuite et privée par défaut. Cette combinaison en fait le premier choix naturel pour une large catégorie de fonctionnalités d'application qui nécessitaient auparavant un backend et une facture.