332 lines
11 KiB
Swift
332 lines
11 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 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
|
|
}
|
|
}
|
|
|
|
}
|