← Todos os Posts

Padrões espaciais do visionOS além da janela

A maioria dos apps que chega ao visionOS entra na plataforma pelo caminho de compatibilidade “Designed for iPad” da Apple: o binário de iPad já existente roda como um painel plano flutuando no espaço 3D, e o desenvolvedor marca uma caixinha em vez de construir uma experiência nativa de visionOS. O caminho é aceitável para o usuário (o app funciona), mas subestima a plataforma. A superfície nativa do visionOS oferece ao desenvolvedor três métodos de apresentação (janelas, volumes e espaços imersivos), além de primitivos estruturais de UI (ornamentos e attachments) que o SDK do iPad não tem.4 Apps que adotam esse vocabulário parecem nativos; os que não adotam ficam com jeito de iPad-no-Vision.

Este post percorre o vocabulário espacial com base na documentação da Apple. O recorte é “o que a plataforma de fato entrega a um app SwiftUI”, e não uma introdução ao visionOS. O post RealityKit e o modelo mental espacial, do mesmo cluster, cobre a camada de conteúdo 3D; este aqui cobre a superfície SwiftUI que a contém.

TL;DR

  • Apps visionOS compõem três tipos de cena: WindowGroup (janelas), WindowGroup com .windowStyle(.volumetric) (volumes) e ImmersiveSpace (espaços imersivos)1.
  • Uma janela é um plano 2D; um volume é uma região 3D delimitada; um espaço imersivo envolve o usuário. Cada um tem regras próprias: volumes têm tamanho imutável depois de criados, espaços imersivos exigem abertura e fechamento explícitos, e janelas se comportam de forma mais parecida com o iPad.
  • A imersão vem em três estilos: .mixed (o conteúdo convive com o cômodo real), .full (o cômodo é substituído por um ambiente virtual) e .progressive (meio-termo, com referência periférica ao ambiente)2.
  • Ornamentos são planos de UI paralelos a uma janela e posicionados à frente dela no eixo z. É assim que o visionOS faz barras de ferramentas e barras de abas3. Attachments incorporam views SwiftUI dentro do conteúdo 3D de uma RealityView — a ponte entre a UI plana e a geometria espacial.
  • O antipadrão do “app-painel”: publicar a UI de iPad como uma janela, sem adotar volume, espaço nem ornamento. O usuário consegue usar o app, mas o valor real da plataforma fica intocado.

Os três tipos de cena

O corpo App de um app visionOS compõe cenas a partir de três classes. Cada uma carrega um modelo mental distinto para o usuário.

Janelas: o plano 2D

WindowGroup produz uma janela 2D com a moldura de vidro do visionOS por padrão. A janela é posicionada no espaço (o sistema a coloca à frente de onde o usuário está olhando) e é movida ou redimensionada pelo usuário com os gestos padrão do sistema. Do ponto de vista do SwiftUI, uma janela é o análogo visionOS de uma janela do macOS: uma superfície de conteúdo plana com um material de vidro sensível à profundidade.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

A janela padrão tem um material de vidro em volta do conteúdo. Apps que querem uma superfície totalmente transparente usam .windowStyle(.plain):

WindowGroup {
    ContentView()
}
.windowStyle(.plain)

Janelas no estilo plain perdem a moldura de vidro do sistema. Use-as quando o próprio conteúdo fornece seu contêiner visual; fora isso, o padrão é o correto.

Volumes: a região 3D delimitada

Um volume é uma região 3D que abriga conteúdo com profundidade (um modelo, uma cena com vários objetos, uma UI que ganha com um terceiro eixo). A cena de volume também é um WindowGroup, com um estilo diferente:

WindowGroup(id: "globe") {
    GlobeView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)

O modificador .defaultSize(width:height:depth:in:) especifica os limites do volume em unidades do mundo real (metros). Por padrão, esses limites são fixados na abertura, e o usuário pode mover o volume, mas não redimensioná-lo. O visionOS 2+ acrescentou um caminho opcional via .windowResizability(.contentSize) e APIs relacionadas, para apps que querem volumes redimensionáveis pelo usuário; o padrão de tamanho fixo segue sendo o caso mais comum. A implicação: escolha o tamanho padrão com cuidado, porque a maioria dos volumes não é redimensionável a menos que o desenvolvedor opte explicitamente por isso.

Os bons candidatos a volume são apps em que o limite espacial faz parte da experiência: uma escultura virtual em torno da qual o usuário caminha, uma trena fixada em uma parede real, uma cena de treino com alvos escalonados em profundidade. Apps que só querem uma tela maior não ganham nada com um volume; uma janela maior é a resposta certa.

Espaços imersivos: o entorno

Um ImmersiveSpace é uma cena que ocupa o ambiente ao redor do usuário.5 Diferentemente de uma janela ou de um volume (ambos visíveis ao lado de outros apps no Espaço Compartilhado), um espaço imersivo assume o entorno e impede o uso simultâneo das janelas de outros apps.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }

        ImmersiveSpace(id: "training") {
            TrainingScene()
        }
        .immersionStyle(selection: .constant(.mixed), in: .mixed, .progressive, .full)
    }
}

O modificador .immersionStyle(...) escolhe o nível da experiência:

  • .mixed. O conteúdo virtual aparece junto com o cômodo real. Usado em apps em que o usuário se beneficia dos dois contextos.
  • .progressive. Uma imersão parcial que sobe ou desce conforme o usuário gira a Digital Crown. O usuário mantém a percepção periférica do ambiente enquanto a visão central é virtual.
  • .full. O cômodo é substituído por um ambiente virtual. Usado em experiências totalmente imersivas (meditação, simulações de treinamento, jogos).

Abrir um espaço imersivo é explícito. O app chama @Environment(\.openImmersiveSpace) com o id do espaço; o sistema cuida da animação de transição e do fechamento de qualquer espaço conflitante:

@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace

Button("Start Session") {
    Task {
        await openImmersiveSpace(id: "training")
    }
}

Só um espaço imersivo pode ficar ativo por vez em cada app. A transição entre espaços (de .mixed para .full, por exemplo) exige fechar explicitamente o espaço antigo e abrir o novo.

Ornamentos: os planos de UI ao redor da janela

Ornamentos são views SwiftUI presas à borda de uma janela, posicionadas um pouco à frente do plano da janela no eixo z. É assim que o visionOS faz barras de ferramentas, barras de abas e controles acessórios. O sistema usa ornamentos o tempo todo: os controles de reprodução no app TV, o controle segmentado no Música, a barra de ferramentas no Mail.

ContentView()
    .ornament(
        attachmentAnchor: .scene(.bottom),
        contentAlignment: .center
    ) {
        HStack {
            Button("Previous", systemImage: "backward.fill") { ... }
            Button("Play", systemImage: "play.fill") { ... }
            Button("Next", systemImage: "forward.fill") { ... }
        }
        .padding()
        .glassBackgroundEffect()
    }

O parâmetro attachmentAnchor: especifica onde o ornamento fica em relação à janela: .scene(.top), .scene(.bottom), .scene(.leading), .scene(.trailing). O tratamento visual do ornamento é responsabilidade do desenvolvedor; .glassBackgroundEffect() produz o material de vidro nativo do visionOS, que combina com a moldura da janela.

Ornamentos resolvem um problema real no visionOS: colocar controles dentro da janela deixa o conteúdo apertado; colocá-los em outra janela obriga o usuário a redirecionar o olhar. Um ornamento paira na visão periférica, pode ser mirado com o olhar e não disputa a área central com o conteúdo principal.

Attachments do RealityView: SwiftUI dentro do espaço 3D

Quando um app precisa de views SwiftUI dentro de uma cena 3D (um rótulo sobre um modelo 3D, um botão flutuando perto de um objeto virtual, uma leitura de medição fixada em uma superfície real), a ponte é o mecanismo de attachments da RealityView.

RealityView { content, attachments in
    let model = ModelEntity(...)
    content.add(model)

    if let label = attachments.entity(for: "label") {
        label.position = [0, 0.5, 0]
        model.addChild(label)
    }
} attachments: {
    Attachment(id: "label") {
        Text("Vintage Globe, 1872")
            .padding()
            .glassBackgroundEffect()
    }
}

O closure attachments: declara views SwiftUI com identificadores estáveis. Dentro do closure principal da RealityView, attachments.entity(for:) recupera a view como uma Entity 3D que pode ser posicionada no espaço de coordenadas da cena. A view participa do ciclo de atualização do SwiftUI (mudanças de estado redesenham a view) e, ao mesmo tempo, é renderizada como um plano texturizado na cena 3D.

