vds_ios/VDS/Typography/Typography.swift
Matt Bruce 3f6e450739 added standard style
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2023-06-29 12:05:42 -05:00

447 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 lineSpacing: CGFloat
public let baselineOffset: CGFloat
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.lineSpacing = style.lineSpacing
self.baselineOffset = style.baselineOffset
}
public init(rawValue: String, fontFace: Font, pointSize: CGFloat = 0.0, lineHeight: CGFloat = 0.0, letterSpacing: CGFloat = 0.0, lineSpacing: CGFloat = 0.0, baselineOffset: CGFloat = 0.0) {
self.rawValue = rawValue
self.fontFace = fontFace
self.pointSize = pointSize
self.lineHeight = lineHeight
self.letterSpacing = letterSpacing
self.lineSpacing = lineSpacing
self.baselineOffset = baselineOffset
}
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
}
}