iconfont 实践及使用优化

公司在前一段时间将大量图标进行了iconfont的替换,大大缩减了app的size。这几天,寻思着在自己app上也使用 iconfont,并且在使用上进行优化,使其在xib和 storyboard 上能像 UIImageView 那样所写即所见,所见即所得。

iconfont 即通过自体形式进行 icon 的展示。其最大特点就是矢量性,即放大图标不虚化。

下面先快速介绍下从创建到使用的过程以及对其使用进行优化,具体如下几点:

  1. 创建 iconfont 图标;
  2. 创建 iconfont 字体裤;
  3. 使用图标字体库;
  4. 在 iOS 使用上优化

创建 iconfont 图标

作为一个程序员,说真的我不会画图标,也觉得画起来特别烦,琐。好在阿里巴巴为我们提供了极大的便利。上网站:http://www.iconfont.cn

在这个网站你不仅可以上传自己设计的字体图标,更重要的是这里有超多牛逼设计师已经设计好的 icon,你只需要根据自己app的需要进行搜索,然后聚合起来,再生成一个字体库就能使用了。当然了,如果你对某个 icon 觉得不满意,你完全可以直接对它进行简单编辑哈。使用起来绝对666。

网站首页

下面介绍下在别人创建好的 icon 上进行修改和保存。

首先在首页上随便选择一个图标库,进入图标库详情后,挑选一个想修改的图标并将其加入购物车,然后添加到自己的项目上,接着在你创建的项目上即可看到这个图标。光标移动到图标,点击编辑即可。

add icon

生成字体库

在选择了需要的图标并添加到项目后,点击【下载至本地】即可生成图标字体库。顺便说一下,创建项目时会让你指定字体库名称(默认是 iconfont),可以根据自己需要进行命名(在使用时会用到这个字体库名称)。

new project

下载到本地后,我们其目录结构如下:

目录结构

使用图标字体库

因为后面会在 iOS 使用上进行优化,所以这里主要介绍在 iOS 上的使用。在android 和 web 上的使用可以参考官网的介绍: http://iconfont.cn/help/detail?spm=a313x.7781069.1998910419.d8cf4382a&helptype=code

iOS上的使用

其实这就是导入自定义字体,步骤是一模一样。

1.创建一个项目,选择 Single View Application,填写项目名称,完成。如下图:

(步骤太简单,图片不好意思显示)

2.把下载的字体库 iconfont.ttf 文件拖入项目中,勾上 Copy Items If Needed,点击【Finish】;
(步骤太简单,图片不好意思显示)

3.选择 Info.plist 文件,添加一个 key,名称为 UIAppFonts,回车。

添加字体key

4.这时新建的这一项应该是个数组类型,在 item0 的 Value 项填上字体名称(注意是字体名称,不是 ttf 文件名称,即上面提到的在创建字体的时候所填的名称)

添加自定义字体

5.在 storyboard 上放置一个 UILabel 或 UIButton,设置为自定义字体,选择新添加的字体

创建label

6.ctrl + 拖动控件,关联属性

关联属性

7.设置字体图标 Unicode
在我们下载的字体目录中,有个 demo_unicode.html 文件,打开它可以看到每个图标对呀的 Unicode 编号。

图标Unicode

给 label 的 text 设置上相应的值即可。如下:

1
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];

// 灯泡
_iconLabel.text = @"\U0000e881";
}

8.编译运行

运行结果


优化部分

以上就是创建和使用 iconfont 的这个过程。使用起来倒不麻烦,就是有几个缺点:

  1. 每次使用图标都得查一下对应的 Unicode 编号;
  2. 对应着一堆 “\U0000xxxx”,如果不写注释,真不知道是什么东西;
  3. 如果是使用存代码创建label,那通过代码设置图标还说得过去,但是使用 xib 或 storyboard 的时候还得用过一行代码来设置就有点说不过去了;

这么说确实感觉挺麻烦的,有问题就有解决的答案。下面,介绍下我是怎么逐一解决这些问题的。

问题2和3:能不能看着编码就知道是什么图标呢?怎样在 xib 上创建 label 的时候同时给设置上图标值呢?
解决方案: 通过继承 label,结合 IB_DESIGNABLE 就能办到。

首先创建 UILabel 子类 IconLabel,添加属性 iconName,顺便拓展下 IconLabel 功能,多添加个属性 selectedIconName,这样 IconLabel 就可以直接通过改变 selected 状态来改变预先设置好的图标了,如下代码:

