iOS8中的動態文本

 

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也支持動態文本。先讓咱們看看最終的效果。佈局

運行示例App

咱們先把項目check out出來。你能夠在[這裏](http://www.iosappsfornonprogrammers.com/media/blog/iDeliverMobileDynamicType.zip)下載示例項目。性能

  1. 在Xcode中,打開iDeliverMobile.xcodeproj文件。
  2. 運行程序以前,打開Xcode->Open->Developer Tool->iOS Simulator菜單。
  3. 在模擬器菜單中,選擇Hardware->Device->iPhone 5S。
  4. 而後,打開設置程序,找到General->Accessibility->Larger Text。將滑塊向右拖動至最大。
  5. 回到Xcode,點擊Scheme,從設備列表中選擇iPhone 5S。
  6. 點擊Run,在模擬器中運行程序,你會看到字體仍然是小字體。顯然,咱們的App還不支持動態文本。
  7. 回到Xcode,退出App。

使用文本樣式

當前,示例程序中的全部UI控件的字體名稱和大小都是硬編碼的。要支持動態文本,咱們須要將這些硬編碼的內容替換成文本樣式。測試

文本樣式是相似文字處理程序中的」樣式「的概念。樣式可以讓咱們以相對大小和字重的方式指定某段文本的字體。圖3列出了可選的字體風格。

圖 3 – 動態文本中使用的文本樣式

讓咱們先來試一試。

  1. 在項目導航窗口中,選擇Main.storyboard文件。
  2. 在Deliveries場景中,選擇Feng Wong標籤,打開屬性面板,將Font修改成Headline(圖4)。

    圖 4 – 設置Font 爲 Headline.
  3. 選擇address標籤,將Font設置爲Subhead。
  4. 要預覽所有動態文本效果,將address標籤的AutoShrink設爲Fixed Font Size(圖5)。這會讓address標籤的text被截斷,但隨後咱們會解決這個問題。

    圖 5 – 設置address 的 Font 爲 Subhead , Autoshrink 爲 Fixed Font Size
  5. 點擊Run,在模擬器中運行程序,你會發現,字體的大小已經發生改變(圖6)。


圖 6 – 動態文本已經起做用了

注意,爲了適應大文本,行高被稍微增高了一點。

如今,讓咱們來看看,當用戶在設置程序中改變字體大小後,又會發生什麼狀況?

  1. 若是 iDeliverMobileCD 未啓動,請點擊Xcode中的Run按鈕。
  2. 當App一啓動,按 Shift+Command+H ,切到Home屏幕。
  3. 打開設置程序,找到 General -> Accessibility -> Larger Text ,將滑塊向左拖動,以減小字體大小。
  4. 按下 Shift+Command+H ,回到Home屏,從新打開iDeliverMobileCD 。注意標籤的字體已經變小了(圖7)。

    Figure 7 – The fonts change dynamically.
  5. 回到Xcode ,退出程序。

動態文本和模板單元格

你看到的例子實際上至關於咱們進行了如下動做:

  1. 將Table view 的Content 設爲Dynamic Prototype(動態模板)
  2. 將Cell的style設爲任意一種內置類型

就如你在圖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種解決辦法:

  1. 修改表格的rowHeight屬性。
  2. 實現tableView:heightForRowAtIndexPath: 協議方法。
  3. 自適應大小的單元格。

使用rowHeight屬性

儘管你的表格的行高應該是動態計算的,但你仍然能夠像過去同樣使用rowHeight屬性。每當字體大小改變(後面咱們會講到如何得到相應的通知),咱們都須要從新計算新的行高,並設置表格的rowHeight屬性。

使用rowHeight屬性的優勢是速度。它提供了最優的滾動性能,由於當用戶滾動表格時,不須要進行任何計算。

缺點是咱們必須手動計算正確的行高。另外,全部的單元格都必須使用相同的行高。

在iOS7中,默認行高爲44,在iOS8中,默認行高是UITableViewAutomaticDimenssion(一個常量,等於-1)。若是你要使用rowHeight屬性,你須要在屬性面板中或者viewDidLoad方法中設置它的初始值。

實現heightForRowAtIndexPath方法

咱們能夠用tableView:heightForRowAtIndexPath: 方法單獨計算每一行的行高。

這種方法沒有什麼明顯的優勢。每一行的行高都會事先被詢問,無論該行是否是已經被建立。若是你的表格有上千行,這會致使性能上的延遲。

自適應大小單元格

若是使用自適應大小單元格,而不是使用rowHeight屬性,則咱們既不用設置estimatedRowHeight屬性,也不用實現tableView:estimatedHeightForRowAtIndexPath:協議方法。

建立自適應大小單元格的步驟大體以下:

  1. 在繪製每一個單元格以前,它會使用estimatedRowHeight屬性或者調用相關的委託方法。
  2. 當表格滾動,該行即將顯示到屏幕時,單元格被建立。

  3. 此時單元格會被詢問其大小。

  4. 若是這個高度和estimatedHeight不一樣,則使用該高度進行調整。
  5. 顯示單元格。

在第3步,又有兩種計算單元格高度的方式:

  1. 自動佈局
  2. 手動計算大小

Table View會調用每一個單元格的systemLayoutSizeFittingSize方法。該方法返回單元格是否已經實現了佈局約束,若是實現,則自動佈局引擎負責指定單元格的大小。

若是沒有實現本身的佈局約束,TableView調用單元格的sizeThatFits方法。在這個方法中咱們能夠自行計算單元格高度並返回——而單元格的寬度是已經計算好的。

在動態文本中使用自動佈局

先讓咱們在示例項目中試下自動佈局,看如何在動態文本中使用。首先須要肯定故事板是否支持自動佈局。

  1. 點擊故事板的白色背景
  2. 打開文件面板。在Interface Builder Document欄下,確保Use Auto Layout 已選中(圖11)。

    圖 11 -選中 Use Auto Layout
  3. 將Deliveries場景的表格單元格的風格修改成自定義。選中表格單元格,打開屬性面板,將Sytle設置爲Custom。這會將單元格的兩個標籤刪除。

  4. 在IB中改變單元格的高度是很是簡單的。點擊表格的灰色區域,在Size面板,將 Row Height設置爲 60。

  5. 從Object Library中拖一個標籤到單元格中,你能夠看到它的水平和垂直導線,如圖12所示。

    圖 12 -加一個標籤到單元格中

  6. 拖住標籤右邊的resizing手柄,將它向右拖,直到垂直導線到達圖13中所示的位置。

    圖 13 – 設置標籤的寬度
  7. 保持標籤選中狀態,打開屬性面板,設置Font爲Headline,Tag爲1。
  8. 再拖一個標籤放到第一個標籤下方,使其導線如圖14所示。

    圖 14 – 添加第2個標籤
  9. 拖住標籤右邊的resizing手柄,向右拖,直到其垂直導線和第一個標籤的垂直導線對齊。
  10. 保持標籤選中,打開屬性面板,設置Font爲Subhead,Lines爲0,Tag爲2。將Lines設置爲0,這樣當標籤顯示長文本時會自動換行顯示。
  11. 選中單元格上面的Headline標籤。
  12. 點擊IB編輯器右下角Pin按鈕(圖15),在彈出的窗口中,反選Constrain to margin勾選框,而後點擊窗口上半部分的4個邊距,使其顯示爲4條紅線。這使得標籤的上、下、左、右4條邊都分別向最近的控件對齊。而後將Height勾選,再點擊Add 5 Constraints按鈕。

    圖 15 – 爲上端的標籤添加布局約束
  13. 選擇單元格下方的Subhead標籤,點擊Pin按鈕。
  14. 和Headline標籤所作的如出一轍:反選Constraints to margin選項,選擇4邊對齊,勾選Heightg,而後點擊Add 5 constraints按鈕。
  15. 繼續選中Subhead標籤,打開Size面板。
    點擊Height約束右邊的Edit按鈕(圖16),將operator改成」great than or equal to「。這將使標籤的高度自動和文本的行數匹配。

    圖 16 – 修改Height約束的operator
  16. 而後對Heading標籤是重複上面的操做。點擊Heading標籤,在Size面板,點擊Height約束的Edit按鈕,將operator修改成」greater than or equal to「。
  17. 而後修改Table View Controller的代碼。選擇DeliveriesViewController.swift文件。在tableView:cellForRowAtIndexPath:方法中,將代碼修改成:

    這段代碼用viewWithTag方法得到指定Tag值的標籤。在代碼中高亮的部分,最後一行是不能少的,由於標籤某些時候會沒法正確換行,所以須要將prefferedMaxLayoutWidth屬性設置爲當前的寬度以解決這個問題。
  18. 還有幾個地方須要改。拉到DeliveriesViewController.swift文件頂部,在viewDidLoad方法中加入代碼:

    將表格的estimatedRowHeight屬性設爲單元格的正確高度。將表格的rowHeight屬性設置爲UITableViewAutomaticDimenssion,告訴iOS咱們須要它自動調整單元格大小。讓咱們來看看運行效果!
  19. 回到模擬器,點開設置程序,將文本大小設爲最大。點擊Xcode的Run按鈕,當程序運行,能夠看到email地址已經換行了(圖17)!

    圖 17 – 郵箱地址換行

字體在自定義單元格上的改變

如今,當Deliveries場景第一次加載時,表格中的標籤採用用戶在設置程序中已經設好的字體大小顯示。顯然,當單元格採用內置的Subtitle樣式時,若是用戶改變了字體大小,則標籤上的字體大小也會隨之改變。但不幸的是,若是使用的是自定義單元格,這個機制就無效了。咱們先來測試一下。

  1. 點擊Run按鈕。
  2. 當App啓動後,按Shift+Command+H鍵退回到Home屏。
  3. 點開設置程序,進入General->Accessibility->Larger Text界面,將滑條向左拖動,將字體大小改小。
  4. 按下Shift+Command+H鍵,返回Home屏幕,切換到iDeliverMobileCD程序。咱們發現,標籤文本的字體沒有發生絲毫改變。關閉App,咱們來解決這個問題。

要讓自定義單元格中的標籤(或其餘任何文本控件)可以根據設置程序中的字體大小來改變其文本字體,咱們必須:

  1. 在viewDidLoad方法中向通知中心註冊UIContentSizeCategoryDidChangeNotification通知。

  2. 在代碼中響應字體改變通知,將標籤的樣式從新設置正確。例如:

  3. 在ViewController的deinit方法中註銷通知。

讓咱們以Deliveries爲例進行演示。

  1. 打開DeliveriesViewController.swift文件,在viewDidLoad方法最後加入:

上述代碼讓通知中心在用戶改變了動態文本設置以後調用handleDynamicTypeChange方法。

  1. 在viewDidLoad方法下面新增方法:

在這個方法中從新加載Table View。

  1. 如今在tableView:cellForRowAtIndexPath:方法最後加入代碼:

    這段代碼從新設置標籤的字體風格。

  2. 最後,在viewDidLoad方法下面增長deinit方法:

  3. 讓咱們測試一下上述代碼。點擊Run按鈕,當App啓動後,咱們將看到標籤文本變成了先前改變的小字體。按下Shift+Command+H鍵回到Home屏。

  4. 打開設置程序,進入General->Accessibility->Larger Text界面,將滑塊向右拖到,調大字體。

  5. 按下Shift+Command+H鍵,回到Home屏,切到iDeliverMobileCD程序。咱們將看到,標籤文本已經在沒有重啓App的前提下變大了!

  6. 回到Xcode,終止程序。

這種方法的弊端

這種方法有如下幾個弊端:

  1. 咱們必須每一個控件都要設置兩次字體。一次是在屬性面板,一次是在代碼中。
  2. 咱們必須爲每一個文本控件都建立一個IBOutlet,哪怕你根本不須要使用這些IBOutlet。
  3. 咱們必須在每一個View Controller中增長一樣的代碼。

每當咱們須要在不一樣的地方重複加入冗餘的代碼時,咱們就應該考慮建立一種通用的解決方法以在全部項目中重用代碼。

我已經建立了幾個類,你能夠在本身的項目中更容易地實現動態文本。在測試運行以前,先移除咱們在前面添加的代碼。

  1. 從viewDidLoad方法中移除下列代碼:

  2. 從viewDidLoad方法下移除該處理方法:

  3. 從tableView:cellForRowAtIndexPath:方法中移除下列代碼:

  4. 刪除位於viewDidLoad下面的deinit方法:

更好的解決方案

如今來看看更好的解決方案。

  1. 在項目導航窗口,右鍵點擊Main.storyboard,選擇Add Files to iDeliverMobileCD…。

  2. 在添加文件對話框,反選Copy items if needed。

  3. 在項目文件夾,選擇mmDynamicTypeExtensions.swift文件,而後點擊Add。等一會咱們在查看代碼,如今先看一下如何在設計時和運行時使用這些代碼。

  4. 在項目導航窗口,選中Main.storyboard。在Deliveries場景,選擇單元格中位於上方的Heading Label。

  5. 打開屬性面板,注意,顯示了一個新的Type Observer屬性(圖18)。

    圖 18 – Type Observer 屬性

    剛纔添加到項目中的代碼爲標籤添加了一個Type Observer屬性。

  6. 將Type Observer屬性設置爲On。

  7. 選擇Subhead標籤,在屬性面板,將Type Observer屬性設置爲On。

  8. 全部工做完成,讓咱們來測試一下。點Run按鈕,當程序啓動後,咱們將看到顯示了先前咱們設置的大字體文本。按下Shift+Command+H鍵回到Home屏。

  9. 打開設置程序,進入General->Accessibility->Larger Text界面。將滑塊向左拖動以減少字體。

  10. 按下Shift+Command+H返回Home屏,切回iDeliverMobileCD程序。你會看到,標籤字體大小已然改變!

  11. 返回Xcode,終止程序。

動態文本的處理

讓咱們來看看代碼。

  1. 在項目導航窗口,打開mmDynamicTypeExtension.swift文件。

  2. 在文件頂部,是一個協議,該協議僅包含了一個叫作typeObserver的Bool屬性。也就是你在標籤中設置爲On的屬性。

  3. 在協議聲明以後,又定義了一個UILabel的擴展:

    這個擴展聲明瞭對DynamicTypeChangeHandler協議的實現並實現了typeObserver屬性。@IBInspectable屬性代表這個屬性能夠顯示在屬性面板中。這個屬性的setter方法調用了動態文本管理器的registerControler方法。

  4. 向下滾動代碼,咱們能夠看到DynamicTypeManager對象被實現爲一個單例對象:

    單例模式使得類的實例始終只有一個。當建立一個類的實例時,若是類還未被實例化,則建立新的實例。若是類已經被實例化,則返回現有的實例對象。

    圖19是一張序列圖,顯示了動態文本改變的處理邏輯。


    圖 19 – 動態文本處理的序列圖

這是幾個關鍵步驟:

  1. 當typeObserver屬性爲true時(經過屬性面板中),UI控件向動態文本管理器進行註冊,將一個 keypath傳遞給控件的字體屬性。

  2. 當第一個控件進行註冊時,Dynamic Type Manager實例被建立,並開始向通知中心註冊動態文本改變通知。

  3. 建立一個對該控件的引用並將它的字體樣式保存到一個NSMapTable中。一個Map Table是字典的一種,保存的是對象的弱引用,所以當key或value被解構時保存的對象自動被移除。這對咱們來講再恰當不過了:咱們並不想保持對UI控件的強引用。當UI控件釋放後(例如,用戶導航到另外一個View Controller,當前View Contoller被解構),該控件在NSMapTable(感謝Big Nerd Ranch分享了這個技巧)中的引用將被自動移除。

  4. 當用戶在設置程序中改變字體大小,通知中心會通知DynamicTypeManager對象。

  5. DynamicTypeManager對象遍歷Map Table中的UI控件,對每一個控件,都設置它們的字體樣式,並調用sizeToFit方法。

上圖這種方式有什麼好處?

  • 它使用的是擴展而不是繼承。所以咱們可使用「盒子以外的」UIKit組件。

  • 你只須要將mmDynamicTypeExtension.swift添加到項目中就可使用它。

  • 再也不須要爲UI控件建立IBOutlet。只須要簡單地在屬性面板中設置一下就好。
  • 這種方式使用的是鬆散耦合。UI控件將本身的屬性提供給動態文本管理器。這意味着你註冊自定義控件(或者蘋果將來發布的新控件),而不須要修改動態文本管理器。

  • 不須要在設爲默認樣式的模板單元格上使用這個特性,你只須要選擇將哪一個控件註冊到動態文本管理器就好了。

動態文本和靜態文本

讓咱們來看一下如何在靜態單元格中使用動態文本。

  1. 在iDeliverMobileDynamicType項目中,選中Main.storyboard文件,找到Deliveries場景(圖20)。

    圖 20 – Shipment 場景

  2. 在這個場景的Table View中,如同Deliveries場景同樣包含了動態模板單元格。不一樣的是Shipment場景中既包含了動態文本也包含了靜態文本。藍色的文本(Phone、Text、和ID)和Status是靜態的。也就是說這些文本在不一樣的發貨單中是固定不變的。其餘文本則是動態的,每一個發貨單都不同。

  3. 要讓這些標籤也使用動態文本,選擇每一個標籤,而後在屬性面板中將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

  4. 在ShipmentViewController.swift文件中,在viewDidLoad方法最後一行加入代碼:

    記住,這些代碼用於告訴Table View使用自適應大小單元格。

  5. 如今讓咱們看看效果。點擊Run按鈕,當程序啓動,在Deliveries窗口選擇shipment進入Shipment窗口。咱們將看到顯示的是咱們先前在設置程序中設置的小字體。

  6. 如今讓咱們看看在程序運行的狀況下App如何處理動態文本的改變。切換到設置程序,選擇最大字體。再回到iDeliverMobileDynamicTypeApp。

如圖21所示,全部的靜態文本都不見了!這是iOS自己的一個Bug,不幸的是,在Xcode6.2中仍然未獲得解決。我但願蘋果之後能修正這個Bug,但目前咱們不須要自定義單元格就能夠解決這個問題。咱們只須要在tableView:cellForRowAtIndexPath: 方法中增長一點代碼去重置靜態文本:

圖 21 -靜態文本不見了!

還有一個問題是,第一個單元格再也不居中對齊。這個問題也是在同一個方法中增長代碼來解決。

  1. 在文件的tableView:cellForRowAtIndexPath: 方法中,添加高亮部分的代碼:

  2. 點擊Run按鈕,當程序啓動,進入Shipment頁面。

3.切到設置程序將字體設置爲最小。回到iDeliverMobileDynamicType,咱們將發現靜態文本又回來了(圖22)!這是由於當動態文本字體發生改變時,Table View的reloadData方法自動會調用。

圖 22 – 靜態文本又回來了

結語

去年,咱們公司在 MacWorld 展會上有一個展臺,展現個人iOS App開發圖書系列。一個有弱視的讀者來展位上問我,能不能教一下開發者們如何建立適用於弱視患者的App。這致使了本文的產生,我終於能夠說Yes了,我但願本文可以讓你在面對這個問題的時候可以一樣說Yes。

相關文章
相關標籤/搜索