Знакомство с Simple Layout Engine

Nov 28 2022
Мне нравится Автомакет. Это очень помогает при разработке сложного пользовательского интерфейса.

Мне нравится Автомакет . Это очень помогает при разработке сложного пользовательского интерфейса. Но бывают случаи, когда пользовательский интерфейс очень прост, и Auto Layout может показаться немного излишним, в то время как в других случаях пользовательский интерфейс может быть слишком сложным, и Auto Layout фактически начинает влиять на производительность приложения. До автоматической компоновки существовал еще один метод создания пользовательского интерфейса, он назывался Springs и Struts (также известный как ручная компоновка , в отличие от автоматической компоновки). Мне также очень нравится Manual Layout за его простоту. Как и в случае с любым другим инструментом, при выборе лучшего инструмента для работы есть компромиссы, и это также применимо при выборе автоматического макета против ручного макета.

Хорошо то, что Auto Layout не был разработан как альтернатива Manual Layout, скорее как дополнение. Таким образом, вместо того, чтобы вычислять frameдля a, UIViewмы начинаем с a CGRect.zeroи позволяем Auto Layout вычислить frameзначение позже. Большую часть времени это замечательно и не влияет на наш поток. В других случаях нам, возможно, придется дождаться выполнения прохода компоновки, чтобы считать вычисленные frameзначения.

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

Вдохновение

Вдохновение от того, как UIBarButtonItemработает с UIToolbarили UINavigationBar. Если бы мы хотели создать пользовательский интерфейс, подобный

Мы бы создали UIToolBarи добавили кучуUIBarButtonItem

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

Я думаю, что этот подход можно использовать для создания механизма компоновки, который очень прост с точки зрения ментальной модели, но может использоваться для создания настолько сложных макетов, сколько нам нужно.

Простой механизм компоновки

Имея в виду этот дизайн, мы можем создать механизм компоновки. Если есть класс Item, который является заполнителем для a, UIViewи другой класс Layout, который принимает один или несколько из них Itemи немедленно вычисляет frameзначение каждого Item. Затем мы можем использовать вычисленное frameзначение при построении наших UIViewобъектов.

Таким образом, для создания полноэкранного подвида мы должны иметь возможность создать как:

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

Реализация этого механизма компоновки оказывается не такой сложной. Если мы предоставим Item, некоторые свойства могут быть фиксированными, а другие гибкими.

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

И теперь нетрудно представить поддержку выравнивания для подпредставлений (в настоящее время все они установлены 0.0или все выровнены по start) с помощью чего-то вроде:

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

Весь код Simple Layout Engineдоступен по адресу:

Существует также реализация Objective-C, которая, как мне кажется, имеет гораздо более простую реализацию.

И, наконец, оригинал статьи: