ขอแนะนำเครื่องมือเค้าโครงอย่างง่าย

Nov 28 2022
ฉันชอบเค้าโครงอัตโนมัติ ช่วยได้มากเมื่อออกแบบ UI ที่ซับซ้อน

ฉันชอบเค้าโครงอัตโนมัติ ช่วยได้มากเมื่อออกแบบ UI ที่ซับซ้อน แต่มีบางครั้งที่ UI เรียบง่ายมากและ Auto Layout อาจรู้สึกว่าเกินความจำเป็นไปเล็กน้อย ในขณะที่บางครั้ง UI อาจซับซ้อนเกินไปเล็กน้อย และ Auto Layout เริ่มส่งผลต่อประสิทธิภาพของแอป ก่อนการจัดวางอัตโนมัติ มีอีกเทคนิคหนึ่งในการสร้าง UI เรียกว่าSprings and Struts (เรียกอีกอย่างว่าManual Layoutเพื่อให้ตรงกันข้ามกับ Auto Layout) ฉันชอบการจัดวางด้วยตนเองมากเช่นกันสำหรับความเรียบง่าย เช่นเดียวกับเครื่องมืออื่น ๆ มีการแลกเปลี่ยนเมื่อเลือกเครื่องมือที่ดีที่สุดสำหรับงาน และยังใช้เมื่อเลือกการจัดวางอัตโนมัติเทียบกับการจัดวางด้วยตนเอง

ข้อดีคือ Auto Layout ไม่ได้ออกแบบมาเป็นทางเลือกแทน Manual Layout แต่เป็นเหมือนส่วนเสริมมากกว่า ดังนั้นแทนที่จะต้องคำนวณค่าframeสำหรับ a UIViewเราเริ่มต้นด้วย a CGRect.zeroและให้ Auto Layout คำนวณframeค่าในภายหลัง ส่วนใหญ่เป็นเรื่องที่ยอดเยี่ยมและไม่ส่งผลกระทบต่อกระแสของเรา ในบางครั้ง เราอาจต้องรอการเรียกใช้ Layout Pass เพื่ออ่านกลับframeค่าที่คำนวณได้

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

แรงบันดาลใจ

แรงบันดาลใจมาจากการUIBarButtonItemทำงานร่วมกับUIToolbarหรือ UINavigationBarหากเราต้องการสร้าง UI แบบ

เราจะสร้าง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 ซึ่งฉันคิดว่ามีการใช้งานที่ง่ายกว่ามาก

และสุดท้ายบทความต้นฉบับ: