Skip to content

Commit aadcbd1

Browse files
KM-13292 Add Dynamic Types support
1 parent 7fa3f47 commit aadcbd1

23 files changed

Lines changed: 379 additions & 67 deletions

LocalPackages/PIADesignSystem/Sources/PIADesignSystem/Typography/Extensions/Font+SFPro.swift

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,22 @@ import SwiftUI
2424

2525
/// SF Pro font extensions for PIA VPN design system.
2626
///
27-
/// Provides SF Pro font variants (Bold, Semibold, Regular) at custom sizes.
27+
/// Provides SF Pro font variants at custom sizes and weights.
2828
///
2929
/// Example:
3030
/// ```swift
31-
/// Text("Bold Text").font(.sfProBold(size: 18))
32-
/// Text("Semibold Text").font(.sfProSemibold(size: 16))
33-
/// Text("Regular Text").font(.sfProRegular(size: 14))
31+
/// Text("Bold Text").font(.sfPro(size: 18, weight: .bold))
32+
/// Text("Semibold Text").font(.sfPro(size: 16, weight: .semibold))
33+
/// Text("Regular Text").font(.sfPro(size: 14, weight: .regular))
3434
/// ```
3535
@available(iOS 13.0, *)
3636
extension Font {
37-
/// SF Pro Bold font
37+
/// SF Pro font
3838
///
3939
/// - Parameter size: The font size in points
40-
/// - Returns: SF Pro Bold font at the specified size
41-
static func sfProBold(size: CGFloat) -> Font {
42-
.system(size: size, weight: .bold, design: .default)
43-
}
44-
45-
/// SF Pro Semibold font
46-
///
47-
/// - Parameter size: The font size in points
48-
/// - Returns: SF Pro Semibold font at the specified size
49-
static func sfProSemibold(size: CGFloat) -> Font {
50-
.system(size: size, weight: .semibold, design: .default)
51-
}
52-
53-
/// SF Pro Regular font
54-
///
55-
/// - Parameter size: The font size in points
56-
/// - Returns: SF Pro Regular font at the specified size
57-
static func sfProRegular(size: CGFloat) -> Font {
58-
.system(size: size, weight: .regular, design: .default)
40+
/// - Parameter weight: The font weight
41+
/// - Returns: SF Pro font at the specified size and weight
42+
static func sfPro(size: CGFloat, weight: Weight) -> Font {
43+
.system(size: size, weight: weight, design: .default)
5944
}
6045
}

LocalPackages/PIADesignSystem/Sources/PIADesignSystem/Typography/Extensions/Text+Typography.swift

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import SwiftUI
2727
/// Typography modifier extensions for PIA VPN design system.
2828
///
2929
/// Provides a convenient `.typography()` modifier for applying typography styles to Text views.
30-
/// This modifier applies both font and line height specifications.
30+
/// This modifier applies both font and line height specifications with Dynamic Type support
31+
/// for accessibility. Fonts and line heights automatically scale based on user preferences.
3132
///
3233
/// Example:
3334
/// ```swift
@@ -38,46 +39,64 @@ import SwiftUI
3839
@available(iOS 13.0, *)
3940
public extension Text {
4041
/// Applies a typography style to the text, including font, line height, decorations, and optional color.
42+
/// Supports Dynamic Type for accessibility - text scales automatically with user's text size preferences.
4143
///
4244
/// - Parameters:
4345
/// - style: The typography style to apply
4446
/// - color: Optional color to apply to the text. If nil, no color is applied.
45-
/// - Returns: A modified Text view with the typography style applied
47+
/// - Returns: A modified Text view with the typography style applied and Dynamic Type support
48+
@MainActor
4649
func typography(_ style: TypographyStyle, color: Color? = nil) -> some View {
47-
self
48-
.font(style.font)
49-
.lineSpacing(style.lineSpacing)
50-
.modifier(ConditionalUnderline(enabled: style.hasUnderline))
51-
.modifier(ConditionalColor(color: color))
50+
if #available(iOS 14.0, *) {
51+
return self
52+
.modifier(AccessibleTypographyModifier(style: style, color: color))
53+
} else {
54+
return self
55+
.modifier(TypographyModifier(style: style, color: color))
56+
}
5257
}
5358
}
5459

