IOS7-应用开发-全-
IOS7 应用开发(全)
原文:
zh.annas-archive.org/md5/bc5ab52501e56ac345b8b0ad147c7d86译者:飞龙
前言
欢迎来到 iOS 7 应用程序开发。随着 iOS 7 的发布,苹果完全改变了我们开发者对移动应用程序设计和开发的看法。除了全面的视觉改造外,iOS 7 还提供了数百个新的 API 和 SDK 改进,以及全新的开发环境 Xcode 5。本书将引导你逐步构建一个功能齐全的应用程序。到本书结束时,你将全面了解 iOS 7 开发中的许多主要变化,并准备好为你的用户制作更好的应用程序!
本书涵盖的内容
第一章, Xcode 5 – 开发者的终极工具,解释了你需要了解的一切,以便充分利用苹果的 IDE。有了新的 Xcode,开发和管理工作从未如此简单。
第二章, Foundation 框架 – 成长,介绍了 Foundation 框架,它是 iOS 开发中最重要的核心框架之一。当苹果对其做出更改时,你需要注意!
第三章, 自动布局 2.0,解释了 iOS 7 中自动布局 2.0 的实现。当自动布局首次推出时,它包含了许多问题,导致许多开发者避免使用它。随着 iOS 7 的推出,苹果注意到了这些担忧,并对许多必要的改进进行了改进。
第四章, 构建我们的 iOS 7 应用程序,引导我们构建自己的应用程序,因为我们现在已经了解了 Xcode 5、Foundation 框架和新的自动布局的方方面面。我们将开始我们的第一个项目,并关注新的 iOS 7 设计原则。
第五章, 创建和保存用户数据,使我们能够准备一个应用程序来支持用户创建新项目并保存数据以供以后使用。例如,在我们的自定义应用程序中,用户将能够保存他们吃的食物以供以后查看。
第六章, 显示用户数据,解释了显示我们已保存数据的技巧。这是在我们继续到两个主要 iOS 7 API 之前完成应用程序的最后一步。
第七章, 使用 TextKit 操作文本,解释了 iOS 7 中新的 API TextKit 的使用,它简化了与文本工作的过程。从动态字体到丰富的文本编辑器样式,TextKit 是任何 iOS 开发者理解的好工具。
第八章, 使用 UIKit Dynamics 添加物理效果,解释了 UIKit Dynamics 的使用,UIKit Dynamics 是一个直接集成到 UIKit 中的功能齐全的物理引擎。UIKit Dynamics 将允许您在应用程序中创建基于物理的运动和动画,以实现真实世界的感受。
您需要为本书准备的内容
您需要以下内容来完成本书:
-
运行 OS X 10.8 或更高版本的 Apple 电脑
-
在您的 Mac 上安装了 Xcode 5
本书面向的对象
本书是为希望学习 iOS 7 和 Xcode 5 新功能的 iOS 开发者编写的。为了正确理解本书的内容,需要具备 Objective-C 和 iOS SDK 的基本理解。
习惯用法
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"iOS 7 在 Foundation 框架中引入了一个全新的类,NSProgress"。
代码块设置如下:
- (void)preferredContentSizeChanged:(NSNotification *)notification {
self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
- (void)preferredContentSizeChanged:(NSNotification *)notification {
self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
}
新术语和重要词汇以粗体显示。您在屏幕上、菜单或对话框中看到的单词,例如,在文本中如下所示:"选择单视图应用程序然后点击下一步"。
注意
警告或重要注意事项以如下框的形式出现。
提示
技巧和窍门看起来像这样。
读者反馈
我们读者的反馈总是受欢迎的。让我们知道您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲所有者,我们有一些事情可以帮助您从购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support 查看任何现有勘误。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面提供的帮助。
询问
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章. Xcode 5 – 开发者的终极工具
随着 iOS 7 的发布,苹果也为开发者提供了一个完全更新的 Xcode 版本,即其 集成开发环境(IDE)。Xcode 5 是一个重大的进步,它提供了比以往任何时候都更多的工具和功能。
理解您 IDE 的强大功能是提高生产力和整体开发便捷性的关键。在本章中,我们将探讨所有这些新功能,并学习它们如何帮助您为 iOS 7 编写应用程序。
新的用户体验
Xcode 5 对其整体用户体验进行了许多受欢迎的改进,包括微妙的设计增强和底层优化。花几分钟时间尝试使用新的 IDE,您会发现尽管变化不大,但更干净的 UI 提供了一个更少干扰的工作环境。更短的工具栏和易于看到的突出按钮有助于将您的内容置于最前面。
以下截图显示了新 IDE 的窗口外观:

例如,Open Quickly 等功能在尺寸上进行了精简,但在功能上得到了改进。通过 File | Open Quickly 导航或使用键盘快捷键 command + shift + O 将在屏幕中间打开一个简化的单行搜索栏。随着您输入选项,搜索结果会更快地返回,并基于相关性进行优先排序。每个结果还提供了关于您的查询的详细数据,例如文件和行号。以下截图显示了搜索结果的一个示例:

为了进行更精确的搜索,您可能需要从导航面板中选择搜索导航器或使用键盘快捷键 command + 3。输入搜索查询并按 enter 键将提示 Xcode 5 默认执行项目范围内的搜索。结果将在搜索栏下方的导航器中显示,并包括新的细化选项。选择 In Project 按钮(在此处,Project 是您项目的名称)将允许您指定要搜索的单独文件夹。为了获得更大的灵活性,新的搜索导航器将允许您构建可保存供将来使用的自定义搜索范围。以下截图显示了选择和不选择 In FoodAndMe 按钮(在这种情况下,FoodAndMe 是项目名称)之间的差异:

顶级文档
苹果公司提供了任何开发平台中最深入的 SDK 文档之一。访问这些文档可能是 iOS 开发最重要的方面之一。Xcode 的早期版本始终可以访问文档;然而,Xcode 5 通过其顶级文档采取了更易于访问的方法。需要注意的是,除非你从苹果公司预先下载文档,否则需要互联网连接。这可以通过导航到Xcode | 首选项 | 下载来完成。
前往菜单栏,导航到帮助 | 文档和 API 参考。Xcode 5 将显示一个专门设计的窗口,用于简化所有文档的搜索和显示。苹果公司构建了这个文档,以便为你服务。当你键入时,Xcode 将显示 API 参考、SDK 指南甚至与你的搜索相关的示例代码的建议。
新的文档视图还提供了对标签的支持,允许你同时查看多个文档。当你浏览结果时,你可以通过点击搜索栏左侧的目录按钮立即看到动态目录。目录将根据你当前查看的文档自动更新。
此外,新的文档内置了书签功能,允许你保存你最常查看的资源。在搜索栏的右侧,你会看到一个分享按钮。点击此按钮将显示一个菜单,其中包含分享或书签当前参考的选项。
你可能也注意到了,在滚动文档时,每个标题或标题的左侧都有一个小的书签图标。你甚至可以保存任何 API 参考的特定部分,而不仅仅是保存整个文档。你所有的书签都可以通过点击位于目录按钮左侧的导航按钮在导航器中查看。这种视图还将允许你一目了然地浏览整个文档库。结合之前按alt键并点击任何代码以显示内联摘要和从代码到完整文档的链接的功能,你将拥有强大的文档集成!
调试器和调试仪表
由于调试器添加了许多新功能,Xcode 5 的调试功能得到了极大的改进。苹果公司已完全从之前的 GDB 引擎切换到更强大的 LLDB 引擎。这允许断点灵活性、内联变量预览以及更容易地找到变量值。
如果你曾经使用断点调试过项目,你会注意到 Xcode 5 管理断点的方式有所变化。断点仍然通过直接点击所需的行号来创建。然后可以通过直接点击它们或使用已移动到 Xcode 窗口底部的调试工具栏上的断点按钮来启用或禁用这些断点。
每个断点也可以配置为有条件地响应。默认情况下,代码会在达到断点时停止。然而,一旦设置了条件,除非满足这些条件,否则断点将被忽略。你可以通过右键单击单个断点并选择编辑断点来编辑这些条件。从这里,设置你的条件和结果操作。这些操作可以包括向控制台记录消息、运行 AppleScript 或 Shell Script,甚至播放声音。
Xcode 5 的调试器的另一个出色功能是能够在调试期间使用数据提示预览变量和对象。在调试你的应用程序时,将鼠标悬停在变量上,其值会自动出现在光标下方。这适用于标准数据类型,如字符串、数值类型和布尔类型。
当涉及到对象时,数据提示也非常强大。例如,在调试模式下,将鼠标悬停在一张图片上,就会显示有关此对象的信息摘要。选择眼睛形状的图标将允许你在代码中预览实际图片,如下面的截图所示:

正确调试任何应用程序还涉及到监控系统资源,以确保你的代码尽可能优化。Xcode 5 引入了调试仪表,这是某些有用仪表工具的轻量级和嵌入式版本。因为调试仪表集成到 Xcode 5 中,所以它们能够在所有时间与应用程序并行运行,同时允许你观察 CPU、内存、iCloud、能源和 OpenGL ES 资源。
通过调试导航器可以找到调试仪表,一旦你运行了一个项目,它们就会自动开始运行。之前提到的资源以易于阅读的视觉图表形式显示,这样你可以一目了然地实时监控应用程序的性能。此外,访问完整的仪表软件只需单击一下,这可以通过点击以下截图所示的在仪表中配置配置文件按钮实现:

使用账户和功能进行自动配置
Apple 提供了各种有用的服务,这些服务可以包含在任何应用程序中。由于开发者需要手动设置的任务数量,使应用程序支持这些服务一直是一个麻烦。这包括添加权限,如 App ID、将框架链接到项目,以及将所需字段添加到项目的 .plist 文件中。此外,这些服务中的每一个都有自己的要求,这意味着支持多个服务需要不同的步骤来完成。
随着 Xcode 5 的推出,Apple 已经使用自动配置消除了这些烦恼。使用自动配置,开发者需要的只是一个与开发者账户关联的 Apple ID。
导航到 Xcode | 首选项 并选择 账户 部分(Xcode 5 新增)。从这里,您可以添加所有您的开发者计划 Apple ID 并查看每个账户的相关详细信息。点击左侧面板上的 + 按钮将为您提供添加新 Apple ID 的选项。这样做将在 Xcode 5 和 Apple 开发者门户之间建立直接连接。一旦登录,点击屏幕右下角的 查看详细信息... 按钮。将弹出一个新窗口,显示所选账户附加的所有代码签名身份和配置文件详细信息。
在项目编辑器的 常规 选项卡下,您将在 标识 部分看到一个新的选项,团队。选择此选项将显示与我们之前添加的账户相关的身份列表。通过选择您的相应签名身份,Xcode 5 将能够验证您是否拥有所有适当的配置文件,并在需要时甚至可以为您创建它们。
自动配置提供的最大进步可能是项目编辑器中的 功能 选项卡(Xcode 5 新增)。这种简化的方法将允许您配置特定平台功能,例如 iCloud、应用内购买 和 游戏中心,而无需像以下截图所示的那样访问开发者门户。Xcode 5 将自动配置配置文件,添加 App ID 权限,并为您链接所有必需的框架,自动:

如果您更喜欢使用旧方法设置功能和功能,您仍然可以在 Apple 开发者门户中这样做。
源代码控制
源代码控制被大型团队和独立开发者广泛使用。它提供了一种极其有用的方式来跟踪代码更改并使用版本控制回滚到项目的稳定构建。开发者团队可以通过创建和管理代码的副本(称为 分支)来分别独立工作在各个组件上,而不会覆盖其他团队成员的代码。更改将在稍后合并,同时跟踪对代码库所做的所有更改。
源代码控制不是 Xcode 5 的新功能;然而,苹果公司决定通过创建顶级菜单项来提供对其功能的更便捷访问。选择它将显示一个下拉菜单,其中包含一键访问大多数源代码控制命令,如提交、推送和拉取。将鼠标悬停在工作副本上,将打开一个新的子菜单,允许您在分支之间切换、创建新分支或合并分支。以下截图显示了此子菜单:

除了在您的计算机上的本地源代码控制之外,Xcode 5 还支持直接连接到托管在流行网站上的远程仓库,例如 GitHub。打开首选项并导航到账户标签页。这次,在点击+按钮后,选择添加仓库。一旦您输入了正确的仓库地址,Xcode 5 将连接到仓库,从而允许您远程访问它。
资源目录
您创建的每个项目都至少包含一些图像文件,形式为启动图像和应用程序图标,以及其他 UI 元素。在 Xcode 5 中,资源目录有两个主要用途。这包括自动化图标和启动图像的命名约定,以及将图像文件分组在单个位置。
资源目录在项目导航器中以一个蓝色文件夹的形式表示为一个单独的组。默认情况下,每个新创建的项目都将包含默认的Images.xcassets项。您还可以根据个人偏好创建自己的资源目录以进行进一步的组织。
Xcode 5 要求每个启动图像文件和图标图像文件根据图像将用于的设备和/或分辨率适当地命名。当选择Images.xcassets项时,您将看到许多空槽等待添加图像。每个槽都有一个描述它所包含的图像。将图像从您的计算机拖动到 Xcode 5 的相应槽位,将添加图像到您的项目,并自动配置所有命名约定。以下截图显示了资源目录窗口:

如果您想添加与您的项目相关的任何其他图像,您只需将它们拖放到资源目录窗口中,Xcode 5 将处理其余部分。高分辨率(2x)和标准分辨率(1x)图像文件将按共同名称分组到它们自己的图像集中。您仍然必须自己提供低分辨率和高分辨率的图像。Xcode 5 不会为您自动缩放它们:它只是将它们分组。此名称的值可以更改为任何值,并且将在代码中使用此值来访问相关的图像,无论实际文件名如何。
快速构建设备选择
开发适用于多设备的应用程序需要一致的设备特定测试。iOS SDK 中包含的 iOS 模拟器为所有苹果设备提供模拟。在 Xcode 5 中,选择要构建的适当设备已被简化为工具栏上的单个下拉选项。
点击工具栏左侧当前设备的名称将提供下拉菜单。连接到您的计算机的所有物理设备将出现在列表的顶部(您可能需要向上滚动才能看到它们),所有标准 iOS 模拟器设备将出现在下面。
简单选择您希望测试的设备,然后点击 运行。模拟器将启动并切换到所选设备。以下截图列出了下拉菜单中的设备:

Storyboard 预览
一直以来,编写支持先前 iOS 版本的应用程序主要涉及更新 API 调用和微小的编码约定。随着 iOS 7 的推出,苹果彻底改变了所有标准 UI 对象的设计。了解 iOS 7 和先前版本中所有对象的大小、位置和布局对于保持一致的用户体验非常重要。这正是 storyboard 预览发挥作用的地方。
为了使用 storyboard 预览,您必须选择辅助编辑器并导航到您希望预览的视图(通常是 .xib 或 .storyboard 文件)。选择 相关文件 菜单选项,导航到 预览,并选择您希望预览的 .xib 或 .storyboard 文件,如图所示:

您将在辅助编辑器的右侧看到视图的相同预览。在视图的右下角,您将看到一个标有 iOS 7.0 及以后版本 的按钮。点击它,然后选择 iOS 6.1 及更早版本,如图所示:

您的视图现在将显示所有 UI 元素,它们将显示在 iOS 6 或更早版本中。如果您希望使应用程序向后兼容,这是一个非常实用的工具。
摘要
Xcode 5 为开发者提供了比以往任何时候都多的功能,每个工具都旨在在构建最佳质量的应用程序时为您提供更高效的使用体验。在本章中,我们学习了如何使用所有这些功能,从新的调试工具到自动配置。尽管我们涵盖了 Xcode 5 的大量新功能,但您应该访问以下链接,查看 Apple 关于 Xcode 5 新功能的文档:developer.apple.com/library/mac/releasenotes/DeveloperTools/RN-Xcode/Introduction/Introduction.html
每次新的 iOS SDK 发布,苹果都会为 Objective-C 编程语言添加一些小的和重大的更新。在下一章中,我们将介绍对 Foundation 框架所做的更改,这可能是 iOS 开发中最重要的框架!
第二章. 基础框架 – 成长
在本章中,我们将学习模块以及它们如何改变我们将框架导入文件的方式。我们将涵盖 Foundation 框架的新旧类,从全新的 NSProgress 类开始。我们将看到对现有类的一些主要改进,包括 NSArray 和 firstObject 方法,NSTimer 的新属性用于管理容差,NSData 现在支持的附加编码,以及最后使用 NSURLUtilities 管理 URL 的新方法。让我们开始吧!
为什么 Foundation 很重要
Foundation 是 Objective-C 的核心框架。没有它,开发 iOS 应用程序将是不可能的。Foundation 定义了所有类的底层,以及包括字符串、数组和字典在内的基本数据类型的功能。
对 Foundation 框架所做的更改可能从微小的增强到完全新类的引入。iOS 7 也不例外,Apple 提供了一些我们将在本章中探索的出色新功能。
模块
在使用 Xcode 和 iOS SDK 开发应用程序时,你可能已经注意到,导入常用头文件,如 UIViewController.h 或 UIView.h,从未是必需的。
打开任何项目中的任何文件,导航到基于视图控制器 .h 文件。代码的第一行将如下所示:
#import <UIKit/UIKit.h>
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载所有示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册以直接将文件通过电子邮件发送给你。
作为一名 iOS 开发者,你可能在一个项目中编写了数百个 #import 语句。当编译器遇到一个导入语句时,它实际上会插入导入头文件中找到的每一行代码。在前面代码的第一行示例中,UIKit.h 导入了 UIKit 框架中可用的所有头文件;因此,你不必担心为不同的实例导入哪个头文件。
如果你曾经查看过 UIKit 中包含的所有文件,你会发现它们的总代码行数超过 11,000 行。这意味着每个导入 UIKit.h 的文件将增加 11,000 行代码。这并不理想;然而,Apple 提供了一个解决方案,即 预编译头文件(PCH)。
预编译头文件 – 一种部分解决方案
你创建的每个项目都会在支持文件组中自动生成自己的 PCH 文件。在编译预处理阶段,PCH 文件将加载并缓存指定的头文件以导入。以下是一个 PCH 文件的示例:
#import <Availability.h>
#ifndef __IPHONE_5_0
#warning "This project uses features only available in iOS SDK 5.0 and later."
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "UIImage+ImageEffects.h"
#endif
你的应用程序可能需要在多个文件中需要特定的框架或类。而不是逐个导入文件(并重复导入),将导入语句添加到 PCH 文件中将在编译的预处理阶段预先计算并缓存大部分工作。这使得当可用时,每个文件都可以从缓存中提取。
尽管这种方法效果很好,但在导入 Apple 框架时,你必须始终记得将框架链接到你的项目中。否则,编译器将抛出许多错误。
模块 – 智能导入
随着 iOS 7 的推出,Apple 引入了一种新的处理模块预编译框架的方法。而不是用每行代码替换导入语句,模块将框架封装到一个自包含的块中。模块的预编译方式与 PCH 文件中导入语句的预编译方式相同;然而,使用模块将自动链接正确的框架并提供与编译加速相同的速度提升。
在使用 Xcode 5 创建的所有新项目中,模块默认启用。对于旧项目,你可以在项目的构建设置中通过搜索模块并将 Enable Modules (C and Objective-C) 设置为 Yes 来启用模块。

现在模块已被启用,你可以开始使用新的语法来导入框架。在你要导入的 .h 文件顶部,只需输入以下代码:
@import QuartzCore;
这就是你的代码中所需的所有内容。Xcode 将自动链接所需的框架(在这种情况下,QuartzCore)并为你提供所有编译加速。
此外,你可以根据需要导入特定的头文件。例如,你可能只需要 QuartzCore 提供的 CoreAnimation 头文件。你可以轻松地通过输入以下代码来导入这些头文件:
@import QuartzCore.CoreAnimation;
此外,Xcode 将在运行时自动将 #import 语句转换为 @import。尽管方便,但仍然建议尽可能更新到新语法。
还需要注意的是,当前模块仅支持 Apple 框架。自定义类和第三方框架仍然需要传统方法或 PCH 文件。
NSProgress
iOS 7 在 Foundation 框架中引入了一个全新的类 NSProgress。使用 NSProgress 涉及将每个动作的任务视为完成的里程碑。通过这样做,作为开发者的你可以在代码中直接跟踪进度并执行每个里程碑的单独任务。
例如,为了执行特定的动作,你可能需要完成四个单独的任务。每个任务都能够监控自己的进度,并在任务完成后报告。在我们的例子中,这将使完成百分比增加到 25%。
NSProgress 使用 键值观察(KVO)来提供与进度相关的通知。这些通知可以用来更新显示给用户的 UI 组件,例如进度条或标签。以下是一个非常简单的实现示例,展示了如何使用 NSProgress 以本地化的方式报告进度:
NSArray *data = @[@"Data 1", @"Data 2", @"Data 3", @"Data 4"];
self.dataProgress = [NSProgress progressWithTotalUnitCount:data.count];
int index = 0;
for (NSString *string in data) {
// Do something with string or other data
index ++;
self.dataProgress.completedUnitCount = index;
NSLog(@"%@", [self.dataProgress localizedDescription]);
}
NSArray
当使用 NSArray 时,你必须确保所有提供的索引都在范围内,并且不超过数组的长度。在通过索引检索元素时,索引必须在零和数字之间(该数字是数组中的总项目数);否则,将抛出异常。这种常见用例包括从数组中获取第一个或最后一个对象。
NSArray 一直都有以下方法来获取最后一个对象:
- (id)lastObject;
以前,获取数组中的第一个对象需要检查索引是否在数组范围内,如下面的代码片段所示:
- (id)firstObjectInArray:(NSArray *)array {
if (array.count > 0) {
return array[0];
}
}
虽然前面的例子相当小,但你也可以看到更复杂的实现可能会很复杂且耗时。幸运的是,随着 iOS 7 的发布,苹果终于公开了一个之前私有的 NSArray 方法来获取第一个对象:
- (id)firstObject;
这个便捷的方法将允许你快速访问任何数组中的第一个对象,而无需麻烦。此外,如果数组为空,此方法将返回 nil。
NSTimer
使用 NSTimer 执行周期性任务是一种常见做法。以下是一个使用 NSTimer 在两秒间隔内执行任务并重复的示例:
[NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(targetMethod:)
userInfo:nil
repeats:YES];
这种方法的缺点是 CPU 会持续活跃以重复执行所需的任务。当同时使用多个计时器时,虽然可能性不大,但可能会降低应用程序其余部分的 CPU 性能。始终最好在应用程序上进行测试以找到此类可能性,并在可能的情况下使用安全措施。
苹果为 NSTimer 添加了一个新的容差属性,以减少使用 NSTimers 时对 CPU 的压力。此属性将告诉应用程序,当计时器超过了其预定间隔时,允许计时器延迟多长时间才触发。因此,应用程序可以将操作组合在一起以减少 CPU 压力。
这个新属性可以通过以下方法访问和设置:
- (NSTimeInterval)tolerance;
- (void)setTolerance:(NSTimeInterval)tolerance;
设置此属性将有助于为与计时器相关的 CPU 使用情况创建安全措施。
NSData
每个应用程序都以某种方式使用数据。在某些情况下,你可能需要能够操作数据的单个字节。NSData 封装了这些原始字节,以便可以使用内置方法轻松操作。
在 iOS 7 中,NSData现在增加了对 Base64 编码和解码的支持;一组 ASCII 格式二进制到文本编码方案。这些方案最常用于在仅支持基于文本数据传输的媒体之间传输数据。从基于 JSON 的 Web API 响应中编码图像是这些方案最常见的使用场景。
在 iOS 7 之前,开发者需要使用第三方库或从头开始构建自己的库。苹果通过以下方法使使用这些编码方法变得异常简单:
- (id)initWithBase64EncodedData:(NSData *)base64Data
options:(NSDataBase64DecodingOptions)options;
- (NSData *)base64EncodedDataWithOptions:
(NSDataBase64EncodingOptions)options;
- (id)initWithBase64EncodedString:(NSString *)base64String
options:(NSDataBase64DecodingOptions)options;
- (NSString *)base64EncodedStringWithOptions:
(NSDataBase64EncodingOptions)options;
前两种方法专注于 UTF-8 编码的数据,而剩下的两种则直接处理字符串值。这两对方法提供相同的功能;然而,每种用例可能提供更好的性能。
NSURLUtilities
基础框架包括许多与处理 URL 相关的不同方法;然而,大多数与操作这些 URL 相关的 API 都是基于NSString,因为NSURL是一个不可变类。
为了解决这个问题,苹果引入了NSURLComponents以允许操作 URL 对象。使用NSURLComponents,NSURL可以被视为一个可变对象,允许直接操作。以下代码片段是一个示例用例:
NSURLComponents *components = [NSURLComponents componentsWithString:@"http://somewebsite.com"];
components.path = @"/somepath";
components.query = @"queryParameter=parameterValue";
NSLog(@"%@", [components URL]);
运行此代码将在控制台输出以下内容:
http://somewebsite.com/somepath?queryParameter=parameterValue
使用NSURLComponents,你现在可以直接操作NSURL值,而无需使用NSString。
摘要
在本章中,我们介绍了 Foundation 框架的一些主要更新。始终建议您关注 Objective-C 和苹果核心框架的进步。有了这些知识,你现在有了构建更高效、性能更好的应用程序的工具!
现在我们对 Foundation 中的新特性有了更好的理解,是时候开始构建我们的应用程序了。在下一章中,我们将开始使用 iOS 7 中的新自动布局功能来构建我们的界面。
第三章:自动布局 2.0
在本章中,我们将创建我们的项目并开始构建我们的应用程序,Food and Me,从自定义菜单视图开始。首先,我们将在 Xcode 5 中创建项目本身。接下来,我们将创建我们的 Storyboard。这包括添加所有必需的元素并使用新的自动布局为我们的视图添加约束。这就是我们将直接深入了解自动布局是如何工作的以及你将如何继续在未来的项目中使用自动布局的地方。最后,我们将一切连接到我们的代码并设置我们的导航。完成本章后,我们将拥有一个功能齐全的菜单视图,包括一个基本的导航控制器。
为什么你应该使用自动布局
在自动布局之前,构建支持动态多屏幕尺寸和方向的程序需要大量工作。自动调整遮罩、弹簧和梁都是开发者通常会努力使用的工具。这些工具并不总是产生正确的结果,因此典型的下一步行动是在代码中检测屏幕尺寸并相应地调整布局。当在一个具有许多视图和布局的应用程序上工作时,这可能会变得令人沮丧。
在 iOS 6 中,苹果引入了一个名为自动布局的新功能。其前提相当简单:允许开发者定义在 Storyboard 中所有视觉元素上的约束,以便控制应用程序的布局和流程。不幸的是,自动布局带来了许多麻烦。
主要问题与自动布局要求每个对象都必须有适当的约束相关联的事实有关。如果你未能提供单个约束,Xcode 会自动生成它,有时会覆盖你当前设置的某些约束。这通常会在运行时导致许多布局问题,从而造成糟糕的用户体验。
在 iOS 7 中,苹果完全重写了自动布局,使其通过简单的工具更容易提供布局约束,并给予开发者对每个约束更多的控制。
正确使用自动布局将大大减少构建动态布局所需的时间。这是通过用我们在 Storyboard 中创建的易于定义的约束替换复杂且繁琐的代码来实现的。自动布局并不是解决所有问题的方案,因此决定何时使用代码与何时使用自动布局非常重要。
你可以通过访问项目的可下载内容并下载文件到你的电脑来下载所有资产,包括完成的项目。让我们开始吧!
创建我们的项目
我们将使用自动布局为Food and Me应用程序的主菜单设置约束。为了简化起见,我们不会为整个应用程序使用自动布局,但我们将涵盖所有必要的元素,以便学习如何使用新的自动布局。
首先,让我们创建一个新的项目。打开 Xcode 并在欢迎屏幕上选择创建一个新的 Xcode 项目(或者在欢迎屏幕未出现时,从菜单栏导航到文件 | 新建 | 项目)。选择单视图应用程序,然后点击下一步。
按照以下模板选项填写:
-
产品名称:
Food and Me -
组织名称: 输入您的组织或公司名称
-
公司标识符: 输入您希望在开发者门户上使用的标识符,使用反向域名表示法
-
类前缀: 请保持此选项为空
-
设备: iPhone
Xcode 为我们创建了一个标准项目,包括应用程序代理、一个单视图控制器、一个故事板文件以及用于我们的启动图像和图标的资源目录。首先,让我们将ViewController.h和ViewController.m重命名为更具描述性的名称。在导航器中选择ViewController.h以在编辑器中显示此文件。在@interface之后立即找到的ViewController文本上右键单击,然后导航到重构 | 重命名...。这种重构有时可能会跳过故事板中的文件名重命名,因此始终检查这是一个好习惯。使用快照和/或源代码控制也是降低风险的好方法。
我们将在本文件中创建菜单视图,所以让我们将其重命名为MenuViewController。输入此名称,并确保重命名相关文件选项被勾选,然后点击预览。一个新窗口将出现,显示将要更改的文件和位置预览。您应该会看到预览中包含一个头文件、一个实现文件以及故事板(Xcode 足够智能,可以更新所有相关的项目文件)。点击保存按钮后,将出现一个提示,询问您是否希望启用快照功能。这与源代码控制菜单类似,完全是可选的。
最后,我们需要将我们的图像文件添加到提供的资源目录中。打开我们之前下载的Food and Me文件夹。您将看到一个名为Final Image Files的文件夹。如果您打开此文件夹,您将看到所有用于我们项目的图像文件(包括常规尺寸和2x视网膜尺寸)。切换到您的 Xcode 项目,并选择Images.xcassets。将Final Images Files文件夹中的每个图像拖放到包含AppIcon和LaunchImage设置的框中。对于每个2x和常规尺寸图像对,将创建一个新的图像集。
开始我们的故事板
现在我们已经添加了所有文件和图像,我们可以开始构建我们的故事板并应用自动布局约束。打开Main.storyboard,我们应该会看到一个空视图控制器分配给我们的MenuViewController类。
我们的主菜单将由四个独立的部分组成。让我们首先将前三个部分添加到我们的故事板文件中(第四个将通过编程创建)。打开 Xcode 的 实用工具 面板(如果尚未打开)并选择视图底部的对象库。
首先,将一个 UIImageView 类拖到我们的 MenuViewController 中,确保它的大小适合整个视图。接下来,将两个 UIButton 拖到 UIImageView 上面,无需担心它们的位置。在我们的 实用工具 面板中,选择 属性检查器,然后选择两个按钮中的一个。删除 默认标题 选项,使其为空。接下来,点击 图像 的下拉菜单并选择 foodButton 作为我们的图像。Xcode 将自动调整 UIButton 的大小以适应我们的按钮图像。对剩余的 UIButton 重复此过程,这次在 属性检查器 中的图像属性中选择 addButton。
现在将屏幕底部的按钮重新定位,使它们彼此之间均匀分布。确切的定位不重要,所以根据个人喜好调整位置。现在故事板的最终视图应该看起来类似于以下截图:

设置按钮动作
在应用自动布局约束之前的最后一步是将我们的按钮连接到类,为每个按钮使用一个 IBAction。当选择 MenuViewController 时,从工具栏打开辅助编辑器并确保您选择了头文件(MenuViewController.h)。
按住键盘上的 control 键,点击并拖动 My Foods 按钮到头文件中。将鼠标放在 @interface 和 @end 之间,并在看到一个小弹出窗口显示 内嵌输出、动作或输出集合 时释放鼠标。在新弹出的视图中,从 连接 下拉菜单中选择 动作,并将此动作命名为 myFoodsPressed。最后,从 类型 下拉选项中选择,并选择 UIButton。为 Add New 重复此过程,动作名称为 addNewPressed。
您的头文件现在应该看起来像以下代码片段:
#import <UIKit/UIKit.h>
@interface MenuViewController : UIViewController
- (IBAction)myFoodsPressed:(UIButton *)sender;
- (IBAction)addNewPressed:(UIButton *)sender;
@end
现在我们已经填充了视图并创建了所有动作,我们可以开始使用自动布局了。
使用自动布局
简而言之,自动布局是一组针对每个视图的指令,这些指令与其父视图或最近的相邻视图的大小和位置有关。自动布局的两个非常常见的用途是确保您的视图在应用程序运行在 3.5 英寸屏幕、4 英寸屏幕或 iPad 屏幕时知道该做什么,以及当设备改变方向时。我们希望我们的应用程序支持这两种屏幕尺寸,因此我们将在添加约束时关注这一点。
Xcode 提供了多种应用约束的方法,每个约束也有其自己的属性,可以单独操作。有了所有这些选项,我更喜欢将我的 Xcode 环境设置为完全拥抱所有 Auto Layout 选项。
确保你的实用工具面板是打开的。这将允许你在处理布局时手动更改你的约束属性。在故事板视图的左下角,你会看到一个带有指向右侧箭头的按钮。这个按钮将打开文档大纲视图。这个面板允许你从全局视角查看所有视图控制器及其子视图,包括应用于每个视图的所有约束。打开这个视图,你的 Xcode 视图现在应该类似于下面的截图:

应用约束
我们的面板菜单有两个按钮,用于导航到应用程序的不同区域。我们希望按钮始终对齐,所以让我们添加一些约束到我们的按钮,以实现这一点。
在两个对象之间添加约束的一种方法是从一个对象控制拖动到另一个对象。在键盘上按住control键,然后从添加新按钮拖动到我的食物按钮。一个新弹出窗口将显示这些多个选项以添加约束。这些项目中的每一个都可以被选中,并将提供两个对象之间的相应约束。按住shift键将允许你一次选择多个选项。
从菜单中选择水平间距。你会注意到添加新按钮周围出现一个橙色轮廓,并且两个按钮之间将出现一条水平 I 形线,如下面的截图所示:

为了让 Auto Layout 正确计算我们视图的位置,它必须有一套完整的约束。所有约束都将突出显示为橙色,直到提供了一套完整的约束。目前,我们只在两个按钮之间有一个约束,这告诉 Xcode 这两个视图需要始终保持相等距离。
让我们添加一些更多的约束。每个按钮也应该保持垂直对齐,所以让我们添加这个约束。然而,这次,请按住command键并选择两个按钮。在选择了这两个项目后,Xcode 5 知道提供的任何约束都将应用于这两个视图。在故事板视图的右下角,你会注意到一组按钮,如下面的截图所示:

这些按钮提供了快速访问所有自动布局选项。在仍然选择两个按钮的情况下,点击之前截图所示的四个按钮组中的第二个按钮。将出现一个新的弹出窗口(见以下图片),其中包含设置/编辑的完整选项和属性列表。你可能已经从我们使用控制拖动从一个按钮到另一个按钮时显示的先前菜单中识别出一些这些属性和约束。
我们想要关注对齐,这可以在弹出视图的底部找到。勾选对齐旁边的框,并从下拉菜单中选择顶部边缘。现在点击添加约束将其应用到我们的按钮视图中。在两个按钮上方将出现一条新线,表示两者将始终对齐。
我们的约束仍然显示为橙色,这意味着我们仍然需要添加更多约束,以便 Xcode 能够进行适当的计算。Xcode 也检测到了这一点,并提供了一个非常棒的工具,它基于所需内容提供建议。在左侧的文档大纲视图中,菜单视图控制器场景旁边出现了一个带有箭头的小红圈。点击此箭头将弹出一个新视图,列出所有缺失的约束和警告。

我们的警告指向与 X 位置和 Y 位置相关的两个非常具体的问题。我们需要添加约束,告诉 Xcode 如何布局按钮的 X 和 Y 位置,所以现在就让我们这么做吧。
只选择添加新按钮。我们的应用程序布局相当简单,可以肯定的是,我们希望按钮与屏幕底部保持相等距离,无论大小如何,所以让我们添加一个约束来实现这一点。在保持按钮选择状态的同时,将鼠标导航到屏幕顶部的编辑器菜单选项,并选择固定 | 左侧空间到父视图。在视图边缘和添加新按钮之间会出现一个新的 I 形条。这将确保按钮和主视图之间保持相等距离。
既然我们已经处理了 X 位置,那么让我们也为 Y 位置做同样的操作。选择编辑器 | 固定 | 底部空间到父视图。从按钮底部到屏幕底部会出现一个新的条形。这将确保按钮和屏幕底部之间保持相等距离。
使用这个新约束,现在我们所有的约束都已经变成了蓝色,这意味着 Xcode 已经拥有了计算我们视图位置所需的所有信息!你可能想知道,当我们没有为我的食物按钮添加这些父视图约束时,这是如何实现的。
答案是我们不需要这样做。我们添加的前几个约束实际上已经为我们处理了这个问题。两个按钮将始终保持顶部对齐,这将处理另一个按钮的 Y 位置。此外,我们设置了按钮之间的水平间距,这将自动处理另一个按钮的 X 位置。以下图像说明了这是如何可能的:

现在我们已经设置了约束,让我们设置背景图像。选择我们之前添加的图像视图,在属性检查器中将图像设置为背景。现在先在 4 英寸 iPhone 上运行应用程序,然后是 3.5 英寸 iPhone。屏幕底部的按钮将根据我们的约束自动定位,而我们不需要写一行代码就能做到这一点!
解决自动布局问题
在看到我们的应用程序在 iPhone 上运行后,为了获得更平衡的布局,可能最好将按钮向下移动一点。返回 Xcode,并按住command键点击每个按钮来选择这两个按钮。根据个人喜好将它们向下移动几个像素。
你会注意到突然有两个虚线红色的线条围绕着我们的按钮。Xcode 在手动重新定位视图时不会自动更新约束,因此之前的计算现在不再有效。这些虚线红色的线条告诉你我们的约束存在错误,需要修正。
幸运的是,Xcode 有一些实用的功能可以帮助解决这些问题。从菜单栏中,导航到编辑器 | 解决自动布局问题 | 更新约束。这也可以通过故事板右下角第四个按钮来完成。通过选择这个按钮,Xcode 将根据我们视图的当前物理位置重新计算之前的约束。所有的错误现在都将消失,没有任何问题。
除了更新约束外,此菜单选项还允许你添加缺失的约束、更新当前约束,甚至清除所有约束。这些自动化选项可能非常有帮助,但始终建议手动设置约束以获得更好的准确性。如果你不确定下一步该做什么,这些选项也可能为你提供一些指导。
完成我们的菜单视图
我们按钮倾向于稍微融入背景中,所以让我们添加一个新的视图来帮助它们更好地突出。首先,让我们导航到我们的故事板并为我们的背景图像创建一个新的出口。选择我们的MenuViewController类,并打开辅助编辑器。从故事板中的背景图像控制拖动到MenuViewController.h文件(在@interface和@end之间)。将此出口命名为mainBackground。现在切换到MenuViewController.m,并在ViewDidLoad中添加以下代码:
// Create a white transparent bar for the bottom of the screen
// Set the color to white with an alpha of 0.5
UIView *bottomBarBG = [[UIView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height - 130, self.view.bounds.size.width, 130)];
bottomBarBG.backgroundColor = [UIColor colorWithWhite:1.0f alpha:0.5f];
// Add the view to the background
[self.view insertSubview:bottomBarBG aboveSubview:self.mainBackground];
第一行创建了一个新的UIView并设置了其框架。我们根据屏幕高度设置其 Y 位置,以确保无论屏幕大小如何,UIview都将位于视图的底部。
接下来,我们将背景颜色设置为纯白色,并将 alpha 设置为0.5(一半),使视图看起来略微透明。
最后,我们将按钮背景视图添加到主视图中。我们知道按钮背景应该位于主背景之上但位于按钮之下,因此我们使用aboveSubview方法插入视图,以确保它始终直接位于主背景之上。运行应用程序并查看我们菜单的最终设计。
准备导航
我们最后需要做的是为我们的视图添加一个导航控制器。这将用于显示(或推送)我们的我的食物视图。我们可以在故事板中通过单击菜单项来完成此操作。
切换到Main.storyboard,然后选择MenuViewController。从顶部菜单栏,导航到编辑 | 嵌入 | 导航控制器。Xcode 将自动将导航控制器添加到故事板中,将我们的MenuViewController设置为根视图控制器,并将我们的新导航控制器设置为初始视图。现在,我们的故事板将看起来像以下截图:

我们不希望我们的菜单视图显示导航栏,所以让我们切换回MenuViewController.m,并在viewDidLoad中添加以下最终代码行:
[self.navigationController setNavigationBarHidden:YES];
摘要
在本章中,我们通过构建我们的菜单视图并应用约束来介绍了 Auto Layout 的新特性。现在,你对 Auto Layout 的新特性和如何使用它们有了很好的理解,我强烈建议你在多个视图中练习所有不同类型的约束。Auto Layout 非常强大,并且当正确使用时,将消除通常归因于动态布局的大量代码!
在下一章中,我们将继续构建我们应用程序的下一部分。我们将探索 iOS 7 的一些新设计原则,并将它们应用到我们的应用程序Food and Me中。
第四章:为 iOS 7 构建我们的应用程序
我们将本章从介绍 iOS 7 中的一些新设计原则开始。这包括对导航和状态栏、新的 UIKit 以及应用程序图标的更改。接下来,我们将创建所需的文件并将它们组织起来以便于导航。最后,我们将向我们的 Storyboard 添加一些新的视图控制器,并将它们指向我们新创建的文件。完成本章后,我们将有一个完整的应用程序框架,准备添加功能。让我们开始吧!
为 iOS 7 设计
随着 iOS 7 的发布,开发者和设计师需要调整他们的方法以适应新的“扁平化”设计。尽管遵循此设计模式不是强制性的,但几乎所有的 UI 元素在 SDK 中都已经完全重制以支持它。
在设计 iOS 7 应用程序时,考虑这些变化对于保持平衡布局非常重要。一些因素包括新的字体和更新的 UIKit 尺寸。在本章中,我们将构建我们应用程序的框架,但首先我们将讨论 iOS 7 中的两个重要变化,这些变化将直接影响你构建未来应用程序的方式。
导航栏和状态栏
可能 iOS 7 最明显的变化是新导航栏和状态栏。这两个项目自 iOS 推出以来一直存在。在 iOS 7 之前,20 像素的状态栏只是一个简单的纯色背景视图,它会覆盖主应用程序窗口的顶部 20 像素。
此外,当使用导航控制器时,导航栏本身也会以同样的方式工作,覆盖视图的下一个 44 像素(总共 64 像素)。因此,定位 y=0 值的项将直接位于导航或状态栏下方。
随着 iOS 7 的发布,这一变化已被完全移除。状态栏本身现在包含一个清晰的背景,允许任何 UI 元素或视图位于其后面。运行我们的应用程序并注意我们的菜单背景图像如何延伸到设备屏幕的顶部,直接位于状态栏后面,如以下截图所示:

对于大多数应用程序来说,状态栏背景与导航栏背景相匹配是非常常见的。在 iOS 7 中,设置导航栏颜色将自动设置状态栏背景以匹配。以下截图是我们完成的应用程序的一个示例:

这种变化的另一个结果是,在 y 轴上程序化定位你的视图需要你考虑状态栏和导航栏的高度。一个定位在 x=0 和 y=0 的视图将出现在屏幕的左上角,位于导航和状态栏后面。
重要的是要理解,这个变化专门适用于在运行时执行的代码。当使用故事板时,这种新的定位不适用。Xcode 会自动调整故事板中的视图,以适应导航栏和状态栏。每个视图将保留其初始的y位置,无论做出任何调整。
最后,苹果为导航栏添加了一个新的半透明属性。在任何运行 iOS 7 的设备上打开联系人应用,滚动查看你的联系人。你会注意到,当每个项目穿过导航栏时,它可以在移动离开屏幕时通过栏看到。这种效果在 iOS 7 及其新设计的应用程序中得到了广泛应用,苹果还允许开发者使用它。默认情况下,此属性将设置为YES,但如果你希望,可以随时禁用。
新的 UIKit
iOS 7 的新扁平化设计改变了许多常见的 UIKit 元素的尺寸。其中一些包括分段控件、搜索显示控制器和警告视图。这些变化大多数导致比 iOS 先前版本更小的框架,但也包括更新的字体和用户交互。新设计的搜索栏如下截图所示:

分段控件如下截图所示:

通知的新警告视图如下截图所示:

删除的新警告视图如下截图所示:

更新后的应用图标
苹果在 iOS 7 发布时对标准 iOS 应用程序图标大小进行了细微的调整。之前,图标具有易于复制的均匀圆角。苹果提供了一种新的形状,称为超级椭圆,具有更拉伸的圆角。此外,应用程序图标上的光泽(光泽效果)已被移除。像往常一样,Xcode 5 会自动剪裁你的应用程序图标图像到正确的形状;然而,如果你希望添加自己的描边或阴影,你需要使用非官方模板。
此外,苹果引入了他们称之为黄金比例网格系统的内容,你将在下面的屏幕截图中看到。苹果建议在设计图标和布局单个元素时使用此网格。这更被视为一个指南而不是规则,所以如果你觉得它更适合你的应用程序图标,可以自由地在这个网格系统之外工作。

整合碎片
现在我们已经介绍了一些 iOS 7 的基本设计方面,是时候开始构建我们的应用程序骨架了。在我们编写任何代码之前,让我们创建基本的项目文件,在我们的故事板中构建我们的视图,并为每个视图控制器创建/连接出口。我们将能够导航到我们的应用程序;然而,它目前什么也不会做。
项目组织
我开发过程中的第一步是在 Xcode 5 中组织我的项目。这样做使得导航项目并找到所需的文件变得更容易。让我们继续组织我们的应用程序。
打开我们的Food and Me项目,查看左侧的导航器。我们有一些文件,没有特定的顺序,我们很快还会创建更多文件。我们将把主要项目文件分为以下三个单独的类别:
-
应用程序代理 -
视图控制器 -
自定义类
在导航器面板上,右键单击顶级文件夹(Food and Me,我们的应用程序名称)并单击新建组。在我们的主要Food and Me组中会出现一个新的组;让我们将其命名为应用程序代理。重复此过程两次,将新组分别命名为视图控制器和自定义类。
选择AppDelegate.h和AppDelegate.m(使用command键),并将这些文件拖动到我们刚刚创建的应用程序代理组中。对MenuViewController.h和MenuViewController.m也执行相同的操作,并将这些文件拖动到视图控制器组中。这些组也可以重新排列,所以请根据您的喜好自由移动它们。
以下截图展示了在Food and Me项目中找到的最终结果:

创建文件
现在我们项目组织得更好了,是时候创建剩余的项目文件。我们的应用程序需要一个用于菜单的视图控制器,添加新食物的视图,当前食物的视图,以及每个食物项目的详细视图。我们已经设置了菜单,所以让我们创建剩余的文件。
右键单击我们的视图控制器组并单击新建文件(您也可以通过在菜单栏中导航到文件 | 新建 | 文件来实现此操作)。确保在左侧菜单栏中选择Cocoa Touch,从选项中选择Objective-C 类,然后单击下一步。将此文件命名为AddNewViewController,并确保它是一个UIViewController的子类。单击下一步然后单击创建。我们的新文件将被创建并添加到我们的视图控制器组中。
重复此过程两次。第一个文件将命名为MyFoodsViewController,并将作为UITableViewController的子类。第二个文件命名为FoodDetailViewController,并将其设置为UIViewController的子类。我们现在已经有了我们应用程序所需的大部分文件。在后面的章节中,我们将在自定义类组中创建最后一个项目文件。
设置故事板
现在我们有了文件,我们需要在我们的故事板中创建一些视图并将它们连接到我们刚刚创建的类。打开Main.storyboard你应该能看到我们的导航控制器及其根视图控制器(菜单视图控制器)。现在让我们将剩余的控制器添加到我们的故事板中。
打开右侧的工具面板(如果尚未打开)并点击对象库。我们的AddNewViewController和FoodDetailViewController文件都是UIViewController的子类,所以将两个视图控制器对象拖放到故事板中。我们的MyFoodsViewController是UITableViewController的子类,所以让我们将一个UITableViewController对象拖放到故事板中。
选择一个UIViewControllers并打开身份检查器。在顶部的自定义类部分,将这个View Controllers类设置为AddNewViewController。现在选择剩余的UIViewController并将其类设置为FoodDetailViewController。最后,选择我们的UITableViewController并将其类设置为MyFoodsViewController。现在我们的故事板已经包含了我们应用所需的所有对象。
AddNewViewController
现在我们已经创建了文件并添加了适当的控制器到我们的故事板中,让我们继续添加每个控制器所需的对象。我们将从AddNewViewController对象开始。选择它,然后导航到对象库在工具面板中。
我们的应用将使用户能够追踪他们所吃的食物。每一项食物将包括一个图片、名称/标题以及创建日期。我们需要提供一个图像视图来存储最终图片、一个占位符图像视图以及一个UITextField对象来输入食物项的名称。
将一个UITextField对象和两个UIImageView对象拖放到AddNewViewController视图中。这将允许我们的用户与视图交互并为我们的应用创建食物项。在本章中,我们只是简单地将所有元素添加到我们的项目中,所以不要担心这些对象的大小或位置。
选择AddNewViewController对象本身,然后点击右上角的辅助编辑器按钮(类似于燕尾服的按钮)。如果尚未显示,切换到AddNewViewController.h。为了在代码中访问这些对象,我们将为代码中的每个项目创建出口。在按住键盘上的控制键的同时,从UITextField拖动到头文件并释放。将此出口命名为nameTextField并点击连接。对两个UIImageView重复此操作。将第一个图像视图命名为placeholderImageView,第二个命名为finalImageView。我们现在有了AddNewViewController所需的所有对象和连接。
FoodDetailViewController
当用户选择他们之前添加的其中一个食品项目时,我们希望显示一个详细视图,该视图包括全屏背景图像、食品图像、食品名称,最后是保存的日期。在故事板中选择FoodDetailViewController类,并导航回实用工具面板和对象库。
将两个UIImageView和两个UILabel拖动到食品详情视图中。再次忽略每个项目的尺寸和位置。我们还将为每个对象在我们的代码中添加出口,所以请打开辅助编辑器并切换到FoodDetailViewController.h文件。从第一个图像视图拖动控制到.h文件中@interface和@end之间的空间,并将出口命名为backgroundImageView。对第二个图像视图执行相同的步骤,并将其命名为foodImageView。
我们将这些标签用于显示与食品项目相关的名称和日期。从第一个标签开始拖动控制,并将此出口命名为foodNameLabel。第二个UILabel应命名为foodDateLabel。现在我们已经有我们FoodDetailViewController类所需的所有视图了。
MyFoodsViewController
当我们将UITableViewController对象拖动到故事板中时,Xcode 5 自动向控制器中添加了一个UITableView对象,带有一个平面原型单元格。Food and Me将使用自定义的UITableViewCell子类来创建和布局我们的表格视图单元格。这将在后面的章节中介绍,所以目前我们将对MyFoodsViewController类进行一个简单的更改。选择原型单元格,单元格框架底部将出现一个小白框。点击并拖动此框以调整单元格大小。将其高度设置为 100 像素,如图所示:

摘要
在本章中,我们学习了与 iOS 7 相关的一些较新的设计原则。此外,我们组织了我们的项目,创建了所有必需的文件,并在故事板中开始我们的视图。我们现在准备好开始编写代码并为我们的应用程序添加功能。
在下一章中,我们将完成构建AddNewViewController类,并添加保存用户数据以便以后查看的功能。
第五章:创建和保存用户数据
在本章中,我们将首先调整导航栏的样式以匹配 iOS 7 的导航样式。接下来,我们将在导航栏中创建我们的按钮并将它们连接到适当的方法。一旦我们调整了我们的故事板,我们就可以开始编写代码,允许用户拍摄或选择图片,为项目添加标题,然后将数据备份到磁盘以供以后使用。完成本章后,我们的应用程序将具备用户保存新食品项目所需的所有功能!
从我们上次停止的地方继续
在上一章中,我们创建了所有文件并将它们连接到我们的故事板。然后我们在每个视图控制器中添加了所有必需的元素(标签、图像视图等)。最后,我们创建了出口并将它们连接到我们的故事板元素。本章以及下一章的目的是为了完成应用程序的构建。我们将实现核心功能的一部分,即能够拍摄或选择照片,添加名称,然后保存数据。一旦我们完成这项工作,我们就可以开始使用 iOS 7 的一些新功能来为我们的应用程序添加额外的视觉吸引力。
导航栏样式
在我们继续之前,让我们先改变一些导航栏样式选项。我们的应用程序在每个视图中都将具有相同的导航栏样式,因此我们最好的方法是在 AppDelegate 对象中使用 UINavigationBar 的外观代理。这将允许我们只编写一次代码,整个应用程序中的导航栏都将遵循这些样式。
切换到 AppDelegate.m 文件并向下滚动到 applicationDidFinishLaunchingWithOptions 方法。我们将设置导航栏的颜色、标题标签的字体以及导航栏的色调颜色(这将改变导航栏上按钮项的颜色)。此外,我们的应用程序将具有基于文本的按钮项,因此我们希望将 UIBarButtonItem 的外观代理设置为与我们的应用程序风格相匹配。将以下代码复制并粘贴到 applicationDidFinishLaunchingWithOptions 中:
[[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:200.0//255 green:0.0/255 blue:23.0/255 alpha:1.0f]];
[[UINavigationBar appearance] setTitleTextAttributes: @{NSForegroundColorAttributeName: [UIColor whiteColor],
NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue" size:19.0f] }];
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];
[[UIBarButtonItem appearance] setTitleTextAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"HelveticaNeue-Light" size:18.0f]} forState:UIControlStateNormal];
首先,我们将导航栏底部的颜色设置为深红色。接下来,我们将标题文本字体颜色设置为白色,并将其字体设置为特定的字体。你可以用你喜欢的任何字体替换这个字体;我只是喜欢这个字体的样子。为了匹配我们的标题文本,所有导航栏按钮也应为白色,因此我们将 navigationTintColor 方法(不要与 navigationBarTintColor 方法混淆,后者将改变导航栏本身的颜色而不是导航项的颜色)设置为白色。最后,我们更改了 UIBarButtonItem 对象的字体以匹配导航栏标题样式。
现在我们已经为导航栏设置了样式,接下来让我们向MenuViewController添加一些代码来最终确定应用程序的样式。切换到MenuViewController.m文件,并向下滚动到viewDidLoad方法。首先,让我们调整菜单按钮的背景颜色。之前,我们将backgroundColor属性设置为白色,但让我们将其改为与我们的深红色导航栏相匹配。用以下代码片段替换之前的背景颜色代码:
bottomBarBG.backgroundColor = [UIColor colorWithRed:200.0/255 green:0.0/255 blue:23.0/255 alpha:0.7f];
最后,将以下代码写入viewDidLoad方法:
// Set this in every view controller so that the back button displays back instead of the root view controller name
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@" " style:UIBarButtonItemStylePlain target:nil action:nil];
这是一段非常实用的代码。默认情况下,当viewController方法被推送到导航堆栈时,它将显示一个返回按钮(小于号)和文本。文本基于前一个视图控制器的标题。我们只想显示<符号,因此我们添加了上一行代码。我们基本上在告诉应用程序,对于每个返回按钮,文本应该等于@"",或者一个空字符串。值得注意的是,这可以通过在故事板中设置导航项的返回按钮属性来更改。以下截图是引入空字符串前后的应用程序示例:

添加按钮动作
我们下一步要添加的是当按下添加新项按钮时要调用的代码。在我们之前的章节中,我们创建了一个名为addNewPressed的动作,并将其连接到我们的添加新项按钮。让我们继续编写当按下此按钮时显示适当视图控制器的代码。首先,切换到MenuViewController.h文件,并在标准的#import UIKit 语句下方,按照以下代码片段导入我们的视图控制器:
#import <UIKit/UIKit.h>
#import "AddNewViewController.h"
#import "MyFoodsViewController.h"
#define ADD_NEW_VIEW_CONTROLLER @"AddNew"
我们还定义了一个用于故事板 ID 的字符串字面量,我们将其命名为ADD_NEW_VIEW_CONTROLLER,以便我们知道它包含的内容。切换回MenuViewController.m文件,并向下滚动到我们的addNewPressed方法。由于我们将要显示这个视图控制器(从底部拖动到屏幕上),我们还需要创建一个导航控制器来持有AddNewViewController对象。以下是为添加按钮动作的代码:
- (IBAction)addNewPressed:(UIButton *)sender {
// Present the addNewFoodViewController
AddNewViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier: ADD_NEW_VIEW_CONTROLLER ];"];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
[self.navigationController presentViewController:nav animated:YES completion:nil];
}
这段代码相当直接。我们通过从故事板初始化来分配我们的 AddNewViewController 对象。确保我们的故事板中的 AddNewViewController 具有我们的故事板 ID 属性设置为 AddNew,以便它与之前定义的字符串字面量相匹配。在某些情况下,如果在按钮按下和视图展示之间有明显的延迟,建议在展示之前预先初始化视图控制器。接下来,我们创建一个导航控制器并将我们新创建的 AddNewViewController 分配为其根视图控制器。最后,我们告诉当前的导航控制器展示新的导航控制器。运行应用程序并测试这个功能。应该滑动到屏幕上的导航控制器中的 AddNewViewController 对象。
将按钮添加到我们的导航栏
你可能已经注意到,在展示我们的 AddNewViewController 对象时,我们没有方法来关闭视图回到菜单。现在让我们添加这个功能。我们将创建两个仅包含文本的按钮项。第一个按钮,取消,将关闭视图,而第二个,保存,将保存新的食物条目。
切换到 AddNewViewController.m 并滚动到 viewDidLoad。在 viewDidLoad 的顶部添加以下代码:
// Add our bar button items
UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelButtonPressed:)];
UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveButtonPressed:)];
// Assign the bar buttons to the navigation controller
[self.navigationItem setLeftBarButtonItem:cancelButton];
[self.navigationItem setRightBarButtonItem:saveButton];
在这里,我们使用 iOS 提供的内置 取消 和 保存 按钮项创建了两个按钮项。每个按钮都有自己的选择器(或方法),我们将在稍后进行编码。接下来,我们将每个按钮项分配给导航栏。我选择将 取消 放在栏的左侧,将 保存 放在右侧;然而,这个顺序完全取决于你。如果我们运行我们的应用程序并点击 添加新项 按钮,我们的视图将滑动到合适的位置,你将看到左侧是 取消,右侧是 保存。我们在应用程序委托中定义的外观代理也应该反映在字体和文字颜色上。接下来,让我们实际添加 取消 按钮的功能。
切换回 AddNewViewController.m 并滚动到 viewDidLoad 的底部。我们希望允许用户取消添加食物项,所以让我们编写之前分配给取消按钮的 cancelButtonPressed 方法。在 viewDidLoad 下方直接添加以下代码:
- (void)cancelButtonPressed:(UIButton *)sender {
// Dismiss the view
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)saveButtonPressed:(UIButton *)sender {
}
我们在这里定义了 cancelButtonPressed 和 saveButtonPressed 方法(saveButtonPressed 已经有意留空,直到本章的后面部分)。在 cancelButtonPressed 中,我们只是告诉视图控制器关闭自己并将动画视图控制器设置为 YES。运行应用程序并测试这个功能。
调整我们的故事板视图
现在我们已经实现了一些代码,我们需要完成在Main.storyboard中排列视图。之前,我们只添加了所需的元素,但没有正确地定位或调整它们的大小。切换到Main.storyboard并向下滚动到AddNewViewController对象。
我们有三个项目需要定位和调整大小,首先是两个图像视图。选择第一个图像视图,在工具面板中,选择大小检查器子菜单。将宽度和高度设置为 180 像素以创建一个完美的正方形。将此图像视图水平放置在视图的中心,并略高于视图的垂直中心。不要担心过于精确,可以自由地将图像视图放置在您认为看起来最好的位置!
重复此过程以处理其他图像视图,使其大小相同且位置完全一致。对于此图像视图,切换到属性检查器子菜单(在右侧的工具面板中),并将其图像设置为placeholder_image用于placeholderImageView。使用文档大纲,确保此图像视图位于其他图像视图的下方。我们将使用两个图像视图来协助保存验证。当用户选择或拍摄图像时,它将被设置为顶部的图像视图(空的一个)并覆盖下方的占位符图像视图。这允许我们检查顶部图像视图是否包含图像。如果没有,这意味着用户尚未添加图像,占位符仍然可见。在这种情况下,我们将提醒用户,让他/她知道他/她必须包含一张照片。
最后,我们需要调整将用于输入食物条目名称的UITextField对象的设置。选择文本字段并重新打开大小检查器子菜单(从工具面板)。将高度设置为 38 像素,宽度设置为 280 像素。将文本字段水平居中并略高于图像视图。
从工具面板中选择属性检查器并更改以下设置:
-
对齐:选择中心图标
-
占位符:
输入食物名称 -
边框样式:此字段应设置为无(四个按钮中的第一个)
-
大小写:选择单词
最终结果应类似于以下截图:

添加我们的代表
现在我们已经完全设置了视图,我们可以开始编写所需的功能代码。在我们可以继续之前,我们需要指定视图控制器将需要的某些委托。我们将与文本字段、图像选择器、导航控制器和操作表一起工作,它们都有自己的自定义委托。切换到AddNewViewController.h并替换#import之后的代码行,替换为以下代码:
@interface AddNewViewController : UIViewController <UITextFieldDelegate, UIActionSheetDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
在这里,我们只是简单地指定我们的类将在代码中遵循的协议。完成这一步后,我们现在可以开始将功能编码到应用程序中!
使用点击手势
对于我们的应用程序,用户可以点击占位符图像来拍照或选择照片。为此,我们将直接在占位符图像视图中添加一个点击手势识别器。切换到AddNewViewController.m并滚动到viewDidLoad。在viewDidLoad文件底部添加以下代码:
// Add a border around our image view
[self.placeholderImageView.layer setBorderWidth:6.0f];
[self.placeholderImageView.layer setBorderColor:[UIColor colorWithRed:129.0/255.0 green:129.0/255.0 blue:130.0/255.0 alpha:1.0].CGColor];
UITapGestureRecognizer *imageViewTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageViewTapped:)];
[imageViewTapGesture setNumberOfTapsRequired:1];];
[self.placeholderImageView setUserInteractionEnabled:YES];
[self.placeholderImageView addGestureRecognizer:imageViewTapGesture];
首先,我们为我们的图像视图添加一个边框和圆角以产生视觉效果。接下来,我们创建一个点击手势识别器并为其分配一个方法。我们还设置属性numberOfTapsRequired为1。最后一步是将用户交互启用在我们的占位符图像视图中设置为YES,然后将我们的手势识别器添加到它上面。现在,我们的图像视图将一直监听单次点击,并在检测到点击时调用我们的imageViewTapped方法。
当图片被点击时,我们希望给用户一个选项,要么从他们的库中选择一个图片,要么使用相机拍摄一个新的。最好的方式是使用操作表。让我们创建imageViewTapped方法,显示一个操作表,然后相应地响应用户的选择。在ButtonPressed方法下方,添加以下代码:
#pragma mark - User Interaction Methods
- (void)imageViewTapped:(id)sender {
[[[UIActionSheet alloc] initWithTitle:nil
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Take Picture", @"Choose From Library", nil]
showInView:self.view];
}
使用这段代码,当用户点击图像视图时,我们创建一个操作表并在当前视图中显示它。我们只需要取消按钮和两个额外的按钮,一个用于拍照,另一个用于从库中选择。为了我们能够相应地响应用户选择的操作表按钮,我们需要实现操作表代理方法。在imageViewTapped方法下方,添加以下代码:
#pragma mark - Action Sheet Delegate
-(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == actionSheet.cancelButtonIndex) {
return;
}
if (buttonIndex == 0 && [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
// Take Picture Selected
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.allowsEditing = YES;
[imagePicker setSourceType:UIImagePickerControllerSourceTypeCamera];
[self.navigationController presentViewController:imagePicker animated:YES completion:nil];
}
if (buttonIndex == 1) {
// Choose Photo From Library
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.allowsEditing = YES;
[imagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
[self.navigationController presentViewController:imagePicker animated:YES completion:nil];
}
}
在这个方法中,我们首先检查选中的按钮是否是取消按钮,如果是,则返回以结束此方法的执行,这将为我们隐藏操作表。接下来,我们检查按钮索引是否等于0,或拍照。如果是,我们创建一个UIImagePickerController实例。我们将代理设置为self,并允许编辑(这将允许用户将图片裁剪成完美的正方形,这对我们的应用程序来说很理想),然后我们将源类型设置为相机。
如果按钮索引是1,或者从库中选择,我们使用完全相同的代码,只有一个例外。对于这个块,将源类型设置为照片库以显示手机的相机库。保存我们的代码并运行应用程序。一切应该按预期工作。
从 UIImagePickerController 获取图片
现在用户可以拍照或从他们的手机照片库中选择,我们需要获取那张图片并显示它。为了做到这一点,我们需要实现图像选择器的代理方法,即didFinishPickingMediaWithInfo方法。在我们的操作表代理方法下方,添加以下代码:
#pragma mark - UIImagePicker Delegate
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
UIImage *pic;
//Grab the stored image
if ([info objectForKey:UIImagePickerControllerEditedImage]) {
pic = [info objectForKey:UIImagePickerControllerEditedImage];
[self.finalImageView setImage:pic];
[self.placeholderImageView setHidden:YES];
}
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
在这个方法中,我们创建一个UIImage实例,并使用图片选择器提供的信息字典来分配它。因为我们希望我们的用户编辑图片,所以我们想要获取编辑后的版本而不是原始版本(可以使用UIImagePickerControllerOriginalImage访问),现在我们有了最终的图片,我们将它分配给最终图片视图,然后隐藏占位符图片视图。最后,我们需要告诉图片选择器控制器自己消失,这样我们就可以回到我们的AddNewViewController。
保存所有内容并运行代码以测试。如果你希望实际上用相机拍照,你必须在一个实际设备上运行此操作。
添加文本字段代理
现在我们有了我们的图片,我们需要设置文本字段代理。这可能是所有代理方法中最简单的一个,因为我们只需要告诉应用程序当按下返回键时应该做什么。对于我们的应用程序,我们只想隐藏键盘。在我们的图片选择器代理方法下方,添加以下代码:
#pragma mark - Text Field Delegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return NO;
}
此方法简单地告诉文本视图在按下返回键时放弃第一个响应者(隐藏键盘)。用户可以输入一个名字,按下返回键,然后隐藏键盘。确保文本字段的代理属性已经在故事板或viewDidLoad中设置为AddNewViewController(self)。
保存数据
我们现在从用户那里获得了创建新食物条目所需的一切。为了保存数据,我们需要遵循多个步骤,以便我们可以在稍后应用程序中再次访问它。我们之前创建的保存数据方法一旦完成将会相当长,所以我们将为了简单起见逐块介绍,从自定义日期辅助方法开始。
获取日期字符串
对于我们的应用程序,我们将创建一个.plist文件,该文件将存储食物条目的名称、创建日期以及图片的文件路径。图片本身将单独保存在文档目录中。在将任何文件保存到文档目录时,你必须指定一个文件名。为了保存多个图片,我们需要确保每个保存的图片文件都有一个不同的文件名。实现这一目标的一种最好(也是最流行)的方法是使用日期。
每个设备都会追踪到毫秒级的当前日期。这意味着在任何给定的毫秒,日期都会与之前和之后的每个日期完全不同。这为根据图片创建时间创建每个图片的唯一标识符提供了一个很好的方法。我们将做的是获取当前日期,设置日期格式,并将其转换为字符串,然后将它附加到每个文件名的末尾。这样,每个图片都将有一个唯一的文件名,该文件名将被存储在我们的.plist文件中,以便稍后访问。
我创建了一个简单的辅助方法,该方法返回当前日期作为字符串值,我们可以将其用于文件名,所以让我们将其添加到我们的代码中。滚动到上一个方法的末尾并添加以下代码:
#pragma mark - Date Helper Method
-(NSString*)stringForCurrentDateTime
{
NSDateFormatter *format = [[NSDateFormatter alloc] init];
[format setDateFormat:@"yyyyMMddHHmmss"];
NSDate *now = [NSDate date];
NSString *dateString = [format stringFromDate:now];
return dateString;
}
使用此代码,我们首先创建一个日期格式化器,它将日期的年、月、日、小时、分钟和秒值组合在一起。然后,我们创建一个日期对象并将其设置为当前日期和时间。最后,我们使用我们的日期格式化器创建一个字符串并返回它。现在我们有了辅助方法,让我们添加保存数据的代码!
添加验证
我们现在准备实现saveButtonPressed方法。在我们编写任何实际保存数据的代码之前,我们首先需要检查用户是否实际上已选择了一个图片并添加了名称。这将防止我们出现任何错误,并保证我们有所需的数据。滚动到我们之前创建的空saveButtonPressed方法,并添加以下代码:
- (void)saveButtonPressed:(UIButton *)sender {
// Check if the image and title have been saved
// If so, save the image to the documents directory and dismiss the view
if (self.finalImageView.image && self.nameTextField.text.length > 0) {
// Image and name have been set, so we can save
} else {
[[[UIAlertView alloc] initWithTitle:@"Missing Data"
message:@"A title and image are both required to save."
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles:nil]
show];
}
}
此验证非常简单但有效。在这里,我们通过检查最终图像视图是否为 nil 来使用多个图像视图。我们还通过检查文本属性的长度是否大于零来确保用户实际上已经向文本框中添加了文本。如果这两个条件中的任何一个为假,我们将显示一个警告视图,告诉用户需要保存标题和图片。如果两者都为真,我们可以继续我们的保存过程。建议您使用数据模型来维护应用程序中的数据;然而,对于我们的应用程序来说,这样做就足够了。
保存图片
保存过程的第一步是将图片本身保存到文档目录中。在第一个if语句块内部,添加以下代码:
// get paths from root direcory and the main documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths firstObject];
// Set up and save our image to the documents directory
NSString *imagePath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"image-%@", [self stringFromCurrentDateTime]]];
NSData* data = UIImagePNGRepresentation(self.finalImageView.image);
[data writeToFile:imagePath atomically:YES];
首先,我们从文件系统中的目录列表中获取第一个元素(它始终是文档目录的路径)。然后,我们在文档路径的末尾添加一个文件名来创建我们的图片路径。文件名是我们稍后在应用程序中访问图片的方式。使用我们的日期辅助方法,我们将文件名设置为 image,并使用连字符分隔返回的日期字符串。现在,每个图片都会在文档目录中以唯一的文件名找到。最后,我们创建一个NSData实例,使用UIImagePNGRepresentation将用户的最终图片作为数据分配给它,然后将数据保存到之前创建的图片路径中。
现在我们已经保存了图片,我们可以保存其余的数据。
创建与加载.plist 文件
为了保存用户数据,我们将创建一个包含每个食物条目所有相关数据的字典。然后,我们将此字典添加到.plist文件中,并将.plist文件保存到文档目录中。为了确保我们不覆盖我们之前的数据,我们必须首先检查我们的.plist文件是否已经存在。在保存图片的上一段代码下方添加以下代码:
// Get the path to our Data/plist file and where we will be saving our images
NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"Data.plist"];
// Forward reference of our array
NSMutableArray *plistDataArray;
// Call the file manager to check if the file exists
NSFileManager *defaultManager = [NSFileManager defaultManager];
if ([defaultManager fileExistsAtPath:plistPath])
{
// Assign the data
// Get the current data from the plist file if it exists
plistDataArray = [NSMutableArray arrayWithContentsOfFile:plistPath];
}
else
{
//create empty file
NSMutableArray *array = [NSMutableArray array];
[array writeToFile:plistPath atomically:YES];
plistDataArray = [NSMutableArray arrayWithContentsOfFile:plistPath];
}
首先,我们通过在之前创建的文档目录末尾添加Data.plist来创建另一个路径(这个名称可以是任何你想要的,只要以.plist结尾)。接下来,我们创建一个空数组来保存最终的.plist数据,并允许我们在文件的末尾追加更多数据。我们创建一个NSFileManager实例,并使用它来检查新创建的路径上的文件是否存在。如果存在,我们将我们的.plist文件的内容设置为plistDataArray方法。
如果.plist文件不存在,我们将创建另一个空数组,将数组保存为.plist文件,然后将plistDataArray方法设置为新创建的(但为空).plist文件的内容。现在我们可以添加更多数据。
添加新条目
现在,我们将获取用户数据并将其转换为字典,以便我们可以将其添加到我们的数据数组中。然后,我们可以将其保存到文档目录中。在之前的代码之后添加以下代码:
// Create a new food item
NSMutableDictionary *foodItem = [[NSMutableDictionary alloc] init];
[foodItem setValue:self.nameTextField.text forKey:@"name"];
[foodItem setValue:imagePath forKey:@"image_filepath"];
[foodItem setValue:[NSDate date] forKey:@"date"];
[plistDataArray addObject:foodItem];
[plistDataArray writeToFile:plistPath atomically:YES];
[self dismissViewControllerAnimated:YES completion:nil];
在这里,我们创建一个新的空可变字典。然后,我们将用户输入的名称、之前使用的图像路径以及当前的日期和时间填充到字典中。然后,我们将这个字典添加到我们的plistDataArray方法中,并告诉它保存(写入)文件。最后,我们关闭视图控制器,回到我们开始时的菜单,我们的数据已经保存!
摘要
在本章中,我们构建了我们应用程序最重要的组件,即创建新的食物条目的能力。现在所有这些数据都已保存,我们可以检索它并开始向用户显示。因为所有内容都直接保存到设备上,所以我们能够即时操作这些数据并按需使用它。
在下一章中,我们将构建我们应用程序核心功能的最后一部分:在表格视图和详细视图中查看用户创建的数据。
第六章:显示用户数据
我们几乎完成了我们的应用程序;然而,我们仍然需要添加一个最后的主要功能。现在,我们的用户可以添加内容,他们需要能够查看这些数据。在本章中,我们将组装一个自定义单元格,构建一个表格视图来显示数据列表,并在用户从列表中选择一个项目时构建项目的详细视图。完成本章后,我们将拥有一个完全功能的应用程序。
我们将首先在故事板中组装我们的自定义单元格。接下来,我们将在导航栏中添加一个按钮,以便用户在查看当前食物项目时可以添加食物。然后,我们将设置表格视图,加载数据,并将数据传递到表格视图中。最后,当用户从表格视图中选择一个项目时,我们将实现项目的详细视图。让我们开始吧!
自定义单元格
在我们开始编写显示数据的代码之前,我们想要创建一个自定义表格视图单元格。在项目打开的情况下,选择文件 | 新建 | 文件。选择Cocoa Touch作为基础,在点击下一步之前选择Objective-C 类。我们希望这个类是UITableViewCell的子类。单元格将显示食物项目,所以让我们给它命名为FoodCell。保存此文件并将其移动到我们的自定义类组(如果它还没有在那里)。
现在我们有了我们的类,让我们将其链接到我们的故事板。打开Main.storyboard并找到之前移动到故事板中的表格视图控制器。在打开Main.storyboard文件后,打开实用工具面板并选择标识部分。确保这个视图控制器的类已被设置为MyFoodsViewController。现在选择空白表格视图单元格并将其类设置为我们的新创建的FoodCell类。
构建单元格
现在我们已经将类链接起来,我们可以在故事板中构建单元格。单元格本身将包含一个UIImageView对象和两个UILabel实例。打开实用工具面板,执行以下步骤:
-
将一个
UIImageView对象拖放到单元格本身以添加它。 -
将宽度和高度都设置为 100 像素。
-
将图像视图放置在单元格的左侧。
-
将两个
UILabel实例拖放到单元格中,一个位于另一个上方。 -
从属性面板中,将顶部标签的字体家族设置为Helvetica Neue。
-
将样式设置为超轻。
-
将大小设置为
20。 -
对第二个(底部)单元格重复此操作,但将其大小设置为
11。 -
将两个标签都设置为左对齐。
-
将标签水平放置到您喜欢的位置。
完成后,您的单元格应类似于以下截图:

连接单元格
现在我们已经可视地布局了单元格,我们需要将其连接到我们之前创建的类;执行以下步骤以连接此单元格到我们的类:
-
选择整个单元格,然后选择辅助编辑器。确保我们在右侧面板中查看
FoodCell.h。 -
从图片拖动以创建一个名为
foodImageView的出口。我们不想将其命名为imageView,因为这个属性已经在UITableViewCell上默认存在。 -
分别给两个标签命名为
foodNameLabel和dateAddedLabel。
创建属性
现在单元格已经准备好了,让我们开始编写 MyFoodsViewController 的代码。这个类将加载保存的用户数据以显示它,因此我们需要创建一个数组属性来保存数据。此外,我们希望日期以用户友好的格式显示,因此我们还要创建一个日期格式化属性。日期格式化器是苹果提供的一个有用的类,它允许您根据特定的模式操纵日期的格式。考虑到不同的地区需要不同的日期格式,这非常有用。切换到 MyFoodsViewController.h 并添加以下代码:
#import <UIKit/UIKit.h>
#import "FoodCell.h"
#import "AddNewViewController.h"
#import "FoodDetailViewController.h"
@interface MyFoodsViewController : UITableViewController
@property (strong, nonatomic) NSArray *myFoodsArray;
@property (strong, nonatomic) NSDateFormatter *dateFormatter;
@end
在前面的代码中,我们简单地创建了所需的两个属性。除了查看已创建的食物项外,用户还将能够从该视图创建新项目。为了支持这一点,我们导入了我们创建的自定义单元格以及 AddNewViewController 类。我们还导入了 FoodDetailViewController 类,以便我们可以显示我们的详细视图。
添加食物
我们想要做的第一件事是让用户能够从该视图添加额外的食物项。最好的方法是向导航栏中添加一个按钮。苹果提供了一个系统按钮来添加项目,它将显示为一个漂亮的加号按钮。切换到 MyFoodsViewController.m 并滚动到 viewDidLoad 方法。添加以下代码:
// Set our views title
self.title = @"MY FOODS";
// Create the plus button
UIBarButtonItem *plusButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addButtonPressed:)];
// Assign the bar buttons to the navigation controller
[self.navigationItem setRightBarButtonItem:plusButton];
// Set this in every view controller so that the back button displays the back button only without the viewcontroller name
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@" " style:UIBarButtonItemStylePlain target:nil action:nil];
在这里,我们创建一个新的 UIBarButtonItem 属性用于导航栏。我们将按钮设置为系统项目添加,以给我们那个加号按钮。接下来,我们告诉导航控制器将此按钮添加为右侧栏按钮项,因此它将显示在导航栏的右侧。
此外,我们设置了导航控制器的标题并调整了返回按钮文本。默认情况下,iOS 将将前一个视图控制器的标题添加到返回按钮上。对于我们的应用程序设计,我们只想显示返回按钮图标,没有文本。此行代码可以添加到任何您希望复制此功能的视图控制器中。
最后,我们想要实现与我们在 MenuViewController 中使用的相同的 addButtonPressed 方法。在 viewDidLoad 下方添加以下代码:
- (void)addButtonPressed:(id)sender {
// Present the addNewFoodViewController
AddNewViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"AddNew"];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
[self.navigationController presentViewController:nav animated:YES completion:nil];
}
准备表格视图
在加载数据之前,让我们先设置我们的表格视图。为此,我们将编辑 Xcode 自动为我们创建的表格视图代理方法。滚动到 numberOfSectionInTableview 方法并将返回值从 0 更改为 1。
我们应该实现的下一个代理方法是 numberOfRowsInSection。这个数字会经常变化,所以我们不会像上一个方法那样硬编码数字,而是将其设置为 myFoodsArray 的计数。每次数组更新时,表格视图也会更新。
下一个需要更新的方法是 cellForRowAtIndexPath。默认代码只要我们更新类名和单元格标识符就可以正常工作。将 Cell 替换为 FoodCell,并将类声明从 UITableViewCell 更改为 FoodCell。
最后,我们需要添加一个之前未添加的额外代理方法。在 cellForRowAtIndexPath 下方,输入 - table,将出现一系列可能的方法。滚动浏览,找到 didSelectRowAtIndexPath 并选择它。Xcode 5 将自动输入方法调用的其余部分。确保包括方法的开始和结束括号。
您的代码应类似于以下代码:
#pragma mark - Tableview Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return self.myFoodsArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = @"FoodCell";
FoodCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
}
加载数据
是时候加载数据了,这样我们就可以在表格视图中显示它。加载数据的方式与我们在上一章中检查文件和保存数据的方式非常相似,因为两者都需要在文档目录中定义一个指定的路径。对于我们的应用程序,我们将创建一个加载数据并返回数组的函数。
将以下代码放置在 addButtonPressed 方法下方:
- (void)loadFoodFromDocumentsDirectory {
// Get paths from root directory and the main documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
// Get the path to our Data/plist file and where we will be saving our images
NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"Data.plist"];
// Call the file manager to check if the file exists
NSFileManager *defaultManager = [NSFileManager defaultManager];
if ([defaultManager fileExistsAtPath:plistPath])
{
// Assign the data
// Get the current data from the plist file if it exists
self.myFoodsArray = [NSMutableArray arrayWithContentsOfFile:plistPath];
[self.tableView reloadData];
}
else
{
// Do nothing
}
}
这段代码应该有些熟悉。首先,我们使用 NSDocumentsDirectory 创建一个路径数组,并将文档目录的路径分配给一个字符串。接下来,我们定义我们想要加载的文件的路径,在本例中是之前创建的 Data.plist 文件。我们分配一个 NSFileManager 实例,并使用它来检查指定路径是否存在该文件。如果存在,我们将文件的內容分配给 self.myFoodsArray,然后重新加载表格视图。现在,我们已经加载了所有数据(如果有),并将这些数据传递给了表格视图。
显示数据
在加载数据后,我们现在可以在自定义单元格中显示数据。滚动到 cellForRowAtIndexPath 并设置我们的单元格。
首先,我们需要从我们的数组中获取当前食品项目。我们将使用传递给 cellForRowAtIndexPath 的 indexPath.row 参数来完成此操作。在我们的单元格分配下方并在 return cell 之前添加以下代码行:
// Create an instance of the current food item
NSDictionary *currentFoodItem = self.myFoodsArray[indexPath.row];
现在我们有了 currentFoodItem,我们可以开始分配我们自定义单元格的属性。让我们从图片开始。将以下代码添加到 cellForRowAtIndexPath:
// Grab the image from the current food item and set the cell image
UIImage *foodImage = [UIImage imageWithContentsOfFile:currentFoodItem[@"image_filepath"]];
cell.foodImageView.image = foodImage;
在这里,我们根据为每个食品项目创建的 image_filepath 键分配一个图像。接下来,我们将此图像设置为当前单元格的图像。现在,我们可以使用以下代码设置食品项目的文本:
// Set the name of the food
cell.foodNameLabel.text = [currentFoodItem objectForKey:@"name"];
在我们的单元格中最后要更新的项目是食品项目添加的日期。我们需要在实际工作之前创建我们的日期格式化器;因此,让我们现在就做。分配日期格式化器可能会非常占用 CPU 资源,因此我们创建了一个属性,它只分配一次,而不是每次加载单元格时都分配。滚动到viewDidLoad方法并添加以下代码:
// Set the date formatter
self.dateFormatter = [[NSDateFormatter alloc] init];
[self.dateFormatter setDateFormat:@"MMM d, YYYY"];
首先,我们分配并初始化我们的NSDateFormatter属性。接下来,我们设置日期格式。我们选择的格式将显示月份作为单词、月份中的日期数字以及全部数字的年份。所有这些设置完成后,运行你的代码以测试它。如果你还没有任何食品项目,请从该视图添加一些以测试其功能。
最后,将以下最终代码添加到cellForRowAtIndexPath:
// Set the date using our date formatter
cell.dateAddedLabel.text = [self.dateFormatter stringFromDate:[currentFoodItem[@"date"]];
在这里,我们将单元格的日期设置为当前项目日期,格式由我们的日期格式化器指定。
显示详细视图
当选择一个食品项目时,用户应被引导到一个新的视图,该视图具有显示更多详细信息的功能。处理所有这些代码将在didSelectRowAtIndexPath中完成。
在我们编写推送详细视图的代码之前,我们将调整其布局。切换到Main.storyboard,定位FoodDetailViewController对象,并执行以下步骤:
-
选择一个图像视图,并调整其大小以适应整个屏幕。这将是我们背景图像,因此它必须是背景层。如果需要,请使用文档大纲来排列视图。
-
选择剩余的图像视图,并设置其大小为 200 x 200 平方像素。将其水平居中并放置在屏幕顶部。这将是大食品项目的图像。
-
接下来,将视图中的两个标签移动到第二个图像(食品图像)下方。选择第一个标签,从实用工具面板打开属性检查器。将字体设置为大小为
24的Helvetica Neue Thin。现在,切换到大小检查器,设置标签的宽度为280,高度为32。将此标签水平居中并放置在食品图像下方。 -
选择第二个标签,并将其字体设置为大小为
13的Helvetica Neue Thin。同时,将其宽度设置为280,高度设置为26。此外,将此标签水平居中放置在名称标签下方。
我们在故事板中的所有内容都是我们最终应用所需的!以下截图显示了你的FoodDetailViewController应该看起来像什么:

编写详细视图的代码
在推送详细视图时,我们将传递食品项目字典作为属性,以便在详细视图中显示与该食品项目相关的数据。为此,让我们添加一个属性。切换到FoodDetailViewController.h,在我们的IBOutlets下方添加以下属性:
@property (strong, nonatomic) NSDictionary *foodItem;
创建模糊图像
现在,我们可以将一个食品项目传递给这个视图控制器。接下来,我们想要设置详细视图的背景图像。在我们的应用程序中,我们将使用我们的食品图像本身作为背景图像。在设置图像之前,我们将对其进行模糊处理并应用深色调以创建一个漂亮的模糊图像效果。
为了完成这项任务,我们将使用苹果开发者门户上提供的UIImage类别。我已经将这个类别与本书提供的资源文件打包在一起。打开之前从 Packt Publishing 网站下载的Food And Me文件夹(如果您尚未下载这些文件,可以通过在浏览器中访问此链接找到它们:www.packtpub.com/),然后打开Apple Code文件夹。您将找到一个名为UIImage+ImageEffects的.h文件和一个.m文件。将这些文件拖到您的项目中,并确保将复制的项目放入目标项目文件夹中。
现在我们已经将文件放入我们的项目中,我们需要导入它们。在#import <UIKit/UIKit.h>下面添加此导入语句:
#import "UIImage+ImageEffects.h"
让我们使用这个类别。切换到FoodDetailViewController.m,在viewDidLoad内部添加以下代码:
UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.36];
UIImage *foodImage = [UIImage imageWithContentsOfFile:[self.foodItem objectForKey:@"image_filepath"]];
UIImage *blurredBackground = [foodImage applyBlurWithRadius:8 tintColor:tintColor saturationDeltaFactor:1.2 maskImage:nil];
self.backgroundImageView.image = blurredBackground;
首先,我们为图像定义一个着色颜色。我们希望它变暗,这样在明亮的食品图像上白色文本就容易被看到。接下来,我们使用foodItem属性中的image_filepath键创建一个UIImage对象。
下一行是魔法发生的地方。我们创建一个新的UIImage实例,并使用ImageEffects类别中的方法将其分配。此方法需要一些参数。
半径将决定图像的模糊程度。为了获得最佳效果,请选择介于1和12之间的值。
着色颜色是我们希望在图像上使用的颜色。您可以根据每个应用程序的设计设置为您喜欢的任何颜色。
SaturationDeltaFactor将调整图像的饱和度。SaturationDeltaFactor的值越低,图像就越不鲜艳。
对图像进行遮罩可以使您传递一个图像遮罩以实现更高级的模糊形状。
代码的最后一行将背景图像设置为食品图像,这样我们的背景就完全填充了;请再次检查故事板,确保背景图像已设置为填充模式。
以下截图是一个前后对比示例:

所有这些参数都可以根据您的喜好和/或应用程序设计进行调整。
完成我们的详细视图
现在我们已经有了背景图像,让我们填写其余的信息。首先,我们将从常规食品图像开始。将以下代码添加到viewDidLoad中:
self.foodImageView.image = foodImage;
[self.foodImageView.layer setCornerRadius:self.foodImageView.frame.size.width / 2];
[self.foodImageView.layer setBorderWidth:4.0f];
[self.foodImageView.layer setBorderColor:[UIColor whiteColor].CGColor];
通过重用foodImage对象并将其设置为foodImageView属性,我们节省了一些代码。在这里,我们还添加了一个圆角半径以创建圆形图像,并应用了一个宽度为四像素的白色边框。
现在,我们可以编写代码来显示名称和日期。将以下代码添加到viewDidLoad:
self.foodNameLabel.text = [self.foodItem objectForKey:@"name"];
// Set the date formatter
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"MMM d, YYYY"];
self.foodDateLabel.text = [dateFormatter stringFromDate:self.foodItem[@"date"]];
首先,我们根据foodItem属性设置我们的名称。我们的日期应该与我们的表格视图单元格中的格式相同,所以我们使用相同的代码创建一个NSDateFormatter对象并设置其格式。现在,我们使用那个日期格式化器来设置我们的日期文本。
推送详细视图
现在我们已经完成了详细视图,当用户选择他们的食物项目时,我们可以开始创建并将其推送到栈上。切换到MyFoodViewController.m并向下滚动到didSelectRowAtIndexPath。添加以下代码:
// Create an instance of the current food item
NSDictionary *currentFoodItem = [self.myFoodsArray objectAtIndex:indexPath.row];
FoodDetailViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"Food_Detail"];
vc.foodItem = currentFoodItem;
[self.navigationController pushViewController:vc animated:YES];
此代码使用indexPath.row获取当前选定的食物项目。接下来,我们分配我们刚刚创建的FoodDetailViewController的一个实例并将其foodItem属性设置为当前选定的食物项目。最后,我们将viewController推送到导航栈上。现在,运行你的应用程序,并测试所有功能。
摘要
在本章中,我们通过添加最后一块功能,在表格视图中显示用户的保存数据并创建详细视图,完成了我们的基础应用程序。我们还学习了如何使用 Apple 提供的UIImage+ImageEffects类别创建模糊图像。
现在我们已经完成了应用程序,我们可以学习如何在 iOS 7 中使用 TextKit 操作文本。然后我们将将这些新功能应用到我们的应用程序中,使其更加生动!
第七章. 使用 TextKit 操作文本
我们将从这个章节开始,概述新的 UIKit 层次结构。从那里,我们将直接深入到动态文本类型以支持全系统字体和大小设置。接下来,我们将介绍一些新功能,例如使用排除路径将文本围绕形状包裹以及使用几行代码添加压痕效果。最后,我们将讨论如何将标准格式应用于文本,例如下划线文本。让我们开始吧!
什么是 TextKit?
在 iOS 6 之前,您可以使用 UIWebView 和 HTML 标记或使用较低级别的框架 Core Text 来提供文本的混合样式。随着 iOS 6 的推出,苹果引入了属性字符串,允许开发者调整任何字符串定义子部分的颜色和字体属性。前 10 个字符可以设置为黄色,其余字符为粗体字体。
在 iOS 6 中,基于文本的 UIKit 控件基于 Core Graphics 和 WebKit。以下是一个示意图来展示其层次结构:

你会注意到 UITextView 实际上使用 WebKit 本身来绘制带有 HTML 的属性文本。尽管属性字符串提供了许多处理文本的解决方案,但它们在高级布局的灵活性方面有限。这种多行渲染文本需要使用 Core Text。这个框架非常难以处理和理解。
在 iOS 7 中,苹果引入了 TextKit 以简化文本处理。苹果现在从 TextKit 继承 UITextView 而不是从 WebKit,如下面的图所示:

TextKit 继承了所有在 Core Text 中找到的强大功能(它是建立在 Core Text 之上的),并以易于使用且改进了许多的 API 提供。现在所有基于文本的 UIKit 控件(除了出于明显原因的 UIWebView)现在都在使用 TextKit。你可以看到核心结构现在变得更加精细,具有更好的流线型。
TextKit 可以分为三个主要类:
-
NSTextStorage:这个类用于存储所有文本属性信息。将其视为所有文本效果的内部蓝图。需要注意的是,NSTextStorage是NSMutableAttributedString类的子类,因此它负责所有文本属性。除了存储文本属性外,NSTextStorage还将确保在所有编辑操作中保持一致性。 -
NSLayoutManager:这个类将管理在NSTextStorage中找到的数据在视图中的布局方式(正如其名所示)。如果存储的文本属性有任何更改或修改,NSTextStorage将通知这个类。然后它会相应地更新视图。因此,更改几乎可以即时反映出来。 -
NSTextContainer:这个类负责指定文本将要显示的视图。NSTextContainer还跟踪与视图相关的信息,例如大小/框架或形状。最值得注意的是,NSTextContainer能够存储一个 bezier 路径数组,我们将在创建排除路径时使用它。这就是 TextKit 能够围绕图像和其他对象流动文本的原因。
TextKit 可用于多种基于文本的效果。这包括对用户选择的文本大小使用动态字体,使用排除路径围绕图像包裹文本,以及类似于富文本编辑器的文本格式。
在本章中,我们将详细介绍所有这些功能,并将其中一些应用到我们的应用程序文本中。首先,让我们看看动态字体。
动态字体
从用户体验的角度来看,iOS 7 最大的新特性之一是能够调整整个操作系统的文本格式。这包括增加字体粗细(粗体)和文本大小。这些设置可以在设备的设置应用程序中设置。尽管支持动态字体不是强制性的,但建议这样做!以下是一个这些设置的示例:

在通常处理字体时,我们指定要设置的字体家族名称和大小,如下所示:
[UIFont fontWithName:@"HelveticaNeue" size:19.0f]
当处理动态字体时,我们将使用具有样式的字体,而不是使用任何字体的字面名称,这与前面的代码不同。UIFont已经配备了一个名为preferredFontForTextStyle的新方法。这个方法从用户的设备加载所选的字体偏好,并将文本设置为给定的样式。以下是一个多字体样式的示例:

左侧的文本是可以渲染的最小尺寸,中间的文本是可能的最大尺寸,而右侧的文本是每个选项的粗体格式。让我们看看使用 TextKit 进行动态字体的一个示例。以下是一个代码示例:
self.foodDateLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
如前所述的代码中提到的,我们不是使用显式的字体名称,而是使用其中的一种六种包含的样式。通过这样做,我们避免了在我们的应用程序中使用硬编码的字体名称。因此,我们的应用程序将很好地响应用户定义的字体选择。
处理更新
前面的代码将根据用户设置自动渲染。当您切换到设置并调整文本大小时,会出现问题。如果您在没有先关闭应用程序的情况下切换回应用程序,文本更新将不会反映出来。这是因为为了响应实际的变化,您的控制器必须响应使用NSNotificationCenter做出的更改。
通过将以下代码添加到任何viewDidLoad方法的末尾,您可以使您的控制器响应用文更新:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(preferredContentSizeChanged:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
定义的选择器将看起来像这样:
- (void)preferredContentSizeChanged:(NSNotification *)notification {
self.textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
}
首先,我们注册我们的类以接收基于首选内容大小变化的更新通知。因此,如果用户切换到设置应用并更改文本大小,我们的应用将拦截此操作并调用定义的方法preferredContentSizeChanged。之前,这个方法只是设置字体;然而,现在它将拉取新的用户定义的文本大小。
改变文本大小也会影响你的视图布局。因此,你希望你的视图能够根据文本做出响应。大部分的这些都可以通过自动布局(Auto Layout)来完成。虽然自动布局在大多数情况下都能很好地工作,但它不擅长处理行高的确定。
排除路径
排除路径允许你围绕特定的视图包裹文本。大多数文本编辑器都提供了对这一功能的支持,而通过 TextKit,你现在可以在你的应用程序中实现这一功能。使用 TextKit,你可以围绕复杂和简单的路径包裹你的文本。例如,你可能想要围绕一个简单的圆形或围绕一个更复杂的形状,如蝴蝶图片。你可能想在显示带有图像的文本或提供与文本相关细节的视图时使用此功能。
假设你有一个包含与一段文本相关的数据的圆形UIView。我们希望将圆形的UIImageView居中,并在其四周包裹文本。为了测试这个功能,让我们在我们的故事板中添加一个文本视图,并在我们的食物图片周围包裹一些填充文本。切换到Main.Storyboard并选择FoodDetailViewController类。拖动一个文本视图并调整其大小,使其比食物图片大。此外,确保文本视图位于图片视图下方。你的故事板应该看起来像这样:

确保为我们的新文本视图FoodDetailViewController创建一个输出端口。给它命名为textview。
切换到FoodDetailViewController.m并滚动到viewDidLoad。在底部添加以下代码行:
UIBezierPath *circleExclusion = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(60, 40, 210, 210)];
self.textView.textContainer.exclusionPaths = @[circleExclusion];
在这里,我们创建一个新的贝塞尔路径,并给它一个与我们的图片视图矩形相等的坐标。现在我们已经定义了排除形状,是时候告诉文本视图注意这个排除路径了。TextKit 为所有基于文本的视图的文本容器增加了一个额外的属性,称为exclusionPaths。这个参数接受一个数组,这意味着可以同时处理多个排除。以下是结果:

添加压印效果
任何文本都可以通过适当的阴影和突出显示效果看起来像是压印的。TextKit 通过一个名为NSTextEffectLetterpressStyle的新属性参数提供了一个简单有效的方法来实现这一点。
下面是一个带有代码的示例:
NSDictionary *attributes = @{ NSForegroundColorAttributeName : [UIColor blueColor],
NSTextEffectAttributeName : NSTextEffectLetterpressStyle};
NSAttributedString* attrString = [[NSAttributedString alloc]
initWithString:someString
attributes:attributes];
使用属性文本,我们可以应用这种特定的文本效果,以及其他属性。这就是应用这种微妙效果的全部!以我们的应用为例看看:

文本格式化
使用 TextKit,我们可以将一些简单的文本编辑属性应用到我们的文本上。这包括粗体、斜体和下划线文本。为了做到这一点,我们将使用 iOS 7 中一个全新的类,UIFontDescriptor。这个类用于描述字体及其所有属性。更重要的是,您可以直接修改属性并创建一个新的字体。所有字体属性都由字典或键字符串常量表示。
制作粗体和斜体文本
让我们看看一段代码,看看我们如何使用UIFontDescriptor来制作粗体文本:
NSDictionary *currentAttributesDict = [self.textView.textStorage attributesAtIndex:0
effectiveRange:nil];
UIFont *currentFont = [currentAttributesDict objectForKey:NSFontAttributeName];
UIFontDescriptor *fontDescriptor = [currentFont fontDescriptor];
UIFontDescriptor *changedFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
UIFont *updatedFont = [UIFont fontWithDescriptor:changedFontDescriptor size:0.0];
NSDictionary *dict = @{NSFontAttributeName: updatedFont};
[self.textView.textStorage setAttributes:dict range:NSMakeRange(0, self.textView.text.length)];
首先,我们从文本视图的文本存储对象中获取当前的属性。接下来,我们创建一个对这个文本块原始字体使用的引用。我们想这样做,以防我们需要这些信息(这主要取决于应用程序为什么使用UIFontDescriptor)。我们还创建了一个当前字体描述符的引用。一旦我们有了所有这些信息,我们就创建一个新的字体描述符,并将其符号特性设置为粗体。最后,我们创建了一个使用我们新字体描述符的新字体实例,并将其分配给我们的文本视图。要更改文本为斜体,只需传递正确的符号特性。
符号特性实际上只是描述字体风格的属性。它是一个无符号 32 位整数。以下是苹果提供所有特性的列表:
typedef enum : uint32_t {
/* Typeface info (lower 16 bits of UIFontDescriptorSymbolicTraits) */
UIFontDescriptorTraitItalic = 1u << 0,
UIFontDescriptorTraitBold = 1u << 1,
UIFontDescriptorTraitExpanded = 1u << 5,
UIFontDescriptorTraitCondensed = 1u << 6,
UIFontDescriptorTraitMonoSpace = 1u << 10,
UIFontDescriptorTraitVertical = 1u << 11,
UIFontDescriptorTraitUIOptimized = 1u << 12,
UIFontDescriptorTraitTightLeading = 1u << 15,
UIFontDescriptorTraitLooseLeading = 1u << 16,
/* Font appearance info (upper 16 bits of UIFontDescriptorSymbolicTraits */
UIFontDescriptorClassMask = 0xF0000000,
UIFontDescriptorClassUnknown = 0u << 28,
UIFontDescriptorClassOldStyleSerifs = 1u << 28,
UIFontDescriptorClassTransitionalSerifs = 2u << 28,
UIFontDescriptorClassModernSerifs = 3u << 28,
UIFontDescriptorClassClarendonSerifs = 4u << 28,
UIFontDescriptorClassSlabSerifs = 5u << 28,
UIFontDescriptorClassFreeformSerifs = 7u << 28,
UIFontDescriptorClassSansSerif = 8u << 28,
UIFontDescriptorClassOrnamentals = 9u << 28,
UIFontDescriptorClassScripts = 10u << 28,
UIFontDescriptorClassSymbolic = 12u << 28
} UIFontDescriptorSymbolicTraits;
下划线文本
使用 TextKit 下划线文本的方法类似于前面代码中显示的任何一种方法,但有一些修改。以下是一个代码示例:
NSDictionary *currentAttributesDict = [self.textView.textStorage attributesAtIndex:0
effectiveRange:nil];
NSDictionary *dict;
if ([currentAttributesDict objectForKey:NSUnderlineStyleAttributeName] == nil || [[currentAttributesDict objectForKey:NSUnderlineStyleAttributeName] intValue] == 0) {
dict = @{NSUnderlineStyleAttributeName: [NSNumber numberWithInt:1]};
}
else{
dict = @{NSUnderlineStyleAttributeName: [NSNumber numberWithInt:0]};
}
[_textView.textStorage setAttributes:dict range:NSMakeRange(0, self.textView.text.length)];
在这里,我们必须检查当前的文本属性中是否已经存在NSUnderlineStyleAttributeName属性。从这里,我们只需简单地打开或关闭下划线属性并将其应用到我们的文本上。
摘要
TextKit 为 iOS 中的文本操作提供了许多优秀的方法。支持这些功能对于提供更好的用户体验至关重要。我建议您花时间浏览苹果的文档。我们已经在本章中涵盖了标准用法。TextKit 是一个非常强大的新 API,将继续提供创新的使用方式。
在最后一章中,我们将介绍 UIKit Dynamics。我们将学习如何通过向 UI 元素添加物理特性来创造令人兴奋的体验!
第八章:使用 UIKit Dynamics 添加物理效果
本章将介绍 UIKit Dynamics 如何管理应用程序的行为的基础知识。我们将涵盖特定的行为,如重力、弹跳和其他物理属性。此外,我们还将学习如何创建物理边界,以便我们的视图可以与之碰撞。如果没有这些边界,我们的视图将永远继续移动而不会停止。我们将介绍我们的视图将如何相互交互,包括碰撞检测/通知和将视图相互连接。最后,我们将讨论动态效果以及创建类似于 iOS 7 主屏幕的视差效果,当倾斜设备时,该效果会移动。我们有很多内容要介绍,所以让我们开始吧!
UIKit 中的运动和物理
随着 iOS 7 的推出,苹果完全移除了自 iPhone 和 iOS 推出以来一直使用的拟物化设计。取而代之的是一种新颖且清新的扁平化设计,其特点为柔和的渐变和最少的界面元素。苹果强烈鼓励开发者远离拟物化和基于现实世界的界面设计,转而采用这些扁平化设计。
尽管我们被引导远离现实世界的外观,但苹果也强烈鼓励你的用户界面具有现实世界的感觉。有些人可能会认为这是矛盾的;然而,目标是让用户与用户界面建立更深的联系。对触摸、手势和方向变化做出响应的 UI 元素是应用这种新设计范例的例子。为了帮助辅助这种新的设计方法,苹果引入了两个非常巧妙的 API,即 UIKit Dynamics 和动态效果。
UIKit Dynamics
简单来说,iOS 7 在 UIKit 中集成了一个功能齐全的物理引擎。你可以操作特定的属性,为你的界面提供更真实世界的感受。这包括重力、弹簧、弹性、弹跳和力等。每个界面元素都将包含其自身的属性,动态引擎将遵循这些属性。
动态效果
在我们的设备上,iOS 7 最酷的功能之一是主屏幕上的视差效果。将设备向任何方向倾斜都会平移背景图像以强调深度。通过使用动态效果,我们可以监控设备加速度计提供的数据,根据运动和方向调整我们的界面。
通过结合这两个功能,你可以创建出看起来很棒、感觉真实的界面,使其栩栩如生。为了演示 UIKit Dynamics,我们将在FoodDetailViewController.m文件中添加一些代码来创建一些很好的效果和动画。
添加重力
打开FoodDetailViewController.m文件,并将以下实例变量添加到视图控制器中:
UIDynamicAnimator* animator;
UIGravityBehavior* gravity;
滚动到viewDidLoad方法底部,并添加以下代码:
animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
gravity = [[UIGravityBehavior alloc] initWithItems:@[self.foodImageView]];
[animator addBehavior:gravity];
运行应用程序,打开 我的食物 视图,从表格视图中选择一个食物项,观察会发生什么。食物图片应该开始加速向屏幕底部移动,直到最终从屏幕上掉落,如下面的截图所示:

