Expo no iPhone Duo
Fonte: https://expo.dev/blog/supporting-iphone-duo (Expo SDK 58). As regras de design estão em apple-design-duo; este skill é a camada de implementação Expo e React Native. Contexto do pipeline de CI: https://docs.expo.dev/tutorial/cicd/introduction/ e o skill expo:eas-workflows.
Requisitos
- Expo SDK 58 (alvo iOS 27) e Xcode 27.1 (runtime iOS 27.1 mais o simulador iPhone Duo).
- Imagem EAS Build:
sdk-58oulatest. - O Xcode 27 substitui o Simulator.app pelo Device Hub (Xcode > Open Developer Tool > Device Hub). Ele possui um Duo 3D que dobra, gira e define o ângulo da dobradiça.
Modelo mental
Dobrado = largura compacta, aberto = largura regular. Deixe os layouts se alargarem, nunca mude os designs. Mesmas funcionalidades e estado em ambos. Em poses semelhantes a um livro, o iOS move a barra de status, barra de abas e barra de ferramentas para uma barra vertical em uma borda.
As quatro regras
- Deixe o iOS desenhar a navegação. Use Expo Router
StackeNativeTabs. Headers JS personalizados e<TabList>não são compatíveis com a barra (SDK 58 adiciona compatibilidade mínima) e<TabList>não pode ir na barra. SafeAreaView(react-native-safe-area-context) em cada tela, incluindo insets horizontais.- Teste cada pose. Dobre, desdobre, gire, Split View.
- Ajuste apenas onde necessário:
ArrangementView,preferredPlacement, regiões reservadas, APIs de dobradiça ou desabilitar a barra.
Áreas seguras (onde a maioria dos bugs está)
- A barra precisa de um inset de 84pt à esquerda ou à direita.
- Envolva o conteúdo de rolagem em
SafeAreaView edges={['left','right']}, não noScrollView, para que os fundos fiquem de ponta a ponta. DefinacontentInsetAdjustmentBehavior="automatic"no ScrollView (o patch RN #59194 deve tornar isso suficiente sozinho). - Carrosséis horizontais: adicionam padding nas extremidades por conta própria.
- Botões flutuantes e toasts:
right: insets.right + 16. Em RTL, leia amboslefteright. - Dentro de
NativeTabs,insets.bottominclui a barra de abas apenas quando ela fica na parte inferior. Nunca adicione uma altura fixa de barra de abas. - Aplique padding ao contêiner de conteúdo com os insets, não à view raiz.
- Leia
useWindowDimensionsno momento da renderização. A janela esquerda do Split View tem 469pt de largura.
Expo Router
- Os botões do header na barra empilham em uma coluna, Back ou Close primeiro. Itens somente texto permanecem horizontais. Views personalizadas são excluídas.
- Uma Atividade ao Vivo na Ilha Dinâmica pode encolher barras de ferramentas no overflow. Não lute contra isso.
<SplitView>envolve a visualização de divisão do sistema: uma coluna quando dobrado, duas quando aberto, ciente da dobra. Experimental no SDK 58.NativeTabspreferredPlacement:'sidebar' | 'tabBar' | 'automatic'. Aplica-se na tela aberta em paisagem.
APIs extras
| Necessidade | API |
|---|---|
| Conteúdo de dois painéis (apenas iOS 27.1, versões mais antigas renderizam filhos sem contêiner) | ArrangementView de @expo/ui/swift-ui, dentro de um Host, com ArrangementView.Primary e .Secondary. Estilize com arrangementViewStyle('split' | 'overlay'), splitArrangementLayoutSize, overlayArrangementEdge. Modificadores de @expo/ui/swift-ui/modifiers. |
| Quadros de dobra e oclusão | npx expo install react-native-reserved-regions, então useReservedRegions() retorna { kind: 'division' | 'occlusion', frame } em pontos. Bom para controles de jogo personalizados e grades de colunas pares. Não use para dividir conteúdo na dobra, use ArrangementView. |
| Ângulo e status da dobradiça | npx expo install expo-hinge. useHinge(), getHinge(), addHingeListener(). Status: closed, partiallyOpen, fullyOpen, unknown. O modificador SwiftUI onHingeChange pode rodar na thread da UI via useWorkletCallback. Funciona com Reanimated e dispositivos dobráveis Android 11+. |
| Borda da barra | useVerticalBarEdgeIOS() retorna 'leading', 'trailing' ou null. Não é um detector de Duo (retrato aberto retorna null). |
| Desabilitar a barra | <StatusBar verticalBarBehaviorIOS="disabled" /> (mapeia para preferredVerticalBarBehavior). Apenas para players de vídeo ou telas tipo calculadora, e mantenha a escolha estável por tela. |
Orientação
No iOS 27, os bloqueios de orientação são preferências em contextos redimensionáveis. lockAsync pode ser recusado e requireFullScreen não mais permite sair. Bloquear para retrato ou paisagem no Duo funciona apenas se o app preencher a tela durante as mudanças de pose.
Limitações conhecidas
- StandBy não está disponível no simulador Duo, e a maioria das extensões de app não está disponível no simulador do Xcode 27.1. Teste widgets e extensões no dispositivo.
expo-app-intents(alpha, também SDK 58) expõe ações para Siri, Shortcuts e Spotlight. Alguns esquemas precisam de iOS mais novo que seu domínio (.mail.openDraftprecisa de iOS 27). Não relacionado ao layout.
Checklist de teste
- Dobrado: nada importante sob a barra, botões respondem.
- Aberto: layout não muda de design, nada sob a barra, barras do retrato voltam para cima e para baixo.
- Parcialmente aberto: nada essencial na dobra, folhas e popovers pousam em um lado.
- Split View: conteúdo livre da barra (borda esquerda da janela esquerda).
- Dobrando no meio da tela: posição de rolagem, estado e ações persistem.
- iPhone padrão: nada muda.
Fluxo de auditoria
- Confirme SDK 58 e reconstrua.
- Grep por headers ou barras de abas personalizados (
headerShown: falsemais barras feitas à mão,TabList) e mova paraStackeNativeTabs. - Grep por deslocamentos fixos de
top,bottom,left,right, alturas fixas de barra de abas e padding no nível raiz. Substitua por insets. - Verifique carrosséis, FABs e toasts conforme a lista de áreas seguras.
- Escolha telas de dois painéis:
SplitViewouArrangementView. - Decida por tela sobre regiões reservadas ou dobradiça apenas se um layout personalizado realmente precisar.
- Verifique no Device Hub, depois no CI (abaixo).
CI: execute no simulador Duo
Use o skill expo:eas-workflows (busque o esquema ao vivo primeiro, não adivinhe parâmetros de job). Uma forma sensata do tutorial de CI/CD: build no simulador acionado por PR na imagem sdk-58 ou latest, Maestro E2E opcional, depois escolha baseada em fingerprint entre uma build nativa e um EAS Update para produção. Valide com npx -y eas-cli@latest workflow:validate .eas/workflows/<file>.yml --non-interactive. Workflows consomem minutos EAS pagos.
Erros comuns
| Erro | Correção |
|---|---|
| Aplicar insets no ScrollView | SafeAreaView ao redor, contentInsetAdjustmentBehavior="automatic" |
| Altura fixa de barra de abas no padding | Use insets.bottom da área segura |
Usar useVerticalBarEdgeIOS() para detectar Duo | É apenas a borda da barra, nulo em retrato aberto |
| Dividir conteúdo na dobra com regiões reservadas | ArrangementView ou SplitView |
| Header ou barra de abas JS feita à mão | Stack e NativeTabs |
Botão flutuante em right: 16 | right: insets.right + 16 |
Assumir que lockAsync sempre funciona | Trate como preferência e preencha a tela |
| Testar apenas em um iPhone padrão | Device Hub, cada pose |