Criar para o iPhone Duo, do layout ao código
Doze capítulos, SwiftUI e UIKit para a mesma situação. Um julgamento que dá para conferir por capítulo, e no fim por que as abas ficam embaixo.
Deixe a interface bonita em qualquer largura primeiro. O hardware dobrável entra depois, como um acréscimo. O iPhone Duo tem tela externa, tela interna e dobradiça. Ele não é um idiom novo e não deve fazer nascer um segundo app. O texto abaixo segue a curva de aprendizado: cada capítulo entrega um único julgamento que dá para verificar. Primeiro vem a forma que quebra, depois o motivo desse desenho, e em seguida o mesmo cenário em SwiftUI e em UIKit.
As assinaturas foram conferidas com o SDK do iOS 27.1 no Xcode 27.1. O comportamento vem de Designing for iPhone Duo, Preparing your app for iPhone Duo e Three steps. O corpo é uma reexposição, não uma cópia literal do original.
Como ler: cada capítulo começa com «Ao terminar, você consegue», segue com o exemplo que quebra, a forma correta e «Quando usar / Quando não usar». Responda o ponto de verificação sozinho. As respostas estão no fim do texto. Os capítulos 0 a 6 não consultam o vinco, não usam Arrangement e não leem o ângulo da dobradiça. Isso fica para os capítulos seguintes.
Capítulo 0 · Seis posturas são a aceitação, não seis interfaces
Ao terminar, você consegue. Ao ver um Duo fechado, aberto, meio dobrado e girado, dizer em qual largura a interface deve pensar, em vez de escrever uma versão para cada postura.
Exemplo que quebra
Não organize o produto assim: retrato fechado, paisagem fechada, retrato aberto, paisagem aberta, meio dobrado como um livro, meio dobrado em pé na mesa, cada um com a própria árvore de views. Abrir ou fechar uma vez troca a navegação, troca os botões e joga fora a posição de rolagem.
SwiftUI ramificado por postura:
enum Pose { case closed, open, half }
struct Root: View {
let pose: Pose
var body: some View {
switch pose {
case .closed: PhoneHome()
case .open: TabletHome()
case .half: BookHome()
}
}
}
UIKit ramificado por postura:
func installRoot(for pose: String) {
let root: UIViewController
switch pose {
case "closed": root = PhoneHomeController()
case "open": root = TabletHomeController()
default: root = BookHomeController()
}
// 三套控制器,三份选中项
}
Por que a Apple só entrega duas larguras
O Duo tem tela externa e tela interna. A dobradiça pode fechar, abrir por completo ou parar no meio. A pessoa segura o aparelho na vertical, na horizontal, abre como um livro ou deixa em pé na mesa. Tudo isso é mudança contínua dentro do mesmo uso. A exigência do HIG é esta: a tela externa usa largura compact, a tela interna usa largura regular, e o layout que você já tem acompanha o espaço disponível. Não refaça o app para cada jeito de segurar. Você continua desenhando um iPhone. As regras de layout, de barras e de área segura do iOS continuam valendo.
| O que você vê | O que é | Como pensar no desenho |
|---|---|---|
| Fechado | Tela externa. Mais larga e mais baixa que um iPhone comum | Largura compact. No retrato, a altura continua regular |
| Totalmente aberto | Tela interna grande, regular × regular | Extensão natural de um layout de iPad, mas ainda é um app de iPhone |
| Meio dobrado | A tela interna fica dividida em dois lados pelo vinco | Continua a mesma hierarquia. Este capítulo ainda não trata o vinco |
| Rotação e Split View | Contêineres diferentes no mesmo aparelho | A largura passa por valores intermediários. Não existem só as duas faixas «tela externa / tela interna inteira» |
Sobre a direção da barra, guarde uma frase: o retrato da tela interna é a única postura que mantém as barras horizontais em cima e embaixo. Na tela externa, e na paisagem da tela interna, as barras do sistema vão para a lateral. No Split View, a barra de cada lado cola na borda externa daquele lado. Esses detalhes viram código no capítulo 5.
A forma correta de organizar
Uma interface só. O estado fica fora do layout. Quando a largura muda, muda o arranjo, não os dados.
struct Root: View {
@State private var selection: Item.ID?
var body: some View {
Home(selection: $selection)
}
}
final class RootController: UIViewController {
let model = HomeModel()
// 开合时只让同一个控制器重新布局
}
As palavras deste capítulo
Ainda não existe API de Duo. Acerte as palavras agora, para não ligar a coisa errada depois.
Tela externa / tela interna. Fechado, você olha a tela externa. Aberto, olha a tela interna. A cena principal troca de tela sozinha. Quando usar: para descrever o hardware e, na aceitação, deixar claro qual face você testou. Quando não usar: não trate as duas faces como duas telas que você mesmo administra.
Postura. Fechado, aberto, meio dobrado, e ainda a orientação. Quando usar: no Device Hub, abrir uma por uma e ver se a mesma interface continua lá. Quando não usar: não faça
switch posepara escolher o layout.compact / regular. Desenhe a tela externa como largura compact e a tela interna como largura regular. Quando usar: decidir entre «uma camada de cada vez» e «duas camadas visíveis ao mesmo tempo». Quando não usar: não leia regular como «isto é um iPad». A forma de ler está no capítulo 3.
Barra horizontal / barra vertical. O retrato da tela interna mantém as barras de cima e de baixo. Nas outras posturas comuns, navegação, barra de ferramentas e barra de abas vão para a lateral. Quando usar: ao desenhar as barras, desenhe as duas direções. Quando não usar: não desenhe uma barra lateral falsa para imitar o sistema.
Ponto de verificação
No retrato da tela interna, as barras do sistema ficam em cima e embaixo, ou na lateral?
Quantas interfaces as seis posturas devem ter?
Depois de abrir o Duo, ele virou um app de iPad?
O que este capítulo ainda não autoriza. Não escreva if isDuo, não leia o ângulo da dobradiça e não consulte o vinco. Agora você só precisa recusar «uma UI para cada postura».
Fonte: HIG · Designing for iPhone Duo, Tech Talk 111466.
Capítulo 1 · Se ficou mais largo, mostre a próxima camada em vez de esticar a frase
Ao terminar, você consegue. Diante de uma área que ficou mais larga, decidir se cabe acrescentar uma coluna, acrescentar uma fileira ou se você só está esticando a interface estreita de antes. Também consegue dizer o que pode cruzar o vinco e o que não pode.
Exemplo que quebra
A página de ajustes é uma coluna confortável na tela externa. Depois de abrir a tela interna, cada linha da mesma lista chega perto da largura inteira. O título e o interruptor ficam nas duas pontas, e o meio vira um vazio grande. Outra forma de quebrar: colocar o botão principal, o código QR ou o rosto exatamente no centro da tela. No meio dobrado, o vinco corta esse elemento.
Quatro princípios que dá para levar à revisão
Função e estado não se interrompem. O que dava para fazer fechado continua disponível depois de abrir. Rolagem, seleção e o que está sendo editado não se reiniciam porque o aparelho abriu ou fechou. O que muda é quantas camadas cabem na tela de uma vez.
A largura que sobrou serve para revelar hierarquia. No e-mail, a tela externa é «lista; a mensagem só aparece depois do toque». A tela interna pode colocar lista e mensagem lado a lado. Isso é ver mais uma camada, não transformar a largura da linha da mensagem em 900 pt.
O vinco é como a lombada de um livro. Fundo, gradiente e um artigo que rola podem atravessar. Botão, frase, rosto, código QR e alça de arraste não podem montar no centro. Este capítulo só estabelece esse julgamento. Como consultar o vinco é o capítulo 7.
O controle fica perto do conteúdo em que ele age. O botão que comanda a lista permanece acima da lista. Não empurre todos eles para longe só porque apareceu uma barra vertical na lateral.
O jogo deve preencher a postura atual. Prefira mudar a proporção da imagem a deixar faixas pretas em cima, embaixo ou nas laterais. O tamanho físico do texto e dos botões deve permanecer estável o quanto for possível.
A mesma página de ajustes, em duas larguras
Dê um limite legível a textos longos e a formulários. O espaço que sobra fica para a hierarquia ao lado, não para uma frase mais comprida.
Form {
Toggle("提醒", isOn: $reminders)
Text("说明保持在容易扫读的宽度里。")
}
.frame(maxWidth: 700)
.frame(maxWidth: .infinity)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let readable = view.readableContentGuide.layoutFrame
formWidth.constant = min(readable.width, 700)
}
Se essa área já é larga o bastante para caber «lista» e «detalhe» ao mesmo tempo, o próximo capítulo faz isso com a coluna dividida do sistema. Por enquanto não escreva uma barra lateral à mão com HStack, e não aumente o corpo do texto só porque a tela ficou larga.
Perguntas para a revisão, ainda sem API
Antes de alargar, pergunte. Navegação persistente, detalhe complementar, inspetor, pré-visualização, mais controles, densidade de informação mais alta. Quando usar: ao sair de compact e chegar a uma largura grande, procure nessa ordem a estrutura que pode aparecer ao mesmo tempo. Quando não usar: não comece aumentando espaçamento e tamanho de letra para preencher o vazio.
Teste da lombada. Imagine uma fresta no centro em que o toque não acerta. Pode atravessar: fundo fotográfico, textura, lista e artigo que continuam rolando. Não atravessa: botão, frase curta que precisa ser lida inteira, rosto, código QR, campo de texto, ícone pequeno.
Agrupe por proximidade. A ação frequente fica ao lado do bloco que ela afeta. Quando usar: o filtro da lista fica na lista; o compartilhar do detalhe fica no detalhe. Quando não usar: não jogue a ação principal num canto longe do conteúdo só para ficar simétrico.
O jogo preenche. Se a postura mudou, mude a proporção da imagem. Quando usar: você pode travar retrato ou paisagem, mas a postura atual precisa preencher. Quando não usar: não improvise com faixas pretas. Se for mesmo preciso deixar uma margem, preencha com imagem, não com cor chapada.
Ponto de verificação
Na tela interna, esticar o formulário de ajustes até 900 pt de largura. Onde está o problema?
No meio dobrado, o fundo em gradiente e o botão «Pagar»: qual dos dois pode ficar no centro?
O «selecionar tudo» da lista de e-mail deve acompanhar a lista, ou a barra do sistema que está na extrema direita?
O que este capítulo ainda não autoriza. Não refaça a página inteira para o meio dobrado, e não escreva agora o código que consulta o vinco. No rascunho, basta marcar os elementos que «não podem montar no centro».
Capítulo 2 · O SDK com que você compila decide o quanto a tela se preenche
Ao terminar, você consegue. Separar destino de implantação e SDK de compilação, e explicar por que um pacote que já abre no Duo ainda deixa faixas vazias e a barra continua no topo.
Exemplo que quebra
Um pacote compilado com o SDK do iOS 26 instala. Depois de abrir a tela interna, a janela fica centralizada e o entorno fica vazio. Fechado, o conteúdo para à esquerda da barra de status e da câmera. Alguém então escreve outro remendo: «se for Duo, estique o frame até a borda».
Três faixas de SDK, sem um interruptor só de Duo
Preencher a tela e a barra vertical chegam com o SDK de compilação. Não são um booleano no Info.plist. O destino de implantação pode continuar num sistema mais antigo, para que aparelhos velhos ainda instalem. Mas, para usar a tela inteira no Duo e deixar a barra do sistema entrar na forma vertical, o pacote precisa ser compilado com o SDK do iOS 27.1 ou mais novo. Antes de publicar o pacote 27.1, confirme que o conteúdo continua tocável depois que a barra fica vertical.
| SDK usado na compilação | Tela interna ao abrir | Barras |
|---|---|---|
| iOS 26 ou anterior | Centralizada, com vazio ao redor. Na tela externa, o conteúdo fica à esquerda da barra de status e da câmera | Mantém a barra horizontal que você já tinha |
| iOS 27 | Preenche a maior parte da tela interna, mas ainda evita a barra de status da direita | A barra de ferramentas continua no topo |
| iOS 27.1 ou mais novo | Usa toda a área de exibição | Barra de ferramentas e barra de abas se alinham numa barra vertical, abaixo da barra de status |
O destino de implantação pode ser mais baixo; a chamada precisa de uma porta
Os dois trechos abaixo ainda não chamam nenhuma API de dobra. Eles só instalam o hábito que todo capítulo seguinte precisa manter: os símbolos do 27.1 ficam dentro da checagem de disponibilidade. No Mac Catalyst, um problema conhecido do Xcode 27.1 é que esses símbolos do 27.1 podem não compilar, e por isso precisam ficar isolados.
var body: some View {
Home()
.modifier(DuoReadyChrome())
}
struct DuoReadyChrome: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 27.1, *) {
content // 第 5 章才把垂直栏调优放进来
} else {
content
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 27.1, *) {
// 同一个控制器。不要在这里换 root。
}
}
#if !targetEnvironment(macCatalyst)
// 27.1 专有调用放这里,避免 Catalyst 编不过
#endif
Fatos ligados à compilação
Destino de implantação
IPHONEOS_DEPLOYMENT_TARGET. Quando usar: decidir o sistema mais antigo em que ainda dá para instalar. Quando não usar: subir isso para 27.1 não é a condição para preencher a tela. Quem decide o preenchimento é o SDK de compilação.SDK do iOS 27.1. O interruptor de tela cheia mais barra vertical. Quando usar: quando você for entregar a forma completa no Duo, compile com esse SDK. Quando não usar: não publique o pacote 27.1 antes de adaptar a barra vertical.
#available(iOS 27.1, *). Se o destino de implantação for menor que 27.1, o símbolo novo precisa ficar embrulhado. Quando usar: toda chamada de API 27.1 a partir do capítulo 5. Quando não usar: não use isso para embrulhar uma classe de tamanho comum. Essas já existem em sistemas anteriores.Mac Catalyst
#if !targetEnvironment(macCatalyst). Quando usar: se uma API exclusiva do 27.1 impedir a compilação do alvo Catalyst, isole essas chamadas. Quando não usar: não apague por causa disso a adaptação do iPhone. O acessório de câmera em si não está disponível no Catalyst.
Ponto de verificação
O destino de implantação continua sendo iOS 18, mas a compilação usa o SDK 27.1. A barra vertical aparece?
O pacote já inicia no Duo e há vazio ao redor. O próximo passo é detectar o modelo e esticar, ou trocar o SDK?
Por que uma chamada nova do 27.1 não pode ser escrita direto no caminho de código do iOS 18?
O que este capítulo ainda não autoriza. Não leia o tamanho da tela para preencher. O próximo capítulo é que substitui UIScreen.main. A regra dos itens da barra vertical também ainda não chegou.
Fonte: Three steps to make your app shine on iPhone Duo. O Xcode 27.1 precisa do macOS Tahoe 26.6 ou mais novo. No simulador, o Device Hub abre, fecha, gira e dobra.
Capítulo 3 · Pergunte a largura desta área, não se isto é um Duo
Ao terminar, você consegue. Trocar os ramos de layout feitos por aparelho, orientação e UIScreen.main por um recálculo a partir da classe de tamanho e do tamanho do contêiner atual.
Exemplo que quebra
O mesmo Duo pode aparecer na tela externa, em tela cheia na tela interna e em Split View na tela interna. A área segura e as barras também mudam. Ler a tela uma vez na inicialização deixa o valor vencido assim que o aparelho dobra. A tela interna também ignora a orientação que você declarou no Info.
let tallPhone = UIScreen.main.bounds.height == 844
let scale = UIScreen.main.scale
if UIDevice.current.userInterfaceIdiom == .pad
|| horizontalSizeClass == .regular {
IPadOnlyLayout()
}
let window = UIWindow(frame: UIScreen.main.bounds)
if UIDevice.current.orientation == .landscape { wide() }
if view.window?.windowScene?.effectiveGeometry
.interfaceOrientation.isLandscape == true { wide() }
let width = view.bounds.width
- view.safeAreaInsets.left * 2
Dois fatos fáceis de inverter
Largura regular já não significa iPad. A tela interna é larga regular e alta regular. O código antigo que dizia «se for regular, vá para a página exclusiva de iPad» agora roda no iPhone. A exigência da Apple é esta: a tela interna deve parecer a extensão natural do seu layout de iPad. Conserte esse layout. Não acrescente outro teste de idiom para desligá-lo.
Orientação não é forma. Para saber se agora está mais largo do que alto, compare a largura e a altura da área que você possui. O retrato da tela externa é largura compact mais altura regular. A paisagem da tela externa é compact nos dois eixos. A tela interna é regular nos dois eixos. Um app de iPhone passa a encontrar qualquer combinação de classe de tamanho.
O tamanho muda no meio da sessão. Não anote um valor em viewIsAppearing e use esse valor até o fim.
Recalcule pelo contêiner
struct Stage: View {
@Environment(\.horizontalSizeClass) private var widthClass
@Environment(\.verticalSizeClass) private var heightClass
@Environment(\.displayScale) private var displayScale
@State private var size: CGSize = .zero
var body: some View {
content
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
size = newSize
}
}
private var isWiderThanTall: Bool { size.width > size.height }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let bounds = view.bounds
let wider = bounds.width > bounds.height
let scale = traitCollection.displayScale
let usable = bounds.inset(by: view.safeAreaInsets)
layout(for: usable.size, wider: wider, scale: scale)
}
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { _ in self.view.setNeedsLayout() }
}
let window = UIWindow(windowScene: scene)
Quando você realmente precisar de UIScreen, pegue na hora em view.window?.windowScene?.screen. Não guarde como propriedade. Crie a janela a partir da scene. Não use os bounds da tela principal como frame.
API
horizontalSizeClass. SwiftUI:@Environment(\.horizontalSizeClass). UIKit:traitCollection.horizontalSizeClass. Quando usar: distinguir largura compact e regular, junto com a classe de tamanho vertical. Quando não usar: não traduza.regularcomo «página de iPad».onGeometryChange. No SwiftUI, entrega oCGSizedo contêiner quando o tamanho muda. No UIKit, o par éviewDidLayoutSubviewseviewWillTransition(to:with:). Quando usar: comparar largura e altura, ou medir localmente pelo contêiner atual. Quando não usar: não use isso para lerUIScreen.main.bounds.displayScale. SwiftUI:@Environment(\.displayScale). UIKit:traitCollection.displayScale. Quando usar: quando você precisa da escala em pixels. Quando não usar: não leia maisUIScreen.main.scale. A referência à tela principal vai ser descontinuada.UIWindow(windowScene:). Cria a janela com a scene. Quando usar: no lugar deUIWindow(frame: UIScreen.main.bounds). Quando não usar: o cálculo de layout continua olhando os bounds da view ou da window, não a tela física inteira.
Ponto de verificação
horizontalSizeClass == .regularainda pode ser verdadeiro num iPhone?A tela interna girou para a paisagem. Usar
interfaceOrientationpara decidir «agora está mais largo» é confiável?Por que
safeAreaInsets.left * 2calcula errado a largura útil?
O que este capítulo ainda não autoriza. Não use um limiar de largura if width > 700 para trocar duas árvores de navegação. O próximo capítulo usa o contêiner do sistema para resolver «estreito avança na pilha, largo fica lado a lado».
Fonte: o Step 2 de Three steps e as considerações de redimensionamento em Preparing. Tech Talk 111461.
Capítulo 4 · Deixe o contêiner do sistema decidir entre lado a lado e avançar na pilha
Ao terminar, você consegue. Usar um contêiner de colunas para preservar o item selecionado, usar a largura mínima do item para decidir o número de colunas da grade, e refazer um arranjo local segundo «qual arranjo cabe», não segundo «em qual aparelho eu estou».
Exemplo que quebra
Compact usa um NavigationStack. Regular usa uma barra lateral escrita à mão com HStack. Cada um guarda o próprio item selecionado. A grade está escrita como «Duo, três colunas; senão, uma». O grupo de botões usa if width > 700 e se parte em duas views completamente diferentes, uma horizontal e outra vertical.
Ordem de preferência
Se der para usar o contêiner de navegação do sistema, use o contêiner. Conteúdo repetido em cartões usa grade adaptativa. O caso local «na horizontal não cabe, então empilhe na vertical» usa um layout que experimenta o espaço restante. Classe de tamanho vem depois. Geometria exata fica por último. A arquitetura de informação da coluna dividida é «coleção, seleção, detalhe». Estreito: a lista faz push do detalhe. Largo: os dois ficam lado a lado. O item selecionado precisa sobreviver a essa mudança. A largura que sobra revela essa camada. O texto longo continua com limite de largura.
Lista e detalhe
NavigationSplitView {
List(items, selection: $selection) { item in
Text(item.title)
}
} detail: {
if let selection {
Detail(id: selection)
.frame(maxWidth: 700)
} else {
Text("选择一项")
}
}
let split = UISplitViewController(style: .doubleColumn)
split.setViewController(list, for: .primary)
split.setViewController(detail, for: .secondary)
// 选中项放在 list 与 detail 共用的 model 上
// preferredPrimaryColumnWidth 用 min / ideal / max
// 不要写死侧栏点数
Grade de cartões: largura mínima, não número de colunas
private let columns = [
GridItem(.adaptive(minimum: 220))
]
LazyVGrid(columns: columns, spacing: 12) {
ForEach(cards) { Card($0) }
}
func makeLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { _, env in
let width = env.container.effectiveContentSize.width
let count = max(1, Int(width / 220))
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(1 / CGFloat(count)),
heightDimension: .estimated(160)))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .estimated(160)),
subitem: item, count: count)
return NSCollectionLayoutSection(group: group)
}
}
Quando o Split View corta você numa largura intermediária, o número de colunas cai sozinho para 2. Não salta de 3 de volta para 1. Se no meio dobrado você quiser um número par de colunas, isso é assunto do capítulo 7. Não escreva agora.
Troque o arranjo do mesmo grupo de views, não a identidade
ViewThatFits(in: .horizontal) {
HStack { Preview(); Inspector() }
VStack { Preview(); Inspector() }
}
let layout = isWiderThanTall
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Chart()
Legend()
}
O UIKit não tem ViewThatFits. A ideia correspondente é: na hora do layout, pergunte «ainda cabe na horizontal?» e então mude o eixo do mesmo UIStackView. Não destrua e recrie os view controllers filhos. Se fizer isso, o texto em edição se perde.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let horizontal = stack.bounds.width
> preview.intrinsicContentSize.width
+ inspector.intrinsicContentSize.width
stack.axis = horizontal ? .horizontal : .vertical
}
API
NavigationSplitView. UIKit:UISplitViewController. Quando usar: coleção, seleção, detalhe. Estreito faz push, largo fica lado a lado. Quando não usar: não substitua isso por uma barra lateral emHStackescrita à mão. Isso não é o arranjo de dois conteúdos do capítulo 8.GridItem(.adaptive(minimum:)). UIKit: divida a largura do contêiner pela largura mínima do item e obtenha o número de colunas. Quando usar: cartões, álbum, painel e outro conteúdo repetido. Quando não usar: não escrevaisDuo ? 3 : 1.ViewThatFits. Experimenta, no eixo dado, qual subárvore cabe. Quando usar: grupo de botões, metadados, pré-visualização mais inspetor, esse tipo de escolha local. Quando não usar: não use isso para trocar a navegação do app inteiro.AnyLayout. Troca o contêiner de layout e mantém a identidade das subviews. Quando usar: gráfico e legenda, pré-visualização e propriedades, quando o arranjo muda mas continuam sendo aquelas duas views. Quando não usar: não escreva as subviews duas vezes, uma em cada ramo deif.
Ponto de verificação
Quando o layout volta de lado a lado para uma coluna só, onde o item selecionado deve viver?
Depois do Split View, a largura só cabe em dois cartões de 220 pt. De onde vem o número de colunas?
ViewThatFitsresponde «qual aparelho», ou «qual arranjo cabe»?
O que este capítulo ainda não autoriza. Se a barra de abas deve virar navegação lateral, isso fica para o próximo capítulo. Fazer dois blocos de conteúdo desviarem do vinco fica para o capítulo 8. Agora não introduza Arrangement.
Capítulo 5 · A barra do sistema vai para a lateral sozinha; uma barra falsa não vai
Ao terminar, você consegue. Fazer navegação, barra de ferramentas e barra de abas continuarem reconhecíveis nas formas horizontal e vertical, e entregar as ações secundárias ao menu de overflow do sistema.
Exemplo que quebra
O topo da página é um HStack desenhado à mão, ou um UINavigationBar, UIToolbar e UITabBar criados com new à parte. O sistema não recolhe esse conteúdo para a barra vertical. Outro problema comum: o botão de compartilhar só tem o título "Share". A forma vertical só mostra o ícone. Um item sem ícone simplesmente não fica em pé. Desenhar de novo um menu «…» disputa o lugar com o menu de overflow do sistema.
Quando a barra fica vertical, e quem tem direito de entrar
Depois de compilar com o SDK 27.1, a tela externa junta barra de navegação, barra de ferramentas e barra de abas numa faixa lateral. A paisagem da tela interna, e algumas views nas posições leading e trailing, também fazem isso. O retrato da tela interna continua com barra horizontal. Na coluna dividida, só a coluna de detalhe participa da barra vertical. A coluna lateral e a coluna de conteúdo permanecem horizontais. O inspetor permanece horizontal.
A barra se alinha à borda do hardware. Um idioma da direita para a esquerda não a troca para o outro lado. No Split View, a barra do app da esquerda fica à esquerda, e a barra do app da direita fica à direita.
O item precisa ter título e ícone ao mesmo tempo. A forma vertical usa o ícone. A forma horizontal prefere o ícone e também pode usar o título. No menu de overflow, ícone e título aparecem juntos. Uma view personalizada fica na barra horizontal por padrão, a menos que você declare que ela serve para a colocação vertical. Um botão só de texto fica na barra horizontal. O botão Edit do sistema é assim: alterna entre símbolo e texto.
O topo da barra vertical fica reservado para voltar ou fechar, e depois para uma ação em destaque como Done. Os demais itens mantêm o agrupamento original. O overflow recolhe de baixo para cima, por padrão. Uma página de navegação prioriza preservar a barra de abas. Uma página de tarefa prioriza preservar os itens de ferramenta.
O mesmo grupo de ações serve nas duas barras
No SwiftUI, pendurado num NavigationStack:
NavigationStack {
DetailView()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close", systemImage: "xmark") { dismiss() }
}
ToolbarItem(placement: .topBarPinnedTrailing) {
Button("Done", systemImage: "checkmark") { save() }
}
ToolbarItem(placement: .bottomBar) {
Button("Share", systemImage: "square.and.arrow.up") { share() }
}
.visibilityPriority(.high)
ToolbarOverflowMenu {
Button("Duplicate", systemImage: "plus.square.on.square") {
duplicate()
}
}
}
.toolbarVerticalCompressionBehavior(.automatic)
}
No UIKit, no view controller filho que está dentro do navigation controller:
let close = UIBarButtonItem(
title: "Close", image: UIImage(systemName: "xmark"),
target: self, action: #selector(closeTapped))
let done = UIBarButtonItem(
title: "Done", image: UIImage(systemName: "checkmark"),
target: self, action: #selector(doneTapped))
let share = UIBarButtonItem(
title: "Share", image: UIImage(systemName: "square.and.arrow.up"),
target: self, action: #selector(shareTapped))
share.visibilityPriority = .high
share.axisBehavior = .automatic
navigationItem.leadingItemGroups = [
UIBarButtonItemGroup.fixedGroup(items: [close])
]
navigationItem.pinnedTrailingGroup = UIBarButtonItemGroup.fixedGroup(items: [done])
toolbarItems = [share]
navigationItem.verticalBarCompressionBehavior = .automatic
O botão de voltar é colocado pelo próprio navigation controller. O fechar personalizado entra no lado leading. Done entra em pinnedTrailingGroup. Quando a busca ocupa o espaço, os itens fixados são os últimos a entrar no overflow. additionalOverflowItems serve para recolher o seu próprio «mais» dentro do menu do sistema. As reticências ficam para o overflow. Os outros menus usam outro símbolo.
Um item que alterna entre símbolo e texto recebe .horizontalOnly: quando não há barra horizontal, ele se esconde, em vez de se espremer na barra vertical. Se a view personalizada realmente tiver um desenho vertical, use .verticalPreferred. O espaço flexível tem tamanho 0 no eixo vertical. Agrupe com ToolbarItemGroup / UIBarButtonItemGroup. Não enfie espaçamento fixo à mão. O emblema usa badge. Não escreva o número dentro do título, senão esse item dificilmente vira um símbolo puro.
Sair da barra vertical é uma exceção estável
NavigationStack {
CalculatorView()
.toolbarVerticalBehavior(.disabled)
}
override var preferredVerticalBarBehavior: UIVerticalBarBehavior {
.disabled
}
O padrão é .automatic. Saia só nestas interfaces: vídeo em tela cheia, um layout de largura inteira que não rola, como uma calculadora, e um sheet que só tem um botão de fechar. Essa é uma escolha estável. Não fique alternando em viewWillAppear conforme o estado da página. Se uma página só precisa esconder a barra, use a API de visibilidade, não desligue o comportamento vertical. No UIKit, se a preferência realmente mudou, chame setNeedsUpdateOfVerticalBarConfiguration(). O navigation controller, por padrão, entrega a decisão ao topo da pilha. A barra de abas entrega à página selecionada no momento.
Saber de que lado a barra está, e como o fundo se estende por baixo
@Environment(\.toolbarVerticalEdge) private var verticalEdge
// HorizontalEdge?;这个上下文从不用垂直栏时为 nil
Image("hero")
.backgroundExtensionEffect()
let edge = traitCollection.verticalBarEdge
// .leading / .trailing / .unspecified
// 表示系统偏好的边,不表示栏此刻是否可见
// 从不用垂直栏的上下文返回 .unspecified
let backdrop = UIBackgroundExtensionView()
backdrop.contentView = hero
Para observar essa borda no UIKit, passe UITraitCollection.systemTraitsAffectingVerticalBarEdge para registerForTraitChanges. O trait pode mudar e a borda continuar a mesma.
Entrada de primeiro nível: a tela interna pode virar navegação lateral
No iPhone, escrever só .sidebarAdaptable continua sendo a barra de baixo. Para virar navegação lateral onde isso é suportado, acrescente o defaultTabBarPlacement(.sidebar) do iOS 27. Não use o defaultAdaptableTabBarPlacement do iOS 18. Aquele é para iPad. A UI ligada à barra lateral olha isTabViewSidebarAvailable. Não volte a adivinhar a classe de tamanho.
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab("Library", systemImage: "books.vertical") { LibraryView() }
}
.tabViewStyle(.sidebarAdaptable)
.defaultTabBarPlacement(.sidebar)
tabBarController.sidebar.preferredPlacement = .sidebar
O produto também pode decidir, de forma explícita, manter a barra de baixo e deixar o espaço que alargou para as colunas dentro do conteúdo. As duas escolhas se sustentam. Não desenhe uma barra lateral falsa conforme a largura.
API
toolbarVerticalBehavior(iOS 27.1)..automatic/.disabled. UIKit: sobrescrevapreferredVerticalBarBehavior, tipoUIVerticalBarBehavior. Quando usar: calculadora, vídeo em tela cheia, sheet com um único botão de fechar. Quando não usar: não alterne conforme a navegação ou o estado de alguma view.axisBehavior(iOS 27.1). SwiftUI:.automatic/.horizontalOnly/.verticalPreferred. UIKit:UIBarButtonItem.axisBehavior, os mesmos três valores. Quando usar: item que alterna símbolo e texto usa horizontalOnly; view personalizada que realmente tem desenho vertical usa verticalPreferred. Quando não usar: um botão comum, com título e ícone, permanece automatic.visibilityPriority(iOS 27.0). SwiftUI:.automatic/.low/.high, e tambéminit(lowerThan:)/init(higherThan:). UIKit:.standard/.low/.high. Quando usar: primeiro dê prioridade ao grupo inteiro, depois ao item dentro do grupo. O que é frequente e o que tem emblema ficam por último para transbordar. Quando não usar: não faça um segundo menu «…».ToolbarOverflowMenueadditionalOverflowItemsé que são o menu do sistema. OadditionalOverflowItemsdo UIKit existe desde o iOS 16.Comportamento de compressão (iOS 27.1). SwiftUI:
.automatic/.prefersToolbarItems/.prefersTabBar. UIKit:navigationItem.verticalBarCompressionBehaviorcom.automatic/.prefersBarItems/.prefersTabBar. Quando usar: página de navegação fica no padrão (recolhe primeiro os itens de ferramenta e preserva as abas). Página de tarefa passa a recolher primeiro a barra de abas. Quando não usar: não escreva duas barras para simular a compressão.topBarPinnedTrailing(iOS 27.0). A propriedade correspondente no UIKit,pinnedTrailingGroup, existe desde o iOS 16. Fechar usa.cancellationAction. No UIKit, isso entra emleadingItemGroups. Quando usar: uma ação principal como Done, que precisa ficar perto de voltar. Quando não usar: ação secundária não se fixa aqui. Ela deve poder ir para o overflow.toolbarVerticalEdge(iOS 27.1). SwiftUI:HorizontalEdge?, nil quando esse contexto nunca usa barra vertical. UIKit:traitCollection.verticalBarEdge, com os valores leading, trailing ou unspecified. O valor reflete a borda preferida, mesmo que a barra esteja invisível neste momento. Quando usar: quando o conteúdo personalizado precisa se alinhar a essa barra. Quando não usar: não leia unspecified como «a barra acabou de ser escondida». Ele significa que esse contexto não vai usar barra vertical.backgroundExtensionEffect()(iOS 26). UIKit:UIBackgroundExtensionView. ColoquecontentViewdentro da área segura e preencha a borda com a extensão. Quando usar: a imagem principal ou o fundo precisam se estender por baixo da barra vertical. Quando não usar: o primeiro plano tocável não deve furar a área segura por meio disso.defaultTabBarPlacement(iOS 27.0)..sidebarAdaptablemais.sidebar. UIKit:sidebar.preferredPlacement = .sidebar. Os valores de ambienteisTabViewSidebarAvailableetabBarPlacementtambém são iOS 27. Quando usar: quando as seções de primeiro nível combinam com navegação permanente num layout largo. Quando não usar: não usedefaultAdaptableTabBarPlacementpara o Duo. Esse modificador existe desde o iOS 18 e age no iPad.
Ponto de verificação
Um botão que só tem título, sem ícone, entra na barra vertical?
Nas três colunas da divisão, a barra de qual coluna fica vertical?
Numa página de navegação, quando falta espaço, o padrão recolhe primeiro os itens de ferramenta ou primeiro a barra de abas?
Por que um
UIToolbardesenhado à parte não fica em pé na lateral?
O que este capítulo ainda não autoriza. Não consulte o vinco para decidir se a barra fica à esquerda ou à direita. A borda da barra é colocada pelo sistema no trait / environment. De que lado o sheet fica é o próximo capítulo.
Fonte: HIG · Vertical controls; Preparing · Optimize bars. Tech Talk 111462.
Capítulo 6 · Calcule as quatro bordas em separado; o sheet declara de que lado se encosta
Ao terminar, você consegue. Escolher a posição do sheet, calcular a área útil com uma área segura assimétrica e preservar uma imagem de largura inteira com cantos concêntricos à tela e com um ponto de foco.
Exemplo que quebra
Usar o inset da esquerda multiplicado por dois como se fosse a área segura total. Split View ou barra vertical deixam esquerda e direita diferentes, e o conteúdo fica torto. Outra classe de problema é fabricar uma máscara de tela cheia no lugar do sheet: o sheet do sistema, o alerta, o menu e o popover desviam do vinco. A sua máscara não desvia. A imagem principal usa .fill e corta. O rosto que aparecia na tela externa some na tela interna.
A barra do sheet acompanha a colocação
Na tela externa, um sheet com barra de ferramentas usa barra vertical por padrão. Na tela interna, o sheet fica centralizado por padrão e a barra é horizontal. O lado leading não tem barra vertical. O lado trailing tem. Só o sheet respeita a colocação. Não faça outra transição personalizada por causa disso. Um sheet com um único botão de fechar é o caso em que o capítulo anterior permite desligar a barra vertical. Quando sourceView está definido, o UIKit ignora preferredPlacement.
.sheet(item: $selection) { item in
ItemDetail(item: item)
.presentationPlacement(.trailing)
}
let sheet = detail.sheetPresentationController
sheet?.preferredPlacement = .trailing
// .automatic / .leading / .center / .trailing
// sourceView 非 nil 时,这个属性被忽略
As quatro bordas da área segura são independentes
content
.safeAreaPadding(.horizontal)
background
.ignoresSafeArea()
let usable = view.bounds.inset(by: view.safeAreaInsets)
// 分别使用 left、right、top、bottom
// 折痕不是 safe area inset
A decoração pode sair da área segura. Conteúdo tocável e texto que precisa ser lido ficam dentro. Câmera, barra vertical e Split View deixam esquerda e direita, ou cima e baixo, desiguais. Uma forma personalizada colada no canto da tela usa um retângulo concêntrico. Não escreva o raio à mão.
RoundedRectangle(cornerRadius: 12) // 不要写死去贴屏幕角
ConcentricRectangle()
view.cornerConfiguration = .corners(radius: .containerConcentric())
A imagem de largura inteira precisa guardar o assunto
Image("cover")
.resizable()
.aspectRatio(contentMode: isWide ? .fit : .fill)
.frame(maxWidth: .infinity, maxHeight: isWide ? 420 : 280)
.clipped()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let wide = view.bounds.width > view.bounds.height
hero.contentMode = wide ? .scaleAspectFit : .scaleAspectFill
hero.clipsToBounds = true
}
Se o assunto não está no centro geométrico, dê um foco ao recorte, em vez de só alternar entre fill e fit. O jogo preenche e muda a proporção. Veja o capítulo 1.
API
presentationPlacement(iOS 27.0)..automatic/.center/.leading/.trailing. UIKit:UISheetPresentationController.preferredPlacement. Quando usar: cada sheet precisa ser visto fechado, aberto e meio dobrado, e só então você decide se encosta na borda ou fica no centro. Quando não usar: popover e alerta não recebem esse modificador. QuandosourceViewestá definido, o UIKit o ignora.Inset assimétrico.
bounds.inset(by: safeAreaInsets). Quando usar: em qualquer lugar em que você calcule à mão largura ou altura útil. Quando não usar: não espelhe o inset de um lado no lado oposto. O vinco também não entra na área segura.ConcentricRectangle(iOS 26). UIKit: raio concêntrico do contêiner emUICornerConfiguration.UIView.cornerConfigurationexiste. Quando usar: um painel personalizado precisa acompanhar o canto da tela ou do contêiner atuais. Quando não usar: o canto pequeno do próprio cartão ainda pode usar um raio fixo. Não use isso para desviar do vinco.fill / fit. Escolha pela proporção atual ou pela classe de tamanho, ou defina um foco. Quando usar: imagem principal e capa de vídeo, da tela externa estreita até a tela interna larga, sempre guardando o assunto. Quando não usar: não escolha o contentMode uma vez só na inicialização.
Ponto de verificação
Na tela interna, a barra de ferramentas de um sheet trailing é horizontal ou vertical?
Por que
left * 2não produz a largura do conteúdo?O sheet do sistema desvia do vinco. Uma máscara de tela cheia desenhada por você desvia?
O que este capítulo ainda não autoriza. Quando uma tela personalizada, um código QR ou um controle de arraste precisam desviar do vinco, o próximo capítulo é que consulta a região reservada. Agora não chute a coordenada do vinco.
Capítulo 7 · O vinco e a câmera são duas regiões reservadas, não a área segura
Ao terminar, você consegue. Num layout personalizado, achar o vinco ativo e a câmera ativa, e deslocar só o pedaço que está tampado, em vez de refazer a página inteira.
Exemplo que quebra
Assim que dobra até a metade, troca para outra árvore de interface. Ou escreve à mão «o vinco fica em 50% da largura». Depois do Split View e da rotação, essa proporção já não é o centro da sua view. O outro extremo é desviar do vinco cada parágrafo do artigo: conteúdo que rola já consegue passar por essa fresta.
Dois tipos de região; a maioria dos componentes do sistema já desvia
Região reservada é o retângulo que outra coisa ocupa no seu sistema de coordenadas. frame inclui margins. Cada uma tem tipo, se está ativa e um identificador. A consulta devolve as regiões que intersectam a view atual. Se a sua view só ocupa metade da tela interna, não suponha que consegue ver o vinco da tela inteira.
division, divisão. No meio dobrado, a dobradiça parte um bloco grande em dois lados. Quando abre por completo e fica plano, essa região não está ativa e a largura é 0.
occlusion, oclusão. A câmera frontal da tela externa está sempre lá e ainda se expande na Ilha Dinâmica. A câmera frontal da tela interna fica escondida sob a tela no dia a dia. Só enquanto está em uso ela ocupa um bloco e faz a interface ceder espaço.
Alerta, menu, sheet e coluna dividida se adaptam sozinhos. Tela personalizada, código QR, rosto, frase curta que precisa continuar inteira e objeto no meio de um arraste é que precisam que você leia esses retângulos. Mova só o elemento que está tampado, e leve junto os controles relacionados. Não o mande para a outra ponta da tela. Artigo e lista que rolam não se deslocam por inteiro.
Filtre primeiro o que está em vigor, depois posicione o conteúdo personalizado
O reservedRegions do SwiftUI ainda aceita layoutDirectionBehavior:, com padrão .mirrors. Só passe .fixed se você mesmo fez o espelhamento. Senão, espelha duas vezes. A posição física da câmera não vira com o idioma. O SwiftUI, por padrão, espelha a geometria para você antes. O proxy de onGeometryChange também consegue consultar.
GeometryReader { proxy in
let folds = proxy.reservedRegions(kind: .division)
.filter(\.isActive)
let cameras = proxy.reservedRegions(kind: .occlusion)
.filter(\.isActive)
CanvasPage(avoiding: folds + cameras)
}
override func layoutSubviews() {
super.layoutSubviews()
let folds = reservedRegions(kind: .division)
.filter(\.isActive)
let cameras = reservedRegions(kind: .occlusion)
.filter(\.isActive)
placeControls(avoiding: folds + cameras)
}
Uma decisão estrutural como «número par de colunas» precisa continuar visível quando o aparelho está plano e a região de divisão ainda não está ativa. Só nesse caso você passa .includeInactive, e continua usando isActive para decidir se move o controle agora.
let divisions = proxy.reservedRegions(
kind: .division,
options: .includeInactive)
let evenColumns = !divisions.isEmpty
let divisions = reservedRegions(
kind: .division,
options: .includeInactive)
let evenColumns = !divisions.isEmpty
API
reservedRegions(kind:options:)(iOS 27.1). No SwiftUI, emGeometryProxy. UIKit:view.reservedRegions(kind:options:), devolve[UIView.ReservedRegion]. Quando usar: conteúdo personalizado que o contêiner do sistema não move por você. Quando não usar: lista comum, coluna dividida, sheet, alerta. Não trate o resultado como inset de área segura.ReservedRegion(iOS 27.1).id,kind,frame,margins,isActive. Os tipos são.divisione.occlusion. Quando usar: posicione comframe.framejá inclui a margem. Quando não usar: uma division inativa não serve para empurrar um botão. Quando está plano, ela não deve mudar o lugar do toque..includeInactive(iOS 27.1).ReservedRegion.QueryOptions. Quando usar: uma decisão que não depende de estar meio dobrado agora, como «se existir vinco, a grade usa número par de colunas». Quando não usar: não mova o botão antes da hora só porque a consulta achou uma região inativa.layoutDirectionBehavior..mirrorsou.fixed. Quando usar: mantenha o padrão, para que as subviews de layout automático e a região usem a mesma direção. Quando não usar: só passe.fixedquando você espelha à mão.
Ponto de verificação
Depois de abrir por completo e ficar plano, a division ainda está ativa? Qual é a largura?
Um artigo que rola precisa tirar cada parágrafo de cima do vinco?
A câmera da tela interna ocupa occlusion no dia a dia?
O que este capítulo ainda não autoriza. Conteúdo em par, como reprodutor mais playlist, o próximo capítulo entrega ao Arrangement. Não use o ângulo da dobradiça para decidir se desvia do vinco.
Fonte: ReservedRegion; Preparing · Adapt to reserved regions. Tech Talk 111463.
Capítulo 8 · Quando dois conteúdos formam uma experiência, deixe o contêiner trocar o lugar deles
Ao terminar, você consegue. Decidir se um par de views usa split ou overlay, e deixar a navegação fora desse contêiner.
Exemplo que quebra
Usar Arrangement no lugar de TabView ou NavigationSplitView. Ou colocar Arrangement dentro de List, ScrollView ou de uma coluna da divisão. O resultado é que metade do conteúdo não rola até aparecer e não dá para tocar.
Se já parece um dos dois empilhamentos, mapeie para lá
Arrangement só tem view primária e view secundária. Ele olha espaço, proporção, classe de tamanho e região de divisão. Não olha se você é um Duo. O contêiner de navegação fica por fora. O próprio exemplo da Apple é uma pilha de navegação com um Arrangement dentro. Não aninhe outra coluna dividida dentro do Arrangement.
split. Corresponde ao antigo
HStack/VStack, ou a uma relação principal-secundária em que nenhum dos dois deve tapar o outro. Quando é mais largo do que alto, divide à esquerda e à direita. Quando é mais alto do que largo, empilha em cima e embaixo. Quando a região de divisão aparece, ele ajusta os dois lados e desvia do vinco. Depois de restringir a um eixo, se esse eixo não servir, só resta a view primária..split.axes(.horizontal)mostra só o conteúdo primário quando não cabe no eixo horizontal.overlay. Corresponde ao antigo
ZStack. Sem região de divisão ativa, a view primária cobre a secundária (fechado ou totalmente aberto). No meio dobrado, a primária vai para o lado de trás ou para baixo do vinco, e a secundária vai para o lado da frente ou para cima.
Reprodutor e lista: split
NavigationStack {
ArrangementView {
Player()
} secondary: {
Queue()
}
.arrangementViewStyle(.split)
}
let arrangement = UIArrangementViewController()
arrangement.setViewController(player, for: .primary)
arrangement.setViewController(queue, for: .secondary)
arrangement.updateArrangement(.split)
navigationController.setViewControllers([arrangement], animated: false)
Só permitir esquerda e direita; na vertical, deixar só o reprodutor:
.arrangementViewStyle(.split.axes(.horizontal))
arrangement.updateArrangement(.split.axes(.horizontal))
Controles cobrindo a imagem: overlay
ArrangementView {
TransportControls()
} secondary: {
Stage()
}
.arrangementViewStyle(.overlay)
arrangement.setViewController(controls, for: .primary)
arrangement.setViewController(stage, for: .secondary)
arrangement.updateArrangement(.overlay)
Proporção e hierarquia ainda têm estes ajustes, todos do iOS 27.1: splitArrangementLayoutRatio, splitArrangementLayoutSize, splitArrangementFixedLayoutSize, overlayArrangementEdge. Os valores de ambiente splitArrangementAxis e overlayArrangementZIndex deixam a subview saber em qual arranjo ela está agora. O UIKit lê zIndex, splitAxis e isHidden com state(for:). A faixa de tamanho das views primária e secundária está em UISplitArrangement.ViewProperties. A borda de oclusão está em UIOverlayArrangement.ViewProperties.edge.
API
ArrangementView(iOS 27.1). Oinitrecebe primary e secondary. UIKit:UIArrangementViewController, e a posição desetViewController(_:for:animated:)é.primary/.secondary. Quando usar: reprodutor e fila, editor e inspetor, tela e propriedades. Os dois blocos são a mesma experiência. Quando não usar: a navegação de primeiro nível do app. Não coloque dentro de coluna dividida, lista ou scroll view..split(iOS 27.1).arrangementViewStyle(.split),.axes(.horizontal)ou.vertical. UIKit:updateArrangement(.split.axes(...)). Quando usar: os dois lados precisam de espaço próprio e nenhum cobre o outro. Quando não usar: o controle já devia flutuar sobre o conteúdo. Isso é overlay..overlay(iOS 27.1). Sem região de divisão, a view primária fica por cima. No meio dobrado, a primária vai para trás ou para baixo, e a secundária vai para a frente ou para cima. Quando usar: primeiro plano / fundo, quando é aceitável que um tape o outro no dia a dia. Quando não usar: e-mail em duas colunas. Isso é a coluna dividida do capítulo 4, ou o split daqui.updateArrangement(_:animated:)(iOS 27.1). Troca split / overlay, ou muda só o eixo. Quando usar: o produto quer travar um eixo de propósito, ou alternar entre os dois estilos. Quando não usar: não recalcule o frame você mesmo a cada rotação e chame isso em seguida. O estilo padrão já acompanha o contêiner.
Ponto de verificação
Antes era um
ZStacke, no meio dobrado, precisa se dividir nos dois lados do vinco. Usa split ou overlay?O Arrangement deve envolver a navegação, ou ser envolvido por ela?
O que acontece se você o colocar dentro de um
ScrollView?
O que este capítulo ainda não autoriza. Não leia o ângulo da dobradiça para decidir entre split e overlay. O ângulo é entrada de interação do capítulo 10.
Fonte: os exemplos de SwiftUI / UIKit em Preparing; HIG · Arrangement views. Assinaturas conferidas com ArrangementView e UIArrangementViewController no SDK do iOS 27.1.
Capítulo 9 · Abrir e fechar é mudança de layout, não reiniciar o app
Ao terminar, você consegue. Manter navegação, rolagem, seleção, edição e reprodução acima do layout, e conhecer o limite de Split View e de janela nova no Duo.
Exemplo que quebra
Fechado, a lista tinha a terceira mensagem selecionada. Aberto, outra árvore de views começa na primeira, e o rascunho também está vazio. Outra forma de quebrar: testar só a tela externa em tela cheia e a tela interna em tela cheia. No meio do Split View, as barras se sobrepõem e o texto é cortado. E ainda supor que «se abriu, com certeza está muito largo».
Uma largura contínua, mais dois limites de hardware
Na tela interna, todos os apps participam do Split View, e o vídeo também pode se sobrepor a um app. A barra vertical de cada app cola na borda externa dele, então a sua barra pode ficar à esquerda ou à direita. Os dois lados precisam ser testados.
Um layout muito largo precisa conseguir recuar para a largura intermediária e, de novo, para compact. O estado — caminho de navegação, rolagem, seleção, editor, progresso de reprodução, formulário não salvo — fica na camada acima daquela que decide entre uma coluna e duas.
O Duo é o primeiro iPhone com suporte a várias scenes. Um app que abre janela nova no iPad também abre aqui. Janela nova não pode ser criada na tela externa. Se o pedido falhar, permaneça na janela atual. O closure de configuração de UIWindowScene.ActivationAction precisa devolver um ActivationConfiguration. Não altere um objeto de pedido. Quando a plataforma não suporta várias janelas, siga o alternate que você forneceu. Não suponha que um toque na tela externa sempre abre uma segunda janela. UIRequiresFullScreen continua valendo, mas a interface ainda muda de tamanho quando o aparelho abre e fecha.
O item selecionado vive fora do layout
struct Mailbox: View {
@State private var path: [Message.ID] = []
@State private var selection: Message.ID?
@State private var draft = ""
var body: some View {
NavigationSplitView {
MessageList(selection: $selection)
} detail: {
Editor(text: $draft, messageID: selection)
}
}
}
let action = UIWindowScene.ActivationAction(
title: "新窗口",
image: UIImage(systemName: "plus.rectangle")
) { _ in
let activity = NSUserActivity(activityType: "mailbox.message")
return UIWindowScene.ActivationConfiguration(userActivity: activity)
}
A posição de rolagem usa o próprio salvamento da scroll view do sistema, ou um identificador guardado no model. Não faça scrollTo de volta ao topo na hora de fechar. A reprodução usa o mesmo objeto de player. Não dê replace nele quando o tamanho muda.
Fatos para conferir
Elevar o estado. Seleção, caminho, rascunho e player ficam fora do contêiner adaptativo. Quando usar: qualquer página que alterna entre compact e regular. Quando não usar: não crie um objeto de estado para a tela externa e outro para a tela interna.
Split View. A barra fica na borda externa daquele app. A metade esquerda tem a barra à esquerda. A metade direita tem a barra à direita. Quando usar: no Device Hub, coloque-se à esquerda e à direita. Quando não usar: não suponha que a barra vertical está sempre em trailing.
UIWindowScene.ActivationAction(iOS 15). O closure devolveUIWindowScene.ActivationConfiguration(userActivity:). Quando a plataforma não suporta várias janelas, usealternate. Quando usar: o app já suporta várias janelas no iPad e você quer o mesmo na tela interna do Duo. Quando não usar: não force um pedido de scene nova na tela externa sem tratar a falha. Janela nova não pode ser criada na tela externa.UIRequiresFullScreen. A exigência de tela cheia continua sendo respeitada. Quando usar: se você já precisava da tela exclusiva, mantenha a configuração original. Quando não usar: não imagine que isso impede a mudança de tamanho trazida por abrir e fechar. O layout ainda precisa acompanhar.
Ponto de verificação
Depois de abrir a tela interna, o rascunho deve ser restaurado de qual estado?
Com o app na metade direita do Split View, a barra vertical tende a ficar de que lado?
Dá para criar uma janela nova na tela externa?
O que este capítulo ainda não autoriza. Não use o ângulo da dobradiça para reiniciar a navegação. Abrir e fechar já se expressam pelo tamanho e pela região reservada. O ângulo fica só para a interação física de verdade.
Fonte: o que o HIG diz sobre continuidade entre telas e sobre a barra externa no Split View; a exigência de teste em vários tamanhos em Preparing. Tech Talk 111464.
Capítulo 10 · O ângulo conduz o efeito, não a barra nem o número de colunas
Ao terminar, você consegue. Quando há dobradiça, transformar o ângulo em tom, enquadramento ou regra de jogo, e voltar a um valor seguro quando não há dobradiça ou quando a interação sai da hierarquia.
Exemplo que quebra
if hinge.angle > someRadians { showSidebar = true }
Plano, Split View e um iPhone comum não vão entregar o ângulo do jeito que você espera. Se a barra lateral deve aparecer, os capítulos 3 e 4 já responderam com espaço.
O layout olha o espaço; a interação é que olha a dobradiça
O ângulo serve para isto: entrada física, mecanismo de jogo, instrumento, postura da câmera, interação de mesa, ou um efeito amarrado de propósito ao abrir e fechar. Não serve para isto: barra lateral, número de colunas, quantidade de itens da grade, se a navegação está recolhida.
No SwiftUI, DeviceHingeContext.hinge nil significa que esse aparelho não tem dobradiça. Não significa «está fechado». Fechado é .closed. angle é Angle. status é .closed, .partiallyOpen, .fullyOpen.
O nil do UIKit tem outro sentido: quando a interação saiu da hierarquia que consegue fornecer atualização da dobradiça, update.hinge é nil. O ângulo está em radianos. UIHinge.Status tem mais um valor, .unknown. A frequência e a precisão da atualização são decididas pelo sistema. Não dependa de uma taxa de quadros. Se você só se importa com fechado, meio aberto e totalmente aberto, leia status.
No meio aberto, transforme o ângulo em tom; no resto do tempo, volte a zero
.onHingeChange { _, context in
if let hinge = context.hinge, hinge.status == .partiallyOpen {
pitch = bend(angle: hinge.angle)
} else {
pitch = 0
}
}
let interaction = UIHingeInteraction { [weak self] _, update in
guard let self else { return }
guard let hinge = update.hinge, hinge.status == .partiallyOpen else {
self.pitch = 0
return
}
self.pitch = self.bend(radians: hinge.angle)
}
view.addInteraction(interaction)
O isEnabled de onHingeChange(isEnabled:_:) é true por padrão. No UIKit, use interaction.isEnabled: enquanto está desligado, não há callback e as atualizações perdidas não entram numa fila. Ao ligar de novo, se a hierarquia atual ainda tiver dobradiça, o estado atual é entregue mais uma vez na hora. Esse closure escapa. Use [weak self] para evitar um ciclo de retenção.
API
onHingeChange(iOS 27.1).(isEnabled:_:). Os parâmetros são oDeviceHingeContextantigo e o novo. Quando usar: o efeito precisa acompanhar a dobradiça de forma contínua. Quando não usar: desviar do vinco, coluna dividida, grade. Isso usa região reservada e Arrangement.DeviceHinge(iOS 27.1).statusmaisangle: Angle. Ohingeno contexto é opcional. Quando usar: se for nil, zere o efeito. Não deixe crashar. Quando não usar: não leia nil como «está fechado». Fechado é.closed.UIHingeInteraction(iOS 27.1).init { interaction, update in }.update.hingeé umUIHingeopcional,angleestá em radianos, e o estado inclui.unknown. Quando usar: adicione à view que deve observar a dobradiça. O exemplo oficial fazaddInteractionemviewDidLoad. Quando não usar: aqui, nil significa que a interação saiu da hierarquia que fornece atualização. Não significa, necessariamente, «este telefone não tem dobradiça».Frequência de atualização. O cabeçalho deixa escrito: taxa e granularidade mudam com o estado do sistema. Quando usar: trate o ângulo como entrada contínua e tolere quadro perdido. Quando não usar: não use «precisa ser N vezes por segundo» para calibrar ou para animar layout.
Ponto de verificação
Para decidir se a barra lateral aparece, dá para olhar
hinge.angle?No SwiftUI, o que significa
hinge == nil? E o nil do UIKit?Se você só quer saber se está meio aberto, lê o ângulo ou o status?
O que este capítulo ainda não autoriza. Não trate a tela externa como uma segunda janela que você posiciona. Isso é o próximo capítulo, e só vale durante a captura.
Capítulo 11 · O conteúdo extra da tela externa só aparece na captura, e quem o mostra é o sistema
Ao terminar, você consegue. Explicar como a cena principal se move entre as duas telas, e quando é permitido colocar um conteúdo complementar na tela externa.
Exemplo que quebra
Ao abrir, partir a interface em dois UIWindow: um desenha a tela interna, o outro desenha a tela externa. O sistema não deixa a sua segunda tela na face que fechou. O simulador também não tem câmera. Esse código não dá para verificar no Device Hub.
A experiência principal segue a única cena principal
Ao abrir e fechar, a cena principal troca de face sozinha. Todos os botões importantes ficam na interface principal. O conteúdo complementar na tela externa pode ser interativo, mas deve ser pouco.
A única forma suportada é o acessório de captura da câmera: aparelho aberto, app em primeiro plano e tela interna em tela cheia, com uma sessão de captura em andamento. Só então o sistema pode mostrá-lo na tela externa. O sistema decide quando e onde, e também pode recolher a qualquer momento. Desligue com isEnabled. Não cancele o registro para ligar e desligar. O estado segue o mesmo model.
O simulador não tem câmera. Esta seção precisa de aparelho real. A direção da câmera pode inverter depois de abrir, fechar e girar. Ao escolher a câmera, escolha por «para qual face ela aponta». Veja Choosing a camera by the direction it faces.
ExternalNonInteractiveAccessory é monitor externo e AirPlay. Não é a tela externa do Duo.
Teleprompter: a interface principal continua conseguindo fotografar
CameraCaptureAccessory está marcado no SDK como indisponível para macOS, tvOS, watchOS, visionOS e Mac Catalyst. sceneAccessory e registerSceneAccessory(_:) existem desde o iOS 27.0. cameraCapture(sceneConfiguration:) e o papel de sessão .windowCameraCaptureAccessory são iOS 27.1. Quando precisar entregar contexto ao delegate da scene, use o método de fábrica com userInfo e recupere em UISceneConnectionOptions.sceneAccessoryUserInfo.
CameraView(model: model)
.sceneAccessory {
CameraCaptureAccessory(isEnabled: $model.prompterEnabled) {
Prompter(model: model)
}
.onAvailabilityChange { model.prompterAvailable = $0 }
}
let configuration = UISceneConfiguration(
name: "Capture Accessory",
sessionRole: .windowCameraCaptureAccessory)
let accessory = UISceneAccessory.cameraCapture(
sceneConfiguration: configuration)
registration = registerSceneAccessory(accessory)
// 配件场景的 delegate 画提词器。
// 快门仍在主场景里。
API
CameraCaptureAccessory(iOS 27.1).init(isEnabled:content:), pendurado emsceneAccessory.onAvailabilityChangeinforma se o sistema ainda quer mostrá-lo. Quando usar: teleprompter, contagem regressiva, deixar a pessoa fotografada ver a imagem. O registro fica na mesma view da UI de captura. Quando não usar: obturador, foco e ajustes do caminho principal. A tela externa pode sumir a qualquer momento.registerSceneAccessory(iOS 27.0). UIKit:UISceneAccessory.cameraCapture(sceneConfiguration:), papel.windowCameraCaptureAccessory. O registration devolvido serve paraunregisterSceneAccessory. Quando usar: uma scene complementar enquanto a sessão de captura existe. Quando não usar: não dê unregister para esconder por um momento. Use o estado de habilitação.ExternalNonInteractiveAccessory. O monitor ao qual você se conectou, ou AirPlay. Quando usar: você já queria suportar uma tela grande externa, e o conteúdo não precisa de interação. Quando não usar: a tela externa do Duo quando ele está fechado.Simulador. O Device Hub consegue abrir, fechar, girar e dobrar. Não tem câmera. A maioria das extensões de app também não roda no simulador do Duo. Quando usar: layout, barras, sheet, Split View. Quando não usar: não declare no simulador que o acessório de captura já foi verificado.
Ponto de verificação
Fora da captura, dá para fixar um painel na tela externa?
Quando o teleprompter não está disponível, onde o obturador deve ficar?
Por que este trecho não pode ser encerrado só no simulador?
Até aqui. Se o produto não tem câmera, este capítulo pode ficar sem código. Os capítulos 0 a 9 já cobrem o caminho principal, do desenho ao layout adaptativo.
Fonte: Preparing · Improve your app’s camera handling. Tech Talk 111465.
Apêndice · Comparação, sinais de alerta e respostas
Os doze capítulos anteriores constroem o julgamento em ordem. Aqui ficam juntas as três camadas de API, as formas que não devem aparecer e as respostas dos pontos de verificação, para você voltar e conferir depois de terminar um capítulo.
Três camadas; não pule
| Camada | Quando escrever | SwiftUI | UIKit |
|---|---|---|---|
| 1 · Qualquer tamanho | Capítulos 3 a 6. Toda janela Apple se beneficia | NavigationSplitView, TabView, GridItem(.adaptive), ViewThatFits, AnyLayout, classe de tamanho, onGeometryChange, .toolbar do sistema |
UISplitViewController, UINavigationController, UITabBarController, largura do contêiner, trait, layoutSubviews, viewWillTransition |
| 2 · Saber do vinco e da barra vertical | Nos capítulos 5 a 8, quando o contêiner do sistema não basta | reservedRegions, ArrangementView, toolbarVerticalBehavior, axisBehavior, visibilityPriority, presentationPlacement, toolbarVerticalEdge |
reservedRegions(kind:options:), UIArrangementViewController, preferredVerticalBarBehavior, axisBehavior, verticalBarEdge, preferredPlacement |
| 3 · Só o hardware em si | Capítulos 10 e 11, quando o produto realmente precisa | onHingeChange, CameraCaptureAccessory, sceneAccessory |
UIHingeInteraction, UISceneAccessory.cameraCapture, papel .windowCameraCaptureAccessory |
A regra dos itens da barra vertical está no capítulo 5 porque o SDK 27.1 coloca a barra do sistema em pé, não porque você vai consultar o vinco. A consulta do vinco só começa no capítulo 7.
Disponibilidade, para não misturar
| Sistema | O que só existe nesta faixa |
|---|---|
| iOS 26 | ConcentricRectangle, backgroundExtensionEffect(), UIBackgroundExtensionView, UICornerConfiguration |
| iOS 27.0 | visibilityPriority, ToolbarOverflowMenu, topBarPinnedTrailing, presentationPlacement, defaultTabBarPlacement, isTabViewSidebarAvailable, sceneAccessory / registerSceneAccessory |
| iOS 27.1 | Comportamento e borda da barra vertical, axisBehavior, comportamento de compressão, região reservada, Arrangement, dobradiça, CameraCaptureAccessory |
No UIKit, leadingItemGroups, pinnedTrailingGroup e additionalOverflowItems existem desde o iOS 16. No Duo, eles cuidam da ordem e do overflow dentro da barra vertical, mas a propriedade em si não nasceu no 27.1.
A prioridade de visibilidade no SwiftUI é .automatic / .low / .high. O valor correspondente no UIKit é .standard / .low / .high. O comportamento de compressão também difere num nome: .prefersToolbarItems no SwiftUI, .prefersBarItems no UIKit.
Ao abrir um arquivo, procure isto primeiro
Se estiver sendo usado para layout comum, deve ser trocado:
UIScreen.main.bounds,UIScreen.main.scale, umUIScreenguardadoUIWindow(frame: UIScreen.main.bounds)userInterfaceIdiom == .padou.phoneUIDevice.current.orientation,statusBarOrientation,interfaceOrientation, inclusiveeffectiveGeometry.interfaceOrientationhorizontalSizeClass == .regularlevando a uma página «só iPad»bounds.height == 844ou uma tabela de alturas de modelos conhecidossafeAreaInsets.left * 2if isDuo,if isFolded, usar o ângulo da dobradiça para decidir barra lateral ou número de colunasframe grande escrito à mão, largura de barra lateral escrita à mão, número de colunas da grade escrito à mão
barra de topo desenhada à parte, ou
UIToolbar/UINavigationBar/UITabBarcriados sozinhosbotão de barra de ferramentas só com título, sem ícone; menu «…» desenhado por você
conteúdo importante centralizado em cima do vinco; compact e expandido cada um com a própria cópia do estado
imagem principal em
.fillsem foco, e sem escolher entre fill e fit conforme a proporçãousar
toolbarVerticalBehavior(.disabled)fora das exceções escritas na documentação, ou alternar conforme o estado da página
Posturas que você precisa ter visto no Device Hub
| Postura | O que se espera |
|---|---|
| Fechado, retrato | Tela externa, largura compact + altura regular, barra vertical |
| Fechado, paisagem | Compact nos dois eixos, barra vertical |
| Aberto, retrato | Tela interna regular × regular, barra horizontal. Esta é a única postura comum que mantém as barras de cima e de baixo |
| Aberto, paisagem | regular × regular, barra vertical |
| Meio dobrado | A mesma hierarquia. Conteúdo preciso não monta no vinco. Conteúdo que rola não muda de rota a página inteira |
| Split View na tela interna, você à esquerda e à direita | A barra cola na borda externa daquele lado. O estado continua lá |
Cada sheet e cada popover precisam ser abertos uma vez nessas posturas. As capturas da loja seguem as Screenshot specifications do App Store Connect. Uma organização comum é tela externa 1398×2034 e tela interna 2007×2853; na paisagem, troque os dois lados. Não escreva esse grupo de números no layout. Se o material disser «a partir de abril de 2027, o envio exige capturas do Duo», a data exata é a do anúncio da Apple naquele momento.
Respostas dos pontos de verificação
Capítulo 0. As barras do sistema no retrato da tela interna ficam em cima e embaixo. As seis posturas continuam sendo uma interface só. Depois de abrir, continua sendo um app de iPhone. A tela interna é que passa a ser regular × regular.
Capítulo 1. Um formulário de ajustes com 900 pt de largura esticou a frase. O rótulo e o interruptor foram separados pelo vazio. O certo é limitar a largura e deixar o espaço que sobra para a próxima camada da hierarquia. O gradiente pode ficar no centro. O botão de pagamento não pode. O selecionar tudo da lista acompanha a lista, não a barra do sistema na extrema direita.
Capítulo 2. O destino de implantação pode continuar sendo iOS 18. Depois de compilar com o SDK 27.1, a barra vertical aparece. Quando há vazio ao redor, troque o SDK de compilação. Não detecte o modelo para esticar o frame. O símbolo novo não existe no sistema antigo, então precisa entrar em #available.
Capítulo 3. A largura regular passa a ser verdadeira na tela interna do Duo, então ela já não é igual a iPad. A tela interna ignora a declaração de orientação. Largo ou alto se decide comparando a largura e a altura do contêiner. Os insets da esquerda e da direita podem ser diferentes. left * 2 calcula o outro lado errado.
Capítulo 4. O item selecionado fica na mesma porção de estado, fora da coluna dividida. O número de colunas é a largura do contêiner dividida pela largura mínima do item. Aqui cai em 2. ViewThatFits responde qual arranjo cabe.
Capítulo 5. Um botão só com título não entra na barra vertical. Na coluna dividida, só a barra da coluna de detalhe fica vertical. Numa página de navegação, o padrão recolhe primeiro os itens de ferramenta para o overflow e preserva a barra de abas. Um UIToolbar criado à parte não é a barra fornecida pelo contêiner de navegação. O sistema não o coloca em pé.
Capítulo 6. Na tela interna, a barra de ferramentas de um sheet trailing é vertical. A área segura da esquerda e da direita é assimétrica. Não se espelha. Uma máscara de tela cheia desenhada por você não desvia do vinco como o sheet do sistema.
Capítulo 7. Depois de abrir por completo e ficar plano, a division não está ativa e a largura é 0. Um artigo que rola não deve ser deslocado parágrafo por parágrafo. A câmera da tela interna não ocupa occlusion no dia a dia. Só ocupa enquanto está em uso.
Capítulo 8. ZStack mapeia para overlay. O Arrangement é envolvido pela navegação. Não o contrário. Colocar dentro de ScrollView faz um dos blocos ficar fora de alcance.
Capítulo 9. O rascunho volta do mesmo estado, não de outra árvore de views. A barra vertical da metade direita do Split View fica à direita, que é a borda externa daquele app. Janela nova não pode ser criada na tela externa.
Capítulo 10. A barra lateral não olha o ângulo da dobradiça. O nil do SwiftUI significa que esse aparelho não tem dobradiça. O nil do UIKit significa que a interação saiu da hierarquia que consegue fornecer atualização. Se você só quer saber se está meio aberto, leia status.
Capítulo 11. Fora da captura, não dá para fixar um painel na tela externa. O obturador fica na interface principal. O simulador não tem câmera. O acessório de captura precisa de aparelho real.
Entradas oficiais
ReservedRegion, e o
UIView.ReservedRegiondo UIKitArrangementView, e
UIArrangementViewControllerTech Talks: 111466 desenho, 111461 preparação, 111462 barras verticais, 111463 postura e Arrangement, 111464 várias telas e scenes, 111465 câmera
A página Preparing ainda lista WWDC 2026 · 278 e WWDC 2025 · 282
Boas práticas · A entrada de baixo permanece; a largura fica com o conteúdo
Depois de ler, você consegue. Confrontar este app com as regras anteriores e separar o que já chegou ao chão do jeito da Apple, o que é escolha de produto e o que ainda não pode ser tratado como verificado.
Este app não tem uma versão Duo. O destino de implantação continua sendo iOS 18. Só a compilação com o SDK 27.1 preenche a tela e permite que a barra do sistema fique vertical. Abrir e fechar não troca a rota e não troca o model da página. A largura regular que sobra é usada nas colunas esquerda e direita dentro de «Hoje» e de «Respiração», não para transformar as três entradas de primeiro nível numa barra lateral. Ângulo da dobradiça, tela externa da câmera e várias janelas não têm motivo de produto, então não são escritos.
Comparação
| Julgamento do tutorial | O que este app faz |
|---|---|
| Capítulos 0 a 2: uma interface só; o SDK é o interruptor | Não ramifica pelo nome do aparelho. API nova fica dentro de #available. No sistema antigo, permanece um HStack / VStack da mesma relação |
| Capítulos 3 a 4: revelar a próxima camada pela largura do contêiner | O shell compact continua uma coluna só, por mais largo que fique. Em regular, 800 pt ainda é uma coluna. A partir de 801 pt, esquerda e direita ficam lado a lado. Só entra na faixa mais folgada quando cabem a coluna principal máxima de 480 pt mais o detalhe legível de 720 pt (1201 pt no total). 1024 pt continua lado a lado. Não estica a frase |
| Capítulo 5: barra do sistema, mas barra lateral não é obrigatória | .sidebarAdaptable mais defaultTabBarPlacement(.tabBar). As três abas ficam embaixo |
| Capítulo 6: o sheet declara a posição; máscara desenhada à mão não desvia do vinco | O sheet do sistema, em regular, segue a intenção: trailing ou center. A camada de assinatura em compact continua sendo uma máscara na raiz. Ela não desvia do vinco sozinha |
| Capítulos 7 a 8: mover só os dois blocos que formam par | Respiração, a faixa horizontal da página de assinatura e as colunas principal e secundária de mesma largura usam o split de ArrangementView. A página inicial que rola e o detalhe não mudam de rota por causa do vinco |
| Capítulos 9 a 11: estado contínuo; API de hardware é opcional | Seleção e rota ficam fora do layout. Não há onHingeChange, acessório de captura nem várias janelas |
A navegação raiz mantém três entradas
A tela interna pode virar barra lateral. Este produto escolhe não virar. «Hoje», Tendências e Respiração continuam como abas de baixo. Ajustes avançam a partir de «Hoje». O caminho de iPad usa defaultAdaptableTabBarPlacement(.tabBar). No iOS 27, ainda se declara defaultTabBarPlacement(.tabBar). Depois de abrir a tela interna, o espaço que sobra aparece dentro da página: em Hoje, cartão à esquerda e detalhe à direita; em Respiração, modo à esquerda e exercício à direita.
Ficar lado a lado exige duas coisas ao mesmo tempo: o shell desta página é regular, e a classe de tamanho horizontal atual também é regular. Assim, a árvore estreita não toma o item selecionado compartilhado. Se o shell for compact, mesmo que o contêiner esteja muito largo, a faixa continua sendo uma coluna só.
TabView(selection: adaptiveSectionSelection) {
// 三个 Tab
}
.tabViewStyle(.sidebarAdaptable)
.defaultAdaptableTabBarPlacement(.tabBar)
// iOS 27:content.defaultTabBarPlacement(.tabBar)
adaptiveShellRole == .regular && horizontalSizeClass == .regular
Arrangement só descreve uma relação que já existia
A navegação fica do lado de fora. O layout da versão antiga e o Arrangement do 27.1 são a mesma relação, não outra página.
| Cena | Relação de antes | 27.1 |
|---|---|---|
| Exercício de respiração | A imagem principal em cima, os controles embaixo | .split. Alto empilha em cima e embaixo, largo coloca à esquerda e à direita, e desvia do vinco. Não trava o eixo e não olha o ângulo da dobradiça |
| Página de assinatura na horizontal | Imagem principal e compra lado a lado | .split. No sistema antigo, continua sendo HStack |
| Colunas principal e secundária da mesma largura | As duas colunas precisam de espaço próprio | .split.axes(.horizontal), proporção 0.5. O fundo pode atravessar o vinco. O conteúdo tocável fica dentro do Arrangement |
O que o sistema faz, e o que ainda não conta como pronto
A área segura é lida nas quatro bordas. A lista que rola e a imagem dentro do detalhe não se deslocam por causa do meio dobrado. Sheet do sistema: compact segue .automatic; regular escolhe trailing ou center conforme a intenção.
O que fica explícito de fora: não usar o ângulo da dobradiça para posicionar a respiração ou a barra lateral. Sem câmera, não se registra acessório de captura. Não se cria janela nova na tela externa. Não se copia o model da página para as seis posturas. Só volte a considerar isso quando o produto realmente quiser tratar abrir e fechar como entrada, ou quando aparecer uma interface de captura.
O que ainda não conta como pronto: as seis posturas ainda precisam ser vistas com as próprias mãos no Device Hub. O comando público do simulador não abre, fecha nem dobra até a metade. A máscara de assinatura em compact não é um sheet do sistema. No sheet de explicação que só tem um botão de fechar, ainda está em aberto se a barra vertical deve ser desligada. A barra de topo fixa usada na transição de elemento compartilhado ainda fica fora da camada de escala. Compilar com sucesso não significa que o vinco e a barra vertical já foram vistos.
Como uma página nova entra: primeiro coloque-a numa aba existente ou na pilha de navegação, e deixe o estado do lado de fora. Depois, pela faixa do contêiner, decida uma coluna ou lado a lado. O texto longo para por volta de 720 pt. Só use Arrangement quando os dois blocos de conteúdo já precisavam trocar de lugar, e deixe o layout antigo com #available(iOS 27.1, *). Escreva primeiro a toolbar do sistema, a classe de tamanho e a largura legível. Escreva região reservada e Arrangement depois. Dobradiça e tela secundária continuam sem código, até o produto precisar.
Teste com este app
Depois que a tela interna fica mais larga, as três abas devem subir para uma barra lateral, ou ficar embaixo?
Com um contêiner regular de apenas 800 pt, «Hoje» deve ser uma coluna ou lado a lado?
A esfera e os controles do exercício de respiração: por que usam
.splite nãoonHingeChange?Se a máscara de assinatura em compact, no meio dobrado, ficar em cima do vinco, dá para dizer «o sheet do sistema desvia»?
Respostas. As três abas ficam embaixo. 800 pt ainda é uma coluna. Só a partir de 801 pt fica lado a lado. A esfera e os controles são a relação de cima e de baixo do mesmo exercício. Isso vai para o split. O ângulo não decide o layout. A máscara de assinatura não é um sheet do sistema e não desvia do vinco sozinha.
Continue lendo
Experimente essas ideias no Stress HRV
Baixar na App StoreEstas notas compartilham pensamento de produto para bem-estar. Não são conselho médico.