Memperkenalkan Mesin Tata Letak Sederhana
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 frame
untuk a, UIView
kita mulai dengan a CGRect.zero
dan membiarkan Tata Letak Otomatis menghitung frame
nilainya 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 UIBarButtonItem
bekerja dengan UIToolbar
or UINavigationBar
. Jika kami ingin membangun UI seperti

Kami akan membuat UIToolBar
dan 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 Item
yang merupakan placeholder untuk a UIView
dan kelas lain Layout
yang mengambil satu atau lebih dari ini Item
dan segera menghitung frame
dari setiap Item
. Kemudian kita bisa menggunakan nilai yang dihitung frame
saat membuat UIView
objek 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 Item
yang 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.0
atau 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 Engine
tersedia di:
Ada juga implementasi Objective-C yang menurut saya memiliki implementasi yang jauh lebih sederhana
Dan akhirnya artikel aslinya: