Trang chủ

Lượt bố cục

Một lời đề nghị là gì, và cách đọc một bố cục đã ra sai

Một lời đề nghị là một ProposedViewSize — hai giá trị CGFloat?, bề rộng và chiều cao. Phần thú vị nằm ở chỗ chúng là optional, vì nil không phải số không và cũng không phải vô cực. Nó có nghĩa là “nói tôi nghe bạn muốn bao nhiêu”.

struct ProposedViewSize {
    var width: CGFloat?
    var height: CGFloat?
}

Ba lời đề nghị có tên riêng và đáng nhận mặt, vì các container dùng chúng để tra hỏi con mình trước khi quyết định bất cứ điều gì:

  • .unspecified — cả hai chiều đều nil. “Kích thước lý tưởng của bạn là bao nhiêu?” Một Text trả lời bằng bề rộng nội dung của nó trên một dòng.
  • .zero — cả hai bằng không. “Bạn nhỏ nhất được đến đâu?” Một Text trả lời bằng bề rộng của từ dài nhất không ngắt được.
  • .infinity — cả hai vô cực. “Bạn lớn nhất một cách có ích được đến đâu?” Một Text lại trả lời bằng bề rộng một dòng của nó; một Rectangle trả lời vô cực.

Một stack hỏi con mình những câu đó để biết ai co giãn được và ai không, rồi mới đưa ra các lời đề nghị thật dựa trên câu trả lời.

Các view thông dụng trả lời thế nào

Biết câu trả lời của từng view lá thì bớt được phần lớn việc đoán mò:

View Trả lời
Text Lấy bề rộng được mời, tối đa bằng thứ nó cần; chiều cao vừa với phần chữ đã xuống dòng
Image Kích thước pixel tự nhiên của nó, phớt lờ hoàn toàn lời đề nghị — trừ khi .resizable()
Rectangle, Color, Spacer Đúng bằng thứ được đề nghị, dù lớn đến đâu
HStack / VStack Tổng của các con theo trục chính, lớn nhất theo trục còn lại
.frame(width:height:) Đúng bằng thế, bất kể con đã chọn gì

Image là cái hay bẫy người ta. Một Image chưa sửa gì thì lấy đúng kích thước của chính nó và không lời đề nghị nào đổi được điều đó, đó là lý do một tấm ảnh lớn tràn ra khỏi container cho tới khi thêm .resizable() — và là lý do riêng .resizable() sẽ bóp méo nó cho tới khi thêm cả một .aspectRatio hay .scaledToFit.

Cảnh báo

.frame(width: 100) đề nghị 100 cho con nhưng không cắt nó. Một đứa con chọn rộng 200 thì sẽ rộng 200, căn giữa, vẽ ra ngoài biên của frame và đè lên hàng xóm. Hãy thêm .clipped() nếu bạn cần lời hứa được thi hành — dù thế nào thì cái frame vẫn báo 100 cho view cha của nó, và đó là cách một view rốt cuộc chồng lên thứ mà nó trông như có đủ chỗ.

Đọc một bố cục hỏng

Hai câu hỏi, theo đúng thứ tự này, trả lời được gần như mọi thứ:

1. View này đã được đề nghị gì? Đi ngược lên cây. Mọi modifier bọc và mọi container nằm giữa đây và gốc đều có thể đã đổi lời đề nghị.

2. Nó đã chọn gì? Đi xuôi xuống. Một đứa con phớt lờ lời đề nghị — Image, một frame cố định, một Text có .fixedSize() — sẽ kết thúc cuộc trò chuyện bất kể được mời gì.

Công cụ thực dụng là một cái viền tạm, đặt trên cả hai view:

ProfileCard(user: user)
    .border(.red)                    // view này đã chọn gì
    .padding()
    .border(.blue)                   // cái lớp bọc padding đã thành ra bao nhiêu

Hai cái viền khác màu cho bạn thấy lời đề nghị và câu trả lời dưới dạng hai hình chữ nhật. Nếu đỏ lấp đầy xanh thì đứa con đã chấp nhận; nếu đỏ nhỏ nằm trong xanh thì nó đã tự chọn kích thước, và phần chênh lệch chính là chỗ bố cục của bạn đi mất.

.fixedSize(), cửa thoát hiểm

.fixedSize() đề nghị nil cho đứa con — “cứ lấy kích thước lý tưởng của bạn” — và là cách chữa tiêu chuẩn cho phần chữ bị cắt cụt bởi một view cha mời quá ít.

Text("Một dòng dài cứ bị cắt cụt mãi")
    .fixedSize(horizontal: false, vertical: true)

Đúng cách viết đó là cái đáng thuộc lòng: chấp nhận bề rộng nào được mời cũng được, nhưng lấy bằng hết chiều cao mình cần. Đó là cách chữa cho phần chữ bị cắt còn một dòng bên trong một stack, và nó xuất hiện liên tục.

Dùng với cả hai là true thì view lấy kích thước lý tưởng ở cả hai chiều và sẽ vui vẻ tràn ra khỏi view cha. Thỉnh thoảng đó đúng là thứ bạn muốn, và thường hơn thì đó là khởi đầu của một lỗi khác.

Vì sao lượt bố cục chạy nhiều hơn một lần

Lượt bố cục không phải một cú quét từ trên xuống duy nhất. Một stack tra hỏi các con bằng .zero, .unspecified và .infinity để phân loại chúng trước khi đề nghị bất cứ thứ gì thật, nên phần logic tính kích thước của một đứa con có thể chạy vài lần trong một khung hình với những lời đề nghị khác nhau.

Bình thường điều này vô hình, và nó quan trọng trong hai trường hợp. Công việc tốn kém nằm trên đường tính kích thước của một view sẽ chạy nhiều lần hơn bạn tưởng — đó là một lý do khiến GeometryReader đặt trong một dòng của List có thể chậm. Và một bố cục phụ thuộc vào trạng thái mà chính nó cũng ghi thì có thể dao động, và đó chính là điều mà cảnh báo lúc chạy “Bound preference … tried to update multiple times per frame” đang nói với bạn.