庖丁UIKit之UIFont

App界面中少不了的肯定是各种各样的文字内容。那如何让文字在不同的位置都有相应的表现效果呢?像Word操作一样,自然干是通过设置各种各样的字体来表达不同的效果。UIKit为此提供了UIFont类型来表示字体。当然,UIFont只是粗略的对已有的字体进行表现,如果希望定制更炫的文本效果,比如数学表达式、电子书,则还需要求助于"CoreText.framework"。

创建系统默认字体

iOS系统默认支持好几种字体,并且系统界面也是有特定字体的,比如iOS9就因为更新了新字体--中文字体「苹方」以及英文字体「San Francisco」,被大家甚是调侃了一番。

所以最简单的创建字体的方式就是用系统默认的字体,比如:

class func systemFont(ofSize fontSize: CGFloat) -> UIFonts // 获取指定大小的系统字体
class func systemFont(ofSize fontSize: CGFloat,                weight: CGFloat) -> UIFont // 获取指定大小和粗细的系统字体
class func boldSystemFont(ofSize fontSize: CGFloat) -> UIFont //获得指定大小的粗体系统字体
class func italicSystemFont(ofSize fontSize: CGFloat) -> UIFont // 获取指定大小的斜体系统字体
class func monospacedDigitSystemFont(ofSize fontSize: CGFloat,                               weight: CGFloat) -> UIFont // 获取指定大小和粗细的系统字体,并且其中的数字字符间距相等

这里介绍的几种方法,无非是在字体大小、粗细、斜体上不同的,但都是系统当前的默认字体。除此之外,iOS10还提供了一套UI推荐标准字号方法:

class func preferredFont(forTextStyle style: UIFontTextStyle) -> UIFont

其通过枚举值规定了各个场景的字号,比如

  • 大标题 .title1 类似HTML的h1
  • 中标题 .title2 类似HTML的h2
  • 小标题 .title3 类似HTML的h3
  • 大抬头 .headline
  • 小抬头 .subheadline
  • 文本内容主体 .body

用这里的枚举,会更符合iOS Human Interface Guidelines

获取系统支持的字体

当然,系统除了默认字体以外还内置支持了很多自带字体,我们可以通过UIFont的类方法:

class func fontNames(forFamilyName familyName: String) -> [String]

获得系统内置的字体族中所有字体的名称。然后再通过

init?(name fontName: String,  size fontSize: CGFloat)

用字体名来创建指定字体。

那这里字体族如何知道呢?一样的UIFont提供了接口:

class var familyNames: [String] { get }

比如在iOS10.0.2上,系统自带的字体:

let family = UIFont.familyNames
for fam in family {
    let fonts = UIFont.fontNames(forFamilyName: fam)
    print("Font Family: \(fam)")
    for f in fonts {
        print("\t\t Font:\(f)")
    }
}

得到一个特别长的列表:

Font Family: Copperplate
         Font:Copperplate-Light
         Font:Copperplate
         Font:Copperplate-Bold
Font Family: Heiti SC
Font Family: Kohinoor Telugu
         Font:KohinoorTelugu-Regular
         Font:KohinoorTelugu-Medium
         Font:KohinoorTelugu-Light
Font Family: Thonburi
         Font:Thonburi
         Font:Thonburi-Bold
         Font:Thonburi-Light
Font Family: Heiti TC
Font Family: Courier New
         Font:CourierNewPS-BoldMT
         Font:CourierNewPS-ItalicMT
         Font:CourierNewPSMT
         Font:CourierNewPS-BoldItalicMT
...

获取字体属性

字体的组成结构和空间占用在Apple的Text Programming Guide for iOS有详细描述,当然我们这里不去深究CoreText的排班过程,仅看看对UIKit的影响,来看一个字体的空间:

font_structure

UIFont提供了一系列的Getter来获得这些属性:

属性 类型 含义
pointSize CGFloat 字体大小
ascender CGFloat 字体基线距离最高点的位置
descender CGFloat 字体的基线距离最低点的位置
leading CGFloat 前导距离
capHeight CGFloat 主体高度
xHeight CGFloat 重心高度
lineHeight CGFloat 行高

