Basit Düzen Motoru Tanıtımı

Nov 28 2022
Auto Layout'u seviyorum. Karmaşık kullanıcı arabirimi tasarlarken çok yardımcı olur.

Auto Layout'u seviyorum . Karmaşık kullanıcı arabirimi tasarlarken çok yardımcı olur. Ancak, kullanıcı arayüzünün çok basit olduğu ve Otomatik Düzenin biraz abartılı olduğu zamanlar vardır, diğer zamanlarda kullanıcı arayüzünün biraz fazla karmaşık olabileceği ve Otomatik Düzenin aslında uygulama performansını etkilemeye başladığı zamanlar vardır. Otomatik mizanpajdan önce, UI oluşturmak için başka bir teknik vardı, buna Yaylar ve Destekler denir ( Otomatik Mizanpajın aksine Manuel Mizanpaj olarak da bilinir ). Manuel Düzeni sadeliği nedeniyle de çok seviyorum. Diğer tüm araçlarda olduğu gibi, iş için en iyi aracı seçerken ödünleşimler vardır ve bu aynı zamanda Otomatik Düzen ve Manuel Düzen'i seçerken de geçerlidir.

İşin iyi yanı, Otomatik Düzen, Manuel Düzen'e bir alternatif olarak tasarlanmamıştır, daha çok bir ek gibi tasarlanmıştır. Dolayısıyla a için hesaplamak yerine framea UIViewile başlıyoruz CGRect.zerove Otomatik Düzenin framedeğeri daha sonra hesaplamasına izin veriyoruz. Çoğu zaman harikadır ve akışımızı etkilemez. frameDiğer zamanlarda, hesaplanan değerleri geri okumak için düzen geçiş çalıştırmasını beklememiz gerekebilir .

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

Esin

İlham, veya ile nasıl UIBarButtonItemçalıştığından gelir . Gibi bir kullanıcı arayüzü oluşturmak isteseydikUIToolbarUINavigationBar

Bir oluşturup UIToolBarbir demet eklerdikUIBarButtonItem

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

Bence bu yaklaşım, zihinsel model açısından çok basit olan ancak istediğimiz kadar karmaşık düzenler oluşturmak için kullanılabilen bir düzen motoru oluşturmak için kullanılabilir.

Basit Düzen Motoru

Bu tasarımı göz önünde bulundurarak, düzen motorunu oluşturabiliriz. Itema için yer tutucu olan bir sınıf ve bunlardan birini veya daha fazlasını alan ve hemen her birini hesaplayan UIViewbaşka bir sınıf varsa . Ardından, nesnelerimizi oluştururken hesaplanan değeri kullanabiliriz.LayoutItemframeItemframeUIView

Bu nedenle, tam ekran bir alt görünüm oluşturmak için şu şekilde oluşturabilmeliyiz:

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

Bu düzen motorunun uygulanmasının o kadar karmaşık olmadığı ortaya çıktı. ItemHangi özelliklerin sabit ve diğerlerinin esnek olabileceğini sağlarsak .

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

Ve şimdi, alt görünümler için hizalamayı desteklemeyi hayal etmek zor görünmüyor (şu anda hepsi ayarlandı 0.0veya hepsi hizalandı start) gibi bir şeyle:

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

için kodun tamamı Simple Layout Engineşu adreste mevcuttur:

Ayrıca çok daha basit bir uygulamaya sahip olduğunu düşündüğüm bir Objective-C uygulaması var.

Ve son olarak orijinal makale: