← Todos os Posts

Padrões espaciais do visionOS além da janela

A maioria dos apps que chegam ao visionOS alcança a plataforma pelo caminho de compatibilidade “Designed for iPad” da Apple: o binário de iPad 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 do visionOS. O caminho é aceitável para o usuário (o app funciona), mas vende a plataforma por menos do que ela vale. A superfície nativa do visionOS oferece aos desenvolvedores três métodos de apresentação (Windows, Volumes e Immersive Spaces) além de primitivas estruturais de UI (Ornaments, Attachments) que o SDK do iPad não tem. Apps que adotam esses recursos parecem nativos; os que não adotam soam como iPad-no-Vision.

O post percorre o vocabulário espacial à luz da documentação da Apple. O recorte é “o que a plataforma realmente oferece a um app SwiftUI” em vez de uma introdução ao visionOS. O post do cluster RealityKit and the Spatial Mental Model cobre a camada de conteúdo 3D; este post cobre a superfície SwiftUI que a contém.

TL;DR

  • Apps visionOS compõem três tipos de cena: WindowGroup (Windows), WindowGroup com .windowStyle(.volumetric) (Volumes) e ImmersiveSpace (Immersive Spaces)1.
  • Uma Window é um plano 2D; um Volume é uma região 3D delimitada; um Immersive Space envolve o usuário. Cada um tem regras diferentes: Volumes têm tamanho imutável após a criação, Immersive Spaces exigem abertura/dispensa explícita, Windows se comportam de forma mais parecida com o iPad.
  • A imersão vem em três estilos: .mixed (o conteúdo coexiste com a sala), .full (a sala é substituída por um ambiente virtual), .progressive (meio-termo com ancoragem periférica)2.
  • Ornaments são planos de UI paralelos a uma Window e à frente no eixo z. É assim que o visionOS faz toolbars e tab bars3. Attachments incorporam views SwiftUI dentro de conteúdo 3D em uma RealityView, a ponte entre UI plana e geometria espacial.
  • O antipadrão “panel app”: entregar a UI do iPad como uma Window sem adoção de Volume, Space ou Ornament. O usuário consegue usar o app, mas o valor real da plataforma fica sem reivindicação.

Os três tipos de cena

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

Windows: o plano 2D

WindowGroup produz uma Window 2D com o frame de vidro do visionOS por padrão. A Window é posicionada no espaço (o sistema a coloca na frente do ponto para onde o usuário está olhando) e é movida ou redimensionada pelo usuário por meio de gestos padrão do sistema. Do ponto de vista do SwiftUI, uma Window é o análogo no 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 Window padrão tem um material de vidro ao redor do conteúdo. Apps que querem uma superfície totalmente transparente usam .windowStyle(.plain):

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

Windows de estilo plain perdem o frame de vidro do sistema. Use-as quando o conteúdo fornece seu próprio container visual; caso contrário, o padrão é o correto.

Volumes: a região 3D delimitada

