Skip to content

Instantly share code, notes, and snippets.

@apptekstudios
Created June 28, 2020 10:03

Revisions

  1. apptekstudios created this gist Jun 28, 2020.
    161 changes: 161 additions & 0 deletions ScaledMetricOniOS13.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,161 @@
    import SwiftUI

    struct AccessibleView: View {
    @ScaledMetricCustom(relativeTo: .title) var someSize: CGFloat = 100
    @ScaledFont(customFontName: "TimesNewRomanPS-BoldMT", size: 18, relativeTo: .body) var bodyFont

    var body: some View {
    VStack {
    Rectangle()
    .frame(width: someSize, height: someSize)
    Text("Hello world I am dynamically scaled!")
    .fixedSize(horizontal: false, vertical: true)
    .font(bodyFont)
    Spacer()
    }
    }
    }

    struct AccessibleView_Previews: PreviewProvider {
    static var previews: some View {
    NavigationView {
    ScrollView {
    VStack {
    AccessibleView()
    AccessibleView()
    .environment(\.sizeCategory, .extraExtraLarge)
    AccessibleView()
    .environment(\.sizeCategory, .accessibilityExtraLarge)
    AccessibleView()
    .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
    }
    }
    .navigationBarTitle("Demo")
    }
    }
    }


    @propertyWrapper
    struct ScaledMetricCustom<Value>: DynamicProperty where Value: BinaryFloatingPoint {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.verticalSizeClass) var verticalSizeClass
    @Environment(\.sizeCategory) var contentSize

    // Creates the scaled metric with an unscaled value and a text style to scale relative to.
    init(wrappedValue: Value, maxValue: Value? = nil, relativeTo textStyle: Font.TextStyle = .body) {
    self.textStyle = textStyle.uiKit
    self.baseValue = wrappedValue
    self.maxValue = maxValue
    }

    let textStyle: UIFont.TextStyle
    let baseValue: Value
    let maxValue: Value?

    var traitCollection: UITraitCollection {
    UITraitCollection(traitsFrom: [
    UITraitCollection(horizontalSizeClass: horizontalSizeClass?.uiKit ?? .unspecified),
    UITraitCollection(verticalSizeClass: verticalSizeClass?.uiKit ?? .unspecified),
    UITraitCollection(preferredContentSizeCategory: contentSize.uiKit)
    ])
    }

    // The value scaled based on the current environment.
    var wrappedValue: Value {
    let scaled = Value(UIFontMetrics(forTextStyle: textStyle).scaledValue(for: CGFloat(baseValue), compatibleWith: traitCollection))
    return maxValue.map { min($0, scaled) } ?? scaled
    }
    }

    @propertyWrapper
    struct ScaledFont: DynamicProperty {
    @ScaledMetricCustom var fontSize: CGFloat
    private var fontDefinition: FontDefinition
    private var maxSize: CGFloat?

    init(systemFontOfSize fontSize: CGFloat, weight: Font.Weight, design: Font.Design, maxSize: CGFloat? = nil, relativeTo textStyle: Font.TextStyle = .body) {
    fontDefinition = .system(weight: weight, design: design)
    self.maxSize = maxSize
    self._fontSize = ScaledMetricCustom(wrappedValue: fontSize, relativeTo: textStyle)
    }

    init(customFontName name: String, size: CGFloat, maxSize: CGFloat? = nil, relativeTo textStyle: Font.TextStyle = .body) {
    fontDefinition = .custom(name: name)
    self.maxSize = maxSize
    self._fontSize = ScaledMetricCustom(wrappedValue: size, relativeTo: textStyle)
    }

    private enum FontDefinition {
    case system(weight: Font.Weight, design: Font.Design)
    case custom(name: String)
    }

    var wrappedValue: Font {
    switch fontDefinition {
    case let .custom(name):
    if #available(iOS 14.0, *) {
    return Font.custom(name, fixedSize: fontSize) // This is actually using the scaled value (so we pass it to fixed size)!
    } else {
    return Font.custom(name, size: fontSize)
    }
    case let .system(weight, design):
    return Font.system(size: fontSize, weight: weight, design: design)
    }

    }
    }


    extension UserInterfaceSizeClass {
    var uiKit: UIUserInterfaceSizeClass {
    switch self {
    case .compact: return .compact
    case .regular: return .regular
    @unknown default: return .unspecified
    }
    }
    }

    extension ContentSizeCategory {
    var uiKit: UIContentSizeCategory {
    switch self {
    case .accessibilityExtraExtraExtraLarge: return .accessibilityExtraExtraExtraLarge
    case .accessibilityExtraExtraLarge: return .accessibilityExtraExtraLarge
    case .accessibilityExtraLarge: return .accessibilityExtraLarge
    case .accessibilityLarge: return .accessibilityLarge
    case .accessibilityMedium: return .accessibilityMedium
    case .extraExtraExtraLarge: return .extraExtraExtraLarge
    case .extraExtraLarge: return .extraExtraLarge
    case .extraLarge: return .extraLarge
    case .extraSmall: return .extraSmall
    case .large: return .large
    case .medium: return .medium
    case .small: return .small
    @unknown default: return .unspecified
    }
    }
    }

    extension Font.TextStyle {
    var uiKit: UIFont.TextStyle {
    switch self {
    case .body: return .body
    case .callout: return .callout
    case .caption: return .caption1
    case .caption2: return .caption2
    case .footnote: return .footnote
    case .headline: return .headline
    case .largeTitle: return .largeTitle
    case .subheadline: return .subheadline
    case .title: return .title1
    case .title2: return .title2
    case .title3: return .title3
    @unknown default: return .body
    }
    }
    }


    // import PlaygroundSupport
    // PlaygroundSupport.PlaygroundPage.current.setLiveView(AccessibleView_Previews.previews)