Presentamos el motor de diseño simple
Me encanta el diseño automático . Ayuda mucho al diseñar una interfaz de usuario compleja. Pero hay momentos en que la interfaz de usuario es muy simple y el diseño automático puede parecer un poco exagerado, mientras que otras veces la interfaz de usuario puede ser demasiado compleja y el diseño automático realmente comienza a afectar el rendimiento de la aplicación. Antes del diseño automático, había otra técnica para crear la interfaz de usuario, se llama Springs and Struts (también conocido como diseño manual para contrastar con el diseño automático). También me gusta mucho el diseño manual por su simplicidad. Al igual que con cualquier otra herramienta, existen ventajas y desventajas al seleccionar la mejor herramienta para el trabajo, y también se aplica al seleccionar Diseño automático frente a Diseño manual.
Lo bueno es que el diseño automático no se ha diseñado como una alternativa al diseño manual, sino más bien como un complemento. Entonces, en lugar de tener que calcular el frame
para a UIView
, comenzamos con a CGRect.zero
y dejamos que Auto Layout calcule el frame
valor más tarde. La mayoría de las veces es maravilloso y no afecta nuestro flujo. Otras veces, es posible que tengamos que esperar a que se ejecute el pase de diseño para volver a leer los frame
valores calculados.
// let Auto layout calculate the frame values
DispatchQueue.main.async {
// start using the frame values for something else.
}
Inspiración
La inspiración proviene de cómo UIBarButtonItem
funciona con UIToolbar
o UINavigationBar
. Si quisiéramos construir una interfaz de usuario como

Crearíamos un UIToolBar
y agregaríamos un montón 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,
]
Creo que este enfoque podría usarse para construir un motor de diseño que es muy simple en términos de modelo mental pero que puede usarse para construir diseños tan sofisticados como queramos.
Motor de diseño simple
Con ese diseño en mente, podemos construir un motor de diseño. Si hay una clase Item
que es un marcador de posición para ay UIView
otra clase Layout
que toma uno o más de estos Item
e inmediatamente calcula el frame
de cada Item
. Entonces podemos usar el frame
valor calculado al construir nuestros UIView
objetos.
Entonces, para crear una subvista de pantalla completa, deberíamos poder crear 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))
La implementación de este motor de diseño resulta no ser tan sofisticada. Si disponemos Item
que puede tener unas propiedades fijas y otras flexibles.
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)
}
}
}
Y ahora no parece difícil imaginar admitir la alineación para subvistas (actualmente están todas configuradas 0.0
o todas alineadas start
) con 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
El código completo para el Simple Layout Engine
está disponible en:
También hay una implementación de Objective-C que creo que tiene una implementación mucho más simple
Y finalmente el artículo original: