ขอแนะนำเครื่องมือเค้าโครงอย่างง่าย
ฉันชอบเค้าโครงอัตโนมัติ ช่วยได้มากเมื่อออกแบบ 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 ซึ่งฉันคิดว่ามีการใช้งานที่ง่ายกว่ามาก
และสุดท้ายบทความต้นฉบับ: