Chart3D no SwiftUI: Guia Completo de Gráficos 3D com Swift Charts no iOS 26

Aprenda a criar gráficos 3D interativos no SwiftUI com Chart3D e SurfacePlot no iOS 26. Scatter plots, superfícies matemáticas, controle de câmera e exemplos prontos pra copiar.

Se você já mexeu com Swift Charts pra montar gráficos 2D nos seus apps, tenho uma notícia boa: o iOS 26 trouxe algo que muda completamente o jogo. Com o Chart3D, dá pra criar gráficos tridimensionais interativos direto no SwiftUI — sem biblioteca de terceiros, sem SceneKit, sem gambiarra nenhuma. E, honestamente, o resultado ficou melhor do que eu esperava.

Neste guia, vamos passar por tudo: scatter plots 3D com PointMark, superfícies matemáticas com SurfacePlot, controle de câmera, projeções, materiais e vários exemplos práticos que você pode copiar e colar direto no seu projeto. Bora mergulhar na terceira dimensão.

Por Que Gráficos 3D no SwiftUI?

Vamos ser sinceros: gráficos 2D resolvem a maioria dos casos. Mas existem cenários onde duas dimensões simplesmente não dão conta:

  • Dados com 3+ variáveis — quando você precisa visualizar a relação entre três grandezas simultâneas (peso × altura × idade, por exemplo)
  • Dados espaciais e de sensores — acelerômetro, giroscópio, dados de localização com altitude
  • Superfícies matemáticas — regressões, modelos preditivos, simulações físicas
  • Exploração interativa — rotacionar o gráfico revela padrões que um gráfico 2D simplesmente esconde
  • Apps para visionOS — gráficos 3D são nativos no Apple Vision Pro, então faz todo sentido

Antes do iOS 26, pra conseguir algo assim você precisava recorrer ao SceneKit ou a frameworks de terceiros. Agora, com uma API que segue os mesmos padrões do Swift Charts 2D, a curva de aprendizado é bem pequena.

Requisitos e Plataformas Suportadas

Antes de botar a mão na massa, confirme que seu ambiente tá em dia:

  • Xcode 26 ou superior
  • iOS 26+, macOS 26+, visionOS 26+ ou watchOS 26+
  • Importar tanto SwiftUI quanto Charts

Os modelos de dados usados nos gráficos precisam conformar com Identifiable — exatamente como você já faz com ForEach no SwiftUI. Nada de novo aqui.

De Chart para Chart3D: A Migração é Simples

Se você já tem gráficos 2D no seu app, a migração pra 3D é surpreendentemente direta. Basicamente, troca Chart por Chart3D e adiciona o parâmetro z: nos seus marks. Só isso.

Antes (2D)

import SwiftUI
import Charts

struct GraficoVendas2D: View {
    var body: some View {
        Chart(vendas) { venda in
            PointMark(
                x: .value("Mês", venda.mes),
                y: .value("Receita", venda.receita)
            )
        }
    }
}

Depois (3D)

import SwiftUI
import Charts

struct GraficoVendas3D: View {
    var body: some View {
        Chart3D(vendas) { venda in
            PointMark(
                x: .value("Mês", venda.mes),
                y: .value("Receita", venda.receita),
                z: .value("Quantidade", venda.quantidade)
            )
        }
    }
}

Pronto. O Chart3D aceita os mesmos marks que você já conhece, mas agora com suporte ao eixo Z. Quando eu vi isso pela primeira vez na WWDC, confesso que achei simples demais pra ser verdade — mas funciona.

PointMark 3D: Scatter Plots Tridimensionais

O PointMark é provavelmente o mark mais intuitivo pra começar com gráficos 3D. Cada ponto representa um registro no espaço tridimensional, e a interação de rotação permite explorar os dados de ângulos diferentes.

Exemplo Completo: Análise de Dataset Iris

Vamos usar o clássico dataset Iris pra criar um scatter plot 3D. A ideia é visualizar as relações entre pétalas e sépalas de diferentes espécies de flores — um caso de uso perfeito pra três dimensões:

import SwiftUI
import Charts

struct FlorIris: Identifiable {
    let id = UUID()
    let comprimentoPetala: Double
    let larguraPetala: Double
    let comprimentoSepala: Double
    let especie: String
}

