Skip to content

Commit 8704a54

Browse files
author
Mark Pospesel
authored
[Issue 76] Add maximumScaleFactor to Typography (#81)
1 parent 08a0d4d commit 8704a54

File tree

6 files changed

+172
-4
lines changed

6 files changed

+172
-4
lines changed

Sources/YMatterType/Typography/Typography+Font.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension Typography {
2727
if !isFixed {
2828
let metrics = UIFontMetrics(forTextStyle: textStyle)
2929

30-
if let maximumPointSize = maximumPointSize {
30+
if let maximumPointSize = getMaximumPointSize(maximumPointSize) {
3131
font = metrics.scaledFont(
3232
for: font,
3333
maximumPointSize: maximumPointSize,
@@ -132,6 +132,32 @@ extension Typography {
132132

133133
return generateLayout(maximumPointSize: maximumPointSize, compatibleWith: traitCollection)
134134
}
135+
136+
/// Maximum point size (if any).
137+
///
138+
/// Calculated from `maximumScaleFactor` (if any) multiplied by `fontSize` or else `nil`
139+
public var maximumPointSize: CGFloat? {
140+
guard let maximumScaleFactor = maximumScaleFactor else {
141+
return nil
142+
}
143+
144+
return maximumScaleFactor * fontSize
145+
}
146+
147+
/// Returns the minimum of the point size (if any) or `maximumPointSize` (if any).
148+
/// - Parameter pointSize: optional point size to evaluate
149+
/// - Returns: the minimum of point size or maximumPointSize
150+
internal func getMaximumPointSize(_ pointSize: CGFloat?) -> CGFloat? {
151+
guard let maximumPointSize = maximumPointSize else {
152+
return pointSize
153+
}
154+
155+
guard let pointSize = pointSize else {
156+
return maximumPointSize
157+
}
158+
159+
return min(pointSize, maximumPointSize)
160+
}
135161
}
136162

137163
private extension Typography {

Sources/YMatterType/Typography/Typography+Mutators.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public extension Typography {
2727
textCase: textCase,
2828
textDecoration: textDecoration,
2929
textStyle: textStyle,
30+
maximumScaleFactor: maximumScaleFactor,
3031
isFixed: isFixed
3132
)
3233
}
@@ -58,6 +59,7 @@ public extension Typography {
5859
textCase: textCase,
5960
textDecoration: textDecoration,
6061
textStyle: textStyle,
62+
maximumScaleFactor: maximumScaleFactor,
6163
isFixed: isFixed
6264
)
6365
}
@@ -79,6 +81,7 @@ public extension Typography {
7981
textCase: textCase,
8082
textDecoration: textDecoration,
8183
textStyle: textStyle,
84+
maximumScaleFactor: maximumScaleFactor,
8285
isFixed: isFixed
8386
)
8487
}
@@ -100,6 +103,7 @@ public extension Typography {
100103
textCase: textCase,
101104
textDecoration: textDecoration,
102105
textStyle: textStyle,
106+
maximumScaleFactor: maximumScaleFactor,
103107
isFixed: isFixed
104108
)
105109
}
@@ -119,6 +123,7 @@ public extension Typography {
119123
textCase: textCase,
120124
textDecoration: textDecoration,
121125
textStyle: textStyle,
126+
maximumScaleFactor: maximumScaleFactor,
122127
isFixed: true
123128
)
124129
}
@@ -140,6 +145,7 @@ public extension Typography {
140145
textCase: textCase,
141146
textDecoration: textDecoration,
142147
textStyle: textStyle,
148+
maximumScaleFactor: maximumScaleFactor,
143149
isFixed: isFixed
144150
)
145151
}
@@ -161,6 +167,7 @@ public extension Typography {
161167
textCase: value,
162168
textDecoration: textDecoration,
163169
textStyle: textStyle,
170+
maximumScaleFactor: maximumScaleFactor,
164171
isFixed: isFixed
165172
)
166173
}
@@ -182,6 +189,29 @@ public extension Typography {
182189
textCase: textCase,
183190
textDecoration: value,
184191
textStyle: textStyle,
192+
maximumScaleFactor: maximumScaleFactor,
193+
isFixed: isFixed
194+
)
195+
}
196+
197+
/// Returns a copy of the Typography but with the new `maximumScaleFactor` applied.
198+
/// - Parameter value: the maximum scale factor to apply
199+
/// - Returns: an updated copy of the Typography
200+
func maximumScaleFactor(_ value: CGFloat?) -> Typography {
201+
if maximumScaleFactor == value { return self }
202+
203+
return Typography(
204+
fontFamily: fontFamily,
205+
fontWeight: fontWeight,
206+
fontSize: fontSize,
207+
lineHeight: lineHeight,
208+
letterSpacing: letterSpacing,
209+
paragraphIndent: paragraphIndent,
210+
paragraphSpacing: paragraphSpacing,
211+
textCase: textCase,
212+
textDecoration: textDecoration,
213+
textStyle: textStyle,
214+
maximumScaleFactor: value,
185215
isFixed: isFixed
186216
)
187217
}

