@ScaledMetric — ce este, property wrapper SwiftUI și Dynamic Type

Autor: IT Sectr Publicat: 2026-06-27 Timp de citire: 10 min

@ScaledMetric — property wrapper SwiftUI care scalează automat valoarea numerică conform setărilor Dynamic Type ale utilizatorului. Valoarea este înfășurată în @ScaledMetric și recalculată la modificarea dimensiunii sistemice a fontului, garantând accesibilitatea interfeței pentru persoanele cu deficiențe de vedere. Conform Apple Developer Documentation (2026), @ScaledMetric utilizează scala UIFontMetrics pentru a calcula scara relativă pe baza preferred content size category. Citiți mai multe despre accessibility în materialul despre accessibility SwiftUI.

Principalele puncte

  • @ScaledMetric — property wrapper SwiftUI pentru scalarea valorilor sub Dynamic Type.
  • Dynamic Type — setare sistemică iOS care modifică dimensiunea fontului din UIFontTextStyle.
  • Scalare — @ScaledMetric acceptă valoarea de bază și multiplicatorul relativeTo.
  • Actualizare automată — la modificarea Dynamic Type, @ScaledMetric se recalculează și UI se actualizează.
  • Accessibility — utilizarea @ScaledMetric îmbunătățește accesibilitatea interfeței fără cod suplimentar.

Ce este @ScaledMetric?

@ScaledMetric — property wrapper SwiftUI, adăugat în iOS 14, care scalează automat valoarea numerică (CGFloat, Int, Double) la dimensiunea actuală a fontului Dynamic Type. Spre deosebire de .font(.body) pentru fonturi, @ScaledMetric scalează orice parametru numeric: padding, spacing, cornerRadius, iconSize — tot ce ar trebui să crească proporțional la textul mare.

Sarcina principală a @ScaledMetric este de a asigura scalarea accessibility a elementelor non-text ale interfeței. Când utilizatorul mărește fontul în setările iOS, butoanele, pictogramele și spațierile trebuie să crească proporțional pentru ca interfața să rămână echilibrată. @ScaledMetric rezolvă această sarcină automat, fără calcularea manuală a multiplicatorilor.

Sintaxa @ScaledMetric

Sintaxa de bază @ScaledMetric utilizează valoarea implicită și parametrul opțional relativeTo. Dacă relativeTo este specificat, scalarea este legată de un stil textual specific (UIFontTextStyle). Dacă nu — se utilizează scala .body.

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

padding va fi scalat relativ la .body (implicit), iconSize — relativ la .title. La text mare, spațierile și pictograma vor crește proporțional. Fără @ScaledMetric, spațierile ar rămâne 12 pt la orice dimensiune a fontului, ceea ce ar duce la dezechilibru vizual.

Cum funcționează @ScaledMetric

Mecanismul @ScaledMetric se bazează pe UIFontMetrics din UIKit. Când SwiftUI creează o instanță @ScaledMetric, calculează multiplicatorul pe baza preferred content size category curente (UIContentSizeCategory). Valoarea de bază este înmulțită cu scaledValue din UIFontMetrics pentru stilul textual specificat.

Matematic: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo). Dacă relativeTo nu este specificat, se utilizează UIFontMetrics.default legat la .body. La modificarea Dynamic Type, SwiftUI recreate corpul View, @ScaledMetric calculează noua scaledValue și UI se actualizează automat prin mecanismul PropertyWrappers asemănător @State.

Scala de scalare Dynamic Type

Scala iOS include 11 dimensiuni: de la .extraSmall (5 pt) la .accessibilityExtraExtraExtraLarge (77 pt pentru .body). Coeficientul de scalare pentru .body variază de la 0.85 (XS) la 1.71 (XXXL) față de valoarea de bază. @ScaledMetric utilizează exact această scară, deci valoarea 12 pt pentru padding poate deveni ~20 pt la dimensiunea maximă de accessibility.

Content Size CategoryCoeficient (body)Exemplu @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

Alegerea relativeTo: utilizați .body pentru valori legate de textul principal (padding, spacing în liste), .title pentru elemente mari (iconSize, imageSize), .caption pentru elemente mici (dimensiune badge). Aceasta garantează că elementele se scalează în armonie cu textul înconjurător.

