Skip to content

Instantly share code, notes, and snippets.

@eoghain
Last active April 16, 2019 22:38
Show Gist options
  • Save eoghain/11272365 to your computer and use it in GitHub Desktop.
Save eoghain/11272365 to your computer and use it in GitHub Desktop.
Fontello to NSString category
#!/usr/bin/env python
import sys
import json
import os
import argparse
from textwrap import dedent
def pascalCase(str):
str = str.replace('-', ' ')
str = str.replace('_', ' ')
components = str.split(' ')
# print components
# print "".join(x.title() for x in components[0:])
return "".join(x.title() for x in components[0:])
def camelCase(str):
str = str.replace('-', ' ')
str = str.replace('_', ' ')
components = str.split(' ')
# print components
# print components[0] + "".join(x.title() for x in components[1:])
return components[0] + "".join(x.title() for x in components[1:])
def writeFile(fileName, contents):
header = dedent('''\
//
// %s
//
''' % os.path.basename(fileName))
f = open(fileName, 'w')
f.write(header)
f.write(contents)
f.close()
def createSwift(outputDir, name, prefixText, glyphs):
outputFileName = outputDir + ('%sIcon.swift' % name)
writeFile(outputFileName, createSwiftEnum(name, prefixText, glyphs))
outputFileName = outputDir + ('%sIconView.swift' % name)
writeFile(outputFileName, createSwiftIconView(name))
outputFileName = outputDir + ('%sIconPicker.swift' % name)
writeFile(outputFileName, createSwiftIconPicker(name))
def createSwiftEnum(name, prefixText, glyphs):
srcName = ''
initCases = ''
cases = ''
names = ''
lookupNames = ''
for glyph in glyphs:
if (srcName != glyph['src']):
srcName = glyph['src']
fontName = glyph['css']
fullFontName = prefixText + glyph['css']
hexCode = '\u{%s}' % hex(glyph['code']).replace('0x', '').title()
lookupName = prefixText + fontName
# Switch and Enum Cases
cases += 'case %s = "%s"\n\t\t' % (camelCase(fontName), hexCode)
initCases += 'case "%s", "%s", "%s": self = .%s\n\t\t\t\t' % (fontName, lookupName, hexCode, camelCase(fontName))
names += 'case .%s: return "%s"\n\t\t\t\t' % (camelCase(fontName), fontName)
lookupNames += 'case .%s: return "%s"\n\t\t\t\t' % (camelCase(fontName), lookupName)
swiftFile = dedent('''\
public enum %(fontName)sIcon: String, CaseIterable {
%(cases)s
case missingIcon = "\u{26A0}"
var string: String { return rawValue }
var name: String {
switch(self) {
%(names)s
default: return "missing-icon"
}
}
var prefixedName: String {
switch(self) {
%(lookupNames)s
default: return "icon-missing"
}
}
static var allCasesButMissing: [%(fontName)sIcon] {
return allCases.filter { $0 != .missingIcon }
}
public init(rawValue: String) {
switch rawValue {
%(initCases)s
default: self = .missingIcon
}
}
}''' % ({"fontName": name, "cases": cases.rstrip(), "names": names.rstrip(), "lookupNames": lookupNames.rstrip(), "initCases": initCases.rstrip()}))
return swiftFile
def createSwiftIconView(name):
swiftFile = dedent('''\
import UIKit
import CoreText
public class %(fontName)sIconView: UIView {
@IBInspectable public var iconString: String = "" {
didSet {
icon = %(fontName)sIcon(rawValue: iconString)
}
}
@IBInspectable public var iconColor: UIColor = .darkText {
didSet {
setNeedsDisplay()
}
}
public var icon: %(fontName)sIcon? {
didSet {
setNeedsDisplay()
}
}
override public func draw(_ rect: CGRect) {
super.draw(rect)
guard let icon = self.icon else { return }
// Initialize the context
guard let context = UIGraphicsGetCurrentContext() else { return }
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1, y: -1)
// Initialize the string and font
let fontSize: CGFloat = min(rect.size.width, rect.size.height) / 2
let fontDescriptor = CTFontDescriptorCreateWithNameAndSize("%(fontName)s" as CFString, fontSize)
let font = CTFontCreateWithFontDescriptor(fontDescriptor, fontSize, nil)
let attributes = [
kCTFontAttributeName : font,
kCTForegroundColorAttributeName : iconColor.cgColor
] as CFDictionary
// Build the font string
let cfString = icon.string as CFString
guard let attrString = CFAttributedStringCreate(kCFAllocatorDefault, cfString, attributes) else { return }
let line = CTLineCreateWithAttributedString(attrString)
let framesetter = CTFramesetterCreateWithAttributedString(attrString)
var range: CFRange = CFRange()
let constraints = rect.size
let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,1), nil, constraints, &range)
// Set text position and draw the line into the graphics context
context.textPosition = CGPoint(x: (rect.size.width - coreTextSize.width) / 2, y: (rect.size.height - coreTextSize.height) / 2)
CTLineDraw(line, context)
}
}''' % ({"fontName": name}))
return swiftFile
def createSwiftIconPicker(name):
swiftFile = dedent('''\
import UIKit
import QuartzCore
@IBDesignable
public class %(fontName)sIconPicker: UIControl {
@IBInspectable var iconString: String = "" {
didSet {
icon = %(fontName)sIcon(rawValue: iconString)
}
}
@IBInspectable var iconColor: UIColor = .darkText {
didSet {
iconKeyboard.selectionColor = iconColor
iconView.iconColor = iconColor
setNeedsDisplay()
}
}
var icon: %(fontName)sIcon? {
didSet {
iconView.icon = icon
setNeedsDisplay()
}
}
private let iconView = %(fontName)sIconView()
private let textField = UITextField()
private let iconKeyboard = %(fontName)sIconKeyboard()
convenience init(icon: %(fontName)sIcon, iconColor: UIColor){
self.init(frame: .zero)
self.icon = icon
self.iconColor = iconColor
}
// Default initializer
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
iconKeyboard.selectionColor = iconColor
iconKeyboard.delegate = self
addSubview(iconView)
iconView.isUserInteractionEnabled = false
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.backgroundColor = backgroundColor
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.topAnchor.constraint(equalTo: topAnchor).isActive = true
iconView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
iconView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
iconView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
addSubview(textField)
textField.inputView = iconKeyboard.view
let flexBarButton = UIBarButtonItem.init(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneBarButton = UIBarButtonItem.init(barButtonSystemItem: .done, target: self, action: #selector(donePressed(_:)))
let keyboardToolbar = UIToolbar()
keyboardToolbar.sizeToFit()
keyboardToolbar.items = [flexBarButton, doneBarButton]
textField.inputAccessoryView = keyboardToolbar
textField.alpha = 0
textField.translatesAutoresizingMaskIntoConstraints = false
textField.topAnchor.constraint(equalTo: topAnchor).isActive = true
textField.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
textField.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
textField.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
@objc func donePressed(_ sender: Any) {
textField.resignFirstResponder()
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if textField.isFirstResponder {
textField.resignFirstResponder()
}
else {
textField.becomeFirstResponder()
}
}
}
extension %(fontName)sIconPicker: %(fontName)sIconKeyboardDelegate {
func selected(icon: %(fontName)sIcon) {
self.icon = icon
self.sendActions(for: UIControl.Event.valueChanged)
}
}
// MARK: - Private internal classes for %(fontName)sIconPicker
protocol %(fontName)sIconKeyboardDelegate {
func selected(icon: %(fontName)sIcon)
}
private class %(fontName)sIconKeyboardCell: UICollectionViewCell {
var selectionColor: UIColor = .cyan
var iconView: %(fontName)sIconView = %(fontName)sIconView()
var icon: %(fontName)sIcon = .missingIcon {
didSet {
iconView.icon = icon
}
}
override var isSelected: Bool {
didSet {
if isSelected {
iconView.iconColor = selectionColor
}
else {
iconView.iconColor = .darkText
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
iconView.icon = nil
isSelected = false
}
func setup() {
backgroundColor = .clear
contentView.backgroundColor = .white
// IconView
iconView.backgroundColor = .white
contentView.addSubview(iconView)
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10).isActive = true
iconView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10).isActive = true
iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10).isActive = true
iconView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10).isActive = true
}
}
private class %(fontName)sIconKeyboard: UIViewController {
public var delegate: %(fontName)sIconKeyboardDelegate?
public var selectionColor: UIColor = .cyan
private var cellIdentifier = "iconButton"
private var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
init() {
super.init(nibName: nil, bundle: nil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
view.backgroundColor = .clear
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = CGSize(width: 95, height: 95)
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 20, right: 10)
collectionView.contentInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
collectionView.backgroundColor = .clear
collectionView.allowsSelection = true
collectionView.allowsMultipleSelection = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(%(fontName)sIconKeyboardCell.self, forCellWithReuseIdentifier: cellIdentifier)
view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension %(fontName)sIconKeyboard: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return %(fontName)sIcon.allCasesButMissing.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! %(fontName)sIconKeyboardCell
cell.icon = %(fontName)sIcon.allCasesButMissing[indexPath.row]
// Selection Color
cell.selectionColor = selectionColor
// Round & Shadow
cell.contentView.layer.cornerRadius = 10.0
cell.contentView.layer.borderWidth = 1.0
cell.contentView.layer.borderColor = UIColor.clear.cgColor
cell.contentView.layer.masksToBounds = true
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 2.0)
cell.layer.shadowRadius = 2.0
cell.layer.shadowOpacity = 0.8
cell.layer.masksToBounds = false
cell.layer.shadowPath = UIBezierPath(roundedRect:cell.bounds, cornerRadius:cell.contentView.layer.cornerRadius).cgPath
return cell
}
}
extension %(fontName)sIconKeyboard: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.selected(icon: %(fontName)sIcon.allCasesButMissing[indexPath.row])
}
}''' % ({"fontName": name}))
return swiftFile
def main(configFile, outputDir):
try:
json_data=open(configFile)
except:
print "Unable to open %s" % configFile
exit(1)
data = json.load(json_data)
name = pascalCase(data['name'])
glyphs = sorted(data['glyphs'], key=lambda k: k['css']) # sort by name
glyphs = sorted(glyphs, key=lambda k: k['src']) #sort by name
prefixText = data['css_prefix_text']
if outputDir[-1] != '/':
outputDir += '/'
createSwift(outputDir, name, prefixText, glyphs)
def usage(fileName):
print 'usage: %s [swift] config.json <output directory>' % fileName
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("configFile", help="config.json file to use")
parser.add_argument("outputDir", help="outputDirectory")
args = parser.parse_args()
main(args.configFile, args.outputDir)
@eoghain
Copy link
Author

eoghain commented Sep 10, 2014

Using fontello in iOS.

  1. Build your font collection at fontello.com
  2. Download your font (set the name if you don't want to use fontello).
  3. Place the config.json and .ttf files into your project directory (be sure to add the .ttf project so it's copied to the bundle, config is only for setup not needed in app).
  4. Run the above code.
  5. Be sure to add the .ttf as one of your "application supplied fonts" / "fonts provided by application" or whatever Xcode is calling it these days in the Info-Plist.
  6. Enjoy fontello fonts with an easy syntax [NSString iconGithub];

Notes
Don't forget to set your font correctly before using it self.searchLabel.font = [UIFont fontWithName:@"fontello" size:17];
The config.json can be dragged to the fontello.com web page to reload all of the exact fonts you are using. Very helpful if you decide to add more fonts later, or want to change existing ones for a UI refresh.

@eoghain
Copy link
Author

eoghain commented Apr 24, 2015

Added a secondary code generation that is <fontName>IconView, a view usable in IB to turn icons into compostable views.

@eoghain
Copy link
Author

eoghain commented Nov 12, 2015

Added helper method for getting icon based on name so you can have a service tell you what font to display by sending you the css_prefix_text + glyph.css (i.e. icon-github). e.g. [NSString iconForName:@"icon-github"]

@eoghain
Copy link
Author

eoghain commented Jan 13, 2016

Updated to build a swift struct if you set the -s/--swift flag. Swift generation is only for the icon strings. The icons can be used from the class either directly FontNameIcons.iconName or through the named function FontNameIcons.iconNamed('icon-iconName') the icon name can be supplied in either short form (i.e. just css), or css-prefix + css combined name (i.e. 'heart' or 'icon-heart').

@eoghain
Copy link
Author

eoghain commented Apr 16, 2019

Updated to remove objective-c code generation (sorry I've moved to Swift and am not looking back). Converted the generated code from a struct to an enum, makes using as a string a little harder (FontIcon.foo.string) but allowed me to do my next trick of adding an IconPicker control that will display an IconKeyboard when tapped allowing the user to change the selected icon.

Use the IconPicker in IB by adding a UIView to your storyboard and then setting it's type to <fontName>IconPicker and then listening for a valueChanged event:

    @IBAction func iconChanged(_ sender: TestIconPicker) {
        print("\(sender.iconColor), \(sender.icon?.name ?? "nil")")
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment