Presentamos el motor de diseño simple

Nov 28 2022
Me encanta el diseño automático. Ayuda mucho al diseñar una interfaz de usuario compleja.

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 framepara a UIView, comenzamos con a CGRect.zeroy dejamos que Auto Layout calcule el framevalor 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 framevalores 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 UIBarButtonItemfunciona con UIToolbaro UINavigationBar. Si quisiéramos construir una interfaz de usuario como

Crearíamos un UIToolBary 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 Itemque es un marcador de posición para ay UIViewotra clase Layoutque toma uno o más de estos Iteme inmediatamente calcula el framede cada Item. Entonces podemos usar el framevalor calculado al construir nuestros UIViewobjetos.

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 Itemque 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.0o 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 Engineestá 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: