// // 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 } }