├── .gitignore ├── Package.swift ├── Sources └── DynamicType │ ├── DynamicType.swift │ ├── DynamicTypeSlider.swift │ ├── SwiftUI.swift │ └── Values.swift └── Tests └── DynamicTypeTests └── DynamicTypeTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DynamicType", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15), 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "DynamicType", 16 | targets: ["DynamicType"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "DynamicType"), 23 | .testTarget( 24 | name: "DynamicTypeTests", 25 | dependencies: ["DynamicType"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/DynamicType/DynamicType.swift: -------------------------------------------------------------------------------- 1 | public enum DynamicTypeStyle: String, Hashable, CaseIterable, Codable { 2 | case largeTitle = "Large Title" 3 | case title1 = "Title 1" 4 | case title2 = "Title 2" 5 | case title3 = "Title 3" 6 | case headline = "Headline" 7 | case body = "Body" 8 | case callout = "Callout" 9 | case subhead = "Subhead" 10 | case footnote = "Footnote" 11 | case caption1 = "Caption 1" 12 | case caption2 = "Caption 2" 13 | static public let caption = Self.caption1 14 | } 15 | 16 | public enum DynamicTypeSize: String, Hashable, CaseIterable, Codable { 17 | case extraSmall = "Extra Small" 18 | case small = "Small" 19 | case medium = "Medium" 20 | case large = "Large" 21 | case extraLarge = "Extra Large" 22 | case extraExtraLarge = "Extra Extra Large" 23 | case extraExtraExtraLarge = "Extra Extra Extra Large" 24 | case accessibilityMedium = "Accessibility Medium" 25 | case accessibilityLarge = "Accessibility Large" 26 | case accessibilityExtraLarge = "Accessibility Extra Large" 27 | case accessibilityExtraExtraLarge = "Accessibility Extra Extra Large" 28 | case accessibilityExtraExtraExtraLarge = "Accessibility Extra Extra Extra Large" 29 | } 30 | 31 | extension DynamicType.DynamicTypeSize { 32 | mutating public func smaller() { 33 | let i = Self.allCases.firstIndex(of: self)! 34 | guard i > 0 else { return } 35 | self = Self.allCases[i-1] 36 | } 37 | 38 | mutating public func larger() { 39 | let i = Self.allCases.firstIndex(of: self)! 40 | guard i + 1 < Self.allCases.count else { return } 41 | self = Self.allCases[i+1] 42 | } 43 | } 44 | 45 | 46 | extension DynamicTypeStyle { 47 | public func value(in instance: DynamicTypeInstance) -> SizeAndLeading { 48 | switch self { 49 | case .largeTitle: 50 | instance.largeTitle 51 | case .title1: 52 | instance.title1 53 | case .title2: 54 | instance.title2 55 | case .title3: 56 | instance.title3 57 | case .headline: 58 | instance.headline 59 | case .body: 60 | instance.body 61 | case .callout: 62 | instance.callout 63 | case .subhead: 64 | instance.subhead 65 | case .footnote: 66 | instance.footnote 67 | case .caption1: 68 | instance.caption1 69 | case .caption2: 70 | instance.caption2 71 | } 72 | } 73 | 74 | public func resolve(for style: DynamicTypeSize) -> SizeAndLeading { 75 | value(in: style.instance) 76 | } 77 | } 78 | 79 | public enum Weight: String, Hashable, CaseIterable, Codable { 80 | case regular = "Regular" 81 | case semibold = "Semibold" 82 | case bold = "Bold" 83 | } 84 | 85 | public struct SizeAndLeading: Hashable, Codable { 86 | public var weight: Weight 87 | public var size: Double 88 | public var leading: Double 89 | } 90 | 91 | public struct DynamicTypeInstance: Hashable, Codable { 92 | public var largeTitle: SizeAndLeading 93 | public var title1: SizeAndLeading 94 | public var title2: SizeAndLeading 95 | public var title3: SizeAndLeading 96 | public var headline: SizeAndLeading 97 | public var body: SizeAndLeading 98 | public var callout: SizeAndLeading 99 | public var subhead: SizeAndLeading 100 | public var footnote: SizeAndLeading 101 | public var caption1: SizeAndLeading 102 | public var caption2: SizeAndLeading 103 | } 104 | 105 | 106 | extension DynamicTypeSize { 107 | public var instance: DynamicTypeInstance { 108 | switch self { 109 | case .extraSmall: 110 | .extraSmall 111 | case .small: 112 | .small 113 | case .medium: 114 | .medium 115 | case .large: 116 | .large 117 | case .extraLarge: 118 | .extraLarge 119 | case .extraExtraLarge: 120 | .extraExtraLarge 121 | case .extraExtraExtraLarge: 122 | .extraExtraExtraLarge 123 | case .accessibilityMedium: 124 | .accessibilityMedium 125 | case .accessibilityLarge: 126 | .accessibilityLarge 127 | case .accessibilityExtraLarge: 128 | .accessibilityExtraLarge 129 | case .accessibilityExtraExtraLarge: 130 | .accessibilityExtraExtraLarge 131 | case .accessibilityExtraExtraExtraLarge: 132 | .accessibilityExtraExtraExtraLarge 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/DynamicType/DynamicTypeSlider.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension DynamicType.DynamicTypeSize { 4 | fileprivate var value: Double { 5 | get { 6 | Double(DynamicTypeSize.allCases.firstIndex(of: self)!) 7 | } 8 | set { 9 | self = DynamicTypeSize.allCases[.init(newValue)] 10 | } 11 | } 12 | } 13 | 14 | @available(macOS 12.0, *) 15 | public struct DynamicTypeSlider: View { 16 | @Binding var dynamicTypeSize: DynamicTypeSize 17 | 18 | public init(dynamicTypeSize: Binding) { 19 | self._dynamicTypeSize = dynamicTypeSize 20 | } 21 | 22 | public var body: some View { 23 | let steps = DynamicTypeSize.allCases.count 24 | Slider(value: $dynamicTypeSize.value, in: 0...Double(steps-1), step: 1) { 25 | Image(systemName: "textformat.size") 26 | .foregroundStyle(.secondary) 27 | } 28 | .frame(maxWidth: 300) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DynamicType/SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | struct DynamicTypeKey: EnvironmentKey { 7 | static var defaultValue: DynamicTypeSize? 8 | } 9 | 10 | extension EnvironmentValues { 11 | public var myDynamicTypeSize: DynamicTypeSize? { 12 | get { self[DynamicTypeKey.self] } 13 | set { self[DynamicTypeKey.self] = newValue } 14 | } 15 | } 16 | 17 | extension Weight { 18 | var weight: Font.Weight { 19 | switch self { 20 | case .regular: 21 | .regular 22 | case .semibold: 23 | .semibold 24 | case .bold: 25 | .bold 26 | } 27 | } 28 | } 29 | 30 | struct MyFontModifier: ViewModifier { 31 | var font: DynamicTypeStyle 32 | @Environment(\.myDynamicTypeSize) private var dynamicTypeSize 33 | func body(content: Content) -> some View { 34 | let metrics = font.resolve(for: dynamicTypeSize ?? .large) 35 | content 36 | .font(.system(size: metrics.size, weight: metrics.weight.weight)) 37 | } 38 | } 39 | 40 | extension View { 41 | public func myFont(_ font: DynamicTypeStyle) -> some View { 42 | modifier(MyFontModifier(font: font)) 43 | } 44 | 45 | public func dynamicTypeSize(size: DynamicTypeSize?) -> some View { 46 | environment(\.myDynamicTypeSize, size) 47 | } 48 | } 49 | 50 | @propertyWrapper public struct MyScaledMetric : DynamicProperty where Value : BinaryFloatingPoint { 51 | @Environment(\.myDynamicTypeSize) private var size 52 | var source: Value 53 | var textStyle: DynamicTypeStyle = .body 54 | 55 | public init(wrappedValue: Value, relativeTo textStyle: DynamicTypeStyle) { 56 | self.source = wrappedValue 57 | self.textStyle = textStyle 58 | } 59 | 60 | /// Creates the scaled metric with an unscaled value using the default 61 | /// scaling. 62 | public init(wrappedValue: Value) { 63 | self.source = wrappedValue 64 | } 65 | 66 | /// The value scaled based on the current environment. 67 | public var wrappedValue: Value { 68 | let multiplier = textStyle.resolve(for: size ?? .large).size / textStyle.resolve(for: .large).size 69 | return source * .init(multiplier) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/DynamicType/Values.swift: -------------------------------------------------------------------------------- 1 | extension DynamicTypeInstance { 2 | static let extraSmall: DynamicTypeInstance = DynamicTypeInstance( 3 | largeTitle: SizeAndLeading(weight: .regular, size: 31, leading: 38), 4 | title1: SizeAndLeading(weight: .regular, size: 25, leading: 31), 5 | title2: SizeAndLeading(weight: .regular, size: 19, leading: 24), 6 | title3: SizeAndLeading(weight: .regular, size: 17, leading: 22), 7 | headline: SizeAndLeading(weight: .semibold, size: 14, leading: 19), 8 | body: SizeAndLeading(weight: .regular, size: 14, leading: 19), 9 | callout: SizeAndLeading(weight: .regular, size: 13, leading: 18), 10 | subhead: SizeAndLeading(weight: .regular, size: 12, leading: 16), 11 | footnote: SizeAndLeading(weight: .regular, size: 12, leading: 16), 12 | caption1: SizeAndLeading(weight: .regular, size: 11, leading: 13), 13 | caption2: SizeAndLeading(weight: .regular, size: 11, leading: 13) 14 | ) 15 | 16 | static let small = DynamicTypeInstance( 17 | largeTitle: .init(weight: .regular, size: 32, leading: 39), 18 | title1: .init(weight: .regular, size: 26, leading: 32), 19 | title2: .init(weight: .regular, size: 20, leading: 25), 20 | title3: .init(weight: .regular, size: 18, leading: 23), 21 | headline: .init(weight: .semibold, size: 15, leading: 20), 22 | body: .init(weight: .regular, size: 15, leading: 20), 23 | callout: .init(weight: .regular, size: 14, leading: 19), 24 | subhead: .init(weight: .regular, size: 13, leading: 18), 25 | footnote: .init(weight: .regular, size: 12, leading: 16), 26 | caption1: .init(weight: .regular, size: 11, leading: 13), 27 | caption2: .init(weight: .regular, size: 11, leading: 13) 28 | ) 29 | 30 | static let medium: DynamicTypeInstance = DynamicTypeInstance( 31 | largeTitle: .init(weight: .regular, size: 33, leading: 40), 32 | title1: .init(weight: .regular, size: 27, leading: 33), 33 | title2: .init(weight: .regular, size: 21, leading: 26), 34 | title3: .init(weight: .regular, size: 19, leading: 24), 35 | headline: .init(weight: .semibold, size: 16, leading: 21), 36 | body: .init(weight: .regular, size: 16, leading: 21), 37 | callout: .init(weight: .regular, size: 15, leading: 20), 38 | subhead: .init(weight: .regular, size: 14, leading: 19), 39 | footnote: .init(weight: .regular, size: 12, leading: 16), 40 | caption1: .init(weight: .regular, size: 11, leading: 13), 41 | caption2: .init(weight: .regular, size: 11, leading: 13) 42 | ) 43 | 44 | static let large = DynamicTypeInstance( 45 | largeTitle: .init(weight: .regular, size: 34, leading: 41), 46 | title1: .init(weight: .regular, size: 28, leading: 34), 47 | title2: .init(weight: .regular, size: 22, leading: 28), 48 | title3: .init(weight: .regular, size: 20, leading: 25), 49 | headline: .init(weight: .semibold, size: 17, leading: 22), 50 | body: .init(weight: .regular, size: 17, leading: 22), 51 | callout: .init(weight: .regular, size: 16, leading: 21), 52 | subhead: .init(weight: .regular, size: 15, leading: 20), 53 | footnote: .init(weight: .regular, size: 13, leading: 18), 54 | caption1: .init(weight: .regular, size: 12, leading: 16), 55 | caption2: .init(weight: .regular, size: 11, leading: 13) 56 | ) 57 | 58 | static let extraLarge = DynamicTypeInstance( 59 | largeTitle: .init(weight: .regular, size: 36, leading: 43), 60 | title1: .init(weight: .regular, size: 30, leading: 37), 61 | title2: .init(weight: .regular, size: 24, leading: 30), 62 | title3: .init(weight: .regular, size: 22, leading: 28), 63 | headline: .init(weight: .semibold, size: 19, leading: 24), 64 | body: .init(weight: .regular, size: 19, leading: 24), 65 | callout: .init(weight: .regular, size: 18, leading: 23), 66 | subhead: .init(weight: .regular, size: 17, leading: 22), 67 | footnote: .init(weight: .regular, size: 15, leading: 20), 68 | caption1: .init(weight: .regular, size: 14, leading: 19), 69 | caption2: .init(weight: .regular, size: 13, leading: 18) 70 | ) 71 | 72 | static let extraExtraLarge = DynamicTypeInstance( 73 | largeTitle: .init(weight: .regular, size: 38, leading: 46), 74 | title1: .init(weight: .regular, size: 32, leading: 39), 75 | title2: .init(weight: .regular, size: 26, leading: 32), 76 | title3: .init(weight: .regular, size: 24, leading: 30), 77 | headline: .init(weight: .semibold, size: 21, leading: 26), 78 | body: .init(weight: .regular, size: 21, leading: 26), 79 | callout: .init(weight: .regular, size: 20, leading: 25), 80 | subhead: .init(weight: .regular, size: 19, leading: 24), 81 | footnote: .init(weight: .regular, size: 17, leading: 22), 82 | caption1: .init(weight: .regular, size: 16, leading: 21), 83 | caption2: .init(weight: .regular, size: 15, leading: 20) 84 | ) 85 | 86 | static let extraExtraExtraLarge = DynamicTypeInstance( 87 | largeTitle: .init(weight: .regular, size: 40, leading: 48), 88 | title1: .init(weight: .regular, size: 34, leading: 41), 89 | title2: .init(weight: .regular, size: 28, leading: 34), 90 | title3: .init(weight: .regular, size: 26, leading: 32), 91 | headline: .init(weight: .semibold, size: 23, leading: 29), 92 | body: .init(weight: .regular, size: 23, leading: 29), 93 | callout: .init(weight: .regular, size: 22, leading: 28), 94 | subhead: .init(weight: .regular, size: 21, leading: 28), 95 | footnote: .init(weight: .regular, size: 19, leading: 24), 96 | caption1: .init(weight: .regular, size: 18, leading: 23), 97 | caption2: .init(weight: .regular, size: 17, leading: 22) 98 | ) 99 | 100 | static let accessibilityMedium = DynamicTypeInstance( 101 | largeTitle: .init(weight: .regular, size: 44, leading: 52), 102 | title1: .init(weight: .regular, size: 38, leading: 46), 103 | title2: .init(weight: .regular, size: 34, leading: 41), 104 | title3: .init(weight: .regular, size: 31, leading: 38), 105 | headline: .init(weight: .semibold, size: 28, leading: 34), 106 | body: .init(weight: .regular, size: 28, leading: 34), 107 | callout: .init(weight: .regular, size: 26, leading: 32), 108 | subhead: .init(weight: .regular, size: 25, leading: 31), 109 | footnote: .init(weight: .regular, size: 23, leading: 29), 110 | caption1: .init(weight: .regular, size: 22, leading: 28), 111 | caption2: .init(weight: .regular, size: 20, leading: 25) 112 | ) 113 | 114 | static let accessibilityLarge = DynamicTypeInstance( 115 | largeTitle: .init(weight: .regular, size: 48, leading: 57), 116 | title1: .init(weight: .regular, size: 43, leading: 51), 117 | title2: .init(weight: .regular, size: 39, leading: 47), 118 | title3: .init(weight: .regular, size: 37, leading: 44), 119 | headline: .init(weight: .semibold, size: 33, leading: 40), 120 | body: .init(weight: .regular, size: 33, leading: 40), 121 | callout: .init(weight: .regular, size: 32, leading: 39), 122 | subhead: .init(weight: .regular, size: 30, leading: 37), 123 | footnote: .init(weight: .regular, size: 27, leading: 33), 124 | caption1: .init(weight: .regular, size: 26, leading: 32), 125 | caption2: .init(weight: .regular, size: 24, leading: 30) 126 | ) 127 | 128 | static let accessibilityExtraLarge = DynamicTypeInstance( 129 | largeTitle: .init(weight: .regular, size: 52, leading: 61), 130 | title1: .init(weight: .regular, size: 48, leading: 57), 131 | title2: .init(weight: .regular, size: 44, leading: 52), 132 | title3: .init(weight: .regular, size: 43, leading: 51), 133 | headline: .init(weight: .semibold, size: 40, leading: 48), 134 | body: .init(weight: .regular, size: 40, leading: 48), 135 | callout: .init(weight: .regular, size: 38, leading: 46), 136 | subhead: .init(weight: .regular, size: 36, leading: 43), 137 | footnote: .init(weight: .regular, size: 33, leading: 40), 138 | caption1: .init(weight: .regular, size: 32, leading: 39), 139 | caption2: .init(weight: .regular, size: 29, leading: 35) 140 | ) 141 | 142 | static let accessibilityExtraExtraLarge = DynamicTypeInstance( 143 | largeTitle: .init(weight: .regular, size: 56, leading: 66), 144 | title1: .init(weight: .regular, size: 53, leading: 62), 145 | title2: .init(weight: .regular, size: 50, leading: 59), 146 | title3: .init(weight: .regular, size: 49, leading: 58), 147 | headline: .init(weight: .semibold, size: 47, leading: 56), 148 | body: .init(weight: .regular, size: 47, leading: 56), 149 | callout: .init(weight: .regular, size: 44, leading: 52), 150 | subhead: .init(weight: .regular, size: 42, leading: 50), 151 | footnote: .init(weight: .regular, size: 38, leading: 46), 152 | caption1: .init(weight: .regular, size: 37, leading: 44), 153 | caption2: .init(weight: .regular, size: 34, leading: 41) 154 | ) 155 | 156 | static let accessibilityExtraExtraExtraLarge = DynamicTypeInstance( 157 | largeTitle: .init(weight: .regular, size: 60, leading: 70), 158 | title1: .init(weight: .regular, size: 58, leading: 68), 159 | title2: .init(weight: .regular, size: 56, leading: 66), 160 | title3: .init(weight: .regular, size: 55, leading: 65), 161 | headline: .init(weight: .semibold, size: 53, leading: 62), 162 | body: .init(weight: .regular, size: 53, leading: 62), 163 | callout: .init(weight: .regular, size: 51, leading: 60), 164 | subhead: .init(weight: .regular, size: 49, leading: 58), 165 | footnote: .init(weight: .regular, size: 44, leading: 52), 166 | caption1: .init(weight: .regular, size: 43, leading: 51), 167 | caption2: .init(weight: .regular, size: 40, leading: 48) 168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /Tests/DynamicTypeTests/DynamicTypeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DynamicType 3 | 4 | final class DynamicTypeTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------