Skip to main content
Le streaming change surtout l’expérience utilisateur. Le temps de génération ne devient pas magique, mais l’utilisateur voit que quelque chose se passe. Pour une app Swift, c’est souvent la différence entre une fonctionnalité perçue comme fluide et une fonctionnalité perçue comme bloquante.

Deux modes à connaître

MéthodeÀ utiliser quand…
respond(to:)vous avez besoin du résultat complet avant de l’utiliser
streamResponse(to:)vous voulez afficher la réponse progressivement
Si votre UI affiche du texte généré, le streaming est souvent le meilleur défaut.

Streaming, isResponding et GenerationOptions

Ces trois éléments vont souvent ensemble :
  • streamResponse(to:options:) pour livrer la réponse progressivement
  • isResponding pour protéger l’UI contre les doubles envois
  • GenerationOptions pour borner la longueur et le niveau de variation
import FoundationModels

let session = LanguageModelSession()

let options = GenerationOptions(
    sampling: .greedy,
    temperature: 0.2,
    maximumResponseTokens: 220
)

for try await partial in session.streamResponse(
    to: "Explique le rôle d'un transcript en 4 points.",
    options: options
) {
    print(partial.content)
}
Dans ce type de flow, isResponding devient votre garde-fou produit. Il évite qu’un utilisateur relance une seconde génération alors que la première est encore affichée en streaming.

Exemple de streaming texte

import FoundationModels
import Observation

@Observable
@MainActor
final class StreamingViewModel {
    var response = ""
    var errorMessage: String?
    var isResponding: Bool { session.isResponding }

    private let session = LanguageModelSession()

    func generate() async {
        response = ""
        errorMessage = nil

        do {
            for try await partial in session.streamResponse(
                to: "Explique MLX à un développeur Swift en 4 lignes."
            ) {
                response = partial.content
            }
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}
À chaque itération, partial.content contient l’état courant complet de la réponse.

Exemple de streaming structuré

Le streaming fonctionne aussi avec Guided Generation.
let session = LanguageModelSession()

@Generable
struct Checklist {
    @Guide(description: "A short title")
    var title: String = ""

    @Guide(description: "Action items", .minimumCount(2))
    var items: [String] = []
}

let stream = session.streamResponse(
    to: "Prépare une checklist pour intégrer une feature IA dans une app iOS.",
    generating: Checklist.self
)

for try await partial in stream {
    print(partial.content)
}
Ce pattern est très utile quand vous remplissez progressivement une vue plus riche qu’un simple bloc de texte.

UX : ce qu’il faut afficher

Un indicateur d’activité

La propriété isResponding de votre session est faite pour ça.
if viewModel.isResponding {
    ProgressView("Génération en cours...")
}

Un bouton désactivé pendant la génération

Button("Générer") {
    Task { await viewModel.generate() }
}
.disabled(viewModel.isResponding)

Un affichage lisible

Le modèle génère souvent du Markdown. Si vous l’affichez tel quel, le résultat peut sembler brouillon.
Text(.init(viewModel.response))

Quand ne pas streamer

Le streaming n’est pas indispensable si :
  • vous avez besoin d’un objet final unique
  • la réponse est très courte
  • le résultat sert uniquement à une logique interne
Dans ces cas-là, respond(to:) reste plus simple.

Bonnes pratiques

  • videz l’ancien contenu avant une nouvelle génération
  • montrez clairement qu’une réponse est en cours
  • gérez l’erreur dans la même zone d’UI
  • bornez souvent maximumResponseTokens si l’écran attend une réponse courte
  • ne forcez pas le streaming si l’écran a surtout besoin d’un résultat final