在 Swift 中使用 IconFont

一个简单的纯 Swift 的 IconFont 使用工具类。可以让我们在项目中轻松使用 IconFont 图标。

IconFont 的好处:

  • 减小 ipa 包的大小
  • 图标缩放保真
  • 颜色切换方便,可适应多主题
  • 多端适配

项目加入 IconFont

  1. 下载所需的 iconfont.ttf 文件,将文件拖入到项目中。确定 Build Phases->Copy Bundle Resources 中有加入的文件。
  2. 在项目 info.plist 文件中,加入 key(Fonts provided by application)。这是个数组,下方增加 key-value(item0: iconfont.ttf)

核心类介绍

JTIconFontProtocol

针对字体的一个协议定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
public protocol JTIconFontProtocol {
/// 字体名称,这个名称与.ttf文件名不一定相同
var name: String { get }

/// .ttf文件的路径。如果文件名和字体名称相同,可使用默认实现。
var path: String { get }

/// icon 最终渲染的样式
var attributes: [NSAttributedString.Key: Any] { get set }

/// icon 的 unicode
var unicode: String { get }
}

并对 path 和 attributes 进行了默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fileprivate struct AssociatedKeys {
static var attributes = "attributes"
}

public extension EFIconFontProtocol {
var path: String {
return Bundle(for: JTIconFont.self).path(forResource: name, ofType: "ttf") ?? Bundle.main.path(forResource: name, ofType: "ttf") ?? ""
}

var attributes: [NSAttributedString.Key : Any] {
get {
if let attributes = objc_getAssociatedObject(self, &AssociatedKeys.attributes) as? [NSAttributedString.Key : Any] {
return attributes
}
let newAttributes: [NSAttributedString.Key : Any] = [:]
objc_setAssociatedObject(self, &AssociatedKeys.attributes, newAttributes, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newAttributes
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.attributes, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

attributes 为属性,这里使用了给扩展添加属性的方法。

JTIconFontElementProtocol

针对当前显示字符的一个协议定义,且这个协议继承自 JTIconFontProtocol,还实现了 CaseIterable 协议,方便后续利用枚举列出字体文件中包含的所有字符元素,并且让其方便遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// 用来做一些数据的存储
private struct Holder {
static var dicIconInfos: [String: [String: JTIconFontProtocol]] = [:]
static var dicIconStyles: [String: [NSAttributedString.Key: Any]] = [:]
}

public protocol JTIconFontElementProtocol: JTIconFontProtocol, CaseIterable {
/// 保存了当前显示字体的所有元素
static var dictionary: [String: JTIconFontProtocol] { get }

/// 字符的名称
func icon(named name: String) -> JTIconFontProtocol?

/// 字体实际的名称
static var name: String { get }

/// 字体文件的路径
static var path: String { get }

static var attributes: [NSAttributedString.Key: Any] { get set }

static var foregroundColor: UIColor? { get set }

static var backgroundColor: UIColor? { get set }
}

这个协议也进行了一些必要的默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public extension JTIconFontElementProtocol {
public static var dictionary: [String: JTIconFontProtocol] {
let key: String = String(describing: Self.self)
if let attributes = Holder.dicIconInfos[key] {
return attributes
}
let newAttributes: [String: JTIconFontProtocol] = generateDictionary()
Holder.dicIconInfos.updateValue(newAttributes, forKey: key)
return newAttributes
}

private static func generateDictionary() -> [String: JTIconFontProtocol] {
var dictionary: [String: JTIconFontProtocol] = [String: JTIconFontProtocol]()
for item in Self.allCases {
dictionary.updateValue(item, forKey: "\(item)")
}
return dictionary
}

public func icon(named name: String) -> JTIconFontProtocol? {
return Self.dictionary[name]
}

public static var path: String {
return Bundle(for: JTIconFont.self).path(forResource: Self.name, ofType: "ttf") ?? Bundle.main.path(forResource: Self.name, ofType: "ttf") ?? ""
}

public static var attributes: [NSAttributedString.Key: Any] {
get {
let key: String = String(describing: Self.self)
return Holder.dicIconStyles[key] ?? [:]
}
set {
let key: String = String(describing: Self.self)
Holder.dicIconStyles.updateValue(newValue, forKey: key)
}
}

public static var foregroundColor: UIColor? {
get {
return Self.attributes[NSAttributedString.Key.foregroundColor] as? UIColor
}
set {
if let newValue = newValue {
Self.attributes.updateValue(newValue, forKey: NSAttributedString.Key.foregroundColor)
} else {
Self.attributes.removeValue(forKey: NSAttributedString.Key.foregroundColor)
}
}
}

public static var backgroundColor: UIColor? {
get {
return Self.attributes[NSAttributedString.Key.backgroundColor] as? UIColor
}
set {
if let newValue = newValue {
Self.attributes.updateValue(newValue, forKey: NSAttributedString.Key.backgroundColor)
} else {
Self.attributes.removeValue(forKey: NSAttributedString.Key.backgroundColor)
}
}
}
}

// MARK:- JTIconFontProtocol
public extension JTIconFontElementProtocol {
public var name: String {
get {
return Self.name
}
}
public var path: String {
get {
return Self.path
}
}
public var attributes: [NSAttributedString.Key: Any] {
get {
return Self.attributes
}
set {
Self.attributes = newValue
}
}

public var foregroundColor: UIColor? {
return Self.foregroundColor
}

public var backgroundColor: UIColor? {
return Self.backgroundColor
}
}

核心功能

经过前面的介绍,我们已经可以将 IconFont 的文件加载进入我们的项目,并利用程序语言描述了字体的内容,现在我们需要真实的来使用字体文件,将其显示在我们的屏幕上,呈现给用户。

字体信息的描述都已经有了之后,我们需要将其呈现。IconFont 的呈现,无非就是属性字符串(NSAttributedString)和图片(UIImage)的展示。那么,接下来,就提供一些方法来提供所需要的信息。

上面定义的所有的协议和类都同时实现了 JTIconFontProtocol 协议,所以,我们的扩展方法也实现在 JTIconFontProtocol 协议的扩展中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public extension JTIconFontProtocol {
// MARK:- For String
public func attributedString(size fontSize: CGFloat, attributes: [NSAttributedString.Key: Any]?) -> NSAttributedString? {
guard let attributes = attributesWith(size: fontSize, attributes: attributes) else {
return nil
}
return NSAttributedString(string: self.unicode, attributes: attributes)
}

public func attributedString(size fontSize: CGFloat, foregroundColor: UIColor? = nil, backgroundColor: UIColor? = nil) -> NSAttributedString? {
var attributesCombine: [NSAttributedString.Key: Any] = [:]
if let foregroundColor = foregroundColor {
attributesCombine.updateValue(foregroundColor, forKey: NSAttributedString.Key.foregroundColor)
}
if let backgroundColor = backgroundColor {
attributesCombine.updateValue(backgroundColor, forKey: NSAttributedString.Key.backgroundColor)
}
return attributedString(size: fontSize, attributes: attributesCombine)
}

private func attributesWith(size fontSize: CGFloat, attributes: [NSAttributedString.Key: Any]?) -> [NSAttributedString.Key: Any]? {
guard let font = font(size: fontSize) else {
return nil
}
var attributesCombine: [NSAttributedString.Key: Any] = self.attributes
if let attributes = attributes {
for attribute in attributes {
attributesCombine.updateValue(attribute.value, forKey: attribute.key)
}
}
attributesCombine.updateValue(font, forKey: NSAttributedString.Key.font)
return attributesCombine
}

// MARK:- For image
public func image(size fontSize: CGFloat, attributes: [NSAttributedString.Key: Any]?, isCircle: Bool = false) -> UIImage? {
guard let imageString: NSAttributedString = attributedString(size: fontSize, attributes: attributes) else {
return nil
}

let rect = imageString.boundingRect(with: CGSize(width: CGFloat(MAXFLOAT), height: fontSize),
options: .usesLineFragmentOrigin,
context: nil)
let imageSize: CGSize = rect.size
let screenScale: CGFloat = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(imageSize, false, screenScale)

if isCircle {
let ctx = UIGraphicsGetCurrentContext()
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: imageSize.width / 2.0, height: imageSize.width / 2.0))
ctx?.addPath(path.cgPath)
ctx?.clip()
imageString.draw(in: rect)
ctx?.drawPath(using: .fillStroke)
} else {
imageString.draw(in: rect)
}

let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}

public func image(size fontSize: CGFloat, foregroundColor: UIColor? = nil, backgroundColor: UIColor? = .clear, isCircle: Bool = false) -> UIImage? {
var attributesCombine: [NSAttributedString.Key: Any] = [:]
if let foregroundColor = foregroundColor {
attributesCombine.updateValue(foregroundColor, forKey: NSAttributedString.Key.foregroundColor)
}
if let backgroundColor = backgroundColor {
attributesCombine.updateValue(backgroundColor, forKey: NSAttributedString.Key.backgroundColor)
}
return image(size: fontSize, attributes: attributesCombine, isCircle: isCircle)
}

public func image(size imageSize: CGSize, attributes: [NSAttributedString.Key: Any]?, isCircle: Bool = false) -> UIImage? {
guard let imageString: NSAttributedString = attributedString(size: 1, attributes: attributes) else {
return nil
}

let rect = imageString.boundingRect(with: CGSize(width: CGFloat(MAXFLOAT), height: 1), options: .usesLineFragmentOrigin, context: nil)
let widthScale = imageSize.width / rect.width
let heightScale = imageSize.height / rect.height
let scale = (widthScale < heightScale) ? widthScale : heightScale
let scaledWidth = rect.width * scale
let scaledHeight = rect.height * scale
var anchorPoint = CGPoint.zero
if widthScale < heightScale {
anchorPoint.y = (imageSize.height - scaledHeight) / 2
} else if widthScale > heightScale {
anchorPoint.x = (imageSize.width - scaledWidth) / 2
}
var anchorRect = CGRect.zero
anchorRect.origin = anchorPoint
anchorRect.size.width = scaledWidth
anchorRect.size.height = scaledHeight
guard let imageStringScale: NSAttributedString = attributedString(size: scale, attributes: attributes) else {
return nil
}
let screenScale: CGFloat = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(imageSize, false, screenScale)

if isCircle {
let ctx = UIGraphicsGetCurrentContext()
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: imageSize.width / 2.0, height: imageSize.width / 2.0))
ctx?.addPath(path.cgPath)
ctx?.clip()
imageString.draw(in: rect)
ctx?.drawPath(using: .fillStroke)
} else {
imageString.draw(in: rect)
}

imageStringScale.draw(in: anchorRect)
let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}

public func image(size imageSize: CGSize, foregroundColor: UIColor? = nil, backgroundColor: UIColor? = .clear, isCircle: Bool = false) -> UIImage? {
var attributesCombine: [NSAttributedString.Key: Any] = [:]
if let foregroundColor = foregroundColor {
attributesCombine.updateValue(foregroundColor, forKey: NSAttributedString.Key.foregroundColor)
}
if let backgroundColor = backgroundColor {
attributesCombine.updateValue(backgroundColor, forKey: NSAttributedString.Key.backgroundColor)
}
return image(size: imageSize, attributes: attributesCombine, isCircle: isCircle)
}
}

