Trang chủ

Thôi với tới GeometryReader

GeometryReader là công cụ đầu tiên phần lớn mọi người tìm thấy khi một bố cục SwiftUI cần một phép đo, và nó thường là công cụ sai. Nó có hai đặc tính khiến nó là một lựa chọn mặc định tệ, và phần lớn những cách dùng truyền thống của nó giờ đã có những thứ thay thế chuyên dụng.

Vì sao nó là lựa chọn mặc định tệ

Nó tham lam. GeometryReader chấp nhận bất cứ kích thước nào được mời, ở cả hai chiều. Bọc một Text vào trong nó và phần chữ không còn tự lấy kích thước nữa — cái reader nở ra lấp đầy, còn phần chữ ngồi ở góc trên bên trái của một cái hộp lớn hơn nhiều.

VStack {
    GeometryReader { proxy in
        Text("Xin chào")       // giờ nằm ở góc trên bên trái của toàn bộ chỗ trống
    }
    Text("Thế giới")           // bị đẩy xuống đáy
}

Đây là nguồn gốc của gần như mọi câu hỏi “sao bố cục của tôi lại có một khoảng hở khổng lồ”.

Nó tham gia vào lượt tính kích thước. Nội dung của nó được đo và bố cục ngay trong lúc layout, và layout chạy nhiều lần trong một khung hình — một stack tra hỏi các con bằng vài lời đề nghị khác nhau trước khi quyết định bất cứ điều gì. Nằm trong một dòng của List, điều đó nghĩa là body của reader bị tính đi tính lại trong lúc cuộn, một cách đáng tin cậy để làm cú cuộn bị giật.

Dùng gì thay thế

Cho “lấp đầy container, hoặc một phần của nó”: containerRelativeFrame.

// cũ
GeometryReader { proxy in
    Card().frame(width: proxy.size.width * 0.8)
}

// mới
Card().containerRelativeFrame(.horizontal) { width, _ in width * 0.8 }

Không tham lam, không có lượt đo riêng, và nó đọc lên đúng như nghĩa của nó.

Cho “xếp các thứ thẳng hàng qua nhiều dòng”: các đường dẫn căn chỉnh.

Đây là trường hợp người ta dựng bằng GeometryReader cộng một PreferenceKey cộng @State, tức là ba bộ phận động, một khung hình trễ, và một vòng lặp bố cục tiềm tàng. Các đường dẫn căn chỉnh tùy biến làm việc đó ngay trong lượt bố cục bình thường:

extension HorizontalAlignment {
    private enum ValueColumn: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat { context[.leading] }
    }
    static let valueColumn = HorizontalAlignment(ValueColumn.self)
}

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] }
    }
}

Mọi cột giá trị thẳng hàng bất kể nhãn dài ngắn ra sao, không trạng thái và không thêm khung hình trễ nào.

Cho “tôi cần biết kích thước của view này”: onGeometryChange.

Card()
    .onGeometryChange(for: CGSize.self) { proxy in
        proxy.size
    } action: { newSize in
        cardSize = newSize
    }

Cái này đọc hình học mà không đổi bố cục chút nào — view tự lấy kích thước bình thường rồi báo lại sau. Đó chính xác là thứ mà GeometryReader không bao giờ làm được, và nó dẹp hẳn hành vi tham lam.

Cho vị trí cuộn và các hiệu ứng theo cuộn: các API cuộn.

ScrollView {
    …
}
.scrollPosition(id: $scrolledID)

// hiệu ứng theo từng phần tử
Card()
    .scrollTransition { content, phase in
        content.opacity(phase.isIdentity ? 1 : 0.5)
    }

Cái khuôn mẫu tiêu đề parallax vốn cần một GeometryReader báo về một khung trong hệ tọa độ .global giờ là visualEffect cộng một geometry proxy, không cần chút trạng thái nào.

Mẹo

visualEffect là cái bị đánh giá thấp. Nó cho bạn một geometry proxy để áp các phép biến hình, độ dịch và độ mờ, nhưng các hiệu ứng đó không tham gia vào bố cục — nên không có gì làm xê dịch các view quanh nó và không có gì chạy lại lượt bố cục.

Khi nào nó vẫn đúng

Tôi không muốn nói quá. GeometryReader vẫn đúng cho:

  • Việc vẽ thật sự cần biết kích thước đầy đủ — một biểu đồ tùy biến, một canvas, một hình nền phải biết biên của nó.
  • Đọc một khung trong một hệ tọa độ cụ thể ở chỗ onGeometryChange không vừa, dù khoảng trống này cứ hẹp dần.
  • Bất cứ thứ gì dù sao cũng nên lấp đầy container của nó, nơi tính tham lam đúng là thứ bạn muốn.

Sự phân biệt: GeometryReader là một container lấp đầy không gian rồi nói cho bạn biết về nó. Hãy dùng nó khi bạn muốn một container lấp đầy. Đừng dùng nó khi bạn muốn một phép đo, vì một phép đo giờ là một API khác.

Quy tắc bỏ túi

Trước khi viết GeometryReader, hãy hỏi bạn thật sự cần gì:

Tôi cần Dùng
Một phần của container containerRelativeFrame
Căn thẳng hàng qua nhiều dòng riêng biệt AlignmentID tự định nghĩa
Kích thước của view này, sau khi bố cục onGeometryChange
Một hiệu ứng thị giác theo cuộn scrollTransition / visualEffect
Đang cuộn tới phần tử nào scrollPosition(id:)
Một cách sắp xếp con tùy biến protocol Layout
Lấp đầy chỗ trống rồi vẽ trong đó GeometryReader

Chỉ dòng cuối cùng mới là công cụ mà ai cũng với tới đầu tiên.