Skip to content

Commit

Permalink
[Issue 76] Add maximumScaleFactor to Typography
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Pospesel committed Mar 29, 2023
1 parent 08a0d4d commit a917f9e
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 4 deletions.
28 changes: 27 additions & 1 deletion Sources/YMatterType/Typography/Typography+Font.swift
Expand Up @@ -27,7 +27,7 @@ extension Typography {
if !isFixed {
let metrics = UIFontMetrics(forTextStyle: textStyle)

if let maximumPointSize = maximumPointSize {
if let maximumPointSize = getMaximumPointSize(maximumPointSize) {
font = metrics.scaledFont(
for: font,
maximumPointSize: maximumPointSize,
Expand Down Expand Up @@ -132,6 +132,32 @@ extension Typography {

return generateLayout(maximumPointSize: maximumPointSize, compatibleWith: traitCollection)
}

/// Maximum point size (if any).
///
/// Calculated from `maximumScaleFactor` (if any) multiplied by `fontSize` or else `nil`
public var maximumPointSize: CGFloat? {
guard let maximumScaleFactor = maximumScaleFactor else {
return nil
}

return maximumScaleFactor * fontSize
}

/// Returns the minimum of the point size (if any) or `maximumPointSize` (if any).
/// - Parameter pointSize: optional point size to evaluate
/// - Returns: the minimum of point size or maximumPointSize
internal func getMaximumPointSize(_ pointSize: CGFloat?) -> CGFloat? {
guard let maximumPointSize = maximumPointSize else {
return pointSize
}

guard let pointSize = pointSize else {
return maximumPointSize
}

return min(pointSize, maximumPointSize)
}
}

private extension Typography {
Expand Down
30 changes: 30 additions & 0 deletions Sources/YMatterType/Typography/Typography+Mutators.swift
Expand Up @@ -27,6 +27,7 @@ public extension Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand Down Expand Up @@ -58,6 +59,7 @@ public extension Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand All @@ -79,6 +81,7 @@ public extension Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand All @@ -100,6 +103,7 @@ public extension Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand All @@ -119,6 +123,7 @@ public extension Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: true
)
}
Expand All @@ -140,6 +145,7 @@ public extension Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand All @@ -161,6 +167,7 @@ public extension Typography {
textCase: value,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand All @@ -182,6 +189,29 @@ public extension Typography {
textCase: textCase,
textDecoration: value,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}

/// Returns a copy of the Typography but with the new `maximumScaleFactor` applied.
/// - Parameter value: the maximum scale factor to apply
/// - Returns: an updated copy of the Typography
func maximumScaleFactor(_ value: CGFloat?) -> Typography {
if maximumScaleFactor == value { return self }

return Typography(
fontFamily: fontFamily,
fontWeight: fontWeight,
fontSize: fontSize,
lineHeight: lineHeight,
letterSpacing: letterSpacing,
paragraphIndent: paragraphIndent,
paragraphSpacing: paragraphSpacing,
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: value,
isFixed: isFixed
)
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/YMatterType/Typography/Typography.swift
Expand Up @@ -31,6 +31,11 @@ public struct Typography {
/// The text style (e.g. Body or Title) that this font most closely represents.
/// Used for Dynamic Type scaling of the font
public let textStyle: UIFont.TextStyle
/// Maximum scale factor to apply for this typography. `nil` means no limit.
///
/// Will not be considered if `isFixed == true`.
/// Do not set to `1.0`, but set `isFixed = true` to disable Dynamic Type scaling.
public let maximumScaleFactor: CGFloat?
/// Whether this font is fixed in size or should be scaled through Dynamic Type
public let isFixed: Bool

Expand All @@ -54,6 +59,7 @@ public struct Typography {
/// - textCase: text case to apply (defaults to `.none`)
/// - textDecoration: text decoration to apply (defaults to `.none`)
/// - textStyle: text style to use for scaling (defaults to `.body`)
/// - maximumScaleFactor: maximum scale factor to apply (defaults to `nil`)
/// - isFixed: `true` if this font should never scale, `false` if it should scale (defaults to `.false`)
public init(
fontFamily: FontFamily,
Expand All @@ -66,6 +72,7 @@ public struct Typography {
textCase: TextCase = .none,
textDecoration: TextDecoration = .none,
textStyle: UIFont.TextStyle = .body,
maximumScaleFactor: CGFloat? = nil,
isFixed: Bool = false
) {
self.fontFamily = fontFamily
Expand All @@ -78,6 +85,7 @@ public struct Typography {
self.textCase = textCase
self.textDecoration = textDecoration
self.textStyle = textStyle
self.maximumScaleFactor = maximumScaleFactor
self.isFixed = isFixed
}

Expand All @@ -94,6 +102,7 @@ public struct Typography {
/// - textCase: text case to apply (defaults to `.none`)
/// - textDecoration: text decoration to apply (defaults to `.none`)
/// - textStyle: text style to use for scaling (defaults to `.body`)
/// - maximumScaleFactor: maximum scale factor to apply (defaults to `nil`)
/// - isFixed: `true` if this font should never scale, `false` if it should scale (defaults to `.false`)
public init(
familyName: String,
Expand All @@ -107,6 +116,7 @@ public struct Typography {
textCase: TextCase = .none,
textDecoration: TextDecoration = .none,
textStyle: UIFont.TextStyle = .body,
maximumScaleFactor: CGFloat? = nil,
isFixed: Bool = false
) {
self.init(
Expand All @@ -120,6 +130,7 @@ public struct Typography {
textCase: textCase,
textDecoration: textDecoration,
textStyle: textStyle,
maximumScaleFactor: maximumScaleFactor,
isFixed: isFixed
)
}
Expand Down
92 changes: 90 additions & 2 deletions Tests/YMatterTypeTests/Typography/Typography+FontTests.swift
Expand Up @@ -87,7 +87,7 @@ final class TypographyFontTests: XCTestCase {

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

// The returned font should not exceed the requested maximum
XCTAssertEqual(layout.font.pointSize, size.fontSize * scaleFactor)
// The returned line height should not exceed th requested maximum
// The returned line height should not exceed the requested maximum
XCTAssertEqual(layout.lineHeight, size.lineHeight * scaleFactor)
// baselineOffset should be >= 0 and pixel-aligned
XCTAssertGreaterThanOrEqual(layout.baselineOffset, 0)
XCTAssertEqual(layout.baselineOffset.ceiled(), layout.baselineOffset)
}
}
}

func testIntrinsicMaximumScaleFactor() {
let fontFamily = DefaultFontFamily(familyName: "Menlo")
let traits = UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)

sizes.forEach { size in
scaleFactors.forEach { scaleFactor in
// Given we build a typography with a built-in max scale factor
let typography = Typography(
fontFamily: fontFamily,
fontWeight: .bold,
fontSize: size.fontSize,
lineHeight: size.lineHeight,
maximumScaleFactor: scaleFactor
)

// When we request a layout without specifying any further max
// (at the largest supported Dynamic Type size)
let layout = typography.generateLayout(compatibleWith: traits)

// The returned font should not exceed the requested maximum
XCTAssertEqual(layout.font.pointSize, size.fontSize * scaleFactor)
// The returned line height should not exceed the requested maximum
XCTAssertEqual(layout.lineHeight, size.lineHeight * scaleFactor)
// baselineOffset should be >= 0 and pixel-aligned
XCTAssertGreaterThanOrEqual(layout.baselineOffset, 0)
Expand Down Expand Up @@ -157,6 +187,64 @@ final class TypographyFontTests: XCTestCase {
}
}
#endif

func test_maximumPointSize() {
// Given
let factors: [CGFloat?] = [nil, 1.5, 2.0, 2.5]

factors.forEach {
let pointSize = CGFloat(Int.random(in: 10...32))
let sut = Typography(
fontFamily: AppleSDGothicNeoInfo(),
fontWeight: .bold,
fontSize: pointSize,
lineHeight: ceil(pointSize * 1.4),
maximumScaleFactor: $0
)

let maximumPointSize = sut.maximumPointSize
if let factor = $0 {
XCTAssertEqual(maximumPointSize, pointSize * factor)
} else {
XCTAssertNil(maximumPointSize)
}
}
}

func test_getMaximumPointSize_withoutMaximumScaleFactor() {
// Given
let sut = Typography(
fontFamily: AppleSDGothicNeoInfo(),
fontWeight: .bold,
fontSize: 12,
lineHeight: 24
)
let maximumPointSize = CGFloat(Int.random(in: 16...48))

// Then
XCTAssertNil(sut.maximumPointSize)
XCTAssertNil(sut.getMaximumPointSize(nil))
XCTAssertEqual(sut.getMaximumPointSize(maximumPointSize), maximumPointSize)
}

func test_getMaximumPointSize_withMaximumScaleFactor() {
// Given
let sut = Typography(
fontFamily: AppleSDGothicNeoInfo(),
fontWeight: .bold,
fontSize: 12,
lineHeight: 24,
maximumScaleFactor: 2
)
let lowerPointSize = CGFloat(Int.random(in: 13..<24))
let higherPointSize = CGFloat(Int.random(in: 25...48))

// Then
XCTAssertEqual(sut.maximumPointSize, 24)
XCTAssertEqual(sut.getMaximumPointSize(nil), sut.maximumPointSize)
XCTAssertEqual(sut.getMaximumPointSize(lowerPointSize), lowerPointSize)
XCTAssertEqual(sut.getMaximumPointSize(higherPointSize), sut.maximumPointSize)
}
}

struct AppleSDGothicNeoInfo: FontFamily {
Expand Down
14 changes: 13 additions & 1 deletion Tests/YMatterTypeTests/Typography/Typography+MutatorsTests.swift
Expand Up @@ -101,6 +101,15 @@ final class TypographyMutatorsTests: XCTestCase {
}
}

func testMaximumScaleFactor() {
let factors: [CGFloat?] = [nil, 1.5, 2.0, 2.5]
types.forEach {
for factor in factors {
_test(original: $0, modified: $0.maximumScaleFactor(factor), maximumScaleFactor: factor)
}
}
}

private func _test(
original: Typography,
modified: Typography,
Expand All @@ -111,7 +120,8 @@ final class TypographyMutatorsTests: XCTestCase {
isFixed: Bool? = nil,
letterSpacing: CGFloat? = nil,
textCase: Typography.TextCase? = nil,
textDecoration: Typography.TextDecoration? = nil
textDecoration: Typography.TextDecoration? = nil,
maximumScaleFactor: CGFloat? = nil
) {
let familyName = familyName ?? original.fontFamily.familyName
let weight = weight ?? original.fontWeight
Expand All @@ -121,6 +131,7 @@ final class TypographyMutatorsTests: XCTestCase {
let kerning = letterSpacing ?? original.letterSpacing
let textCase = textCase ?? original.textCase
let textDecoration = textDecoration ?? original.textDecoration
let maximumScaleFactor = maximumScaleFactor ?? original.maximumScaleFactor

// familyName, fontWeight, fontSize, lineHeight, isFixed,
// letterSpacing, textCase, and textDecoration should be as expected
Expand All @@ -132,6 +143,7 @@ final class TypographyMutatorsTests: XCTestCase {
XCTAssertEqual(modified.letterSpacing, kerning)
XCTAssertEqual(modified.textCase, textCase)
XCTAssertEqual(modified.textDecoration, textDecoration)
XCTAssertEqual(modified.maximumScaleFactor, maximumScaleFactor)

// the other variables should be the same
XCTAssertEqual(modified.textStyle, original.textStyle)
Expand Down

0 comments on commit a917f9e

Please sign in to comment.