这些属性用中文描述出来,不是特别容易理解,可以将属性名对照上面的图进行理解。

除了这几个属性,还可以通过下面的方法获得字体的名称:

var familyName: String { get }
var fontName: String { get }

前者获取字体的家族名,后者获得字体名。

创建自定义字体

说完系统字体,现在我们来说这篇文章的重点,如何加载自定义字体。根据资源的提供者,我们可以分成三类来说:随包Bundle、动态数据文件以及Apple提供的系统扩展

随包Bundle字体

最简单的方式就是如同做PC端应用或者做游戏一样,把一个字体包当做一个Bundle资源打入ipa包中。所以我将他称为"随包Bundle字体"。比如要在App中加入Monaco这个程序员专属字体。

首先将自己的字体文件像一个Bundle文件一样加入到Xcode工程中,比如这里我加入一个MONACO.ttf的字体文件,然后在plist文件中添加“Fonts provided by application”表示数组的字段,里面每个单元就是一个要加入的字体的文件名。

font_plist

之后,在我们的系统库中就有了这个字体了,比如上面的枚举系统的代码就会看到:

Font Family: MONACO
     Font:MONACO

最后我们再如上面介绍的调用:

let monacoFont = UIFont(name: "Monaco", size: UIFont.systemFontSize)

来创建一个Monaco字体。

这个方法虽然简单,但是也带来了一些缺点。比如发布包ipa会以为字体而变大。

动态数据文件字体

上面说了,虽然“随包Bundle字体”能很方便的解决增加一个系统不支持的字体的问题,但是随之带来的却是ipa包的增大。那要如何解决呢?

做程序猿的自然就会想到,能不能资源不随包发布,而在程序运行的期间从网上下载到document目录在加载呢?

答案当然是肯定的,所以我又将其称为“动态数据文件字体”。但是要用到一个不属于UIKit的技术:CoreText里面的CTFontManagerRegisterGraphicsFont函数,所以我们需要先 :

import CoreText

然后假设字体文件内容下载到了NSData

let fontData = NSData(contentsOfFile: fontURL!) //这里用本地文件模拟网络下载到NSData中

来看完整代码:

let monacoFont = UIFont(name: "Monaco", size: UIFont.systemFontSize)
print("monacoFont is \(monacoFont)")
let fontURL = Bundle.main.path(forResource: "MONACO", ofType: "ttf")
let fontData = NSData(contentsOfFile: fontURL!)
let providerRef = CGDataProvider(data: fontData!)
let fontRef = CGFont(providerRef!)
var error: Unmanaged<CFError>?
if CTFontManagerRegisterGraphicsFont(fontRef, &error) {
    let mFont = UIFont(name: "Monaco", size: UIFont.systemFontSize)
    print("mFont is \(mFont)")
}

可以得到输出:

monacoFont is nil
mFont is Optional(<UICTFont: 0x100c11cc0> font-family: "MONACO"; font-weight: normal; font-style: normal; font-size: 14.00pt)

第一次没有“MONACO”等注册后就有了。

这里要注意下,Swift3以后CoreText.framework的改变。这里用了CGDataProvider以及CGFont

Apple提供的系统扩展字体

通过动态下载字体文件好像基本上完美解决所有问题了,但是还有个小问题,很多字体尤其是中文字体都是有版权的,如果App发现量巨大,这块被发现了就不好了。但是iOS系统就只提供了上面罗列出来的哪些字体么?好像都没几个中文的。

既然中国用户为Apple共享了那么多美金,Apple自然也不会忘记中文的支持,我们会发现Mac上提供了“字体簿”(FontBook)是提供了N多中文字体,比如中国特色的“隶书”:

font_book

这里会看到有个“PostScript name”,Apple提供了一种通过这个名称下载字体到自己的手机系统位置的方式,也就是为出厂的手机系统的位置(/private/var/mobile/Library/Assets/com_apple_MobileAsset_Font/)新增一个字体,并且所有的应用都可以用了(上面两种方法都是针对当前App)的。

这里我们查询到图中的隶书的PostScript为“STBaoliSC-Regular”,然后我们一样用CoreText的服务:

