@ScaledMetric — какво е, property wrapper SwiftUI и Dynamic Type

Автор: IT Sectr Публикувано: 2026-06-27 Време за четене: 10 мин

@ScaledMetric — property wrapper SwiftUI, който автоматично мащабира числова стойност според настройките на Dynamic Type на потребителя. Стойността се обвива в @ScaledMetric и се преизчислява при промяна на системния размер на шрифта, което гарантира достъпност на интерфейса за хора с увредено зрение. Според Apple Developer Documentation (2026), @ScaledMetric използва скалата UIFontMetrics за изчисляване на относителния мащаб въз основа на preferred content size category. Научете повече за достъпността в материала за достъпност на SwiftUI.

Основни моменти

  • @ScaledMetric — property wrapper SwiftUI за мащабиране на стойности под Dynamic Type.
  • Dynamic Type — системна настройка на iOS, променяща размера на шрифта от UIFontTextStyle.
  • Мащабиране — @ScaledMetric приема базова стойност и множител relativeTo.
  • Автоматично обновяване — при промяна на Dynamic Type, @ScaledMetric се преизчислява и UI се обновява.
  • Accessibility — използването на @ScaledMetric подобрява достъпността на интерфейса без допълнителен код.

Какво е @ScaledMetric?

@ScaledMetric — property wrapper SwiftUI, добавен в iOS 14, който автоматично мащабира числова стойност (CGFloat, Int, Double) спрямо текущия размер на шрифта Dynamic Type. За разлика от .font(.body) за шрифтове, @ScaledMetric мащабира всякакви числови параметри: padding, spacing, cornerRadius, iconSize — всичко, което трябва да се увеличава пропорционално при голям текст.

Основната задача на @ScaledMetric е да осигури мащабиране за достъпност на нетекстови елементи на интерфейса. Когато потребителят увеличи шрифта в настройките на iOS, бутоните, иконите и разстоянията трябва да се увеличат пропорционално, за да остане интерфейсът балансиран. @ScaledMetric решава тази задача автоматично, без ръчно изчисляване на множители.

Синтаксис на @ScaledMetric

Основен синтаксис @ScaledMetric използва стойност по подразбиране и незадължителен параметър relativeTo. Ако relativeTo е зададен, мащабирането е обвързано с конкретен текстов стил (UIFontTextStyle). Ако не — се използва скалата .body.

swift
struct AccessibleButton: View {
    @ScaledMetric private var padding: CGFloat = 12
    @ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
    
    var body: some View {
        Label("Изпращане", systemImage: "checkmark.circle.fill")
            .font(.body)
            .padding(padding)
            .imageScale(.init(rawValue: iconSize / 24) ?? .medium)
    }
}

padding ще се мащабира спрямо .body (по подразбиране), iconSize — спрямо .title. При голям текст разстоянията и иконата ще се увеличат пропорционално. Без @ScaledMetric разстоянията биха останали 12 pt при произволен размер на шрифта, което би довело до визуален дисбаланс.

Как работи @ScaledMetric

Механизмът на @ScaledMetric се основава на UIFontMetrics от UIKit. Когато SwiftUI създава инстанция на @ScaledMetric, той изчислява множител въз основа на текущата preferred content size category (UIContentSizeCategory). Базовата стойност се умножава по scaledValue от UIFontMetrics за зададения текстов стил.

Математически: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo). Ако relativeTo не е зададен, се използва UIFontMetrics.default, обвързан с .body. При промяна на Dynamic Type, SwiftUI пресъздава тялото на View, @ScaledMetric изчислява нов scaledValue и UI се обновява автоматично чрез механизма на PropertyWrappers, подобни на @State.

Скала на мащабиране на Dynamic Type

iOS скала включва 11 размера: от .extraSmall (5 pt) до .accessibilityExtraExtraExtraLarge (77 pt за .body). Коефициентът на мащабиране за .body варира от 0.85 (XS) до 1.71 (XXXL) спрямо базовата стойност. @ScaledMetric използва именно тази скала, така че стойност 12 pt за padding може да стане ~20 pt при максимален размер за достъпност.