@ScaledMetric și Dynamic Type

Dynamic Type — funcție iOS care permite utilizatorului să ajusteze dimensiunea fontului sistemului în Settings → Display & Brightness → Text Size. Modificarea se aplică global tuturor aplicațiilor. @ScaledMetric reacționează automat la această modificare: SwiftUI actualizează toate variabilele @ScaledMetric la schimbarea UIContentSizeCategory.

Important: @ScaledMetric scalează doar valorile numerice, dar nu gestionează direct fonturile. Pentru fonturi utilizați .font() cu stil textual (.body, .title, .headline) — SwiftUI scalează automat fontul. @ScaledMetric completează scalarea fonturilor pentru padding, spacing și dimensiuni ale elementelor.

Verificarea accessibility prin Canvas

Canvas Preview suportă Dynamic Type: în panoul de instrumente Canvas există un glisor Text Size (A–A) pentru verificarea UI la diferite dimensiuni ale fontului. Utilizați-l cu @ScaledMetric pentru a vă asigura că spațierile și dimensiunile se scalează corect.

swift
struct CardView: View {
    @ScaledMetric private var cornerRadius: CGFloat = 16
    @ScaledMetric private var spacing: CGFloat = 8
    
    var body: some View {
        VStack(spacing: spacing) {
            Text("Titlu card")
                .font(.headline)
            Text("Descriere cu suport 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 se scalează de la 16 pt la ~27 pt la dimensiunea maximă de accessibility. spacing — de la 8 la ~14 pt. Aceasta garantează că cardul rămâne vizual echilibrat la orice dimensiune a fontului.

Exemple @ScaledMetric

Exemplu: pictogramă cu suport Dynamic Type. Dimensiunea pictogramelor Image(systemName:) în mod implicit nu se scalează sub Dynamic Type. @ScaledMetric rezolvă această problemă: modificând imageScale sau dimensiunea frame pe baza factorului de scară curent.

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

Exemplu: component badge accesibil. Badge cu număr trebuie să se scaleze proporțional cu textul. @ScaledMetric pentru dimensiunea minimă a badge-ului garantează că badge-ul circular rămâne vizibil la font mare.

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 scalează suplimentar conținutul Circle pentru a se potrivi cu badgeSize mărit. Fără fontScale, textul din interiorul badge-ului ar putea să nu încapă la font mare.

@ScaledMetric vs @State — diferențe

@ScaledMetric și @State — ambii property wrappers care urmăresc modificările, dar cu surse de actualizare diferite. @State actualizează valoarea la modificarea programatică (prin $stateBinding). @ScaledMetric actualizează valoarea automat la modificarea Dynamic Type de sistem, dar nu permite modificarea directă a valorii din cod.

Diferența cheie: @ScaledMetric — read-only pentru dezvoltator și write-only pentru sistem. Nu puteți modifica scaledValue prin setter — este calculat de SwiftUI pe baza valorii de bază și a Dynamic Type curent. @State, dimpotrivă, este complet gestionat de dezvoltator. Dacă aveți nevoie de o valoare care atât se scalează sub Dynamic Type, cât și se modifică programatic — combinați @ScaledMetric cu @State sau utilizați o proprietate calculată.

Caracteristică@ScaledMetric@State
Sursa de actualizareDynamic Type (sistem)Programatic (dezvoltator)
Tipul valoriiCGFloat, Int, DoubleOrice
Modificare din codNu se poateSe poate prin binding
Re-redarea ViewLa modificarea Dynamic TypeLa modificarea valorii
Versiune iOSiOS 14+iOS 13+

Model combinat: dacă trebuie să modificați padding programatic (de exemplu, animație la apăsare) și simultan să scalați sub Dynamic Type, creați un @ScaledMetric pentru valoarea de bază scalată și un @State pentru multiplicatorul de animație. Valoarea finală = scaledValue × animationMultiplier.

Greșeli tipice cu @ScaledMetric

Greșeala 1: utilizarea @ScaledMetric pentru fonturi. @ScaledMetric scalează numere, nu fonturi. Pentru fonturi utilizați .font(.body) — SwiftUI aplică automat Dynamic Type. Nu utilizați niciodată @ScaledMetric cu font(.system(size: scaledSize)) — aceasta strică accessibility-ul sistemului.

Greșeala 2: lipsa relativeTo pentru elemente eterogene. Dacă aveți padding (legat de .body) și iconSize (legat de .title), specificați relativeTo corect pentru fiecare. Fără relativeTo, ambele se vor scala după .body, ceea ce va duce la o creștere disproporționată a pictogramei față de contextul său textual.

Greșeala 3: @ScaledMetric în ViewModel/@ObservableObject. @ScaledMetric este un property wrapper SwiftUI care funcționează doar în interiorul View. Nu poate fi utilizat în ViewModel sau servicii. Pentru scalare în ViewModel, transmiteți valoarea scalată din View ca parametru sau utilizați @Environment(\.sizeCategory) în View.

Obținerea sizeCategory curente în cod

@Environment(\.sizeCategory) — metodă alternativă de obținere a Dynamic Type curent în View. Utilizați-o când aveți nevoie de mai mult control: calcularea unui multiplicator personalizat, transmiterea sizeCategory la ViewModel sau combinarea cu @ScaledMetric pentru scalare flexibilă.

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("Conținut scalat personalizat")
            .font(.body)
            .padding(basePadding + extraPadding)
    }
}

Spațiere suplimentară extraPadding se adaugă doar la dimensiunile de accessibility, oferind mai mult spațiu pentru textul mare fără a modifica logica de bază @ScaledMetric.

Întrebări frecvente

Cu ce se deosebește @ScaledMetric de @ScaledFont?

@ScaledMetric — property wrapper oficial SwiftUI pentru scalarea numerelor. @ScaledFont nu există ca API standard — este un wrapper personalizat implementat de comunitate. Pentru fonturi utilizați întotdeauna .font() încorporat cu stiluri textuale (.body, .title), iar @ScaledMetric — pentru padding, spacing și dimensiuni.

Funcționează @ScaledMetric pe watchOS și tvOS?

@ScaledMetric este disponibil pe iOS 14+, watchOS 7+, tvOS 14+ și macOS 11+. Pe watchOS, Dynamic Type este limitat la un interval mai mic — dimensiuni de la .extraSmall la .extraLarge fără dimensiuni de accessibility. Pe tvOS, Dynamic Type lipsește — @ScaledMetric returnează întotdeauna valoarea de bază.

Se poate testa @ScaledMetric în unit-teste?

Da, pentru testarea @ScaledMetric creați un View cu @ScaledMetric și transmiteți valoarea environment .sizeCategory prin .environment(\.sizeCategory, .extraExtraLarge). Apoi obțineți dimensiunea elementului prin GeometryReader sau SwiftUI Inspector. Alternativ, verificați logica de scalare prin UIFontMetrics într-un modul separat.

Cum interacționează @ScaledMetric cu .dynamicTypeSize?

.dynamicTypeSize — un modifier View care limitează Dynamic Type maxim pentru o ierarhie (de exemplu, .dynamicTypeSize(...large)). @ScaledMetric ține cont de această limitare: dacă .dynamicTypeSize este setată, valoarea scalată nu va depăși dimensiunea corespunzătoare. Combinați ambele API-uri pentru un control precis.

Ce faceți dacă @ScaledMetric nu actualizează UI?

Asigurați-vă că View utilizează @ScaledMetric în interiorul său (nu în ViewModel). Verificați că View se abonează la Dynamic Type: @ScaledMetric apelează automat reîmprospătarea body, dar dacă View utilizează .equatable() sau .id(), mecanismul se poate strica. Utilizați @Environment(\.sizeCategory) ca fallback.

Rezumat

  • @ScaledMetric — property wrapper SwiftUI pentru scalarea automată a numerelor sub Dynamic Type.
  • Legare — relativeTo leagă scara de un stil textual specific (body, title, caption).
  • Accessibility — @ScaledMetric îmbunătățește accesibilitatea interfeței fără cod manual.
  • Doar numere — wrapper-ul scalează CGFloat, Int, Double, dar nu fonturi.
  • Interval — de la 0.85 (XS) la 1.71 (XXXL) față de valoarea de bază.
  • Read-only — @ScaledMetric nu poate fi modificat din cod, doar prin sistem.

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și