Um Volume é uma região 3D que contém conteúdo sensível à profundidade (um modelo, uma cena com múltiplos objetos, uma UI que se beneficia de 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, os limites são fixados na abertura, e o usuário pode mover o volume mas não redimensioná-lo. O visionOS 2+ adicionou 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 continua 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 candidatos certos a Volumes 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 apenas querem uma tela mais ampla não ganham nada com um Volume; uma Window maior é a resposta certa.

Immersive Spaces: o envolvimento

Um ImmersiveSpace é uma cena que ocupa o ambiente ao redor do usuário. Diferentemente de uma Window ou Volume (ambos visíveis junto a outros apps no Shared Space), um Immersive Space toma conta do entorno do usuário e bloqueia 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. Conteúdo virtual aparece junto à sala real. Usado para apps em que o usuário se beneficia dos dois contextos.
  • .progressive. Uma imersão parcial que usa a Digital Crown para aumentar ou diminuir. O usuário mantém consciência periférica da sala enquanto a visão central é virtual.
  • .full. A sala é substituída por um ambiente virtual. Usado para experiências totalmente imersivas (meditação, simulações de treino, jogos).

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

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

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

Apenas um Immersive Space pode estar ativo por vez por app. A transição entre Spaces (de .mixed para .full, por exemplo) exige a dispensa explícita do Space antigo e a abertura do novo.

Ornaments: os planos de UI ao redor de uma Window

Ornaments são views SwiftUI fixadas à borda de uma Window, posicionadas ligeiramente à frente do plano da Window no eixo z. É assim que o visionOS faz toolbars, tab bars e controles acessórios. O sistema usa ornaments por toda parte: os controles de reprodução em TV, o segmented control em Music, a toolbar em 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 ornament fica em relação à Window: .scene(.top), .scene(.bottom), .scene(.leading), .scene(.trailing). O tratamento visual do ornament é responsabilidade do desenvolvedor; .glassBackgroundEffect() produz o material de vidro nativo do visionOS que combina com o frame da Window.

Ornaments resolvem um problema real no visionOS: colocar controles dentro da Window sobrecarrega o conteúdo; colocá-los em uma Window separada força o usuário a redirecionar o olhar. Um ornament flutua na visão periférica do usuário, alvo do olhar, mas não compete com o conteúdo principal pela visão central.

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

Quando um app precisa de views SwiftUI dentro de uma cena 3D (um rótulo em um modelo 3D, um botão flutuando perto de um objeto virtual, uma leitura de medição fixada a uma superfície do mundo 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 RealityView, attachments.entity(for:) recupera a view como um Entity 3D que pode ser posicionado no espaço de coordenadas da cena. A view participa do ciclo de atualização do SwiftUI (mudanças de estado redesenham a view) enquanto é renderizada como um plano texturizado na cena 3D.

O mecanismo é o certo para qualquer UI dentro do mundo: um rótulo que segue um objeto em movimento, uma anotação de medição, um botão contextual. A criação de views SwiftUI permanece inalterada; o posicionamento 3D acontece na camada da RealityView.

O antipadrão do “panel app”

O erro de envio mais comum no visionOS é o panel app: um app de iPad que chega ao visionOS pela compatibilidade “Designed for iPad” e é entregue como uma única Window sem Volume, sem Immersive Space e sem Ornaments. O app funciona, mas não conquista a plataforma.

Três sinais de que um app é um panel app:

Cena de Window única. Sem .windowStyle(.volumetric), sem ImmersiveSpace declarado. O app é uma superfície plana e nada mais.

Nenhum ornament adotado. A tab bar do app vive dentro do conteúdo da Window em vez de fora dela. O resultado é mais carregado do que um app nativo do visionOS na 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 Space, nenhuma UI posicionada em z via attachments. O app faz a mesma coisa que fazia no iPad, só que flutuando.

Panel apps não são fracassos; são a escolha 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 falha é entregar um panel app e reivindicar autoridade nativa do visionOS para ele. O post do cluster Apple Platform Matrix argumenta que a inclusão em uma plataforma é uma decisão de produto; para o visionOS, a decisão é “este app deve conquistar a superfície espacial, ou o panel é suficiente?”

Falhas comuns

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

Volumes que são na verdade conteúdo 2D com padding em profundidade. Uma UI “3D” que preenche um Volume mas renderiza planos achatados dentro dele produz espaço desperdiçado. Volumes são para conteúdo 3D; conteúdo plano pertence a uma Window.

Estilo de imersão que briga com o caso de uso. Um app de meditação que entrega apenas imersão .full força o usuário a sair do ambiente em sessões curtas. Um app de treino que entrega apenas .mixed não vai longe o suficiente para exercícios totalmente focados. Combine o estilo de imersão com a sessão real do usuário.

Ornaments competindo com o conteúdo. Ornaments são periféricos por design. Um ornament que exige atenção central (uma cor piscando, movimento animado) anula o propósito. Use ornaments para controles estáveis e reconhecíveis num relance.

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 Window. Um modelo 3D que o usuário inspeciona é um Volume. Um ambiente envolvente é um Immersive Space. Misturá-los em um único app (uma Window com um Volume aberto sob demanda, um Immersive Space acessível a partir do botão de uma Window) é o padrão nativo do visionOS.

  2. Adote ornaments para toolbars e UI acessória. Ornaments são como o visionOS comunica “esta UI é suplementar”; colocar toolbars dentro do conteúdo da Window soa como iPad-no-Vision. A integração é pequena e a diferença visual é grande.

  3. Use attachments para UI dentro do mundo em RealityView. Rótulos em 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 falha é não usá-la e acabar com renderização ad-hoc de texto 3D no lugar.

O cluster Apple Ecosystem completo: App Intents tipados; servidores MCP; a questão de roteamento; Foundation Models; a distinção entre LLM de runtime e tooling; três superfícies; o padrão single source of truth; Two MCP Servers; hooks para desenvolvimento Apple; Live Activities; o contrato de runtime do watchOS; internals do SwiftUI; o modelo mental espacial do RealityKit; disciplina de schema do SwiftData; padrões de Liquid Glass; envio multiplataforma; a matriz de plataformas; framework Vision; Symbol Effects; inferência com Core ML; API de Writing Tools; Swift Testing; Privacy Manifest; acessibilidade como plataforma; tipografia SF Pro; sobre o que me recuso a escrever. O hub está na série Apple Ecosystem. Para um contexto mais amplo de iOS com agentes de IA, veja o guia de iOS Agent Development.

FAQ

Qual é a diferença entre um Volume e um Immersive Space?

Um Volume é uma região 3D delimitada que vive no Shared Space ao lado de outros apps. O usuário pode caminhar ao seu redor, o sistema o emoldura, e as Windows de outros apps continuam visíveis. Um Immersive Space envolve o usuário, toma conta do ambiente e impede o uso simultâneo de outros apps. Volumes são para “olhe esta coisa 3D”; Spaces são para “esteja neste ambiente”.

Posso abrir vários Volumes ao mesmo tempo?

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

Posso abrir vários Immersive Spaces ao mesmo tempo?

Não. Apenas um Immersive Space pode estar ativo por app por vez. Alternar entre Spaces exige a dispensa explícita do atual e a abertura do novo via @Environment(\.openImmersiveSpace) e @Environment(\.dismissImmersiveSpace).

O tamanho do Volume é mesmo imutável?

Os limites do Volume são fixos na abertura por padrão; o enquadramento da HIG do visionOS é que Volumes representam conteúdo 3D específico com limites intencionais, e o redimensionamento arbitrário pelo usuário distorceria a escala pretendida do conteúdo. O visionOS 2+ adicionou uma adesão opcional do desenvolvedor para volumes redimensionáveis via .windowResizability(.contentSize) e APIs relacionadas, então apps que precisam de containers espaciais redimensionáveis pelo usuário podem solicitá-lo. A maioria dos volumes é entregue com o padrão fixo, que a HIG continua a recomendar para conteúdo com escala específica (uma escultura virtual, um modelo dimensionado fisicamente).

Como adiciono uma tab bar a uma Window do visionOS?

Use uma TabView dentro da Window para abas dentro do conteúdo (o padrão estilo iPad), ou use um ornament com fileiras de botões customizadas para uma UI de abas periférica nativa do visionOS. O caminho do ornament é o que os próprios apps da Apple usam (Music, Mail) e é o que parece mais nativo aos usuários do visionOS.

Os attachments de RealityView podem interagir com hand tracking?

Sim. Os attachments são entidades 3D depois de posicionados, e participam do mesmo sistema de gestos e hit-testing que outras entidades do RealityKit. Gestos de toque, arraste e hover se ligam a eles via os modificadores de gesto padrão do SwiftUI; o post do cluster RealityKit cobre os padrões de integração com hand tracking.

Referências


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

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

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

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

  5. Documentação do Apple Developer: Creating an immersive space in visionOS with SwiftUI. O guia ponta a ponta para definir e abrir immersive spaces. 

Artigos relacionados

RealityKit And The Spatial Mental Model

RealityKit is an entity-component-system, not SwiftUI in 3D. Anchors place entities in real space. Five ways the model d…

16 min de leitura

Liquid Glass in SwiftUI: Three Patterns From Shipping Return on iOS 26

Apple's Liquid Glass is a one-line SwiftUI API. Three patterns from Return go beyond .glassEffect(): glass on text via C…

19 min de leitura

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 min de leitura