Aby zmierzyć etykietę w aplikacji na Maca bez jej rozciągania, dołącz onGeometryChange do widoku, który chcesz sprawdzić. W przypadku starszego celu wdrożenia umieść GeometryReader w tle widoku. Położenie modyfikatora określa, którą szerokość odczytasz.

Ten przewodnik mierzy syntetyczną etykietę wraz z jej wypełnieniem, zachowując niezależność otaczającego ją układu od wyniku. Wyjaśnia też, jak odróżnić bieżącą szerokość etykiety od jej szerokości idealnej i szerokości jej kontenera.

Dlaczego geometry reader powoduje rozszerzanie układu?

Układ SwiftUI opiera się na propozycjach i odpowiedziach: widok nadrzędny proponuje przestrzeń, a jego widoki podrzędne zgłaszają obliczone rozmiary. Dodanie ramki wprowadza do tej hierarchii kolejny widok, więc etykieta i jej ramka mogą mieć różne szerokości. Apple objaśnia ten proces w artykule Układanie prostego widoku.

GeometryReader to kontener o elastycznym preferowanym rozmiarze. Umieszczenie w nim kompaktowej etykiety zmienia więc element układu przedstawiany widokowi nadrzędnemu. Mierzysz kontener readera, który może zajmować znacznie więcej miejsca, niż potrzebuje tekst. Zobacz dokumentację GeometryReader firmy Apple.

Aby obserwować widok, dołącz pomiar do istniejącego widoku. Gdy potrzebny jest reader w tle, liczy się jego położenie: Apple dokumentuje, że tło otrzymuje rozmiar widoku, do którego jest dołączone. Dzięki temu obszar pomiaru wyznacza etykieta. Zobacz Dodawanie tła do widoku.

Zanim wybierzesz implementację, zdecyduj, na jakie pytanie ma odpowiadać interfejs:

Pytanie Odpowiednie podejście
Jaką szerokość ma ta etykieta w bieżącym układzie? Obserwuj geometrię etykiety.
Jaką szerokość ma plakietka z wypełnieniem wokół etykiety? Obserwuj widok po dodaniu wypełnienia.
Jaką szerokość ma wiersz zawierający etykietę? Obserwuj wiersz lub jego ramkę.
Jak szeroki byłby cały tekst zgodnie z jego preferencjami? Osobno sprawdź rozmiar idealny.
Która z kilku prezentacji się mieści? Rozważ ViewThatFits.
Jak wspólnie mierzyć i rozmieszczać wiele widoków podrzędnych? Rozważ własny Layout.

To rozróżnienie pozwala uniknąć częstego błędu: uznania szerokości ograniczonej, być może uciętej etykiety za przestrzeń potrzebną do wyświetlenia całego ciągu tekstowego.

Mierz bieżącą szerokość w macOS 13 lub nowszym

W aktualnej dokumentacji Apple jednoargumentowa wersja onGeometryChange(for:of:action:) z akcją jest dostępna w macOS 13 i nowszych. Przekształca proxy geometrii w wartość równoważną i wywołuje akcję, gdy ta wartość się zmieni. Użyj SDK zawierającego ten interfejs API i zastosuj pokazaną tu akcję z jednym parametrem. Zobacz onGeometryChange(for:of:action:).

Utwórz w Xcode projekt aplikacji na macOS korzystający ze SwiftUI i z celem wdrożenia macOS 13 lub nowszym. Zastąp wygenerowane deklaracje aplikacji i widoku zawartości tym kompletnym programem, pozostawiając w celu tylko jedną deklarację aplikacji @main. Przykład używa wyłącznie lokalnych, syntetycznych etykiet.

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

Obserwator odczytuje proxy.size.width; Apple definiuje GeometryProxy.size jako rozmiar widoku kontenera. W tym przykładzie obserwowany widok obejmuje tekst i wypełnienie, a następująca po nim ramka zapewnia niezależnie kontrolowaną propozycję szerokości. Zobacz GeometryProxy.size.

