Einführung der Simple-Layout-Engine

Nov 28 2022
Ich liebe Auto-Layout. Es hilft sehr beim Entwerfen komplexer Benutzeroberflächen.

Ich liebe Auto-Layout . Es hilft sehr beim Entwerfen komplexer Benutzeroberflächen. Aber es gibt Zeiten, in denen die Benutzeroberfläche sehr einfach ist und das automatische Layout sich etwas übertrieben anfühlt, während die Benutzeroberfläche manchmal etwas zu komplex ist und das automatische Layout tatsächlich anfängt, die App-Leistung zu beeinträchtigen. Vor dem automatischen Layout gab es eine andere Technik zum Erstellen der Benutzeroberfläche, sie heißt Springs and Struts ( im Gegensatz zum automatischen Layout auch als manuelles Layout bekannt). Ich mag auch das manuelle Layout wegen seiner Einfachheit sehr. Wie bei jedem anderen Werkzeug gibt es Kompromisse bei der Auswahl des besten Werkzeugs für den Job, und dies gilt auch bei der Auswahl von Auto Layout vs. Manual Layout.

Das Gute ist, dass das Auto-Layout nicht als Alternative zum manuellen Layout konzipiert ist, sondern eher als Ergänzung. Anstatt also das framefür a berechnen zu UIViewmüssen, beginnen wir mit a CGRect.zeround lassen den frameWert später vom Auto-Layout berechnen. Meistens ist es wunderbar und beeinflusst unseren Fluss nicht. In anderen Fällen müssen wir möglicherweise auf den Layoutdurchlauf warten, um die berechneten frameWerte zurückzulesen.

// let Auto layout calculate the frame values
DispatchQueue.main.async {
  // start using the frame values for something else.
}

Inspiration

Die Inspiration stammt von der Funktionsweise UIBarButtonItemvon UIToolbaroder UINavigationBar. Wenn wir eine Benutzeroberfläche wie

Wir würden eine erstellen UIToolBarund eine Reihe von hinzufügenUIBarButtonItem

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,
  ]

Ich denke, dieser Ansatz könnte verwendet werden, um eine Layout-Engine zu erstellen, die in Bezug auf das mentale Modell sehr einfach ist, aber zum Erstellen von so anspruchsvollen Layouts verwendet werden kann, wie wir möchten.

Einfache Layout-Engine

Mit diesem Design im Hinterkopf können wir eine Layout-Engine bauen. Wenn es eine Klasse Itemgibt, die ein Platzhalter für a ist , UIViewund eine andere Klasse Layout, die eine oder mehrere davon aufnimmt Itemund sofort die framevon every berechnet Item. Dann können wir den errechneten frameWert beim Konstruieren unserer UIViewObjekte verwenden.

Um also eine Vollbild-Unteransicht zu erstellen, sollten wir Folgendes erstellen können:

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))

Die Implementierung dieser Layout-Engine erweist sich als nicht so ausgeklügelt. Wenn wir bereitstellen Item, können einige Eigenschaften fest und andere flexibel sein.

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)
    }
  }
}

Und jetzt scheint es nicht schwer vorstellbar, die Ausrichtung für Unteransichten zu unterstützen (derzeit sind sie alle festgelegt 0.0oder alle ausgerichtet auf start) mit etwas wie:

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

Der gesamte Code für Simple Layout Engineist verfügbar unter:

Es gibt auch eine Objective-C-Implementierung, die meiner Meinung nach eine viel einfachere Implementierung hat

Und zum Schluss der Originalartikel: