Memperkenalkan Mesin Tata Letak Sederhana

Nov 28 2022
Saya suka Tata Letak Otomatis. Ini sangat membantu saat mendesain UI yang kompleks.

Saya suka Tata Letak Otomatis . Ini sangat membantu saat mendesain UI yang kompleks. Namun ada kalanya UI sangat sederhana dan Tata Letak Otomatis mungkin terasa sedikit berlebihan, sementara di lain waktu UI mungkin agak terlalu rumit dan Tata Letak Otomatis benar-benar mulai memengaruhi kinerja aplikasi. Sebelum tata letak otomatis ada teknik lain untuk membuat UI, itu disebut Pegas dan Struts (juga dikenal sebagai Tata Letak Manual berbeda dengan Tata Letak Otomatis). Saya juga sangat menyukai Tata Letak Manual karena kesederhanaannya. Seperti setiap alat lainnya, ada pertukaran saat memilih alat terbaik untuk pekerjaan itu, dan itu juga berlaku saat memilih Tata Letak Otomatis vs Tata Letak Manual.

Hal baiknya adalah, Tata Letak Otomatis belum dirancang sebagai alternatif Tata Letak Manual, melainkan lebih seperti pelengkap. Jadi, daripada kita harus menghitung frameuntuk a, UIViewkita mulai dengan a CGRect.zerodan membiarkan Tata Letak Otomatis menghitung framenilainya nanti. Sebagian besar waktu itu luar biasa dan tidak memengaruhi aliran kami. Di lain waktu kita mungkin harus menunggu layout pass berjalan untuk membaca kembali nilai yang dihitung frame.

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

Inspirasi

Inspirasinya adalah dari cara UIBarButtonItembekerja dengan UIToolbaror UINavigationBar. Jika kami ingin membangun UI seperti

Kami akan membuat UIToolBardan menambahkan banyakUIBarButtonItem

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

Saya pikir pendekatan ini dapat digunakan untuk membangun mesin tata letak yang sangat sederhana dalam hal model mental tetapi dapat digunakan untuk membangun tata letak canggih seperti yang kita inginkan.

Mesin Tata Letak Sederhana

Dengan desain itu, kita dapat membuat mesin tata letak. Jika ada kelas Itemyang merupakan placeholder untuk a UIViewdan kelas lain Layoutyang mengambil satu atau lebih dari ini Itemdan segera menghitung framedari setiap Item. Kemudian kita bisa menggunakan nilai yang dihitung framesaat membuat UIViewobjek kita.

Jadi untuk membuat subview layar penuh kita harus bisa membuat seperti:

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

Penerapan layout engine ini ternyata tidak secanggih itu. Jika kami menyediakan Itemyang dapat memiliki beberapa properti tetap dan yang lainnya fleksibel.

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

Dan sekarang sepertinya tidak sulit untuk membayangkan mendukung penyelarasan untuk sub tampilan (saat ini semua disetel 0.0atau semua disejajarkan dengan start) dengan sesuatu seperti:

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

Seluruh kode untuk Simple Layout Enginetersedia di:

Ada juga implementasi Objective-C yang menurut saya memiliki implementasi yang jauh lebih sederhana

Dan akhirnya artikel aslinya: