@ScaledMetric — co to jest, property wrapper SwiftUI i Dynamic Type

Autor: IT Sectr Opublikowano: 2026-06-27 Czas czytania: 10 min

@ScaledMetric — property wrapper SwiftUI, który automatycznie skaluje wartość liczbową zgodnie z ustawieniami Dynamic Type użytkownika. Wartość jest opakowywana w @ScaledMetric i przeliczana przy zmianie systemowego rozmiaru czcionki, co gwarantuje dostępność interfejsu dla osób z wadami wzroku. Według Apple Developer Documentation (2026), @ScaledMetric używa skali UIFontMetrics do obliczenia względnej skali na podstawie preferred content size category. Więcej o accessibility przeczytasz w materiale o accessibility SwiftUI.

Najważniejsze

  • @ScaledMetric — property wrapper SwiftUI do skalowania wartości pod Dynamic Type.
  • Dynamic Type — systemowe ustawienie iOS zmieniające rozmiar czcionki z UIFontTextStyle.
  • Skalowanie — @ScaledMetric przyjmuje wartość bazową i mnożnik relativeTo.
  • Automatyczna aktualizacja — przy zmianie Dynamic Type @ScaledMetric przelicza się i UI jest aktualizowany.
  • Accessibility — użycie @ScaledMetric poprawia dostępność interfejsu bez dodatkowego kodu.

Czym jest @ScaledMetric?

@ScaledMetric — property wrapper SwiftUI, dodany w iOS 14, który automatycznie skaluje wartość liczbową (CGFloat, Int, Double) do bieżącego rozmiaru czcionki Dynamic Type. W przeciwieństwie do .font(.body) dla czcionek, @ScaledMetric skaluje dowolne parametry liczbowe: padding, spacing, cornerRadius, iconSize — wszystko, co powinno proporcjonalnie zwiększać się przy dużym tekście.

Głównym zadaniem @ScaledMetric jest zapewnienie accessibility-skalowania nie-tekstowych elementów interfejsu. Gdy użytkownik zwiększa czcionkę w ustawieniach iOS, przyciski, ikony i odstępy powinny zwiększać się proporcjonalnie, aby interfejs pozostał zbalansowany. @ScaledMetric rozwiązuje to zadanie automatycznie, bez ręcznego obliczania mnożników.

Składnia @ScaledMetric

Podstawowa składnia @ScaledMetric używa wartości domyślnej i opcjonalnego parametru relativeTo. Jeśli relativeTo jest określony, skalowanie jest powiązane z konkretnym stylem tekstu (UIFontTextStyle). Jeśli nie — używana jest skala .body.

swift
struct AccessibleButton: View {
    @ScaledMetric private var padding: CGFloat = 12
    @ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
    
    var body: some View {
        Label("Prześlij", systemImage: "checkmark.circle.fill")
            .font(.body)
            .padding(padding)
            .imageScale(.init(rawValue: iconSize / 24) ?? .medium)
    }
}

padding będzie skalowany względem .body (domyślnie), iconSize — względem .title. Przy dużym tekście odstępy i ikona zwiększą się proporcjonalnie. Bez @ScaledMetric odstępy pozostałyby 12 pt przy dowolnym rozmiarze czcionki, co prowadziłoby do wizualnej nierównowagi.

Jak działa @ScaledMetric

Mechanizm @ScaledMetric opiera się na UIFontMetrics z UIKit. Gdy SwiftUI tworzy instancję @ScaledMetric, oblicza mnożnik na podstawie bieżącej preferred content size category (UIContentSizeCategory). Wartość bazowa jest mnożona przez scaledValue z UIFontMetrics dla określonego stylu tekstu.

Matematycznie: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo). Jeśli relativeTo nie jest określone, używany jest UIFontMetrics.default powiązany z .body. Przy zmianie Dynamic Type SwiftUI odtwarza ciało View, @ScaledMetric oblicza nową scaledValue i UI aktualizuje się automatycznie przez mechanizm @State-podobnych PropertyWrappers.

