@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 оновлюється.
  • Доступність — використання @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 на основі базового значення та поточного Dynamic Type. @State, навпаки, повністю керується розробником. Якщо вам потрібне значення, яке і масштабується під Dynamic Type, і змінюється програмно — комбінуйте @ScaledMetric з @State або використовуйте обчислювану властивість.

Характеристика@ScaledMetric@State
Джерело оновленняDynamic Type (система)Програмно (розробник)
Тип значенняCGFloat, Int, DoubleБудь-який
Зміна з кодуНе можнаМожна через binding
Перемальовування ViewПри зміні Dynamic TypeПри зміні значення
Версія iOSiOS 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), вкажіть правильний relativeTo для кожного. Без relativeTo обидва масштабуватимуться по .body, що дасть непропорційне збільшення іконки відносно її текстового контексту.

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

Отримання поточної 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).
  • Доступність — @ScaledMetric покращує доступність інтерфейсу без ручного коду.
  • Тільки числа — wrapper масштабує CGFloat, Int, Double, але не шрифти.
  • Діапазон — від 0.85 (XS) до 1.71 (XXXL) відносно базового значення.
  • Read-only — @ScaledMetric не можна змінити з коду, тільки через систему.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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