├── .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://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fkharrison%2FScaledFont%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/kharrison/ScaledFont) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fkharrison%2FScaledFont%2Fbadge%3Ftype%3Dplatforms)](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 | ![Noteworthy font](noteworthy) 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 | ![Noto Serif font](noto) 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 | --------------------------------------------------------------------------------