Pomiar aktualizuje oddzielny odczyt. Nie ustawia szerokości mierzonej plakietki. Stały obszar demonstracyjny i szerokość odczytu ułatwiają sprawdzenie wyniku, nie pozwalając, by tekst diagnostyczny zmieniał rozmiar otaczającego stosu.

Zwracanie tylko wartości CGFloat sprawia też, że obserwacja dotyczy wyłącznie szerokości: zmiana samej wysokości nie zmienia przekształconej wartości. Apple zaleca przekształcanie geometrii w konkretne potrzebne dane, ponieważ może ona zmieniać się często. Zobacz onGeometryChange(for:of:action:).

Przykłady sprawdzono pod kątem pobranych deklaracji API. Nie zostały tu skompilowane ani uruchomione, ponieważ dostępne środowisko to Linux bez łańcucha narzędzi Swift; zbuduj projekt i sprawdź interfejs w Xcode na Macu.

Gdzie umieścić modyfikator pomiaru?

Czytaj łańcuch modyfikatorów jako kolejne zagnieżdżone widoki. Pomiar powinien znajdować się bezpośrednio za ostatnim modyfikatorem wpływającym na rozmiar, który chcesz uwzględnić. Wyjaśnienie układu firmy Apple opisuje, jak ramki opakowują widoki, a wypełnienie zmienia przestrzeń dostępną dla ich zawartości. Zobacz Układanie prostego widoku.

Położenie pomiaru w przykładzie Co mierzy
Przed oboma modyfikatorami wypełnienia Bieżącą szerokość układu widoku tekstu
Po obu modyfikatorach wypełnienia Bieżącą szerokość plakietki z wypełnieniem
Za ramką o jawnie określonej szerokości Szerokość zewnętrznej ramki
Na otaczającym VStack Szerokość stosu

Dla ilustracji załóżmy, że widok tekstu zgłasza 96 punktów przy bieżącej propozycji. Dodanie po 12 punktów z każdej strony poziomej daje szerokość z wypełnieniem wynoszącą 120 punktów. Zewnętrzna ramka o szerokości 260 punktów to jeszcze inny pomiar. Te liczby ilustrują granice modyfikatorów; nie są wynikami pomiarów dla przykładowego ciągu.

Aby zbadać nieoczekiwaną wartość, tymczasowo umieść obramowanie w tym samym miejscu łańcucha modyfikatorów co obserwator. Porównaj tę granicę z zamierzonym celem. Reader dołączony do całego wiersza może prawidłowo zgłaszać jego szerokość, nawet gdy widoczna etykieta zajmuje tylko niewielką część wiersza.

Użyj readera w tle dla celu wdrożenia macOS 12

Dla celu wdrożenia macOS 12 użyj GeometryReader w tle, aby przekazać szerokość za pomocą preferencji. Użyty poniżej interfejs API tła z domknięciem końcowym jest dostępny od macOS 12. Apple zaleca tła, gdy widok z modyfikatorem powinien dominować w układzie. Zobacz background(alignment:content:).

To kompletny program alternatywny. Zastąp nim poprzedni przykład, zamiast dodawać drugą deklarację aplikacji.

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 przenosi wartość w kierunku widoków nadrzędnych, a onPreferenceChange odbiera zmiany tej wartości. Reader emituje pomiar zamiast przypisywać stan wewnątrz domknięcia zawartości. Zobacz dokumentację Apple: PreferenceKey i onPreferenceChange(_:perform:).

Ten reduktor zakłada, że emituje tylko jedna plakietka. Ponowne użycie tego samego klucza dla kilku widoków podrzędnych połączyłoby ich wartości, a ta implementacja zachowałaby ostatnią zredukowaną wartość. Jeśli śledzisz niezależnie kilka etykiet, zaprojektuj kolekcję pomiarów indeksowaną kluczami, zamiast używać jednej wartości skalarnej dla kilku widoków.

Jak sprawdzić, czy pomiar pozostaje niezależny?

