Layout personalizado SwiftUI com Simple Layout Engine

A matemática necessária para o layout personalizado do SwiftUI me lembra dos dias anteriores ao AutoLayout e ao sistema baseado em restrições. O bom é que o Simple Layout Engine já oferece um bom sistema para lidar com toda a matemática envolvida. Para demonstrar, eu usaria o subconjunto do aplicativo de demonstração da sessão WWDC neste tópico: Compor layouts personalizados com SwiftUI .
Problema
A ideia é ter uma exibição de contêiner semelhante a HStack
onde cada filho tem a mesma largura, mas com a exceção de que a largura deve ser a máxima que um filho tem. É assim que HStack
coloca os filhos por padrão.
HStack {
WLText("hi")
WLText("!")
WLText("beautiful")
WLText("world")
}
O que realmente queremos é ter algo parecido com width = max(children.width) , que faria com que todos os filhos tivessem a largura igual a qualquer que seja a largura do belo texto
BalancedHStack {
WLText("hi")
WLText("!")
WLText("beautiful")
WLText("world")
}
SwiftUI
fornece uma maneira de conectar o sistema de layout para fornecer toda a matemática personalizada. Para o nosso caso, podemos criar um BalancedHStack
que esteja em conformidade com o Layout
protocolo. O Layout
protocolo requer dois métodos:
sizeThatFits
: Para fornecer o totalCGSize
do contêiner ao sistemaplaceSubviews
: Para atualizar as posições das crianças dentro dos limites fornecidos- Motor de Esquema Simples
- Disposição
- ViewThatFits
struct BalancedHStack: Layout {
func sizeThatFits(proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) -> CGSize {
fatalError()
// TODO
}
func placeSubviews(in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()) {
// TODO
}
}
struct BalancedHStack: Layout {
struct CacheData {
let childSize: CGSize
let distances: [CGFloat]
}
func makeCache(subviews: Subviews) -> CacheData {
let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
let width = subviewSizes.map { $0.width }.max() ?? 0
let height = subviewSizes.map { $0.height }.max() ?? 0
let distances: [CGFloat] = (0..<subviews.count).map { idx in
guard idx < subviews.count - 1 else { return 0 }
return subviews[idx].spacing.distance(to: subviews[idx + 1].spacing, along: .horizontal)
}
return CacheData(
childSize: CGSize(width: width, height: height),
distances: distances
)
}
// ...
}
func sizeThatFits(proposal: ProposedViewSize,
subviews: Subviews,
cache: inout CacheData) -> CGSize {
let totalDistance = cache.distances.reduce(0, +)
return CGSize(
width: cache.childSize.width * CGFloat(subviews.count) + totalDistance,
height: cache.childSize.height
)
}
func placeSubviews(in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout CacheData) {
let layout = SLELayout(parentFrame: bounds, direction: .row, alignment: .center)
do {
var items: [SLEItem] = []
for idx in 0..<subviews.count {
items.append(try layout.add(item: .size(cache.childSize)))
try layout.add(item: .width(cache.distances[idx]))
}
for (idx, subview) in subviews.enumerated() {
subview.place(
at: try items[idx].frame().origin,
proposal: ProposedViewSize(cache.childSize)
)
}
}
catch { print("Unable to layout \(error)") }
}
extension SLEDirection {
var axis: Axis {
switch self {
case .row: return .horizontal
case .column: return .vertical
}
}
}
struct BalancedStack: Layout {
let direction: SLEDirection
init(_ direction: SLEDirection) {
self.direction = direction
}
struct CacheData {
let childSize: CGSize
let distances: [CGFloat]
}
func makeCache(subviews: Subviews) -> CacheData {
let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
let width = subviewSizes.map { $0.width }.max() ?? 0
let height = subviewSizes.map { $0.height }.max() ?? 0
let distances: [CGFloat] = (0..<subviews.count).map { idx in
guard idx < subviews.count - 1 else { return 0 }
return subviews[idx].spacing.distance(to: subviews[idx + 1].spacing, along: direction.axis)
}
return CacheData(
childSize: CGSize(width: width, height: height),
distances: distances
)
}
func sizeThatFits(proposal: ProposedViewSize,
subviews: Subviews,
cache: inout CacheData) -> CGSize {
let totalDistance = cache.distances.reduce(0, +)
let containerWidth: CGFloat
let containerHeight: CGFloat
switch direction {
case .row:
containerWidth = cache.childSize.width * CGFloat(subviews.count) + totalDistance
containerHeight = cache.childSize.height
case .column:
containerWidth = cache.childSize.width
containerHeight = cache.childSize.height * CGFloat(subviews.count) + totalDistance
}
return CGSize(width: containerWidth, height: containerHeight)
}
func placeSubviews(in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout CacheData) {
let layout = SLELayout(parentFrame: bounds, direction: direction, alignment: .center)
do {
var items: [SLEItem] = []
for idx in 0..<subviews.count {
items.append(try layout.add(item: .size(cache.childSize)))
try layout.add(item: .width(cache.distances[idx]))
}
for (idx, subview) in subviews.enumerated() {
subview.place(
at: try items[idx].frame().origin,
proposal: ProposedViewSize(cache.childSize)
)
}
}
catch { print("Unable to layout \(error)") }
}
}
struct TextList: View {
var body: some View {
WLText("hi")
WLText("!")
WLText("beautiful")
WLText("world")
}
}
ViewThatFits {
BalancedStack(.row) {
TextList()
}
BalancedStack(.column) {
TextList()
}
}