1
2
3
4
5
6
7
8
9
10
@interface IconLabel : UILabel

/// 设置选中状态
@property(nonatomic, assign) BOOL selected;
/// 普通状态下的图片名称(只需要后面的4位 16 进制, 如:f3f7)
@property(nonatomic, copy, nullable) NSString *iconName;
/// 被选中状态下的图片名称(只需要后面的4位 16 进制,如:f3f7)
@property(nonatomic, copy, nullable) NSString *selectedIconName;

@end

重写 iconName 和 selectedIconName setter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma mark - setter and getter

- (void)setIconName:(NSString *)iconName {
NSString *iName = iconUnicodeWithName(iconName);
if (iName != _iconName) {
_iconName = iName;
}
}

- (void)setSelectedIconName:(NSString *)selectedIconName {
NSString *iName = iconUnicodeWithName(selectedIconName);
if (iName != _selectedIconName) {
_selectedIconName = iName;
}
}

其中 iconUnicodeWithName,是通过 NSScanner 将16进制字符串进行扫描,并转换为Unicode,如下:

1
2
3
4
5
6
NSString *iconUnicodeWithName(NSString *name) {
NSScanner *scanner = [NSScanner scannerWithString:name];
unsigned int code;
[scanner scanHexInt:&code];
return [NSString stringWithFormat:@"%C", (unsigned short)code];
}

最后,因为我们要在 xib 上直接进行值的设置,所有需要为 IconLabel 类和其想在 xib 上显示的属性设置上关键字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
IB_DESIGNABLE

@interface IconLabel : UILabel

/// 设置选中状态
@property(nonatomic, assign) BOOL selected;
/// 普通状态下的图片名称(只需要后面的4位 16 进制, 如:f3f7)
@property(nonatomic, copy, nullable) IBInspectable NSString *iconName;
/// 被选中状态下的图片名称(只需要后面的4位 16 进制,如:f3f7)
@property(nonatomic, copy, nullable) IBInspectable NSString *selectedIconName;

@end

其中 IB_DESIGNABLE 标识该类为可设计类, IBInspectable 标识属性为可视察属性(下面就能看到效果)。

为了能让设置的值对应的图标实时显示,我们需要重写 UILabel 的 prepareForInterfaceBuilder 方法,为了不需要手动设置字体名称,我们需要在 awakeFromNib 方法上进行默认字体的设置,如下:

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
#define IconFontName @"my_like_icon"

@implementation IconLabel

- (void)awakeFromNib {
[super awakeFromNib];

[self setupFontWithSize:self.font.pointSize];
[self reloadView];
}

- (void)prepareForInterfaceBuilder {
[super prepareForInterfaceBuilder];

[self setupFontWithSize:self.font.pointSize];
[self reloadView];
}

// 初始化字体
- (void)setupFontWithSize:(CGFloat)fontSize {
self.font = [UIFont fontWithName:IconFontName size:fontSize];
}

- (void)reloadView {
self.text = _selected ? _selectedIconName : _iconName;
}

@end

类的自定义这样就算完成了。现在,切换到 Main.storyboard,同样添加一个 UILabel 组件,并修改类为 IconLabel

设置label类

接着,切换到 Show the Attributes inspector 选择,你会发现多了两个可填框

多出的框

设置一下值,看看效果

设置值

so cool,如此美妙。现在你只需在 Icon Name 出设置上图标对应的字符串编号即可实时显示图标了。问题2和3解决。

接着我们看看问题1:能不能不需要查图标对应的 Unicode 编号就能设置图标呢?
等等,什么?你说问题2只解决了一半?虽然在 storyboard 上可以实时显示对应编码图标,但是如果我就用纯代码呢,不还是要注释是什么图标吗。而且,通过 storyboard 设置值又衍生出另一个问题了。
问题4: 突然有一天产品说:把所有收藏图标从红心都换成五角星吧… 我的刀呢

是的,如果只是上面这点实现,你还是要写注释说明。而问题4其实我们只要通过全局搜索再替换就行了(你就不想全局替换?那就听我慢慢道来哈)

下面在解决问题1的同时,会相应的把 问题2和4解决。
不查编码就能设置图标,其实很简单,你只需要将图标编码记住,就不用查了。

逗我吗

哈,还是进入正题吧。。。

要想做到这点,其实一个映射表就能解决了(有没有顿悟的感觉)。映射表的格式很简单,key 值是图标名称(命名时当然是要做到见名称如见图标啦),而且这个名称是要中文还是英文完全取决于你,当然,你想让两者并存也完全没问题。而 value 的值就是图标对应的编号了。如下,我新建了个 plist 文件进行管理:

IconMap

怎么样,看到 key 可以很形象的想到是什么图标吧。
技巧: 有个技巧可以快速往这个文件填充内容,就是通过选择文件然后 右击,然后选择 Open As -> Source Code,前面说到在下载下来的文件中有个叫 demo_unicode.html 的文件,通过 atom 或者 sublime 编辑器将其打开;然后复制需要的代码(即:图标编码那一块代码);接着,command + n 新建文件,将内容粘贴;然后还是见下图 gif 好了:

快速设置内容

最后将其粘贴到新建的plist 文件即可。

粘贴内容

接下来要做的就是通过 key 来映射相应的值,修改一下前面重写的 setter 方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)setIconName:(NSString *)iconName {
NSString *iName = IconNameSelector(iconName);
if (iName.length) {
iName = iconUnicodeWithName(iName);
} else {
iName = iconUnicodeWithName(iconName);
}
if (iName != _iconName) {
_iconName = iName;
}
}

- (void)setSelectedIconName:(NSString *)selectedIconName {
NSString *iName = IconNameSelector(selectedIconName);
if (iName.length) {
iName = iconUnicodeWithName(iName);
} else {
iName = iconUnicodeWithName(selectedIconName);
}
if (iName != _selectedIconName) {
_selectedIconName = iName;
}
}

这里做了下兼容,如果 IconNameSelector 返回的内容是空的,则认为 iconName 就是图标的编码,直接进行字符串编码扫描;如果返回结果非空,则说明映射成功,把映射结果进行编码扫描。 这里 IconNameSelector 所做的就是读取映射文件并返回值。

下面看看改进后的使用方法:

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];

IconLabel *iconLabel = [[IconLabel alloc] initWithIconName:@"灯泡" fontSize:30];
iconLabel.frame = CGRectMake(100, 100, iconLabel.frame.size.width, iconLabel.frame.size.height);
[self.view addSubview:iconLabel];
}

这下是不是感觉简单明了了。再看看 storyboard的使用:

storyboard使用改进

这下,不需要查看图标编码就可以设置图标了。
先看看 IconNameSelector 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define SRCROOT [[NSString stringWithCString:__FILE__ encoding:(NSUTF8StringEncoding)] stringByReplacingOccurrencesOfString:@"Classes/IconFontMap.h" withString:@""]

static NSDictionary *iconFontMap = nil;

static inline NSString * _Nullable iconfontWithName(NSString * __nonnull name) {
#if DEBUG
NSString *fullPath = [SRCROOT stringByAppendingString:@"IconFontMap.plist"];
iconFontMap = [NSDictionary dictionaryWithContentsOfFile:fullPath];
#endif
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle mainBundle] pathForResource:@"IconFontMap" ofType:@"plist"];
iconFontMap = [NSDictionary dictionaryWithContentsOfFile:path];
});
return iconFontMap[name];
}

#define IconNameSelector(key) iconfontWithName(key)

注意到 #if Debug ... #endif 包起来的代码。其实,因为将映射表放在文件上,通过 [NSBundle mainBundle] 获取的路径在编译时是没法读取到的,所以为了在 storyboard 的使用中能实时显示图标,我稍微做了黑科技,本来想通过代码获取 ${SRCROOT} 环境变量的路径的,不过没找到方法,所有通过 FILE 这个编译器内置宏来获取当前文件路径,再将后面路径替换掉来获取到项目路径。
如果你觉得这个方法有点矬,你可以尝试用另一种方法:
通过脚本获取 ${SRCROO} 路径,然后编译后动态去定义
另外,其实你完全可有直接定义一个 NSDictionary 对象,直接将代码贴上:

1
2
3
4
5
6
7
8
static NSDictionary *iconFontMap = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
iconFontMap = @{
@"key1": @"name1",
@"key2": @"name2"
};
});

以上,即在使用 iconfont 时的基本优化过程。当然,在实际用途中,还封装了 IconButton 和 IconImageView,项目地址:
https://github.com/linshaolie/IconFontExtension
如有发现什么问题,欢迎提出,谢谢。