Apple聲稱鼓勵第三方App可以支持動態文本。可是,若是你嘗試在App中實現這個特性,你會發現其中有不少坑(例如靜態cell和定製cell樣式)。在本文中,咱們將介紹動態文本的機理以及它在各類場景中的應用。咱們也會介紹一些Swift代碼,這將極大地幫助你在本身的App中實現動態文本。ios
在iOS7中,Apple引入了動態文本的概念。動態文本容許用戶經過設置程序修改App的字體大小(只是針對支持動態文本的App)。swift
對於視力很差的用戶,很容易就能將文本字體增大,另外一方面,對於視力較好的用戶,則能夠將字體改小,以便在同一屏中容納更多的內容。xcode
要在設置App中修改動態文本設置,選擇通用->輔助功能->更大字體,如圖1所示。用戶經過拖動滑條來改變字體的大小。要使用更大的字體,能夠打開屏幕上方的「輔助功能中的更大字體」開關。
圖 1 – 更大字體設置app
圖2左邊的圖顯示的是聯繫人App在最小字體下的顯示效果,右邊的圖是在沒有打開「輔助功能中的更大字體」時的最大字體的顯示效果。iphone
圖 2 – 聯繫人 app 的小字體和大字體async
下面是系統內置的支持動態文本的App:編輯器
正是由於這些App都支持動態文本,用戶也會要求第三方的App也支持動態文本。先讓咱們看看最終的效果。佈局
咱們先把項目check out出來。你能夠在[這裏](http://www.iosappsfornonprogrammers.com/media/blog/iDeliverMobileDynamicType.zip)下載示例項目。性能
當前,示例程序中的全部UI控件的字體名稱和大小都是硬編碼的。要支持動態文本,咱們須要將這些硬編碼的內容替換成文本樣式。測試
文本樣式是相似文字處理程序中的」樣式「的概念。樣式可以讓咱們以相對大小和字重的方式指定某段文本的字體。圖3列出了可選的字體風格。
圖 3 – 動態文本中使用的文本樣式
讓咱們先來試一試。
圖 6 – 動態文本已經起做用了
注意,爲了適應大文本,行高被稍微增高了一點。
如今,讓咱們來看看,當用戶在設置程序中改變字體大小後,又會發生什麼狀況?
你看到的例子實際上至關於咱們進行了如下動做:
就如你在圖5中所見,示例中的Table View確實使用了模板單元格。
若是你選擇Deliveries場景中的Table View中的單元格,在屬性面板中你會看到其style是Subtitle(圖8)。
圖 8 – 單元格的Style 爲 Subtitle.
呆會你會明白,Table View的靜態單元格和動態單元格是大相徑庭的。
在一些iOS內置應用中,蘋果容許文本在加大字體後被截斷。在聯繫人應用中,你會在email地址中看到這樣的例子(圖9)。
圖 9 – email 地址被截斷
在你的App中,你能夠容許文本被截斷,或者換到下一行。如今,讓咱們看看如何換行。
在Deliveries場景中,選擇detail text標籤,打開屬性面板,將number of lines設置爲0。這會致使email地址換行(圖10)。
圖 10 – 標籤文字超出了行高
然而,iOS卻不能正確地計算行高。接下來咱們就來討論動態單元格的行高。
當在Table View中使用動態文本時,表格的行高必須也可以自動適應字體大小的變化。蘋果提供了3種解決辦法:
儘管你的表格的行高應該是動態計算的,但你仍然能夠像過去同樣使用rowHeight屬性。每當字體大小改變(後面咱們會講到如何得到相應的通知),咱們都須要從新計算新的行高,並設置表格的rowHeight屬性。
使用rowHeight屬性的優勢是速度。它提供了最優的滾動性能,由於當用戶滾動表格時,不須要進行任何計算。
缺點是咱們必須手動計算正確的行高。另外,全部的單元格都必須使用相同的行高。
在iOS7中,默認行高爲44,在iOS8中,默認行高是UITableViewAutomaticDimenssion(一個常量,等於-1)。若是你要使用rowHeight屬性,你須要在屬性面板中或者viewDidLoad方法中設置它的初始值。
咱們能夠用tableView:heightForRowAtIndexPath: 方法單獨計算每一行的行高。
這種方法沒有什麼明顯的優勢。每一行的行高都會事先被詢問,無論該行是否是已經被建立。若是你的表格有上千行,這會致使性能上的延遲。
若是使用自適應大小單元格,而不是使用rowHeight屬性,則咱們既不用設置estimatedRowHeight屬性,也不用實現tableView:estimatedHeightForRowAtIndexPath:協議方法。
建立自適應大小單元格的步驟大體以下:
當表格滾動,該行即將顯示到屏幕時,單元格被建立。
此時單元格會被詢問其大小。
在第3步,又有兩種計算單元格高度的方式:
Table View會調用每一個單元格的systemLayoutSizeFittingSize方法。該方法返回單元格是否已經實現了佈局約束,若是實現,則自動佈局引擎負責指定單元格的大小。
若是沒有實現本身的佈局約束,TableView調用單元格的sizeThatFits方法。在這個方法中咱們能夠自行計算單元格高度並返回——而單元格的寬度是已經計算好的。
先讓咱們在示例項目中試下自動佈局,看如何在動態文本中使用。首先須要肯定故事板是否支持自動佈局。
將Deliveries場景的表格單元格的風格修改成自定義。選中表格單元格,打開屬性面板,將Sytle設置爲Custom。這會將單元格的兩個標籤刪除。
在IB中改變單元格的高度是很是簡單的。點擊表格的灰色區域,在Size面板,將 Row Height設置爲 60。
從Object Library中拖一個標籤到單元格中,你能夠看到它的水平和垂直導線,如圖12所示。
圖 12 -加一個標籤到單元格中
如今,當Deliveries場景第一次加載時,表格中的標籤採用用戶在設置程序中已經設好的字體大小顯示。顯然,當單元格採用內置的Subtitle樣式時,若是用戶改變了字體大小,則標籤上的字體大小也會隨之改變。但不幸的是,若是使用的是自定義單元格,這個機制就無效了。咱們先來測試一下。
要讓自定義單元格中的標籤(或其餘任何文本控件)可以根據設置程序中的字體大小來改變其文本字體,咱們必須:
在viewDidLoad方法中向通知中心註冊UIContentSizeCategoryDidChangeNotification通知。
在代碼中響應字體改變通知,將標籤的樣式從新設置正確。例如:
在ViewController的deinit方法中註銷通知。
讓咱們以Deliveries爲例進行演示。
上述代碼讓通知中心在用戶改變了動態文本設置以後調用handleDynamicTypeChange方法。
在這個方法中從新加載Table View。
如今在tableView:cellForRowAtIndexPath:方法最後加入代碼:
這段代碼從新設置標籤的字體風格。
最後,在viewDidLoad方法下面增長deinit方法:
讓咱們測試一下上述代碼。點擊Run按鈕,當App啓動後,咱們將看到標籤文本變成了先前改變的小字體。按下Shift+Command+H鍵回到Home屏。
打開設置程序,進入General->Accessibility->Larger Text界面,將滑塊向右拖到,調大字體。
按下Shift+Command+H鍵,回到Home屏,切到iDeliverMobileCD程序。咱們將看到,標籤文本已經在沒有重啓App的前提下變大了!
回到Xcode,終止程序。
這種方法有如下幾個弊端:
每當咱們須要在不一樣的地方重複加入冗餘的代碼時,咱們就應該考慮建立一種通用的解決方法以在全部項目中重用代碼。
我已經建立了幾個類,你能夠在本身的項目中更容易地實現動態文本。在測試運行以前,先移除咱們在前面添加的代碼。
從viewDidLoad方法中移除下列代碼:
從viewDidLoad方法下移除該處理方法:
從tableView:cellForRowAtIndexPath:方法中移除下列代碼:
刪除位於viewDidLoad下面的deinit方法:
如今來看看更好的解決方案。
在項目導航窗口,右鍵點擊Main.storyboard,選擇Add Files to iDeliverMobileCD…。
在添加文件對話框,反選Copy items if needed。
在項目文件夾,選擇mmDynamicTypeExtensions.swift文件,而後點擊Add。等一會咱們在查看代碼,如今先看一下如何在設計時和運行時使用這些代碼。
在項目導航窗口,選中Main.storyboard。在Deliveries場景,選擇單元格中位於上方的Heading Label。
打開屬性面板,注意,顯示了一個新的Type Observer屬性(圖18)。
圖 18 – Type Observer 屬性
剛纔添加到項目中的代碼爲標籤添加了一個Type Observer屬性。
將Type Observer屬性設置爲On。
選擇Subhead標籤,在屬性面板,將Type Observer屬性設置爲On。
全部工做完成,讓咱們來測試一下。點Run按鈕,當程序啓動後,咱們將看到顯示了先前咱們設置的大字體文本。按下Shift+Command+H鍵回到Home屏。
打開設置程序,進入General->Accessibility->Larger Text界面。將滑塊向左拖動以減少字體。
按下Shift+Command+H返回Home屏,切回iDeliverMobileCD程序。你會看到,標籤字體大小已然改變!
返回Xcode,終止程序。
讓咱們來看看代碼。
在項目導航窗口,打開mmDynamicTypeExtension.swift文件。
在文件頂部,是一個協議,該協議僅包含了一個叫作typeObserver的Bool屬性。也就是你在標籤中設置爲On的屬性。
在協議聲明以後,又定義了一個UILabel的擴展:
這個擴展聲明瞭對DynamicTypeChangeHandler協議的實現並實現了typeObserver屬性。@IBInspectable屬性代表這個屬性能夠顯示在屬性面板中。這個屬性的setter方法調用了動態文本管理器的registerControler方法。
向下滾動代碼,咱們能夠看到DynamicTypeManager對象被實現爲一個單例對象:
單例模式使得類的實例始終只有一個。當建立一個類的實例時,若是類還未被實例化,則建立新的實例。若是類已經被實例化,則返回現有的實例對象。
圖19是一張序列圖,顯示了動態文本改變的處理邏輯。
圖 19 – 動態文本處理的序列圖
這是幾個關鍵步驟:
當typeObserver屬性爲true時(經過屬性面板中),UI控件向動態文本管理器進行註冊,將一個 keypath傳遞給控件的字體屬性。
當第一個控件進行註冊時,Dynamic Type Manager實例被建立,並開始向通知中心註冊動態文本改變通知。
建立一個對該控件的引用並將它的字體樣式保存到一個NSMapTable中。一個Map Table是字典的一種,保存的是對象的弱引用,所以當key或value被解構時保存的對象自動被移除。這對咱們來講再恰當不過了:咱們並不想保持對UI控件的強引用。當UI控件釋放後(例如,用戶導航到另外一個View Controller,當前View Contoller被解構),該控件在NSMapTable(感謝Big Nerd Ranch分享了這個技巧)中的引用將被自動移除。
當用戶在設置程序中改變字體大小,通知中心會通知DynamicTypeManager對象。
DynamicTypeManager對象遍歷Map Table中的UI控件,對每一個控件,都設置它們的字體樣式,並調用sizeToFit方法。
上圖這種方式有什麼好處?
它使用的是擴展而不是繼承。所以咱們可使用「盒子以外的」UIKit組件。
你只須要將mmDynamicTypeExtension.swift添加到項目中就可使用它。
這種方式使用的是鬆散耦合。UI控件將本身的屬性提供給動態文本管理器。這意味着你註冊自定義控件(或者蘋果將來發布的新控件),而不須要修改動態文本管理器。
不須要在設爲默認樣式的模板單元格上使用這個特性,你只須要選擇將哪一個控件註冊到動態文本管理器就好了。
讓咱們來看一下如何在靜態單元格中使用動態文本。
在iDeliverMobileDynamicType項目中,選中Main.storyboard文件,找到Deliveries場景(圖20)。
圖 20 – Shipment 場景
在這個場景的Table View中,如同Deliveries場景同樣包含了動態模板單元格。不一樣的是Shipment場景中既包含了動態文本也包含了靜態文本。藍色的文本(Phone、Text、和ID)和Status是靜態的。也就是說這些文本在不一樣的發貨單中是固定不變的。其餘文本則是動態的,每一個發貨單都不同。
要讓這些標籤也使用動態文本,選擇每一個標籤,而後在屬性面板中將Font設爲任意一種iOS字體風格,好比:
Name – Headline
Address Line 1 – Body
Address Line 2 – Subhead
Phone labels – Body
Text labels – Body
Status labels – Body
ID labels – Body
iPod Touch label – Body
在ShipmentViewController.swift文件中,在viewDidLoad方法最後一行加入代碼:
記住,這些代碼用於告訴Table View使用自適應大小單元格。
如今讓咱們看看效果。點擊Run按鈕,當程序啓動,在Deliveries窗口選擇shipment進入Shipment窗口。咱們將看到顯示的是咱們先前在設置程序中設置的小字體。
如今讓咱們看看在程序運行的狀況下App如何處理動態文本的改變。切換到設置程序,選擇最大字體。再回到iDeliverMobileDynamicTypeApp。
如圖21所示,全部的靜態文本都不見了!這是iOS自己的一個Bug,不幸的是,在Xcode6.2中仍然未獲得解決。我但願蘋果之後能修正這個Bug,但目前咱們不須要自定義單元格就能夠解決這個問題。咱們只須要在tableView:cellForRowAtIndexPath: 方法中增長一點代碼去重置靜態文本:
圖 21 -靜態文本不見了!
還有一個問題是,第一個單元格再也不居中對齊。這個問題也是在同一個方法中增長代碼來解決。
在文件的tableView:cellForRowAtIndexPath: 方法中,添加高亮部分的代碼:
點擊Run按鈕,當程序啓動,進入Shipment頁面。
3.切到設置程序將字體設置爲最小。回到iDeliverMobileDynamicType,咱們將發現靜態文本又回來了(圖22)!這是由於當動態文本字體發生改變時,Table View的reloadData方法自動會調用。
圖 22 – 靜態文本又回來了
去年,咱們公司在 MacWorld 展會上有一個展臺,展現個人iOS App開發圖書系列。一個有弱視的讀者來展位上問我,能不能教一下開發者們如何建立適用於弱視患者的App。這致使了本文的產生,我終於能夠說Yes了,我但願本文可以讓你在面對這個問題的時候可以一樣說Yes。