Esse é o mecanismo certo para qualquer UI dentro do mundo 3D: um rótulo que acompanha um objeto em movimento, uma anotação de medida, um botão contextual. A autoria da view SwiftUI não muda; o posicionamento 3D acontece na camada da RealityView.

O antipadrão do “app-painel”

O erro mais comum ao publicar no visionOS é o app-painel: um app de iPad que chega à plataforma pela compatibilidade “Designed for iPad” e é lançado como uma única janela, sem volume, sem espaço imersivo e sem ornamentos. O app funciona, mas não conquista a plataforma.

Três sinais de que um app é um app-painel:

Uma única cena de janela. Nenhum .windowStyle(.volumetric), nenhum ImmersiveSpace declarado. O app é uma superfície plana e nada além disso.

Nenhum ornamento adotado. A barra de abas vive dentro do conteúdo da janela, em vez de ficar fora dela. O resultado fica mais carregado do que um app nativo de visionOS com a mesma densidade de conteúdo.

Nenhum recurso exclusivamente espacial. O app não usa o terceiro eixo para nada: nenhum modelo 3D em um volume, nenhuma cena ambiental em um espaço, nenhuma UI posicionada em z via attachments. O app faz o mesmo que fazia no iPad, só que flutuando.

Apps-painel não são fracassos; são a decisão certa para categorias de conteúdo que não se beneficiam da computação espacial (um app de chat, um app de notas, um utilitário de configurações). O modo de falhar é publicar um app-painel e reivindicar autoridade nativa em visionOS por causa dele. O post A matriz de plataformas da Apple, do mesmo cluster, defende que incluir uma plataforma é uma decisão de produto; no caso do visionOS, a decisão é: “este app deve conquistar a superfície espacial ou o painel já basta?”

Falhas comuns

Três padrões que produzem uma UX ruim no visionOS:

Volumes que, na prática, são conteúdo 2D com preenchimento de profundidade. Uma UI “3D” que ocupa um volume mas renderiza planos chapados lá dentro desperdiça espaço. Volumes são para conteúdo 3D; conteúdo plano pertence a uma janela.

Estilo de imersão que briga com o caso de uso. Um app de meditação que só oferece imersão .full tira o usuário do próprio ambiente em sessões curtas. Um app de treino que só oferece .mixed não vai longe o bastante para exercícios de foco total. Adeque o estilo de imersão à sessão real do usuário.

Ornamentos competindo com o conteúdo. Ornamentos são periféricos por design. Um ornamento que exige atenção central (cor piscando, movimento animado) destrói o próprio propósito. Use ornamentos para controles estáveis, de leitura rápida.

O que esse padrão significa para apps visionOS

Três conclusões.

  1. Escolha o tipo de cena pelo modelo mental do usuário, não pelo que é fácil. Uma lista plana de itens é uma janela. Um modelo 3D que o usuário inspeciona é um volume. Um ambiente que envolve o usuário é um espaço imersivo. Combinar os três em um mesmo app (uma janela com um volume aberto sob demanda, um espaço imersivo acessível pelo botão de uma janela) é o padrão nativo do visionOS.

  2. Adote ornamentos para barras de ferramentas e UI acessória. É por meio deles que o visionOS comunica “esta UI é complementar”; colocar barras de ferramentas dentro do conteúdo da janela passa a impressão de iPad-no-Vision. A integração é pequena e a diferença visual é grande.

  3. Use attachments para UI dentro do mundo 3D na RealityView. Rótulos sobre objetos 3D, botões perto de conteúdo virtual, leituras contextuais. A ponte entre SwiftUI e o espaço 3D já está resolvida; o modo de falhar é não usá-la e acabar renderizando texto 3D de forma improvisada.

O cluster completo do ecossistema Apple: App Intents tipados; servidores MCP; a questão do roteamento; Foundation Models; a distinção entre ambiente de execução e LLM de ferramentas; três superfícies; o padrão de fonte única da verdade; Dois servidores MCP; hooks para desenvolvimento Apple; Live Activities; o contrato do ambiente de execução do watchOS; do que o SwiftUI é feito; o modelo mental espacial do RealityKit; disciplina de schema no SwiftData; padrões de Liquid Glass; publicação multiplataforma; a matriz de plataformas; o framework Vision; Symbol Effects; inferência com Core ML no dispositivo; a API do Writing Tools; Swift Testing; Privacy Manifest a fundo; acessibilidade como plataforma; tipografia SF Pro; sobre o que me recuso a escrever. O hub fica na série Ecossistema Apple. Para um contexto mais amplo de iOS com agentes de IA, veja o guia de desenvolvimento iOS com agentes.

