@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. Подробнее об accessibility читайте в материале об accessibility 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 — обеспечить accessibility-масштабирование не-текстовых элементов интерфейса. Когда пользователь увеличивает шрифт в настройках 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("Submit", 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 обновляется автоматически через механизм @State-подобных PropertyWrappers.

Шкала масштабирования Dynamic Type

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

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 и размеров элементов.

Проверка accessibility через 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("Card Title")
                .font(.headline)
            Text("Description with dynamic type support")
                .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 при максимальном accessibility-размере. spacing — от 8 до ~14 pt. Это гарантирует, что карточка остаётся визуально сбалансированной при любом размере шрифта.

Примеры @ScaledMetric

Пример: иконка с поддержкой Dynamic Type. Размер иконок Image(systemName:) по умолчанию не масштабируется под Dynamic Type. @ScaledMetric решает эту проблему: изменяя imageScale или frame размер на основе 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)
        }
    }
}

Пример: доступный 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 wrappers, отслеживающие изменения, но с разными источниками обновлений. @State обновляет значение при программном изменении (через $stateBinding). @ScaledMetric обновляет значение автоматически при изменении системного Dynamic Type, но не позволяет изменять значение из кода напрямую.

Ключевое отличие: @ScaledMetric — read-only для разработчика и write-only для системы. Вы не можете изменить scaledValue через setter — он вычисляется SwiftUI на основе базового значения и current 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 для базового scaled-значения и @State для множителя анимации. Финальное значение = scaledValue × animationMultiplier.

Типичные ошибки с @ScaledMetric

Ошибка 1: использование @ScaledMetric для шрифтов. @ScaledMetric масштабирует числа, а не шрифты. Для шрифтов используйте .font(.body) — SwiftUI автоматически применяет Dynamic Type. Никогда не используйте @ScaledMetric с font(.system(size: scaledSize)) — это ломает системное accessibility.

Ошибка 2: отсутствие relativeTo для разнородных элементов. Если у вас padding (связан с .body) и iconSize (связан с .title), укажите correct relativeTo для каждого. Без relativeTo оба будут масштабироваться по .body, что даст непропорциональное увеличение иконки относительно её текстового контекста.

Ошибка 3: @ScaledMetric в ViewModel/@ObservableObject. @ScaledMetric — SwiftUI property wrapper, работающий только внутри View. Его нельзя использовать в ViewModel или сервисах. Для масштабирования в ViewModel передавайте scaled-значение из View как параметр или используйте @Environment(\.sizeCategory) в View.

Получение current size category в коде

@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("Custom scaled content")
            .font(.body)
            .padding(basePadding + extraPadding)
    }
}

Дополнительный отступ extraPadding добавляется только при accessibility-размерах, давая больше воздуха для крупного текста без изменения базовой @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 без accessibility-размеров. На 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 установлен, scaled-значение не превысит соответствующий размер. Комбинируйте оба API для точного контроля.

Что делать если @ScaledMetric не обновляет UI?

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

Итоги

  • @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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также