let dadosIris: [FlorIris] = [
    FlorIris(comprimentoPetala: 1.4, larguraPetala: 0.2, comprimentoSepala: 5.1, especie: "Setosa"),
    FlorIris(comprimentoPetala: 4.7, larguraPetala: 1.4, comprimentoSepala: 7.0, especie: "Versicolor"),
    FlorIris(comprimentoPetala: 6.0, larguraPetala: 2.5, comprimentoSepala: 6.3, especie: "Virginica"),
    // ... mais dados
]

struct ScatterPlot3DView: View {
    var body: some View {
        Chart3D(dadosIris) { flor in
            PointMark(
                x: .value("Comp. Pétala", flor.comprimentoPetala),
                y: .value("Larg. Pétala", flor.larguraPetala),
                z: .value("Comp. Sépala", flor.comprimentoSepala)
            )
            .foregroundStyle(by: .value("Espécie", flor.especie))
        }
        .chartXAxisLabel("Comprimento da Pétala (cm)")
        .chartYAxisLabel("Largura da Pétala (cm)")
        .chartZAxisLabel("Comprimento da Sépala (cm)")
        .frame(height: 400)
    }
}

O resultado é um scatter plot interativo onde você pode rotacionar com gestos de arrastar. É nesse momento que clusters e padrões que ficariam escondidos numa projeção 2D começam a aparecer. Dá quase um "efeito eureka".

Personalizando Símbolos 3D

Por padrão, o PointMark usa esferas. Mas dá pra trocar por outros sólidos geométricos:

PointMark(
    x: .value("X", dado.x),
    y: .value("Y", dado.y),
    z: .value("Z", dado.z)
)
.symbol(.cube)        // Opções: .sphere (padrão), .cylinder, .cone, .cube
.symbolSize(0.05)     // Tamanho relativo do símbolo

Uma dica: usar símbolos diferentes pra cada categoria (cubos pra uma, esferas pra outra) ajuda bastante na diferenciação visual, especialmente quando as cores são parecidas.

Personalizando Escalas dos Eixos

Assim como no Swift Charts 2D, você controla o domínio e range de cada eixo. A novidade aqui é o .chartZScale:

Chart3D(dados) { item in
    PointMark(
        x: .value("Peso", item.peso),
        y: .value("Altura", item.altura),
        z: .value("Idade", item.idade)
    )
}
.chartXScale(domain: 40...120, range: -1.5...1.5)
.chartYScale(domain: 1.4...2.1, range: -0.5...0.5)
.chartZScale(domain: 18...80, range: -0.5...0.5)   // Novo no iOS 26
.chartXAxisLabel("Peso (kg)")
.chartYAxisLabel("Altura (m)")
.chartZAxisLabel("Idade (anos)")

O domain define o intervalo dos dados e o range controla o tamanho visual de cada eixo no espaço 3D. Isso é particularmente útil quando um eixo tem valores muito maiores que os outros — sem esse ajuste, ele pode "achatar" a visualização inteira.

SurfacePlot: Superfícies Matemáticas em 3D

Agora a coisa fica realmente interessante. O SurfacePlot é o mark exclusivo do Chart3D — não existe nada parecido em 2D. Ele funciona como uma extensão tridimensional do LinePlot e permite visualizar funções matemáticas como superfícies contínuas.

Como Funciona

O SurfacePlot recebe uma closure com dois parâmetros Double (representando X e Z) e retorna um Double (o valor em Y). O framework avalia a expressão pra diferentes combinações de X e Z e gera a superfície automaticamente. Você não precisa se preocupar com a malha de triângulos nem nada disso.

Exemplo: Superfície Simples

struct SuperficieSimples: View {
    var body: some View {
        Chart3D {
            SurfacePlot(x: "Velocidade", y: "Distância", z: "Tempo") { velocidade, tempo in
                velocidade * tempo
            }
        }
        .frame(width: 500, height: 400)
    }
}

Essa superfície plana representa a boa e velha relação distância = velocidade × tempo. Simples, mas já dá pra ter uma ideia do poder da API.

Exemplo: Superfície Trigonométrica