FAQ

Qual é a diferença entre um volume e um espaço imersivo?

Um volume é uma região 3D delimitada que vive no Espaço Compartilhado, ao lado dos outros apps. O usuário pode caminhar ao redor dele, o sistema o emoldura e as janelas dos demais apps continuam visíveis. Um espaço imersivo envolve o usuário, assume o ambiente e impede o uso simultâneo de outros apps. Volumes são para “olhe esta coisa em 3D”; espaços são para “esteja neste ambiente”.

Posso abrir vários volumes ao mesmo tempo?

Sim. Várias cenas WindowGroup com .volumetric podem ficar abertas simultaneamente, cada uma com seu tamanho e conteúdo. O sistema posiciona cada uma de forma independente no espaço.

Posso abrir vários espaços imersivos ao mesmo tempo?

Não. Só um espaço imersivo pode ficar ativo por app de cada vez. Alternar entre espaços exige fechar explicitamente o atual e abrir o novo por meio de @Environment(\.openImmersiveSpace) e @Environment(\.dismissImmersiveSpace).

O tamanho do volume é realmente imutável?

Os limites do volume são fixados na abertura por padrão; a leitura do HIG do visionOS é que volumes representam conteúdo 3D específico, com limites intencionais, e que o redimensionamento arbitrário pelo usuário distorceria a escala pretendida do conteúdo. O visionOS 2+ acrescentou uma opção para o desenvolvedor habilitar volumes redimensionáveis via .windowResizability(.contentSize) e APIs relacionadas, então apps que precisam de contêineres espaciais redimensionáveis podem solicitar isso. A maioria dos volumes é publicada com o padrão fixo, que o HIG continua recomendando para conteúdo de escala específica (uma escultura virtual, um modelo em tamanho real).

Como adiciono uma barra de abas a uma janela do visionOS?

Use uma TabView dentro da janela para abas no próprio conteúdo (o padrão estilo iPad) ou use um ornamento com linhas de botões personalizadas para uma UI de abas periférica, nativa do visionOS. O caminho do ornamento é o que os próprios apps da Apple usam (Música, Mail) e o que soa mais natural para quem usa visionOS.

Os attachments da RealityView funcionam com rastreamento de mãos?

Sim. Uma vez posicionados, os attachments são entidades 3D e participam do mesmo sistema de gestos e hit-testing das demais entidades do RealityKit. Gestos de toque, arrasto e hover são associados a eles pelos modificadores de gesto padrão do SwiftUI; o post sobre RealityKit, do mesmo cluster, cobre os padrões de integração com rastreamento de mãos.

Referências


  1. Apple Developer: Meet SwiftUI for spatial computing (sessão 10109 da WWDC 2023). Apresentação de WindowGroup, WindowGroup volumétrico e ImmersiveSpace como os três tipos de cena do visionOS. 

  2. Documentação Apple Developer: ImmersionStyle. Os três estilos de imersão (.mixed, .progressive, .full) e a API do modificador .immersionStyle(selection:in:)

  3. Documentação Apple Developer: ornament(visibility:attachmentAnchor:contentAlignment:ornament:). O modificador de view do SwiftUI que adiciona um plano de UI de ornamento a uma janela com a âncora especificada. 

  4. Apple Developer: Go beyond the window with SwiftUI (sessão 10111 da WWDC 2023). A sessão que cobre volumes, espaços imersivos e os padrões para ir além da UI de painel plano no visionOS. 

  5. Documentação Apple Developer: Creating an immersive space in visionOS with SwiftUI. O guia completo para definir e abrir espaços imersivos. 

Artigos relacionados

RealityKit e o modelo mental espacial

RealityKit é um sistema de entidade-componente, não SwiftUI em 3D. Cinco maneiras pelas quais o modelo difere de uma jan…

14 min de leitura

O que há de novo no visionOS 27 para devs de experiências espaciais

O visionOS 27 adiciona Spatial Preview do Mac, Foveated Streaming de um PC, ambientes imersivos no Safari e rastreamento…

16 min de leitura

Instalar e atualizar o Codex CLI: Mac, Linux, Windows

Todas as formas de instalar, atualizar, fixar versão e desinstalar a CLI do OpenAI Codex -- script de instalação, npm, H…

16 min de leitura