refactor the string helper to remove these tests Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
444 lines
22 KiB
Swift
444 lines
22 KiB
Swift
//
|
|
// Typography.swift
|
|
// VDS
|
|
//
|
|
// Created by Matt Bruce on 8/16/22.
|
|
//
|
|
|
|
import Foundation
|
|
import VDSTypographyTokens
|
|
|
|
public enum TextPosition: String, CaseIterable {
|
|
case left, right, center
|
|
|
|
var textAlignment: NSTextAlignment {
|
|
switch self {
|
|
case .left:
|
|
return NSTextAlignment.left
|
|
case .right:
|
|
return NSTextAlignment.right
|
|
case .center:
|
|
return NSTextAlignment.center
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct TextStyle: Equatable, RawRepresentable {
|
|
public let rawValue: String
|
|
public let pointSize: CGFloat
|
|
public let lineHeight: CGFloat
|
|
public let letterSpacing: CGFloat
|
|
public let fontFace: Font
|
|
public let edgeInsets: UIEdgeInsets
|
|
|
|
public init?(rawValue: String) {
|
|
guard let style = TextStyle.textStyle(for: rawValue) else { return nil }
|
|
self.rawValue = style.rawValue
|
|
self.pointSize = style.pointSize
|
|
self.lineHeight = style.lineHeight
|
|
self.letterSpacing = style.letterSpacing
|
|
self.fontFace = style.fontFace
|
|
self.edgeInsets = style.edgeInsets
|
|
}
|
|
|
|
public init(rawValue: String, fontFace: Font, pointSize: CGFloat = 0.0, lineHeight: CGFloat = 0.0, letterSpacing: CGFloat = 0.0, edgeInsets: UIEdgeInsets = .zero) {
|
|
self.rawValue = rawValue
|
|
self.fontFace = fontFace
|
|
self.pointSize = pointSize
|
|
self.lineHeight = lineHeight
|
|
self.letterSpacing = letterSpacing
|
|
self.edgeInsets = edgeInsets
|
|
}
|
|
|
|
public var isBold: Bool {
|
|
rawValue.hasPrefix("bold")
|
|
}
|
|
}
|
|
|
|
extension VDSTypography {
|
|
public static let letterSpacingSemiWide: CGFloat = 0.25
|
|
}
|
|
//MARK: Definitions
|
|
extension TextStyle {
|
|
|
|
// Static properties for different text styles
|
|
public static let featureXLarge = TextStyle(rawValue: "featureXLarge",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature144 : VDSTypography.fontSizeFeature96,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature136 : VDSTypography.lineHeightFeature88,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldFeatureXLarge = TextStyle(rawValue: "boldFeatureXLarge",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature144 : VDSTypography.fontSizeFeature96,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature136 : VDSTypography.lineHeightFeature88,
|
|
letterSpacing: 0)
|
|
|
|
public static let featureLarge = TextStyle(rawValue: "featureLarge",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature128 : VDSTypography.fontSizeFeature80,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature120 : VDSTypography.lineHeightFeature76,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldFeatureLarge = TextStyle(rawValue: "boldFeatureLarge",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature128 : VDSTypography.fontSizeFeature80,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature120 : VDSTypography.lineHeightFeature76,
|
|
letterSpacing: 0)
|
|
|
|
public static let featureMedium = TextStyle(rawValue: "featureMedium",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature96 : VDSTypography.fontSizeFeature64,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature88 : VDSTypography.lineHeightFeature64,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldFeatureMedium = TextStyle(rawValue: "boldFeatureMedium",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature96 : VDSTypography.fontSizeFeature64,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature88 : VDSTypography.lineHeightFeature64,
|
|
letterSpacing: 0)
|
|
|
|
public static let featureSmall = TextStyle(rawValue: "featureSmall",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature80 : VDSTypography.fontSizeFeature48,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature76 : VDSTypography.lineHeightFeature48,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldFeatureSmall = TextStyle(rawValue: "boldFeatureSmall",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature80 : VDSTypography.fontSizeFeature48,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature76 : VDSTypography.lineHeightFeature48,
|
|
letterSpacing: 0)
|
|
|
|
public static let featureXSmall = TextStyle(rawValue: "featureXSmall",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature64 : VDSTypography.fontSizeFeature40,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature64 : VDSTypography.lineHeightFeature40,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldFeatureXSmall = TextStyle(rawValue: "boldFeatureXSmall",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeFeature64 : VDSTypography.fontSizeFeature40,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightFeature64 : VDSTypography.lineHeightFeature40,
|
|
letterSpacing: 0)
|
|
|
|
public static let title2XLarge = TextStyle(rawValue: "title2XLarge",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle64 : VDSTypography.fontSizeTitle40,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle64 : VDSTypography.lineHeightTitle40,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldTitle2XLarge = TextStyle(rawValue: "boldTitle2XLarge",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle64 : VDSTypography.fontSizeTitle40,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle64 : VDSTypography.lineHeightTitle40,
|
|
letterSpacing: 0)
|
|
|
|
public static let titleXLarge = TextStyle(rawValue: "titleXLarge",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle48 : VDSTypography.fontSizeTitle32,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle48 : VDSTypography.lineHeightTitle36,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldTitleXLarge = TextStyle(rawValue: "boldTitleXLarge",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle48 : VDSTypography.fontSizeTitle32,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle48 : VDSTypography.lineHeightTitle36,
|
|
letterSpacing: 0)
|
|
|
|
public static let titleLarge = TextStyle(rawValue: "titleLarge",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle32 : VDSTypography.fontSizeTitle24,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle36 : VDSTypography.lineHeightTitle28,
|
|
letterSpacing: VDSTypography.letterSpacingSemiWide)
|
|
|
|
public static let boldTitleLarge = TextStyle(rawValue: "boldTitleLarge",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle32 : VDSTypography.fontSizeTitle24,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle36 : VDSTypography.lineHeightTitle28,
|
|
letterSpacing: 0)
|
|
|
|
public static let titleMedium = TextStyle(rawValue: "titleMedium",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle24 : VDSTypography.fontSizeTitle20,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle28 : VDSTypography.lineHeightTitle24,
|
|
letterSpacing: 0)
|
|
|
|
public static let boldTitleMedium = TextStyle(rawValue: "boldTitleMedium",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle24 : VDSTypography.fontSizeTitle20,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle28 : VDSTypography.lineHeightTitle24,
|
|
letterSpacing: 0)
|
|
|
|
public static let titleSmall = TextStyle(rawValue: "titleSmall",
|
|
fontFace: .dsLight,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle20 : VDSTypography.fontSizeTitle16,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle24 : VDSTypography.lineHeightTitle20,
|
|
letterSpacing: 0)
|
|
|
|
public static let boldTitleSmall = TextStyle(rawValue: "boldTitleSmall",
|
|
fontFace: .dsBold,
|
|
pointSize: UIDevice.isIPad ? VDSTypography.fontSizeTitle20 : VDSTypography.fontSizeTitle16,
|
|
lineHeight: UIDevice.isIPad ? VDSTypography.lineHeightTitle24 : VDSTypography.lineHeightTitle20,
|
|
letterSpacing: 0)
|
|
|
|
public static let bodyLarge = TextStyle(rawValue: "bodyLarge",
|
|
fontFace: .dsRegular,
|
|
pointSize: VDSTypography.fontSizeBody16,
|
|
lineHeight: VDSTypography.lineHeightBody20,
|
|
letterSpacing:VDSTypography.letterSpacingWide)
|
|
|
|
public static let boldBodyLarge = TextStyle(rawValue: "boldBodyLarge",
|
|
fontFace: .dsBold,
|
|
pointSize: VDSTypography.fontSizeBody16,
|
|
lineHeight: VDSTypography.lineHeightBody20,
|
|
letterSpacing: VDSTypography.letterSpacingWide)
|
|
|
|
public static let bodyMedium = TextStyle(rawValue: "bodyMedium",
|
|
fontFace: .dsRegular,
|
|
pointSize: VDSTypography.fontSizeBody14,
|
|
lineHeight: VDSTypography.lineHeightBody18,
|
|
letterSpacing: VDSTypography.letterSpacingWide)
|
|
|
|
public static let boldBodyMedium = TextStyle(rawValue: "boldBodyMedium",
|
|
fontFace: .dsBold,
|
|
pointSize: VDSTypography.fontSizeBody14,
|
|
lineHeight: VDSTypography.lineHeightBody18,
|
|
letterSpacing: VDSTypography.letterSpacingWide)
|
|
|
|
public static let bodySmall = TextStyle(rawValue: "bodySmall",
|
|
fontFace: .dsRegular,
|
|
pointSize: VDSTypography.fontSizeBody12,
|
|
lineHeight: VDSTypography.lineHeightBody16,
|
|
letterSpacing: 0)
|
|
|
|
public static let boldBodySmall = TextStyle(rawValue: "boldBodySmall",
|
|
fontFace: .dsBold,
|
|
pointSize: VDSTypography.fontSizeBody12,
|
|
lineHeight: VDSTypography.lineHeightBody16,
|
|
letterSpacing: 0)
|
|
|
|
public static let micro = TextStyle(rawValue: "micro",
|
|
fontFace: .dsRegular,
|
|
pointSize: VDSTypography.fontSizeMicro11,
|
|
lineHeight: VDSTypography.lineHeightMicro16,
|
|
letterSpacing: 0)
|
|
|
|
public static let boldMicro = TextStyle(rawValue: "boldMicro",
|
|
fontFace: .dsBold,
|
|
pointSize: VDSTypography.fontSizeMicro11,
|
|
lineHeight: VDSTypography.lineHeightMicro16,
|
|
letterSpacing: 0)
|
|
|
|
public static var allCases: [TextStyle] {
|
|
return [
|
|
featureXLarge,
|
|
boldFeatureXLarge,
|
|
featureLarge,
|
|
boldFeatureLarge,
|
|
featureMedium,
|
|
boldFeatureMedium,
|
|
featureSmall,
|
|
boldFeatureSmall,
|
|
featureXSmall,
|
|
boldFeatureXSmall,
|
|
title2XLarge,
|
|
boldTitle2XLarge,
|
|
titleXLarge,
|
|
boldTitleXLarge,
|
|
titleLarge,
|
|
boldTitleLarge,
|
|
titleMedium,
|
|
boldTitleMedium,
|
|
titleSmall,
|
|
boldTitleSmall,
|
|
bodyLarge,
|
|
boldBodyLarge,
|
|
bodyMedium,
|
|
boldBodyMedium,
|
|
bodySmall,
|
|
boldBodySmall,
|
|
micro,
|
|
boldMicro
|
|
]
|
|
}
|
|
}
|
|
|
|
extension TextStyle {
|
|
public enum StandardStyle: String, CaseIterable {
|
|
case featureXLarge,
|
|
featureLarge,
|
|
featureMedium,
|
|
featureSmall,
|
|
featureXSmall,
|
|
title2XLarge,
|
|
titleXLarge,
|
|
titleLarge,
|
|
titleMedium,
|
|
titleSmall,
|
|
bodyLarge,
|
|
bodyMedium,
|
|
bodySmall,
|
|
micro
|
|
|
|
public var bold: TextStyle {
|
|
return TextStyle(rawValue: "bold\(rawValue.prefix(1).uppercased())\(rawValue.dropFirst())")!
|
|
}
|
|
|
|
public var regular: TextStyle {
|
|
TextStyle(rawValue: rawValue)!
|
|
}
|
|
}
|
|
|
|
public func toStandardStyle() -> StandardStyle {
|
|
var rawName = rawValue
|
|
if rawName.hasPrefix("bold") {
|
|
let updatedRaw = rawName.replacingOccurrences(of: "bold", with: "")
|
|
rawName = updatedRaw.prefix(1).lowercased() + updatedRaw.dropFirst()
|
|
}
|
|
return StandardStyle(rawValue: rawName)!
|
|
}
|
|
}
|
|
|
|
//MARK: FontCategory
|
|
extension TextStyle {
|
|
public enum FontCategory: String, CaseIterable {
|
|
case feature
|
|
case title
|
|
case body
|
|
case micro
|
|
|
|
public var sizes: [FontSize] {
|
|
switch self {
|
|
case .feature:
|
|
return [.xlarge, .large, .medium, .small, .xsmall]
|
|
case .title:
|
|
return [.xxlarge, .xlarge, .large, .medium, .small]
|
|
case .body:
|
|
return [.large, .medium, .small]
|
|
case .micro:
|
|
return []
|
|
}
|
|
}
|
|
|
|
public func style(for fontSize: FontSize?, isBold: Bool = false) -> TextStyle? {
|
|
var styleName = ""
|
|
if isBold {
|
|
let newRaw = rawValue.prefix(1).description.uppercased() + rawValue.dropFirst()
|
|
styleName = "\(isBold ? "bold" : "")\(newRaw)\(fontSize?.rawValue ?? "")"
|
|
} else {
|
|
styleName = "\(rawValue)\(fontSize?.rawValue ?? "")"
|
|
}
|
|
|
|
guard let style = TextStyle.textStyle(for: styleName) else {
|
|
return nil
|
|
}
|
|
return style
|
|
}
|
|
}
|
|
}
|
|
|
|
//MARK: FontSize
|
|
extension TextStyle {
|
|
public enum FontSize: String, CaseIterable {
|
|
case xxlarge = "2XLarge"
|
|
case xlarge = "XLarge"
|
|
case large = "Large"
|
|
case medium = "Medium"
|
|
case small = "Small"
|
|
case xsmall = "XSmall"
|
|
}
|
|
}
|
|
|
|
//MARK: Alignments
|
|
extension TextStyle {
|
|
public var aligments: [TextPosition] {
|
|
return [.left, .center]
|
|
}
|
|
}
|
|
|
|
//MARK: Fonts
|
|
extension TextStyle {
|
|
public var font: UIFont {
|
|
return fontFace.font(ofSize: pointSize)
|
|
}
|
|
}
|
|
|
|
extension TextStyle {
|
|
public static func style(for fontName: String, size: CGFloat) -> TextStyle? {
|
|
//filter all styles by fontName
|
|
let styles = allCases.filter{$0.fontFace.fontName == fontName }.sorted { lhs, rhs in lhs.pointSize < rhs.pointSize }
|
|
|
|
//if there are no styles then return nil
|
|
guard styles.count > 0 else { return nil }
|
|
|
|
//if there is an exact match on a style with this pointSize then return it
|
|
if let style = styles.first(where: {$0.pointSize == size }) {
|
|
return style
|
|
|
|
} else if let largerIndex = styles.firstIndex(where: { $0.pointSize > size}) { //find the closet one to pointSize
|
|
return styles[max(largerIndex - 1, 0)]
|
|
|
|
} else { //return the last style
|
|
return styles.last!
|
|
}
|
|
}
|
|
}
|
|
|
|
extension RawRepresentable where Self.RawValue: Equatable {
|
|
public func isWithin(_ collection: [Self]) -> Bool {
|
|
(collection.first(where: {$0 == self}) != nil)
|
|
}
|
|
}
|
|
|
|
extension TextStyle {
|
|
public struct SpacingConfig {
|
|
public var defaultSpacing: CGFloat = 8.0
|
|
public var configs: [TextStyle.DeviceSpacingConfig]
|
|
|
|
public func spacing(for style: TextStyle, neighboring: TextStyle) -> CGFloat {
|
|
let deviceType: TextStyle.DeviceSpacingConfig.DeviceType = UIDevice.isIPad ? .iPad : .iPhone
|
|
if let config = configs.first(where:
|
|
{ style.isWithin($0.primaryStyles) && neighboring.isWithin($0.neighboringStyles) &&
|
|
($0.deviceType == deviceType || $0.deviceType == .all )})
|
|
{
|
|
return config.spacing
|
|
}
|
|
return defaultSpacing
|
|
}
|
|
}
|
|
|
|
public struct DeviceSpacingConfig {
|
|
public enum DeviceType {
|
|
case iPhone, iPad, all
|
|
}
|
|
public var spacing: CGFloat
|
|
public var deviceType: DeviceType = .iPhone
|
|
public var primaryStyles: [TextStyle]
|
|
public var neighboringStyles: [TextStyle]
|
|
|
|
public init(_ primaryStyles: [TextStyle], neighboring: [TextStyle], spacing: CGFloat, deviceType: DeviceType = .iPhone) {
|
|
self.spacing = spacing
|
|
self.primaryStyles = primaryStyles
|
|
self.neighboringStyles = neighboring
|
|
self.deviceType = deviceType
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension TextStyle: CustomDebugStringConvertible {
|
|
public var debugDescription: String {
|
|
"Name: \(self.rawValue) FontFace: \(font.fontName) FontWeight: \(self.rawValue.hasPrefix("bold") ? "bold" : "normal") PointSize: \(font.pointSize) LetterSpacing: \(letterSpacing) LineHeight: \(lineHeight)"
|
|
}
|
|
}
|
|
|
|
extension TextStyle {
|
|
public static var defaultStyle: TextStyle { return bodyLarge }
|
|
|
|
public static func textStyle(for name: String) -> TextStyle? {
|
|
guard let style = TextStyle.allCases.first(where: {$0.rawValue == name }) else { return nil }
|
|
return style
|
|
}
|
|
}
|