30

I want to use a custom font within a Cocoapod, but I can't find anything on using a custom font within a static library. As there is no info.plist file, there is no where to tell the app what font to use.

Any ideas?

Quentamia
  • 3,244
  • 3
  • 33
  • 42

6 Answers6

17

There is a way of using a custom font without adding anything to the plist file.

    NSBundle *bundle = [NSBundle bundleForClass:[self class]];
    NSURL *fontURL = [bundle URLForResource:<#fontName#> withExtension:@"otf"/*or TTF*/];
    NSData *inData = [NSData dataWithContentsOfURL:fontURL];
    CFErrorRef error;
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)inData);
    CGFontRef font = CGFontCreateWithDataProvider(provider);
    if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
        CFStringRef errorDescription = CFErrorCopyDescription(error);
        NSLog(@"Failed to load font: %@", errorDescription);
        CFRelease(errorDescription);
    }
    CFSafeRelease(font);
    CFSafeRelease(provider);

You also need the CFSafeRelease function for this to work.

void CFSafeRelease(CFTypeRef cf) {
    if (cf != NULL) {
        CFRelease(cf);
    }
}

Source: Loading iOS fonts dynamically.

Swift equivalent:

extension UIFont {
    static func registerFont(bundle: Bundle, fontName: String, fontExtension: String) -> Bool {
        guard let fontURL = bundle.url(forResource: fontName, withExtension: fontExtension) else {
            fatalError("Couldn't find font \(fontName)")
        }

        guard let fontDataProvider = CGDataProvider(url: fontURL as CFURL) else {
            fatalError("Couldn't load data from the font \(fontName)")
        }

        guard let font = CGFont(fontDataProvider) else {
            fatalError("Couldn't create font from data")
        }

        var error: Unmanaged<CFError>?
        let success = CTFontManagerRegisterGraphicsFont(font, &error)
        guard success else {
            print("Error registering font: maybe it was already registered.")
            return false
        }

        return true
    }
}
Mick F
  • 7,312
  • 6
  • 51
  • 98
Adam
  • 26,549
  • 8
  • 62
  • 79
  • 1
    but then you can't use the fonts in Storyboards, right? Unless you load all the custom fonts during startup... – swalkner May 17 '16 at 13:16
  • 1
    Since this is being sent over in a Cocoapod, and the idea is to have the font immediately available to those that consume the pod, when and where would `registerFont` be called? – h.and.h Feb 27 '20 at 17:41
  • @RIP.Ben.Franklin anywhere in the code of your library/pod – Adam Feb 27 '20 at 17:43
13

For those of you finding this in 2018+, I got custom fonts to work with interface builder support (XCode 9) with these two steps:

  1. Add your fonts to the resource bundle of your framework (in the .podspec file)

    s.resources = "PodName/**/*.{ttf}"
    
  2. Load fonts at runtime using Adam's answer above

    #import <CoreText/CoreText.h>
    
    void CFSafeRelease(CFTypeRef cf) { // redefine this
      if (cf != NULL) {
        CFRelease(cf);
      }
    }
    
    
    + (void) loadFonts {
      NSBundle *frameworkBundle = [NSBundle bundleForClass:self.classForCoder];
      NSURL *bundleURL = [[frameworkBundle resourceURL] URLByAppendingPathComponent:@"PodName.bundle"];
      NSBundle *bundle = [NSBundle bundleWithURL:bundleURL];
    
      NSURL *fontURL = [bundle URLForResource:@"HindMadurai-SemiBold" withExtension:@"ttf"];
      NSData *inData = [NSData dataWithContentsOfURL:fontURL];
      CFErrorRef error;
      CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)inData);
      CGFontRef font = CGFontCreateWithDataProvider(provider);
      if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
          CFStringRef errorDescription = CFErrorCopyDescription(error);
          NSLog(@"Failed to load font: %@", errorDescription);
          CFRelease(errorDescription);
      }
      CFSafeRelease(font);
      CFSafeRelease(provider);
    }
    
  3. Run pod install

Eden
  • 1,782
  • 15
  • 23
tommybananas
  • 5,718
  • 1
  • 28
  • 48
  • Hi! Thanks for this answer. It's working pretty well for me so far. One question... When did you end up calling the loadFonts method? – jmg Mar 16 '18 at 03:59
  • I'm building an SDK so I ended up throwing it in an initialization method where the api key gets set, etc that i can be relatively certain will only be called once and guaranteed before anything in my library gets presented – tommybananas Mar 16 '18 at 05:58
  • Well, it seemed to work in the simulator, but for some reason the fonts don't load when I run it on the device. Did you happen to run into this problem also, or did it "just work"? – jmg Mar 23 '18 at 00:14
  • Just a path problem. Works now :) – jmg Mar 24 '18 at 04:34
  • Don't forget to run 'pod install' after editing your podspec – Eden Feb 05 '21 at 23:11
