要在不拉伸標籤的情況下測量 Mac 應用程式中的標籤,請將 onGeometryChange 附加到您要檢查的視圖。針對較舊的部署目標,請在其背景中放置 GeometryReader。修飾符的位置決定您讀取哪個寬度。

本演示測量合成標籤(包括其內邊距),同時保持其周圍佈局獨立於結果。它還解釋如何區分標籤的當前寬度、理想寬度及其容器的寬度。

為什麼幾何讀取器會導致佈局擴展?

SwiftUI 佈局涉及提議和回應:父視圖提議空間,子視圖報告其計算出的尺寸。添加 frame 會在該層級中引入另一個視圖,因此標籤及其 frame 可能有不同的寬度。Apple 在 佈局簡單視圖 中解釋了此過程。

GeometryReader 是一個具有靈活首選尺寸的容器。因此,將緊湊標籤包裝在其中會改變呈現給父視圖的佈局參與者。您測量的是讀取器的容器,它佔用的空間可能比文字所需的大得多。請參閱 Apple 的 GeometryReader 文檔。

為了觀察,請將測量附加到現有視圖。當需要背景讀取器時,其位置很重要:Apple 文檔說明背景會接收其附加視圖的尺寸。這讓標籤決定測量區域。請參閱 為您的視圖添加背景。

在選擇實現方式之前,請決定您的介面需要回答哪個問題:

您的問題 適當的方法
此標籤在其當前佈局中的寬度是多少? 觀察標籤的幾何資訊。
標籤周圍帶內邊距的徽章寬度是多少? 在內邊距後觀察。
其包含行的寬度是多少? 觀察行或其 frame。
整個文字傾向的寬度是多少? 單獨調查理想尺寸。
多種呈現中哪個適合? 考慮 ViewThatFits。
如何一起測量和定位多個子視圖? 考慮自訂 Layout。

這種區分可以防止常見錯誤:將受約束、可能被截斷的標籤寬度視為顯示其完整字串所需的空間。

在 macOS 13 及更新版本上測量當前寬度

Apple 目前的文檔將 onGeometryChange(for:of:action:) 的單值動作多載列為在 macOS 13 及更新版本可用。它將幾何代理轉換為可比較的值,並在該值改變時調用動作。使用包含此 API 的 SDK,並保留此處所示的單參數動作。請參閱 onGeometryChange(for:of:action:)。

在 Xcode 中使用 SwiftUI 建立 macOS 應用程式專案,部署目標為 macOS 13 或更新版本。將生成的應用程式和內容視圖宣告替換為此完整程式,目標中僅保留一個 @main 應用程式宣告。範例僅使用本地合成標籤。

import SwiftUI
import Foundation

@main
struct WidthDemoApp: App {
    var body: some Scene {
        WindowGroup {
            WidthDemoView()
        }
    }
}

struct WidthDemoView: View {
    @State private var measuredWidth: CGFloat = 0
    @State private var offeredWidth: Double = 260
    @State private var showLongLabel = false

    private var label: String {
        showLongLabel
            ? "Sample collection of blue circles"
            : "Blue circles"
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text(label)
                .font(.system(size: 16))
                .lineLimit(1)
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .background(Color.blue.opacity(0.12))
                .onGeometryChange(for: CGFloat.self) { proxy in
                    proxy.size.width
                } action: { newWidth in
                    measuredWidth = newWidth
                }
                .frame(
                    width: CGFloat(offeredWidth),
                    alignment: .leading
                )

            Text(String(
                format: "Measured badge: %.1f pt",
                Double(measuredWidth)
            ))
            .monospacedDigit()
            .frame(width: 340, alignment: .leading)

            Slider(value: $offeredWidth, in: 120...340)

            Button("Change sample label") {
                showLongLabel.toggle()
            }
        }
        .frame(width: 360, alignment: .leading)
        .padding(20)
    }
}

觀察器讀取 proxy.size.width;Apple 將 GeometryProxy.size 定義為容器視圖的尺寸。這裡,被觀察視圖包含文字和內邊距,而後續 frame 提供獨立控制的寬度提議。請參閱 GeometryProxy.size。

測量更新單獨的讀數。它不設定被測量徽章的寬度。固定的演示區域和讀數寬度使檢查結果更容易,而不允許診斷文字調整周圍堆疊的大小。

僅返回 CGFloat 也使觀察專注於寬度:僅高度改變不會改變轉換後的值。Apple 建議將幾何資訊轉換為您需要的特定資料,因為幾何資訊可能頻繁變化。請參閱 onGeometryChange(for:of:action:)。

這些範例經過檢索的 API 宣告檢查。由於可用環境是沒有 Swift 工具鏈的 Linux,因此這裡未編譯或執行它們;請在 Mac 上使用 Xcode 建構並檢查介面。

測量修飾符應放在哪裡?

將修飾符鏈視為包裝視圖的連續序列。測量應緊接在您想要包含的最後一個影響尺寸的修飾符之後。Apple 的佈局解釋描述了 frame 如何包裝視圖以及內邊距如何改變其內容可用空間。請參閱 佈局簡單視圖。

範例中的測量位置 觀察內容
兩個內邊距修飾符之前 文字視圖的當前佈局寬度
兩個內邊距修飾符之後 帶內邊距徽章的當前寬度
顯式寬度 frame 之後 外部 frame 的寬度
周圍的 VStack 上 堆疊的寬度

作為說明性計算,假設文字視圖在其當前提議下報告 96 點。在每個水平邊緣添加 12 點會產生 120 點的帶內邊距寬度。260 點的外部 frame 是另一個不同的測量值。這些數字說明修飾符邊界;它們不是範例字串的測量結果。

