Présentation du moteur de mise en page simple
J'adore la mise en page automatique . Cela aide beaucoup lors de la conception d'interfaces utilisateur complexes. Mais il y a des moments où l'interface utilisateur est très simple et la mise en page automatique peut sembler un peu exagérée, tandis que d'autres fois, l'interface utilisateur peut être un peu trop complexe et la mise en page automatique commence à affecter les performances de l'application. Avant la mise en page automatique, il existait une autre technique de création d'interface utilisateur, appelée Springs and Struts (également connue sous le nom de mise en page manuelle pour contraster avec la mise en page automatique). J'aime aussi beaucoup la mise en page manuelle pour sa simplicité. Comme avec tous les autres outils, il existe des compromis lors de la sélection du meilleur outil pour le travail, et cela s'applique également lors de la sélection de la mise en page automatique par rapport à la mise en page manuelle.
La bonne chose est que la mise en page automatique n'a pas été conçue comme une alternative à la mise en page manuelle, plutôt comme un supplément. Ainsi, au lieu de devoir calculer le frame
pour a UIView
, nous commençons par a CGRect.zero
et laissons la mise en page automatique calculer la frame
valeur plus tard. La plupart du temps, c'est merveilleux et n'affecte pas notre flux. D'autres fois, nous devrons peut-être attendre l'exécution de la passe de mise en page pour relire les frame
valeurs calculées.
// let Auto layout calculate the frame values
DispatchQueue.main.async {
// start using the frame values for something else.
}
Inspiration
L'inspiration vient de comment UIBarButtonItem
fonctionne avec UIToolbar
ou UINavigationBar
. Si nous voulions créer une interface utilisateur comme

Nous créerions un UIToolBar
et ajouterions un tas 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,
]
Je pense que cette approche pourrait être utilisée pour construire un moteur de mise en page qui est très simple en termes de modèle mental mais qui peut être utilisé pour construire des mises en page aussi sophistiquées que nous le souhaiterions.
Moteur de mise en page simple
Avec cette conception à l'esprit, nous pouvons créer un moteur de mise en page. S'il y a une classe Item
qui est un espace réservé pour a UIView
et une autre classe Layout
qui prend un ou plusieurs d'entre eux Item
et calcule immédiatement le frame
de chaque Item
. Ensuite, nous pouvons utiliser la valeur calculée frame
lors de la construction de nos UIView
objets.
Donc, pour créer une sous-vue en plein écran, nous devrions pouvoir créer comme :

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))
L'implémentation de ce moteur de mise en page s'avère moins sophistiquée. Si nous fournissons Item
qui peut avoir certaines propriétés fixes et d'autres 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)
}
}
}
Et maintenant, il ne semble pas difficile d'imaginer prendre en charge l'alignement des sous-vues (actuellement, elles sont toutes définies 0.0
ou toutes alignées sur start
) avec quelque chose comme :
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
Le code complet du Simple Layout Engine
est disponible sur :
Il y a aussi une implémentation Objective-C qui, je pense, a une implémentation beaucoup plus simple
Et enfin l'article original :