Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue 76] Add maximumScaleFactor to Typography #81

Merged
merged 1 commit into from Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
1 change: 1 addition & 0 deletions Tests/YMatterTypeTests/Typography/TypogaphyTests.swift
Expand Up @@ -50,6 +50,7 @@ final class TypogaphyTests: XCTestCase {
// Confirm default init parameter values
XCTAssertEqual(typography.letterSpacing, 0)
XCTAssertEqual(typography.textStyle, UIFont.TextStyle.body)
XCTAssertNil(typography.maximumScaleFactor)
XCTAssertFalse(typography.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