Skala skalowania Dynamic Type

Skala iOS obejmuje 11 rozmiarów: od .extraSmall (5 pt) do .accessibilityExtraExtraExtraLarge (77 pt dla .body). Współczynnik skalowania dla .body waha się od 0.85 (XS) do 1.71 (XXXL) względem wartości bazowej. @ScaledMetric używa właśnie tej skali, więc wartość 12 pt dla padding może stać się ~20 pt przy maksymalnym accessibility-rozmiarze.

Content Size CategoryWspółczynnik (body)Przykład @ScaledMetric(12)
extraSmall0.85~10 pt
small0.93~11 pt
medium (default)1.0012 pt
large1.07~13 pt
extraLarge1.15~14 pt
extraExtraLarge1.28~15 pt
accessibilityExtraLarge1.47~18 pt
accessibilityXXXL1.71~20 pt

Wybór relativeTo: używaj .body dla wartości powiązanych z głównym tekstem (padding, spacing w listach), .title dla dużych elementów (iconSize, imageSize), .caption dla małych elementów (rozmiar badge). Gwarantuje to, że elementy skalują się zgodnie z otaczającym tekstem.

@ScaledMetric i Dynamic Type

Dynamic Type — funkcja iOS, która pozwala użytkownikowi dostosować rozmiar czcionki systemowej w Settings → Display & Brightness → Text Size. Zmiana jest stosowana globalnie do wszystkich aplikacji. @ScaledMetric reaguje na tę zmianę automatycznie: SwiftUI aktualizuje wszystkie zmienne @ScaledMetric przy zmianie UIContentSizeCategory.

Ważne: @ScaledMetric skaluje tylko wartości liczbowe, ale nie zarządza bezpośrednio czcionkami. Do czcionek używaj .font() ze stylem tekstu (.body, .title, .headline) — SwiftUI automatycznie skaluje czcionkę. @ScaledMetric uzupełnia skalowanie czcionek dla padding, spacing i rozmiarów elementów.

Sprawdzanie accessibility przez Canvas

Canvas Preview obsługuje Dynamic Type: w panelu narzędzi Canvas znajduje się suwak Text Size (A–A) do sprawdzania UI przy różnych rozmiarach czcionki. Używaj go z @ScaledMetric, aby upewnić się, że odstępy i rozmiary skalują się prawidłowo.

swift
struct CardView: View {
    @ScaledMetric private var cornerRadius: CGFloat = 16
    @ScaledMetric private var spacing: CGFloat = 8
    
    var body: some View {
        VStack(spacing: spacing) {
            Text("Tytuł karty")
                .font(.headline)
            Text("Opis z obsługą dynamic type")
                .font(.body)
        }
        .padding(spacing * 2)
        .background(.regularMaterial)
        .cornerRadius(cornerRadius)
    }
}

struct CardView_Previews: PreviewProvider {
    static var previews: some View {
        CardView()
            .dynamicTypeSize(.large)
            .previewDisplayName("Large")
        CardView()
            .dynamicTypeSize(.accessibility5)
            .previewDisplayName("Accessibility 5")
    }
}

cornerRadius skaluje się od 16 pt do ~27 pt przy maksymalnym accessibility-rozmiarze. spacing — od 8 do ~14 pt. Gwarantuje to, że karta pozostaje wizualnie zbalansowana przy dowolnym rozmiarze czcionki.

Przykłady @ScaledMetric

Przykład: ikona z obsługą Dynamic Type. Rozmiar ikon Image(systemName:) domyślnie nie skaluje się pod Dynamic Type. @ScaledMetric rozwiązuje ten problem: zmieniając imageScale lub frame rozmiar na podstawie current scale factor.

swift
struct IconLabel: View {
    let title: String
    let icon: String
    
    @ScaledMetric private var iconDimension: CGFloat = 28
    @ScaledMetric(relativeTo: .body) private var spacing: CGFloat = 6
    
    var body: some View {
        HStack(spacing: spacing) {
            Image(systemName: icon)
                .resizable()
                .frame(width: iconDimension, height: iconDimension)
            Text(title)
                .font(.body)
        }
    }
}

Przykład: dostępny badge-komponent. Badge z liczbą powinien skalować się proporcjonalnie do tekstu. @ScaledMetric dla minimalnego rozmiaru badge gwarantuje, że okrągły badge pozostaje widoczny przy dużej czcionce.

swift
struct BadgeView: View {
    let count: Int
    
    @ScaledMetric(relativeTo: .caption) private var badgeSize: CGFloat = 20
    @ScaledMetric(relativeTo: .caption) private var fontScale: CGFloat = 1
    
    var body: some View {
        ZStack {
            Circle()
                .fill(.red)
                .frame(width: badgeSize, height: badgeSize)
            
            Text("\(count)")
                .font(.caption)
                .foregroundColor(.white)
                .scaleEffect(fontScale)
        }
        .fixedSize()
    }
}

fontScale dodatkowo skaluje zawartość Circle, aby dopasować się do zwiększonego badgeSize. Bez fontScale tekst wewnątrz badge mógłby się nie zmieścić przy dużej czcionce.

@ScaledMetric vs @State — różnice

@ScaledMetric i @State — oba property wrappers śledzące zmiany, ale z różnymi źródłami aktualizacji. @State aktualizuje wartość przy programowej zmianie (przez $stateBinding). @ScaledMetric aktualizuje wartość automatycznie przy zmianie systemowego Dynamic Type, ale nie pozwala na bezpośrednią zmianę wartości z kodu.

Kluczowa różnica: @ScaledMetric — read-only dla programisty i write-only dla systemu. Nie możesz zmienić scaledValue przez setter — jest obliczany przez SwiftUI na podstawie wartości bazowej i bieżącego Dynamic Type. @State natomiast jest w pełni zarządzany przez programistę. Jeśli potrzebujesz wartości, która zarówno skaluje się pod Dynamic Type, jak i zmienia programowo — łącz @ScaledMetric z @State lub użyj właściwości obliczanej.

Cecha@ScaledMetric@State
Źródło aktualizacjiDynamic Type (system)Programowo (programista)
Typ wartościCGFloat, Int, DoubleDowolny
Zmiana z koduNie możnaMożna przez binding
Ponowne rysowanie ViewPrzy zmianie Dynamic TypePrzy zmianie wartości
Wersja iOSiOS 14+iOS 13+

Połączony wzorzec: jeśli potrzebujesz zmieniać padding programowo (np. animacja naciśnięcia) i jednocześnie skalować pod Dynamic Type, utwórz @ScaledMetric dla bazowej scaled-wartości i @State dla mnożnika animacji. Wartość końcowa = scaledValue × animationMultiplier.

Typowe błędy z @ScaledMetric

Błąd 1: używanie @ScaledMetric dla czcionek. @ScaledMetric skaluje liczby, a nie czcionki. Do czcionek używaj .font(.body) — SwiftUI automatycznie stosuje Dynamic Type. Nigdy nie używaj @ScaledMetric z font(.system(size: scaledSize)) — to psuje systemowe accessibility.

Błąd 2: brak relativeTo dla różnorodnych elementów. Jeśli masz padding (powiązany z .body) i iconSize (powiązany z .title), określ poprawny relativeTo dla każdego. Bez relativeTo oba będą skalowane według .body, co da nieproporcjonalne zwiększenie ikony względem jej kontekstu tekstowego.

Błąd 3: @ScaledMetric w ViewModel/@ObservableObject. @ScaledMetric — property wrapper SwiftUI działający tylko wewnątrz View. Nie można go używać w ViewModel lub serwisach. Do skalowania w ViewModel przekazuj scaled-wartość z View jako parametr lub używaj @Environment(\.sizeCategory) w View.

