Stack, frame và căn chỉnh
Không gian được chia thế nào, và làm sao cho các dòng thẳng hàng với nhau
Một stack phải chia một lời đề nghị hữu hạn cho những đứa con mà mỗi đứa muốn một phần khác nhau. Thuật toán này đáng biết vì nó giải thích mọi câu “sao cái view kia lại chiếm hết chỗ?”.
- Stack trừ đi phần khoảng cách của chính nó khỏi lời đề nghị.
- Nó hỏi từng đứa con kích thước nhỏ nhất (
.zero) và lớn nhất (.infinity) theo trục chính. - Nó sắp xếp các con theo độ co giãn — khoảng cách giữa hai con số đó.
- Nó đề nghị cho đứa ít co giãn nhất trước, cho nó một phần bằng nhau của chỗ còn lại.
- Phần đứa con đó không dùng đến quay về quỹ chung cho những đứa còn lại.
Những đứa con cứng nhắc — Text, Image, một frame cố định — được phục vụ trước và lấy đúng thứ
chúng cần. Những đứa co giãn — Spacer, Color, một view có maxWidth: .infinity — chia nhau
phần thừa.
HStack {
Text("Tên") // cứng nhắc: lấy đúng thứ nó cần
Spacer() // co giãn: hút hết phần còn lại
Text("Giá trị") // cứng nhắc
}
Đây là lý do một Text dài đứng cạnh một Spacer không bao giờ bị cắt cụt cho tới khi chỗ thật sự
hết, và là lý do hai Spacer trong một stack chia đều phần dư.
Mẹo
Spacer không phải khoảng trống rỗng. Nó là một view chấp nhận mọi lời đề nghị và không vẽ gì
cả, với một độ dài tối thiểu của riêng nó. Hai Spacer kẹp quanh một Text là cách thành ngữ để
căn giữa nó trong một stack mà vẫn cho nó đẩy hai bên ra khi nó dài thêm.
layoutPriority ghi đè thứ tự sắp xếp
Khi hai đứa con co giãn ngang nhau mà bạn muốn một đứa thắng, layoutPriority đẩy nó lên đầu bước 4:
HStack {
Text(title).layoutPriority(1) // phục vụ trước, lấy đúng thứ nó cần
Text(subtitle) // nhận phần dư, cắt cụt nếu thiếu
}
Hãy dùng dè dặt. Nó là công cụ thật cho trường hợp “tiêu đề không bao giờ được cắt cụt, phụ đề thì được”, và cũng là một cách phổ biến để che đi một bố cục mà vấn đề thật sự là view cha đề nghị quá ít.
Căn chỉnh bên trong một stack
Tham số căn chỉnh của một stack quyết định những đứa con hẹp hơn stack sẽ được đặt ở đâu theo trục còn lại:
VStack(alignment: .leading) { … } // các con thẳng hàng theo cạnh đầu
HStack(alignment: .top) { … } // các con thẳng hàng theo cạnh trên
Điểm tinh tế là căn chỉnh được tính từ một đường dẫn căn chỉnh, và đường mặc định cho .leading
là cạnh đầu của chính view đó — nhưng không phải lúc nào cũng vậy. .firstTextBaseline căn một hình
ảnh theo đường chân chữ của phần chữ bên cạnh, đúng thứ bạn thật sự muốn ở một dòng ghép cả hai và là
thứ không thể chỉnh tay bằng padding cho đúng được.
HStack(alignment: .firstTextBaseline) {
Image(systemName: "star.fill")
Text("Nổi bật")
}
Cho các dòng thẳng hàng với nhau
Căn chỉnh bên trong một stack thì dễ. Xếp một cột nhãn thẳng hàng qua nhiều dòng riêng biệt — để mọi giá trị bắt đầu ở cùng một tọa độ x, bất kể nhãn dài ngắn ra sao — mới là bài toán mà các đường dẫn căn chỉnh tùy biến sinh ra để giải.
extension HorizontalAlignment {
private enum ValueColumn: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.leading]
}
}
static let valueColumn = HorizontalAlignment(ValueColumn.self)
}
Rồi dùng nó làm căn chỉnh của VStack, và đánh dấu điểm cần thẳng hàng ở mỗi dòng:
VStack(alignment: .valueColumn) {
HStack {
Text("Tên")
Text(user.name).alignmentGuide(.valueColumn) { $0[.leading] }
}
HStack {
Text("Địa chỉ email")
Text(user.email).alignmentGuide(.valueColumn) { $0[.leading] }
}
}
VStack thu giá trị đường dẫn từ mỗi đứa con, lấy giá trị lớn nhất, rồi dịch các dòng cho khớp
nhau. Không GeometryReader, không lượt đo riêng, không trạng thái — nó diễn ra ngay trong lượt bố
cục bình thường.
Cảnh báo
Đây là thứ mà người ta hay với tới GeometryReader cùng một PreferenceKey để tự dựng. Các
đường dẫn căn chỉnh làm việc đó ngay trong lượt bố cục, không trễ thêm khung hình nào, và chúng
là câu trả lời đúng thường xuyên hơn nhiều so với cách đi đo.
Các biến thể của frame
Ba dạng frame, làm những việc thật sự khác nhau:
.frame(width:height:)— đề nghị đúng chừng đó cho con và báo đúng chừng đó cho cha. Đứa con vẫn có thể chọn lớn hơn và tràn ra..frame(minWidth:idealWidth:maxWidth:…)— kẹp lời đề nghị vào một khoảng.maxWidth: .infinitylà kiểu “lấp đầy bề rộng sẵn có” tiêu chuẩn, cònminWidth:là cách bạn ngăn một thứ co sập lại..frame(alignment:)— đi kèm một trong hai dạng trên, quyết định đứa con ngồi ở đâu khi nó chọn nhỏ hơn cái frame.
Cái người ta hay viết nhầm là .frame(maxWidth: .infinity) lên một thứ mà sau đó vẫn không lấp đầy
bề rộng — vì maxWidth nâng trần của lời đề nghị lên, còn một Text vẫn tự do chọn ít hơn. Hãy
thêm alignment: .leading nếu thứ bạn thật sự muốn là phần chữ ghim sát trái trong một dòng rộng
hết cỡ.