<!-- https://vatio.ai/pt/guias/agente-ia-no-seu-app-ios · Atualizado: 2026-10-11 -->

> Coloque no seu app de iPhone o mesmo agente de IA do WhatsApp e da web, com o pacote Swift da Vatio, autenticado como o seu usuário e com a sua própria UI.

# Um agente de IA no seu app iOS

Para colocar um agente de IA dentro do seu app de iPhone, você adiciona o pacote Swift da Vatio, cria um token publicável que permita o bundle id do app e liga um `VatioChat` às suas próprias views SwiftUI. O agente é o mesmo que você descreve em um arquivo `vatio.yml` e que responde no WhatsApp, no Instagram e na web. No app, ele também pode saber quem é o usuário: o seu backend assina um token, e as tools privadas do agente leem os dados desse usuário.

Este guia adiciona um assistente ao app de uma academia, do pacote até um aluno logado perguntando pelas reservas dele.

## Por que colocar o agente no app

Um usuário logado no seu app é um visitante cuja identidade você já conhece. No WhatsApp e no Instagram, o contato precisa [entrar na conta pelo seu site](https://vatio.ai/docs/authentication/sessions#on-whatsapp) antes de uma tool privada rodar. No app, o mesmo agente pode responder “em quais aulas eu estou inscrito?” com os dados daquele aluno.

Não é um segundo agente. Um workspace roda um único agente, e o app é mais um canal para ele. As instruções, as tools, o conhecimento, as regras de transferência e o inbox são os mesmos, então uma correção que você publica chega a todos os canais de uma vez.

## O que o SDK entrega e o que você constrói

O SDK é um pacote Swift para iOS 15 ou posterior, sem dependências além de Foundation e Combine, e sem UI.

| O SDK cuida de | Você constrói |
|---|---|
| A conversa como estado observável: `messages`, `isTyping`, `status` | Cada tela: lista de mensagens, balões, campo de texto, indicador de digitação |
| A mensagem do usuário na tela na hora, com `isPending`, e depois confirmada | Devolver o texto ao campo quando `send` lança um erro |
| Reconectar com backoff, fazer polling enquanto isso, buscar o que perdeu e deduplicar por id | Exibir `content`, que é Markdown escrito pelo agente: trate como não confiável |
| Guardar a conversa em `UserDefaults`, por workspace e por usuário logado | O seletor de fotos e o gravador que produzem os anexos |
| Os anexos, a transcrição de mensagens de voz e o momento de feedback, como estado | O cartão de avaliação e qualquer efeito de texto progressivo |
| Nome, avatar, cor de destaque, saudação e sugestões do agente, com `vatio.config()` | O seu login e o endpoint do seu backend que assina o token do usuário |

## Libere o seu app: um token publicável

O app fala com a Vatio usando um token publicável (`vatpub_…`), feito para ir dentro dele. Um token corresponde a um workspace e a um ambiente, e só funciona a partir das origens permitidas. A origem de um app iOS é `ios-app://` mais o bundle id:

```bash
vatio tokens create --env live --label ios --origin ios-app://example.academiaritmo.app
```

O `create` imprime o token completo uma única vez. Para adicionar o app a um token que já existe, `vatio tokens origins PREFIX URL...` substitui a lista desse token, então repita as origens que ele já tinha; a seção de tokens de **Canais → Widget web**, no inbox, faz o mesmo. O SDK envia o bundle id de `Bundle.main` em minúsculas, e um token que não o permite falha com `origin_not_allowed`.

A origem mantém o token no seu app. Ela não diz nada sobre quem é o usuário; isso vem do seu backend, mais abaixo. Nunca coloque um token de desenvolvedor (`vat_…`) em um app: com ele é possível fazer deploy, ler secrets e criar tokens.

## Adicione o pacote

No Xcode, escolha File → Add Package Dependencies…, digite `https://github.com/vatio-ai/ios` e adicione a biblioteca `Vatio` ao target do app. Em um `Package.swift`:

```swift
dependencies: [
    .package(url: "https://github.com/vatio-ai/ios", from: "0.1.0")
],
targets: [
    .target(name: "RitmoKit", dependencies: [.product(name: "Vatio", package: "ios")])
]
```

As tags seguem semver, então `from:` nunca leva você a uma nova versão major de surpresa.

## Uma primeira conversa

```swift
import SwiftUI
import Vatio

struct AssistenteView: View {
    @ObservedObject var chat: VatioChat
    @State private var texto = ""

    var body: some View {
        VStack {
            ScrollView {
                LazyVStack(spacing: 12) {
                    ForEach(chat.messages) { message in
                        Text(LocalizedStringKey(message.content))   // Markdown básico
                            .frame(maxWidth: .infinity,
                                   alignment: message.isFromVisitor ? .trailing : .leading)
                            .opacity(message.isPending ? 0.6 : 1)
                    }
                    if chat.isTyping { ProgressView() }
                }
                .padding()
            }
            HStack {
                TextField("Pergunte o que quiser", text: $texto)
                Button("Enviar", action: enviar)
            }
            .padding()
        }
        .task { await chat.resume() }
    }

    private func enviar() {
        let pergunta = texto
        texto = ""
        Task {
            do { try await chat.send(pergunta) }
            catch { texto = pergunta }   // o send removeu a mensagem pendente
        }
    }
}
```

Crie o chat uma única vez, por exemplo como um `@StateObject` com `VatioChat(Vatio(workspace: "academia-ritmo", token: "vatpub_..."))`. Criá-lo não abre nada: a conversa começa no primeiro `send`, então um usuário que nunca escreve nunca vira contato. `resume()` reabre a conversa guardada, se houver, e nunca começa uma nova. `send` lança um `VatioError` com um `code`; as falhas fora de um `send` ficam em `chat.lastError`.

## Autentique o usuário com um token assinado pelo seu backend

Para dizer ao agente quem é o usuário, o seu backend assina um JWT e o app o repassa. **A chave privada nunca vai no app**: um binário pode ser desmontado, e com essa chave qualquer pessoa poderia se passar por qualquer usuário seu. A Vatio guarda só a metade pública, então consegue verificar uma identidade, mas nunca criar uma.

Rode `vatio auth --new-key` uma vez. O `identity.pub` fica no workspace, referenciado no bloco `auth:` do `vatio.yml`. O `identity.pem` vai para o seu backend como secret, não para o `vatio secrets` nem para o app. Depois adicione um endpoint, atrás do login do seu app, que assine um token RS256 de curta duração:

```js
// O seu backend, por exemplo POST /me/vatio-token, atrás do login que você já tem
import jwt from "jsonwebtoken";

const visitorToken = jwt.sign(
  { sub: String(user.id), name: user.name, email: user.email },
  process.env.VATIO_IDENTITY_PRIVATE_KEY,
  { algorithm: "RS256", audience: "academia-ritmo", expiresIn: "1h" }
);
```

O `sub` vira `$auth.subject`, o `aud` precisa ser exatamente o slug do workspace, e o `exp` é conferido de novo a cada chamada de tool. Se você adicionar um claim `metadata` com um link `profile` e `badges`, o inbox os mostra no cabeçalho da conversa, para quem lê saber de qual aluno se trata.

O `VatioChat` recebe o token na criação, então entrar ou sair da conta significa criar um novo. Um store pequeno, como o do app de exemplo do SDK, cuida disso:

```swift
@MainActor
final class AssistenteStore: ObservableObject {
    @Published private(set) var chat: VatioChat
    private let vatio: Vatio

    init(vatio: Vatio) {
        self.vatio = vatio
        chat = VatioChat(vatio)
    }

    // visitorToken: o que o /me/vatio-token do seu backend devolveu
    func entrou(visitorToken: String) async {
        chat.close()
        chat = VatioChat(vatio, visitorToken: visitorToken)
        await chat.resume()
    }

    func saiu() {
        chat.close()
        chat = VatioChat(vatio)
    }
}
```

## Agora as tools privadas rodam para esse usuário

Com um token verificado, as tools marcadas `access: private` rodam e chegam à sua API com o token do próprio usuário. `tools/minhas_reservas.yml`:

```yaml
description: As próximas reservas de aulas do aluno logado.
when_to_use: Um aluno logado pergunta pelas próprias reservas ou aulas.
access: private
request:
  method: GET
  base_url: "$env.RITMO_API"
  path: /me/reservas
  headers:
    Authorization: "Bearer $auth.token"
respond:
  data:
    reservas: "$.reservas"
  message:
    when_empty: Este aluno não tem reservas futuras.
    default: "Encontrei {{count}} reservas futuras."
```

O seu endpoint verifica o JWT com a mesma chave pública e limita a consulta ao `sub` dele. A tool não recebe um id de aluno, e nem pode: uma tool privada que declara `member_id`, `user_id`, `customer_id`, `account_id`, `patient_id` ou `subject` faz o deploy falhar, porque quem preenche os parâmetros é o modelo, e ele pode ser convencido a colocar o de outra pessoa. `reservar_aula` também é privada e recebe só o id da aula.

```yaml
workspace: academia-ritmo
agent:
  name: Ritmo
  instructions: |
    Você ajuda os alunos da Academia Ritmo com aulas, horários e as
    próprias reservas.

    Horários: chame grade_de_aulas.
    As reservas do aluno: chame minhas_reservas, e reservar_aula para
    reservar. Se você não consegue ver as reservas porque ele não entrou
    na conta, peça que ele entre no app primeiro. Nunca invente uma reserva.
  tools: [grade_de_aulas, minhas_reservas, reservar_aula, remember, recall]
auth:
  public_key: identity.pub
```

A `grade_de_aulas` é pública, então também responde no WhatsApp e no Instagram para quem não entrou na conta. [Conectar seu backend](/pt/guias/conectar-seu-backend) aprofunda as tools e os tokens.

## Continuidade da conversa

- **Reabrir.** A conversa e o id do visitante ficam guardados em `UserDefaults`, por workspace e por usuário logado. Chame `resume()` quando a view aparecer.
- **Entrar na conta mantém a conversa.** Uma conversa anônima continua quando o usuário entra, então ele não precisa digitar de novo a pergunta que o levou a fazer login. Um token novo para o mesmo `sub` mantém a mesma conversa.
- **Outro usuário é outra conversa.** Um token com outro `sub` tem conversa e histórico próprios, então sair da conta no seu app também sai do chat, e um celular compartilhado nunca mostra a um aluno a conversa de outro.
- **Doze horas.** As credenciais do chat valem 12 horas. Depois disso, `resume()` não encontra nada e o próximo `send` começa uma conversa nova; o histórico fica no servidor.
- **Conversas anteriores.** `vatio.conversations(visitorToken:)` lista as conversas desse usuário neste aparelho, da mais recente para a mais antiga, e `chat.open(_:)` troca para uma delas. `chat.startOver()` esquece a conversa guardada sem apagar nada no servidor.
- **Memória.** A memória é do contato: com `remember` e `recall` ativadas, o agente vê as notas guardadas sobre um usuário logado, então uma conversa nova pode partir do que as anteriores já estabeleceram.

## Fotos e mensagens de voz

`send(_:attachments:)` aceita até quatro `VatioUpload`: imagens (JPEG, PNG, WebP, GIF, HEIC), PDFs, arquivos de texto ou áudio (M4A, MP3, OGG, WAV, AAC, FLAC), de até 8 MB cada. Com arquivos, o texto pode ir vazio:

```swift
try await chat.send("Essa é a pegada certa?", attachments: [
    VatioUpload(data: jpegData, filename: "foto.jpg", contentType: "image/jpeg")
])
try await chat.send(attachments: [try VatioUpload(fileURL: gravacaoURL)])  // um .m4a do AVAudioRecorder
```

Uma mensagem de voz é transcrita em segundo plano: a mensagem mostra "Audio" no começo, o `content` vira a transcrição alguns segundos depois, e o agente responde depois disso. Quando `isMediaLabel` é true, desenhe o anexo em vez da palavra. Gravar exige `NSMicrophoneUsageDescription` no seu Info.plist.

## Teste contra o preview

O token decide com qual ambiente o app fala, então um build de desenvolvimento não deve levar o do live. Crie um para o `preview`:

```bash
vatio push
vatio tokens create --env preview --label ios-dev --origin ios-app://example.academiaritmo.app
```

`vatio env preview` mostra os dados desse ambiente. Escolha o token pela configuração do build:

```swift
#if DEBUG
let vatio = Vatio(workspace: "academia-ritmo", token: "vatpub_TOKEN_PREVIEW")
#else
let vatio = Vatio(workspace: "academia-ritmo", token: "vatpub_TOKEN_LIVE")
#endif
```

O `vatio chat` fala como você, o desenvolvedor, e não como um aluno, então as tools privadas se testam pelo próprio app. Entre na conta no simulador, pergunte pelas suas reservas e leia a conversa com `vatio chat show CHAT_ID` (o `chat.conversationID` tem o id). Ele mostra cada chamada de tool, o que o seu backend respondeu e se o token foi aceito: uma chave errada, um `exp` vencido ou um `aud` incorreto deixam o usuário anônimo em vez de dar erro. `no_agent_deployed` significa que ainda não há nenhum deploy no ambiente do token. Depois peça as reservas de outro aluno: sem um parâmetro de id não há o que o modelo preencher, e o seu endpoint só responde pelo `sub` do token. A [documentação de deploys](/docs/deployments) explica preview e live.

## Limites

Pelos [limites do SDK](https://vatio.ai/docs/sdk/limits), os mesmos da web:

- **Mensagens:** até 4.000 caracteres (`content_too_long`), com até quatro arquivos de 8 MB cada.
- **O tipo do arquivo** é lido dos bytes, não do nome; um tipo que o agente não consegue ler é recusado com `unsupported_file_type`.
- **Limites de uso,** por IP e workspace, por minuto: 10 conversas iniciadas, 30 mensagens enviadas, 60 leituras de configuração e 60 listagens de conversas. Acima disso, as requisições respondem HTTP 429 e o SDK lança `rate_limited`: espere antes de tentar de novo, em vez de repetir na hora.
- **As credenciais** valem 12 horas; uma vencida aparece como `subscription_rejected`, e o próximo `send` começa uma conversa nova.
- **O estilo de resposta** fica fixo quando a conversa começa: `.stream` (o padrão: uma resposta completa, não token a token), `.paced` (mensagens curtas com indicadores de digitação) ou `.instant`. O app de exemplo pinta a resposta de forma progressiva na tela.

## O que o SDK não faz

- **Não há notificações push.** Nada no SDK se registra para recebê-las. As respostas chegam enquanto o app está conectado, e quando ele volta do segundo plano o SDK reconecta e busca o que perdeu.
- **Não há chamadas de voz.** Mensagens de voz, sim; uma conversa de voz ao vivo, não.
- **O agente não envia arquivos.** Ele lê imagens, áudio e PDFs, mas responde em texto: não consegue gerar nem enviar arquivos ou imagens.
- **O agente não escreve primeiro.** Ele responde ao que o usuário envia; nunca começa uma conversa.

## Publique

```bash
vatio publish
```

Lance o build com o token do live. As conversas do app chegam ao mesmo inbox que as do WhatsApp e da web, com cada chamada de tool. Um supervisor pode assumir uma conversa ou marcar uma resposta errada com uma flag, e cada flag vira um caso de eval a partir do qual o seu coding agent corrige: veja [Melhorar seu agente](/pt/guias/melhorar-seu-agente-de-ia). A versão web desta mesma configuração está em [Um chat com IA para o seu site](/pt/guias/chat-com-ia-para-seu-site), e cada método, na [documentação do SDK de iOS](https://vatio.ai/docs/sdk/ios).

## Perguntas frequentes

### Como adiciono um agente de IA com chat ao meu app iOS?

Adicione o pacote Swift https://github.com/vatio-ai/ios, crie um token publicável cuja origem permitida seja ios-app:// mais o bundle id do seu app, e crie um VatioChat com ele. O SDK mantém a conversa como estado observável, então uma view SwiftUI que o observa é redesenhada conforme as respostas chegam. O agente é o mesmo vatio.yml que atende os seus outros canais.

### É seguro colocar o token da Vatio dentro de um app iOS?

O token publicável vatpub_ foi feito para ir dentro do app. Ele corresponde a um workspace e a um ambiente e só funciona a partir das origens permitidas, mas não prova quem é o usuário. Nunca coloque em um app um token de desenvolvedor vat_ nem a sua chave privada de identidade.

### O agente consegue ler os dados do usuário logado no meu app?

Sim. O seu backend assina um JWT RS256 de curta duração para o usuário, o app passa esse token ao VatioChat como visitor token, e as tools marcadas access: private rodam para esse usuário. O seu endpoint verifica o mesmo token e limita a consulta ao sub dele.

### O SDK de iOS da Vatio vem com uma interface de chat?

Não. É um pacote Swift sem UI e sem dependências: ele entrega as mensagens, o indicador de digitação, o status da conexão, os arquivos e o momento de feedback como estado, e você desenha as telas. O README do pacote tem um exemplo completo em SwiftUI, e o repositório inclui um app de exemplo que dá para rodar.

### O SDK de iOS tem notificações push ou chamadas de voz?

Não. As respostas chegam por um WebSocket enquanto o app está conectado, e o SDK busca o que perdeu quando reconecta. Os usuários podem mandar mensagens de voz, que são transcritas antes de o agente responder, mas não há chamadas de voz e o agente responde em texto.
