Présentation du moteur de mise en page simple

Nov 28 2022
J'adore la mise en page automatique. Cela aide beaucoup lors de la conception d'interfaces utilisateur complexes.

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 framepour a UIView, nous commençons par a CGRect.zeroet laissons la mise en page automatique calculer la framevaleur 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 framevaleurs 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 UIBarButtonItemfonctionne avec UIToolbarou UINavigationBar. Si nous voulions créer une interface utilisateur comme

Nous créerions un UIToolBaret 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 Itemqui est un espace réservé pour a UIViewet une autre classe Layoutqui prend un ou plusieurs d'entre eux Itemet calcule immédiatement le framede chaque Item. Ensuite, nous pouvons utiliser la valeur calculée framelors de la construction de nos UIViewobjets.

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 Itemqui 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.0ou 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 Engineest 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 :