Content Size CategoryКоефициент (body)Пример @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

Избор на relativeTo: използвайте .body за стойности, свързани с основния текст (padding, spacing в списъци), .title за големи елементи (iconSize, imageSize), .caption за малки елементи (размер на badge). Това гарантира, че елементите се мащабират в съответствие с околния текст.

@ScaledMetric и Dynamic Type

Dynamic Type — функция на iOS, която позволява на потребителя да настрои размера на системния шрифт в Settings → Display & Brightness → Text Size. Промяната се прилага глобално за всички приложения. @ScaledMetric реагира автоматично на тази промяна: SwiftUI обновява всички @ScaledMetric променливи при промяна на UIContentSizeCategory.

Важно: @ScaledMetric мащабира само числови стойности, но не управлява директно шрифтовете. За шрифтове използвайте .font() с текстов стил (.body, .title, .headline) — SwiftUI автоматично мащабира шрифта. @ScaledMetric допълва мащабирането на шрифтове за padding, spacing и размери на елементи.

Проверка на достъпност чрез Canvas

Canvas Preview поддържа Dynamic Type: в лентата с инструменти на Canvas има плъзгач Text Size (A–A) за проверка на UI при различни размери на шрифта. Използвайте го с @ScaledMetric, за да се уверите, че разстоянията и размерите се мащабират правилно.

swift
struct CardView: View {
    @ScaledMetric private var cornerRadius: CGFloat = 16
    @ScaledMetric private var spacing: CGFloat = 8
    