struct SuperficieOnda: View {
    var body: some View {
        Chart3D {
            SurfacePlot(x: "X", y: "Amplitude", z: "Z") { x, z in
                sin(sqrt(x * x + z * z)) / sqrt(x * x + z * z + 0.1)
            }
            .foregroundStyle(.heightBased)
        }
        .frame(height: 400)
    }
}

Esse exemplo gera uma superfície estilo "onda circular" — tipo aquelas ondas que se formam quando você joga uma pedra na água. Perfeita pra visualizar funções de propagação ou pra impressionar na demo de um projeto.

Exemplo Avançado: Regressão Linear 3D

Aqui é onde as coisas ficam realmente práticas. Você pode combinar PointMark e SurfacePlot no mesmo gráfico pra sobrepor dados reais com uma superfície de regressão:

struct RegressaoView: View {
    let dados: [PontoMedido]

    var body: some View {
        Chart3D {
            // Pontos de dados reais
            ForEach(dados) { ponto in
                PointMark(
                    x: .value("Temperatura", ponto.temperatura),
                    y: .value("Consumo", ponto.consumo),
                    z: .value("Umidade", ponto.umidade)
                )
            }

            // Superfície de regressão
            SurfacePlot(
                x: "Temperatura",
                y: "Consumo Previsto",
                z: "Umidade"
            ) { temperatura, umidade in
                regressaoLinear(temperatura, umidade)
            }
            .foregroundStyle(.gray.opacity(0.7))
        }
        .chart3DCameraProjection(.perspective)
    }

    func regressaoLinear(_ temp: Double, _ umid: Double) -> Double {
        // Coeficientes hipotéticos
        return 2.5 * temp + 0.8 * umid + 10.0
    }
}

Repare como dá pra combinar marks diferentes no mesmo Chart3D. Os pontos reais aparecem como esferas, a superfície de regressão fica semitransparente por baixo. Isso dá uma noção visual imediata de quão bem (ou mal) o modelo se ajusta aos dados.

Materiais e Estilos de Superfície

O SurfacePlot vai além de cores básicas. Ele oferece controle fino sobre a aparência usando um sistema de renderização fisicamente baseado (PBR), similar ao do RealityKit.

Roughness (Rugosidade)

SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in
    sin(5 * x) + cos(5 * z)
}
.roughness(0.3)   // 0 = espelhado, 1 = totalmente rugoso

Um valor baixo de rugosidade cria um efeito quase metálico e espelhado. Um valor alto dá uma aparência mais fosca. Na prática, algo entre 0.2 e 0.5 costuma ficar bom na maioria dos casos.

Estilos de Cor Baseados em Dados

O Swift Charts oferece dois estilos semânticos especiais pra superfícies:

  • .foregroundStyle(.heightBased) — colore a superfície de acordo com a elevação (valores Y). Áreas mais altas ficam com cores quentes, mais baixas com cores frias
  • .foregroundStyle(.normalBased) — colore de acordo com o ângulo da superfície. Áreas inclinadas e planas recebem cores diferentes, destacando a topografia

Pra quem tá vindo do mundo de visualização científica, o .heightBased é basicamente um colormap aplicado automaticamente. Bem prático.

Gradientes Personalizados

SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in
    (sin(5 * x) + sin(5 * z)) / 2
}
.foregroundStyle(
    EllipticalGradient(colors: [.blue, .cyan, .green, .yellow, .orange, .red])
)

Você também pode usar LinearGradient se preferir efeitos mais direcionais.

Controle de Câmera e Projeção

Um gráfico 3D só é realmente útil se você consegue observá-lo do ângulo certo. O Chart3D oferece controle total sobre a "câmera" através de duas APIs: pose (posição/ângulo) e projeção (como o 3D é mapeado pra tela).

Pose: Azimute e Inclinação

A pose define de onde você "olha" pro gráfico:

  • Azimute — rotação horizontal (esquerda/direita)
  • Inclinação — rotação vertical (cima/baixo)

Poses Predefinidas

// Poses prontas
.chart3DPose(.front)     // Vista frontal
.chart3DPose(.back)      // Vista traseira
.chart3DPose(.left)      // Vista lateral esquerda
.chart3DPose(.right)     // Vista lateral direita
.chart3DPose(.default)   // Ângulo padrão do sistema