Sources/YMatterType/Typography/Typography.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public struct Typography {
3131
/// The text style (e.g. Body or Title) that this font most closely represents.
3232
/// Used for Dynamic Type scaling of the font
3333
public let textStyle: UIFont.TextStyle
34+
/// Maximum scale factor to apply for this typography. `nil` means no limit.
35+
///
36+
/// Will not be considered if `isFixed == true`.
37+
/// Do not set to `1.0`, but set `isFixed = true` to disable Dynamic Type scaling.
38+
public let maximumScaleFactor: CGFloat?
3439
/// Whether this font is fixed in size or should be scaled through Dynamic Type
3540
public let isFixed: Bool
3641

@@ -54,6 +59,7 @@ public struct Typography {
5459
/// - textCase: text case to apply (defaults to `.none`)
5560
/// - textDecoration: text decoration to apply (defaults to `.none`)
5661
/// - textStyle: text style to use for scaling (defaults to `.body`)
62+
/// - maximumScaleFactor: maximum scale factor to apply (defaults to `nil`)
5763
/// - isFixed: `true` if this font should never scale, `false` if it should scale (defaults to `.false`)
5864
public init(
5965
fontFamily: FontFamily,
@@ -66,6 +72,7 @@ public struct Typography {
6672
textCase: TextCase = .none,
6773
textDecoration: TextDecoration = .none,
6874
textStyle: UIFont.TextStyle = .body,
75+
maximumScaleFactor: CGFloat? = nil,
6976
isFixed: Bool = false
7077
) {
7178
self.fontFamily = fontFamily
@@ -78,6 +85,7 @@ public struct Typography {
7885
self.textCase = textCase
7986
self.textDecoration = textDecoration
8087
self.textStyle = textStyle
88+
self.maximumScaleFactor = maximumScaleFactor
8189
self.isFixed = isFixed
8290
}
8391

@@ -94,6 +102,7 @@ public struct Typography {
94102
/// - textCase: text case to apply (defaults to `.none`)
95103
/// - textDecoration: text decoration to apply (defaults to `.none`)
96104
/// - textStyle: text style to use for scaling (defaults to `.body`)
105+
/// - maximumScaleFactor: maximum scale factor to apply (defaults to `nil`)
97106
/// - isFixed: `true` if this font should never scale, `false` if it should scale (defaults to `.false`)
98107
public init(
99108
familyName: String,
@@ -107,6 +116,7 @@ public struct Typography {
107116
textCase: TextCase = .none,
108117
textDecoration: TextDecoration = .none,
109118
textStyle: UIFont.TextStyle = .body,
119+
maximumScaleFactor: CGFloat? = nil,
110120
isFixed: Bool = false
111121
) {
112122
self.init(
@@ -120,6 +130,7 @@ public struct Typography {
120130
textCase: textCase,
121131
textDecoration: textDecoration,
122132
textStyle: textStyle,
133+
maximumScaleFactor: maximumScaleFactor,
123134
isFixed: isFixed
124135
)
125136
}

Tests/YMatterTypeTests/Typography/TypogaphyTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ final class TypogaphyTests: XCTestCase {
5050
// Confirm default init parameter values
5151
XCTAssertEqual(typography.letterSpacing, 0)
5252
XCTAssertEqual(typography.textStyle, UIFont.TextStyle.body)
53+
XCTAssertNil(typography.maximumScaleFactor)
5354
XCTAssertFalse(typography.isFixed)
5455
}
5556
}

Tests/YMatterTypeTests/Typography/Typography+FontTests.swift

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ final class TypographyFontTests: XCTestCase {
8787

8888
// The returned font should not exceed the requested maximum
8989
XCTAssertEqual(layout.font.pointSize, $0.fontSize * 2)
90-
// The returned line height should not exceed th requested maximum
90+
// The returned line height should not exceed the requested maximum
9191
XCTAssertEqual(layout.lineHeight, $0.lineHeight * 2)
9292
// baselineOffset should be >= 0 and pixel-aligned
9393
XCTAssertGreaterThanOrEqual(layout.baselineOffset, 0)
@@ -117,7 +117,37 @@ final class TypographyFontTests: XCTestCase {
117117

118118
// The returned font should not exceed the requested maximum
119119
XCTAssertEqual(layout.font.pointSize, size.fontSize * scaleFactor)
120-
// The returned line height should not exceed th requested maximum
120+
// The returned line height should not exceed the requested maximum
121+
XCTAssertEqual(layout.lineHeight, size.lineHeight * scaleFactor)
122+
// baselineOffset should be >= 0 and pixel-aligned
123+
XCTAssertGreaterThanOrEqual(layout.baselineOffset, 0)
124+
XCTAssertEqual(layout.baselineOffset.ceiled(), layout.baselineOffset)
125+
}
126+
}
127+
}
128+
129+
func testIntrinsicMaximumScaleFactor() {
130+
let fontFamily = DefaultFontFamily(familyName: "Menlo")
131+
let traits = UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)
132+
133+
sizes.forEach { size in
134+
scaleFactors.forEach { scaleFactor in
135+
// Given we build a typography with a built-in max scale factor
136+
let typography = Typography(
137+
fontFamily: fontFamily,
138+
fontWeight: .bold,
139+
fontSize: size.fontSize,
140+
lineHeight: size.lineHeight,
141+
maximumScaleFactor: scaleFactor
142+
)
143+
144+
// When we request a layout without specifying any further max
145+
// (at the largest supported Dynamic Type size)
146+
let layout = typography.generateLayout(compatibleWith: traits)
147+
148+
// The returned font should not exceed the requested maximum
149+
XCTAssertEqual(layout.font.pointSize, size.fontSize * scaleFactor)
150+
// The returned line height should not exceed the requested maximum
121151
XCTAssertEqual(layout.lineHeight, size.lineHeight * scaleFactor)
122152
// baselineOffset should be >= 0 and pixel-aligned
123153
XCTAssertGreaterThanOrEqual(layout.baselineOffset, 0)
@@ -157,6 +187,64 @@ final class TypographyFontTests: XCTestCase {
157187
}
158188
}
159189
#endif
190+
191+
func test_maximumPointSize() {
192+
// Given
193+
let factors: [CGFloat?] = [nil, 1.5, 2.0, 2.5]
194+
195+
factors.forEach {
196+
let pointSize = CGFloat(Int.random(in: 10...32))
197+
let sut = Typography(
198+
fontFamily: AppleSDGothicNeoInfo(),
199+
fontWeight: .bold,
200+
fontSize: pointSize,
201+
lineHeight: ceil(pointSize * 1.4),
202+
maximumScaleFactor: $0
203+
)
204+
205+
let maximumPointSize = sut.maximumPointSize
206+
if let factor = $0 {
207+
XCTAssertEqual(maximumPointSize, pointSize * factor)
208+
} else {
209+
XCTAssertNil(maximumPointSize)
210+
}
211+
}
212+
}
213+
214+
func test_getMaximumPointSize_withoutMaximumScaleFactor() {
215+
// Given
216+
let sut = Typography(
217+
fontFamily: AppleSDGothicNeoInfo(),
218+
fontWeight: .bold,
219+
fontSize: 12,
220+
lineHeight: 24
221+
)
222+
let maximumPointSize = CGFloat(Int.random(in: 16...48))
223+
224+
// Then
225+
XCTAssertNil(sut.maximumPointSize)
226+
XCTAssertNil(sut.getMaximumPointSize(nil))
227+
XCTAssertEqual(sut.getMaximumPointSize(maximumPointSize), maximumPointSize)
228+
}
229+
230+
func test_getMaximumPointSize_withMaximumScaleFactor() {
231+
// Given
232+
let sut = Typography(
233+
fontFamily: AppleSDGothicNeoInfo(),
234+
fontWeight: .bold,
235+
fontSize: 12,
236+
lineHeight: 24,
237+
maximumScaleFactor: 2
238+
)
239+
let lowerPointSize = CGFloat(Int.random(in: 13..<24))
240+
let higherPointSize = CGFloat(Int.random(in: 25...48))
241+
242+
// Then
243+
XCTAssertEqual(sut.maximumPointSize, 24)
244+
XCTAssertEqual(sut.getMaximumPointSize(nil), sut.maximumPointSize)
245+
XCTAssertEqual(sut.getMaximumPointSize(lowerPointSize), lowerPointSize)
246+
XCTAssertEqual(sut.getMaximumPointSize(higherPointSize), sut.maximumPointSize)
247+
}
160248
}
161249

162250
struct AppleSDGothicNeoInfo: FontFamily {

Tests/YMatterTypeTests/Typography/Typography+MutatorsTests.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ final class TypographyMutatorsTests: XCTestCase {
101101
}
102102
}
103103

104+
func testMaximumScaleFactor() {
105+
let factors: [CGFloat?] = [nil, 1.5, 2.0, 2.5]
106+
types.forEach {
107+
for factor in factors {
108+
_test(original: $0, modified: $0.maximumScaleFactor(factor), maximumScaleFactor: factor)
109+
}
110+
}
111+
}
112+
104113
private func _test(
105114
original: Typography,
106115
modified: Typography,
@@ -111,7 +120,8 @@ final class TypographyMutatorsTests: XCTestCase {
111120
isFixed: Bool? = nil,
112121
letterSpacing: CGFloat? = nil,
113122
textCase: Typography.TextCase? = nil,
114-
textDecoration: Typography.TextDecoration? = nil
123+
textDecoration: Typography.TextDecoration? = nil,
124+
maximumScaleFactor: CGFloat? = nil
115125
) {
116126
let familyName = familyName ?? original.fontFamily.familyName
117127
let weight = weight ?? original.fontWeight
@@ -121,6 +131,7 @@ final class TypographyMutatorsTests: XCTestCase {
121131
let kerning = letterSpacing ?? original.letterSpacing
122132
let textCase = textCase ?? original.textCase
123133
let textDecoration = textDecoration ?? original.textDecoration
134+
let maximumScaleFactor = maximumScaleFactor ?? original.maximumScaleFactor
124135

125136
// familyName, fontWeight, fontSize, lineHeight, isFixed,
126137
// letterSpacing, textCase, and textDecoration should be as expected
@@ -132,6 +143,7 @@ final class TypographyMutatorsTests: XCTestCase {
132143
XCTAssertEqual(modified.letterSpacing, kerning)
133144
XCTAssertEqual(modified.textCase, textCase)
134145
XCTAssertEqual(modified.textDecoration, textDecoration)
146+
XCTAssertEqual(modified.maximumScaleFactor, maximumScaleFactor)
135147

136148
// the other variables should be the same
137149
XCTAssertEqual(modified.textStyle, original.textStyle)

0 commit comments

Comments
 (0)