Użyj pierwszego programu jako krótkiej sekwencji diagnostycznej:

  1. Zacznij od Blue circles i dużej dostępnej szerokości. Porównaj kolorową plakietkę z odczytem.
  2. Przeciągnij suwak. Zewnętrzna ramka zmienia się niezależnie; plakietka może pozostać kompaktowa, dopóki jest wystarczająco dużo miejsca.
  3. Wybierz Change sample label, a następnie zmniejsz dostępną szerokość. Sprawdź bieżącą, ograniczoną plakietkę zamiast oczekiwać szerokości idealnej całego ciągu.
  4. Przenieś obserwatora poniżej jawnie określonej ramki. Powinien teraz zgłaszać szerokość tej ramki.
  5. Przed ponownym użyciem pomiaru plakietki przywróć obserwatora nad ramkę.

Nie używaj obserwowanej szerokości w formule określającej szerokość tego samego widoku. Na przykład ustawienie jego ramki na obserwowaną szerokość powiększoną o wypełnienie może wielokrotnie przekazywać do układu coraz większy wynik. Wynika to bezpośrednio z utworzonej zależności: obserwacja zmienia stan, stan zmienia rozmiar, a nowy rozmiar powoduje kolejną obserwację.

Jeśli Twoim rzeczywistym zadaniem jest wybór między pełną etykietą a jej kompaktową wersją, ViewThatFits dostępny w macOS 13 i nowszych może wybrać pierwszy widok podrzędny, którego rozmiar idealny mieści się w ograniczonych osiach. Dzięki temu możesz w ogóle nie przechowywać szerokości. Zobacz ViewThatFits.

Jeśli trzeba wspólnie mierzyć i rozmieszczać kilka widoków podrzędnych, użyj własnego Layout w macOS 13 lub nowszym. Jego metody pomiaru i rozmieszczania pozwalają kontenerowi odpytać widoki podrzędne przed ich rozmieszczeniem, pozostawiając tę decyzję wewnątrz procesu układu. Zobacz Layout.

Czy rozmiar idealny lub metryki tekstu AppKit odpowiedzą na to samo pytanie?

Używaj .fixedSize(horizontal: true, vertical: false) tylko wtedy, gdy chcesz zachować idealną szerokość tekstu. Apple dokumentuje, że może to spowodować, iż tekst przekroczy granice widoku nadrzędnego. Dodanie tego modyfikatora w celu uzyskania pomiaru zmienia zachowanie wyświetlanego widoku dotyczące rozmiaru, więc nie zachowuje pierwotnego, ograniczonego układu. Zobacz fixedSize(horizontal:vertical:).

Liczba znaków również nie nadaje się do mierzenia szerokości. String.count w języku Swift liczy wartości Character reprezentowane przez rozszerzone klastry grafemów; nie zwraca wymiarów typograficznych. Pomnożenie tej liczby przez zgadywaną szerokość znaku nie stanowi pomiaru widoku SwiftUI. Zobacz Ciągi znaków i znaki.

W zadaniu rysowania w AppKit NSAttributedString.size() zwraca rozmiar potrzebny do narysowania danego ciągu atrybutowanego, który może zawierać ułamkowe wymiary. Apple zaleca zaokrąglić wynik w górę, jeśli używasz go do określenia rozmiaru widoku. Odpowiada to na pytanie dotyczące rysowania tekstu; wynik nie uwzględnia wypełnienia widoku SwiftUI ani otaczającej go ramki. Zobacz size().

W istniejącym układzie TextKit 1 NSLayoutManager.usedRect(for:) zgłasza obszar zajmowany przez glify ułożone obecnie w kontenerze tekstowym. Sam nie generuje glifów ani nie wykonuje układu; wywołaj ensureLayout(for:), jeśli układ musi zostać najpierw ukończony. Te API mierzą skonfigurowany kontener tekstowy, dlatego ich wyniki należy traktować oddzielnie od geometrii SwiftUI. Zobacz usedRect(for:) i ensureLayout(for:).

W przypadku etykiety już wyświetlanej w interfejsie SwiftUI na Macu obserwuj jej geometrię na potrzebnej granicy, utrzymuj odczyt jako niezależny i sprawdzaj zmiany treści oraz dostępnej przestrzeni. Dzięki temu uzyskasz przydatną szerokość, zachowując układ, który chcesz sprawdzić.

Źródła