├── .gitignore
├── .spi.yml
├── Examples
├── Futura.plist
├── Noteworthy.plist
└── NotoSerif.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── ScaledFont
│ ├── ScaledFont.docc
│ ├── Resources
│ │ ├── noteworthy.png
│ │ ├── noteworthy@2x.png
│ │ ├── noto.png
│ │ └── noto@2x.png
│ ├── ScaledFont.md
│ └── StyleDictionary.md
│ ├── ScaledFont.swift
│ └── ScaledFontUI.swift
└── Tests
└── ScaledFontTests
├── ScaledFontTests.swift
└── TestData
├── Futura.plist
├── InvalidBodyStyle.plist
└── MissingHeadline.plist
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | /.swiftpm
7 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - platform: ios
5 | documentation_targets: [ScaledFont]
--------------------------------------------------------------------------------
/Examples/Futura.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | largeTitle
6 |
7 | fontName
8 | Futura-Medium
9 | fontSize
10 | 34
11 |
12 | title
13 |
14 | fontName
15 | Futura-Medium
16 | fontSize
17 | 28
18 |
19 | title2
20 |
21 | fontName
22 | Futura-Medium
23 | fontSize
24 | 22
25 |
26 | title3
27 |
28 | fontName
29 | Futura-Medium
30 | fontSize
31 | 20
32 |
33 | headline
34 |
35 | fontName
36 | Futura-Bold
37 | fontSize
38 | 17
39 |
40 | subheadline
41 |
42 | fontName
43 | Futura-MediumItalic
44 | fontSize
45 | 15
46 |
47 | body
48 |
49 | fontName
50 | Futura-Medium
51 | fontSize
52 | 17
53 |
54 | callout
55 |
56 | fontName
57 | Futura-Medium
58 | fontSize
59 | 16
60 |
61 | footnote
62 |
63 | fontName
64 | Futura-Medium
65 | fontSize
66 | 13
67 |
68 | caption
69 |
70 | fontName
71 | Futura-MediumItalic
72 | fontSize
73 | 12
74 |
75 | caption2
76 |
77 | fontName
78 | Futura-MediumItalic
79 | fontSize
80 | 11
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Examples/Noteworthy.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | largeTitle
6 |
7 | fontName
8 | Noteworthy-Light
9 | fontSize
10 | 34
11 |
12 | title
13 |
14 | fontName
15 | Noteworthy-Light
16 | fontSize
17 | 28
18 |
19 | title2
20 |
21 | fontName
22 | Noteworthy-Light
23 | fontSize
24 | 22
25 |
26 | title3
27 |
28 | fontName
29 | Noteworthy-Light
30 | fontSize
31 | 20
32 |
33 | headline
34 |
35 | fontName
36 | Noteworthy-Bold
37 | fontSize
38 | 17
39 |
40 | subheadline
41 |
42 | fontName
43 | Noteworthy-Light
44 | fontSize
45 | 15
46 |
47 | body
48 |
49 | fontName
50 | Noteworthy-Light
51 | fontSize
52 | 17
53 |
54 | callout
55 |
56 | fontName
57 | Noteworthy-Light
58 | fontSize
59 | 16
60 |
61 | footnote
62 |
63 | fontName
64 | Noteworthy-Light
65 | fontSize
66 | 13
67 |
68 | caption
69 |
70 | fontName
71 | Noteworthy-Light
72 | fontSize
73 | 12
74 |
75 | caption2
76 |
77 | fontName
78 | Noteworthy-Light
79 | fontSize
80 | 11
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Examples/NotoSerif.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | largeTitle
6 |
7 | fontName
8 | NotoSerif
9 | fontSize
10 | 34
11 |
12 | title
13 |
14 | fontName
15 | NotoSerif
16 | fontSize
17 | 28
18 |
19 | title2
20 |
21 | fontName
22 | NotoSerif
23 | fontSize
24 | 22
25 |
26 | title3
27 |
28 | fontName
29 | NotoSerif
30 | fontSize
31 | 20
32 |
33 | headline
34 |
35 | fontName
36 | NotoSerif-Bold
37 | fontSize
38 | 17
39 |
40 | subheadline
41 |
42 | fontName
43 | NotoSerif-Italic
44 | fontSize
45 | 15
46 |
47 | body
48 |
49 | fontName
50 | NotoSerif
51 | fontSize
52 | 17
53 |
54 | callout
55 |
56 | fontName
57 | NotoSerif
58 | fontSize
59 | 16
60 |
61 | footnote
62 |
63 | fontName
64 | NotoSerif
65 | fontSize
66 | 13
67 |
68 | caption
69 |
70 | fontName
71 | NotoSerif-Italic
72 | fontSize
73 | 12
74 |
75 | caption2
76 |
77 | fontName
78 | NotoSerif-Italic
79 | fontSize
80 | 10
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2017-2024 Keith Harrison. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 |
9 | 2. Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 |
13 | 3. Neither the name of the copyright holder nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "ScaledFont",
7 | platforms: [
8 | .iOS(.v13), .tvOS(.v13), .watchOS(.v6)
9 | ],
10 | products: [
11 | .library(
12 | name: "ScaledFont",
13 | targets: ["ScaledFont"]),
14 | ],
15 | targets: [
16 | .target(name: "ScaledFont"),
17 | .testTarget(
18 | name: "ScaledFontTests",
19 | dependencies: ["ScaledFont"],
20 | resources: [.process("TestData")]),
21 | ]
22 | )
23 |
24 | for target in package.targets {
25 | var settings = target.swiftSettings ?? []
26 | settings.append(.enableExperimentalFeature("StrictConcurrency"))
27 | target.swiftSettings = settings
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScaledFont - Custom Fonts With Dynamic Type
2 |
3 | [](https://swiftpackageindex.com/kharrison/ScaledFont)
4 | [](https://swiftpackageindex.com/kharrison/ScaledFont)
5 |
6 | **A utility type to help you use custom fonts with dynamic type.**
7 |
8 | Dynamic type is an **essential iOS feature** that allows the user to choose their preferred text size. Fully supporting dynamic type with a custom font requires two things:
9 |
10 | 1. Define a base font with a suitable font weight and size for each of the possible text styles at the Large (Default) content size.
11 | 2. Scale the base font across the range of dynamic type content sizes.
12 |
13 | For the first step, you might want to start with the typography section of the [Apple Human Interface Guidelines for iOS](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/) which list the font metrics Apple uses for the default San Francisco font.
14 |
15 | For example, here I'm creating a bold Noteworthy font at 17 points as the base headline font and a light version of the font for the base body font:
16 |
17 | ```swift
18 | let headlineFont = UIFont(name: "Noteworthy-Bold", size: 17)
19 | let bodyFont = UIFont(name: "Noteworthy-Light", size: 17)
20 | ```
21 |
22 | For the second step, it's been possible since iOS 11 to scale a base font for the user's chosen dynamic type size using font metrics:
23 |
24 | ```swift
25 | let headMetrics = UIFontMetrics(forTextStyle: .headline)
26 | headlineLabel.font = headMetrics.scaledFont(for: headlineFont)
27 |
28 | let bodyMetrics = UIFontMetrics(forTextStyle: .body)
29 | bodyLabel.font = bodyMetrics.scaledFont(for: bodyFont)
30 | ```
31 |
32 | The problem, if you do this for every text style you use, is that you end up with those font metrics spread all over your app. That's both difficult to maintain and hard to keep consistent when you want to make design changes.
33 |
34 | **TIP: Don't forget when using UIKit labels, text fields and text views to enable automatic adjustments when the user changes their preferred content size:**
35 |
36 | ```swift
37 | label.adjustsFontForContentSizeCategory = true
38 | ```
39 |
40 | ## Style Dictionary
41 |
42 | To make it easier to manage the base font metrics for all of the possible text styles the `ScaledFont` type collects them into a style dictionary. You store the style dictionary as a property list file that, by default, you include in the main bundle.
43 |
44 | The style dictionary contains an entry for each text style. The available text styles are:
45 |
46 | - `largeTitle`, `title`, `title2`, `title3`
47 | - `headline`, `subheadline`, `body`, `callout`
48 | - `footnote`, `caption`, `caption2`
49 |
50 | The value of each entry is a dictionary with two keys:
51 |
52 | + `fontName`: A `String` which is the font name.
53 | + `fontSize`: A number which is the point size to use at the `.large` (base) content size.
54 |
55 | For example, to use a 17 pt Noteworthy-Bold font for the `.headline` style at the `.large` content size:
56 |
57 | ```
58 |
59 | headline
60 |
61 | fontName
62 | Noteworthy-Bold
63 | fontSize
64 | 17
65 |
66 |
67 | ```
68 |
69 | You do not need to include an entry for every text style but if you try to use a text style that is not included in the dictionary it will fallback to the system preferred font.
70 |
71 | If you are not sure what font names to use you can print all available names with this code snippet:
72 |
73 | ```swift
74 | let families = UIFont.familyNames
75 | families.sorted().forEach {
76 | print("\($0)")
77 | let names = UIFont.fontNames(forFamilyName: $0)
78 | print(names)
79 | }
80 | ```
81 |
82 | ## Example Style Dictionaries
83 |
84 | See the `Examples` folder included in this package for some examples. The `Noteworthy` style dictionary uses a built-in iOS font.
85 |
86 | To use the `NotoSerif` example you'll need to download the font files from [Google fonts](https://fonts.google.com/specimen/Noto+Serif), add them to your application target, and list them under "Fonts provided by application" in the `Info.plist` file of the target.
87 |
88 | **Check the license for any fonts you plan on shipping with your application.**
89 |
90 | ## Using A ScaledFont - UIKit
91 |
92 | When using `UIKit` you apply the scaled font to the text label, text field or text view in code. You need a minimum deployment target of iOS 11 or later.
93 |
94 | 1. Create the `ScaledFont` by specifying the name of the style dictionary. Add the style dictionary to the main bundle along with any custom fonts you are using:
95 |
96 | ```swift
97 | let scaledFont = ScaledFont(fontName: "Noteworthy")
98 | ```
99 |
100 | 2. Use the `font(forTextStyle:)` method of the scaled font when setting the font of any text labels, fields or views:
101 |
102 | ```swift
103 | let label = UILabel()
104 | label.font = scaledFont.font(forTextStyle: .headline)
105 | ```
106 |
107 | 3. Remember to set the `adjustsFontFotContentSizeCategory` property to have the font size adjust automatically when the user changes their preferred content size:
108 |
109 | ```swift
110 | label.adjustsFontForContentSizeCategory = true
111 | ```
112 |
113 | ## Using A ScaledFont - SwiftUI
114 |
115 | When using SwiftUI you create the scaled font and add it to the environment of a view. You then apply the scaled font using a view modifier to any view in the view hierarchy. You need a minimum deployment target of iOS 13 or later to use SwiftUI.
116 |
117 | 1. Create the `ScaledFont` by specifying the name of the style dictionary. Add the style dictionary to the main bundle along with any custom fonts you are using:
118 |
119 | ```swift
120 | let scaledFont = ScaledFont(fontName: "Noteworthy")
121 | ```
122 |
123 | 2. Apply the scaled font to the environment of a view. This might typically be the root view of your view hierarchy:
124 |
125 | ```swift
126 | ContentView()
127 | .environment(\.scaledFont, scaledFont)
128 | ```
129 |
130 | 3. Apply the scaled font view modifier to a view containing text in the view hierarchy:
131 |
132 | ```swift
133 | Text("Headline")
134 | .scaledFont(.headline)
135 | ```
136 |
137 | **Note: A SwiftUI view presented in a sheet does not inherit the environment of the presenting view**. If you want to use a scaled font in the presented view you will need to pass it in the environment:
138 |
139 | ```swift
140 | struct ContentView: View {
141 | @Environment(\.scaledFont) private var scaledFont
142 | @State private var isShowingSheet = false
143 |
144 | var body: some View {
145 | Button("Present View") {
146 | isShowingSheet = true
147 | }
148 | .sheet(isPresented: $isShowingSheet) {
149 | SheetView()
150 | .environment(\.scaledFont, scaledFont)
151 | }
152 | }
153 | }
154 | ```
155 |
156 | ## Further Reading
157 |
158 | The following blog posts on [useyourloaf.com](https://useyourloaf.com) provide more details:
159 |
160 | + [Using A Custom Font With Dynamic Type](https://useyourloaf.com/blog/using-a-custom-font-with-dynamic-type/)
161 | + [Scaling Custom SwiftUI Fonts With Dynamic Type](https://useyourloaf.com/blog/scaling-custom-swiftui-fonts-with-dynamic-type/)
162 |
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.docc/Resources/noteworthy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kharrison/ScaledFont/287a1348f790c81247b79be6a681128e2af04cfb/Sources/ScaledFont/ScaledFont.docc/Resources/noteworthy.png
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.docc/Resources/noteworthy@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kharrison/ScaledFont/287a1348f790c81247b79be6a681128e2af04cfb/Sources/ScaledFont/ScaledFont.docc/Resources/noteworthy@2x.png
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.docc/Resources/noto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kharrison/ScaledFont/287a1348f790c81247b79be6a681128e2af04cfb/Sources/ScaledFont/ScaledFont.docc/Resources/noto.png
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.docc/Resources/noto@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kharrison/ScaledFont/287a1348f790c81247b79be6a681128e2af04cfb/Sources/ScaledFont/ScaledFont.docc/Resources/noto@2x.png
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.docc/ScaledFont.md:
--------------------------------------------------------------------------------
1 | # ``ScaledFont``
2 |
3 | A utility type to help you use custom fonts with dynamic type.
4 |
5 | ## Overview
6 |
7 | Dynamic type is an **essential iOS feature** that allows the user to choose their preferred text size. Fully supporting dynamic type with a custom font requires two things:
8 |
9 | 1. Define a base font with a suitable font weight and size for each of the possible text styles at the Large (Default) content size.
10 | 2. Scale the base font across the range of dynamic type content sizes.
11 |
12 | The problem, if you do this for every text style you use, is that you end up with those font metrics spread all over your app. That's both difficult to maintain and hard to keep consistent when you want to make design changes.
13 |
14 | To make it easier to manage the base font metrics for all of the possible text styles the `ScaledFont` type collects them into a **style dictionary**. You store the style dictionary as a property list file that, by default, you include in the main bundle.
15 |
16 | ## Topics
17 |
18 | ### Getting Started
19 |
20 | -
21 |
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.docc/StyleDictionary.md:
--------------------------------------------------------------------------------
1 | # Creating A Style Dictionary
2 |
3 | Create a style dictionary to control how a custom font scales with dynamic type content size.
4 |
5 | ## Overview
6 |
7 | A style dictionary collects the base font metrics for each of the dynamic type text styles. You need to create a style dictionary for each custom font you want to use in your app.
8 |
9 | A style dictionary is a property list file that you include with your app. Add an entry for each text style. The available text styles are:
10 |
11 | - `largeTitle`, `title`, `title2`, `title3`
12 | - `headline`, `subheadline`, `body`, `callout`
13 | - `footnote`, `caption`, `caption2`
14 |
15 | The value of each entry is a dictionary with two keys:
16 |
17 | + `fontName`: A `String` which is the font name.
18 | + `fontSize`: A number which is the point size to use at the `.large` (base) content size.
19 |
20 | If you're not sure which font sizes to use for each style refer to the typography section of the [Apple Human Interface Guidelines for iOS](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/). It lists the font metrics Apple uses for the default San Francisco font.
21 |
22 | For example, to use a 17 pt Noteworthy-Bold font for the `.headline` style at the `.large` content size:
23 |
24 | ```
25 |
26 | headline
27 |
28 | fontName
29 | Noteworthy-Bold
30 | fontSize
31 | 17
32 |
33 |
34 | ```
35 |
36 | You do not need to include an entry for every text style but if you try to use a text style that is not included in the dictionary it will fallback to the system preferred font.
37 |
38 | ### Finding Font Names
39 |
40 | If you are not sure what font names to use you can print all available names with this code snippet:
41 |
42 | ```swift
43 | let families = UIFont.familyNames
44 | families.sorted().forEach {
45 | print("\($0)")
46 | let names = UIFont.fontNames(forFamilyName: $0)
47 | print(names)
48 | }
49 | ```
50 |
51 | **Note that the system installed fonts are not the same for iOS, tvOS and watchOS platforms.**
52 |
53 | ## Example Style Dictionaries
54 |
55 | See the `Examples` folder included in this package for some examples. The `Futura` font is available on iOS, tvOS and watchOS.
56 |
57 | The `Noteworthy` style dictionary uses a built-in iOS font.
58 |
59 | 
60 |
61 | To use the `NotoSerif` example you'll need to download the font files from [Google fonts](https://fonts.google.com/specimen/Noto+Serif), add them to your application target, and list them under "Fonts provided by application" in the `Info.plist` file of the target.
62 |
63 | 
64 |
65 | **Check the license for any fonts you plan on shipping with your application.**
66 |
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFont.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017-2024 Keith Harrison. All rights reserved.
2 | //
3 | // Redistribution and use in source and binary forms, with or without
4 | // modification, are permitted provided that the following conditions are met:
5 | //
6 | // 1. Redistributions of source code must retain the above copyright
7 | // notice, this list of conditions and the following disclaimer.
8 | //
9 | // 2. Redistributions in binary form must reproduce the above copyright
10 | // notice, this list of conditions and the following disclaimer in the
11 | // documentation and/or other materials provided with the distribution.
12 | //
13 | // 3. Neither the name of the copyright holder nor the names of its
14 | // contributors may be used to endorse or promote products derived from
15 | // this software without specific prior written permission.
16 | //
17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | // POSSIBILITY OF SUCH DAMAGE.
28 |
29 | import UIKit
30 |
31 | /// A utility type to help you use custom fonts with
32 | /// dynamic type.
33 | ///
34 | /// To use this type you must supply the name of a style
35 | /// dictionary for the font when creating the `ScaledFont`.
36 | /// The style dictionary should be stored as a property list
37 | /// file in the main bundle.
38 | ///
39 | /// The style dictionary contains an entry for each text
40 | /// style. The available text styles are:
41 | ///
42 | /// - `largeTitle`, `title`, `title2`, `title3`
43 | /// - `headline`, `subheadline`, `body`, `callout`
44 | /// - `footnote`, `caption`, `caption2`
45 | ///
46 | /// The value of each entry is a dictionary with two keys:
47 | ///
48 | /// + `fontName`: A `String` which is the font name.
49 | /// + `fontSize`: A number which is the point size to use
50 | /// at the `.large` (base) content size.
51 | ///
52 | /// For example to use a 17 pt Noteworthy-Bold font
53 | /// for the `.headline` style at the `.large` content size:
54 | ///
55 | ///
56 | /// headline
57 | ///
58 | /// fontName
59 | /// Noteworthy-Bold
60 | /// fontSize
61 | /// 17
62 | ///
63 | ///
64 | ///
65 | /// You do not need to include an entry for every text style
66 | /// but if you try to use a text style that is not included
67 | /// in the dictionary it will fallback to the system preferred
68 | /// font.
69 | ///
70 | /// ## Using With UIKit
71 | ///
72 | /// For `UIKit`, apply the scaled font to text labels, text fields or text
73 | /// views:
74 | ///
75 | /// ```swift
76 | /// let scaledFont = ScaledFont(fontName: "Noteworthy")
77 | /// label.font = scaledFont.font(forTextStyle: .headline)
78 | /// label.adjustsFontForContentSizeCategory = true
79 | /// ```
80 | ///
81 | /// Remember to set the `adjustsFontFotContentSizeCategory` property
82 | /// to have the font size adjust automatically when the user changes
83 | /// their preferred content size.
84 | ///
85 | /// ## Using With SwiftUI
86 | ///
87 | /// For SwiftUI, add the scaled font to the environment of a view:
88 | ///
89 | /// ```swift
90 | /// ContentView()
91 | /// .environment(\.scaledFont, scaledFont)
92 | /// ```
93 | ///
94 | /// Then apply the scaled font view modifier to any view containing
95 | /// text in the view hierarchy:
96 | ///
97 | /// ```swift
98 | /// Text("Headline")
99 | /// .scaledFont(.headline)
100 | /// ```
101 | ///
102 |
103 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
104 | public struct ScaledFont: Sendable {
105 | internal enum StyleKey: String, Decodable {
106 | case largeTitle, title, title2, title3
107 | case headline, subheadline, body, callout
108 | case footnote, caption, caption2
109 | }
110 |
111 | internal struct FontDescription: Decodable {
112 | let fontSize: CGFloat
113 | let fontName: String
114 | }
115 |
116 | internal typealias StyleDictionary = [StyleKey.RawValue: FontDescription]
117 | internal var styleDictionary: StyleDictionary?
118 |
119 | /// Create a `ScaledFont`
120 | ///
121 | /// - Parameter fontName: Name of a plist file (without the extension)
122 | /// that contains the style dictionary used to scale fonts for each
123 | /// text style.
124 | /// - Parameter bundle: The `Bundle` that contains the style dictionary.
125 | /// Default is the main bundle.
126 |
127 | public init(fontName: String, bundle: Bundle = .main) {
128 | if let url = bundle.url(forResource: fontName, withExtension: "plist"),
129 | let data = try? Data(contentsOf: url)
130 | {
131 | let decoder = PropertyListDecoder()
132 | styleDictionary = try? decoder.decode(StyleDictionary.self, from: data)
133 | }
134 | }
135 |
136 | /// Get the scaled font for the given text style using the
137 | /// style dictionary supplied at initialization.
138 | ///
139 | /// - Parameter textStyle: The `UIFont.TextStyle` for the
140 | /// font.
141 | /// - Returns: A `UIFont` of the custom font that has been
142 | /// scaled for the users currently selected preferred
143 | /// text size.
144 | ///
145 | /// - Note: If the style dictionary does not have
146 | /// a font for this text style the default preferred
147 | /// font is returned.
148 |
149 | public func font(forTextStyle textStyle: UIFont.TextStyle) -> UIFont {
150 | guard let styleKey = StyleKey(textStyle),
151 | let fontDescription = styleDictionary?[styleKey.rawValue],
152 | let font = UIFont(name: fontDescription.fontName, size: fontDescription.fontSize)
153 | else {
154 | return UIFont.preferredFont(forTextStyle: textStyle)
155 | }
156 |
157 | let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
158 | return fontMetrics.scaledFont(for: font)
159 | }
160 | }
161 |
162 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
163 | extension ScaledFont.StyleKey {
164 | init?(_ textStyle: UIFont.TextStyle) {
165 | #if !os(tvOS)
166 | if #available(watchOS 5.0, *) {
167 | if textStyle == .largeTitle {
168 | self = .largeTitle
169 | return
170 | }
171 | }
172 | #endif
173 | switch textStyle {
174 | case .title1: self = .title
175 | case .title2: self = .title2
176 | case .title3: self = .title3
177 | case .headline: self = .headline
178 | case .subheadline: self = .subheadline
179 | case .body: self = .body
180 | case .callout: self = .callout
181 | case .footnote: self = .footnote
182 | case .caption1: self = .caption
183 | case .caption2: self = .caption2
184 | default: return nil
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/Sources/ScaledFont/ScaledFontUI.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2021-2024 Keith Harrison. All rights reserved.
2 | //
3 | // Redistribution and use in source and binary forms, with or without
4 | // modification, are permitted provided that the following conditions are met:
5 | //
6 | // 1. Redistributions of source code must retain the above copyright
7 | // notice, this list of conditions and the following disclaimer.
8 | //
9 | // 2. Redistributions in binary form must reproduce the above copyright
10 | // notice, this list of conditions and the following disclaimer in the
11 | // documentation and/or other materials provided with the distribution.
12 | //
13 | // 3. Neither the name of the copyright holder nor the names of its
14 | // contributors may be used to endorse or promote products derived from
15 | // this software without specific prior written permission.
16 | //
17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | // POSSIBILITY OF SUCH DAMAGE.
28 |
29 | import SwiftUI
30 |
31 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
32 | extension ScaledFont.StyleKey {
33 | init?(_ textStyle: Font.TextStyle) {
34 | switch textStyle {
35 | case .largeTitle: self = .largeTitle
36 | case .title: self = .title
37 | case .title2: self = .title2
38 | case .title3: self = .title3
39 | case .headline: self = .headline
40 | case .subheadline: self = .subheadline
41 | case .body: self = .body
42 | case .callout: self = .callout
43 | case .footnote: self = .footnote
44 | case .caption: self = .caption
45 | case .caption2: self = .caption2
46 | @unknown default: return nil
47 | }
48 | }
49 | }
50 |
51 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
52 | extension ScaledFont {
53 | /// Get the scaled font for the given text style using the
54 | /// style dictionary supplied at initialization.
55 | ///
56 | /// - Parameter textStyle: The `Font.TextStyle` for the
57 | /// font.
58 | /// - Returns: A `Font` of the custom font that has been
59 | /// scaled for the users currently selected preferred
60 | /// text size.
61 | ///
62 | /// - Note: If the style dictionary does not have
63 | /// a font for this text style the default system
64 | /// font is returned.
65 |
66 | internal func font(forTextStyle textStyle: Font.TextStyle) -> Font {
67 | if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) {
68 | guard let styleKey = StyleKey(textStyle),
69 | let fontDescription = styleDictionary?[styleKey.rawValue]
70 | else {
71 | return Font.system(textStyle)
72 | }
73 |
74 | return Font.custom(fontDescription.fontName, size: fontDescription.fontSize, relativeTo: textStyle)
75 | } else {
76 | // Falback to UIKit methods for iOS 13
77 | return Font(font(forTextStyle: uiTextStyle(textStyle)))
78 | }
79 | }
80 |
81 | private func uiTextStyle(_ textStyle: Font.TextStyle) -> UIFont.TextStyle {
82 | switch textStyle {
83 | case .largeTitle: return largeTitle()
84 | case .title: return .title1
85 | case .title2: return .title2
86 | case .title3: return .title3
87 | case .headline: return .headline
88 | case .subheadline: return .subheadline
89 | case .body: return .body
90 | case .callout: return .callout
91 | case .footnote: return .footnote
92 | case .caption: return .caption1
93 | case .caption2: return .caption2
94 | @unknown default: return .body
95 | }
96 | }
97 |
98 | // On tvOS fallback to .title1 text style as
99 | // UIKit (but not SwiftUI) is missing .largeTitle.
100 | private func largeTitle() -> UIFont.TextStyle {
101 | #if os(tvOS)
102 | return .title1
103 | #else
104 | return .largeTitle
105 | #endif
106 | }
107 | }
108 |
109 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
110 | private struct ScaledFontModifier: ViewModifier {
111 | @Environment(\.scaledFont) var scaledFont
112 | let textStyle: Font.TextStyle
113 |
114 | func body(content: Content) -> some View {
115 | content
116 | .font(scaledFont.font(forTextStyle: textStyle))
117 | }
118 | }
119 |
120 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
121 | public extension View {
122 | /// Sets the text style of the scaled font for text
123 | /// in the view.
124 | /// - Parameter textStyle: A dynamic type text styles
125 | /// - Returns: A View that uses the scaled font with the
126 | /// specified dynamic type text style.
127 | func scaledFont(_ textStyle: Font.TextStyle = .body) -> some View {
128 | return modifier(ScaledFontModifier(textStyle: textStyle))
129 | }
130 | }
131 |
132 | private struct ScaledFontKey: EnvironmentKey {
133 | static let defaultValue = ScaledFont(fontName: "Default")
134 | }
135 |
136 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
137 | public extension EnvironmentValues {
138 | /// A custom font that supports dynamic type
139 | /// text styles.
140 | var scaledFont: ScaledFont {
141 | get { self[ScaledFontKey.self] }
142 | set { self[ScaledFontKey.self] = newValue }
143 | }
144 | }
145 |
146 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
147 | public extension View {
148 | /// Set the scaledFont environment property
149 | /// - Parameter scaledFont: A ScaledFont to use
150 | func scaledFont(_ scaledFont: ScaledFont) -> some View {
151 | environment(\.scaledFont, scaledFont)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Tests/ScaledFontTests/ScaledFontTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2021 Keith Harrison. All rights reserved.
2 | //
3 | // Redistribution and use in source and binary forms, with or without
4 | // modification, are permitted provided that the following conditions are met:
5 | //
6 | // 1. Redistributions of source code must retain the above copyright
7 | // notice, this list of conditions and the following disclaimer.
8 | //
9 | // 2. Redistributions in binary form must reproduce the above copyright
10 | // notice, this list of conditions and the following disclaimer in the
11 | // documentation and/or other materials provided with the distribution.
12 | //
13 | // 3. Neither the name of the copyright holder nor the names of its
14 | // contributors may be used to endorse or promote products derived from
15 | // this software without specific prior written permission.
16 | //
17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | // POSSIBILITY OF SUCH DAMAGE.
28 |
29 | import ScaledFont
30 | import XCTest
31 |
32 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
33 | final class ScaledFontTests: XCTestCase {
34 | private let testFont = "Futura"
35 | private let defaultFontName = "Futura-Medium"
36 | private let boldFontName = "Futura-Bold"
37 | private let italicFontName = "Futura-MediumItalic"
38 |
39 | #if !os(tvOS)
40 | func testLargeTitleStyle() {
41 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
42 | let font = scaledFont.font(forTextStyle: .largeTitle)
43 | XCTAssertEqual(font.fontName, defaultFontName)
44 | }
45 | #endif
46 |
47 | func testTitle1Style() {
48 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
49 | let font = scaledFont.font(forTextStyle: .title1)
50 | XCTAssertEqual(font.fontName, defaultFontName)
51 | }
52 |
53 | func testTitle2Style() {
54 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
55 | let font = scaledFont.font(forTextStyle: .title2)
56 | XCTAssertEqual(font.fontName, defaultFontName)
57 | }
58 |
59 | func testTitle3Style() {
60 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
61 | let font = scaledFont.font(forTextStyle: .title3)
62 | XCTAssertEqual(font.fontName, defaultFontName)
63 | }
64 |
65 | func testHeadlineStyle() {
66 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
67 | let font = scaledFont.font(forTextStyle: .headline)
68 | XCTAssertEqual(font.fontName, boldFontName)
69 | }
70 |
71 | func testSubheadlineStyle() {
72 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
73 | let font = scaledFont.font(forTextStyle: .subheadline)
74 | XCTAssertEqual(font.fontName, italicFontName)
75 | }
76 |
77 | func testBodyStyle() {
78 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
79 | let font = scaledFont.font(forTextStyle: .body)
80 | XCTAssertEqual(font.fontName, defaultFontName)
81 | }
82 |
83 | func testCalloutStyle() {
84 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
85 | let font = scaledFont.font(forTextStyle: .callout)
86 | XCTAssertEqual(font.fontName, defaultFontName)
87 | }
88 |
89 | func testFootnoteStyle() {
90 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
91 | let font = scaledFont.font(forTextStyle: .footnote)
92 | XCTAssertEqual(font.fontName, defaultFontName)
93 | }
94 |
95 | func testCaption1Style() {
96 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
97 | let font = scaledFont.font(forTextStyle: .caption1)
98 | XCTAssertEqual(font.fontName, italicFontName)
99 | }
100 |
101 | func testCaption2Style() {
102 | let scaledFont = ScaledFont(fontName: testFont, bundle: .module)
103 | let font = scaledFont.font(forTextStyle: .caption2)
104 | XCTAssertEqual(font.fontName, italicFontName)
105 | }
106 |
107 | func testFallbackWhenHeadlineStyleMissing() {
108 | let scaledFont = ScaledFont(fontName: "MissingHeadline", bundle: .module)
109 | let font = scaledFont.font(forTextStyle: .headline)
110 | XCTAssertEqual(font.familyName, ".AppleSystemUIFont")
111 | }
112 |
113 | func testTitleStyleWhenHeadlineStyleMissing() {
114 | let scaledFont = ScaledFont(fontName: "MissingHeadline", bundle: .module)
115 | let font = scaledFont.font(forTextStyle: .title1)
116 | XCTAssertEqual(font.fontName, defaultFontName)
117 | }
118 |
119 | func testFallbackWhenDictionaryInvalid() {
120 | let fontName = "InvalidBodyStyle"
121 | let scaledFont = ScaledFont(fontName: fontName, bundle: .module)
122 | let font = scaledFont.font(forTextStyle: .body)
123 | XCTAssertEqual(font.familyName, ".AppleSystemUIFont")
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Tests/ScaledFontTests/TestData/Futura.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | largeTitle
6 |
7 | fontName
8 | Futura-Medium
9 | fontSize
10 | 34
11 |
12 | title
13 |
14 | fontName
15 | Futura-Medium
16 | fontSize
17 | 28
18 |
19 | title2
20 |
21 | fontName
22 | Futura-Medium
23 | fontSize
24 | 22
25 |
26 | title3
27 |
28 | fontName
29 | Futura-Medium
30 | fontSize
31 | 20
32 |
33 | headline
34 |
35 | fontName
36 | Futura-Bold
37 | fontSize
38 | 17
39 |
40 | subheadline
41 |
42 | fontName
43 | Futura-MediumItalic
44 | fontSize
45 | 15
46 |
47 | body
48 |
49 | fontName
50 | Futura-Medium
51 | fontSize
52 | 17
53 |
54 | callout
55 |
56 | fontName
57 | Futura-Medium
58 | fontSize
59 | 16
60 |
61 | footnote
62 |
63 | fontName
64 | Futura-Medium
65 | fontSize
66 | 13
67 |
68 | caption
69 |
70 | fontName
71 | Futura-MediumItalic
72 | fontSize
73 | 12
74 |
75 | caption2
76 |
77 | fontName
78 | Futura-MediumItalic
79 | fontSize
80 | 11
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Tests/ScaledFontTests/TestData/InvalidBodyStyle.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | largeTitle
6 |
7 | fontName
8 | Futura-Medium
9 | fontSize
10 | 34
11 |
12 | title
13 |
14 | fontName
15 | Futura-Medium
16 | fontSize
17 | 28
18 |
19 | title2
20 |
21 | fontName
22 | Futura-Medium
23 | fontSize
24 | 22
25 |
26 | title3
27 |
28 | fontName
29 | Futura-Medium
30 | fontSize
31 | 20
32 |
33 | headline
34 |
35 | fontName
36 | Futura-Bold
37 | fontSize
38 | 17
39 |
40 | subheadline
41 |
42 | fontName
43 | Futura-MediumItalic
44 | fontSize
45 | 15
46 |
47 | body
48 |
49 | fontName
50 | Futura-Medium
51 |
52 | callout
53 |
54 | fontName
55 | Futura-Medium
56 | fontSize
57 | 16
58 |
59 | footnote
60 |
61 | fontName
62 | Futura-Medium
63 | fontSize
64 | 13
65 |
66 | caption
67 |
68 | fontName
69 | Futura-MediumItalic
70 | fontSize
71 | 12
72 |
73 | caption2
74 |
75 | fontName
76 | Futura-MediumItalic
77 | fontSize
78 | 11
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/Tests/ScaledFontTests/TestData/MissingHeadline.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | largeTitle
6 |
7 | fontName
8 | Futura-Medium
9 | fontSize
10 | 34
11 |
12 | title
13 |
14 | fontName
15 | Futura-Medium
16 | fontSize
17 | 28
18 |
19 | title2
20 |
21 | fontName
22 | Futura-Medium
23 | fontSize
24 | 22
25 |
26 | title3
27 |
28 | fontName
29 | Futura-Medium
30 | fontSize
31 | 20
32 |
33 | subheadline
34 |
35 | fontName
36 | Futura-MediumItalic
37 | fontSize
38 | 15
39 |
40 | body
41 |
42 | fontName
43 | Futura-Medium
44 | fontSize
45 | 17
46 |
47 | callout
48 |
49 | fontName
50 | Futura-Medium
51 | fontSize
52 | 16
53 |
54 | footnote
55 |
56 | fontName
57 | Futura-Medium
58 | fontSize
59 | 13
60 |
61 | caption
62 |
63 | fontName
64 | Futura-MediumItalic
65 | fontSize
66 | 12
67 |
68 | caption2
69 |
70 | fontName
71 | Futura-MediumItalic
72 | fontSize
73 | 11
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------