為了調查意外值,請在修飾符鏈中與觀察器相同的位置臨時放置邊框。將該邊界與預期目標進行比較。附加到整行的讀取器可以正確報告行寬,即使可見標籤僅佔其中一小部分。

針對 macOS 12 目標使用背景讀取器

針對 macOS 12 部署目標,請使用背景 GeometryReader 透過偏好設定發布寬度。下方使用的尾隨閉包背景 API 從 macOS 12 起可用。Apple 建議當修改後的視圖應主導佈局時使用背景。請參閱 background(alignment:content:)。

這是一個完整的替代程式。請用它替換前一個範例,而不是添加第二個應用程式宣告。

import SwiftUI
import Foundation

private struct BadgeWidthKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }

    static func reduce(
        value: inout CGFloat,
        nextValue: () -> CGFloat
    ) {
        value = nextValue()
    }
}

@main
struct LegacyWidthDemoApp: App {
    var body: some Scene {
        WindowGroup {
            LegacyWidthDemoView()
        }
    }
}

struct LegacyWidthDemoView: View {
    @State private var measuredWidth: CGFloat = 0
    @State private var showLongLabel = false

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text(showLongLabel ? "Blue circles collection" : "Blue circles")
                .font(.system(size: 16))
                .lineLimit(1)
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .background(Color.blue.opacity(0.12))
                .background {
                    GeometryReader { proxy in
                        Color.clear.preference(
                            key: BadgeWidthKey.self,
                            value: proxy.size.width
                        )
                    }
                }
                .onPreferenceChange(BadgeWidthKey.self) { newWidth in
                    measuredWidth = newWidth
                }

            Text(String(
                format: "Measured badge: %.1f pt",
                Double(measuredWidth)
            ))
            .frame(width: 340, alignment: .leading)

            Button("Change sample label") {
                showLongLabel.toggle()
            }
        }
        .frame(width: 360, alignment: .leading)
        .padding(20)
    }
}

PreferenceKey 將值向祖先傳遞,而 onPreferenceChange 接收該值的變化。讀取器發出測量值,而不是在其內容閉包中指派狀態。請參閱 Apple 的 PreferenceKey 和 onPreferenceChange(_:perform:) 文檔。

此縮減器刻意假設有一個發射徽章。為多個子視圖重複使用相同鍵會合併它們的值,此實現保留最後縮減的值。針對獨立追蹤的標籤,請設計鍵控測量集合,而不是讓一個標量代表多個視圖。

如何驗證測量保持獨立?

使用第一個程式作為小型診斷序列:

  1. 從 Blue circles 和寬裕的提供寬度開始。比較彩色徽章與讀數。
  2. 拖動滑桿。外部 frame 獨立變化;只要空間足夠,徽章可以保持緊湊。
  3. 選擇 Change sample label,然後減小提供寬度。檢查當前受約束的徽章,而不是期望完整字串的理想寬度。
  4. 將觀察器移至顯式 frame 下方。它現在應報告該 frame 的寬度。
  5. 在其他地方使用徽章測量之前,將觀察器恢復到 frame 上方。

將觀察到的寬度排除在決定該視圖寬度的公式之外。例如,將其 frame 設為觀察寬度加上內邊距,會反覆將更大的結果餵回佈局。這直接源於您創建的依賴關係:觀察改變狀態,狀態改變尺寸,新尺寸產生另一次觀察。

如果您的實際任務是在完整標籤和緊湊替代方案之間選擇,macOS 13 及更新版本上的 ViewThatFits 可以選擇理想尺寸適合受約束軸的第一個子視圖。這可以完全消除儲存寬度的需求。請參閱 ViewThatFits。

如果必須一起測量和定位多個子視圖,請在 macOS 13 及更新版本上使用自訂 Layout。其測量和放置方法讓容器在定位前查詢子視圖,將該決策保持在佈局過程中。請參閱 Layout。

理想尺寸或 AppKit 文字度量能回答同樣的問題嗎?

僅當您打算保留文字的理想寬度時才使用 .fixedSize(horizontal: true, vertical: false)。Apple 文檔說明這可能導致文字超出其父視圖邊界。為了獲得測量而添加它會改變顯示視圖的尺寸調整行為,因此它不保留原始受約束佈局。請參閱 fixedSize(horizontal:vertical:)。

字元計數也不適合作為寬度測量。Swift 的 String.count 計算由擴展字素群表示的 Character 值;它不返回排版尺寸。將該計數乘以猜測的字元寬度無法提供 SwiftUI 視圖的測量值。請參閱 字串與字元。

對於 AppKit 繪圖任務,NSAttributedString.size() 返回繪製該屬性字串所需的尺寸,可能包含小數尺寸。Apple 建議在使用該結果調整視圖大小時向上取整。這回答了文字繪製問題;不包含 SwiftUI 視圖的內邊距或外框。請參閱 size()。

對於現有的 TextKit 1 佈局,NSLayoutManager.usedRect(for:) 報告其文字容器中當前佈局的字形佔用區域。它本身不生成字形或執行佈局;當必須先完成佈局時調用 ensureLayout(for:)。這些 API 測量該配置的文字容器,因此請將其結果與 SwiftUI 幾何資訊分開。請參閱 usedRect(for:) 和 ensureLayout(for:)。

對於已在 SwiftUI Mac 介面中顯示的標籤,在您需要的邊界觀察其幾何資訊,保持讀數獨立,並測試內容和可用空間的變化。這會產生有用的寬度,同時保留您打算檢查的佈局。

來源