Protocolo Layout do SwiftUI: Construindo layouts personalizados de sizeThatFits a placeSubviews
O iOS 16 adicionou o protocolo Layout ao SwiftUI, a API pública para construir views container personalizadas que participam do passo de layout do SwiftUI1. Antes do Layout, formatos de container personalizados exigiam ou hacks com GeometryReader (que quebram a composição porque solicitam o tamanho proposto inteiro) ou trabalho com ViewModifier personalizado que luta contra o sistema. O Layout é a resposta certa: um protocolo de dois métodos (sizeThatFits e placeSubviews) mais extensões opcionais de espaçamento e cache, com um contrato que se integra de forma limpa ao modelo de layout do SwiftUI em que o pai propõe e o filho dispõe.
O post percorre o protocolo confrontando-o com a documentação da Apple. O enquadramento é “no que o Layout realmente contrata” porque o padrão de uso indevido (tratar o Layout como uma ferramenta de espaço de coordenadas em vez de uma ferramenta de negociação de tamanho) produz layouts que funcionam em uma tela e falham em outra, e o post What SwiftUI Is Made Of do cluster argumentou que a arquitetura do SwiftUI é melhor compreendida lendo seus protocolos públicos.
TL;DR
Layouté um protocolo com dois métodos obrigatórios:sizeThatFits(proposal:subviews:cache:)retorna o tamanho preferido do layout dado a proposta do pai;placeSubviews(in:proposal:subviews:cache:)posiciona cada filho chamando seu métodoplace(at:anchor:proposal:)2.- O parâmetro
proposalé umProposedViewSizecomwidtheheightcomo CGFloats opcionais.nilsignifica “use seu tamanho ideal”; um valor finito é a oferta do pai;.infinitysignifica “use o quanto quiser.” Subviewsé um typealias paraLayoutSubviews, uma coleção de proxiesLayoutSubview. Cada proxy pode ser consultado quanto ao seu tamanho dada qualquer proposta e posicionado em qualquer ponto. Os proxies são a única forma como o Layout interage com os filhos.- Valores de layout personalizados fluem dos filhos para o pai através de tipos
LayoutValueKeyanexados via.layoutValue(...)em views filhas, legíveis a partir dos subscripts deLayoutSubviewdentro dos métodos de layout. - O
cacheserve para amortizar computação entresizeThatFitseplaceSubviews(cada passo chama ambos, frequentemente com os mesmos valores intermediários). Tipe o cache como uma struct que armazena os tamanhos pré-computados; construa-o uma vez, reutilize entre os dois métodos.
O contrato do protocolo
Um Layout é uma struct (tipicamente) que declara dois métodos que o framework da Apple chama durante o passo de layout2:
struct DiagonalLayout: Layout {
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
// Compute and return the size your layout wants
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
// Position each subview by calling subview.place(...)
}
}
Use-o como um container nativo:
DiagonalLayout {
Text("First")
Text("Second")
Text("Third")
}
O framework chama sizeThatFits com o tamanho proposto pelo pai (um ProposedViewSize), depois chama placeSubviews com os bounds que foram concedidos ao layout. Os dois métodos juntos descrevem o comportamento do layout: quão grande ele quer ser, e onde cada filho fica dentro dessa alocação.
ProposedViewSize: a oferta do pai
O layout no SwiftUI segue um contrato em que o pai propõe e o filho dispõe3. O pai passa um tamanho proposto; o filho retorna seu tamanho real; o pai posiciona o filho dentro de seus próprios bounds. O Layout participa desse contrato via ProposedViewSize:
struct ProposedViewSize {
var width: CGFloat?
var height: CGFloat?
}
Os eixos opcionais carregam significado semântico:
nilpara um eixo significa “use seu tamanho ideal/natural.” UmTextcom proposta.zeroretorna sua largura mínima (um caractere por linha); com propostanilretorna sua largura ideal (uma linha, sem quebra).- Um valor finito significa “o pai oferece este espaço; você decide o que fazer.” Um
Textcom proposta de largura 100pt pode quebrar, pode usar menos, pode usar exatamente 100. .infinitysignifica “use o quanto quiser.” UmColorcom proposta.infinityocupa todo o espaço disponível.
A convenção ProposedViewSize.unspecified (width: nil, height: nil) é a solicitação do tamanho ideal; ProposedViewSize.zero é a solicitação do tamanho mínimo; ProposedViewSize.infinity é a solicitação de expansão gulosa.
O sizeThatFits de um Layout personalizado deve respeitar a proposta: retornar um tamanho que o layout realmente queira para os bounds propostos, não sempre o mesmo valor codificado. Tamanhos codificados quebram a capacidade do layout de se adaptar a containers diferentes (uma view de cartão, uma célula de lista, uma sheet).
Lendo tamanhos de subviews através de LayoutSubview
Dentro de sizeThatFits, o layout pergunta a cada filho qual tamanho ele quer para várias propostas. A consulta passa pelo proxy LayoutSubview4:
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
let proposed = ProposedViewSize(
width: proposal.width.map { $0 / CGFloat(subviews.count) },
height: proposal.height
)
let sizes = subviews.map { $0.sizeThatFits(proposed) }
let totalWidth = sizes.reduce(0) { $0 + $1.width }
let maxHeight = sizes.map(\.height).max() ?? 0
return CGSize(width: totalWidth, height: maxHeight)
}
O padrão subviews.map { $0.sizeThatFits(proposal) } é como um layout descobre quais tamanhos seus filhos querem. O método sizeThatFits(_:) do proxy LayoutSubview não é o mesmo que o método do protocolo Layout; é a consulta do proxy ao tamanho preferido do filho dada uma proposta. Os dois compartilham o nome porque participam da mesma negociação, mas são camadas diferentes do contrato.
Um layout que quer saber os tamanhos dos filhos chama proxy.sizeThatFits(_:). Um layout que quer posicionar filhos chama proxy.place(at:anchor:proposal:) dentro de placeSubviews.
Posicionando subviews
placeSubviews é onde o layout toma decisões de posicionamento2:
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
var x = bounds.minX
let y = bounds.midY
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
subview.place(
at: CGPoint(x: x + size.width / 2, y: y),
anchor: .center,
proposal: ProposedViewSize(size)
)
x += size.width
}
}
A chamada place(at:anchor:proposal:) posiciona uma única subview. Três parâmetros:
at: a posição no espaço de coordenadas do pai.anchor: qual ponto da subview está emat..centercoloca o centro da subview emat;.topLeadingcoloca o canto superior esquerdo lá.proposal: o tamanho com o qual a subview deve renderizar. Passe o tamanho retornado pelosizeThatFitsda própria subview para honrar sua preferência, ou passe uma proposta personalizada para restringi-la.
Toda subview deve ser posicionada exatamente uma vez por chamada de placeSubviews. Pular uma subview a deixa sem posição (ela desaparece do layout renderizado); posicionar uma duas vezes é um erro em runtime.
Valores de layout personalizados através de LayoutValueKey
Quando um filho precisa comunicar algo ao seu layout pai (uma prioridade, um span, uma categoria), o canal é LayoutValueKey5:
struct PriorityKey: LayoutValueKey {
static let defaultValue: Int = 0
}
extension View {
func layoutPriority(_ value: Int) -> some View {
layoutValue(key: PriorityKey.self, value: value)
}
}
// Inside the Layout:
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let sortedSubviews = subviews.sorted {
$0[PriorityKey.self] > $1[PriorityKey.self]
}
// ... place sortedSubviews
}
O protocolo LayoutValueKey fornece um canal tipado para comunicação pai-filho. O filho anexa um valor via o modificador layout-value; o pai o lê via o subscript de LayoutSubview. Cada chave tem um valor padrão para subviews que não especificam um explicitamente.
O padrão é conceitualmente o que modificadores nativos como .layoutPriority(_:) expressam. O framework expõe esse valor específico através de uma propriedade dedicada priority: Double em LayoutSubview em vez de através de uma LayoutValueKey pública, então o acesso pelo proxy à prioridade de layout é subview.priority em vez de um subscript de chave. Layouts personalizados declaram seus próprios tipos LayoutValueKey para qualquer outro dado estruturado que precisem dos filhos.
O parâmetro cache
Ambos os métodos de layout recebem um parâmetro cache: inout. O cache é o lugar do layout para amortizar trabalho entre sizeThatFits e placeSubviews6:
struct DiagonalLayout: Layout {
struct Cache {
var sizes: [CGSize]
}
func makeCache(subviews: Subviews) -> Cache {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
return Cache(sizes: sizes)
}
func updateCache(_ cache: inout Cache, subviews: Subviews) {
cache.sizes = subviews.map { $0.sizeThatFits(.unspecified) }
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
let totalWidth = cache.sizes.reduce(0) { $0 + $1.width }
let totalHeight = cache.sizes.reduce(0) { $0 + $1.height }
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
var x = bounds.minX
var y = bounds.minY
for (subview, size) in zip(subviews, cache.sizes) {
subview.place(
at: CGPoint(x: x, y: y),
anchor: .topLeading,
proposal: ProposedViewSize(size)
)
x += size.width
y += size.height
}
}
}
O tipo padrão de cache é Void. A maioria dos layouts pode ignorar o cache; ele justifica seu lugar quando a computação de tamanho é genuinamente cara (medições recursivas, decisões de dimensionamento dinâmico) e os mesmos intermediários alimentam ambos os métodos de layout.
makeCache(subviews:) roda uma vez por passo de layout; updateCache(_:subviews:) roda quando as subviews mudam entre passos. O padrão permite que o layout invalide o estado em cache corretamente quando os próprios filhos mudam.
Layouts personalizados comuns que vale a pena construir
Três padrões que vale a pena construir você mesmo:
Flow layout (itens com quebra). Itens quebram em múltiplas linhas quando ultrapassam a largura disponível. O HStack da Apple não quebra. Um Layout personalizado pode: medir cada filho, posicionar da esquerda para a direita, avançar para a próxima linha quando a largura da linha exceder a largura da proposta.
Stack diagonal. Itens escalonados diagonalmente (cada filho posicionado um pouco para baixo e para a direita do anterior). Útil para UIs de cartões empilhados, layouts de pré-visualização de galeria, stacks com sensação de paralaxe.
Layout em pizza/círculo. Itens dispostos ao redor da circunferência de um círculo. Útil para menus radiais, UIs baseadas em tempo, rótulos categóricos com espaçamento igual.
Cada um destes é implementável com sizeThatFits + placeSubviews + (opcionalmente) um cache personalizado. O framework lida com a negociação em que o pai propõe e o filho dispõe; o desenvolvedor lida com a matemática de posicionamento.
Falhas comuns de layout
Três padrões que produzem layouts personalizados quebrados:
Tamanhos codificados que ignoram a proposta. Um layout que sempre retorna CGSize(width: 200, height: 100) não se adapta ao seu container. O resultado: o layout parece bom no simulador, mas quebra em telas menores, em orientações diferentes ou dentro de containers redimensionáveis.
Pular subviews em placeSubviews. Toda subview deve ser posicionada exatamente uma vez por chamada. Um loop for que tem um continue para alguma condição deixa essas subviews sem posição; elas desaparecem da saída renderizada.
Usar GeometryReader dentro dos filhos de um Layout personalizado. O GeometryReader sempre propõe o espaço completo recebido para seu conteúdo, o que luta contra as propostas por filho do layout. A combinação produz tamanhos sem sentido. Layouts personalizados não devem colocar GeometryReader dentro de si mesmos; se um filho precisa saber seu tamanho alocado, o mecanismo de proposta do protocolo de layout é o canal certo.
Quando recorrer ao Layout (e quando não)
Três sinais de que um Layout personalizado é a ferramenta certa:
- O formato não é expressável com composição de HStack/VStack/ZStack/Grid. Layouts em pizza, grids tipo masonry, quebra de fluxo personalizada. As primitivas nativas não conseguem se compor nesses formatos.
- Informação por filho dirige o posicionamento. Layouts onde os filhos têm prioridades, pesos ou categorias que o pai usa para posicioná-los.
LayoutValueKeyé o canal certo. - O dimensionamento do layout depende de negociar com os filhos. Layouts que perguntam “qual é a menor altura que cabe a linha mais longa?” ou “qual largura dá colunas iguais a N filhos?” precisam de acesso a consultas
subviews.sizeThatFits(...).
Três sinais de que a composição nativa é suficiente:
- Empilhamento padrão horizontal/vertical/em profundidade.
HStack,VStack,ZStackcobrem os casos comuns. - Grid com linhas/colunas regulares.
GrideLazyVGrid/LazyHGridlidam com a maioria dos casos de grid. - Um pouco de posicionamento de overlay.
.overlay,.background,ZStackcom alinhamento cobrem a maioria dos padrões “X em cima de Y”.
A regra prática: não construa um Layout personalizado para um formato que os nativos lidam. Construa um quando o formato estiver genuinamente além do conjunto de expressão dos nativos.
O que esse padrão significa para apps iOS 26+
Três pontos para levar.
-
Honre a proposta em
sizeThatFits. Um layout que retorna o mesmo tamanho independentemente deproposalnão participa do sistema de layout do SwiftUI corretamente. Leia a proposta, retorne um tamanho apropriado a ela. -
Use
LayoutValueKeypara comunicação pai-filho estruturada. Passar dados através de chaves anexadas por modificadores de view é o padrão nativo do SwiftUI. Não recorra a@EnvironmentouPreferenceKeypersonalizado para dados que são especificamente sobre decisões em nível de layout;LayoutValueKeyé o canal tipado para isso. -
Construa um cache apenas quando a medição for cara. O cache padrão
Voidestá bom para a maioria dos layouts. Recorra a um tipo de cache personalizado apenas quando a mesma computação cara aparecer tanto emsizeThatFitsquanto emplaceSubviews.
O cluster completo do Apple Ecosystem: App Intents tipados; servidores MCP; a questão de roteamento; Foundation Models; a distinção entre LLM de runtime vs tooling; três superfícies; o padrão de fonte única da verdade; Two MCP Servers; hooks para desenvolvimento Apple; Live Activities; o contrato de runtime do watchOS; internals do SwiftUI; o modelo mental espacial do RealityKit; disciplina de schema do SwiftData; padrões do Liquid Glass; shipping multi-plataforma; a matriz de plataformas; framework Vision; Symbol Effects; inferência Core ML; API de Writing Tools; Swift Testing; Privacy Manifest; Acessibilidade como plataforma; tipografia SF Pro; padrões espaciais do visionOS; framework Speech; migrações do SwiftData; focus engine do tvOS; internals do @Observable; sobre o que me recuso a escrever. O hub está na Apple Ecosystem Series. Para contexto mais amplo de iOS-com-agentes-de-IA, veja o guia de iOS Agent Development.
FAQ
Por que não usar simplesmente GeometryReader?
O GeometryReader sempre propõe o tamanho completo recebido ao seu conteúdo (ele não tem opinião sobre o que seu conteúdo quer). O resultado é que qualquer view dentro de um GeometryReader recebe infinity proposto para os eixos que o reader não restringe, e views como Text se dimensionam de forma gulosa. A composição luta contra si mesma: o reader passa adiante sem alteração, o conteúdo pede o tamanho máximo, o layout quebra. O Layout é a ferramenta certa porque permite que o desenvolvedor tome decisões explícitas por filho sobre o tamanho proposto.
Posso escrever um substituto personalizado para HStack?
Sim. Um Layout personalizado equivalente a HStack lê os tamanhos preferidos dos filhos, soma suas larguras, pega a altura máxima e os posiciona da esquerda para a direita. O HStack real faz mais (espaçamento, alinhamento, resolução de prioridade de layout), mas o formato básico é direto em Layout. O exercício é uma forma útil de internalizar como o protocolo funciona.
Como faço para suportar .layoutPriority(_:) no meu layout personalizado?
Leia-o através da propriedade dedicada priority: Double do proxy LayoutSubview: subview.priority. O SwiftUI expõe .layoutPriority(_:) diretamente no proxy em vez de através de uma LayoutValueKey pública. O valor padrão é 0. Use a prioridade ao distribuir espaço extra (dê preferencialmente a filhos de alta prioridade) ou ao truncar (trunque filhos de baixa prioridade primeiro).
Qual é a diferença entre proposal: .infinity e proposal: .zero?
.infinity propõe tamanho máximo em cada eixo (width: .infinity, height: .infinity). Filhos que respondem a propostas gulosas (como Color) ocupam todo o espaço disponível. .zero propõe tamanho mínimo (width: 0, height: 0). Filhos retornam seu tamanho mínimo (Text retorna o tamanho de seu maior token inquebrável). Os dois são endpoints úteis para medir o intervalo de dimensionamento dos filhos; muitos layouts usam .unspecified (ambos nil) para perguntar “qual é seu tamanho ideal?”.
O Layout funciona em watchOS, tvOS e visionOS?
Sim. O protocolo Layout está no núcleo cross-platform do SwiftUI. Layouts personalizados funcionam da mesma forma em iOS, iPadOS, macOS, watchOS, tvOS e visionOS. O post Apple Platform Matrix do cluster argumenta que a inclusão de plataformas é uma decisão de produto; o mecanismo Layout do SwiftUI é agnóstico em relação à plataforma para os casos em que múltiplas plataformas se aplicam.
Como o Layout interage com modelos @Observable?
Layout é uma struct que não armazena estado observável diretamente; ele não rastreia mudanças. Quando um modelo atualiza, o body da view pai é reavaliado, o que faz o Layout rodar novamente com quaisquer filhos que o body produzir. O Layout é reativo através do body em que vive, não através de hooks de observação próprios. O post @Observable internals do cluster cobre o lado de observação.
Referências
-
Apple Developer Documentation:
Layout. A referência do protocolo cobrindo os requisitossizeThatFitseplaceSubviews, além dos hooks opcionaismakeCache,updateCache,spacinge de alinhamento explícito. ↩ -
Apple Developer Documentation:
sizeThatFits(proposal:subviews:cache:)eplaceSubviews(in:proposal:subviews:cache:). Os dois métodos obrigatórios do protocoloLayout. ↩↩↩ -
Apple Developer Documentation:
ProposedViewSize. O tipo de dois CGFloats opcionais que carrega a proposta de tamanho do pai, com os valores convencionais.unspecified,.zeroe.infinity. ↩ -
Apple Developer Documentation:
LayoutSubview. O tipo proxy representando uma view filha dentro dos métodos deLayout, comsizeThatFits(_:)para consultar tamanhos preferidos eplace(at:anchor:proposal:)para posicionamento. ↩ -
Apple Developer Documentation:
LayoutValueKeyelayoutValue(key:value:). O canal tipado para dados em nível de layout do filho para o pai, acessado via subscript emLayoutSubview. ↩ -
Apple Developer: Composing custom layouts with SwiftUI. O guia da Apple cobrindo cache, alignment guides e quando recorrer ao
Layoutem vez de containers nativos. ↩