    var body: some View {
        VStack(spacing: spacing) {
            Text("Заглавие на карта")
                .font(.headline)
            Text("Описание с поддръжка на 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 се мащабира от 16 pt до ~27 pt при максимален размер за достъпност. spacing — от 8 до ~14 pt. Това гарантира, че картата остава визуално балансирана при произволен размер на шрифта.

Примери за @ScaledMetric

Пример: икона с поддръжка на Dynamic Type. Размерът на иконите Image(systemName:) по подразбиране не се мащабира под Dynamic Type. @ScaledMetric решава този проблем: чрез промяна на imageScale или размер на frame въз основа на текущия коефициент на мащабиране.

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

Пример: достъпен badge-компонент. Badge с число трябва да се мащабира пропорционално на текста. @ScaledMetric за минималния размер на badge гарантира, че кръглият badge остава видим при голям шрифт.

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 допълнително мащабира съдържанието на Circle, за да съответства на увеличения badgeSize. Без fontScale текстът вътре в badge може да не се побере при голям шрифт.

@ScaledMetric vs @State — разлики

@ScaledMetric и @State — и двата property wrapper-а, които проследяват промени, но с различни източници на обновяване. @State обновява стойността при програмна промяна (чрез $stateBinding). @ScaledMetric обновява стойността автоматично при промяна на системния Dynamic Type, но не позволява директна промяна на стойността от код.

Ключова разлика: @ScaledMetric — read-only за разработчика и write-only за системата. Не можете да промените scaledValue чрез setter — той се изчислява от SwiftUI на базата на базовата стойност и текущия Dynamic Type. @State, от друга страна, се управлява изцяло от разработчика. Ако имате нужда от стойност, която както се мащабира под Dynamic Type, така и се променя програмно — комбинирайте @ScaledMetric с @State или използвайте изчисляемо свойство.

Характеристика@ScaledMetric@State
Източник на обновяванеDynamic Type (система)Програмно (разработчик)
Тип стойностCGFloat, Int, DoubleПроизволен
Промяна от кодНе можеМоже чрез binding
Прерисуване на ViewПри промяна на Dynamic TypeПри промяна на стойност
iOS версияiOS 14+iOS 13+

Комбиниран модел: ако трябва да променяте padding програмно (напр. анимация при натискане) и едновременно да мащабирате под Dynamic Type, създайте @ScaledMetric за базовата мащабирана стойност и @State за множителя на анимацията. Крайната стойност = scaledValue × animationMultiplier.

Типични грешки с @ScaledMetric

Грешка 1: използване на @ScaledMetric за шрифтове. @ScaledMetric мащабира числа, а не шрифтове. За шрифтове използвайте .font(.body) — SwiftUI автоматично прилага Dynamic Type. Никога не използвайте @ScaledMetric с font(.system(size: scaledSize)) — това разрушава системната достъпност.

Грешка 2: липса на relativeTo за разнородни елементи. Ако имате padding (свързан с .body) и iconSize (свързан с .title), посочете правилния relativeTo за всеки. Без relativeTo и двата ще се мащабират според .body, което ще доведе до непропорционално увеличаване на иконата спрямо текстовия ѝ контекст.

Грешка 3: @ScaledMetric в ViewModel/@ObservableObject. @ScaledMetric е SwiftUI property wrapper, който работи само вътре в View. Не може да се използва в ViewModel или услуги. За мащабиране в ViewModel предайте мащабираната стойност от View като параметър или използвайте @Environment(\.sizeCategory) в View.

Получаване на текущата sizeCategory в код

@Environment(\.sizeCategory) — алтернативен начин за получаване на текущия Dynamic Type в View. Използвайте го, когато имате нужда от повече контрол: изчисляване на персонализиран множител, предаване на sizeCategory на ViewModel или комбиниране с @ScaledMetric за гъвкаво мащабиране.

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("Персонализирано мащабирано съдържание")
            .font(.body)
            .padding(basePadding + extraPadding)
    }
}

Допълнително разстояние extraPadding се добавя само при размери за достъпност, давайки повече пространство за голям текст без промяна на основната @ScaledMetric логика.

Често задавани въпроси

С какво @ScaledMetric се различава от @ScaledFont?

@ScaledMetric — официален property wrapper SwiftUI за мащабиране на числа. @ScaledFont не съществува като стандартен API — това е персонализиран wrapper, реализиран от общността. За шрифтове винаги използвайте вградения .font() с текстови стилове (.body, .title), а @ScaledMetric — за padding, spacing и размери.

Работи ли @ScaledMetric на watchOS и tvOS?

@ScaledMetric е достъпен на iOS 14+, watchOS 7+, tvOS 14+ и macOS 11+. На watchOS Dynamic Type е ограничен до по-малък диапазон — размери от .extraSmall до .extraLarge без размери за достъпност. На tvOS Dynamic Type липсва — @ScaledMetric винаги връща базовата стойност.

Може ли @ScaledMetric да се тества в unit-тестове?

Да, за тестване на @ScaledMetric създайте View с @ScaledMetric и предайте environment стойността .sizeCategory чрез .environment(\.sizeCategory, .extraExtraLarge). След това вземете размера на елемента чрез GeometryReader или SwiftUI Inspector. Алтернативно, проверете логиката на мащабиране чрез UIFontMetrics в отделен модул.

Как @ScaledMetric взаимодейства с .dynamicTypeSize?

.dynamicTypeSize — модификатор на View, който ограничава максималния Dynamic Type за йерархия (напр. .dynamicTypeSize(...large)). @ScaledMetric взема предвид това ограничение: ако .dynamicTypeSize е зададен, мащабираната стойност няма да надвиши съответния размер. Комбинирайте двата API за прецизен контрол.

Какво да направите, ако @ScaledMetric не обновява UI?

Уверете се, че View използва @ScaledMetric вътре в себе си (не в ViewModel). Проверете дали View се абонира за Dynamic Type: @ScaledMetric автоматично задейства обновяване на body, но ако View използва .equatable() или .id(), механизмът може да се счупи. Използвайте @Environment(\.sizeCategory) като резервен вариант.

Резюме

  • @ScaledMetric — property wrapper SwiftUI за автоматично мащабиране на числа под Dynamic Type.
  • Обвързване — relativeTo обвързва мащаба с конкретен текстов стил (body, title, caption).
  • Accessibility — @ScaledMetric подобрява достъпността на интерфейса без ръчно кодиране.
  • Само числа — wrapper-ът мащабира CGFloat, Int, Double, но не и шрифтове.
  • Диапазон — от 0.85 (XS) до 1.71 (XXXL) спрямо базовата стойност.
  • Read-only — @ScaledMetric не може да се променя от код, само чрез системата.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също