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。

相关文章
相关标签/搜索