55-
// MARK: - Helper ViewModifiers
56-
57-
/// Conditionally applies underline decoration
60+
/// ViewModifier that applies typography
5861
@available(iOS 13.0, *)
59-
private struct ConditionalUnderline: ViewModifier {
60-
let enabled: Bool
62+
@available(*, deprecated, renamed: "AccessibleTypographyModifier", message: "Remove this modifier once the package minimum platform is raised to .v15")
63+
private struct TypographyModifier: ViewModifier {
64+
let style: TypographyStyle
65+
let color: Color?
66+
67+
init(style: TypographyStyle, color: Color?) {
68+
self.style = style
69+
self.color = color
70+
}
6171

6272
func body(content: Content) -> some View {
63-
if enabled, #available(iOS 16.0, *) {
64-
content.underline()
65-
} else {
66-
content
67-
}
73+
content
74+
.font(style.font)
75+
.lineSpacing(style.lineSpacing)
76+
.modifier(ConditionalUnderline(enabled: style.hasUnderline))
77+
.modifier(ConditionalColor(color: color))
6878
}
6979
}
7080

71-
/// Conditionally applies foreground color
72-
@available(iOS 13.0, *)
73-
private struct ConditionalColor: ViewModifier {
81+
/// ViewModifier that applies typography with Dynamic Type support for accessibility
82+
@available(iOS 14.0, *)
83+
private struct AccessibleTypographyModifier: ViewModifier {
84+
let style: TypographyStyle
7485
let color: Color?
7586

87+
@ScaledMetric private var lineSpacing: CGFloat
88+
89+
init(style: TypographyStyle, color: Color?) {
90+
self.style = style
91+
self.color = color
92+
_lineSpacing = ScaledMetric(wrappedValue: style.lineSpacing)
93+
}
94+
7695
func body(content: Content) -> some View {
77-
if let color = color {
78-
content.foregroundColor(color)
79-
} else {
80-
content
81-
}
96+
content
97+
.modifier(ScaledFontModifier(baseSize: style.fontSize, weight: style.fontWeight))
98+
.lineSpacing(lineSpacing)
99+
.modifier(ConditionalUnderline(enabled: style.hasUnderline))
100+
.modifier(ConditionalColor(color: color))
82101
}
83102
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// ConditionalColor.swift
3+
// PIADesignSystem
4+
//
5+
// Created by Diego Trevisan on 09.12.25.
6+
// Copyright © 2025 Private Internet Access, Inc.
7+
//
8+
// This file is part of the Private Internet Access iOS Client.
9+
//
10+
// The Private Internet Access iOS Client is free software: you can redistribute it and/or
11+
// modify it under the terms of the GNU General Public License as published by the Free
12+
// Software Foundation, either version 3 of the License, or (at your option) any later version.
13+
//
14+
// The Private Internet Access iOS Client is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16+
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17+
// details.
18+
//
19+
// You should have received a copy of the GNU General Public License along with the Private
20+
// Internet Access iOS Client. If not, see <https://www.gnu.org/licenses/>.
21+
//
22+
23+
import SwiftUI
24+
25+
/// Conditionally applies foreground color
26+
@available(iOS 13.0, *)
27+
struct ConditionalColor: ViewModifier {
28+
let color: Color?
29+
30+
func body(content: Content) -> some View {
31+
if let color = color {
32+
content.foregroundColor(color)
33+
} else {
34+
content
35+
}
36+
}
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// ConditionalUnderline.swift
3+
// PIADesignSystem
4+
//
5+
// Created by Diego Trevisan on 09.12.25.
6+
// Copyright © 2025 Private Internet Access, Inc.
7+
//
8+
// This file is part of the Private Internet Access iOS Client.
9+
//
10+
// The Private Internet Access iOS Client is free software: you can redistribute it and/or
11+
// modify it under the terms of the GNU General Public License as published by the Free
12+
// Software Foundation, either version 3 of the License, or (at your option) any later version.
13+
//
14+
// The Private Internet Access iOS Client is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16+
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17+
// details.
18+
//
19+
// You should have received a copy of the GNU General Public License along with the Private
20+
// Internet Access iOS Client. If not, see <https://www.gnu.org/licenses/>.
21+
//
22+
23+
import SwiftUI
24+
25+
/// Conditionally applies underline decoration
26+
@available(iOS 13.0, *)
27+
struct ConditionalUnderline: ViewModifier {
28+
let enabled: Bool
29+
30+
func body(content: Content) -> some View {
31+
if enabled, #available(iOS 16.0, *) {
32+
content.underline()
33+
} else {
34+
content
35+
}
36+
}
37+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// ScaledFontModifier.swift
3+
// PIADesignSystem
4+
//
5+
// Created by Diego Trevisan on 09.12.25.
6+
// Copyright © 2025 Private Internet Access, Inc.
7+
//
8+
// This file is part of the Private Internet Access iOS Client.
9+
//
10+
// The Private Internet Access iOS Client is free software: you can redistribute it and/or
11+
// modify it under the terms of the GNU General Public License as published by the Free
12+
// Software Foundation, either version 3 of the License, or (at your option) any later version.
13+
//
14+
// The Private Internet Access iOS Client is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16+
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17+
// details.
18+
//
19+
// You should have received a copy of the GNU General Public License along with the Private
20+
// Internet Access iOS Client. If not, see <https://www.gnu.org/licenses/>.
21+
//
22+
23+
import SwiftUI
24+
25+
/// Applies scaled font based on typography style weight
26+
@available(iOS 14.0, *)
27+
struct ScaledFontModifier: ViewModifier {
28+
let weight: Font.Weight
29+
@ScaledMetric private var scaledSize: CGFloat
30+
31+
init(baseSize: CGFloat, weight: Font.Weight) {
32+
self.weight = weight
33+
_scaledSize = ScaledMetric(wrappedValue: baseSize)
34+
}
35+
36+
func body(content: Content) -> some View {
37+
content.font(
38+
.system(
39+
size: scaledSize,
40+
weight: weight,
41+
design: .default
42+
)
43+
)
44+
}
45+
}

LocalPackages/PIADesignSystem/Sources/PIADesignSystem/Typography/TypographyStyle.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ extension TypographyStyle {
114114
}
115115
}
116116

117+
/// Font weight for this typography style (used for Dynamic Type scaling)
118+
@available(iOS 13.0, *)
119+
var fontWeight: Font.Weight {
120+
switch self {
121+
case .title1:
122+
.bold
123+
case .title2, .subtitle1, .subtitle2, .subtitle3:
124+
.semibold
125+
case .title3, .body1, .body2, .button1, .button2, .caption1, .caption2, .caption3:
126+
.regular
127+
}
128+
}
129+
117130
/// Line spacing (lineHeight - fontSize)
118131
var lineSpacing: CGFloat {
119132
lineHeight - fontSize
@@ -132,15 +145,9 @@ extension TypographyStyle {
132145
/// Returns the SwiftUI Font for this typography style
133146
@available(iOS 13.0, *)
134147
var font: Font {
135-
switch self {
136-
case .title1:
137-
.sfProBold(size: fontSize)
138-
case .title2:
139-
.sfProSemibold(size: fontSize)
140-
case .title3, .body1, .body2, .button1, .button2, .caption1, .caption2, .caption3:
141-
.sfProRegular(size: fontSize)
142-
case .subtitle1, .subtitle2, .subtitle3:
143-
.sfProSemibold(size: fontSize)
144-
}
148+
.sfPro(
149+
size: fontSize,
150+
weight: fontWeight
151+
)
145152
}
146153
}

LocalPackages/PIADesignSystem/Tests/PIADesignSystemTests/SnapshotTests/ColorSnapshotTests.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ struct ColorSnapshotTests {
3434
@Test func colorPreviewLight() {
3535
let view = ColorPreview()
3636
.environment(\.colorScheme, .light)
37-
.frame(width: 2800, height: 1900)
37+
.frame(width: 1024)
38+
.fixedSize()
3839

3940
assertSnapshot(
4041
of: view,
41-
as: .image
42+
as: .image(traits: .init(displayScale: 1.0))
4243
)
4344
}
4445

@@ -47,11 +48,12 @@ struct ColorSnapshotTests {
4748
@Test func colorPreviewDark() {
4849
let view = ColorPreview()
4950
.environment(\.colorScheme, .dark)
50-
.frame(width: 2800, height: 1900)
51+
.frame(width: 1024)
52+
.fixedSize()
5153

5254
assertSnapshot(
5355
of: view,
54-
as: .image
56+
as: .image(traits: .init(displayScale: 1.0))
5557
)
5658
}
5759

0 commit comments

Comments
 (0)