12

If I understand correctly, you are trying to provide a font with your Cocoapod, and you intent the iOS apps which include the pod to be able to use your custom font.

This post_install hook seems to work:

Pod::Spec.new do |s|
  # ...
  s.resources = "Resources/*.otf"
  # ...
  s.post_install do |library_representation|
    require 'rexml/document'

    library = library_representation.library
    proj_path = library.user_project_path
    proj = Xcodeproj::Project.new(proj_path)
    target = proj.targets.first # good guess for simple projects

    info_plists = target.build_configurations.inject([]) do |memo, item|
      memo << item.build_settings['INFOPLIST_FILE']
    end.uniq
    info_plists = info_plists.map { |plist| File.join(File.dirname(proj_path), plist) }

    resources = library.file_accessors.collect(&:resources).flatten
    fonts = resources.find_all { |file| File.extname(file) == '.otf' || File.extname(file) == '.ttf' }
    fonts = fonts.map { |f| File.basename(f) }

    info_plists.each do |plist|
      doc = REXML::Document.new(File.open(plist))
      main_dict = doc.elements["plist"].elements["dict"]
      app_fonts = main_dict.get_elements("key[text()='UIAppFonts']").first
      if app_fonts.nil?
        elem = REXML::Element.new 'key'
        elem.text = 'UIAppFonts'
        main_dict.add_element(elem)
        font_array = REXML::Element.new 'array'
        main_dict.add_element(font_array)
      else
        font_array = app_fonts.next_element
      end

      fonts.each do |font|
        if font_array.get_elements("string[text()='#{font}']").empty?
          font_elem = REXML::Element.new 'string'
          font_elem.text = font
          font_array.add_element(font_elem)
        end
      end

      doc.write(File.open(plist, 'wb'))
    end
  end

The hook finds the user project, and in the first target (you probably can complete this solution by asking CocoaPods to give you the real target) it looks for its Info.plist file(s) (normally there is only one). Finally it looks for the UIAppFonts key of the file, creates it if not found, and fill the array with the font names if they are not already there.

yonosoytu
  • 3,319
  • 1
  • 17
  • 23
5

Swift 5 implementation

I was able to solve this by creating the below class in my Cocoapod, then just calling CustomFonts.loadAll() from my main app's AppDelegate.swift. After that I can just use a font like this in my app:

let myFont = CustomFonts.Style.regular.font

Note that the Style enum is not necessary, just a convenient way to separate concerns. You could also just call:

let myFont = UIFont(name: "SourceSansPro-SemiBold", size: 14)

import CoreText

public class CustomFonts: NSObject {

  public enum Style: CaseIterable {
    case mono
    case regular
    case semibold
    public var value: String {
      switch self {
      case .mono: return "SourceCodePro-Medium"
      case .regular: return "SourceSansPro-Regular"
      case .semibold: return "SourceSansPro-SemiBold"
      }
    }
    public var font: UIFont {
      return UIFont(name: self.value, size: 14) ?? UIFont.init()
    }
  }

  // Lazy var instead of method so it's only ever called once per app session.
  public static var loadFonts: () -> Void = {
    let fontNames = Style.allCases.map { $0.value }
    for fontName in fontNames {
      loadFont(withName: fontName)
    }
    return {}
  }()

  private static func loadFont(withName fontName: String) {
    guard
      let bundleURL = Bundle(for: self).url(forResource: "[CococpodName]", withExtension: "bundle"),
      let bundle = Bundle(url: bundleURL),
      let fontURL = bundle.url(forResource: fontName, withExtension: "ttf"),
      let fontData = try? Data(contentsOf: fontURL) as CFData,
      let provider = CGDataProvider(data: fontData),
      let font = CGFont(provider) else {
        return
    }
    CTFontManagerRegisterGraphicsFont(font, nil)
  }

}
Ricky Padilla
  • 226
  • 5
  • 7
1

Well, idk if it can be an answer, but you can also look to cocoapod that has needed font, like this: https://github.com/parakeety/GoogleFontsiOS

Library contains many fonts, i needed Chivo, so i added pod 'GoogleFontsiOS/Chivo' and used it instead of writing font-loading code by myself.

Zaporozhchenko Oleksandr
  • 4,660
  • 3
  • 26
  • 48
0

The function CTFontManagerRegisterFontsForURL makes life more easier.

Karsten
  • 1,869
  • 22
  • 38