Foundation Models é o framework da Apple que integra um modelo de linguagem (LLM, do inglês Large Language Model) diretamente no dispositivo. Esse framework permite geração de texto, criação de dados estruturados, respostas por streaming e chamada de ferramentas (tool calling) com baixo tempo de resposta e privacidade local. A API foi desenhada para Swift e se encaixa naturalmente em apps com SwiftUI e arquiteturas reativas. Com poucas linhas é possível abrir uma sessão com o modelo, enviar um prompt e renderizar o resultado. A proposta a seguir apresenta conceitos, código-pronto e um caminho sólido para incluir recursos generativos em um aplicativo.
O material introduz as bases do framework, descreve como lidar com disponibilidade do modelo e evolui até saídas tipadas seguras. Também demonstra técnicas de prompting, exibição incremental com streaming e extensão do modelo por meio de ferramentas. Um pequeno conjunto de tipos @Generable dá suporte à geração de um itinerário de viagem completo e pronto para UI. Tudo é apresentado com exemplos autocontidos para Playground e trechos que se integram a uma ViewModel e a uma View SwiftUI. O resultado final forma um guia didático para iniciar e aprofundar o uso do Foundation Models em projetos reais.
Visão geral do framework e requisitos do ambiente
Foundation Models expõe um modelo de linguagem local por meio de tipos de alto nível como LanguageModelSession e SystemLanguageModel. A sessão encapsula estado conversacional, instruções e ferramentas, enquanto o modelo do sistema informa disponibilidade e condições de uso. A execução on-device reduz latência, melhora privacidade e funciona mesmo sem conexão contínua. As APIs oferecem tanto respostas completas quanto fluxos parciais, balanceando qualidade e velocidade. O ecossistema combina macros como @Generable e @Guide para orientar o formato de saída.
Para desenvolvimento, Xcode recente e um projeto SwiftUI simplificam a integração. Assinatura de código ativa habilita execução em dispositivos elegíveis e no destino “My Mac” quando apropriado. Um Playground facilita exploração isolada das chamadas da API antes da integração ao app. A estrutura do projeto normalmente separa Views, ViewModels e modelos de dados geráveis. Esse arranjo organiza responsabilidades e facilita a evolução do recurso generativo.
Primeiros passos: geração de texto e checagem de disponibilidade
A primeira experiência envolve abrir uma sessão e pedir uma resposta textual a partir de um prompt simples. Instruções persistentes podem ser fornecidas para definir papel, regras e formato desejado, tornando a saída mais consistente. A seguir, exemplos mostram tanto o uso mínimo quanto o uso com instruções. Paralelamente, a consulta de disponibilidade do modelo evita falhas silenciosas e habilita condutas alternativas. Esse cuidado cobre casos de dispositivo inelegível, Apple Intelligence desativado ou modelo ainda não pronto.
O bloco a seguir demonstra geração básica no Playground, seguida de um exemplo com instruções permanentes e uma checagem de disponibilidade. Os identificadores foram mantidos em português para facilitar leitura e padronização. Comentários curtos destacam a intenção de cada parte do código. Essa base será reutilizada nas próximas seções ao migrar de texto livre para dados estruturados.
import FoundationModels
import Playgrounds
#Playground {
// Exemplo 1: geração mínima de texto
let sessao = LanguageModelSession()
let resposta = try await sessao.respond(to: "Gerar um roteiro de 3 dias para Paris.")
print(resposta.content)
}
#Playground {
// Exemplo 2: sessão com instruções persistentes
let instrucoes = """
A tarefa é criar um roteiro de viagem.
Cada dia precisa ter atividade, hotel e restaurante.
Sempre inclua título, breve descrição e plano dia a dia.
"""
let sessao = LanguageModelSession(instructions: instrucoes)
let resposta = try await sessao.respond(to: "Gerar um roteiro de 3 dias para Paris.")
print(resposta.content)
}
#Playground {
// Exemplo 3: checagem de disponibilidade do modelo
let modelo = SystemLanguageModel.default
switch modelo.availability {
case .available:
print("Foundation Models disponível.")
case .unavailable(.deviceNotEligible):
print("Modelo indisponível neste dispositivo.")
case .unavailable(.appleIntelligenceNotEnabled):
print("Apple Intelligence desativado nos Ajustes.")
case .unavailable(.modelNotReady):
print("Modelo ainda não está pronto. Tentar novamente em breve.")
case .unavailable(_):
print("Modelo indisponível por motivo desconhecido.")
}
}
Do Playground ao app: ViewModel e exibição inicial
Depois dos primeiros testes, a integração ao app segue uma divisão clara entre exibição e geração. A ViewModel centraliza a sessão do modelo de linguagem, as instruções e o tratamento de erros. A View SwiftUI observa o estado publicado e renderiza resultados, exibindo carregamento e conteúdo. Esse padrão facilita a evolução de texto bruto para dados tipados gerados pelo modelo. A seguir, um exemplo mínimo de ViewModel com saída textual simples.
O código mantém as instruções no inicializador e expõe um método assíncrono de geração. Um estado publicado guarda o conteúdo e outro opcional captura erros ocorridos na chamada. A View cria a instância sob demanda e dispara a geração conforme eventos de UI. Essa organização prepara o terreno para recursos mais avançados nas seções seguintes.
import FoundationModels
import SwiftUI
// Modelo simples de domínio usado pelos exemplos
struct Landmark: Identifiable {
let id = UUID()
let name: String
}
// ViewModel inicial com texto livre
@MainActor
final class GeradorDeItinerario: ObservableObject {
private let marco: Landmark
private var sessao: LanguageModelSession
@Published var conteudoBruto: String?
@Published var erro: Error?
init(marco: Landmark) {
self.marco = marco
let instrucoes = """
A tarefa é criar um roteiro de viagem.
Cada dia precisa ter atividade, hotel e restaurante.
Sempre inclua título, breve descrição e plano dia a dia.
"""
self.sessao = LanguageModelSession(instructions: instrucoes)
}
func gerarTexto(dias: Int = 3) async {
do {
let prompt = "Gerar um roteiro de \(dias) dias para \(marco.name)."
let resposta = try await sessao.respond(to: prompt)
self.conteudoBruto = resposta.content
} catch {
self.erro = error
}
}
}
// View SwiftUI mínima para acionar e exibir texto
struct TelaDeViagem: View {
let marco: Landmark
@State private var gerador: GeradorDeItinerario?
@State private var carregando = false
var body: some View {
ScrollView {
if let texto = gerador?.conteudoBruto {
Text(LocalizedStringKey(texto)).padding()
} else if carregando {
ProgressView("Gerando roteiro...")
} else {
Text("Toque no botão para gerar um roteiro.")
}
}
.task {
// Criação do gerador quando a tela entra em cena
self.gerador = GeradorDeItinerario(marco: marco)
}
.toolbar {
Button("Gerar roteiro") {
Task {
carregando = true
await gerador?.gerarTexto()
carregando = false
}
}
}
}
}
Saída estruturada com @Generable: tipos seguros e previsíveis
Texto livre é útil, porém dados estruturados trazem previsibilidade, validação e melhor UI. O macro @Generable torna um tipo compatível com a geração do modelo, e @Guide fornece dicas sobre cada propriedade. Com isso, a resposta do modelo já chega no formato de um struct Swift, reduzindo pós-processamento. A geração pode inclusive fixar contagens de elementos e categorias válidas. O exemplo abaixo mostra um tipo simples e outro completo com composição e listas.
Primeiro, um roteiro simples com título, descrição e três dias em forma de texto. Em seguida, um conjunto mais rico com planos diários e atividades tipadas. Esse formato facilita renderização em Views dedicadas com hierarquia e estilo. As mesmas instruções do modelo passam a focar qualidade em vez de formato. O ganho em manutenção e robustez é imediato.
import FoundationModels
import Playgrounds
#Playground {
// Exemplo simples: struct gerável
let instrucoes = "A tarefa é criar um roteiro de viagem."
let sessao = LanguageModelSession(instructions: instrucoes)
let resposta = try await sessao.respond(
to: "Gerar um roteiro de 3 dias para Paris.",
generating: ItinerarioSimples.self
)
print(resposta.content)
}
// Tipo simples
@Generable
struct ItinerarioSimples {
@Guide(description: "Um nome empolgante para a viagem.")
let titulo: String
@Guide(description: "Uma descrição breve e envolvente da viagem.")
let descricao: String
@Guide(description: "Planos dia a dia em frases curtas.")
@Guide(.count(3))
let dias: [String]
}
// Modelo completo com composição
@Generable
struct Itinerary {
@Guide(description: "Título curto e marcante do roteiro.")
let title: String
@Guide(description: "Resumo atrativo do que será vivido.")
let description: String
@Guide(description: "Motivação ou raciocínio da proposta.")
let rationale: String?
@Guide(description: "Conjunto de planos diários.")
@Guide(.count(3))
let days: [DayPlan]
}
@Generable
struct DayPlan {
@Guide(description: "Título do dia (ex.: Dia 1 — Centro Histórico).")
let title: String
@Guide(description: "Breve subtítulo descritivo.")
let subtitle: String
@Guide(description: "Destino principal do dia.")
let destination: String
@Guide(description: "Atividades previstas, incluindo hotel e restaurante.")
let activities: [Activity]
}
@Generable
struct Activity {
@Guide(description: "Nome da atividade, hotel ou restaurante.")
let title: String
@Guide(description: "Breve descrição do que acontece, come ou visita.")
let description: String
}
Refatorando a ViewModel para gerar Itinerary
Com o modelo de dados definido, a ViewModel troca o texto livre por um tipo gerado. A chamada passa a informar o tipo-alvo no parâmetro generating: Type.self, recebendo um objeto forte e validado. Isso remove parsing manual e reduz erros de formatação. A UI, por sua vez, pode ser especializada para cada parte do roteiro. O trecho a seguir mostra a mudança na ViewModel.
O método de geração cria um prompt com cidade e duração, e a sessão retorna um Itinerary. O estado publicado agora mantém um Itinerary opcional em vez de String. A coleta de erros continua idêntica, preservando a robustez. Essa etapa prepara a evolução para prompting avançado e streaming.
@MainActor
final class GeradorDeItinerarioEstruturado: ObservableObject {
private let marco: Landmark
private var sessao: LanguageModelSession
@Published private(set) var itinerario: Itinerary?
@Published var erro: Error?
init(marco: Landmark) {
self.marco = marco
let instrucoes = """
A tarefa é criar um roteiro de viagem.
Cada dia precisa ter atividade, hotel e restaurante.
"""
self.sessao = LanguageModelSession(instructions: instrucoes)
}
func gerar(dias: Int = 3) async {
do {
let prompt = "Gerar um roteiro de \(dias) dias para \(marco.name)."
let resposta = try await sessao.respond(
to: prompt,
generating: Itinerary.self
)
self.itinerario = resposta.content
} catch {
self.erro = error
}
}
}
Técnicas de prompting: Prompt builder e one-shot
Prompts ganham expressividade com o Prompt builder, que aceita condicionais e composição dinâmica. Isso permite reutilizar partes, ativar restrições e montar instruções contextuais em tempo de execução. Outra técnica essencial é o one-shot, que inclui um exemplo de alta qualidade no prompt. O exemplo demonstra estrutura e tom, guiando o modelo a reproduzir o padrão sem copiar conteúdo. As duas abordagens elevam consistência e precisão.
O primeiro trecho constrói um prompt com exigência opcional de roteiro infantil. O segundo adiciona um exemplo completo por meio de um valor estático no tipo de dados. O exemplo não é copiado, mas pauta formato, ritmo e nível de detalhe. Na prática, a melhoria de qualidade é perceptível sem alterar o tipo gerado. Seguem os trechos com geradores e o exemplo necessário.
import FoundationModels
import Playgrounds
#Playground {
let instrucoes = "A tarefa é criar um roteiro de viagem."
let sessao = LanguageModelSession(instructions: instrucoes)
let infantil = true
let prompt = Prompt {
"Gerar um roteiro de 3 dias para o Grand Canyon."
if infantil {
"O roteiro deve ser adequado para crianças."
}
}
let resposta = try await sessao.respond(to: prompt, generating: Itinerary.self)
print(resposta.content)
}
#Playground {
let instrucoes = "A tarefa é criar um roteiro de viagem."
let sessao = LanguageModelSession(instructions: instrucoes)
let prompt = Prompt {
"Gerar um roteiro de 3 dias para o Grand Canyon."
"Dar um título divertido e uma descrição envolvente."
"Este é um exemplo do formato desejado; não copiar o conteúdo:"
Itinerary.exemploViagemAoJapao
}
let resposta = try await sessao.respond(to: prompt, generating: Itinerary.self)
print(resposta.content)
}
// Exemplo para one-shot
extension Itinerary {
static var exemploViagemAoJapao: Itinerary {
Itinerary(
title: "Trilogia de Tókio",
description: "Três dias intensos explorando cultura, culinária e tecnologia.",
rationale: "Equilíbrio entre tradição, modernidade e descanso.",
days: [
DayPlan(
title: "Dia 1 — Shibuya e Shinjuku",
subtitle: "Ritmo urbano e vistas panorâmicas",
destination: "Tókio",
activities: [
Activity(title: "Cruzar Shibuya Crossing", description: "Movimento icônico com fotos no entardecer."),
Activity(title: "Hotel Sakura", description: "Check-in próximo à estação."),
Activity(title: "Restaurante Ichiran", description: "Ramen individualizado e rápido.")
]
),
DayPlan(
title: "Dia 2 — Asakusa e Ueno",
subtitle: "Templos, parque e museus",
destination: "Tókio",
activities: [
Activity(title: "Templo Senso-ji", description: "Caminho por Nakamise e compras de lembranças."),
Activity(title: "Hotel Sakura", description: "Pausa breve para descanso."),
Activity(title: "Restaurante Sushizanmai", description: "Sushis clássicos em ambiente informal.")
]
),
DayPlan(
title: "Dia 3 — Akihabara",
subtitle: "Eletrônicos, jogos e cultura pop",
destination: "Tókio",
activities: [
Activity(title: "Arcades e lojas", description: "Explorar retro games e figures."),
Activity(title: "Hotel Sakura", description: "Check-out e preparo para partida."),
Activity(title: "Restaurante Tonkatsu Maisen", description: "Tonkatsu crocante para fechar a viagem.")
]
)
]
)
}
}
Respostas por streaming: interação mais dinâmica
O streaming exibe resultados à medida que o modelo gera conteúdo, reduzindo o tempo de espera perceptível. Para isso, o tipo gerado passa a ser PartiallyGenerated, permitindo acesso opcional aos campos já preenchidos. A sessão fornece um AsyncSequence de respostas parciais, e a ViewModel atualiza o estado a cada iteração. A View lê propriedades de forma segura com unwraps, mantendo fluidez na UI. Essa abordagem melhora a experiência e mantém o mesmo esquema de dados.
A seguir, a ViewModel migra para streaming e a View exibe campos apenas quando disponíveis. O loop assíncrono coleta trechos e atualiza o itinerário parcial. O exemplo usa o mesmo prompt builder demonstrado anteriormente. Comentários destacam pontos importantes de transição e de exibição condicional.
@MainActor
final class GeradorDeItinerarioStreaming: ObservableObject {
private let marco: Landmark
private var sessao: LanguageModelSession
@Published private(set) var itinerario: Itinerary.PartiallyGenerated?
@Published var erro: Error?
init(marco: Landmark) {
self.marco = marco
let instrucoes = "A tarefa é criar um roteiro de viagem."
self.sessao = LanguageModelSession(instructions: instrucoes)
}
func gerar(dias: Int = 3) async {
do {
let prompt = Prompt {
"Gerar um roteiro de \(dias) dias para \(marco.name)."
"Dar um título divertido e uma descrição breve."
"Este é um exemplo do formato desejado; não copiar o conteúdo:"
Itinerary.exemploViagemAoJapao
}
let fluxo = sessao.streamResponse(
to: prompt,
generating: Itinerary.self
)
for try await parcial in fluxo {
self.itinerario = parcial.content
}
} catch {
self.erro = error
}
}
}
import SwiftUI
// View simples para exibir conteúdo parcial
struct ItinerarioViewStreaming: View {
let itinerario: Itinerary.PartiallyGenerated
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if let titulo = itinerario.title {
Text(titulo).font(.title).bold()
}
if let descricao = itinerario.description {
Text(descricao)
}
if let planos = itinerario.days {
ForEach(Array(planos.enumerated()), id: \.offset) { index, plano in
VStack(alignment: .leading, spacing: 6) {
if let t = plano.title {
Text("Dia \(index + 1) — \(t)").font(.headline)
}
if let s = plano.subtitle {
Text(s).foregroundColor(.secondary)
}
if let atividades = plano.activities {
ForEach(Array(atividades.enumerated()), id: \.offset) { _, atividade in
if let tt = atividade.title { Text("• \(tt)") }
if let dd = atividade.description {
Text(dd).foregroundColor(.secondary)
}
}
}
}
}
}
}
.padding()
}
}
Tool calling: ampliando capacidades com ferramentas
Ferramentas permitem que o modelo chame funções Swift para buscar dados ou executar ações. O protocolo Tool descreve nome, descrição, argumentos e a função de chamada. Argumentos são definidos como @Generable, garantindo que o modelo escolha valores válidos. A ferramenta de exemplo abaixo retorna hotéis e restaurantes fictícios para um marco. Em produção, a lógica pode consultar MapKit ou APIs próprias.
A enum de categorias estabelece as opções pesquisáveis que o modelo pode solicitar. Os argumentos explicitam o tipo de ponto de interesse esperado. A implementação de call(arguments:) formata um texto amigável com os resultados. Uma função auxiliar entrega dados simulados de maneira determinística. Esse padrão permite resultados ricos, locais e auditáveis.
import FoundationModels
// Ferramenta para buscar pontos de interesse
struct FindPointsOfInterestTool: Tool {
let landmark: Landmark
// Nome e descrição usados pelo modelo para decisão de uso
let name = "findPointsOfInterest"
let description = "Encontra pontos de interesse para um marco turístico."
// Categorias pesquisáveis
@Generable
enum Category: String, CaseIterable {
case hotel
case restaurant
}
// Argumentos que o modelo precisa fornecer ao chamar a ferramenta
@Generable
struct Arguments {
@Guide(description: "Tipo de ponto de interesse a procurar.")
let pointOfInterest: Category
}
// Chamada principal da ferramenta
func call(arguments: Arguments) async throws -> String {
let resultados = await obterSugestoes(
categoria: arguments.pointOfInterest,
nomeDoMarco: landmark.name
)
return """
Foram encontrados estes \(arguments.pointOfInterest) em \(landmark.name): \
\(resultados.joined(separator: ", "))
"""
}
// Dados simulados; em produção, integrar MapKit ou API própria
private func obterSugestoes(categoria: Category, nomeDoMarco: String) async -> [String] {
switch categoria {
case .hotel:
return ["Hotel 1", "Hotel 2", "Hotel 3"]
case .restaurant:
return ["Restaurante 1", "Restaurante 2", "Restaurante 3"]
}
}
}
Integrando a ferramenta na sessão e na ViewModel
Para o modelo usar ferramentas, a sessão recebe uma lista de instâncias e instruções que exigem seu uso. As instruções podem especificar, por exemplo, que cada dia contenha hotel e restaurante obtidos via ferramenta. A geração pode também ajustar amostragem para resultados determinísticos com GenerationOptions. O prompt inclui exemplo one-shot para reforçar formato e estilo. Em conjunto, a qualidade se mantém alta e os dados retornados ganham nomes concretos.
O código abaixo cria a sessão com a ferramenta, passa instruções claras e emite um prompt. A resposta tipada segue o esquema Itinerary, e a ferramenta é chamada de forma autônoma quando necessário. Esse desenho garante separação de responsabilidades entre modelo e integrações. A sessão também pode ser inspecionada para auditoria de chamadas de ferramentas. Esse mecanismo é particularmente útil em domínios com dados dinâmicos.
import FoundationModels
import Playgrounds
#Playground {
let marco = Landmark(name: "Grand Canyon")
let ferramentaPOI = FindPointsOfInterestTool(landmark: marco)
let instrucoes = Instructions {
"A tarefa é criar um roteiro de viagem."
"Para cada dia, incluir um hotel e um restaurante."
"Sempre usar a ferramenta 'findPointsOfInterest' para hotéis e restaurantes em \(marco.name)."
}
let sessao = LanguageModelSession(
tools: [ferramentaPOI],
instructions: instrucoes
)
let prompt = Prompt {
"Gerar um roteiro de 3 dias para \(marco.name)."
"Dar um título divertido e uma descrição envolvente."
}
let resposta = try await sessao.respond(
to: prompt,
generating: Itinerary.self,
options: GenerationOptions(sampling: .greedy) // saída mais determinística
)
print(resposta.content)
}
Desempenho: pré-aquecimento do modelo e otimização do prompt
O tempo até o primeiro token diminui ao carregar o modelo previamente na memória. O método prewarm() faz o pré-aquecimento e reduz a latência do primeiro uso perceptivelmente. Em prompts com bom exemplo one-shot, o esquema completo pode ser omitido do contexto. O parâmetro includeSchemaInPrompt: false reduz o tamanho do prompt e acelera o processamento. Essas otimizações somadas melhoram a fluidez geral da experiência.
O trecho seguinte adiciona uma função de pré-aquecimento na ViewModel. Em seguida, a geração por streaming inclui greedy sampling e omite o esquema no prompt. Os demais aspectos do fluxo permanecem inalterados, preservando segurança e previsibilidade. Essa combinação é recomendada para inicialização rápida e consistência de respostas. A abordagem é simples e traz ganhos importantes em aparelhos compatíveis.
@MainActor
final class GeradorOtimizado: ObservableObject {
private let marco: Landmark
private var sessao: LanguageModelSession
@Published private(set) var itinerario: Itinerary.PartiallyGenerated?
init(marco: Landmark) {
self.marco = marco
let ferramentaPOI = FindPointsOfInterestTool(landmark: marco)
let instrucoes = Instructions {
"A tarefa é criar um roteiro de viagem."
"Para cada dia, incluir um hotel e um restaurante."
"Sempre usar a ferramenta 'findPointsOfInterest' em \(marco.name)."
}
self.sessao = LanguageModelSession(tools: [ferramentaPOI], instructions: instrucoes)
}
// Pré-aquecimento do modelo
func prepararModelo() {
sessao.prewarm()
}
// Geração por streaming com otimizações
func gerar(dias: Int = 3) async {
let prompt = Prompt {
"Gerar um roteiro de \(dias) dias para \(marco.name)."
"Dar um título divertido e uma descrição breve."
"Este é um exemplo do formato desejado; não copiar o conteúdo:"
Itinerary.exemploViagemAoJapao
}
let fluxo = sessao.streamResponse(
to: prompt,
generating: Itinerary.self,
options: GenerationOptions(sampling: .greedy),
includeSchemaInPrompt: false
)
do {
for try await parcial in fluxo {
self.itinerario = parcial.content
}
} catch {
// Tratamento de erro omitido para brevidade
}
}
}
Checklist mental de arquitetura e boas práticas
Separação entre sessão, ViewModel e View mantém o código coeso e testável. Instruções estáveis reduzem variação e melhoram a qualidade das respostas. O uso de @Generable e @Guide desloca esforço de parsing para o modelo, simplificando a UI. O streaming melhora a percepção de performance e engajamento sem sacrificar estrutura. Ferramentas ampliam escopo com dados dinâmicos, mantendo controle e rastreabilidade.
Pré-aquecimento e prompts enxutos impactam diretamente a responsividade. Amostragem determinística com .greedy facilita depuração e consistência, especialmente em ambientes de demonstração. Exemplos one-shot funcionam como contrato de formato e tom, acelerando iteração de design. A checagem de disponibilidade evita arestas em dispositivos não elegíveis. Esses elementos formam um conjunto coeso para introduzir IA generativa on-device com segurança e clareza.