扩展功能

为 JTIconFontProtocol 提供一些扩展功能,方便使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public extension EFIconFontProtocol {
/// 自动加载字体
private func loaded() -> Bool {
if UIFont.fontNames(forFamilyName: name).isEmpty == false {
return true
}
if path.isEmpty {
return false
}
guard let fontData = NSData(contentsOfFile: path), let dataProvider = CGDataProvider(data: fontData), let cgFont = CGFont(dataProvider) else {
return false
}
var error: Unmanaged<CFError>?
if !CTFontManagerRegisterGraphicsFont(cgFont, &error) {
var errorDescription: CFString = "Unknown" as CFString
if let takeUnretainedValue = error?.takeUnretainedValue() {
errorDescription = CFErrorCopyDescription(takeUnretainedValue)
}
print("Unable to load \(path): \(errorDescription)")
return false
}
return true
}

func font(size fontSize: CGFloat) -> UIFont? {
if !loaded() { return nil }
return UIFont(name: self.name, size: fontSize)
}
}

提供数据

现在,IconFont 描述和显示功能都已经齐全了,接下来就是提供需要的 IconFont 的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension JTIconFont: JTIconFontElementProtocol {
public static var name: String {
return "iconfont"
}
public var unicode: String {
return self.rawValue
}
}

// 这里有很多数据,这里省略了
public enum JTIconFont: String {
case close = "\u{e857}"
case goback = "\u{e61a}"
...
}

关于这个枚举值从何来的话,其实,我们在下载 IconFont 文件时,都会伴随着一份 json 文件在其中,我们只需要写一段代码或者利用脚本,将 json 解析为我们所需的枚举即可。这里就不做展开了。

使用示例

1
2
3
4
5
6
7
// String
let label = UILabel()
label.font = JTIconFont.close.font(size: 18)
label.text = JTIconFont.close.unicode

// UIImage
let image = JTIconFont.close.image(size: 18, foregroundColor: JTThemeManager.color(.nameNoColorDark))

总结

  • 使用了关联对象,为协议扩展了属性
  • 可遍历的枚举
  • 协议扩展,添加默认实现及添加一些便利功能