Apresentando o mecanismo de layout simples
Eu amo o layout automático . Isso ajuda muito ao projetar uma interface do usuário complexa. Mas há momentos em que a interface do usuário é muito simples e o layout automático pode parecer um pouco exagerado, enquanto outras vezes a interface do usuário pode ser um pouco complexa demais e o layout automático realmente começa a afetar o desempenho do aplicativo. Antes do layout automático, havia outra técnica para criar UI, chamada Springs and Struts (também conhecida como Layout Manual para contrastar com o Layout Automático). Eu gosto muito do Layout Manual também por sua simplicidade. Como acontece com todas as outras ferramentas, há compensações ao selecionar a melhor ferramenta para o trabalho e também se aplica ao selecionar Layout automático versus Layout manual.
O bom é que o Layout Automático não foi concebido como uma alternativa ao Layout Manual, mas sim como um suplemento. Portanto, em vez de calcularmos o frame
for a UIView
, começamos com a CGRect.zero
e deixamos o Auto Layout calcular o frame
valor mais tarde. Na maioria das vezes é maravilhoso e não afeta nosso fluxo. Outras vezes, podemos ter que esperar a execução do passe de layout para ler os frame
valores calculados.
// let Auto layout calculate the frame values
DispatchQueue.main.async {
// start using the frame values for something else.
}
Inspiração
A inspiração vem de como UIBarButtonItem
funciona com UIToolbar
ou UINavigationBar
. Se quiséssemos construir uma interface do usuário como

Nós criaríamos um UIToolBar
e adicionaríamos um monte deUIBarButtonItem
let toolbar = UIToolbar(frame: toolbarFrame)
let playButton = UIBarButtonItem(systemItem: .play)
let pauseButton = UIBarButtonItem(systemItem: .pause)
let rewindButton = UIBarButtonItem(systemItem: .rewind)
let forwardButton = UIBarButtonItem(systemItem: .fastForward)
let spaceButton = UIBarButtonItem(systemItem: .flexibleSpace)
toolbar.items = [
spaceButton,
rewindButton, spaceButton,
playButton, spaceButton,
pauseButton, spaceButton,
forwardButton, spaceButton,
]
Acho que essa abordagem poderia ser usada para construir um mecanismo de layout que é muito simples em termos de modelo mental, mas pode ser usado para construir layouts tão sofisticados quanto quisermos.
Motor de Esquema Simples
Com esse design em mente, podemos construir o mecanismo de layout. Se houver uma classe Item
que seja um espaço reservado para a UIView
e outra classe Layout
que receba um ou mais deles Item
e calcule imediatamente o valor frame
de cada Item
. Então podemos usar o frame
valor calculado ao construir nossos UIView
objetos.
Portanto, para criar uma subvisualização em tela cheia, devemos ser capazes de criar como:

let layout = Layout(parentFrame: frame, direction: .column)
let mainItem = try layout.add(item: .flexible)
let redView = SLECreateView(try mainItem.frame(), .red)
addSubview(redView)
private func SLECreateView(_ frame: CGRect, _ color: UIColor) -> UIView {
let view = UIView(frame: frame)
view.backgroundColor = color
return view
}

let layout = Layout(parentFrame: frame, direction: .column, alignment: .center)
try layout.add(item: .flexible)
try layout.add(item: .height(200))
let topFrame = try layout.frame(at: 0)
let bottomFrame = try layout.frame(at: 1)
addSubview(SLECreateView(topFrame, .red))
addSubview(SLECreateView(bottomFrame, .blue))

let mainLayout = Layout(parentFrame: frame, direction: .column)
try mainLayout.add(items: [.flexible, .height(44), .height(200)])
let headerFrame = try mainLayout.frame(at: 0)
let toolbarFrame = try mainLayout.frame(at: 1)
let footerFrame = try mainLayout.frame(at: 2)
addSubview(SLECreateView(headerFrame, .red))
addSubview(SLECreateView(toolbarFrame, .blue))
addSubview(SLECreateView(footerFrame, .yellow))
let contentLayout = Layout(parentFrame: footerFrame, direction: .row)
try contentLayout.add(items: [.flexible, .flexible])
let content1Frame = try contentLayout.frame(at: 0)
let content2Frame = try contentLayout.frame(at: 1)
addSubview(SLECreateView(content1Frame, .cyan))
addSubview(SLECreateView(content2Frame, .magenta))
A implementação desse mecanismo de layout acaba não sendo tão sofisticada. Se fornecermos Item
quais podem ter algumas propriedades fixas e outras flexíveis.
public class Item {
// no values fixed
public static var flexible: Item { get }
// partially fixed
public static func width(_ value: CGFloat) -> Item
public static func height(_ value: CGFloat) -> Item
// all fixed
public static func size(_ value: CGSize) -> Item
// ...
}
public class Item {
// ...
public func frame() throws -> CGRect {
return try rect.frame()
}
internal let originalWidth: CGFloat?
internal let originalHeight: CGFloat?
private let rect = Rect()
private init(width: CGFloat?, height: CGFloat?) {
originalWidth = width
originalHeight = height
}
// called by layout engine
func updateSize(value: CGFloat,
in direction: Direction,
parentSize: CGSize) { /* update rect */ }
func updateOrigin(itemOrigin: CGPoint,
in direction: Direction,
alignment: Alignment,
parentFrame: CGRect) -> CGPoint { /* update rect */ }
}
private class Rect {
internal private(set) var width: CGFloat?
internal private(set) var height: CGFloat?
private var x: CGFloat?
private var y: CGFloat?
// read back by Item
func frame() throws -> CGRect {
guard let originX = x, let originY = y, let width = width, let height = height else {
throw LayoutError.itemIncomplete
}
return CGRect(x: originX, y: originY, width: width, height: height)
}
// set by layout engine
func set(origin: CGPoint) {
x = origin.x
y = origin.y
}
// set by layout engine
func set(size: CGSize) {
width = size.width
height = size.height
}
}
extension Layout {
public func add(item: Item) throws {
items.append(item)
try updateFrames()
}
}
private extension Layout {
func updateFrames() throws {
// calculate total flex height
var totalFlexSpace = parentFrame.height
var flexItems = 0
for item in items {
if let space = item.originalHeight {
totalFlexSpace -= space
} else {
flexItems += 1
}
}
// calculate height per flex item
let itemSpace = totalFlexSpace/CGFloat(max(flexItems, 1))
guard itemSpace >= 0 else {
throw LayoutError.outOfSpace
}
// update final frames per item
var itemOrigin = parentFrame.origin
for item in items {
item.updateSize(value: itemSpace,
in: .column,
parentSize: parentFrame.size)
itemOrigin = item.updateOrigin(itemOrigin: itemOrigin,
in: .column,
alignment: alignment,
parentFrame: parentFrame)
}
}
}
E agora não parece difícil imaginar o suporte ao alinhamento para subviews (atualmente elas estão todas definidas 0.0
ou alinhadas a start
) com algo como:
public enum Alignment {
case leading
case center
case trailing
}
private extension Alignment {
func align(parent: CGFloat, item: CGFloat) -> CGFloat {
switch self {
case .leading: return 0
case .trailing: return (parent - item)
case .center: return (parent - item) / 2.0
}
}
}
let offset = alignment.align(parent: parentFrame.height, item: rect.height)
y = parentFrame.origin.y + offset
O código completo para o Simple Layout Engine
está disponível em:
Há também uma implementação Objective-C que eu acho que tem uma implementação muito mais simples
E finalmente o artigo original: