taler-ios

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

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