간단한 레이아웃 엔진 소개

Nov 28 2022
나는 자동 레이아웃을 좋아합니다. 복잡한 UI를 디자인할 때 많은 도움이 됩니다.

나는 자동 레이아웃 을 좋아 합니다. 복잡한 UI를 디자인할 때 많은 도움이 됩니다. 그러나 UI가 매우 단순하고 Auto Layout이 약간 과도하게 느껴질 때가 있고, UI가 너무 복잡하여 Auto Layout이 실제로 앱 성능에 영향을 미치기 시작하는 경우도 있습니다. 자동 레이아웃 이전에는 UI를 생성하는 또 다른 기술이 있었는데, 이를 Springs 및 Struts 라고 합니다( 자동 레이아웃과 대조되는 수동 레이아웃 이라고도 함 ). 나는 단순성 때문에 수동 레이아웃을 많이 좋아합니다. 다른 모든 도구와 마찬가지로 작업에 가장 적합한 도구를 선택할 때 장단점이 있으며 자동 레이아웃과 수동 레이아웃을 선택할 때도 적용됩니다.

좋은 점은 자동 레이아웃이 수동 레이아웃의 대안이 아니라 보완책에 가깝다는 것입니다. 따라서 framefor a 를 계산하는 대신 a UIView로 시작 CGRect.zero하여 자동 레이아웃이 frame나중에 값을 계산하도록 합니다. 대부분의 경우 훌륭하고 흐름에 영향을 미치지 않습니다. frame다른 경우에는 계산된 값 을 다시 읽기 위해 레이아웃 패스 실행을 기다려야 할 수도 있습니다 .

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

영감

또는 UIBarButtonItem와 함께 작동 하는 방식 에서 영감을 얻었습니다 . 다음과 같은 UI를 만들고 싶다면UIToolbarUINavigationBar

우리는 를 만들고 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,
  ]

이 접근 방식은 정신 모델 측면에서 매우 단순하지만 우리가 원하는 정교한 레이아웃을 구축하는 데 사용할 수 있는 레이아웃 엔진을 구축하는 데 사용할 수 있다고 생각합니다.

간단한 레이아웃 엔진

이러한 설계를 염두에 두고 레이아웃 엔진을 구축할 수 있습니다. Itema 의 자리 표시자인 클래스 와 이들 중 하나 이상을 가져와 즉시 the every 를 계산하는 UIView다른 클래스 가 있는 경우 . 그런 다음 개체 를 구성할 때 계산된 값 을 사용할 수 있습니다.LayoutItemframeItemframeUIView

따라서 전체 화면 하위 보기를 만들려면 다음과 같이 만들 수 있어야 합니다.

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 구현도 있습니다.

그리고 마지막으로 원본 기사: