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。