let fontPSName = "STBaoliSC-Regular"

var attr : [String:String] = [kCTFontNameAttribute as String : fontPSName]
let desc = CTFontDescriptorCreateWithAttributes(attr as CFDictionary)
var descs : [CTFontDescriptor] = [desc,]
CTFontDescriptorMatchFontDescriptorsWithProgressHandler(descs as CFArray, nil) { (stat, prama) -> Bool in
    //
    if .didFinish == stat {
        let nishuFont = UIFont(name: fontPSName, size: UIFont.systemFontSize)
        print("nishuFont is \(nishuFont)")
    }
    return true
}

如果你是使用的Xcode8的话,会发现这里有一堆的下载日志,当然最后回打印:

"SandboxExtension" => <string: 0x174250170> { length = 249, contents = "524979fa2be9f37ea83d93d0f3f00b1ef8977255;00000000;00000000;0000000000000015;com.apple.assets.read;00000001;01000004;00000000014f47f8;/private/var/MobileAsset/Assets/com_apple_MobileAsset_Font3/3abf40766b4cf50b77ebd28e6affbd1849ea61c6.asset/AssetData" }
}
nishuFont is Optional(<UICTFont: 0x100c16b90> font-family: "STBaoliSC-Regular"; font-weight: normal; font-style: normal; font-size: 14.00pt)

这里可以看到,字体下载到了"/private/var/MobileAsset/Assets/com_apple_MobileAsset_Font3/3abf40766b4cf50b77ebd28e6affbd1849ea61c6.asset/AssetData",然后成功创建了字体。这里注意哈,字体名也是“PostScript name”而不是截图中的字体名。

这里使用了CoreText的CTFontDescriptorMatchFontDescriptorsWithProgressHandler,一个下载字体的系统服务。既然是下载动作,肯定是异步的了,所以这里用了一个闭包CTFontDescriptorProgressHandler

typealias CTFontDescriptorProgressHandler = (CTFontDescriptorMatchingState, CFDictionary) -> Bool

来处理回调结果。CTFontDescriptorMatchingState定了了各个下载结果,这里,我们只关注了成功的结果。而CFDictionary里面则包含了进度(kCTFontDescriptorMatchingPercentage)等信息

因为下载动作不是在UI线程里面,所以这里假设要更新UI上的下载进度,需要通过dispatch_async来实现。

stat 下载状态
didBegin 开始下载时
didFinish 下载完成时
willBeginQuerying 第一次向服务器发起查询时
stalled 等待服务器相应
willBeginDownloading 每当有新字体下载时
downloading 正在下载
didFinishDownloading 一次下载完成
didMatch 当找到一个字体
case didFailWithError 出错时
下载信息key 类型 下载信息值 对应状态
kCTFontDescriptorMatchingSourceDescriptor UIFontDescriptor 当前字体下载完成时 .didFinish
kCTFontDescriptorMatchingDescriptors Array 要下载的字体描述, willBeginQuerying
kCTFontDescriptorMatchingResult Array 匹配的字体描述UIFontDescriptor .didMatch
kCTFontDescriptorMatchingPercentage CFNumber 进度0-100之间 .downloading
kCTFontDescriptorMatchingCurrentAssetSize CFNubmer 当前下载大大小 .downloading
kCTFontDescriptorMatchingTotalDownloadedSize CFNumber 总下载大小 .downloading
kCTFontDescriptorMatchingError CFError 出错信息 .didFailWithError

总结

UIFont提供了对系统自带字体的访问,从而为UILabel、UITextView等提供丰富的字体支持。除了可以设置系统自己的字体之外,UIFont还可以创建自己提供的资源字体,字体资源既可以随包打入ipa也可以放在网络上通过下载到资源包中,同时Apple还提供了大量的扩展字体。通过这些字体,我们可以对UI界面做非常大的自定义。但是UIFont并不能解决所有的文字排版问题,类似于数学表达式,电子书排版这种负责的文字排版,我们还需要借助CoreText.framework。

参考:

  1. Text Programming Guide for iOS

  2. UIFont Class Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容