Apresentando o mecanismo de layout simples

Nov 28 2022
Eu amo o layout automático. Isso ajuda muito ao projetar uma interface do usuário complexa.

Eu amo o layout automático . Isso ajuda muito ao projetar uma interface do usuário complexa. Mas há momentos em que a interface do usuário é muito simples e o layout automático pode parecer um pouco exagerado, enquanto outras vezes a interface do usuário pode ser um pouco complexa demais e o layout automático realmente começa a afetar o desempenho do aplicativo. Antes do layout automático, havia outra técnica para criar UI, chamada Springs and Struts (também conhecida como Layout Manual para contrastar com o Layout Automático). Eu gosto muito do Layout Manual também por sua simplicidade. Como acontece com todas as outras ferramentas, há compensações ao selecionar a melhor ferramenta para o trabalho e também se aplica ao selecionar Layout automático versus Layout manual.

O bom é que o Layout Automático não foi concebido como uma alternativa ao Layout Manual, mas sim como um suplemento. Portanto, em vez de calcularmos o framefor a UIView, começamos com a CGRect.zeroe deixamos o Auto Layout calcular o framevalor mais tarde. Na maioria das vezes é maravilhoso e não afeta nosso fluxo. Outras vezes, podemos ter que esperar a execução do passe de layout para ler os framevalores calculados.

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

Inspiração

A inspiração vem de como UIBarButtonItemfunciona com UIToolbarou UINavigationBar. Se quiséssemos construir uma interface do usuário como

Nós criaríamos um UIToolBare adicionaríamos um monte 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,
  ]

Acho que essa abordagem poderia ser usada para construir um mecanismo de layout que é muito simples em termos de modelo mental, mas pode ser usado para construir layouts tão sofisticados quanto quisermos.

Motor de Esquema Simples

Com esse design em mente, podemos construir o mecanismo de layout. Se houver uma classe Itemque seja um espaço reservado para a UIViewe outra classe Layoutque receba um ou mais deles Iteme calcule imediatamente o valor framede cada Item. Então podemos usar o framevalor calculado ao construir nossos UIViewobjetos.

Portanto, para criar uma subvisualização em tela cheia, devemos ser capazes de criar 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))

A implementação desse mecanismo de layout acaba não sendo tão sofisticada. Se fornecermos Itemquais podem ter algumas propriedades fixas e outras flexíveis.

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

E agora não parece difícil imaginar o suporte ao alinhamento para subviews (atualmente elas estão todas definidas 0.0ou alinhadas a start) com 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

O código completo para o Simple Layout Engineestá disponível em:

Há também uma implementação Objective-C que eu acho que tem uma implementação muito mais simples

E finalmente o artigo original: