taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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