Font+Taler.swift (16084B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 10 // Use enums for multiple font types and functions for the set custom font. 11 12 fileprivate let ATKINSON = "AtkinsonHyperlegibleNext-" 13 fileprivate let NUNITO = "Nunito-" 14 15 fileprivate let REGULAR = "Regular" 16 fileprivate let ITALIC = "Italic" 17 fileprivate let BOLD = "Bold" 18 fileprivate let BOLDITALIC = "BoldItalic" 19 fileprivate let BLACK = "Black" 20 fileprivate let BLACKITALIC = "BlackItalic" 21 22 public extension Font { 23 static func logo(_ name: String, size: CGFloat, weight: UIFont.Weight) -> Font { 24 let traits = [UIFontDescriptor.TraitKey.weight: weight] // regular, medium, semibold, bold 25 let fontDescriptor = UIFontDescriptor().withFamily(name) 26 .addingAttributes([UIFontDescriptor.AttributeName.traits: traits]) 27 return Font(UIFont(descriptor: fontDescriptor, size: size)) 28 } 29 } 30 31 extension UIFont { 32 /// scalable system font for style and weight (and italic) 33 /// https://stackoverflow.com/users/2145198/beebcon 34 static func preferredFont(for style: TextStyle, weight: Weight, italic: Bool = false) -> UIFont { 35 @Environment(\.sizeCategory) var sizeCategory 36 37 // Get the style's default pointSize 38 let traits = UITraitCollection(preferredContentSizeCategory: .large) 39 let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits) 40 41 // Get the font at the default size and preferred weight 42 var font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight) 43 if italic == true { 44 font = font.with([.traitItalic]) 45 } 46 47 // Setup the font to be auto-scalable 48 let metrics = UIFontMetrics(forTextStyle: style) 49 return metrics.scaledFont(for: font) 50 } 51 52 private func with(_ traits: UIFontDescriptor.SymbolicTraits...) -> UIFont { 53 guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits).union(fontDescriptor.symbolicTraits)) else { 54 return self 55 } 56 return UIFont(descriptor: descriptor, size: 0) 57 } 58 } 59 // Use it like this: 60 // UIFont.preferredFont(for: .largeTitle, weight: .regular) 61 // UIFont.preferredFont(for: .headline, weight: .semibold, italic: true) 62 63 64 65 /// provides a (custom) scalable UIFont based on the first parameter: 0 = system, 1 = Atkinson, 2 = Nunito, 3 = NunitoItalic 66 struct TalerUIFont { 67 @Environment(\.legibilityWeight) private var legibilityWeight: LegibilityWeight? 68 69 private static func scalableSystemFont(for style: UIFont.TextStyle, legibilityBold: Bool = false, 70 bold: Bool = false, italic: Bool = false) -> UIFont { 71 let black = bold && legibilityBold 72 return UIFont.preferredFont(for: style, 73 weight: black ? .heavy 74 : (bold || legibilityBold) ? .semibold : .regular, 75 italic: italic) 76 } 77 78 /// check wether a custom font for fontName is available 79 /// fontName already contains "Bold" (instead of "Regular") - the bold and italic params are only for the fallback 80 private static func scalableUIFont(_ fontName: String, size: CGFloat, relativeTo style: UIFont.TextStyle, 81 legibilityBold: Bool = false, bold: Bool = false, italic: Bool = false) -> UIFont { 82 @Environment(\.sizeCategory) var sizeCategory 83 if let font = UIFont(name: fontName, size: size) { 84 // return a scalable UIFont 85 let fontMetrics = UIFontMetrics(forTextStyle: style) 86 return fontMetrics.scaledFont(for: font) 87 } else { 88 // fallback: return the system font 89 return scalableSystemFont(for: style, legibilityBold: legibilityBold, bold: bold, italic: italic) 90 } 91 } 92 93 private static func atkinson(size: CGFloat, relativeTo style: UIFont.TextStyle, 94 legibilityBold: Bool = false, bold: Bool = false, italic: Bool = false) -> UIFont { 95 let useBold = bold || legibilityBold 96 let fontName = ATKINSON + (italic ? (useBold ? BOLDITALIC : ITALIC) 97 : (useBold ? BOLD : REGULAR)) 98 return scalableUIFont(fontName, size: size, relativeTo: style, 99 legibilityBold: legibilityBold, bold: bold, italic: italic) 100 } 101 102 private static func nunito(size: CGFloat, relativeTo style: UIFont.TextStyle, 103 legibilityBold: Bool = false, bold: Bool = false, italic: Bool = false) -> UIFont { 104 let black = bold && legibilityBold 105 let fontName = NUNITO + (italic ? (black ? BLACKITALIC 106 : (bold || legibilityBold) ? BOLDITALIC : ITALIC) 107 : (black ? BLACK 108 : (bold || legibilityBold) ? BOLD : REGULAR)) 109 return scalableUIFont(fontName, size: size, relativeTo: style, 110 legibilityBold: legibilityBold, bold: bold, italic: italic) 111 } 112 113 static func uiFont(_ selectedFont: Int, size: CGFloat, relativeTo style: UIFont.TextStyle, 114 legibilityBold: Bool = false, bold: Bool = false, italic: Bool = false) -> UIFont { 115 switch selectedFont { 116 case 1: return TalerUIFont.atkinson(size: size, relativeTo: style, 117 legibilityBold: legibilityBold, bold: bold, italic: italic) 118 case 2: return TalerUIFont.nunito(size: size, relativeTo: style, 119 legibilityBold: legibilityBold, bold: bold, italic: italic) 120 default: 121 // return UIFont.preferredFont(forTextStyle: style) 122 return TalerUIFont.scalableSystemFont(for: style, legibilityBold: legibilityBold, bold: bold, italic: italic) 123 } 124 } 125 126 static func uiFont(_ styleSize: StyleSizeBold) -> UIFont { 127 return uiFont(Controller.shared.talerFontIndex, size: styleSize.size, relativeTo: styleSize.style) 128 } 129 } 130 131 struct TalerFont { // old running 132 var regular: Font 133 var bold: Font 134 static var talerFontIndex: Int { return 2 } 135 136 init(_ base: String, size: CGFloat, relativeTo: Font.TextStyle, isBold: Bool = false) { 137 if TalerFont.talerFontIndex == 0 { 138 self.regular = .system(relativeTo) 139 self.bold = .system(relativeTo).bold() 140 } else if isBold { 141 // Nunito has Black Variants, but AtkinsonHyperlegible doesn't 142 self.regular = Font.custom(base + (TalerFont.talerFontIndex == 2 ? BOLD : BOLDITALIC), size: size, relativeTo: relativeTo) 143 self.bold = Font.custom(base + (TalerFont.talerFontIndex == 2 ? BLACK : BLACKITALIC), size: size, relativeTo: relativeTo) 144 } else { 145 self.regular = Font.custom(base + (TalerFont.talerFontIndex > 2 ? ITALIC : REGULAR), size: size, relativeTo: relativeTo) 146 self.bold = Font.custom(base + (TalerFont.talerFontIndex > 2 ? BOLDITALIC : BOLD), size: size, relativeTo: relativeTo) 147 } 148 } 149 150 init(regular: Font, bold: Font) { 151 self.regular = regular 152 self.bold = bold 153 } 154 155 func value(_ legibilityWeight: LegibilityWeight?) -> Font { 156 switch legibilityWeight { 157 case .bold: return bold 158 default: return regular 159 } 160 } 161 } 162 163 struct StyleSizeBold { 164 let style: UIFont.TextStyle 165 let size: CGFloat 166 let bold: Bool 167 let italic: Bool = false 168 169 static var largeTitle: StyleSizeBold { StyleSizeBold(style: .largeTitle, size: 34, bold: false) } // 34 -> 38 170 static var title: StyleSizeBold { StyleSizeBold(style: .title1, size: 28, bold: false) } // 28 -> 31 171 static var title2: StyleSizeBold { StyleSizeBold(style: .title2, size: 22, bold: false) } // 22 -> 25 172 static var title3: StyleSizeBold { StyleSizeBold(style: .title3, size: 20, bold: false) } // 20 -> 23 173 static var headline: StyleSizeBold { StyleSizeBold(style: .headline, size: 17, bold: true) } // 17 bold -> 19 bold 174 static var body: StyleSizeBold { StyleSizeBold(style: .body, size: 17, bold: false) } // 17 -> 19 175 static var callout: StyleSizeBold { StyleSizeBold(style: .callout, size: 16, bold: false) } // 16 -> 18 176 static var subheadline: StyleSizeBold { StyleSizeBold(style: .subheadline, size: 15, bold: false) } // 15 -> 17 177 static var footnote: StyleSizeBold { StyleSizeBold(style: .footnote, size: 13, bold: false) } // 13 -> 15 178 static var caption: StyleSizeBold { StyleSizeBold(style: .caption1, size: 12, bold: false) } // 12 -> 13 179 // static var caption2: AccessibleFont { AccessibleFont(fontName, size: 11, relativeTo: .caption2) } // 11 -> 12 180 } 181 182 extension TalerFont { // old running 183 static var fontName: String { NUNITO } 184 185 static var largeTitle: TalerFont { TalerFont(fontName, size: 34, relativeTo: .largeTitle) } // 34 -> 38 186 static var title: TalerFont { TalerFont(fontName, size: 28, relativeTo: .title) } // 28 -> 31 187 static var title1: TalerFont { TalerFont(fontName, size: 24, relativeTo: .title2) } // Destructive Buttons 188 static var title2: TalerFont { TalerFont(fontName, size: 22, relativeTo: .title2) } // 22 -> 25 189 static var title3: TalerFont { TalerFont(fontName, size: 20, relativeTo: .title3) } // 20 -> 23 190 static var picker: TalerFont { TalerFont(fontName, size: 18, relativeTo: .body) } // picker uses a different size! 191 static var headline: TalerFont { TalerFont(fontName, size: 17, relativeTo: .headline, isBold: true) } // 17 bold -> 19 bold 192 static var body: TalerFont { TalerFont(fontName, size: 17, relativeTo: .body) } // 17 -> 19 193 static var callout: TalerFont { TalerFont(fontName, size: 16, relativeTo: .callout) } // 16 -> 18 194 static var subheadline: TalerFont { TalerFont(fontName, size: 15, relativeTo: .subheadline) } // 15 -> 17 195 static var table: TalerFont { TalerFont(fontName, size: 14, relativeTo: .subheadline) } // tableview uses a different size! 196 static var footnote: TalerFont { TalerFont(fontName, size: 13, relativeTo: .footnote) } // 13 -> 15 197 static var caption: TalerFont { TalerFont(fontName, size: 12, relativeTo: .caption) } // 12 -> 13 198 static var badge: TalerFont { TalerFont(fontName, size: 10, relativeTo: .caption) } // 12 -> 13 199 } 200 201 struct StyleSizeBoldViewModifier: ViewModifier { 202 @Environment(\.legibilityWeight) private var legibilityWeight 203 var legibilityBold: Bool { legibilityWeight == .bold } 204 205 let styleSize: StyleSizeBold 206 207 static var talerFontIndex: Int { 208 if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { 209 return 2 210 } else { 211 return Controller.shared.talerFontIndex 212 } 213 } 214 215 func body(content: Content) -> some View { // TODO: italic 216 let uiFont = TalerUIFont.uiFont(Self.talerFontIndex, size: styleSize.size, relativeTo: styleSize.style, 217 legibilityBold: legibilityBold, bold: styleSize.bold) 218 content.font(Font(uiFont)) 219 } 220 } 221 222 struct TalerFontViewModifier2: ViewModifier { // old running 223 @Environment(\.legibilityWeight) private var legibilityWeight 224 225 var font: TalerFont 226 227 func body(content: Content) -> some View { 228 content.font(font.value(legibilityWeight)) 229 } 230 } 231 232 extension View { 233 func talerFont(_ font: TalerFont) -> some View { 234 return self.modifier(TalerFontViewModifier2(font: font)) 235 } 236 func talerFont1(_ styleSize: StyleSizeBold) -> some View { 237 return self.modifier(StyleSizeBoldViewModifier(styleSize: styleSize)) 238 } 239 } 240 // MARK: - 241 /// This works on-the-fly to update NavigationTitles when you change the font 242 struct NavigationBarBuilder: UIViewControllerRepresentable { 243 var build: (UINavigationController) -> Void = { _ in } 244 245 func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationBarBuilder>) -> UIViewController { 246 UIViewController() 247 } 248 249 func updateUIViewController(_ uiViewController: UIViewController, 250 context: UIViewControllerRepresentableContext<NavigationBarBuilder>) { 251 if let navigationController = uiViewController.navigationController { 252 self.build(navigationController) 253 } 254 } 255 } 256 257 /// This works only once. Each following call does nothing - including (re-)setting to nil 258 @MainActor 259 struct TalerNavBar: ViewModifier { 260 let talerFontIndex: Int 261 262 static func setNavBarFonts(talerFontIndex: Int) -> Void { 263 let navBarAppearance = UINavigationBar.appearance() 264 navBarAppearance.titleTextAttributes = nil 265 navBarAppearance.largeTitleTextAttributes = nil 266 if talerFontIndex != 0 { 267 navBarAppearance.titleTextAttributes = [.font: TalerUIFont.uiFont(talerFontIndex, size: 24, relativeTo: .title2)] 268 navBarAppearance.largeTitleTextAttributes = [.font: TalerUIFont.uiFont(talerFontIndex, size: 38, relativeTo: .largeTitle)] 269 } 270 } 271 272 init(_ talerFontIdx: Int) { 273 self.talerFontIndex = talerFontIdx 274 TalerNavBar.setNavBarFonts(talerFontIndex: talerFontIdx) 275 } 276 277 func body(content: Content) -> some View { 278 let _ = TalerNavBar.setNavBarFonts(talerFontIndex: talerFontIndex) 279 content 280 } 281 282 } 283 284 extension View { 285 @MainActor func talerNavBar(talerFontIndex: Int) -> some View { 286 self.modifier(TalerNavBar(talerFontIndex)) 287 } 288 } 289 290 291 #if false 292 //init() { 293 // NavigationBarConfigurator.configureTitles() 294 //} 295 struct NavigationBarConfigurator { 296 static func configureTitles() { 297 let appearance = UINavigationBarAppearance() 298 let design = UIFontDescriptor.SystemDesign.rounded 299 if let descriptorWithDesign = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .largeTitle) 300 .withDesign(design), 301 let descriptorWithTraits = descriptorWithDesign.withSymbolicTraits(.traitBold) { 302 let font = UIFont(descriptor: descriptorWithTraits, size: 34) 303 appearance.largeTitleTextAttributes = [.font: font, .foregroundColor: UIColor.label] 304 } 305 if let smallTitleDescriptorWithDesign = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline) .withDesign(design) { 306 let smallTitleFont = UIFont(descriptor: smallTitleDescriptorWithDesign, size: 24) 307 appearance.titleTextAttributes = [.font:smallTitleFont, .foregroundColor: UIColor.label] 308 } 309 UINavigationBar.appearance().standardAppearance = appearance 310 } 311 } 312 #endif 313 // MARK: - 314 #if DEBUG 315 struct ContentViewFonts: View { 316 // let myWeight: Font.Weight 317 var body: some View { 318 VStack { 319 HStack { 320 Text(verbatim: "title a") 321 Text(verbatim: "bold").bold() 322 } 323 .talerFont(.title) 324 .padding() 325 326 HStack { 327 Text(verbatim: "title2 a") 328 Text(verbatim: "italic").italic() 329 Text(verbatim: "bold").bold() 330 } 331 .talerFont(.title2) 332 .padding() 333 Text(verbatim: "headline") 334 .talerFont(.headline) 335 .padding(.top) 336 Text(verbatim: "headline bold") 337 .bold() 338 .talerFont(.headline) 339 .padding(.bottom) 340 Text(verbatim: "title2 bold italic") 341 .bold() 342 .italic() 343 .talerFont(.title2) 344 .padding() 345 } 346 } 347 } 348 349 #Preview("Font View") { 350 ContentViewFonts() 351 } 352 #endif