Przedstawiamy prosty silnik układu

Nov 28 2022
Uwielbiam układ automatyczny. Bardzo pomaga przy projektowaniu złożonego interfejsu użytkownika.

Uwielbiam układ automatyczny . Bardzo pomaga przy projektowaniu złożonego interfejsu użytkownika. Ale są chwile, kiedy interfejs użytkownika jest bardzo prosty, a układ automatyczny może wydawać się nieco przesadzony, a innym razem interfejs użytkownika może być nieco zbyt złożony, a układ automatyczny faktycznie zaczyna wpływać na wydajność aplikacji. Przed automatycznym układem istniała inna technika tworzenia interfejsu użytkownika, nazywana sprężynami i rozpórkami (znana również jako układ ręczny , aby kontrastować z układem automatycznym). Bardzo lubię Układ ręczny ze względu na jego prostotę. Podobnie jak w przypadku każdego innego narzędzia, istnieją kompromisy przy wyborze najlepszego narzędzia do zadania, dotyczy to również wyboru układu automatycznego w porównaniu z układem ręcznym.

Dobrą rzeczą jest to, że układ automatyczny nie został zaprojektowany jako alternatywa dla układu ręcznego, a raczej jako dodatek. Więc zamiast obliczać framedla a UIView, zaczynamy od a CGRect.zeroi pozwalamy Auto Layout obliczyć framewartość później. Przez większość czasu jest to wspaniałe i nie wpływa na nasz przepływ. Innym razem może być konieczne poczekanie na uruchomienie przebiegu układu, aby odczytać obliczone framewartości.

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

Inspiracja

Inspiracją jest to, jak UIBarButtonItemdziała z UIToolbarlub UINavigationBar. Gdybyśmy chcieli zbudować interfejs użytkownika podobny do

Stworzylibyśmy UIToolBari dodali kilkaUIBarButtonItem

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

Myślę, że to podejście można wykorzystać do zbudowania silnika układu, który jest bardzo prosty pod względem modelu mentalnego, ale może być używany do tworzenia tak wyrafinowanych układów, jak byśmy chcieli.

Prosty silnik układu

Mając ten projekt na uwadze, możemy zbudować silnik układu. Jeśli istnieje klasa Item, która jest symbolem zastępczym dla a UIViewi inna klasa Layout, która przyjmuje jedną lub więcej z nich Itemi natychmiast oblicza frameco Item. Następnie możemy wykorzystać obliczoną framewartość podczas konstruowania naszych UIViewobiektów.

Aby więc utworzyć widok podrzędny pełnego ekranu, powinniśmy być w stanie utworzyć jako:

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

Implementacja tego silnika układu okazuje się niezbyt wyrafinowana. Jeśli zapewnimy, Itemktóre mogą mieć pewne właściwości stałe, a inne elastyczne.

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

A teraz nietrudno sobie wyobrazić obsługę wyrównania dla widoków podrzędnych (obecnie wszystkie są ustawione 0.0lub wszystkie wyrównane do start) za pomocą czegoś takiego:

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

Cały kod programu Simple Layout Enginejest dostępny pod adresem:

Istnieje również implementacja Objective-C, która moim zdaniem ma znacznie prostszą implementację

I na koniec oryginalny artykuł: