// // 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 enum TextStyle: String, CaseIterable { case featureXLarge case boldFeatureXLarge case featureLarge case boldFeatureLarge case featureMedium case boldFeatureMedium case featureSmall case boldFeatureSmall case featureXSmall case boldFeatureXSmall case title2XLarge case boldTitle2XLarge case titleXLarge case boldTitleXLarge case titleLarge case boldTitleLarge case titleMedium case boldTitleMedium case titleSmall case boldTitleSmall case bodyLarge case boldBodyLarge case bodyMedium case boldBodyMedium case bodySmall case boldBodySmall case micro case boldMicro public static var defaultStyle: TextStyle { return .bodyLarge } } //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(rawValue: 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: PointSize extension TextStyle { public var pointSize: CGFloat { switch self { case .featureXLarge, .boldFeatureXLarge: return UIDevice.isIPad ? VDSTypography.fontSizeFeature144 : VDSTypography.fontSizeFeature96 case .featureLarge, .boldFeatureLarge: return UIDevice.isIPad ? VDSTypography.fontSizeFeature128 : VDSTypography.fontSizeFeature80 case .featureMedium, .boldFeatureMedium: return UIDevice.isIPad ? VDSTypography.fontSizeFeature96 : VDSTypography.fontSizeFeature64 case .featureSmall, .boldFeatureSmall: return UIDevice.isIPad ? VDSTypography.fontSizeFeature80 : VDSTypography.fontSizeFeature48 case .featureXSmall, .boldFeatureXSmall: return UIDevice.isIPad ? VDSTypography.fontSizeFeature64 : VDSTypography.fontSizeFeature40 case .title2XLarge, .boldTitle2XLarge: return UIDevice.isIPad ? VDSTypography.fontSizeTitle64 : VDSTypography.fontSizeTitle40 case .titleXLarge, .boldTitleXLarge: return UIDevice.isIPad ? VDSTypography.fontSizeTitle48 : VDSTypography.fontSizeTitle32 case .titleLarge, .boldTitleLarge: return UIDevice.isIPad ? VDSTypography.fontSizeTitle32 : VDSTypography.fontSizeTitle24 case .titleMedium, .boldTitleMedium: return UIDevice.isIPad ? VDSTypography.fontSizeTitle24 : VDSTypography.fontSizeTitle20 case .titleSmall, .boldTitleSmall: return UIDevice.isIPad ? VDSTypography.fontSizeTitle20 : VDSTypography.fontSizeTitle16 case .bodyLarge, .boldBodyLarge: return VDSTypography.fontSizeBody16 case .bodyMedium, .boldBodyMedium: return VDSTypography.fontSizeBody14 case .bodySmall, .boldBodySmall: return VDSTypography.fontSizeBody12 case .micro, .boldMicro: return VDSTypography.fontSizeMicro11 } } } //MARK: LineHeight extension TextStyle { public var lineHeight: CGFloat { switch self { case .featureXLarge, .boldFeatureXLarge: return UIDevice.isIPad ? VDSTypography.lineHeightFeature136 : VDSTypography.lineHeightFeature88 case .featureLarge, .boldFeatureLarge: return UIDevice.isIPad ? VDSTypography.lineHeightFeature120 : VDSTypography.lineHeightFeature76 case .featureMedium, .boldFeatureMedium: return UIDevice.isIPad ? VDSTypography.lineHeightFeature88 : VDSTypography.lineHeightFeature64 case .featureSmall, .boldFeatureSmall: return UIDevice.isIPad ? VDSTypography.lineHeightFeature76 : VDSTypography.lineHeightFeature48 case .featureXSmall, .boldFeatureXSmall: return UIDevice.isIPad ? VDSTypography.lineHeightFeature64 : VDSTypography.lineHeightFeature40 case .title2XLarge, .boldTitle2XLarge: return UIDevice.isIPad ? VDSTypography.lineHeightTitle64 : VDSTypography.lineHeightTitle40 case .titleXLarge, .boldTitleXLarge: return UIDevice.isIPad ? VDSTypography.lineHeightTitle48 : VDSTypography.lineHeightTitle36 case .titleLarge, .boldTitleLarge: return UIDevice.isIPad ? VDSTypography.lineHeightTitle36 : VDSTypography.lineHeightTitle28 case .titleMedium, .boldTitleMedium: return UIDevice.isIPad ? VDSTypography.lineHeightTitle28 : VDSTypography.lineHeightTitle24 case .titleSmall, .boldTitleSmall: return UIDevice.isIPad ? VDSTypography.lineHeightTitle24 : VDSTypography.lineHeightTitle20 case .bodyLarge, .boldBodyLarge: return VDSTypography.lineHeightBody20 case .bodyMedium, .boldBodyMedium: return VDSTypography.lineHeightBody18 case .bodySmall, .boldBodySmall: return VDSTypography.lineHeightBody16 case .micro, .boldMicro: return VDSTypography.lineHeightMicro16 } } } //MARK: LetterSpacing extension TextStyle { public var letterSpacing: CGFloat { switch self { case .featureXLarge, .featureLarge, .featureMedium, .featureSmall, .featureXSmall, .title2XLarge, .titleXLarge, .titleLarge: return 0.25 case .boldBodyLarge, .bodyLarge, .boldBodyMedium, .bodyMedium: return 0.5 default: return 0.0 } } } //MARK: Alignments extension TextStyle { public var aligments: [TextPosition] { return [.left, .center] } } //MARK: Fonts extension TextStyle { public var fontFace: Fonts { switch self { case .boldFeatureXLarge, .boldFeatureLarge, .boldFeatureMedium, .boldFeatureSmall, .boldFeatureXSmall, .boldTitle2XLarge, .boldTitleXLarge, .boldTitleLarge, .boldTitleMedium, .boldTitleSmall, .boldBodyLarge, .boldBodyMedium: return .dsBold case .featureXLarge, .featureLarge, .featureMedium, .featureSmall, .featureXSmall, .title2XLarge, .titleXLarge: return .dsLight case .titleLarge, .titleMedium, .titleSmall, .bodyLarge, .bodyMedium: return .dsRegular case .boldBodySmall, .boldMicro: return .txBold case .bodySmall, .micro: return .txRegular } } public var font: UIFont { return fontFace.font(ofSize: pointSize) } public var superScriptFont: UIFont { return fontFace.font(ofSize: pointSize / 2) } } 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 TextStyle { public func isWithin(_ collection: [TextStyle]) -> 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 } } }