庖丁UIKit之UIFont

App界面中少不了的肯定是各種各樣的文字內容。那如何讓文字在不同的位置都有相應的表現(xiàn)效果呢?像Word操作一樣,自然干是通過設置各種各樣的字體來表達不同的效果。UIKit為此提供了UIFont類型來表示字體。當然,UIFont只是粗略的對已有的字體進行表現(xiàn),如果希望定制更炫的文本效果,比如數(shù)學表達式、電子書,則還需要求助于"CoreText.framework"。

創(chuàng)建系統(tǒng)默認字體

iOS系統(tǒng)默認支持好幾種字體,并且系統(tǒng)界面也是有特定字體的,比如iOS9就因為更新了新字體--中文字體「蘋方」以及英文字體「San Francisco」,被大家甚是調侃了一番。

所以最簡單的創(chuàng)建字體的方式就是用系統(tǒng)默認的字體,比如:

class func systemFont(ofSize fontSize: CGFloat) -> UIFonts // 獲取指定大小的系統(tǒng)字體
class func systemFont(ofSize fontSize: CGFloat,                weight: CGFloat) -> UIFont // 獲取指定大小和粗細的系統(tǒng)字體
class func boldSystemFont(ofSize fontSize: CGFloat) -> UIFont //獲得指定大小的粗體系統(tǒng)字體
class func italicSystemFont(ofSize fontSize: CGFloat) -> UIFont // 獲取指定大小的斜體系統(tǒng)字體
class func monospacedDigitSystemFont(ofSize fontSize: CGFloat,                               weight: CGFloat) -> UIFont // 獲取指定大小和粗細的系統(tǒng)字體,并且其中的數(shù)字字符間距相等

這里介紹的幾種方法,無非是在字體大小、粗細、斜體上不同的,但都是系統(tǒng)當前的默認字體。除此之外,iOS10還提供了一套UI推薦標準字號方法:

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

其通過枚舉值規(guī)定了各個場景的字號,比如

  • 大標題 .title1 類似HTML的h1
  • 中標題 .title2 類似HTML的h2
  • 小標題 .title3 類似HTML的h3
  • 大抬頭 .headline
  • 小抬頭 .subheadline
  • 文本內容主體 .body

用這里的枚舉,會更符合iOS Human Interface Guidelines

獲取系統(tǒng)支持的字體

當然,系統(tǒng)除了默認字體以外還內置支持了很多自帶字體,我們可以通過UIFont的類方法:

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

獲得系統(tǒng)內置的字體族中所有字體的名稱。然后再通過

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

用字體名來創(chuàng)建指定字體。

那這里字體族如何知道呢?一樣的UIFont提供了接口:

class var familyNames: [String] { get }

比如在iOS10.0.2上,系統(tǒng)自帶的字體:

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 }

前者獲取字體的家族名,后者獲得字體名。

創(chuàng)建自定義字體

說完系統(tǒng)字體,現(xiàn)在我們來說這篇文章的重點,如何加載自定義字體。根據(jù)資源的提供者,我們可以分成三類來說:隨包Bundle、動態(tài)數(shù)據(jù)文件以及Apple提供的系統(tǒng)擴展

隨包Bundle字體

最簡單的方式就是如同做PC端應用或者做游戲一樣,把一個字體包當做一個Bundle資源打入ipa包中。所以我將他稱為"隨包Bundle字體"。比如要在App中加入Monaco這個程序員專屬字體。

首先將自己的字體文件像一個Bundle文件一樣加入到Xcode工程中,比如這里我加入一個MONACO.ttf的字體文件,然后在plist文件中添加“Fonts provided by application”表示數(shù)組的字段,里面每個單元就是一個要加入的字體的文件名。

font_plist

之后,在我們的系統(tǒng)庫中就有了這個字體了,比如上面的枚舉系統(tǒng)的代碼就會看到:

Font Family: MONACO
     Font:MONACO

最后我們再如上面介紹的調用:

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

來創(chuàng)建一個Monaco字體。

這個方法雖然簡單,但是也帶來了一些缺點。比如發(fā)布包ipa會以為字體而變大。

動態(tài)數(shù)據(jù)文件字體

上面說了,雖然“隨包Bundle字體”能很方便的解決增加一個系統(tǒng)不支持的字體的問題,但是隨之帶來的卻是ipa包的增大。那要如何解決呢?

做程序猿的自然就會想到,能不能資源不隨包發(fā)布,而在程序運行的期間從網上下載到document目錄在加載呢?

答案當然是肯定的,所以我又將其稱為“動態(tài)數(shù)據(jù)文件字體”。但是要用到一個不屬于UIKit的技術:CoreText里面的CTFontManagerRegisterGraphicsFont函數(shù),所以我們需要先 :

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提供的系統(tǒng)擴展字體

通過動態(tài)下載字體文件好像基本上完美解決所有問題了,但是還有個小問題,很多字體尤其是中文字體都是有版權的,如果App發(fā)現(xiàn)量巨大,這塊被發(fā)現(xiàn)了就不好了。但是iOS系統(tǒng)就只提供了上面羅列出來的哪些字體么?好像都沒幾個中文的。

既然中國用戶為Apple共享了那么多美金,Apple自然也不會忘記中文的支持,我們會發(fā)現(xiàn)Mac上提供了“字體簿”(FontBook)是提供了N多中文字體,比如中國特色的“隸書”:

font_book

這里會看到有個“PostScript name”,Apple提供了一種通過這個名稱下載字體到自己的手機系統(tǒng)位置的方式,也就是為出廠的手機系統(tǒng)的位置(/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的話,會發(fā)現(xiàn)這里有一堆的下載日志,當然最后回打印:

"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",然后成功創(chuàng)建了字體。這里注意哈,字體名也是“PostScript name”而不是截圖中的字體名。

這里使用了CoreText的CTFontDescriptorMatchFontDescriptorsWithProgressHandler,一個下載字體的系統(tǒng)服務。既然是下載動作,肯定是異步的了,所以這里用了一個閉包CTFontDescriptorProgressHandler

typealias CTFontDescriptorProgressHandler = (CTFontDescriptorMatchingState, CFDictionary) -> Bool

來處理回調結果。CTFontDescriptorMatchingState定了了各個下載結果,這里,我們只關注了成功的結果。而CFDictionary里面則包含了進度(kCTFontDescriptorMatchingPercentage)等信息

因為下載動作不是在UI線程里面,所以這里假設要更新UI上的下載進度,需要通過dispatch_async來實現(xiàn)。

stat 下載狀態(tài)
didBegin 開始下載時
didFinish 下載完成時
willBeginQuerying 第一次向服務器發(fā)起查詢時
stalled 等待服務器相應
willBeginDownloading 每當有新字體下載時
downloading 正在下載
didFinishDownloading 一次下載完成
didMatch 當找到一個字體
case didFailWithError 出錯時
下載信息key 類型 下載信息值 對應狀態(tài)
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提供了對系統(tǒng)自帶字體的訪問,從而為UILabel、UITextView等提供豐富的字體支持。除了可以設置系統(tǒng)自己的字體之外,UIFont還可以創(chuàng)建自己提供的資源字體,字體資源既可以隨包打入ipa也可以放在網絡上通過下載到資源包中,同時Apple還提供了大量的擴展字體。通過這些字體,我們可以對UI界面做非常大的自定義。但是UIFont并不能解決所有的文字排版問題,類似于數(shù)學表達式,電子書排版這種負責的文字排版,我們還需要借助CoreText.framework。

參考:

  1. Text Programming Guide for iOS

  2. UIFont Class Reference

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容