Pose Personalizada

.chart3DPose(
    Chart3DPose(
        azimuth: .degrees(30),       // Rotação horizontal
        inclination: .degrees(15)    // Inclinação vertical
    )
)

Pose Interativa com Binding

Essa é a parte que eu mais gosto. Pra permitir que o usuário rotacione o gráfico livremente com gestos, basta usar um binding:

struct GraficoInterativo: View {
    @State private var pose = Chart3DPose(
        azimuth: .degrees(0),
        inclination: .degrees(20)
    )

    var body: some View {
        Chart3D(dados) { item in
            PointMark(
                x: .value("X", item.x),
                y: .value("Y", item.y),
                z: .value("Z", item.z)
            )
        }
        .chart3DPose($pose)   // Binding permite interação por gestos
    }
}

Com o $pose, o gráfico responde automaticamente a gestos de arrastar. Simples e funcional.

Projeção: Ortográfica vs Perspectiva

O Chart3D suporta dois modos de projeção:

  • Ortográfica (padrão) — todos os objetos têm o mesmo tamanho, independente da distância. Melhor pra análise precisa de dados
  • Perspectiva — objetos mais próximos parecem maiores, criando sensação de profundidade. Fica ótimo em apresentações e apps pra visionOS
// Ativar projeção em perspectiva
.chart3DCameraProjection(.perspective)

Rotação Automática (Auto-Spin)

Quer impressionar numa demo ou dashboard? Um gráfico girando sozinho chama bastante atenção. Dá pra conseguir isso com um Timer simples:

import SwiftUI
import Charts
import Spatial   // Necessário para Angle2D

struct GraficoRotativo: View {
    @State private var anguloAzimute: Angle2D = .degrees(0)
    @State private var pose = Chart3DPose(
        azimuth: .degrees(0),
        inclination: .degrees(20)
    )

    private let timer = Timer.publish(every: 0.03, on: .main, in: .common).autoconnect()

    var body: some View {
        Chart3D {
            SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in
                sin(sqrt(x * x + z * z))
            }
            .foregroundStyle(.heightBased)
            .roughness(0.2)
        }
        .chart3DPose($pose)
        .onReceive(timer) { _ in
            anguloAzimute += .degrees(1)
            if anguloAzimute.degrees >= 360 {
                anguloAzimute = .degrees(0)
            }
            pose = Chart3DPose(
                azimuth: anguloAzimute,
                inclination: .degrees(20)
            )
        }
    }
}

O truque é manter o ângulo de azimute numa variável separada, já que o Chart3DPose não expõe diretamente uma propriedade de leitura pro azimute atual. A cada tick do timer, incrementa e recria a pose.

Exemplo Completo: Dashboard com Gráficos 3D

Bom, chega de teoria. Vamos juntar tudo num exemplo mais robusto — uma view de dashboard que combina scatter plot 3D categorizado com uma superfície de tendência:

import SwiftUI
import Charts

struct MedicaoSensor: Identifiable {
    let id = UUID()
    let temperatura: Double
    let umidade: Double
    let pressao: Double
    let zona: String
}

let medicoes: [MedicaoSensor] = [
    MedicaoSensor(temperatura: 22.5, umidade: 65, pressao: 1013, zona: "Interna"),
    MedicaoSensor(temperatura: 28.3, umidade: 45, pressao: 1010, zona: "Externa"),
    MedicaoSensor(temperatura: 19.8, umidade: 78, pressao: 1015, zona: "Interna"),
    MedicaoSensor(temperatura: 34.1, umidade: 30, pressao: 1008, zona: "Externa"),
    MedicaoSensor(temperatura: 24.0, umidade: 55, pressao: 1012, zona: "Interna"),
    MedicaoSensor(temperatura: 31.5, umidade: 38, pressao: 1009, zona: "Externa"),
]

struct DashboardSensores: View {
    @State private var pose = Chart3DPose(
        azimuth: .degrees(25),
        inclination: .degrees(15)
    )

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Monitoramento Ambiental 3D")
                .font(.title2.bold())