让我们回顾一下代码,特别是刚刚介绍的两个新类,UIDynamicAnimator 和 UIGravityBehavior。
UIDynamicAnimator
这是 UIKit Dynamics 的核心组件。可以说,动态动画器本身就是一个物理引擎,被封装在一个方便且易于使用的类中。动画器本身不会做任何事情,而是跟踪分配给它的行为。每个行为都会在这个物理引擎内部进行交互。
UIGravityBehavior
行为是 UIKit Dynamics 动画的核心组成部分。这些行为都定义了个体对物理环境的响应。这个特定的行为通过施加力来模拟重力效果。每个行为在创建时都与一个视图(或多个视图)相关联。由于你明确地定义了这个属性,你可以控制哪些视图将执行该行为。
行为属性
几乎所有行为都有多个可以调整以达到所需效果的属性。一个很好的例子是重力行为。我们可以调整其角度和大小。在将行为添加到动画器之前添加以下代码:
gravity.magnitude = 0.1f;
运行应用程序并测试它,看看会发生什么。图片视图将开始下落;然而,这次下落的速度要慢得多。将前面的代码行替换为以下行:
gravity.magnitude = 10.0f;
运行应用程序,这次你会注意到图片下落得更快。你可以随意调整这些属性,感受每个值。
创建边界
当处理重力时,UIKit Dynamics 不遵循屏幕的边界。尽管它不可见,食物图片在穿过屏幕边缘后仍然继续下落。除非我们设置包含图片视图的边界,否则它将继续下落。在文件顶部创建另一个实例变量:
UICollisionBehavior *collision;
现在,在我们的 viewDidLoad 方法中,在我们的重力代码下方添加以下代码:
collision = [[UICollisionBehavior alloc] initWithItems:@[self.foodImageView]];
collision.translatesReferenceBoundsIntoBoundary = YES;
[animator addBehavior:collision];
在这里,我们创建了一个新类(这是一个行为)的实例,UICollisionBehavior。就像我们的重力行为一样,我们将这个行为与我们的食物图片视图相关联。
我们不是明确地定义边界的坐标,而是使用碰撞行为上的方便的 translatesReferenceBoundsIntoBoundary 属性。通过将此属性设置为 yes,边界将由我们分配 UIDynamics 动画器时设置的参考视图的边界定义。因为参考视图是 self.view,所以边界现在是视图的可视空间。
运行应用程序并观察图片如何下落,但一旦到达屏幕底部就停止,如下面的截图所示:

碰撞
由于我们的图像视图对重力以及屏幕边界做出响应,我们可以开始检测碰撞。你可能已经注意到,当图像视图下落时,它会直接穿过其下方的两个标签。
这是因为 UIKit Dynamics 只会对被分配了行为的UIView元素做出响应。每个行为可以被分配给多个对象,每个对象也可以有多个行为。由于我们的标签没有与之关联的行为,UIKit Dynamics 物理引擎简单地忽略它。
让我们让食物图像视图与日期标签发生碰撞。为此,我们只需将标签添加到碰撞行为分配调用中。以下是新代码的示例:
collision = [[UICollisionBehavior alloc] initWithItems:@[self.foodImageView, self.foodDateLabel]];
如你所见,我们所做的只是将self.foodDateLabel添加到initWithItems数组属性中。如前所述,任何单一的行为都可以与多个项目关联。运行你的代码,看看会发生什么。当图像下落时,它会击中日期标签,但继续下落,推动日期标签。
由于我们没有将重力行为与标签关联,它不会立即下落。尽管它不会对重力做出反应,但标签仍然会被移动,因为毕竟它是一个物理对象。这种方法并不理想,所以让我们使用 UIKit Dynamics 的另一个酷炫功能,不可见边界。
创建不可见边界
我们将采取一种稍微不同的方法来解决这个问题。我们的标签只是我们想要添加一个将阻止食物图像视图停止的边界的参考点。因此,标签不需要与任何 UIKit Dynamic 行为相关联。从以下代码中移除self.foodDateLabel:
collision = [[UICollisionBehavior alloc] initWithItems:@[self.foodImageView, self.foodDateLabel]];
相反,将以下代码添加到viewDidLoad的底部,但在我们向动画器添加碰撞行为之前:
// Add a boundary to the top edge
CGPoint topEdge = CGPointMake(self.foodDateLabel.frame.origin.x + self.foodDateLabel.frame.size.width, self.foodDateLabel.frame.origin.y);
[collision addBoundaryWithIdentifier:@"barrier" fromPoint:self.foodDateLabel.frame.origin toPoint:topEdge];
在这里,我们向碰撞行为添加一个边界并传递一些参数。首先我们定义一个标识符,稍后我们将使用它,然后我们传递食物日期标签的原点作为fromPoint属性。toPoint属性设置为使用食物日期标签的框架创建的 CGPoint。
运行应用程序,你会看到食物图像现在会在我们定义的不可见边界处停止。标签对用户仍然可见,但动态动画器会忽略它。相反,动画器看到我们定义的障碍物,并相应地做出反应,尽管障碍物对用户来说是不可见的。
这里是前后对比:

动态元素
当使用 UIKit Dynamics 时,了解 UIKit Dynamics 项目非常重要。它们不是作为视图引用动力学,而是作为项目引用,这些项目遵循UIDynamicItem协议。这个协议定义了遵循此协议的任何对象的中心、变换和边界。UIView是最常见的遵循UIDynamicItem协议的类。另一个符合此协议的类的例子是UICollectionViewLayoutAttributes类。
操作项目属性
如前所述,UIDynamics 项目具有可以操作并应用于界面中多个视图/项目的属性。让我们看看调整弹性属性并将其应用于我们的食物图像视图会是什么样子。
滚动到viewDidLoad并在其末尾添加以下代码:
UIDynamicItemBehavior* itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[self.foodImageView]];
itemBehaviour.elasticity = 0.6;
[animator addBehavior:itemBehaviour];
在这里,我们创建了一个UIDynamicItemBehavior实例,并用我们的self.foodImageView初始化它。然后我们设置弹性属性,然后将这种新行为添加到我们的动画器中。运行你的代码,看看食物图像视图现在会多弹跳几次。调整弹性值以查看不同的结果。
弹性是许多可以改变的行为之一。以下是一个使用UIDynamicItemBehavior的所有可用属性的列表:
-
弹性:这个属性将定义碰撞的弹性。最好的记住方式是看物体有多弹。值越高,项目就会弹得越远。
-
摩擦:如果一个物体在另一个表面上滑动,摩擦属性用于确定物体所受到的阻力有多大。
-
密度:这设置了项目的整体模拟质量。与真实物理一样,质量越高,移动项目所需的力就越大。一个防止项目在碰撞时移动的例子是给它一个非常高的密度,相对于与之碰撞的其他项目。
-
阻力:这是施加在任何运动上的阻力,不仅仅是像摩擦那样在另一个表面上滑动。
-
angularResistance:当项目旋转时,这个属性将确定旋转的阻力。
-
allowsRotation:一个可选属性,用于防止项目旋转,无论什么碰撞和力如何影响它。
碰撞通知
到目前为止,我们已经设置了重力并添加了一些边界,包括我们日期标签的无形边界。通过执行某种任务来响应碰撞是非常常见的。例如,在一个游戏中,一旦敌人与子弹碰撞,我们会摧毁敌人并增加分数。
我们可以通过使用碰撞通知来跟踪碰撞。为了做到这一点,我们必须让我们的类采用UICollisionBehaviorDelegate。切换到FoodDetailViewController.h并添加以下协议:
@interface FoodDetailViewController : UIViewController <UICollisionBehaviorDelegate>
现在切换回FoodDetailViewController.m并找到我们创建碰撞行为的代码。添加以下代码行:
collision.collisionDelegate = self;
通过设置碰撞代理,我们现在可以使用以下代理方法:
- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p {
NSLog(@"Boundary contact occurred - %@", identifier);
}
每当发生碰撞时,此代理方法都会被调用,并且我们已经将其设置为输出我们之前定义的碰撞标识符。运行代码,你的控制台输出应该如下所示:

通过将标识符和其他传递给此代理方法的属性组合起来,我们可以检测正在发生的碰撞,并相应地做出反应。例如,让我们在发生碰撞时动画食物图像视图的 alpha 值。将你的代理方法代码替换为以下代码:
- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p {
if ([(NSString *)identifier isEqualToString:@"barrier"]) {
// The barrier was collided with
[UIView animateWithDuration:0.3f animations:^{
self.foodImageView.alpha = 0.0f;
}];
}
}
在这里,我们将标识符转换为NSString,然后检查它是否等于我们想要的碰撞标识符。如果是这样,我们执行一个简单的UIView动画,将图像视图的alpha值设置为零,从而使其不可见。正确使用此代理方法将允许你根据碰撞完成大量任务。
将项目附加到其他项目
除了重力和其他物理属性之外,UIKit Dynamics 还允许你的物理对象以它们在现实物理世界中的方式相互交互。例如,我们可以使用UIAttachmentBehavior方法将项目连接起来,就像它们被一个不可见的支架连接一样。让我们让我们的应用程序创建一个新的方块视图,并将其附加到我们的食物图像视图上,但仅在发生碰撞时。因为我们的食物图像视图会弹跳几次,所以每次都会检测到碰撞。为了避免创建多个方块,让我们创建另一个实例变量来跟踪第一次弹跳。
在FoodDetailViewController.m的实现块中添加以下代码行:
BOOL firstBounce;
现在将我们的代理方法代码替换为以下代码:
- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p {
if (!firstBounce) {
firstBounce = YES;
UIView* square = [[UIView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width / 2 - 50, 400, 100, 100)];
square.backgroundColor = [UIColor greenColor];
[self.view addSubview:square];
[collision addItem:square];
[gravity addItem:square];
UIAttachmentBehavior* attach = [[UIAttachmentBehavior alloc] initWithItem:self.foodImageView attachedToItem:square];
[animator addBehavior:attach];
}
}
在这里,我们检测firstBounce布尔值是否不是YES,然后创建一个新的UIView,将重力和碰撞项添加到其中,使用UIAttachmentBehavior方法,并将此新视图附加到我们的食物图像视图上。运行应用程序,你将看到在第一次弹跳时,创建了一个绿色方块。因为我们将这个新视图附加到食物图像视图上,所以当你第二次和第三次弹跳时,方块视图会随着它移动,就像被附着一样。
快照项
我们将在本书中介绍的最后一个行为是UISnapBehavior类。UIKit Dynamics 提供了一个内置的行为,可以将一个项目从其起始点快速移动到指定的终点,并具有内置的阻尼。让我们让我们的食物图像视图从屏幕顶部快速移动到其最终位置。
滚动到viewDidLoad并移除我们所有的重力和碰撞代码(保留我们的动画器)。将以下代码添加到viewDidLoad中:
UISnapBehavior *snapBehaviour = [[UISnapBehavior alloc] initWithItem:self.foodImageView snapToPoint:CGPointMake(160, 202)];
snapBehaviour.damping = 0.65f;
[animator addBehavior:snapBehaviour];
在这里,我们为食物图像视图分配新的UISnapBehavior和init选项。我们还传递了想要项目弹回的点,在这种情况下,是图像视图的最终位置。我们将阻尼值设置得稍高一些,以产生较温和的弹簧效果(数字越低,项目越有弹性)。
最后要做的就是改变食物图像视图的起始点。切换到Main.storyboard,并将食物图像视图拖动到屏幕顶部,尽可能高(甚至超出屏幕)。需要注意的是,起始点到终点的距离越大,弹跳效果越明显,因此在设置damping属性时要考虑这一点。
运行我们的应用程序并查看结果。食物图像视图应该以漂亮的弹簧效果弹回位置。正如你所看到的,使用 UIKit Dynamics 不仅简单,而且非常强大。
在我们的应用中使用运动
除了 UIKit Dynamics,我们还可以使用UIMotionEffects来调整设备水平倾斜时的用户界面。UIMotionEffects是一个抽象类,在子类化时效果最佳。苹果已经为UIMotionEffects创建了一个子类,几乎可以覆盖你应用中所有运动的用例。这个子类是UIInterpolatingMotionEffect类。
UIInterpolatingMotionEffect实例使用一个键路径和一个类型进行初始化。类型定义了垂直和水平运动。该类将根据设备的运动自动设置键值路径。
在我们的viewDidLoad方法中,在底部添加以下代码:
UIInterpolatingMotionEffect *horizontalMotionEffect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
horizontalMotionEffect.minimumRelativeValue = @(-30);
horizontalMotionEffect.maximumRelativeValue = @(30);
[self.foodImageView addMotionEffect:horizontalMotionEffect];
[self.foodNameLabel addMotionEffect:horizontalMotionEffect];
[self.foodDateLabel addMotionEffect:horizontalMotionEffect];
在这里,我们创建了一个UIInterpolatingMotionEffect实例,并将其分配给水平轴运动跟踪。然后我们设置最小和最大相对值。这决定了项目将向左或向右移动多少以模拟我们想要的视差效果。最后,我们将运动效果添加到我们想要的所有视图中。我们的keyPath值可以分配给多个不同的值以产生不同的效果。在设备上运行应用程序,并选择一个食物项目的详细视图以查看结果!
此外,我们还可以通过组合多个运动效果来进一步扩展,例如垂直和水平运动。用以下代码替换前面的代码:
UIInterpolatingMotionEffect *horizontalMotionEffect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
horizontalMotionEffect.minimumRelativeValue = @(-30);
horizontalMotionEffect.maximumRelativeValue = @(30);
UIInterpolatingMotionEffect *verticalMotionEffect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
verticalMotionEffect.minimumRelativeValue = @(-30);
verticalMotionEffect.maximumRelativeValue = @(30);
UIMotionEffectGroup *group = [UIMotionEffectGroup new];
group.motionEffects = @[horizontalMotionEffect, verticalMotionEffect];
[self.foodImageView addMotionEffect:group];
[self.foodNameLabel addMotionEffect:group];
[self.foodDateLabel addMotionEffect:group];
在这里,我们只是简单地复制了水平运动效果,但我们将keyPath设置为center.y,将type设置为vertical。运行应用程序并查看结果。
这些效果虽然既伟大又简单,但要注意不要过度使用。本章讨论的每个项目都是为了添加微妙的视觉效果,这些效果共同作用,从而提升整体的用户体验。
摘要
我们做到了!从开始到结束,我们使用 iOS 7 和 Xcode 5 的许多新特性构建了一个功能齐全的应用程序。在这一章中,我们给我们的视图添加了一些酷炫的物理属性。将这些行为和动态效果叠加在一起,可以创造出一些非常独特的界面效果。现在,随着这本书的结束,你应该非常熟悉 iOS 7 的开发了。利用所有这些新特性是构建具有更好体验的更好应用程序的第一步!


浙公网安备 33010602011771号