Pobieranie bieżącej size category w kodzie

@Environment(\.sizeCategory) — alternatywny sposób pobierania bieżącego Dynamic Type w View. Używaj go, gdy potrzebujesz większej kontroli: obliczyć niestandardowy mnożnik, przekazać sizeCategory do ViewModel lub połączyć z @ScaledMetric dla elastycznego skalowania.

swift
struct CustomScaledView: View {
    @Environment(\.sizeCategory) private var sizeCategory
    @ScaledMetric private var basePadding: CGFloat = 12
    
    private var extraPadding: CGFloat {
        if sizeCategory >= .accessibilityLarge {
            return basePadding * 0.5
        }
        return 0
    }
    
    var body: some View {
        Text("Niestandardowa skalowana zawartość")
            .font(.body)
            .padding(basePadding + extraPadding)
    }
}

Dodatkowy odstęp extraPadding jest dodawany tylko przy accessibility-rozmiarach, dając więcej powietrza dla dużego tekstu bez zmiany podstawowej logiki @ScaledMetric.

Często zadawane pytania

Czym @ScaledMetric różni się od @ScaledFont?

@ScaledMetric — oficjalny property wrapper SwiftUI do skalowania liczb. @ScaledFont nie istnieje jako standardowe API — to niestandardowy wrapper zaimplementowany przez społeczność. Do czcionek zawsze używaj wbudowanego .font() ze stylami tekstu (.body, .title), a @ScaledMetric — dla padding, spacing i rozmiarów.

Czy @ScaledMetric działa na watchOS i tvOS?

@ScaledMetric jest dostępny na iOS 14+, watchOS 7+, tvOS 14+ i macOS 11+. Na watchOS Dynamic Type jest ograniczony do mniejszego zakresu — dostępne rozmiary od .extraSmall do .extraLarge bez accessibility-rozmiarów. Na tvOS Dynamic Type nie występuje — @ScaledMetric zawsze zwraca wartość bazową.

Czy można testować @ScaledMetric w unit-testach?

Tak, do testowania @ScaledMetric utwórz View z @ScaledMetric i przekaż environment-wartość .sizeCategory przez .environment(\.sizeCategory, .extraExtraLarge). Następnie pobierz rozmiar elementu przez GeometryReader lub SwiftUI Inspector. Alternatywnie sprawdzaj logikę skalowania przez UIFontMetrics w osobnym module.

Jak @ScaledMetric współdziała z .dynamicTypeSize?

.dynamicTypeSize — modyfikator View ograniczający maksymalny Dynamic Type dla hierarchii (np. .dynamicTypeSize(...large)). @ScaledMetric uwzględnia to ograniczenie: jeśli .dynamicTypeSize jest ustawiony, scaled-wartość nie przekroczy odpowiedniego rozmiaru. Łącz oba API dla precyzyjnej kontroli.

Co zrobić, jeśli @ScaledMetric nie aktualizuje UI?

Upewnij się, że View używa @ScaledMetric wewnątrz siebie (nie w ViewModel). Sprawdź, czy View subskrybuje Dynamic Type: @ScaledMetric automatycznie wywołuje odświeżenie body, ale jeśli View używa .equatable() lub .id(), mechanizm może się zepsuć. Użyj @Environment(\.sizeCategory) jako fallback.

Podsumowanie

  • @ScaledMetric — property wrapper SwiftUI do automatycznego skalowania liczb pod Dynamic Type.
  • Powiązanie — relativeTo wiąże skalę z konkretnym stylem tekstu (body, title, caption).
  • Accessibility — @ScaledMetric poprawia dostępność interfejsu bez ręcznego kodowania.
  • Tylko liczby — wrapper skaluje CGFloat, Int, Double, ale nie czcionki.
  • Zakres — od 0.85 (XS) do 1.71 (XXXL) względem wartości bazowej.
  • Read-only — @ScaledMetric nie można zmienić z kodu, tylko przez system.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również