            Chart3D {
                ForEach(medicoes) { m in
                    PointMark(
                        x: .value("Temperatura", m.temperatura),
                        y: .value("Pressão", m.pressao),
                        z: .value("Umidade", m.umidade)
                    )
                    .foregroundStyle(by: .value("Zona", m.zona))
                    .symbol(m.zona == "Interna" ? .sphere : .cube)
                    .symbolSize(0.06)
                }

                SurfacePlot(
                    x: "Temperatura",
                    y: "Pressão Prevista",
                    z: "Umidade"
                ) { temp, umid in
                    1020.0 - 0.3 * temp - 0.05 * umid
                }
                .foregroundStyle(.gray.opacity(0.5))
                .roughness(0.4)
            }
            .chart3DPose($pose)
            .chart3DCameraProjection(.perspective)
            .chartXAxisLabel("Temperatura (°C)")
            .chartYAxisLabel("Pressão (hPa)")
            .chartZAxisLabel("Umidade (%)")
            .frame(height: 450)

            Text("Arraste para rotacionar o gráfico")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .padding()
    }
}

Esse exemplo junta vários conceitos que vimos: pontos categorizados por cor e símbolo, superfície de regressão semitransparente, projeção em perspectiva, pose interativa e rótulos nos três eixos. É um bom ponto de partida pra adaptar pro seu caso de uso.

Quando Usar (e Quando Não Usar) Gráficos 3D

Gráficos 3D são legais, mas nem sempre são a escolha certa. Já vi gente colocando gráfico 3D onde um bar chart 2D resolvia muito melhor. Então, uma orientação rápida:

Use Gráficos 3D Quando:

  • Você tem três ou mais variáveis que precisa visualizar ao mesmo tempo
  • A interação de rotação realmente revela algo que um gráfico 2D esconderia
  • O contexto é exploratório — análise de dados, dashboards científicos
  • Está desenvolvendo para visionOS, onde a terceira dimensão é literalmente nativa

Evite Gráficos 3D Quando:

  • Leitura precisa de valores é essencial — gráficos 2D são mais fáceis de interpretar
  • Um gráfico 2D já comunica a mesma informação com clareza
  • O público-alvo não está acostumado com visualizações 3D
  • Performance é crítica em dispositivos mais antigos

Dicas de Performance

Pra fechar, alguns pontos que podem te poupar dor de cabeça:

  • Limite a quantidade de pontosPointMark com milhares de pontos pode travar, principalmente em iPhones mais antigos. Se o dataset é grande, considere amostrar antes de plotar
  • Prefira projeção ortográfica quando a perspectiva não faz diferença — ela é computacionalmente mais leve
  • Mantenha funções do SurfacePlot simples — funções muito pesadas vão ser avaliadas muitas vezes, e isso pesa
  • Teste em dispositivo real — o Simulator não reflete o desempenho real de renderização 3D, e isso já me pegou de surpresa mais de uma vez

Perguntas Frequentes

O Chart3D funciona no iPhone ou só no Vision Pro?

Funciona em todas as plataformas Apple a partir do iOS 26, macOS 26, watchOS 26 e visionOS 26. No iPhone e iPad, a interação é por gestos de toque (arrastar pra rotacionar). No Vision Pro, a interação é tridimensional de verdade.

Posso usar Chart3D junto com gráficos 2D no mesmo app?

Pode sim. Chart e Chart3D são views completamente independentes. Dá pra ter os dois na mesma tela sem conflito — ambos fazem parte do mesmo framework Charts.

Quais marks suportam o eixo Z no Chart3D?

No momento, três marks funcionam com Z: PointMark, RuleMark e RectangleMark. Fora esses, tem o SurfacePlot, que é exclusivo do Chart3D.

É possível exportar ou fazer screenshot de um Chart3D?

Sim. O Chart3D é uma view SwiftUI comum, então ImageRenderer (disponível desde o iOS 16) funciona normalmente. A captura registra o gráfico no ângulo atual da pose.

O SurfacePlot é adequado para datasets grandes?

O SurfacePlot avalia uma função matemática, então o tamanho do dataset não é o gargalo — a complexidade da função é. Pra superfícies baseadas em dados reais (e não funções), use PointMark com muitos pontos ou pré-processe os dados numa função de interpolação.

Sobre o Autor Editorial Team

Our team of expert writers and editors.