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