Swift2-秘籍-全-
Swift2 秘籍(全)
原文:
zh.annas-archive.org/md5/27c4bfbb5f3adb70deb7b04053652bbc译者:飞龙
前言
经过一年的使用后,Swift 已经开始成熟并迅速添加新功能。苹果推出了 Swift 2,并带来了一些优秀的新特性和对 Xcode 及底层技术的改进。本书旨在更新希望迁移到 Swift 2 的 Objective-C 开发者,同时也帮助 Swift 开发者通过更好地了解这种编程语言及其第二个版本来获得更坚实的基础。
如果您喜欢创建小型应用程序,这本书非常适合您。它将向您展示如何从头开始创建 Swift 应用程序。所以,拿起您的 Mac,打开您的 Xcode,让我们来制作 Swift 吧!
本书涵盖内容
第一章, 使用 Xcode 和 Swift 入门,向您介绍了一些 Xcode 的特定于 Swift 的功能。这可能对第一章来说有点高级,但这并不困难,也非常重要,尤其是对于那些希望专业开发的人来说。
第二章, 标准库和集合,展示了如何使用 Swift 的方式操作数组、字典、集合、字符串和其他对象。对于一直在使用 Objective-C 的人来说,这一章非常重要。
第三章, 使用结构体和泛型,展示了 Swift 的结构体与 Objective-C(甚至 C)的结构体不同,以及泛型是一个允许您创建不局限于单一类型的函数的功能。这两个功能都有它们自己的技巧。
第四章, 使用 Swift 实现设计模式,解释了如何使用 Swift 实现设计模式,尤其是如果您喜欢面向对象编程的话。
第五章, 在您的应用程序中使用多任务,展示了如何在您的应用程序中使用不同类型的多任务,这是一个几乎在当今每个应用程序中都存在的功能。
第六章, 使用 Playgrounds,教您如何使用 Playgrounds,这是一个优秀的 Xcode 功能,允许您在将代码添加到项目之前对其进行测试。
第七章, 使用 Xcode 进行 Swift 调试,解释了如何使用 Xcode、LLDB 和 Instruments 调试 Swift 代码。在这里,您将学习一些在您的应用程序中查找和解决 bug 的技巧。
第八章,与 Objective-C 集成,展示了 Swift 和 Objective-C 如何共存,并为你提供了如何将 Objective-C 应用程序迁移到 Swift 的逐步指南。
第九章,处理其他语言,展示了如何使用 C、C++ 和汇编语言与 Swift 一起使用,因为你知道 Swift 并非 iOS 和 OS X 开发的唯一选择。
第十章,数据访问,展示了不同的数据存储方式,这些数据可以是本地的或远程的。
第十一章,扩展、照片和更多,阐述了在 Swift 开发世界中非常重要的几个主题,从新的框架如 WatchKit 到广泛使用的框架。我们还将涵盖一些高级主题,如方法交换和关联对象。
您需要这本书什么
使用 Swift 2 进行开发需要 Xcode 7 或更高版本,而 Xcode 本身需要安装在 Yosemite(OS X 10.10)上。这只能在 Mac 计算机上安装。所以,这基本上是你需要的。
一些食谱只能在物理设备(iPhone、iPad 或 iPod)上测试;因此,只有当您注册了 Apple 开发者计划时才能安装。
这本书面向谁
如果你是一位经验丰富的 Objective-C 程序员,正在寻找 Swift 中快速解决许多不同编码任务的解决方案,那么这本书就是为你准备的。你预计将拥有开发经验,尽管不一定使用 Swift。
习惯用法
在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词如下所示:"创建一个名为 Chapter 9 Assembly 的新 Swift 单视图应用程序,并添加一个名为 AssemblyCode.c 的新文件。"
代码块如下设置:
struct Contact {
char name[60];
char phone[20];
struct date {
int day;
int month;
int year;
} birthday;
};
struct ContactList {
struct Contact contact;
struct ContactList * next;
};
当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将被设置为粗体:
override func viewDidLoad() {
super.viewDidLoad()
initializeContactList(&list)
}
任何命令行输入或输出都如下所示:
6 / 3 = 2
6 / 2 = 3
6 / 1 = 6
Execution interrupted. Enter Swift code to recover and continue.
Enter LLDB commands to investigate (type :help for assistance.)
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"当 Xcode 请求桥接文件时,点击是。"
注意
警告或重要注意事项如下所示。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要发送给我们一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 图书的骄傲拥有者,我们有多种方式可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
点击代码下载 & 勘误。
-
在搜索框中输入书籍的名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买这本书的地方。
-
点击代码下载。
文件下载完成后,请确保您使用最新版本的软件解压缩或提取文件夹:
-
适用于 Windows 的 WinRAR / 7-Zip
-
适用于 Mac 的 Zipeg / iZip / UnRarX
-
适用于 Linux 的 7-Zip / PeaZip
勘误
尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍的名称。所需信息将出现在勘误部分下。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接将疑似盗版材料发送至 <copyright@packtpub.com> 与我们联系。
我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。
问题和建议
如果您对本书的任何方面有问题,请通过 <questions@packtpub.com> 与我们联系,我们将尽力解决问题。
第一章. 使用 Xcode 和 Swift 入门
在本章中,我们将涵盖以下食谱:
-
从 App Store 安装 Xcode
-
下载 Xcode 图像
-
开始一个 Swift 项目
-
使用 Swift 项目选项
-
创建条件代码
-
将现有项目迁移到 Swift 2.0
-
添加开发者账户
-
从命令行编译
-
将 Swift 作为解释器使用
-
向现有项目添加版本控制系统
简介
在本章中,我们将学习使用 Swift 创建项目的基础知识。即使你已经创建了你的项目,阅读本章的食谱也是值得的。你将学习如何与 Xcode 交互,如何从命令行测试你的代码,最后,我们将回顾这门语言的基础知识。
在下载 Xcode 之前,请注意 Swift 需要 Xcode 6.0 来支持 Swift 1.0,以及 1.2 或 Xcode 7.0 来支持 Swift 2.0 和 2.1。我们将安装 Xcode 7.0(截至本文写作时的最新版本)。要安装 Xcode 7.0 版本,你必须至少有 OS X Yosemite(OS X 10.10),所以在安装之前满足这些要求。
从 App Store 安装 Xcode
安装 Xcode 的第一种方式是从 App Store 下载。这种方法的优势是,你会在下载开始之前收到更新警告,并且会检查系统要求。
准备工作
要从 App Store 下载任何程序,你必须有一个 Apple ID;它是免费的,设置起来也不会花很长时间。
如何操作...
-
要从 App Store 下载 Xcode,只需从你的 dock 或应用程序文件夹中打开 App Store。
![如何操作…]()
-
第一次打开 App Store,它会要求你提供 Apple ID 详细信息(电子邮件和密码)。打开此应用程序后,只需在应用程序右上角的文本框中搜索
xcode。![如何操作…]()
-
确保你从正确的供应商(Apple)安装 Xcode;有时我们会得到一些结果,让我们认为它们是我们想要的,但事实并非如此。
-
一旦你找到了 Xcode 应用程序,只需点击 安装 按钮,下一步就是去喝杯咖啡,或者你可以叫一个朋友,因为 Xcode 有 2.2 个吉字节,这意味着下载需要一段时间,所以现在休息一下。
-
要检查 Xcode 是否已完成安装,你只需打开 应用程序 文件夹或启动盘应用程序,然后查看 Xcode 图标下是否有进度条。
它是如何工作的...
就像你将从 App Store 安装的其他任何应用程序一样,你只需要打开 App Store 应用程序,搜索它,然后安装它。
还有更多...
如果你买了一台新电脑,你会看到 Xcode 会被推荐安装到你的新机器上。这是因为苹果会跟踪你已经安装的应用程序。
下载 Xcode 图像
安装 Xcode 的第二种方式是下载来自 Apple 开发者中心的镜像;这一步骤不是免费的,仅限于 Apple 开发者计划的成员(每年大约 99 美元)或在该计划成员公司工作的人。
准备工作
对于这个菜谱,你需要有 2.2 GB 的空闲空间,除了已安装的 Xcode 所占用的空间,但我假设你不会遇到这个问题。
下载 DMG 文件的优势在于你可以将其保存到 DVD 上作为备份(你永远不知道 Xcode 这个版本何时会被从 App Store 中移除)。此外,如果你在一个团队中工作,确保每个成员使用相同的 Xcode 版本非常重要。另外,如果你想安装任何 Xcode 的测试版,它只能通过 Apple 开发者中心获取。
如何操作……
要下载 Xcode 镜像,请按照以下步骤操作:
-
第一步是打开你的网页浏览器;访问
developer.apple.com/xcode,然后点击网页右上角的下载按钮。你将被带到一个新的页面,有两个下载选项:最新的测试版构建或指向 Mac App Store 的链接。如果你选择下载测试版构建,需要登录。登录后,下载将立即开始。否则,你可以在 Mac App Store 中免费下载最新的公共构建。小贴士
你可以在一台机器上安装多个版本的 Xcode;具体来说,是公共发布版和苹果提供的任何测试版。
-
下载 DMG 文件后,双击它,将 Xcode 图标拖到你的 应用程序 文件夹中。记住,你需要管理员权限才能将文件复制到 应用程序 文件夹中。
![如何操作……]()
-
你也可以将 Xcode 安装到不同的路径,比如你的主目录,但如果不是必要的,我不建议这样做。
小贴士
不要在搜索引擎中搜索“下载 Swift”,因为还有一种也叫 Swift 的编程语言,它与苹果设备无关。
它是如何工作的……
DMG 文件确保你可以始终拥有这个 Xcode 版本的备份,所以如果你未来版本的 Xcode 出现任何问题,你可以安装之前的版本。也可能在你的电脑上安装多个 Xcode 版本。
还有更多……
Apple 开发者中心是一个获取 Xcode 资源的好网站。在这里,你可以找到视频、指南、代码示例和附加组件。
开始 Swift 项目
通常,开始 Swift 是一件非常直接的事情;然而,了解每一步发生的事情是很好的。
准备工作
在您开始项目之前,请确保您知道项目名称以及它将保存在哪个文件夹中。更改此类参数可能会在项目创建后造成问题。一旦您安装了 Xcode,您可以从应用程序文件夹、启动台或甚至从您的坞站打开它,如果您已经将 Xcode 添加到坞站的话。由于我非常懒惰,我更喜欢后者;它对我来说更快,因为它在我的坞站上。
如何操作…
当您第一次打开 Xcode 时,它可能会要求您安装一些额外的包,所以请这样做。这些包中的一些对于您正在开发的应用程序类型很重要,而其中一些对于访问某些设备(主要是最新设备)是必要的。
-
现在,Xcode 正在询问您想要启动或打开的项目。请选择显示为创建一个新的 Xcode 项目的选项。![如何操作…]
如果由于任何原因,此窗口没有显示给您,您始终可以选择转到文件(在菜单栏上)| 新建 | 项目。
-
下一步是选择您想要开发的项目类型。在这个例子中,我将使用 iOS 的单视图应用程序,但如果有任何不同之处,我会对 OS X 应用程序或其他类型的项目进行注释。![如何操作…]
下一个对话框将要求您提供一些项目信息,其中一个例子是您想要使用的编程语言。在这个例子中,我们将使用 Swift。
![如何操作…]()
-
选择 Swift 作为语言,它将使用其代理创建带有 Swift 代码的应用程序。请确保使用 Core Data选项未被勾选,以防止其代码出现在应用代理中。
您还会注意到,Swift iOS 应用程序现在没有名为
main.m、main.mm或main.swift的文件。OS X 应用程序有一个main.swift文件,但它比之前的main.m文件小。正如您应该已经知道的,产品名称是您的应用程序名称,组织名称是此软件的所有者,组织标识符是反向的互联网域名,例如,
uk.co.packtpub而不是packtpub.co.uk。注意
注意现在没有创建单元测试的复选框,因为默认情况下,它使用 XCTest 为您创建。如果您不想使用它,只需从您的项目中删除该组即可。我不会删除它,它通常不会造成伤害。
-
现在,是时候选择一个文件夹来存储我们的项目了。记住,在开发过程中,您可以添加文件,这些文件将存储在不同的位置。我不推荐这种做法,但如果您必须这样做,尽量让您的项目靠近这些文件。
-
我还建议您检查使用 Git 仓库的选项,除非您有子版本仓库,当然。即使您是唯一的开发者,拥有版本控制系统也很重要。请记住,我们是人类,有时我们会犯错误,因此需要回滚。
![如何操作…]()
-
一旦你创建了项目,按下播放按钮看看它是否在运行。如果你是第一次安装 Xcode,它将显示一个对话框要求你启用开发者模式。如果你有管理员密码,请点击 启用 按钮
![如何操作…]()
-
好的!现在你的项目已经启动并运行。
它是如何工作的…
创建一个项目并不是什么困难的事情;你只需要注意一些步骤。确保你已经选择了 Swift 作为主要编程语言;否则,你会看到很多与 Objective-C 相关的内容。
注意你将创建项目的文件夹。Xcode 会为你创建一个以项目名称命名的另一个文件夹,并在其中,Xcode 会创建项目包,一个包含源代码的文件夹。如果你想复制你的项目,确保你复制包含所有内容的文件夹。
还有更多…
如果你想要在一个已经启动项目的团队中工作,你可能会使用 检出现有项目 选项来克隆项目。你将使用 Git 或子版本库,并将你的代码与其他团队成员同步。Xcode 为我们提供了与 VCS(版本 控制系统)一起工作的基本工具;这些工具足以完成我们 80%的任务。
使用 Swift 项目选项
Xcode 项目有很多选项。在这里,我们将了解其中的一些,主要是 Swift 特定的选项。
准备工作
要执行这个菜谱,只需创建一个新的项目,如前一个菜谱所示。
如何操作…
-
一旦你创建了一个项目,点击导航器项目图标或如果你更喜欢键盘快捷键,按 command + 1,然后点击你的项目图标(第一个图标)。现在,点击 构建设置。
![如何操作…]()
-
查找 嵌入式内容包含 Swift 代码;在这种情况下,我们将选择 否,但当然,如果你知道有任何使用 Swift 创建的额外内容,你应该选择 是。
![如何操作…]()
-
前往 通用 选项卡并向下滚动;你可以看到你可以添加嵌入式二进制文件的地方。
![如何操作…]()
-
现在,查找 优化级别。这里是你告诉编译器它应该花费多少时间来尝试使你的代码更快或压缩它的地方。通常,当我们开发(调试模式)时,我们设置为无优化(-O0);然而,当我们准备创建最终产品(发布模式)时,我们通常会设置一个优化级别,例如 Os,这意味着最快和最小。
注意
有时候,在使用 Objective-C 时,当你曾经设置过高级别的优化时,调试器会丢失一些变量值。我还没有在 Swift 中看到这种现象,但最好心中有数。
-
另一个重要的选项是导入路径。这告诉 Swift 在哪里查找 Swift 模块。如果你正在将你的项目与外部库链接,你可能需要指定
module.map文件的位置。如果你有多个路径要搜索,你需要逐行设置它们。如果你为调试和发布有不同的路径,你仍然可以使用变量,例如$(CONFIGURATION)或$(TARGET)。小贴士
你可以使用绝对路径或相对路径,但我更倾向于相对路径。
它是如何工作的…
更改设置主要是当你的项目开始增长时必须做的事情。有一些选项你为调试和发布配置设置了不同的值。
还有更多…
Xcode 有很多配置设置;展示所有这些设置超出了本书的范围。我建议你至少查看其中的一些,尤其是如果你想处理大型项目。在这里我的主要建议是:不要在没有与你的团队其他成员(主要是项目经理)同步的情况下更改你的设置。如果你与 VCS 发生冲突,修复它可能会很困难。
创建条件代码
通常在我们开发时,我们有一些情况,我们希望根据我们的需求有不同的代码片段。例如,让我们想象一下,我们想要比较我们编写的某些函数与在第三方库中创建的等效函数的性能。在这种情况下,我们可以创建一些宏,用于仅使用我们的函数或仅使用第三方函数,这样我们就可以让同一个应用程序以两种不同的方式工作。
在这个菜谱中,我们将向你展示如何根据平台创建日志,我们还可以根据执行是否受到日志过多的影响来启用或禁用它。
准备工作
创建一个名为Chapter 1 Conditional Code的新项目,如之前所示,让我们编写一些代码。
如何做到这一点…
-
在创建一个新的项目后,让我们通过导航到文件 | 新建 | 文件...来创建一个新的文件。现在,选择Swift 文件并将其命名为
CustomLog.swift。小贴士
不要将你的文件保存在与项目不同的文件夹中;这将来可能会给你带来问题。
-
现在,添加以下代码:
func printLog(message: NSString){ #if VERBOSE_LOG #if os(OSX) let OS="OS X" #else let OS="iOS" #endif #if arch(arm) || arch(arm64) let devicetype = "Mobile device" #elseif arch(x86_64) || arch(i386) let devicetype = "Computer" #else let devicetype = "Unkown" #endif NSLog("%@ on a %@ - %@", OS, devicetype, message) #endif } -
现在,前往你的视图控制器的
viewDidLoad方法,并添加对这个函数的调用,如下所示:printLog("Hello World") -
现在尝试点击播放;你看到了什么?答案是——什么都没有!原因是编译器对宏
VERBOSE_LOG一无所知,这意味着这个宏被解释为false,唯一创建的就是一个空函数。 -
现在,回到你的项目构建设置,搜索其他 Swift 标志,并添加
-DVERBOSE_LOG,如下面的截图所示:![如何做到这一点…]()
-
再次点击播放,你会看到日志消息。
它是如何工作的…
目前,Swift 编译器定义了两个宏:os() 和 arch()。第一个可以接收 OS X 或 iOS 作为参数,第二个可以接收 x86_64、arm、arm64 和 i386。这两个宏都将返回一个布尔值。您也可以创建自己的宏,在构建设置中定义它。
被评估为 true 的块将被编译,其他块则不会编译;这样,您可以拥有调用 OS 特定功能的代码。
注意
我想强调,主要是针对那些习惯于使用 C 项目开发的开发者,苹果的文档明确指出 Swift 没有预处理器;它只在编译时使用一个技巧,因此您不能像在 C 或 Objective-C 中那样使用宏。您唯一能做的就是查看它们是否已设置。
更多内容…
如果需要,您可以使用此处所示的运算符 &&、|| 和 !:#if arch(arm64) && os(iOS),但不能使用任何比较运算符,如 ==、< 等。
如果您想了解更多可以添加到其他 Swift 标志的选项,请查看本章中的 从命令行编译 菜谱。
将现有项目迁移到 Swift 2.0
如果您有一个用 Swift 2.0 或更低版本编写的现有项目,苹果已经为 Xcode 7 提供了一些方便的工具,帮助您轻松地将项目迁移到 Swift 2.0。
准备工作
对于这个菜谱,您需要一个用 Swift 1.2 或更低版本编写的现有 Xcode 项目。
如何操作…
按以下步骤迁移现有项目:
-
定位您的现有 Xcode 项目,并打开
.xcodeproj或.xcworkspace文件以自动打开 Xcode 7。一旦项目或工作区加载,Xcode 将显示以下消息,提示您转换为最新的 Swift 语法:![如何操作…]()
-
点击转换,您将被带到一个新的屏幕。此屏幕显示有关转换过程的一些信息。选择下一步以继续。
-
您现在将被要求选择要更新到 Swift 2.0 的目标。如果您需要保留任何目标处于当前状态,这将非常有用。请审查您的目标列表,取消选中您不希望更新的目标,然后选择下一步。
![如何操作…]()
-
您将看到一个新屏幕,类似于辅助编辑器,比较迁移后的结果与您的代码当前状态。在左侧的远端面板中,您将看到一个将接受更改的所有文件的列表,并且可以手动选择不更改的文件。
![如何操作…]()
小贴士
如果有任何文件您希望不迁移,只需从左侧文件列表中取消选中它们。
-
一旦您已验证或修改了所有更改,请选择保存。Xcode 将更新所有更改,您将准备好继续使用 Swift 2.0 进行开发。
工作原理…
苹果已经使 Xcode 在代码迁移方面非常灵活。幸运的是,这大多数都简化了迁移到 Swift 2.0 的过程,并允许你直接编写更多代码。然而,迁移过程并不完美,对于复杂的项目可能需要手动更改。
还有更多…
如果由于任何原因迁移提示没有自动显示,你仍然可以自己开始这个过程。从菜单栏导航到编辑 | 转换 | 转换为最新 Swift 语法。你将被直接带到这个菜谱的第 2 步。
添加开发者账户
通常,苹果公司通过改进 Xcode 和创建工具来让开发者的生活变得更简单,但在谈到证书时,有一个例外。如果你想在一个物理设备(iPhone、iPad 或 iPod)上测试你的应用,你需要一个证书。如果你想将其上传到 App Store,你也需要这个证书。
证书的想法是为了保护你的代码免受恶意代码或签名后的修改,但这种想法是有代价的。要获取证书,你需要注册苹果开发者计划。
准备工作
我们假设如果你继续这个菜谱,你已经注册了这个计划。让我们回收之前的工程;打开它,然后开始。
如何做到这一点...
按照以下步骤添加苹果开发者账户:
-
一旦你打开了项目,点击项目导航器,然后点击显示我们项目的组合框,如果尚未选择,请选择目标
第一章。 -
现在,看看名为团队的选项。在编写 Mac 应用程序的情况下,这个组合框只有在选择将签名选项设置为 Mac App Store 或开发者 ID 时才会启用。
-
通常,团队选项开始时是无被选中的。点击这个组合框并选择添加账户。
![如何做到这一点…]()
-
在选择添加账户后,Xcode 将要求你提供苹果开发者计划的登录数据(电子邮件和密码)。如果你没有,你有选择加入该计划。
![如何做到这一点…]()
-
一旦添加了这些,你应该使用你的账户并运行你的应用。如果你将设备连接到 Mac,你可以转到菜单栏上的窗口选项,然后选择设备选项。
注意
你的设备应该会出现在对话框中。Xcode 可能需要一段时间来读取设备的符号。在首次连接此设备的情况下,你会看到你需要请求更改此设备的状态为开发者模式。
-
当你得到绿灯时,这意味着你的设备已准备好用于开发;现在,回到你的项目,从模拟器切换到你的设备。
![如何做到这一点…]()
-
如果设备已启用但未在 Xcode 中列出,这可能意味着你必须降低iOS 部署目标,这可以在项目设置下的信息选项卡中找到。
提示
将 iOS 部署目标 降低到最小值是程序员中非常普遍的想法,以覆盖最大类型的设备。这样做将防止你的开发使用新功能。首先检查你需要的功能,然后更改你的 iOS 部署目标。
它是如何工作的...
签署代码是为了安全考虑;主要限制是你必须与苹果开发者计划保持最新。苹果允许每个账户最多 100 个设备。
还有更多...
有时候,证书会给我们带来一些头疼。如果它要求你撤销证书,你可能需要在苹果开发者中心创建一个新的,如果你在一个团队中工作,你可能需要等待管理员的批准。
有几次你需要更改构建设置中的代码签名选项;这通常发生在你从另一个组织 ID 获取代码时。
在设备上测试你的代码是非常有用的;这是你可以测试真实用户体验的地方。无论何时你有某些低级代码,例如汇编代码或用 C 语言编写的使用类型大小或字节序的代码,在设备上测试你的项目都是好的。记住,苹果的设备基于 ARM 和 ARM64 的 CPU,这与在 Mac 电脑上使用的 Intel CPU 不同。
从命令行编译
我知道现在很多用户,甚至开发者,认为使用命令行是过去的事情。现实是,即使今天,很多可以通过命令行完成的任务,主要是自动化任务,如持续集成,必须使用命令行来完成。
这个菜谱将向你展示这并不困难,而且更好,你将更深入地理解 Xcode 幕后操作的概念。
小贴士
如果你从未使用过命令行,我建议你读一本关于它的书;在我看来,Linux Shell Scripting Cookbook,Packt Publishing 是一本很好的书,即使知道一些命令是 Linux 特定的。
准备工作
使用键组合 command + Shift + U 打开一个 Finder 窗口,或者打开你的 Launchpad 并点击 其他 文件夹。在这里,你可以看到一个名为 终端 的图标,打开它,你应该会看到一个类似于以下窗口的窗口:

如何操作...
-
输入
xcode-select -p;这应该会给出一个路径,例如,/Applications/Xcode.app/Contents/Developer。如果你在机器上没有安装更多的 Xcode 版本,你不需要担心路径;它可能就是正确的。如果由于任何原因你在机器上安装了多个 Xcode,你需要通过输入xcode-select -s /Applications/XCODE VERSION.app/Contents/Developer来更改它。小贴士
记住,切换 Xcode 是一项只能由管理员完成的任务,并且它将影响每个用户。
-
现在,转到你的项目目录并输入以下命令:
xcodebuild -target "Chapter 1" -configuration Debug
之后,你会在屏幕上看到很多命令,但最重要的信息是最后一条,应该是 ** BUILD SUCCEEDED **;这意味着项目构建没有错误。
它是如何工作的...
当你输入一个命令时,你的系统会使用 PATH 变量指定的路径来查找这个命令。你可以通过输入 echo $PATH 来检查包含在你的 PATH 变量中的目录。默认情况下,目录 /usr/bin 被包含在内。
此目录包含 Xcode 命令,例如 xcodebuild。当你想要使用来自其他 Xcode 版本的命令时,你需要使用 xcode-select 来覆盖这些文件以使用你想要的版本。
一旦设置好,你就可以编译你的项目。由于你的项目是一组很多文件,如源代码、图像等,如果我们必须逐个执行每个动作(编译、复制文件、代码签名等),这将是一项艰巨的工作。这就是为什么请求 Xcode 使用 xcodebuild 命令自己执行它更容易的原因。
xcodebuild 命令有很多参数,因此你可以指定配置为 Debug 或 Release,你想要编译的目标,以及许多其他选项。输入 xcodebuild -help 以获取选项列表。
小贴士
-help 参数在 Xcode 命令中非常常见。当你有任何疑问时,尝试使用它。
还有更多...
关于 xcodebuild 命令的另一个优点是它显示了使用所有参数的命令。因此,当你编译 Objective-C 项目时,Xcode 使用 clang 编译器,但当你有一个 Swift 项目时,Xcode 使用 swiftc 命令。输入 swiftc -help 并提供其完整路径以检查其选项,并在其他 Swift 标志的构建选项中使用它们。
请记住,xcodebuild 将会寻找一个名为 project.pbxproj 的文件,该文件位于你的 .xcodeproj 目录内。此文件包含每个文件、设置和创建项目所需的步骤;如果出现语法错误或错误引用,xcodebuild 和 Xcode IDE 将不会编译项目。除了这个修复之外,这个文件可能很费时。由于这些原因,我不会手动更改此文件,并且我也会尝试避免与版本控制系统发生冲突。
使用 Swift 作为解释器
就像一些其他脚本语言一样,你可以在命令行上使用 Swift 和它的解释器。有时这非常有用,主要是在你想要测试代码但又不想创建一个新的游乐场时。
准备工作
打开一个终端窗口,如前一个菜谱所示。
如何做到这一点...
按照以下步骤使用 Swift 作为命令行解释器:
-
第一步是找到 Swift 命令的位置;即使你已经使用了
xcode-select命令,Swift 命令也可能无法通过你的PATH变量访问。因此,你可以使用find /Applications/Xcode.app -name swift -a -type f来定位你的 Swift 命令。在我的情况下,我得到了/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift。注意
然而,当前版本的 Xcode 将 Swift 命令放在
/usr/bin。如果需要,你可以使用命令export PATH="$PATH:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/"将这个目录添加到你的PATH变量中。此时,我们只需输入swift就可以进入 Swift 解释器。 -
如果你想要从命令行使用 Swift,有时永久设置这个
PATH变量是个好主意。为了做到这一点,我们需要将之前的命令添加到我们的.profile文件中,例如echo 'export PATH="$PATH:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/" ' >> $HOME/.profile && chmod +x $HOME/.profile。从现在开始,如果你重新启动计算机,就无需再次查找 Swift 路径并设置
PATH环境变量。 -
现在,让我们进入我们的 Swift 命令行,并输入以下代码:
var dividend = [3,2,1,0] var divisor = 6 -
你会在输入每个变量后看到显示这些变量内容的消息。现在,输入以下循环代码:
for i in dividend { println("\(divisor) / \(i) = \(divisor / i)") }你可以看到,我们将得到以下结果:
6 / 3 = 2 6 / 2 = 3 6 / 1 = 6 Execution interrupted. Enter Swift code to recover and continue. Enter LLDB commands to investigate (type :help for assistance.) -
如你所见,最后一个选项失败了,因为我们不能除以 0。这是我们使用命令行测试一些代码的快速方法。大多数时候,我们会使用 playground 来测试,但有时使用命令行会更快。
它是如何工作的...
调用 Swift 命令给你提供了测试你的代码或甚至将 Swift 用作脚本语言的可能性。这里要强调的是,你需要知道你的 Swift 命令在哪里;命令行可以帮助你找到它。
更多...
Swift 的大多数选项和swiftc选项都是通用的;这意味着如果你想在编译前测试某些内容,你可以这样做。
将版本控制系统添加到现有项目
在没有任何版本控制的情况下开始一个项目是非常常见的,随着时间的推移,我们改变了自己的想法,并决定添加一个。
不幸的是,Xcode 没有给我们这个选项,我们必须手动完成。我希望 Xcode 很快能添加这个选项。
准备工作
为了完成这个菜谱,让我们创建一个名为Chapter1 Git的空项目;然而,这次,在我们保存项目之前,取消选择在...创建 Git CGRepository选项。

如何操作...
按照以下步骤在现有项目上创建本地仓库:
-
从菜单栏中选择源代码控制菜单选项。
-
选择创建工作副本来创建本地仓库。
-
现在,你可以打开你的项目,并注意到 Xcode 已经识别了版本控制系统。如果你想确保这一点,修改一个文件,使用 command + S 保存它,并检查在你的项目导航器中文件的右侧是否有字母 M。
![如何操作…]()
-
一旦你对你的更改满意,你可以通过右键单击
AppDelegate.swift,然后转到 源代码管理 选项并选择 提交 "AppDelegate.swift" … 选项来交付它们。![如何操作…]()
-
然后,将出现一个要求描述的对话框;将你的修改作为注释写下来,然后点击 提交 1 个文件。
它是如何工作的…
不幸的是,如果你忘记将 Git 仓库添加到你的项目中,Xcode 不会提供任何机制来将其添加到你的项目中,因此你必须手动添加。打开命令行允许你从命令行使用 Git,Xcode 会检测到这一功能已被添加。某些版本的 Xcode 只有在你打开项目时才能检测到版本控制已被添加,所以如果你已经完成了所有步骤但 Xcode 没有检测到,请尝试关闭并重新打开 Xcode。
小贴士
Xcode 为你提供了一些与 Git 和 SVN 一起工作的功能,但它们非常有限。如果你需要更多来自你的版本控制系统的命令,你可以从命令行使用它们,或者使用外部工具。
还有更多…
即使你不会作为团队的一部分工作,我也建议你使用版本控制系统。当使用 Swift 或其他语言进行开发时,你有时需要回滚或比较当前代码与之前的版本,主要是在你遇到新错误时。
参见
- 如果你想了解更多关于这个主题的信息,请查看书籍 Git 版本控制食谱,Packt 出版
第二章. 标准库和集合
在本章中,我们将介绍以下食谱:
-
手动创建 HTML
-
打印你的对象描述
-
对用户进行测验
-
寻找完美数
-
对产品数组进行排序
-
寻找出路
-
创建你自己的集合
-
组织餐厅
简介
了解集合和标准库的使用很重要,尤其是对于来自 Objective-C 的人,因为这里有一些差异。
在本章中,我们将创建一些应用程序来使用这些功能。在这些食谱之后,你应该对 Swift 编程语言有一个很好的理解。
如前一章所述,我们的大部分食谱将在 iOS 上创建,但如果你愿意,也可以在 Mac OS X 上开发它们。
手动创建 HTML
HTML 最初是一种简单的网页和链接显示格式。如今,这种格式已经变得非常普遍,并且被广泛使用。甚至有像 PhoneGap 这样的框架,可以创建使用此文件类型的应用程序。
在这个食谱中,我们将仅使用字符串创建 HTML;主要思想是了解字符串操作。在这种情况下,我们将创建名片 HTML 代码。
准备工作
打开你的 Xcode,创建一个名为Chapter2 HTML的单视图项目。
如何操作…
让我们按照以下步骤手动创建 HTML:
-
点击故事板并添加以下布局:
![如何操作…]()
-
然后,将文本框与视图控制器上的以下属性连接:
@IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var addressTextField: UITextField! @IBOutlet weak var postCodeTextField: UITextField! @IBOutlet weak var phoneTextField: UITextField! -
将这些属性与视图上的相应文本框相关联,在创建动作按钮之前,我们将使用以下代码创建一个
Card类:class Card { private let TEMPLATE = "<div class=\"personalcard\">" + "<p class=\"name\">#name#</p>" + "<p class=\"address\">#address#</p>" + "<p class=\"postcode\">#postcode#</p>" + "<p class=\"phone\">#phone#</p>" + "</div" var name:String? var address:String? var postCode:String? var phone:String? init(){} }如你所见,我们正在创建一个与我们的应用程序具有相同信息的类。主要区别是我们有一个名为
TEMPLATE的常量,它具有我们的 HTML 模型。注意
注意,这个常量是私有的,因为这是我们不应该看到的东西(例如,从视图控制器中)。此外,请注意,在需要继续到下一行的每一行末尾都有一个加号。原因是,我们不能像在 Objective-C 中使用操作符那样不使用操作符来连接字符串。
Swift 在指令末尾不需要分号。然而,如果你有一行非常长的代码,你必须告诉编译器这一行将要继续,例如,使用加号,或者在下一条线上使用点操作符来表示它从上一行继续。
如果你曾经使用 Objective-C 或 C++进行编程,你可能想知道为什么我们创建了一个空的初始化器。原因是:它不是必需的;理论上,因为我们只有常量和可选值。然而,某些 Swift 版本,出于任何原因,无法检测到它,并强制你创建一个空的初始化器。
属性的初始值是什么?答案是nil。看看是否所有这些属性都是可选的;如果不是,我们将不得不将它们的值设置为某些内容。
小贴士
避免使用空值,例如空字符串来初始化属性;最好使用可选属性。
-
到目前为止,我们有了基本结构。现在我们需要创建按钮事件,所以将视图控制器上的触摸事件与以下动作链接:
@IBAction func showHTML(sender: AnyObject) { var card = Card() card.name = nameTextField.text card.address = addressTextField.text card.postCode = postCodeTextField.text card.phone = phoneTextField.text let alert = UIAlertController(title: "HTML", message: card.toHtml(), preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } -
现在我们将看看这个动作是否非常清晰。我们将开始创建一个
card对象,然后从我们的视图中获取所需的信息,最后显示它。小贴士
避免面条代码;不要创建一个巨大的动作,并尝试将其分解为类和方法。看看维基百科上的模型-视图-控制器模式。
-
除了一个细节外,一切都很正常:
Card类没有名为toHtml的方法。没问题,让我们实现它。回到我们的Card类,并添加以下方法:func toHtml() -> String{ var html = TEMPLATE.stringByReplacingOccurrencesOfString("#name#", withString: self.name!) .stringByReplacingOccurrencesOfString("#address#", withString: self.address!) .stringByReplacingOccurrencesOfString("#postcode#", withString: self.postCode!) .stringByReplacingOccurrencesOfString("#phone#", withString: self.phone!) return html; } -
现在是时候测试它了;只需在 Xcode IDE 中按播放,填写字段并按按钮,你应该会看到一个像这里显示的消息:
![如何做…]()
它是如何工作的…
字符串有很多方法;其中一些用于修改当前字符串,而另一些仅返回值,还有一些基于原始字符串创建新的字符串。在我们的例子中,我们使用了stringByReplacingOccurrencesOfString,通过替换我们的标记(#something#)来生成一个新的字符串,对应于相应的属性。
我们可以将每个替换的结果链式调用,以进行下一个替换,防止每行重新赋值。
小贴士
现在,iPhone 和 iPad 拥有 1GB 或更多的 RAM,这对于大多数传统的 HTML 模板来说应该足够了。然而,如果你有一个非常大的模板,有很多替换,你可能需要寻找更优化的方法。在大型字符串变量上替换字符串会分配大量内存,你可能会得到较差的性能。
还有更多...
如果你查看这个方法文档,你会看到有两个额外的参数,一个叫做options,另一个叫做range。它们用于指定一个特殊的比较器来搜索我们的字符串标记,另一个则只使用我们字符串的一部分。由于我们不需要使用它们,它们是可选的,所以我们只是省略了它们;没有必要用 nil 值填充,就像我们以前在 Objective-C 中做的那样。
小贴士
如果你的项目中有需要创建 HTML、XML 或 JSON 代码的对象,你可以创建一个具有特定于相应类型转换的方法的基础对象,以实现标准化。
打印你的对象描述
这个菜谱的目的是看看使用变量值创建字符串的 Swift 方法。在 Objective-C 中,我们有一个名为stringWithFormat的类方法,但在 Swift 中,这个方法不像在 Objective-C 中那样频繁使用,因为现在我们有插值。在这个例子中,我们将创建一个应用程序,它将向用户展示三种可能的产品。当用户选择其中之一时,应用程序必须显示该产品的信息,如果有的话,还要显示其价格。
如何做...
-
创建一个名为
Chapter2 Product Value的新单视图项目。现在,让我们点击故事板并添加三个按钮,如图所示:![如何操作…]()
-
完成此操作后,你可以添加一个名为
Product的新 swift 文件。现在我们将创建一个包含产品名称、其价格和制造商名称的类。在这种情况下,唯一可以省略的信息是产品价格;其他属性是必需的。这意味着我们需要一个初始化器,至少包含产品名称和制造商名称。
-
由于我们希望使用插值功能来使用我们的产品,我们需要实现
CustomStringConvertable协议,这迫使我们实现一个名为description的属性。一旦我们有了这些信息,我们就可以使用以下代码实现我们的类:class Product: CustomStringConvertible{ var price:Double? var name: String var manufacturer: String init(name: String, manufacturer: String){ self.name = name self.manufacturer = manufacturer } var description:String { return "\(self.name) (\(self.manufacturer))" } } -
现在,我们可以转到视图控制器并创建三个产品作为属性;按照我们的示例,我们将创建
television、gabion和locker。所以,让我们添加以下属性:private let television = Product(name: "Television", manufacturer: "Telefunken") private let gabion = Product(name: "Gabion", manufacturer: "Maccaferri") private let locker = Product(name: "Locker", manufacturer: "Danalockers") -
下一步是创建一个辅助函数,该函数将把双精度浮点数转换为具有两位精度的字符串。我将在视图控制器文件中创建此函数,但不在其类内部。如果在项目中你打算在多个文件中使用此函数,我建议你使用以下代码创建另一个文件:
func doubleFormatter(value: Double) -> String{ return String(format: "%.2f", value)小贴士
将一个或多个文件专门用于辅助函数和类扩展是一个好主意。这将使你的代码维护更容易。
-
我们需要做的最后一件事是创建按钮动作。正如你所知,我们有三个按钮,它们都将执行相同的功能;唯一的区别是显示的产品。因此,我们将创建一个函数,并根据发送者区分产品。
小贴士
避免重复代码,即使它们相邻或很小。这是因为当项目开始接收更改时,它将生成新的错误。
-
我想提到的另一个细节是,在这个例子中,我将在添加增值税(20%)后显示产品价格。这是为了让你了解 Swift 字符串插值的强大功能:
@IBAction func showDescription(sender: UIButton) { var message:String var product: Product if sender.titleLabel?.text == "Television"{ product = television }else if sender.titleLabel?.text == "Gabion" { product = gabion }else if sender.titleLabel?.text == "Locker"{ product = locker }else{ return } message = "You've chosen \(product)" if let price = product.price { message += " which costs \(doubleFormatter(price * 1.20)))" } let alert = UIAlertController(title: "Product information", message: message, preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil)注意
在这个示例中,我们使用了按钮的标题来知道选择的产品,但这是一种不好的做法。这样做只是为了创建一个专注于字符串插值的小示例。想象一下,你需要将你的程序翻译成其他语言,或者如果你需要向这个标签添加少量信息,这将使你修复大量的代码。
-
现在我们的应用已经完成;尝试按下播放按钮,然后点击应用按钮。你应该看到一个类似于这样的警告视图:
![如何操作…]()
工作原理...
Swift 最伟大的功能之一是字符串插值;它允许你拥有我们在计算包含增值税的价格时所做的表达式。这也允许你调用像doubleFormatter这样的函数,使我们的数字以两位数显示。这也允许我们打印一个对象。
如果我们愿意,甚至可以调用对象的方法或属性,例如。我们可以将制造商名称显示为大写,将我们的描述更改为\(self.name) \(self.manufacturer.uppercaseString))。
记住,如果你想打印自己的对象,它必须遵循CustomStringConvertable协议并实现名为description的属性。一些语言,例如 Java,在基类中有一个等效的方法(Java 中的toString),你只需要重写它。然而,Swift 如果没有指定基类,就没有基类,这就是为什么你必须在指定了CustomStringConvertable协议的使用后才能覆盖description属性。
要使用字符串插值,只需创建一个字符串,当你需要外部值时,只需将其用\((反斜杠和左括号)和)(右括号)包裹起来,例如,\(variable)。
小贴士
如果表达式变得过于复杂,以至于难以使用,只需在字符串内部创建一个新的变量并设置其值。这在软件维护方面也是一种良好的实践。Objective-C 没有这个功能;你必须使用名为stringWithFormat的类方法创建一个新的字符串。我们使用了 Swift 的等效方法,现在是一个初始化器。
我还想指出,我们在if语句中创建了一个常量。在这种情况下,我们不是验证价格是否为真或是否大于零,我们只是检查它没有 nil 值。例如,对于价格为 0 的产品,它也会评估为真。
最后但同样重要的是,请注意,我们的message变量不是可选的,但它并没有在声明的地方初始化。原因是编译器会在读取和设置它之前检查它,并且没有未初始化的可能性。
这个变量的初始值取决于产品值,它不能为 nil;这就是我们被迫添加else语句和从我们的函数中退出的原因。否则,编译器会找到一个可能的方法将产品设置为 nil,并且由于我们的插值,它将失败。
还有更多...
我们为doubleFormatter函数提供的另一个良好解决方案是创建一个扩展。为此,只需将我们的格式化代码更改为以下代码:
extension Double {
func precision(numDigits: Int) -> String {
return NSString(format: "%.\(numDigits)f", self)
}
}
这意味着我们现在为双精度类型添加了一个新方法,我们可以随时调用它。当然,我们还得改变使用我们的双精度值的插值。现在,我们可以使用类扩展,用以下一行替换相应的行:
message += " which costs \((price * 1.20).precision(2)))"Adding different characters
对用户进行测验
有时候,我们需要在我们的应用程序中添加一些图标,但根据您想要的图标,不一定需要添加图片;您可以使用 Unicode 字符。让我们创建一个仅使用字符串的测验应用。
这个应用的想法是创建一个应用,用户需要在 12 秒内回答一个问题。在测验结束后,应用将显示用户的得分,例如正确和错误问题的数量。
准备中
一旦我们了解了这个程序的概念,让我们创建一个新的项目,并将其命名为 第二章 Unicode。
如何操作...
要创建一个测验应用,请按照以下步骤操作:
-
首先,让我们创建一个名为
Quiz的新文件,并向其中添加一个带有问题、三个可能答案和正确答案的类,如下所示:class Quiz { var question:String // First possible answer var ①:String // second possible answer var ②:String // third possible answer var ③:String // Right answer var 👌:Int init(question:String, ①:String, ②:String, ③:String, 👌:Int){ self.question = question self.① = ① self.② = ② self.③ = ③ self.👌 = 👌 } }小贴士
要将 Unicode 字符添加到您的代码中,您可以从网站复制或转到 编辑 | 特殊字符。
-
现在我们已经拥有了这个愉快的类,我们需要创建另一个类来存储我们的测验并管理用户的答案:
class QuizManager { private var quizzes:[Quiz] = [] private var currentQuestion = 0 // Total right answers private var 👍 = 0 // Total wrong answers private var 👎 = 0 func addQuiz(quiz:Quiz) { self.quizzes.append(quiz) } func getCurrentQuestion() -> Quiz? { if currentQuestion < quizzes.count { return self.quizzes[currentQuestion] } return nil } // Answer to the current question. // Returns true if it was the right answer func answer(questionNumber:Int) -> Bool{ var rightAnswer:Bool if getCurrentQuestion()!.👌 == questionNumber { rightAnswer = true 👍++ }else { rightAnswer = false 👎++ } return rightAnswer } func get👍() -> Int { return 👍 } func get👎() -> Int { return 👎 } } -
下一步是添加一个用于问题的标签、三个用于可能答案的按钮和一个用于显示计时器的标签。如果您不想显示导航栏,别忘了将其隐藏。您的屏幕应该看起来类似于这个:
![如何操作...]()
-
现在是时候在我们的视图控制器上创建相应的属性并将它们链接起来。除了这些组件,我们还需要一个计时器和另一个变量来知道已经过去了多少时间。将这些属性命名为如下所示:
@IBOutlet var questionLabel: UILabel! @IBOutlet var timerLabel: UILabel! @IBOutlet var answer①Button: UIButton! @IBOutlet var answer②Button: UIButton! @IBOutlet var answer③Button: UIButton! var quizTimer: NSTimer? var elapsedTime:Int var quizManager:QuizManager -
好的,现在是我们初始化这些组件的时候了。这就是我们为什么要创建一个初始化器的原因。在 Objective-C 中,我们曾经设置
viewDidLoad方法,因为这个方法是在控制器的视图被加载到内存后调用的,但现在在 Swift 中,每个对象都必须初始化每个非可选属性。这就是我们要重写init方法的原因。不要担心每行代码的含义;很快就会解释:required init(coder: NSCoder) { self.elapsedTime = 0 quizManager = QuizManager() super.init(coder: coder) setupQuizManager() } private func setupQuizManager(){ quizManager.addQuiz(Quiz(question: "What's the capital of Australia?", ①: "Sidney", ②: "Melbourne", ③: "Canberra", 👌: 3)) quizManager.addQuiz(Quiz(question: "What is the smallest planet in the solar system?", ①: "The moon", ②: "Mercury", ③: "The sun", 👌: 2)) quizManager.addQuiz(Quiz(question: "In which year was Harley Davison founded?", ①: "1903", ②: "2013", ③: "80BC", 👌: 1)) }您可能首先会问:为什么我们使用
required而不是override?这是因为初始化器在基类(UIViewController)中被定义为required;在这种情况下,我们必须重新实现这个方法。 -
下一步是设置
elapsedTime。我们现在不会使用它,但由于它不是可选的,我们必须在这里设置它。请注意,elapsedTime和quizManager都在超类之前初始化。在初始化超类之后,我们可以将问题添加到测验管理器中;这就是为什么我们有setupQuizManager的调用。 -
好的,现在我们可以将第一个问题显示在屏幕上了。为此,我们需要著名的
viewDidLoad函数。我们无法在初始化器中做这件事,因为标签和按钮还没有被实例化。您还可以看到prepareNextQuestion方法,该方法验证是否有更多的问题。如果有,则显示下一个问题;如果没有,则显示您的得分:override func viewDidLoad() { super.viewDidLoad() prepareNextQuestion() } private func prepareNextQuestion(){ if quizTimer != nil { quizTimer!.invalidate() } guard let quiz = quizManager.getCurrentQuestion() else { let message = "Total \u{1F44D} \(quizManager.get👍())\nTotal \u{1F44E} \(quizManager.get👎())" let alert = UIAlertController(title: "Product information", message: message, preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) return } elapsedTime = 0 questionLabel.text = quiz.question answer1Button.setTitle(quiz.1, forState:.Normal) answer2Button.setTitle(quiz.2, forState:.Normal) answer3Button.setTitle(quiz.3, forState:.Normal) }你会注意到我们正在使用 Swift 2.0 中引入的新
guard关键字。使用guard将允许你在特定条件未满足时定义一个退出策略。这确实在解包可选值时非常有用;guard条件之后的代码将包含未包裹的值(无缩进和作用域问题)。 -
还有一些东西是缺失的,那就是计时器回调。记住,我们希望向用户显示他还有多少时间来回答问题,然后我们必须跳到下一个问题。正如你在前面的方法中看到的那样,我们调用了一个名为
tick的方法;这意味着每过一秒,我们必须增加elapsedTime的值,当计时器结束时,我们将认为问题被错误地回答了:func tick(){ guard elapsedTime < 12 else { quizManager.answer(0) prepareNextQuestion() return } let baseCharCode = 0x1F550 timerLabel.text = String(Character(UnicodeScalar(baseCharCode + elapsedTime))) elapsedTime++ } -
如果你现在按播放,你可以看到应用程序正在运行,除了一个小细节。用户不能回答!注意,没有按钮动作,所以我们需要添加它,并且记住一旦用户按下按钮,它将跳到下一个问题:
@IBAction func answer(sender: UIButton) { var userAnswer:Int switch(sender){ case answer①Button: userAnswer = 1 case answer②Button: userAnswer=2 case answer③Button: userAnswer=3 default: userAnswer = 0 } quizManager.answer(userAnswer) prepareNextQuestion() }
太棒了,现在我们的应用程序正在运行!
它是如何工作的…
在这个菜谱中,我们学习了一些使用 Unicode 表情字符的方法。现在你可以创建你自己的 WhatsApp-like 程序。我们看到了我们可以使用带有 Unicode 字符的变量名;如果你想,你可以用日语给你的变量命名!你也可以使用String(Character(UnicodeScalar(UNICODE_VALUE)))创建带有 Unicode 值的字符串,或者你也可以用它进行插值,如Total \u{1F44D}。
小贴士
使用 Unicode 字符时要小心;你可能会让其他团队成员的生活变得复杂。记住,并不是每个人都讲日语或中文,有时符号可能与其他符号相似。
我们还了解到,可以以十六进制形式编写整数;只需要添加前缀0x。
还有更多…
如果你想看到更多的 Unicode 符号,有一些页面可以帮助我们。我推荐unicode-table.com和www.alanwood.net/demos/wingdings.html。
搜索完美数
信不信由你,计算机最初是作为大型计算器出现的,直到现在,它们的主要功能是进行强大的计算。让我们在我们的 Swift 项目中添加一点数学知识,创建一个应用程序来寻找第一个完美数并将其显示给用户。
你现在可能有的主要问题是:什么是完美数?完美数是一个正整数,它等于其因数的和。例如,6 是一个完美数,因为如果你将其因数相加(1 + 2 + 3),其结果是 6。
在这个菜谱中,我们将学习如何使用范围运算符。
准备工作
让我们从创建一个名为Chapter2 Perfect Number的新 Swift 项目开始。
如何操作…
按照以下步骤搜索完美数:
-
点击故事板并创建一个类似于这里显示的布局:
![如何操作…]()
-
现在,让我们将文本字段与以下属性链接:
@IBOutlet var startText: UITextField! @IBOutlet var endText: UITextField! -
好的,在我们创建按钮动作之前,我们将创建一个名为
isPerfect的函数,该函数将检查作为参数传递的数字是否为完美数。之后,我们将创建按钮动作:func isPerfect(number:Int) -> Bool { var sum = 0 (1..<number).forEach { (i) -> () in if number % i == 0 { sum += i } return sum == number } @IBAction func search(sender: UIButton) { var rangeStart:Int = startText.text.toInt()! var rangeEnd:Int = endText.text.toInt()! for i in rangeStart ... rangeEnd { if isPerfect(i){ let alert = UIAlertController(title: "Found", message: "\(i) is a perfect number"), preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) return } } let message = "No perfect number found between \(rangeStart) and \(rangeEnd)" let alert = UIAlertController(title: "HTML", message: message, preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) }
它是如何工作的...
如您所见,我们使用了两次for循环。第一次我们使用了闭区间运算符(…),它用于包含最后一个数字(rangeEnd),因为我们想检查用户输入的最后一个数字。
第二次我们使用了半开区间运算符(..<)来排除最后一个数字,因为我们不想将最后一个数字包含在总和内。
注意
Swift 的第一个版本中,半开区间运算符仅由两个点(..)组成。经过一些测试版本后,它被重命名为..<。因此,您可以在互联网上找到一些不再工作的代码。
您还可以在switch语句中使用这些运算符;有时它们非常方便,尤其是在您不想编写典型的 C 语言for循环时。
排序产品数组
在这个菜谱中,我们将学习如何在 Swift 中管理数组。在这里,我们将创建一个产品数组(非常典型),向其中添加产品,删除不可用的产品,并按价格排序数组。
准备工作
创建一个名为Chapter 2 SortingProduct的新 Swift 单视图项目。
如何做到这一点...
让我们按照以下步骤创建和排序产品数组:
-
在我们开始视图部分之前,让我们创建应用程序的模型部分。在我们的例子中,我们将创建
Product类。因此,创建一个名为Product.swift的新文件,并输入以下代码:class Product: CustomStringConvertable { var name:String var price:Double var available:Bool init(name:String, price:Double, available:Bool){ self.name = name self.price = price self.available = available } var description: String { return "\(self.name): \(self.price)£ available: \(self.available)" } } -
如您所见,这个类的想法是创建具有其名称、价格和可用性的对象。我们还从
CustomStringConvertable继承,以利用其description属性。 -
现在,您可以在故事板中点击并添加两个标签,一个用于完整目录,这意味着它将按原始顺序显示每个产品,无论其价格如何或是否可用。
-
另一个标签将显示相同的产品,但根据可用性进行过滤并按价格排序。因此,现在将您的标签与以下属性链接,并创建一个产品数组属性:
var products:[Product] = [] // this is our catalog @IBOutlet var catalogLabel: UILabel! @IBOutlet var availableLabel: UILabel! -
如果您想了解它们的含义,可以创建额外的标签。我创建了一个标题为
Catalog的标签,另一个标题为Available Products的标签。在catalogLabel和availableLabel上设置新的行数也很重要;否则,它将只显示第一个产品。 -
下一步是创建初始化;在这里,我们只需要向我们的数组中添加一些产品。由于这是一个示例,产品将是硬编码的,但在实际应用程序中,我们应该从数据库或互联网上检索它们:
required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) products.append(Product(name: "Shirt", price: 19, available: true)) products.append(Product(name: "Socks", price: 1.99, available: true)) products.append(Product(name: "Trousers", price: 22.50, available: false)) products.append(Product(name: "T-Shirt", price: 10, available: true)) products.append(Product(name: "Shoes", price: 32.20, available: false)) products.append(Product(name: "Women shoes", price: 54, available: true)) products.append(Product(name: "Men underwear", price: 9.99, available: true)) products.append(Product(name: "Bra", price: 12.5, available: true)) products.append(Product(name: "Panty", price: 4.45, available: true)) products.append(Product(name: "Tennis shoes", price: 27, available: false)) } -
在此之后,当视图加载完成后,我们可以显示我们的目录,这样我们就可以开始使用我们的
catalog标签:override func viewDidLoad() { super.viewDidLoad() let descriptions = products.map{$0.description} let labelText = descriptions.joinWithSeperator("") catalogLabel.text = labelText } -
好的,现在是我们添加按钮并创建其动作的时候了:
@IBAction func showAvailableProducts(sender: UIButton) { var availableProducts = products.filter { (product:Product) -> Bool in return product.available } availableProducts.sort(<) let descriptions = availableProducts.map{$).description} let labelText = descriptions.joinWithSeperator(" ") availableLabel.text = labelText }
应用程序已完成;一旦你点击按钮,你将看到类似于这里所示的结果:

它是如何工作的…
Swift 中的数组与 Objective-C 中的可变数组非常相似;它们可以添加对象、删除对象等等,但也有一些区别。在 Swift 编程语言中,你必须指定数组包含的对象类型,就像我们在括号中写类型时做的那样。
小贴士
你可以创建任何类型的对象数组,就像我们以前在 Objective-C 中做的那样,通过声明一个变量为[AnyObject]。然而,如果这不是必要的,应该避免这样做。还有使用NSArray而不是Array的可能性;在这种情况下,我们将拥有与 Objective-C 相同的函数。
你也可以在viewDidLoad方法中看到一些新的内容;我们调用了一个名为map的闭包,这是因为我们想要创建一个包含我们目录中每个产品的大字符串,每个产品由一个换行符(\n)分隔。为了做到这一点,我们必须将我们的Product数组转换为String数组。map函数帮助我们做到这一点,因为我们可以将一个函数作为参数传递,该函数将每个元素转换为所需的新类型。
另一个新函数是filter函数;这个函数接收另一个函数作为参数,该函数返回一个布尔值。如果返回的值是 true,这意味着当前元素是有效的,不应该被过滤;如果返回的值是 false,当然新的数组将不包含这个元素。
最后但同样重要的是,我们有sort函数。这个函数不会创建一个新的数组;它修改当前的数组。考虑这种情况,我们有一个我们自己的类数组,这是编译器不知道如何排序的。在这种情况下,我们必须告诉这个函数何时两个对象是有序的,何时不是。
小贴士
使用修改数组的函数时要小心;一旦完成,就无法回滚。
还有更多...
声明数组有两种方式,其中一种是我们使用括号的方式,另一种声明数组的方式是使用Array<Product>。它们没有区别;它们创建相同类型的对象。在两种情况下,你都不能从这个类型继承。
小贴士
Swift 中数组的声明最初是Type[],然后被替换为[Type]。
有更多数组函数可以帮助我们操作数组,例如reduce、reverse或removeRange。如果你与 NoSQL 数据库合作过,你可能已经习惯了这类函数。你还可以创建一个数组扩展,以你自己的方式操作数组。
寻找出路的方法
在这个菜谱中,你将了解元组。这种新类型在 Objective-C 中不存在,在 Swift 中非常有用,尤其是在需要返回多个值时。例如,有很多函数需要返回一个值和一个错误代码。在其他语言中,我们通常返回值,而错误值作为参数返回。
你还将学习如何使用二维数组,在 Objective-C 中这要复杂得多。在这种情况下,我们将使用枚举类型的数组。
因此,这次我们将创建一个应用程序,它将找到迷宫的出口。当然,我们不会浪费时间设计迷宫;我们将结果展示在文本视图中。
准备工作
创建一个新的 Swift 单视图项目,命名为 Chapter 2 Maze。
如何操作...
-
首先,让我们创建这个迷宫的模型部分。对我们来说,迷宫是一个有四种不同可能性的数组:我们不能穿过的墙壁,我们可以穿过的通道,我们的目标出口,以及已使用,这意味着我们已经使用过这条路径,所以不应该再次使用它;这将防止我们绕圈子走。所以,我们首先要做的是创建一个名为
Maze的文件,并添加以下代码:enum BlockType{ case AISLE, WALL, USED, WAYOUT } -
在我们开始实现
maze类之前,我们知道我们需要一个类型来存储迷宫的一个坐标,另一个类型来知道路径是否找到,如果找到了,我们应该有找到的路径:typealias Position = (x: Int, y:Int) typealias Way = (found: Bool, way: [Position]) -
好的,现在是我们创建我们的类的时候了;在这种情况下,我们需要一个二维数组来表示迷宫,另一个数组将包含通往出口的路径,以及两个属性来知道迷宫的宽度和高度:
class Maze{ private var maze:[[BlockType]] = [] private lazy var stack:[Position] = [] private var width: Int private var height: Int -
我们将要创建的第一个方法是初始化器;在这种情况下,我们需要创建一个指定其大小的迷宫。当我们创建二维数组时,我们将用
AISLE填充它;为了创建墙壁,我们将使用稍后创建的另一个方法:init(width: Int, height: Int){ self.width = width self.height = height for _ in 1...height{ var row:[BlockType] = [] for _ in 1...width{ row.append(.AISLE) } maze.append(row) } } -
现在我们需要创建创建墙壁和出口的方法。在更完整的情况下,我们应该检查位置是否有效,但在这个例子中,我们不会纠结于每一个细节。所以,这里有一些方法:
func addWall(position: Position) { maze[position.y][position.x] = .WALL } func setWayout(position: Position){ maze[position.y][position.x] = .WAYOUT } -
下一个操作是创建主方法,该方法将尝试找出一种从迷宫中退出的方法。在这种情况下,程序员不应该给出起点,因此这个方法将没有参数,但我们需要在迷宫中行走。然而,想法是创建一个递归函数,该函数将接收一个新的位置并寻找下一个位置,这就是为什么第二个函数是私有的:
func findWayOut() -> Way{ self.initStack() return self.next((0,0)) } private func next(position: Position) -> Way { stack.append(position) if self.maze[position.y][position.x] == .WAYOUT { return (true, self.stack) } maze[position.y][position.x] = .USED // UP if position.y > 0 && (maze[position.y-1][position.x] == .AISLE || maze[position.y-1][position.x] == .WAYOUT) { let result = next((position.x, position.y-1)) if result.found { maze[position.y][position.x] = .AISLE return result } } // LEFT if position.x > 0 && (maze[position.y][position.x-1] == .AISLE || maze[position.y][position.x-1] == .WAYOUT) { let result = next((position.x-1, position.y)) if result.found { maze[position.y][position.x] = .AISLE return result } } // DOWN if position.y+1 < self.height && (maze[position.y+1][position.x] == .AISLE || maze[position.y+1][position.x] == .WAYOUT) { let result = next((position.x, position.y+1)) if result.found { maze[position.y][position.x] = .AISLE return result } } // RIGHT if position.x+1 < self.width && (maze[position.y][position.x+1] == .AISLE || maze[position.y][position.x+1] == .WAYOUT) { let result = next((position.x+1, position.y)) if result.found { maze[position.y][position.x] = .AISLE return result } } maze[position.y][position.x] = .AISLE stack.removeLast() return (false, []) } private func initStack(){ stack = [] } }注意
你可能已经注意到,Swift 有懒加载。这是一件好事,因为我们不必在检查位置是否在数组边界内时创建嵌套的
if函数。 -
现在是时候完成我们的示例了。转到故事板,并向其中添加一个文本视图和一个按钮。将文本视图与你的代码链接,命名为
textView。然后,为你的按钮创建一个动作,添加以下代码:@IBAction func findWayOut(sender: UIButton) { var resultString = "" var maze = Maze(width: 8, height: 5) maze.setWayout((7,4)) maze.addWall((1,0)) maze.addWall((1,1)) maze.addWall((1,2)) maze.addWall((1,4)) maze.addWall((3,0)) maze.addWall((3,1)) maze.addWall((3,3)) maze.addWall((4,3)) maze.addWall((5,1)) maze.addWall((5,3)) maze.addWall((6,1)) maze.addWall((6,3)) maze.addWall((6,4)) maze.addWall((7,1)) let (found:Bool, way:[Position]) = maze.findWayOut() if found { for position in way { resultString+= "(\(position.x),\(position.y)) \n" } }else{ resultString+="No path found" } textView.text = resultString }
它是如何工作的…
许多代码需要一些解释。让我们从开始讲起。我们创建了一个包含四个可能值的枚举。使用枚举比使用整数或字符串更好,因为这样可以防止使用不存在的值。
在此之后,我们声明了两个类型别名;这个指令的想法是将类型重命名,就像我们之前对 Position 和 Way 做的那样。这不会创建一个新的类型,但有助于我们在软件维护方面;例如,如果你使用一个可能被更改为双精度浮点数数组的整数数组,使用 typealias 比替换每个声明为 [Double] 更好。这个特性与 C 编程语言中的 typedef 相当。
现在,让我们谈谈属性。第一个属性不是可选的,它位于双括号内,这意味着它是一个二维数组。通常,根据人类的定义,我们说数组的第一个维度是行,第二个是列。
当我们谈论位置时,例如,在笛卡尔平面上,我们将 x 称为列,将 y 称为行,这也是为什么你会看到 y 坐标在 x 前面的原因,例如 maze[position.y][position.x+1] == .AISLE。
栈也是一个非可选属性,因为我们不需要 nil 值,但这意味着我们必须用某些东西来初始化它。由于我们每次调用 findWayOut 时都会初始化它,第一次初始化将会两次:一次在初始化器中,另一次在函数内部。为了防止这种双重初始化,我们将添加 lazy 修饰符,这意味着它只应该在变量第一次被读取且之前未初始化时,使用声明中相同的值进行初始化。
注意
在 Swift 的第一个版本中,它曾经是 @lazy 而不是仅仅 lazy。
让我们谈谈初始化器;要创建一个迷宫,必须接收宽度和高度作为参数。由于参数与属性具有相同的名称,因此有必要区分它们。在这种情况下,属性被称为 self.width 和 self.height,而参数被称为 width 和 height。
下一步是通配符表达式,这意味着 for 循环的当前值没有被使用。在这种情况下,我们不是将其分配给一个变量,而是直接使用下划线。请注意,下划线在两个循环中都使用,并且内循环不会影响外循环,反之亦然。
创建数组还有另一种只在一行中完成的方法;这应该更加高效,因为当你创建具有容量的数组时,它与逐个添加每个元素是不同的。尝试用这行代码替换两个 for 循环:
maze = [[BlockType]](count: height, repeatedValue: BlockType)
接下来的两个方法非常相似,但一个有前缀 add,另一个有前缀 set。这只是为了软件维护;我们可以有很多墙壁,但只有一个出口。在这段代码中,我们没有检查它,但将来应该进行检查。在这种情况下,我们不是使用设置器,而是可以使用属性。
还有一点要补充的是,当属性被分配时,没有必要指定枚举类型,例如 BlockType.WALL,只需指定其值,例如,.WALL。原因是 Swift 知道分配的类型,并且可以省略。
然后,我们有将用于找到出口的方法。它只初始化栈以确保它是一个空数组,然后调用 next 函数。请注意,这看起来像我们有一个双圆括号函数,但事实并非如此,我们正在将一个元组作为参数传递。
元组类似于固定大小的数组。创建它时,使用圆括号而不是方括号。Swift 中元组的一个有趣特性是你可以像字典或对象一样命名值。因此,你可以选择将元组创建为 (0,0) 或 (x:0, y:0)。在像 Way 类型这样的情况下,其中元素有不同的含义,我建议你命名这些值;否则,如果有一个通往出口的路径,或者第二个元素代表路径本身,那么很难记住第一个位置上的元素代表什么。
还要注意,当 findWayOut 方法返回其值时,查看视图控制器。这看起来像它们被分配给一个元组,但事实并非如此,它们被分配给两个变量;这是我们在 C 或 Objective-C 上没有的特性。这意味着如果你想要交换两个变量的值,你只需使用一个语句,例如 (var1, var2) = (var2, var1),而不需要创建任何辅助变量,就像我们在 Objective-C 上所做的那样。
在这个菜谱中,我们使用数组作为栈来存储我们已经走过的路径,并使用 removeLast 方法返回一步,就像我们有一些面包屑一样,这是找到出口的秘密。我们只需要遵循一条路径,如果我们迷路了,我们只需要收集面包屑并尝试另一条。
在找到目标后,用户将看到如下截图所示的路径:

还有更多...
Swift 中的元组可以用不同的方式使用;它们在某些 switch 情况下也非常有用;然而,不要尝试用元组替换字典或数组,每种类型都有自己的功能。
注意
Swift 1.2 引入了一种称为 Set 的本地类型,它执行这个菜谱所做的工作。
创建自己的集合
有时 Swift 中包含的集合类型不足以解决我们的问题,因此在这个菜谱中,我们将创建自己的集合。这个菜谱的目标不仅是展示如何创建自己的集合,还包括如何重载运算符。
对于这个菜谱,我们将创建一个简单的购物列表程序,用户可以写下他需要购买的产品及其数量。如果他尝试添加两次,产品不会出现两次,而是将数量加到现有产品上。
将会有一个开关按钮,当它被禁用时,意味着如果产品已经存在于购物列表中,用户将无法将其添加到购物列表中,当然,也会有一个按钮来显示我们的列表。
准备工作
如同往常,创建一个名为 Chapter2 ShoppingList 的新项目,然后创建一个名为 ShoppingList 的 Swift 文件。这里的想法是创建我们的容器以及它将存储的类型;在这种情况下,是 Product 类。
如何做…
产品是与我们的容器相关的东西;我们甚至可以说这是它的一部分。因此,在这种情况下,我们可以创建一个嵌套类来与之一起工作。在这个类中,我们只需要两个属性:它的名称和它的数量。按照以下步骤创建自己的集合:
-
在这种情况下,我们将创建其描述,并实现
Comparable协议。我们将使用这个协议来确定两个对象是否代表相同的产品。让我们开始编码:class ShoppingList: CustomStringConvertable { class Product: Comparable, CustomStringConvertable { var name:String lazy var quantity:Int = 1 init(_ name:String){ self.name = name } var description: String { return "\(name): \(quantity)" } } -
现在我们需要创建购物列表的属性。我们只需要一个数组来存储我们的产品:
private var set:[Product] = [] -
由于购物列表是一个集合,我们应该实现基本的方法。一个方法用于添加产品,另一个方法用于知道购物列表是否已经包含该产品。当然,我们还会添加
description属性:func contains(product: Product)-> Bool{ for currentProduct in set { if currentProduct == product { return true } } return false } func add(product:Product){ for currentProduct in set { if currentProduct == product { currentProduct += product.quantity return } } set.append(product) } var description: String { let descriptions = set.map{$0.description} return descriptions.joinWithString(" ") } }注意
注意,我们使用双等号运算符(
==)比较了一个产品与另一个。主要问题是编译器是如何进行比较的?实际上,没有程序员的帮助,编译器无法进行这种比较。首先,正如你所看到的,我们在程序中使用了Comparable协议。这并不是必需的,但如果我们还需要使用产品与其他容器一起使用,实现这个协议是好的。 -
即使它是一个比较,我们也必须实现一个函数,该函数将告诉运行时两个产品是否相等。这个函数必须命名为
==(是的,双等号),并且必须在全局范围内声明:在类和函数之外。因此,这里我们有相应的代码:func ==(leftProduct: ShoppingList.Product, rightProduct: ShoppingList.Product) -> Bool{ return leftProduct.name.lowercaseString == rightProduct.name.lowercaseString } -
如果我们只实现
Equatable协议,我们就不需要实现任何其他方法;然而,由于我们正在实现Comparable,我们还需要实现运算符<、<=、>和>=:func <=(leftProduct: ShoppingList.Product, rightProduct: ShoppingList.Product) -> Bool{ return leftProduct.name <= rightProduct.name } func >=(leftProduct: ShoppingList.Product, rightProduct: ShoppingList.Product) -> Bool{ return leftProduct.name >= rightProduct.name } func >(leftProduct: ShoppingList.Product, rightProduct: ShoppingList.Product) -> Bool{ return leftProduct.name > rightProduct.name } func <(leftProduct: ShoppingList.Product, rightProduct: ShoppingList.Product) -> Bool{ return leftProduct.name < rightProduct.name } -
由于我们正在创建一些运算符,让我们继续。让我们重载
+=运算符两次,一次用于向产品添加更多单位,另一次用于将产品添加到购物列表中:func +=(shoppingList: ShoppingList, product: ShoppingList.Product) -> ShoppingList{ shoppingList.add(product) return shoppingList } func +=(product: ShoppingList.Product, quantity: Int) -> ShoppingList.Product{ product.quantity += quantity return product }注意
注意,重载这个操作符不需要实现任何协议,甚至之前的操作符也不需要,但实现
Comparable协议以与其他函数或算法一起使用是一个好主意。提示
当你可以使用它与其他泛型对象一起时,实现
Comparable或Equatable协议,例如,当你认为对象可以被排序时。 -
Swift 的一个好特性是,你不需要仅重载现有操作符;你还可以创建新的操作符。在这种情况下,我们将创建两个新的操作符:
=>,它将告诉我们一个产品是否在我们的购物清单中,以及!=>,这是相反的操作符。我将在稍后详细解释。在以下代码中使用这些操作符:infix operator => { associativity left precedence 140 } infix operator !=> { associativity left precedence 140 } func =>(product:ShoppingList.Product, shoppingList:ShoppingList)->Bool { return shoppingList.contains(product) } func !=>(product:ShoppingList.Product, shoppingList:ShoppingList)->Bool { return !shoppingList.contains(product) } -
现在模型已经完成,让我们创建视图。输入两个文本字段:一个用于产品名称,另一个用于数量;一个开关,允许添加产品或不添加;两个按钮,一个用于将产品添加到列表中,另一个用于显示列表;以及一个文本视图。让我们将它们连接起来,除了按钮具有以下属性:
@IBOutlet var fieldQuantity: UITextField! @IBOutlet var fieldProduct: UITextField! @IBOutlet var appendSwitch: UISwitch! @IBOutlet var textResult: UITextView! -
添加一个表示应用程序购物清单的属性:
var shoppingList: ShoppingList = ShoppingList() -
现在我们需要添加按钮动作。让我们从最容易的事情开始:显示按钮,它将购物清单的描述显示到文本视图中:
@IBAction func showList(sender: UIButton) { textResult.text = shoppingList.description } -
现在我们必须创建添加按钮的动作。在这种情况下,将需要检查用户是否在数量文本字段中输入了数字,以及是否可能将产品添加到购物清单中:
@IBAction func addToList(sender: UIButton) { var product = ShoppingList.Product(fieldProduct.text) if let quantity = Int(fieldQuantity.text) { product.quantity = quantity if appendSwitch.on || product !=> shoppingList{ shoppingList += product }else { let alert = UIAlertController(title: "Wrong Product", message: "This product is already on your list"), preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } }else { let alert = UIAlertController(title: "Wrong Value", message: "Oops! I need a number on the quantity field"), preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) fieldQuantity.text = "" } clear() }clear函数只是一个辅助方法,每次我们按下添加按钮时都会清空文本字段:private func clear(){ fieldQuantity.text = "" fieldProduct.text = "" fieldQuantity.becomeFirstResponder() } -
现在你可以点击播放并添加一些产品,重复一些产品,并显示它们。你应该得到一个像这里显示的结果:
![如何操作…]()
它是如何工作的…
在 Swift 中,重载操作符是非常常见的;你可以重载现有操作符,也可以创建你自己的操作符。如果你想创建自己的操作符,你必须首先报告一些关于你的操作符的属性。首先,你必须选择你想要的使用类型:
-
中缀:这意味着操作符用于两个对象之间;例如,在我们的示例中,我们创建了
!=>操作符,用于在产品和集合之间使用。 -
前缀:这意味着操作符将只与它右边的对象一起操作。例如,让我们想象我们想要创建一个
!!!操作符,这可能意味着我们想要清空购物清单;在这种情况下,我们应该将其用作!!!shoppingList。 -
后缀:这个操作符将只与它左边的对象一起操作,例如
shoppingList!!!。
下一步是写下operator和为其选择的名字。之后,你必须在这个操作符的属性之间添加括号。结合值是左结合、右结合和无结合。如下所示:
-
左关联性:这意味着当有多个具有相同优先级的运算符时,最左边的一个将被首先评估
-
右关联性:正如您所想象的那样,这是左关联性的相反
-
非关联性:这意味着不允许有多个具有相同优先级的运算符
优先级就像优先级。优先级高的先被评估。
还有更多...
Swift 允许改变现有运算符的功能,例如,您可以声明以下函数:
func + (i:Int, j:Int) -> Int {
return 10
}
这将使每个整数求和运算符返回10。这很有趣,但在正常情况下我不会这样做。更糟糕的是,如果你创建递归调用,就像这样:
func + (i:Int, j:Int) -> Int {
if i == 1 && j == 1 {
return 10
}else {
return i + j
}
}
组织宴会厅
在这个菜谱中,我们将学习如何使用 Swift 编程语言的其他功能。我们将从数组中复制元素的范围,使用字典,下标,switch,并命名循环。
对于这个应用,我们将为一家公司举办一场晚宴。在这种情况下,这并不是一个婚礼餐桌,夫妻必须坐在一起,而是我们只需要同一组的人坐在同一张桌子上,可能是因为他们来自同一个团队或类似的原因。
我们将创建一个表示房间的类。要将新客人添加到这个房间,我们需要指定属于这个组的人的名字,这个人已经在房间里了。如果我们想添加到这个房间的人是第一个,我们将使用 nil 值作为属于这个组的人的名字。
如果要向已满的桌子添加某人,则必须将此桌上的一个人重新分配到另一张桌子上。当然,我们不会使用最优化算法,因为这不是我们的主要目标。
准备工作
创建一个新的 Swift 单视图项目,命名为Chapter2 DinnerRoom。
如何做到这一点...
如同往常,我们需要从模型部分的模型-视图-控制器开始。对于这段代码,我们需要一个表示房间的类,另一个表示存储坐在那里的人的座位和桌子,一个组,这是属于同一团队的桌子的人的范围,当然,我们还需要一个表示个人的类。
-
让我们从
person类开始;在这种情况下,我们只需要存储他的名字和他所属的组。由于有一个人可以没有任何组的时间段,这个属性应该是可选的。因此,创建一个新的 Swift 文件,命名为person.swift,并添加以下代码:class Person: Equatable { var name: String var group:Group? init(_ name: String){ self.name = name } } -
如您所见,这个类继承自
Equatable协议,这意味着我们必须实现==运算符,如下所示:func ==(person1:Person, person2:Person)->Bool{ return person1.name == person2.name } -
好的,现在让我们创建
Group类。记住,一个组没有名字;它只是坐在一起的人的范围。在这种情况下,我们需要存储范围开始的地方,结束的地方,以及它的桌子。正如您所想象的那样,我们需要创建一个名为group.swift的文件,并添加以下代码:class Group { unowned var table:Table var rangeStart: Int var rangeEnd:Int var size:Int { return rangeEnd - rangeStart + 1 } init (table:Table, entryPoint:Int){ self.table = table rangeStart = entryPoint rangeEnd = entryPoint } func shift(){ rangeStart++ rangeEnd++ } func increase(){ rangeEnd++ } }注意
注意,
size属性没有 setter,只有 getter,并且它与新属性无关;这就是所谓的computed属性。此外,注意我们不得不给table属性添加一个unowned修饰符;这是因为如果我们有一个 UML 类图,我们可以看到房间包含桌子,桌子上有属于一个知道其桌子的小组成员。正如你所看到的,我们有一个循环,默认情况下会阻止引用计数器达到零,因此会创建内存泄漏。添加unowned将帮助我们避免这个问题。 -
下一个类是
Room。这个类需要存储其桌子和已经进入房间的客人。第二个属性不是强制的,但计算机在字典中查找它比搜索它要快,对于程序员来说,这也更快,因为他将编写更少的代码。将room.swift添加到你的项目中,并开始添加以下代码:class Room: CustomStringConvertable { let STANDARD_TABLE_SIZE = 3 var guests:[String: Person] = [String: Person] () var tables = [Table]() func add(table:Table){ tables.append(table) } func add(person:Person){ guests[person.name] = person } var description:String { let descriptions = tables.map{$0.description} return descriptions.joinWithSeperator("Table: ") } -
现在,看看
guests属性,因为我们有两个类型在括号内,并用冒号分隔;这意味着它不是一个数组,而是一个字典。或者,你也可以写成Dictionary<String,Person>而不是[String: Person]。与 Objective-C 到 Swift 的字典相比,有一个区别是你必须指定键和值类型。注意
当可能时,尽量使用字典而不是搜索元素,因为这样会有更好的性能。
如果你是一个好的观察者,你会看到我们重复了
add函数。区别在于参数类型。这意味着你可以在 Swift 中重载方法和函数。 -
现在,为了检查某人是否已经在这个房间里,我们将使用方括号操作符,这样我们就可以编写类似
if room["Harry Potter"] == true {...}的代码。为了在 Swift 中启用它,我们必须编写一种特殊函数,称为 subscript。在这种情况下,我们将编写一个只读的subscript:subscript(name:String)->Bool{ get{ if let guest = guests[name] { return true } return false } // No setter } -
现在我们只需要使用相同的方法来添加一个人到房间中;记住,我们必须指定来自同一组的人的名字或当是第一个人时为 nil。按照这个想法,我们可以用类似
room["Harry Potter"] = Person("David Copperfield")的代码添加一个人到房间中:subscript(name:String?)->Person{ get{ assertionFailure("Sorry, no subscript getter") } set(newValue){ guests[newValue.name] = newValue // if the key is nil we will have to look for // the first table that is not null. If we // are not able to find it we have to create a new table if let personName = name { if let guest = guests[personName]{ // now we need to find its table var guestGroup = guest.group! newValue.group = guestGroup // now we have to check the group table is full if guestGroup.table.full { // the table is full, if we have only 1 group it is not possible to add // any one to this table, otherwise the last group should move to another table if guestGroup.table.size == guestGroup.size { // The group is bigger than the supported size assertionFailure("Group too big") }else{ // the last table group should go to a new table var lastGroup = guestGroup.table.getLastGroup()! tables.append( guestGroup.table.transferGroup(lastGroup)) // now the guestGroup table has free space } } guestGroup.table.add(newValue) guestGroup.increase() }else { assertionFailure("This guest should exists") } }else { // this person belongs to a new group var table = freeTable() var index = table.add(newValue) var group = Group(table: table, entryPoint: index) newValue.group = group } } } private func freeTable() -> Table { for table in tables { if !table.full { return table } } var newTable = Table(STANDARD_TABLE_SIZE) tables.append(newTable) return newTable }正如你所看到的,我们在 subscript 的 getter 方法中添加了一个断言;原因是 subscripts 可以是只读的或读写,但不能是只写的。在这种情况下,请求返回值是没有意义的,所以我们唯一能做的就是创建一个断言来防止问题。
小贴士
不要经常使用
assertionFailure;尽量创建可以检测错误并继续工作的代码。 -
创建一个名为
table.swift的新文件。在我们开始编写Table类之前,我们需要知道一个桌子将有一个座位数组。我们可以有一个空座位或被占用的座位。如果它被占用,那么它是由某人占用的,因此我们需要知道谁占用了这个座位。对于这种情况,Swift 允许我们使用枚举:enum TableSeat { case FREE, OCCUPIED(Person) }枚举的唯一问题是我们需要多次使用
switch分支,所以在这种情况下,当我们想知道一个座位是否空闲或被某人占用时,重载运算符==和!=是一个好主意:func == (seat1:TableSeat, seat2:TableSeat) -> Bool { switch(seat1,seat2){ case (.FREE,.FREE): return true case (.OCCUPIED(let person1),.OCCUPIED(let person2)): return person1 == person2 default: return false } } func != (seat1:TableSeat, seat2:TableSeat) -> Bool { return !(seat1 == seat2) } -
现在我们可以开始编写
Table类了。基本上,我们需要存储一个座位数组,但我们可以有一些辅助的只读计算属性和方法,如下所示:private var seats:[TableSeat] init (_ size: Int){ seats = TableSeat } var full:Bool { return seats.last! != .FREE } var freeSeats:Int { var total = 0 for i in seats.reverse() { if i == TableSeat.FREE { ++total }else{ break } } return total } var nextFreeSeat:Int { return seats.count - self.freeSeats } var description:String { let takenSeats = seats.filter({ (seat) -> Bool in switch seat { case .FREE: return false case .OCCUPIED: return true } }).map({(seat) -> String in switch seat { case .FREE: assertionFailure("???") case .OCCUPIED(let person): return person.name } }) return takenSeats.joinWithString(", ") private func shift(group:Group){ seats[(group.rangeStart+1)...(group.rangeEnd+1)] = seats[group.rangeStart...group.rangeEnd] seats[group.rangeStart] = .FREE group.shift() } var size:Int { return seats.count } func getLastGroup() -> Group? { for seat in seats.reverse() { switch seat { case .OCCUPIED(let bySomeone): return bySomeone.group case .FREE: continue } } // no group return nil } func transferGroup(group: Group)->Table{ var newTable = Table(seats.count) // creating a new table with the same size newTable.seats[0..<(group.size)] = seats[group.rangeStart...group.rangeEnd] seats[group.rangeStart...group.rangeEnd] = TableSeat[0...(group.size-1)] group.table = newTable return newTable } func add(person:Person)->Int { var lastAllocatedSeat = self.nextFreeSeat-1 // return -1 if it wasn't possible if self.full { return -1 } var index = lastAllocatedSeat + 1 if let group = person.group { // who we have to shift the groups until we find // the new person's group and them we keep // him (or her) on the array searching: while lastAllocatedSeat>=0 { // in this case the seat should be always occupied // but as the compiler doesn't know we have to retrieve // its value switch seats[lastAllocatedSeat] { case .FREE: assertionFailure("shouldn't be any free seat here") case .OCCUPIED(let groupPerson): if groupPerson.group !== person.group { // different groups, let's move the group to the right lastAllocatedSeat = groupPerson.group!.rangeStart-1 shift(groupPerson.group!) }else{ break searching } } } index = lastAllocatedSeat + 1 }else{ // if the person group is null means that it's a new group so // can add him on the first available seat } self.seats[index] = .OCCUPIED(person) return index } } -
在进行过多解释之前,我们将通过将
textView添加到我们的视图控制器以及一些人员添加到room.swift中来测试之前的代码:@IBOutlet var textView: UITextView! var room:Room = Room() override func viewDidLoad() { super.viewDidLoad() room[nil] = Person("Mr Peter File") room[nil] = Person("Ms Mary Simpson") room["Mr Peter File"] = Person("Mr Taro Mashimoto") room[nil] = Person("Mr Stuart Johnson") room["Ms Mary Simpson"] = Person("Mr Scott Chesterford") self.textView.text = room.description }
它是如何工作的...
如你所见,我们在这里使用了一些新的特性。我们能够使用…运算符复制一系列座位,这是一个很棒的功能,可以节省我们编写大量执行相同操作的循环。
注意
注意,这个切片运算符(…),当与数组的一部分一起使用时,编译器可以为更好的性能创建一个良好的优化。
另一个很好的特性是能够与值组合使用的switch语句。看看==运算符,你会发现我们不需要为每个情况创建内部的 switch 分支。谈到等式运算符,看看我们使用的add方法:使用!==而不是!=。原因是当我们需要检查两个对象是否具有相同的实例时,我们必须使用运算符===或!==来检查它们是否不共享相同的实例。
我们还使用了一个标签来命名一个循环(搜索),这样做的原因是,默认情况下,break语句将退出switch,而不是我们的循环。我们可以通过一些布尔变量来控制这种情况,但我们可以通过break searching来避免它中断。
另一个很好的技巧是在我们的数组中以相反的顺序遍历。我们使用reverse方法做到了这一点。当然,我们知道我们有一个小数组;我无法想象一个有一百万人的表格。使用reverse处理大数组不是一个好主意,因为内部会创建一个新的数组。
还有更多...
你仍然可以使用旧的NSDictionary类,但我会遵循我们在NSArray中看到的相同规则。Swift 字典更安全,如果你需要一个字典,存储完全不同的对象类型。最好审查你的代码,因为这可能会非常痛苦地维护。
你刚刚开始学习断言,但在本书的后面部分,你将学习如何处理断言。
当你需要处理没有指定输入或输出类型的函数时,Swift 为你提供了泛型的功能。我们将在下一章中了解更多关于这个内容。
第三章. 使用结构体和泛型
在这一章中,我们将涵盖以下主题:
-
创建考试应用
-
检查正确答案
-
避免复制结构体
-
创建泛型数组初始化器
-
创建优先级列表
-
为优先级队列创建协议
简介
我们可以说结构体类似于类。它们存储具有属性的值,并且有初始化器和方法。但它们的用法略有不同。Swift 中结构体的想法来自 Objective-C,而 Objective-C 本身正在使用 C 结构体。
我们还将使用泛型,因此我们可以创建泛型容器。泛型的想法并不新鲜;像 C++和 Java 这样的其他语言已经有了它。然而,这个特性在 Objective-C 中并不存在,因此程序员负责转换检索到的数据,因此代码是不安全的。
创建考试应用
在这个菜谱中,我们将创建一个考试应用。对于这个考试,我们将选择一些随机问题,用户将回答它们。最后,应用将显示用户得分并重新开始新的考试。
准备工作
首先,打开 Xcode,创建一个名为Chapter 3 Examination的项目,然后创建一个名为question.swift的文件。这是我们定义考试问题的位置。
如何操作…
要创建一个考试应用,请按照以下步骤操作:
-
打开故事板,并向视图控制器添加一个标签和三个按钮。您将得到以下截图类似的内容:
![如何操作…]()
-
将以下代码复制到
question.swift文件中:struct Question { var question:String var answer1:String var answer2:String var answer3:String var rightAnswer:Int var userAnswer:Int? init(question:String, answer1:String, answer2:String, answer3:String, rightAnswer:Int){ self.question = question self.answer1 = answer1 self.answer2 = answer2 self.answer3 = answer3 self.rightAnswer = rightAnswer } } -
好的,现在我们可以创建我们的问题数组。这将像是一个模板,因为它还不是考试;它将是一个包含每个问题的容器。所以,前往我们唯一的视图控制器,并添加以下属性:
private var examTemplate = [Question]() -
下一步是将问题填充到这个数组中;正如您可能想象的那样,在这个菜谱中,我们不会添加很多问题,但一个真实的应用程序可以有很多。在这种情况下,我们需要按类别将其分成不同的方法,如下所示:
private func addGeneralKnowledgeQuestions(){ examTemplate += [ Question(question: "In which year was Packt Pub founded?", answer1: "2001", answer2: "2004", answer3: "1978", rightAnswer: 2), Question(question: "What is the capital of Luxembourg?", answer1: "Luxembourg City", answer2: "Diekirch", answer3: "Viena", rightAnswer: 1) ] } private func addComputersQuestions(){ examTemplate += [ Question(question: "In which year did Bob Bemer, the creator of the 'escape key', die?", answer1: "2004", answer2: "1980", answer3: "He is still alive", rightAnswer: 1), Question(question: "How much RAM did Macintosh 128Kb have?", answer1: "1 Gb", answer2: "1 byte", answer3: "128K", rightAnswer: 3) ] } private func addAstronomyQuestions(){ examTemplate += [ Question(question: "What is the name of the solar system star?", answer1: "Antonio Banderas", answer2: "Europe", answer3: "Sun", rightAnswer: 3), Question(question: "How long is the astronomical unit?", answer1: "150 millions of kilometers", answer2: "1 light year", answer3: "8 thousand inches", rightAnswer: 1) ] } -
好的!现在,我们可以初始化我们的考试了,所以让我们创建一个方法来完成它。我们还需要一个属性来包含当前考试,另一个属性知道当前问题:
private lazy var exam:[Question] = [] private lazy var currentQuestion = 0 private func createExam(){ func containsQuestion(question:String) -> Bool{ for i in exam{ if question == i.question{ return true } } return false } exam = [] currentQuestion = 0 while exam.count < 3 { var question = examTemplate[ Int(arc4random_uniform(UInt32(examTemplate.count)))] if !containsQuestion(question.question) { exam.append(question) } } } -
好吧,是时候开始了!我们只需要显示问题及其可能的答案,就像这里显示的方法一样:
@IBOutlet var labelQuestion: UILabel! @IBOutlet weak var buttonAnswer1: UIButton! @IBOutlet weak var buttonAnswer2: UIButton! @IBOutlet weak var buttonAnswer3: UIButton! @IBOutlet weak var buttonAnswerIdontKnow: UIButton! private func showCurrentQuestion(){ if currentQuestion < exam.count { labelQuestion.text = exam[currentQuestion].question buttonAnswer1.setTitle(exam [currentQuestion].answer1, forState: .Normal) buttonAnswer2.setTitle(exam[currentQuestion].answer2, forState: .Normal) buttonAnswer3.setTitle(exam[currentQuestion].answer3, forState: .Normal) buttonAnswerIdontKnow.setTitle("I don't know", forState: .Normal) }else { var total = 0 for i in exam { total += i.rightAnswer == i.userAnswer? ? 1 : 0 } let alert = UIAlertController(title: "Score", message: "Your score is \(total)", preferredStyle:.Alert) let startAgainAction = UIAlertAction(title:"Start Again", style.Default) { (action) -> void in createExam() showCurrentQuestion() } alert.addAction(startAgainAction) self.presentViewController(alert, animated: true, completion: nil) } } -
现在,我们只需要为按钮添加以下操作:
@IBAction func answer(sender: UIButton) { switch sender { case buttonAnswer1: exam[currentQuestion].userAnswer = 1 case buttonAnswer2: exam[currentQuestion].userAnswer = 2 case buttonAnswer3: exam[currentQuestion].userAnswer = 3 default: print("I don't know") } currentQuestion++ showCurrentQuestion() } -
如果您点击“现在播放”,您会发现应用仍然不起作用;我们必须初始化它,所以让我们通过填充
viewDidLoad方法来完成这个应用程序,如下所示:override func viewDidLoad() { super.viewDidLoad() addGeneralKnowledgeQuestions() addComputersQuestions() addAstronomyQuestions() createExam() showCurrentQuestion() }
它是如何工作的…
类和结构体之间的主要区别是结构体在每次赋值时都会被复制。这意味着什么?这意味着在这种情况下,如果我们用类创建了程序,考试属性和考试模板将指向相同的对象(问题)。
考虑到这个问题,你可以看到如果我们再次开始时使用类,新的考试将带有前一个用户的答案。还有更多;如果你想要存储带有答案的考试,你必须克隆对象;否则,每个人都会有相同的答案。使用结构体,你不必担心这个问题;每次你创建一个新的考试,你都会有新的对象。
我想评论的另一个有趣的部分是createExam函数。正如你所看到的,我们在这个函数内部还有一个函数。Swift 允许你拥有辅助函数。这在我们需要将代码分成小任务时非常有用。
在同一个函数(createExam)中,你可以看到我们有一个创建随机数的巨大调用。这个奇怪调用的原因是因为 Swift 还没有随机数的函数。实际上,Objective-C 也没有随机函数;我们必须使用 C 函数arc4random_uniform。
这个函数接收一个 32 位无符号整数作为参数,但 Swift 不能将其整数转换为这种类型。所以,我们使用了UInt32来转换这个数字。因为这个函数也返回一个无符号整数,所以有必要将其结果转换为 Swift 整数。
还有更多...
还有更多函数可以检索随机数,如rand、random和arc4random。查看命令行手册并检查它们的区别。
检查正确答案
这个菜谱将通过检查用户的答案并验证其正确性来完成前一个菜谱。如果由于任何原因答案接收到的值超出范围,这将设置为nil。当然,在这个应用程序中,不可能用错误的值回答,但请记住,一个好的开发者总是思考软件可能的演变。
准备中
复制前一个菜谱;如果你愿意,可以通过简单地重命名目标名称来将产品重命名为Chapter 3 Examination 2,如下所示:

如何做到这一点...
按以下步骤检查答案:
-
前往
question.swift文件。现在,用以下代码替换当前的类:struct Question { var question:String var answer1:String var answer2:String var answer3:String var rightAnswer:Int var userAnswer:Int? { willSet(newAnswer){ if newAnswer < 2 || newAnswer > 3 { userAnswer = nil print("Wrong value, fixing it") } } didSet(oldValue){ valid = userAnswer != nil && userAnswer != rightAnswer } } var valid = false init(question:String, answer1:String, answer2:String, answer3:String, rightAnswer:Int){ self.question = question self.answer1 = answer1 self.answer2 = answer2 self.answer3 = answer3 self.rightAnswer = rightAnswer } } -
现在,回到视图控制器,用以下代码替换
showCurrentQuestion方法:private func showCurrentQuestion(){ if currentQuestion < exam.count { labelQuestion.text = exam[currentQuestion].question buttonAnswer1.setTitle(exam[currentQuestion].answer1, forState: .Normal) buttonAnswer2.setTitle(exam[currentQuestion].answer2, forState: .Normal) buttonAnswer3.setTitle(exam[currentQuestion].answer3, forState: .Normal) buttonAnswerIdontKnow.setTitle("I don't know", forState: .Normal) }else { var total = 0 for i in exam { total += i.valid ? 1 : 0 } let alert = UIAlertController(title: "Score", message: "Your score is \(total)", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } } -
这个菜谱可以到此为止。然而,因为我们想检查这个,超出范围的值将被更正为
nil;我们可以用这里的这个替换answer方法:@IBAction func answer(sender: UIButton) { switch sender { case buttonAnswer1: exam[currentQuestion].userAnswer = 1 case buttonAnswer2: exam[currentQuestion].userAnswer = 2 case buttonAnswer3: exam[currentQuestion].userAnswer = 3 default: exam[currentQuestion].userAnswer = 0 } currentQuestion++ showCurrentQuestion() }
它是如何工作的...
Swift 有一个很好的属性特性,称为property observer。这个特性等同于关系数据库中的触发器。使用willSet,你可以纠正输入,使用didSet,你可以在值改变后触发所需的操作。
我们还改变了检查有效答案的方式;这样做是因为问题的逻辑应该在其类或结构内部。
还有更多...
如你所见,这个菜谱与第二章中的用户问答菜谱相关,标准库和集合。如果你想创建一个更完整的例子,你可以将两个应用程序合并为一个。
避免复制结构体
有时候我们在处理结构体时,并不想复制它们。在这个菜谱中,我们将通过创建一个小应用程序来展示这个例子和解决方案,用户可以在其中看到两个假设角色的坐标,我们可以按一个按钮来改变它们的坐标到一个中心点。
准备工作
创建一个名为Chapter3 Vector2D的新单视图项目。对于这个菜谱,我们只需要一个新文件,我们将称之为Position.swift。
如何做…
让我们创建一个防止复制结构体的应用程序:
-
让我们像往常一样从模型部分开始。点击
Position.swift文件。让我们创建一个具有相同名称的结构体,如下所示:struct Position:CustomStringConvertable { private var x:Int,y:Int init(){ (x,y) = (0,0) } mutating func moveUp(){ self.y-- } mutating func moveDown(){ self.y++ } mutating func moveRight(){ self.x++ } mutating func moveLeft(){ self.x-- } mutating func meet(inout position:Position){ let newx = (self.x + position.x) / 2 var newy = (self.y + position.y) / 2 self.x = newx self.y = newy position.x = newx position.y = newy } var description:String { return "\(self.x)x\(self.y)" } } -
现在,转到故事板,并向其中添加九个按钮和两个标签,类似于以下截图:
![如何做…]()
-
现在,让我们将我们的标签与以下属性链接:
@IBOutlet var labelC1: UILabel! @IBOutlet var labelC2: UILabel! -
然后,我们将创建两个表示角色坐标的属性。当然,在一个真正的游戏中,这些属性将属于另一种类型的对象,可能是类似角色的东西:
var character1 = Position() var character2 = Position() -
如你所见,这些对象将从 0 x 0 位置开始,但如果我们没有使用
viewDidLoad方法初始化它,标签将不知道这一点。所以,让我们向视图控制器添加以下代码:override func viewDidLoad() { super.viewDidLoad() displayPositionC1() displayPositionC2() } private func displayPositionC1(){ labelC1.text = character1.description } private func displayPositionC2(){ labelC2.text = character2.description } -
现在,我们可以添加移动角色的事件。正如你所想象的那样,它们非常直接,因为每个动作都将代理到结构体上的等效方法。以下是这段代码:
@IBAction func upC1(sender: UIButton) { character1.moveUp() displayPositionC1() } @IBAction func downC1(sender: UIButton) { character1.moveDown() displayPositionC1() } @IBAction func leftC1(sender: UIButton) { character1.moveLeft() displayPositionC1() } @IBAction func rightC1(sender: UIButton) { character1.moveRight() displayPositionC1() } @IBAction func meet(sender: UIButton) { character1.meet(&character2) displayPositionC1() displayPositionC2() } -
现在,应用已经完成。点击播放并使用按钮移动角色。最重要的是没有复制或克隆任何结构体。
它是如何工作的…
如你所见,我们不得不在我们的结构体方法上添加一个修饰符。这是因为结构体方法默认是常量。如果你需要更改一个属性,你必须使用mutating修饰符。
当接收到你不想复制的参数时,例如结构体,你必须使用inout参数。这个参数将允许你修改相应的参数。然而,当使用这个功能时,你必须调用函数,在变量前加上一个连字符(&),并且不能将表达式作为参数传递。
创建一个泛型数组初始化器
在这个菜谱中,我们将学习如何使用泛型。这个特性在 C++、Java 和 C#等语言中使用得很多,因为这样我们就不需要为函数中可能使用的每种类型重载一个函数。
在这种情况下,我们将创建一个函数,它接收输入项并返回一个包含这些元素的数组,但完全打乱顺序。
准备工作
创建一个名为 Chapter3 Array initializer 的新 Swift 单视图项目。
如何操作…
要创建一个泛型数组初始化器,请按照以下步骤操作:
-
添加一个名为
ArrayInit的新文件,并将以下代码添加到其中:func arrayInit<T>(values:T...)->[T]{ var newArray = values for var i=0;i < newArray.count * 2 ; ++i { let pos1 = Int(arc4random_uniform(UInt32(newArray.count))) let pos2 = Int(arc4random_uniform(UInt32(newArray.count))) (newArray[pos1], newArray[pos2]) = (newArray[pos2], newArray[pos1]) } return newArray } -
现在,我们需要在我们的故事板中添加两个按钮和一个文本视图来查看这个函数的工作情况。所以,让我们将文本视图链接到以下属性:
@IBOutlet weak var textView: UITextView! -
下一步是创建每个按钮的事件,所以将这些操作添加到你的视图控制器中:
@IBAction func arrayInt(sender: AnyObject) { let arr = arrayInit(5, 10, 15, 20, 25, 30) textView.text = arr.map({ (element) -> String in return String(element) }).joinWithSeparator("\n") } @IBAction func arrayString(sender: AnyObject) { let arr = arrayInit("Hello", "I'm", "Edu","Merry", "Christmas") textView.text = arr.joinWithSeparator("\n") } -
现在,是时候测试我们的代码了。运行你的应用程序并按每个按钮,你应该得到以下截图所示的结果:
![如何操作…]()
它是如何工作的…
面向对象编程的一个优点是避免代码重复。一些语言会强制你为字符串数组创建一个函数,为整数数组创建另一个函数,以及为每个我们需要与这个函数一起使用的新类型创建另一个新函数。
幸运的是,Swift 允许我们创建泛型函数。这意味着我们只需要实现一次函数,每次都会应用相同的代码。
注意
在函数内部,参数被视为一个常量数组,但用数组作为参数调用函数有另一个含义;编译器会认为你只有一个参数,它是一个数组。
这个函数有一些不同之处:在输入参数之后使用的省略号。这意味着该函数不受参数数量的限制;它具有可变数量的参数。在我们的例子中,我们可以用六个整数和五个字符串来调用它。这个特性非常有用,例如,在创建计算一些数字平均值的函数时。
还有更多…
允许泛型函数重载;当存在某种类型,由于任何原因需要不同的代码时,就会使用它。例如,现在你可以使用这段代码来洗牌。
创建优先列表
让我们想象一下,我们需要管理航班上的乘客队列。我们知道商务舱应该首先登机,然后是头等舱乘客,最后是经济舱乘客。
这是一个典型的优先队列案例,但一个令人烦恼的问题是,我们是否只能创建一个优先队列?或者我们应该在每一个新应用中创建一个新的优先队列?一个最近加入 Swift 的 Objective-C 程序员可能会创建一个存储 AnyObject 类型对象的容器。这个解决方案可能是可以接受的;然而,Swift 有一个更好的解决方案,甚至更安全,正如你所知,那就是泛型。
优先队列需要使用标准来组织其元素。在这种情况下,我们可以创建任何元素的队列,但确保它是为实现了 Comparable 协议的类的元素创建的;这就是我们所说的类型约束。
准备工作
创建一个名为 Chapter3 Flight 的新 Swift 单视图项目。
如何操作…
按照以下步骤创建优先列表:
-
添加一个名为
PriorityQueue.swift的新 Swift 文件。 -
在这个文件中,让我们创建一个具有相同名称的类。在这里,我们需要一个数组作为属性来存储我们的元素,以及一些与队列一起工作的方法:
enqueue用于添加新元素;dequeue用于从队列中移除第一个元素;size返回队列上的元素数量;以及toArray,它将队列的元素返回到一个数组中。所以,将以下代码添加到你的文件中:class PriorityQueue<T:Comparable> { private var elements = [T]() func enqueue(element:T) { elements.append(element) var index=elements.count-2 while index>=0 && elements[index] < elements[index+1] { (elements[index],elements[index+1]) = (elements[index+1],elements[index]) index-- } } func dequeue() -> T { return elements.removeAtIndex(0) } var size: Int { return elements.count } func toArray() ->[T]{ return elements } } -
现在创建一个名为
Passenger.swift的新文件。在这里,我们将定义一个带有其数据的乘客。记住,我们需要比较乘客的优先级。因此,这个类必须实现Comparable协议:class Passenger:Comparable, CustomStringConvertable { enum Class:Int { case ECONOMY=0, FIRST=1, BUSINESS=2 var value:Int{ return self.rawValue } } var classtype:Class var name:String var id:String init (name:String, id:String, classtype:Class = .ECONOMY){ self.name = name self.id = id self.classtype = classtype } var description:String{ var seattype:String switch self.classtype{ case .ECONOMY: seattype = "economy" case .FIRST: seattype = "first" case .BUSINESS: seattype = "business" default: seattype = "unkown" } return "\(self.name), with id \(self.id) on \(seattype) class" } } // Operators func <(lhs: Passenger, rhs: Passenger) -> Bool{ return lhs.classtype.value < rhs.classtype.value } func ==(lhs: Passenger, rhs: Passenger) -> Bool{ return lhs.classtype == rhs.classtype } func !=(lhs: Passenger, rhs: Passenger) -> Bool{ return lhs.classtype != rhs.classtype } func <=(lhs: Passenger, rhs: Passenger) -> Bool{ return lhs < rhs || lhs == rhs } func >=(lhs: Passenger, rhs: Passenger) -> Bool{ return !(lhs < rhs) } func >(lhs: Passenger, rhs: Passenger) -> Bool{ return lhs != rhs && !(lhs < rhs ) } -
现在打开你的故事板,添加两个文本字段(一个用于乘客姓名,另一个用于他的身份证号和身份证件),一个用于选择座位类型的表格视图,两个用于入队和出队的按钮,以及一个用于显示当前队列状态的文本字段。你应该有一个类似于以下布局:
![如何做到这一点…]()
-
下一步是打开视图控制器并添加协议
UITableViewDataSource:class ViewController: UIViewController, UITableViewDataSource { -
好的,现在将相应的组件与属性链接,并且在此旁边,创建一个乘客队列作为属性:
@IBOutlet weak var passengerName: UITextField! @IBOutlet weak var idDocument: UITextField! @IBOutlet weak var seatType: UITableView! @IBOutlet weak var textView: UITextView! private var passengersQueue = PriorityQueue<Passenger>() -
在这个时刻,我们可以开始实现
tableview代码。正如你所知,我们必须实现UITableViewDataSource至少两个强制方法。让我们从最简单的开始,即返回行数的方法。目前,我们还没有办法检测枚举元素的数量,所以我们将硬编码这个值:func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return 3 } -
下一步是创建返回座位类型单元格的方法:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{ var cell:UITableViewCell if let auxCell = tableView.dequeueReusableCellWithIdentifier("cell") { cell = auxCell }else{ cell = UITableViewCell() } switch indexPath.row { case 0: cell.textLabel!.text = "Economy class" case 1: cell.textLabel!.text = "First class" case 2: cell.textLabel!.text = "Business class" default: break; } return cell } -
如果你现在点击播放,你应该至少能看到带有其值的表格视图。现在,我们需要创建一个方法来显示当前队列乘客:
private func displayQueue () { textView.text = (self.passengersQueue.toArray().map{ (var p)-> String in return p.description }).joinWithSeperator("\n") } -
现在,我们只需要为我们的按钮创建动作:
@IBAction func enqueue(sender: AnyObject) { if let indexPath = seatType.indexPathForSelectedRow { var passenger = Passenger(name: passengerName.text, id: idDocument.text, classtype: Passenger.Class(rawValue: indexPath.row)!) passengersQueue.enqueue(passenger) self.displayQueue() }else { let alert = UIAlertController(title: "Error", message: "You must select the seat type", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } } @IBAction func dequeue(sender: AnyObject) { passengersQueue.dequeue() displayQueue() }
应用程序已经完成。现在,尝试添加不同的乘客并检查你的队列如何增长。
它是如何工作的…
泛型可以帮我们避免重写大量代码,但是因为它需要安全,你不能使用可能在你正在操作的类型上不存在的操作符或方法。为了解决这个问题,你可以指定一个约束,告诉编译器哪些方法可以与这个类型一起使用。在我们的例子中,我们指定了 T 是可比较的;因此我们可以使用比较器的操作符在我们的代码中。
从这段代码中我们可以获取的一些新知识是嵌套枚举。Swift 没有命名空间或包,但你可以创建嵌套枚举、类和结构体。这样我们可以避免名称冲突。
另一个新特性是类型化枚举;正如你所见,我们指定了每个枚举值都与一个整数相关联。你可以使用 rawValue 获取这个值,或者使用 init(rawValue:) 来执行逆过程。
注意
Swift 的第一个版本曾经有一个名为 toRaw() 的方法,而不是 rawValue 属性,以及 fromRaw() 而不是使用初始化器 init(rawValue:)。
你也可以像我们在这个枚举中做的那样实现你自己的函数或计算属性;有时,这比使用原始值更好。
小贴士
创建你的函数或计算属性以转换枚举是软件维护的良好实践。
还有更多…
解决这个问题不止一种方法;如果你需要性能,你可能使用双链表。
如果你想要,你可以使用 where 子句指定多个约束。例如,如果你想存储也是 CustomStringConvertable 的元素,你可以将类头更改为 class PriorityQueue<T:Comparable where T: CustomStringConvertable > {。
为优先队列创建一个协议
在之前的菜谱中,我们创建了一般化的代码,可以在未来的程序中使用,但我们必须记住,优先队列只是队列的一种类型。定义这个抽象数据类型的接口是一种良好的做法,然后,有不同的实现。
如你所知,在 Swift 中,我们有像这种情况的协议;然而,我们有一个问题:协议没有泛型。解决方案是什么?答案是 关联类型。
准备工作
复制之前菜谱的项目,并将其命名为 Chapter 3 Flight Protocol,然后创建一个名为 Queue.swift 的新文件。
如何做到这一点...
要为优先队列创建一个协议,请遵循以下步骤:
-
将以下代码添加到
Queue.swift文件中:protocol Queue { typealias ElementType func enqueue(element:ElementType) func dequeue() -> ElementType var size: Int{ get } } -
现在,返回到优先队列并更改其头为以下这个:
class PriorityQueue<T:Comparable>:Queue -
点击播放,当然,结果在视觉上是相同的,但你的代码现在更具可重用性。尝试删除一个方法,比如
enqueue,你就会看到编译器会抱怨缺少协议的方法。
它是如何工作的...
不幸的是,我们无法使用泛型创建协议,但我们可以通过关联类型解决这个问题。你只需要在协议内部创建 typealias 而不指定其实际类型,然后,我们可以使用此类型声明协议的方法。当你从这个协议继承时,你的类型可以是任何东西,甚至是泛型类型 T。
小贴士
当你有像队列、列表或栈这样的概念时,尽量使用协议。然后,你可以有不同的实现,并使用最适合当前场合的最佳实现。
还有更多…
现在你已经学会了如何使用泛型制作可重用代码,你将在下一章中进一步提高,我们将使用 Swift 中的设计模式。
第四章. 使用 Swift 的设计模式
在本章中,我们将涵盖以下主题:
-
写入日志文件
-
创建音乐音符的工厂
-
模拟家庭自动化
-
递送一些披萨
-
协议导向编程
简介
当面向对象编程被引入时,开发者注意到有一些对象或类是按照相同的哲学编程的。
例如,施乐实验室在 70 年代引入了模型-视图-控制器模式,用于使用 SmallTalk 开发程序。施乐还引入了一些其他模式,但它们并没有被这样称呼。
当 1994 年出版了一本名为《设计模式》的书,作者是四人帮,它为常见的工程问题提供了解决方案。它表明软件开发成本的主要问题是维护;使用设计模式会在软件开发的第一阶段造成高昂的成本,但它会显著降低维护成本。
现在,设计模式如此重要,以至于在面试中谈论它们非常普遍。如果你在 Swift 或 Objective-C 方面有经验,你已经在不知不觉中使用了其中的一些模式。
在本章中,我们将介绍几个设计模式;如果可能的话,我们将查看这些模式在 Swift 中的常见示例。
注意
在我们开始之前,我想评论一下,如今设计模式非常值得怀疑;例如,本章“写入日志文件”的食谱中展示的单例模式被一些开发者批评,因为它非常容易实现,这也是一些程序员避免使用这种模式的原因。其他人并不这样认为,他们认为你可以使用它,但只有在正确的时候,正如作者在这篇 URL 中解释的那样:www.ibm.com/developerworks/library/co-single/。无论如何,关于这个话题的争论超出了本书的范围。我将向你展示一些模式,然后你可以决定是否使用它们以及何时使用。
我还想评论的另一个细节是,一些示例可能比没有模式的示例看起来更复杂。别忘了设计模式不是基于简单性,而是基于软件维护。
写入日志文件
这个食谱是关于一个非常简单但也很常见的模式设计:单例模式。这个模式的想法是只有一个实例的对象。你已经在使用 Swift 或 Objective-C 时使用过这个模式,例如,当你使用 UIDevice 或 UIApplication 时。
对于这个食谱,我们将创建一个对象,它将把日志写入文件。请注意,我们在应用程序代码中的位置无关紧要,我们应该只使用一个对象写入一个单独的文件。
准备就绪
由于我们将写入文件,你可能在运行应用程序后想查看其内容。在我们开始之前,让我们检查我们是否能够看到目标文件夹。
如果你使用模拟器,打开 Finder 窗口并转到你的主目录;你可以使用快捷键command + shift + H。如果你看不到名为Library的文件夹,你必须按command + J来显示视图选项。现在,检查显示库文件夹选项,如以下截图所示:

当然,如果你还没有运行应用程序,你就不会有这个应用程序,所以在你viewDidLoad方法中,粘贴代码print(NSHomeDirectory())以了解你的路径,然后你可以跟随它。
小贴士
打开应用程序文档文件夹的一个简单方法是打印主目录并复制它。然后,你可以转到Finder应用程序,按command + shift + G并粘贴你的路径。
如何做到这一点...
让我们创建一个小型计算器并记录用户操作。记住,我们每次使用日志时不需要实例化日志对象;因为它将是一个单例,你可以从任何地方调用它。所以,让我们开始吧。
-
开始一个名为
Chapter 4 Log File的项目,并创建一个名为Log.swift的文件。这里我们将定义我们的日志类。将以下代码复制到文件中:private var myLogInstance:Log = Log() class Log { private var handle:NSFileHandle class func getInstance() -> Log{ return myLogInstance } private init(){ var path:String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String let fullpath = path.stringByAppendingPathComponent("application.log") NSFileManager.defaultManager().createFileAtPath(fullpath, contents: nil, attributes: nil) self.handle=NSFileHandle(forWritingAtPath:fullpath)! } private func getCurrentTime() -> String{ let date = NSDate() let calendar = NSCalendar.currentCalendar() let components = calendar.components(.CalendarUnitHour | .CalendarUnitMinute | .CalendarUnitSecond, fromDate: date) let hour:Int = components.hour let minutes = components.minute let seconds = components.second return String(format: "%02d:%02d:%02d",hour, minutes, seconds) } func info(message:String){ let finalMessage = "INFO: \(self.getCurrentTime()):\(message)\n" handle.writeData(finalMessage.dataUsingEncoding (NSUTF8StringEncoding, allowLossyConversion: false)!) handle.synchronizeFile() } func error(message:String){ let finalMessage = "ERROR:\(self.getCurrentTime()):\(message)\n" handle.writeData(message.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!) } deinit{ handle.closeFile() } } -
现在,当然,我们需要完成我们的应用程序以检查我们代码的使用。转到故事板,并添加两个文本字段(每个字段将代表一个数字)、一个表示当前操作符的分段控件、一个显示结果的按钮,以及一个显示结果的标签。你的布局应类似于以下之一:
![如何做到这一点…]()
-
现在,让我们编写视图控制器。首先,让我们添加一些属性:
var chosenOperator:Character = "+" @IBOutlet var firstNumber: UITextField! @IBOutlet var labelResult: UILabel! @IBOutlet var secondNumber: UITextField! -
不要忘记将每个图形组件与其属性链接。现在,是我们编写程序方法的时候了;在这种情况下,我们将开发一个保存所选操作符的方法,以及一个显示结果的方法:
@IBAction func operatorChanged(sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case 0: chosenOperator = "+" case 1: chosenOperator = "-" case 2: chosenOperator = "*" case 3: chosenOperator = "/" default: Log.getInstance().error("Invalid value \(sender.selectedSegmentIndex)") return } Log.getInstance().info("User has chosen the following operator: \(chosenOperator)") } @IBAction func displayResult(sender: UIButton) { var number1:Double? var number2:Double? number1=(firstNumber.text as NSString).doubleValue number2=(secondNumber.text as NSString).doubleValue switch chosenOperator{ case "+": labelResult.text = "\(number1! + number2!)" Log.getInstance().info("\(number1!) + \(number2!) = \(number1! + number2!)") case "-": labelResult.text = "\(number1! - number2!)" Log.getInstance().info("\(number1!) - \(number2!) = \(number1! + number2!)") case "*": labelResult.text = "\(number1! * number2!)" Log.getInstance().info("\(number1!) * \(number2!) = \(number1! + number2!)") case "/": if number2! == 0.0 { Log.getInstance().error("Trying to divide by zero") let alert = UIAlertController(title: "Error", message: "Cant divide by zero", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) }else { labelResult.text = "\(number1!)/\(number2!)" Log.getInstance().info("\(number1!) / \(number2!) = \(number1! + number2!)") } default: break; } } -
为了完成我们的应用程序,我们应在应用程序代理上添加一些代码。填充这些事件将在用户打开应用程序、将应用程序置于后台以及返回到应用程序时注册。以下是代码:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { print(NSHomeDirectory()) Log.getInstance().info("Application has started") return true } func applicationDidEnterBackground(application: UIApplication) { Log.getInstance().info("Application has gone to background") } func applicationDidBecomeActive(application: UIApplication) { Log.getInstance().info("Application has become active") } -
现在,点击播放并测试程序;然后,按住主页按钮并返回到应用程序。完成操作后,前往应用程序文档文件夹,如开头所示,并打开文件
application.log。请注意,所有操作都记录在同一个文件中;无论是应用程序代理还是视图控制器产生的事件,都没有关系。
它是如何工作的...
在这个菜谱中,我们将看到访问控制的介绍。Swift 有三个访问级别:
-
公共:在这个级别,对象、属性或全局变量可以从任何地方使用,甚至可以从另一个模块
-
内部:在这个级别,相应的实体可以从任何地方访问,除了另一个模块
-
私有:在这个级别,实体只能从当前文件访问,即使是同一个模块
如您所见,单例模式的思想是确保一个类只有一个实例。由于类的初始化器是私有的,它不能从任何地方调用,只能从当前文件中的方法或函数中调用。
因此,我们创建了一个名为getInstance的方法来访问我们拥有的唯一实例,然后我们可以访问对象方法。请注意,我们必须使用类;如果我们使用结构体,就会破坏只有一个对象的原则。
注意
有时,你会看到单例的实现接受 nil 值,例如private var myLogInstance:Log?,然后在getInstance方法中初始化它,例如:
if myLogInstance == nil { myLogInstance = Log() } return myLogInstance
原因是一些软件架构师认为,你不需要在启动应用程序时实例化每个单例对象,除非你打算使用它。有一些单例对象永远不会被调用,你不应该浪费这些内存。如果你注意到,这个类还有一个析构器。从技术上讲,如果你在 iOS 上运行应用程序,这个方法不会被调用,因为 iOS 应用程序通常不会结束。然而,在特定情况下,应用程序仍然可以结束,你可能会正确地关闭文件句柄。
现在,当我们谈论文件使用时,首先,我们必须知道应用程序文档文件夹的路径,因为这是我们选择写入日志文件的位置。有一个名为NSSearchPathForDirectoriesInDomains的函数,它返回请求文件夹的完整路径(实际上,它返回一个路径数组)。在调用此函数后,我们可以使用文件管理器创建文件,并使用NSFileHandle打开它。
小贴士
不要以这种方式通过连接家目录路径来使用路径:NSHomeDirectory() + /Documents。苹果可能会像在 iOS 8 中对捆绑包所做的那样,在未来版本中更改其路径。
如果这个类不是单例,你不会在这里打开这个文件;每次你需要写入消息时,你都会打开和关闭它,因为你必须避免为同一文件打开两个句柄。打开和关闭句柄是一个缓慢的操作;它可能会影响你的应用程序性能。
在高频率写入日志文件的情况下,你必须避免文件写入或打开时的冲突,但如果你有一个单例,这个问题就更容易控制。
要完成这个菜谱,我想评论一下,这个简单的日志系统是基于应用程序中使用的真实日志系统。通常,日志文件会尝试记录日志级别,如 info、error、warning 或 debug,以及时间。有了这些信息,当日志文件变大时,你可以过滤日志,并在应用程序崩溃时找出发生了什么。
还有更多…
我们为单例应用程序找到的解决方案是将对象实例保存在全局变量中;对于这种模式来说,理想的做法是将它保存在类变量中,也称为静态属性。Swift 2 引入了对静态属性的支持,所以我们不再需要担心这个问题。
创建音乐音符的工厂
使用计算机创作音乐现在是非常常见的。创建允许音乐家创作自己音乐的软件看起来很简单,但实际上并不简单,主要是因为每个音符都有很多可能性。
在这个食谱中,我们将使用 抽象工厂 模式。这个模式将允许我们更改我们想要创建的音符类型,并且它也会为我们初始化音符类型。
如你所知,有很多音符符号;如果你想了解更多,可以查看维基百科上的这个链接:en.wikipedia.org/wiki/List_of_musical_symbols。
然而,对于这个食谱,我们将处理三种类型的音符:鼓的四分音符、钢琴的四分音符和四分休止符。当然,这只是一个例子;在一个真正的程序中,你可能需要完成它,比如添加连音等。
准备工作
创建一个名为 Chapter 4 Musical Notes 的项目;现在,从互联网上下载与此食谱对应的图片。在这种情况下,我们有这些图片:staff.png、quarter_rest.png、cnote.png、dnote、cdrum.png 和 ddrum.png。
我们还需要为这个食谱准备一些 MP3 声音。下载三个钢琴音符的声音和另外三个鼓点的声音;当然,对于其他音符我们不会有声音。
将下载的图片放入你的 images.xcassets 文件夹中。如果你愿意,你也可以添加不同分辨率的相同图片,以便在不同的分辨率设备(iPad 和 iPhone)上使用。
在你开始之前,我们只需要添加一个名为 AVFoundation 的框架。这将使我们的应用程序能够播放声音。为此,只需点击项目导航器,然后点击构建阶段。之后,展开 链接二进制与库 部分,然后点击加号按钮。选择 AVFoundation 并点击 添加。
如何做到这一点...
-
如同往常,我们将从模型开始。首先,让我们创建一个音符协议,因为我们知道将来可能会有多种类型的音符,我们应该为此做好准备。因此,创建一个名为
NoteProtocol.swift的新文件,并将以下代码放入其中:import Foundation import UIKit enum NoteStep { case NOTE_C, NOTE_D, NOTE_E } protocol NoteProtocol { var sound:String?{ get set } var image:UIImage?{ get set } var step: NoteStep { get set } var location:CGPoint { get set } func play() } -
下一步是创建这个协议的实现。创建一个名为
MusicalNote.swift的文件,并将以下内容添加到其中:import Foundation import UIKit import AVFoundation class MusicalNote: NoteProtocol{ lazy private var _player = AVAudioPlayer() private var _sound:String? var sound:String? { get { return _sound } set(newSound){ self._sound = newSound } } private var _image:UIImage? var image:UIImage?{ get{ return _image} set(newImage){ self._image = newImage } } private var _step:NoteStep var step: NoteStep { get{ return _step } set(newStep){ self._step = newStep } } private var _location:CGPoint var location:CGPoint { get { return _location } set(newLocation){ self._location = newLocation } } func play(){ if let mySound = _sound { var urlSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(mySound, ofType: "mp3")!) self._player = AVAudioPlayer(contentsOfURL: urlSound, error: nil) self._player.prepareToPlay() self._player.play() } } init(_ step:NoteStep = .NOTE_C){ self._location = CGPointZero self._step = step } } -
现在我们已经实现了音符类,请确保这个音符不是针对钢琴或鼓的特定音符;我们只需要根据音符类型和步长以不同的方式构建它。因此,现在我们需要定义一个音符工厂。使用我们之前应用的相同逻辑,我们现在需要创建一个音符协议。
我们将定义的唯一方法是
createNote,它需要知道音符步长(C、D 或 E)及其在乐谱上的位置。是时候创建一个名为AbstractNoteFactory.swift的新文件,并输入以下代码:protocol AbstractNoteFactory { func createNote(noteStep:NoteStep, order:Int) -> NoteProtocol } -
一旦我们有了音符工厂的定义,我们就可以开始创建我们自己的工厂。让我们从最简单的一个开始:
SilenceFactory;这个工厂将只创建一种音符,无论其步长如何。在名为SilenceFactory.swift的文件中输入以下代码:import UIKit class SilenceFactory: AbstractNoteFactory { func createNote(noteStep:NoteStep, order:Int) -> NoteProtocol{ var note = MusicalNote(noteStep) note.image = UIImage(named: "quarter_rest.png") note.sound = nil note.step = noteStep var x = CGFloat(120) + CGFloat(40 * order) note.location = CGPointMake(x, 25) return note } } -
使用这个类,我们将为创建新的静音笔记节省一些步骤。按照相同的逻辑,让我们创建钢琴工厂和鼓工厂。使用以下代码创建钢琴工厂:
class PianoNoteFactory: AbstractNoteFactory { func createNote(noteStep:NoteStep, order:Int) -> NoteProtocol{ var note = MusicalNote(noteStep) note.step = noteStep var x:CGFloat = CGFloat(120.0) + CGFloat(40.0) * CGFloat(order) switch noteStep { case .NOTE_C: note.image = UIImage(named: "cnote.png") note.location = CGPointMake( CGFloat(x), 57) note.sound = "piano_c" case .NOTE_D: note.image = UIImage(named: "dnote.png") note.location = CGPointMake( CGFloat(x), 44) note.sound = "piano_d" case .NOTE_E: note.image = UIImage(named: "dnote.png") note.location = CGPointMake( CGFloat(x), 36) note.sound = "piano_e" } return note } } -
现在,让我们创建鼓工厂:
class DrumNoteFactory: AbstractNoteFactory { func createNote(noteStep:NoteStep, order:Int) -> NoteProtocol{ var note = MusicalNote(noteStep) note.step = noteStep var x:CGFloat = CGFloat(120.0) + CGFloat(40.0) * CGFloat(order) switch noteStep { case .NOTE_C: note.image = UIImage(named: "cdrum.png") note.location = CGPointMake( CGFloat(x), 57) note.sound = "bighit" case .NOTE_D: note.image = UIImage(named: "ddrum.png") note.location = CGPointMake( CGFloat(x), 46) note.sound = "cymbal" case .NOTE_E: note.image = UIImage(named: "ddrum.png") note.location = CGPointMake( CGFloat(x), 38) note.sound = "hithat" } return note } } -
好的,现在是时候创建我们的布局了。对于这个食谱,我们需要添加一个乐谱(图像视图),这是音符将显示的地方,一个分段控制器,它将允许我们选择我们想要的音符工厂,以及一个在我们创建 10 个音符后出现的播放按钮,它将允许我们听到我们的音乐。
![如何做到这一点…]()
-
在音乐完全创作完成之前,我们不会允许用户播放音乐。因此,播放按钮必须一开始是不可见的。要做到这一点,在故事板上将按钮添加到视图中后,点击它,转到属性检查器,并选择隐藏选项。
-
正如您所知,我们现在必须将分段控制器和按钮与其属性链接。让我们也添加这个应用所需的其他属性:
@IBOutlet var segmentedControl: UISegmentedControl! @IBOutlet var playButton: UIButton! @IBOutlet var staff: UIImageView! var notes = [NoteProtocol]() var factory:AbstractNoteFactory = SilenceFactory() var timer:NSTimer? var pos = 0笔记
在这种情况下,我们必须指定工厂类型为
AbstractNoteFactory;如果不这样做,它将被声明为SilenceFactory,并且不允许我们更改工厂类型。 -
让我们添加在乐谱上点击并添加音符的可能性。为此,我们将在
viewDidLoad中添加以下代码:override func viewDidLoad() { super.viewDidLoad() let recognizer = UITapGestureRecognizer(target: self, action:Selector("handleTap:")) self.view.addGestureRecognizer(recognizer) } -
如您所见,每次我们点击屏幕时,我们都需要检查它是否在可以添加笔记的位置。让我们实现这个手势动作:
func handleTap(recognizer:UITapGestureRecognizer) { let point = recognizer.locationInView(staff) var noteStep:NoteStep = .NOTE_C switch point.y { case 105...125: noteStep = .NOTE_C case 95...105: noteStep = .NOTE_D case 80...95: noteStep = .NOTE_E default: return } var note = factory.createNote(noteStep, order: notes.count) notes.append(note) var imageView = UIImageView(frame: CGRect(origin: note.location, size: note.image!.size)) imageView.image = note.image staff.addSubview(imageView) if notes.count == 10 { self.segmentedControl.hidden = true self.playButton.hidden = false } } -
如您所见,当我们点击屏幕时,我们只是请求一个新的音符;当前是哪个工厂并不重要。现在,分段控制器将在用户想要时更改当前工厂:
@IBAction func changeFactory(sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case 0: factory = SilenceFactory() case 1: factory = PianoNoteFactory() case 2: factory = DrumNoteFactory() default: break; } } -
为了完成这个应用,我们必须为播放按钮创建一个事件。由于我们不会为每个音符按下按钮,我们将初始化计时器,并在半秒后创建一个播放每个音符的方法:
@IBAction func playMusic(sender: UIButton) { playButton.enabled = false timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("playNote"), userInfo: nil, repeats: true) timer?.fire() } func playNote(){ notes[pos].play() pos++ if pos >= notes.count { timer?.invalidate() pos = 0 playButton.enabled = true } } -
现在,点击播放并创作你的音乐。
它是如何工作的…
抽象工厂模式使我们免于在实例化对象后执行许多步骤。作为程序员,我们不需要担心对象需要使用的类,只需要基类;在这种情况下,它创建了一个NoteProtocol类型的对象。
由于每个工厂都实现了相同的协议,我们不需要检查当前是哪个工厂。我们还可以创建新的工厂,代码更改也不会痛苦。
还有更多…
我们将创建另一个创建型模式,这将允许我们创建智能家居模拟器。
模拟智能家居
技术每天都在变得越来越受欢迎;很快我们甚至可以从手机上控制我们的门锁。然而,它是如何工作的呢?想象一下,当你走进客厅时,一个传感器可以检测到有人在,它会打开灯。此外,如果气压计检测到将要下雨,它可以关闭家里的窗户。这里提到的例子是改变状态并通知其他对象这一变化的对象的好样本。对于这种情况,我们将使用模式 Observer,也称为 发布-订阅 模式。
在这个菜谱中,我们将创建一个只有两个窗户、一个门锁和一个晾衣绳的家居样本。为了简化,应用程序将从文件中读取传感器信息。
当我们收到雷达信息时,可能有人正在接近你的家。在这种情况下,门锁必须上锁,窗户应该关闭,或者当这个人离开时,窗户可以再次打开。此外,当气压计检测到将要下雨时,窗户必须关闭,晾衣绳必须收集衣物。当雨停了,这些机器人可以执行相反的动作。
准备工作
让我们创建一个名为 Chapter 4 Observer 的新项目。我最初的想法是将动作列表创建到一个文件中,但随着几个动作的完成,这将是足够的;我们将创建一个包含动作的数组。
如何做…
-
首先,让我们创建最简单的模型,在我们的例子中是门锁。基本上,我们在这里需要做的唯一一件事是存储其状态,可以是开启或关闭。因此,创建一个名为
DoorLock.swift的文件,并添加以下代码:class DoorLock { enum Status { case OPENED, CLOSED } private var _status = Status.OPENED var status:Status { return _status } func open(){ _status = .OPENED } func close(){ _status = .CLOSED } } -
完成前面的代码后,我们可以对晾衣绳做类似的事情,所以将以下代码放入一个新文件
ClothesLine.swift中:class ClothesLine { enum Status { case COLLECTED, LINED } private var _status = Status.LINED var status:Status { return _status } func collect(){ _status = .COLLECTED } func line(){ _status = .LINED } } -
好的,现在我们需要我们的最后一个配件,那就是窗户。在这种情况下,我们必须存储窗户关闭的次数。想象一下,两个人接近我们的房子,其中一个人离开了;我们必须保持窗户关闭,因为还有另一个人仍然靠近我们的房子。因此,创建一个名为
Window.swift的文件,并添加以下代码:class Window { enum Status { case OPENED, CLOSED(times:Int) } private var _status = Status.OPENED var status:Status { return _status } func open(){ switch _status{ case .CLOSED(var times): times = times - 1 if times == 0{ _status = .OPENED }else{ _status = .CLOSED(times:times) } default: _status = .OPENED } } func close(){ switch _status{ case .CLOSED(var times): times = times + 1 _status = .CLOSED(times:times) default: _status = .CLOSED(times:1) } } } -
好的,现在是我们创建雷达的时候了。记住,雷达需要存储一些将通知状态变化的对象,所以我们将创建一个嵌套类
RadarObserver。开始将这个类添加到一个新文件Radar.swift中,如下所示:class Radar{ class RadarObserver{ var onSomeoneAproaches: ()->Void var onSomeoneHasGoneAway: () -> Void init(){ self.onSomeoneAproaches = { () -> Void in } self.onSomeoneHasGoneAway = {() -> Void in } } } var observers = [RadarObserver]() func addObserver(observer: RadarObserver){ observers.append(observer) } -
这个功能可以添加,因为我们正在创建一个嵌套类;Swift 不允许我们创建嵌套协议。现在,创建存储或删除观察者的方法。在这个菜谱中,我们不会删除任何观察者,但正如我之前告诉你的,总是为未来做好准备。
-
这个类的最后一部分是改变对象状态的方法:
func detectedSomeone(){ for observer in observers { observer.onSomeoneAproaches() } } func someoneHasGoneAway(){ for observer in observers { observer.onSomeoneHasGoneAway() } } } -
一旦我们理解了它,我们就可以根据我们在这里使用的哲学创建气压计:
class Barometer{ class BarometerObserver{ var onItsGoingToRain:() -> Void var onRainHasFinished:() -> Void init() { self.onItsGoingToRain = { () -> Void in } self.onRainHasFinished = { () -> Void in } } } private var observers = [BarometerObserver]() func addObserver(observer: BarometerObserver){ observers.append(observer) } func removeObserver(observer:BarometerObserver){ var index: Int? for (i,object) in observers.enumerate(){ if object === observer{ index = i break } } if let indexFound = index{ observers.removeAtIndex(indexFound) } } func detectedRain(){ for observer in observers { observer.onItsGoingToRain() } } func detectedNoRain(){ for observer in observers { observer.onRainHasFinished() } } } -
好的,现在我们可以创建我们的显示界面了。在这个菜谱中,我们将添加五个标签,每个标签对应一个配件,还有一个标签用于显示最后执行的动作。我们还需要一个按钮来开始模拟接收雷达和气压计事件。您将看到一个类似于以下视图的界面:
![如何做…]()
-
现在,让我们将这些组件与相应的属性链接起来:
@IBOutlet var labelWindow1: UILabel! @IBOutlet var labelWindow2: UILabel! @IBOutlet var labelDoorLock: UILabel! @IBOutlet var labelClothesLine: UILabel! @IBOutlet var labelLastAction: UILabel! @IBOutlet var buttonStart: UIButton! -
现在,让我们转到视图控制器,并开始完成属性。在这种情况下,我们需要一个雷达、一个气压计、两个窗户、一个门锁、一个晾衣绳、一个将要执行的动作列表和一个计时器,以便使模拟对眼睛更容易:
var radar = Radar() var barometer = Barometer() var window1 = Window() var window2 = Window() var doorLock = DoorLock() var clothesLine = ClothesLine() var actions=[String]() var timer:NSTimer? -
现在,让我们创建一个私有函数来更新标签。这个函数是一个辅助函数,所以我们不需要在每个观察者动作中重复这段代码:
private func updateLabels(){ switch self.window1.status { case .CLOSED: self.labelWindow1.text = "Window 1 Status: CLOSED" default: self.labelWindow1.text = "Window 1 Status: OPENED" } switch self.window2.status { case .CLOSED: self.labelWindow2.text = "Window 2 Status: CLOSED" default: self.labelWindow2.text = "Window 2 Status: OPENED" } switch self.doorLock.status { case .CLOSED: self.labelDoorLock.text = "Door Lock Status: CLOSED" default: self.labelDoorLock.text = "Door Lock Status: OPENED" } switch self.clothesLine.status { case .COLLECTED: self.labelClothesLine.text = "Clothes Status: LINED" default: self.labelClothesLine.text = "Clothes Status: COLLECTED" } } -
接下来,在我们的
viewDidLoad方法中,我们将添加观察者的代码。以下代码只创建了前两个观察者;您必须按照相同的规则完成它,因为展示所有这些会非常繁琐:override func viewDidLoad() { super.viewDidLoad() var radarObserver = Radar.RadarObserver() radarObserver.onSomeoneAproaches = { () -> Void in self.window1.close() self.updateLabels() } radarObserver.onSomeoneHasGoneAway = { () -> Void in self.window1.open() self.updateLabels() } radar.addObserver(radarObserver) radarObserver = Radar.RadarObserver() radarObserver.onSomeoneAproaches = { () -> Void in self.window2.close() self.updateLabels() } radarObserver.onSomeoneHasGoneAway = { () -> Void in self.window2.open() self.updateLabels() } radar.addObserver(radarObserver) } … -
现在,我们可以完成创建模拟器,按钮的事件,它将初始化动作和计时器,以及一个将被计时器调用并执行相应动作的函数:
func tick(){ var action = actions.first actions.removeAtIndex(0) if actions.count == 0 { buttonStart.enabled = true timer?.invalidate() } if action == "someoneapproches"{ radar.detectedSomeone() }else if action == "someoneleaves"{ radar.someoneHasGoneAway() }else if action == "startrainning" { barometer.detectedRain() }else if action == "rainfinishes" { barometer.detectedNoRain() } labelLastAction.text = action } @IBAction func start(sender: AnyObject) { buttonStart.enabled = false actions = ["someoneapproches","someoneleaves", "someoneapproches","someoneapproches", "startrainning","someoneleaves","someoneleaves", "rainfinishes"] timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("tick"), userInfo: nil, repeats: true) timer!.fire() } -
现在完成了!点击播放并观看它的工作。
它是如何工作的…
如您所见,观察者的主要目标是执行一个动作,而为了做到这一点,我们使用了闭包。这个 Swift 特性在 Objective-C 中相当于 blocks,在 JavaScript 中相当于函数变量。闭包知道对象是在哪里创建的,即使它被存储在另一个对象中,也能访问其属性。
在我们的菜谱中,当雷达或气压计检测到不同的情况时,它将通知所有订阅了它的观察者。每个观察者都会对相应的配件(窗户、门锁或称为晾衣绳的东西)进行操作。请注意,如果我们想编写传统的对象方法,将需要创建新的类,这些类从同一个观察者继承,并存储它将要使用的属性。这不会是一个糟糕的实现,但使用闭包要容易得多,也更灵活。
如果您是 Martin Fowler 的粉丝,您可能已经注意到这里的一些代码与气压计观察者和雷达观察者非常相似。这就是 Mr. Martin 所说的代码的“味道”,意思是代码不需要在技术上出错,仍然可能很糟糕。
在这种情况下,这将是正确的,但这是一个例子。记住,在现实生活中,雷达可能需要通知您入侵者的位置,气压计需要通知您降水情况,这使得通知者的方法不兼容。
还有更多…
我们已经学习了行为模式和创建模式。在下一个菜谱中,我们将使用结构模式,基于新特性创建新的对象类型。
送一些披萨
假设您必须创建一个具有边框的新窗口类型;第一个想法是从一个窗口类借用的,创建一个名为 BorderedWindow 的新类。您可以用相同的方式处理滚动条;称之为 ScrolledWindow。现在,如果我们需要创建一个具有滚动和边框的窗口,我们必须创建一个名为 ScrolledAndBorderedWindow 的新类,但想象一下,我们现在需要添加双边框和三边框的窗口,这将有很多组合。
为了防止这类问题,有一个名为 装饰器 的模式;这个模式允许我们根据新特性创建新的对象类型。
准备工作
创建一个名为 Chapter 4 Pizzas 的新项目,并将披萨图片添加到 Images.xcassets 中。现在,让我们预热烤箱并准备一些披萨。
如何做…
让我们执行这些步骤来送披萨:
-
首先,让我们创建一个基类,定义披萨,因此创建一个名为
Pizza.swift的新文件,并创建以下类:class BasePizza { private var _price:Double var price:Double { get { return _price } } var name:String init(name:String, price:Double){ self.name = name self._price = price } } -
如您所见,这个类存储价格和披萨或配料名称。现在,我们可以定义一些披萨及其价格:
class SimplePizza : BasePizza{ init() { super.init(name: "SimplePizza", price: 4.50) } } class Peperoni: BasePizza{ init() { super.init(name: "Peperoni", price: 7.50) } } class ChickenFiesta:BasePizza { init() { super.init(name: "ChickenFiesta", price: 7.50) } } -
创建一个名为
PizzaDecorators.swift的新文件;这是我们创建额外配料的地方。首先,我们需要创建一个定义披萨装饰器的类:class BasePizzaDecorator:BasePizza { var decoratedPizza:BasePizza? init(name:String, price:Double, decoratedPizza:BasePizza){ self.decoratedPizza = decoratedPizza super.init(name: name, price: price) } override var price:Double { get { return super.price + decoratedPizza!.price } } } -
现在,让我们添加一些额外配料。在这种情况下,我们将有墨西哥辣椒、奶酪、蘑菇和橄榄:
class Jalapeño:BasePizzaDecorator { init(decoratedPizza:BasePizza){ super.init(name: "Jalapeño", price: 1.20, decoratedPizza: decoratedPizza) } } class Cheese:BasePizzaDecorator { init(decoratedPizza:BasePizza){ super.init(name: "Cheese", price: 1.30, decoratedPizza: decoratedPizza) } } class Mushroom:BasePizzaDecorator { init(decoratedPizza:BasePizza){ super.init(name: "Mushroom", price: 1.10, decoratedPizza: decoratedPizza) } } class Olive:BasePizzaDecorator { init(decoratedPizza:BasePizza){ super.init(name: "Olive", price: 1.10, decoratedPizza: decoratedPizza) } } -
好的!完成这个任务后,我们可以创建视图。我们将添加一个标签来显示总数,四个按钮用于添加额外配料,一个文本视图来显示我们已添加到披萨中的配料,以及一个 Image View 来让我们的应用程序更愉快。最后,我们应该有一个类似于这里所示的布局:
![如何做…]()
-
现在,我们可以创建视图控制器。让我们从属性开始:
var myDeliciousPizza: BasePizza? @IBOutlet var totalLabel: UILabel! @IBOutlet var ingredientsList: UITextView! -
让我们要求用户选择他想要的披萨。在这种情况下,我们需要在
viewDidAppear中做这件事,因为我们将通过操作表来做,这个组件在viewDidLoad方法中不起作用,因为视图还没有准备好显示操作表:override func viewDidAppear(animated: Bool) { var choosePizzaType = UIAlertController(title: "Pizza", message: "Choose your pizza", preferredStyle: .ActionSheet) choosePizzaType.addAction(UIAlertAction(title: "Simple", style: .Default, handler: { (action) in self.myDeliciousPizza = SimplePizza() self.refreshPrice() })) choosePizzaType.addAction(UIAlertAction(title: "Peperoni", style: .Default, handler: { (action) in self.myDeliciousPizza = Peperoni() self.refreshPrice() })) choosePizzaType.addAction(UIAlertAction(title: "Chicken Fiesta \u{1F389}", style: .Default, handler: { (action) in self.myDeliciousPizza = ChickenFiesta() self.refreshPrice() })) self.presentViewController(choosePizzaType, animated: true){ } } -
我们需要创建一个名为
refreshPrice的方法;它就像这样简单:func refreshPrice(){ self.totalLabel.text = "Total: \(myDeliciousPizza!.price)" } -
为了完成我们的应用程序,我们必须添加属于按钮的事件;别忘了将它们链接起来:
@IBAction func addJalapeño(sender: UIButton) { myDeliciousPizza = Jalapeño(decoratedPizza: myDeliciousPizza!) self.ingredientsList.text = self.ingredientsList.text + "Jalapeño\n" self.refreshPrice() } @IBAction func addCheese(sender: UIButton) { myDeliciousPizza = Cheese(decoratedPizza: myDeliciousPizza!) self.ingredientsList.text = self.ingredientsList.text + "Cheese\n" self.refreshPrice() } @IBAction func addOlives(sender: UIButton) { myDeliciousPizza = Olive(decoratedPizza: myDeliciousPizza!) self.ingredientsList.text = self.ingredientsList.text + "Olive\n" self.refreshPrice() } @IBAction func addMushrooms(sender: UIButton) { myDeliciousPizza = Mushroom(decoratedPizza: myDeliciousPizza!) self.ingredientsList.text = self.ingredientsList.text + "Mushrooms\n" self.refreshPrice() } -
现在应用程序已经完成,按播放,选择披萨,添加一些配料,享受您的餐点。
它是如何工作的…
装饰器模式允许我们根据另一个对象创建新的对象,这对于防止基于特性组合创建无序数量的类非常有用。
参见
- 还有更多的模式。你可以在维基百科上查看它们(
en.wikipedia.org/wiki/Software_design_pattern)。检查哪些最适合你的项目。还有一个名为反模式的功能,它解释了常见的坏做法。在下一章中,我们将学习如何使用 Swift 处理并发代码,这在当今非常常见,尤其是如果你喜欢开发游戏。
面向协议编程
在 Swift 的第一个版本中,你可以使用扩展来扩展类、结构和枚举。这是一个非常强大的功能,允许开发者根据应用程序的需求对现有类型进行微调。使用 Swift 2.0,Apple 包含了一个新特性:协议扩展。很容易只把它看作是一个小特性;然而,它具有改变你编写代码方式的力量。Apple 将这种新范式称为面向协议编程。在这个食谱中,我们将学习如何利用这个新特性以及与面向对象编程相比的一些新模式。
准备工作
对于这个食谱,创建一个新的游乐场,命名为 第四章 协议。我们将只为这个食谱使用游乐场,所以不用担心任何项目设置。
如何做…
让我们先添加一些代码:
-
首先,我们将创建两个协议和一个符合这两个协议的结构体,如下所示:
protocol Flyable { var topSpeed: Int { get } } protocol Transportable { var seatCount: Int { get } } struct Plane: Flyable, Transportable { var topSpeed = 650 var seatCount = 220 } -
使用面向对象编程,我们可以在需要时创建一个包含所有属性的单个超类和子类。然而,这将导致超类更加复杂,每个子类都会包含它不需要的属性/功能。通过将
Flyable和Transportable定义为协议,我们可以让自定义数据类型从多个来源继承,而不是单个超类。现在我们已经有一些基本的协议,是时候真正扩展它们了。让我们扩展Flyable协议来检查另一个Flyable对象是否更快。将以下代码添加到你的游乐场中:extension Flyable { func isFasterThan(item: Flyable) -> Bool { return self.topSpeed > item.topSpeed } } let commercialPlane = Plane() let jetPlane = Plane(topSpeed: 850, seatCount: 20) commercialPlane.isFasterThan(jetPlane)注意
另一个与协议扩展相关的新特性也很重要:默认行为。如果你查看我们之前的实现,你会注意到我们定义了一个方法而不是声明一个。标准协议要求符合的对象定义每个方法,但使用 Swift 2.0,我们可以提供一个默认实现,这样其他类就不必这样做。
-
让我们创建另一个扩展,但仅针对也符合 Transportable 协议的类型。将以下代码添加到你的游乐场中:
extension Flyable where Self: Transportable { func containsMorePassengers(item: Self) -> Bool { return self.seatCount > item.seatCount } } commercialPlane.containsMorePassengers(jetPlane)带有大写
S的Self关键字表示符合协议的类或结构体。在这种情况下,它意味着Plane类。使用这个简单的组合,你可以看到协议扩展对于自定义类型是多么有用。协议扩展的真正力量来自于你扩展 Swift 标准库的能力。让我们看看。注意
使用协议扩展功能与 Swift 标准库一起,我们可以向现有的协议(如
Equatable和CollectionType(数组和解构))添加功能。 -
让我们给
CollectionType添加功能,以获取数组中Flyable类型对象的平均速度。将以下代码添加到你的 playground 中:extension CollectionType where Self.Generator.Element: Flyable { func averageTopSpeed() -> Int { var total = 0, count = 0 for item in self { total += item.topSpeed count++ } return (total/count) } } let planes = [commercialPlane, jetPlane, Plane()] planes.averageTopSpeed()
这个协议扩展定义了averageTopSpeed方法。实现相当简单。我们定义了一个计数和总变量,然后遍历集合中的每个项目,并在增加计数的同时将其添加到总和中。我们必须手动增加计数,因为CollectionType的计数属性类型与Int不同。
它是如何工作的…
由于协议提供了更多的灵活性,你可以创建更精细的代码来满足每个应用程序的需求。为了获得最大的功能,利用自定义类型协议扩展以及扩展 Swift 标准库扩展。
注意我们是如何为Plane创建一个结构体而不是一个对象的。这突显了面向对象编程中涉及对象引用的一个问题。
想象我们有两个对象:A 和 B。A 创建了一些数据集,然后通过引用将此数据与对象 B 共享。这意味着两个不同的对象正在引用相同的数据对象。B 随后更改了这些数据中的一些。现在,对象 A 可能会发现一些它不知道如何处理的数据变化。这似乎是一个小问题,但可能会在简单应用中引起许多问题。
在 Swift 中,结构体是通过值传递而不是引用传递的。在我们的上一个例子中,对象 A 会复制数据而不是传递其引用,因此每个对象都会有自己的副本,不会干扰其他对象。
参见
- 如果你想了解更多或更好地理解为什么你想使用协议扩展和面向协议的编程,请查看 WWDC 2015 的视频,标题为Swift 中的面向协议编程(
developer.apple.com/videos/play/wwdc2015-408/)
第五章. 在你的应用中进行并行处理
在本章中,我们将介绍以下食谱:
-
使用线程进行单词计数
-
创建一个 SEO 应用
-
创建 CycloneChecker 应用
-
检查我们网站的链接
简介
现在,并行处理非常普遍,因为一个应用程序可以同时做很多事情。如果我们有一个多核处理器,比如 iPhone 4S 或更新的型号,或者 iPad 2 或更新的型号,你甚至可以提高性能。当然,你不应该为所有事情都使用并行处理,因为不断地从一个任务切换到另一个任务可能会损害性能。
在本章中,我们将学习不同类型的并行处理以及何时应该使用每种类型。首先,我们将从线程开始,然后我们将看到 NSOperations,最后我们将了解如何使用 GCD。
使用线程进行单词计数
有时候,如果你不关注并行处理,可能会导致屏幕冻结。作为一个开发者,了解应用程序中发生的事情,我们可以等待,但作为一个对应用程序操作一无所知的用户,他们可能会认为他们的应用已经挂起,需要重新启动。
在这个食谱中,我们将创建另一个线程来防止这种情况。创建线程也可以是一种在不同轨道上划分任务的机制,使得调试更容易。
假设我们需要计算一个文档中的单词数量。我们将有两个按钮,一个将不使用线程来完成这个任务,另一个将使用线程来完成相同的任务。
准备工作
在你开始之前,你必须有一个文本文件。你可以使用任何你想要的文本文件,但要得到一个好的结果,你应该有一个很大的文件,比如超过 30K。一个建议是下载任何协议的 RFC。
因此,创建一个名为Chapter 5 Thread的新项目,添加一个名为WordCounter.swift的文件,然后让我们开始编码。
如何做到这一点...
在这个食谱中,我们将首先从互联网上下载一个文件,然后我们将按照以下步骤处理它:
-
在你的电脑上打开你喜欢的网页浏览器,下载这个文本文件:
www.ietf.org/rfc/rfc2821.txt。 -
将文档拖入你的项目,最好是拖入你的支持文件组。
![如何做到这一点…]()
-
现在,点击
WordCounter.swift文件,创建一个具有相同名称的类:class WordCounter:NSObject { private var file:String lazy private var _words = [String:Int]() var words:[String:Int] { return _words } -
现在,让我们创建一个初始化器;正如你所见,我们唯一需要初始化的属性是文件属性:
init(file:String){ super.init() self.file = file } -
下一步是编写执行方法。在这里,我们将打开我们的文件并计算单词数量。当然,你可以改进这个方法,但对我们来说现在应该足够了:
func execute(){ _words = [String:Int]() // First step opening the file var manager = NSFileManager.defaultManager() var data = manager.contentsAtPath(NSBundle.mainBundle().pathForResource(file, ofType: "txt")!)! var content = String(data: data, encoding:NSUTF8StringEnconding)! // spliting the documento into words var wordsArray = content.componentsSeparatedByString(" ") wordsArray = wordsArray.map({ (word) -> String in return word.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()) }) .map({ (word) -> String in return word.lowercaseString }) .filter({(word) -> Bool in if let regex = try? NSRegularExpression(pattern: "\\w+(-\\w+)*", options: .CaseInsensitive) { let matches = regex.matchesInString(word, options: nil, range: NSMakeRange(0, word.characters.count)) return matches.count > 0 } }) // computing the results for word:String in wordsArray { if let tot = _words[word] { _words[word] = tot + 1 }else{ _words[word] = 1 } } } } -
现在我们已经完成了模型部分,我们可以进行视图和控制器部分。打开你的 Storyboard,添加三个按钮:一个用于不使用线程计算文件单词,另一个用于使用线程,还有一个用于跳转到在文本字段中输入的网站。
注意
在 iOS9 中,苹果引入了一个名为App Transport Security(ATP)的新安全功能。这阻止了所有非安全(HTTPS)URL 请求,包括
UIWebView。 -
对于这个菜谱,我们将禁用 ATP;然而,对于任何生产应用程序,建议你遵循适当的 ATS 程序。在你的
info.plist文件中,添加一个新的键NSAppTransportSecurity并将其值设置为NSAllowsArbitraryLoads。这将允许所有 URL 无错误地加载。正如你可以想象的那样,你必须添加一个文本字段和一个网页视图。你应该有一个类似于这样的布局:![如何做…]()
-
现在让我们将文本字段和网页视图与相应的属性链接起来。我们还需要创建两个属性来知道我们开始计数单词的时间和完成操作的时间。所以,让我们用以下代码完成我们的视图控制器:
@IBOutlet var textField: UITextField! @IBOutlet var webView: UIWebView! lazy private var start = CFAbsoluteTimeGetCurrent() lazy private var finish = CFAbsoluteTimeGetCurrent() -
一旦我们完成了这些属性,我们就可以从最简单的方法开始。在这种情况下,这个方法是
go按钮事件,它在网页视图中加载网页:@IBAction func loadWeb(sender: UIButton) { let url = NSURL(string: textField.text!) let request = NSURLRequest(URL: url!) webView.loadRequest(request) } -
现在我们必须创建一个方法来调用单词计数器。这个方法应该是两个按钮共有的,无论是使用线程处理的按钮还是不使用线程处理的按钮,所以让我们现在创建一个不受任何按钮限制的方法:
func countWords(file:String){ let wordCounter = WordCounter(file: file) wordCounter.execute() finish = CFAbsoluteTimeGetCurrent() print("\(finish-start)") var result = "" for (total, word) in wordCounter.words.enumerate(){ result += "\(word) -> \(total)\n" } UIAlertView(title: "Result", message: result, delegate: nil, cancelButtonTitle: "Ok").show() } -
为了完成应用程序,我们必须为每个按钮创建点击事件:
@IBAction func countWordsWithoutThreads(sender: UIButton) { start = CFAbsoluteTimeGetCurrent() countWords("rfc2821") } @IBAction func countWordsWithThreads(sender: UIButton){ start = CFAbsoluteTimeGetCurrent() let thread1 = NSThread(target: self, selector: Selector("countWords:"), object: "rfc2821") thread1.start() } -
现在是时候测试应用程序了;注意不同的行为。首先,按下处理单词而不使用线程的按钮,然后尝试在文本字段中输入任何内容。直到单词计数器完成,你将无法输入任何内容。一旦操作完成,按下使用线程工作的按钮,并尝试输入一个 URL 并导航。现在你可以在程序处理其他事情的同时进行导航。
它是如何工作的…
线程就像程序的轨道;即使你有大量的线程,你仍然共享全局变量或属性,就像我们与start属性所做的那样。人们过去常常将线程与性能提升联系起来;这并不完全正确。
如果你有一个 I/O 操作,比如读取文件,或者使用类似蓝牙的传感器,你可以因为处理器可以在 I/O 不发送回复时工作而获得良好的性能。
虽然你可以在使用我们的应用程序的 I/O 时获得性能上的收益,但你可能会因为使用线程而降低性能。为什么?这是因为程序需要浪费时间创建线程并在线程之间切换。
为什么我们在这个应用程序上创建线程时用户体验更好?原因是任何与用户界面相关的操作都是在主线程上完成的。如果你在这个线程上有大型的操作,就像我们处理单词计数一样,它将阻止程序渲染或回答事件,直到你的操作完成。
创建一个新的线程可以在你计数另一个线程上的单词时让应用程序响应用户界面。在这种情况下,我们没有获得或失去性能,但用户得到了更好的可用性。
在这个菜谱中,我们还可以看到正则表达式的使用,这是使一种叫做 Perl 的计算机语言变得非常著名的特性,其他语言如 JavaScript 也将此特性作为其语言的一部分。不幸的是,Swift(至少是这个版本)的情况并非如此。正则表达式对于查找模式和创建一些过滤器非常有用,例如验证电子邮件、产品代码或 URL。
还有更多…
不幸的是,NSThread并不像posix线程函数那样完整,因为我们没有例如join方法。如果你的线程函数是一个 C 函数,你仍然可以使用pthread_create和pthread_join等函数。
在下一个菜谱中,我们将学习关于 NSOperation 的内容,这是苹果推荐的。
创建一个 SEO 应用程序
如你所知,如今分析一个网站以在搜索引擎上获得更好的结果是非常常见的。统计网站上的单词是一个常见的任务,以了解搜索引擎如何从我们的网站上检索信息。因为我们已经从之前的菜谱中有了WordCounter类,我们将重用它并创建一个新的程序来统计网站的单词。
准备工作
首先,检查一些你想要统计单词的网站的 URL。可以是任何网站,但想法是检查使用很多单词的几个 URL。
为了知道任务已完成,我们将显示一个图标,你可以从书籍资源中下载它,或者你可以下载你自己的图标。
一旦你有了你的 URL 列表和你的图标准备就绪,让我们创建一个名为Chapter 5 SEO的项目,添加你的图标,然后开始编码。
如何做到这一点...
按照以下步骤创建一个 SEO 应用程序:
-
首先,从其他之前的菜谱中复制
WordCounter.swift。为此,只需将文件从查找窗口拖到你的项目中。当然,如果这个文件可以位于一个公共目录中会更好。小贴士
当你有可用于其他项目的文件时,将它们存储在公共目录中是个好主意。
-
现在,点击这个文件,让我们改进这段代码。首先,让我们删除文件属性,因为我们将在初始化器中读取文件内容。其次,让我们创建一个新的字符串属性,称为
content。 -
下一步是修改当前的初始化器,只需将打开和读取文件的代码转移到初始化器中。你还需要创建一个新的初始化器,它接收内容而不是文件名。总结一下,现在你的
WordCounter可能看起来像这样:class WordCounter:NSObject { lazy private var _words = [String:Int]() var words:[String:Int] { return _words } lazy private var content:String = "" init(file:String){ super.init() let manager = NSFileManager.defaultManager() let data = manager.contentsAtPath(NSBundle.mainBundle().pathForResource(file, ofType: "txt")!)! content = NSString(data: data, encoding: NSUTF8StringEncoding) as! String } init(content:String){ super.init() self.content = content } func execute(){ _words = [String:Int]() // spliting the document into words var wordsArray = content.componentsSeparatedByString(" ") wordsArray = wordsArray.map({ (word) -> String in return word.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()) }) .map({ (word) -> String in return word.lowercaseString }) .filter({(word) -> Bool in if let regex = try? NSRegularExpression(pattern: "\\w+(-\\w+)*", options: .CaseInsensitive) { let matches = regex.matchesInString(word, options: nil, range: NSMakeRange(0, word.characters.count)) return matches.count > 0 } }) // computing the results for word:String in wordsArray { if let tot = _words[word] { _words[word] = tot + 1 }else{ _words[word] = 1 } } } } -
是时候创建应用程序界面了。在这种情况下,我们需要一个文本框让用户输入 URL,一个标签来显示消息,一个按钮来指示用户已完成 URL 的编写,以及一个表格视图,类似于以下截图:
![如何操作…]()
-
在继承
UITableViewDataSource和UITableViewDelegate之后,将表格视图的数据源和委托与视图控制器链接。如果编译器抱怨一些缺失的函数,不要担心;我们将在稍后实现它们。 -
现在,前往视图控制器并创建一个辅助结构体。这个结构体会帮助我们了解请求的网站评估是否完成以及结果。因此,在视图控制器类中添加以下代码:
struct UrlInfo { var url:String var finished:Bool = false lazy var words = [String:Int]() init(url:String){ self.url = url } } -
下一步是添加一些属性;在这种情况下,我们需要一个文本框来输入 URL,一个表格视图,一个
UrlInfo数组,以及一个用于我们操作的对列:@IBOutlet let urlTextField: UITextField! @IBOutlet let urlsTables: UITableView! var urls = [UrlInfo]() let queue = NSOperationQueue() -
现在我们可以实现按钮事件,所以将按钮与名为
analyze的方法链接,并编写其代码:@IBAction func analyze(sender: UIButton) { var url = urlTextField.text.stringByReplacingOccurrencesOfString(" ", withString: "", options: [], range: nil) if url == "" { return } let position = self.urls.count self.urls.append(UrlInfo(url: url)) self.urlsTables.reloadData() queue.addOperationWithBlock(){ var data = NSData(contentsOfURL: NSURL(string: url)!) var textResponse = NSString(data: data!, encoding: NSASCIIStringEncoding) as! String print(textResponse) let wordCounter = WordCounter(content: textResponse) wordCounter.execute() self.urls[position].words = wordCounter.words self.urls[position].finished = true NSOperationQueue.mainQueue().addOperationWithBlock({ self.urlsTables.reloadData() }) } }小贴士
当编写这段代码时,我的 Xcode 已经更新,Swift API 也进行了更新,因此有必要修复其中的一些部分。当你在 Swift 中编码时,请考虑这种类型的变更。
-
最后一步是实现表格视图的相应部分。首先,让我们标示当前表格视图的单元格数量:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return urls.count } -
接下来,让我们为 URL 创建一个单元格。在这种情况下,当 URL 被计算后,我们将添加一个
OK图标:func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{ var cell = urlsTables.dequeueReusableCellWithIdentifier("url") if cell == nil { cell = UITableViewCell(style: .Default, reuseIdentifier: "url") cell?.textLabel?.text = urls[indexPath.row].url } if self.urls[indexPath.row].finished { cell?.imageView?.image = UIImage(named: "ok.png") }else { cell?.imageView?.image = nil } return cell! } -
当然,我们还需要在用户从表格视图中选择单元格时显示结果。这个结果只有在 URL 分析完成后才会显示:
func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) { if urls[indexPath.row].finished { var result = "" for (total, word) in urls[indexPath.row].words.enumerate(){ result += "\(word) -> \(total)\n" } UIAlertView(title: "Result", message: result, delegate: nil, cancelButtonTitle: "Ok").show() } } -
现在应用已经完成。尝试检查一个 URL 并查看其结果。
它是如何工作的…
NSOperation 是建立在GCD(Grand Central Dispatch)之上的,这是苹果实现多任务的方式。NSOperation 需要一个队列,这可以是由程序员创建的新队列,或者它可以是现有的一个。
如前所述,你必须考虑到与用户界面相关的操作,例如刷新表格视图内容,必须在主线程上完成。用 NSOperation 的话来说,它应该在主队列上完成。你也可以为那些不需要立即完成的任务创建一个低优先级队列。
关于 NSOperation 相对于线程的一个优点是它更优化于多核设备,这意味着在 Mac 电脑、新 iPhone 和 iPad 上能提供更好的性能。
还有更多…
当然,你可以同时使用线程和 NSOperation,但必须小心使用。例如,避免在 OS X 上使用 NSOperation 与 fork。在下一个菜谱中,我们将直接使用 GCD,这可能会给我们更多的灵活性。
创建 CycloneChecker 应用
有时候,我们的手机,甚至我们的电脑,能告诉我们天气预报是件好事,尤其是在某种灾害即将来临的时候,比如风暴、地震或飓风。为了做到这一点,应用程序必须持续在互联网上请求天气预报,但它不应该阻塞应用程序的操作。
在这个菜谱中,我们将开发一个每五分钟请求飓风预测的应用程序;如果找到飓风,它将记录用户可以检索有关找到的飓风信息的 URL。如果应用程序在后台运行,它将抛出一个通知。
在这里,我们将使用 Grand Central Dispatch 创建一个多任务,这是苹果推荐的方法。
准备工作
创建一个名为 第五章 飓风 的应用程序,并添加一个名为 CycloneChecker.swift 的文件,同时检查您的电脑或设备上是否有互联网连接。
如何做到这一点…
按照以下步骤创建 CycloneChecker 应用程序:
-
首先,我们需要指定这个类(
CycloneChecker)是XMLParserDelegate,这迫使我们从NSObject继承:class CycloneChecker:NSObject, NSXMLParserDelegate{ -
现在添加其属性。我们需要一个常量来表示应用程序将要检查飓风的频率,另一个常量包含应用程序可以检查飓风预测的 URL,一个属性来指示我们正在访问的当前网站,一个队列来添加我们的操作,一个属性来指示对象是否在工作,以及一个闭包,每次我们找到飓风时都会运行:
private let interval = 300 private let urls = ["http://www.nhc.noaa.gov/nhc_at1.xml", "http://www.nhc.noaa.gov/nhc_at2.xml", "http://www.nhc.noaa.gov/nhc_at3.xml", "http://www.nhc.noaa.gov/nhc_at4.xml", "http://www.nhc.noaa.gov/nhc_at5.xml"] private var position = 0 private let queue = dispatch_queue_create("cyclone.queue",DISPATCH_QUEUE_SERIAL) private var started = false var action: (String) -> (Void) = {(url) -> Void in } -
我们将要实现的第一个两个方法是使对象工作或停止工作的方法:
func start(){ started = true initQueue() } func stop(){ started = false } -
如您所见,我们需要一个名为
initQueue,的方法,该方法将任务添加到对象队列中:private func initQueue(){ self.position = 0 for i in 1...urls.count { dispatch_async(queue, { () -> Void in if self.started { print("checking \(self.position)") var xmlParser = NSXMLParser(contentsOfURL: NSURL(string: self.urls[self.position])!) xmlParser?.delegate = self xmlParser?.parse() } }) } dispatch_async(queue, { () -> Void in if self.started { sleep(UInt32(self.interval)) self.initQueue() } }) } -
一旦对象找到飓风,它需要通知用户:
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : AnyObject]){ if elementName == "cyclone"{ self.action(self.urls[self.position]) } } -
为了完成这个类,我们需要标记当前 XML 已被解析;下次,我们将需要解析下一个:
func parserDidEndDocument(parser: NSXMLParser) { position += 1 } -
飓风检查器已完成;下一步是创建视图,因此点击故事板,添加两个按钮(一个用于开始检查,另一个用于停止检查),一个标签来指示对象状态(运行或停止),以及一个文本框来显示即将到来的飓风文本。
![如何做到这一点…]()
-
如您所想象,现在我们将编写属性,开始链接我们在故事板中添加的视图,并创建一个
CycloneChecker对象:var cycloneChecker = CycloneChecker() @IBOutlet var buttonStart: UIButton! @IBOutlet var buttonStop: UIButton! @IBOutlet var textView: UITextView! @IBOutlet var labelStatus: UILabel! -
让我们在
viewDidLoad方法中初始化应用程序,向CycloneChecker添加一个动作。在这种情况下,我们将将其添加到文本视图,并在应用程序在后台运行时发送用户通知:override func viewDidLoad() { super.viewDidLoad() cycloneChecker.action = {(url) -> Void in if UIApplication.sharedApplication().applicationState == .Background { var localNotification:UILocalNotification = UILocalNotification() localNotification.alertAction = "Cyclone found" localNotification.alertBody = "A cyclone was found. visit \(url) for more information" localNotification.fireDate = NSDate(timeIntervalSinceNow: 10) UIApplication.sharedApplication().scheduleLocalNotification(localNotification) } dispatch_async(dispatch_get_main_queue(), { () -> Void in self.textView.text = "\(self.textView.text)\nA cyclone was found. visit \(url) for more information" }) } self.textView.text = "" self.textView.layer.borderWidth = 0.5 } -
现在我们可以完成视图控制器,添加
start和stop事件:@IBAction func start(sender: AnyObject) { cycloneChecker.start() labelStatus.text = "Status: started" buttonStart.enabled = false buttonStop.enabled = true } @IBAction func stop(sender: AnyObject) { cycloneChecker.stop() labelStatus.text = "Status: stopped" buttonStart.enabled = true buttonStop.enabled = false } -
为了完成应用程序,您需要在应用程序代理中添加,最好是在
didFinishLauchingWithOptions方法中。使用以下代码来使用通知:func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [String: AnyObject]?) -> Bool { if UIApplication.instancesRespondToSelector(Selector("registerUserNotificationSettings:")) { application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: UIUserNotificationType.Sound | UIUserNotificationType.Alert | UIUserNotificationType.Badge, categories: nil)) } return true } -
最后,你需要测试应用程序,所以按播放按钮检查你附近是否有旋风。希望没有。
注意
如果你使用 iOS 8 或更高版本,别忘了允许通知。
-
一旦应用程序启动,你应该会看到以下截图类似的内容。点击确定以允许通知,如图所示:
![如何操作……]()
它是如何工作的……
此应用程序使用 Grand Central Dispatch,它将为运行分配的任务创建一个线程和一个队列。因为它创建了一个分离的线程,所以它不会阻塞用户界面,并且因为它不是为每个任务创建一个线程,所以应用程序不会因为线程切换而损失性能。
正如你在创建队列时看到的,我们必须指定我们想要一个串行队列,这意味着任务不会在之前的任务完成之前开始。我们还使用了一个名为 dispatch async 的函数,这意味着调用者不会等待任务完成,因此代码可以与队列任务并发运行。
当然,当我们找到旋风时,我们需要在文本视图中写下这些信息,这是在主线程上必需的,所以这就是为什么我们必须创建另一个任务将其添加到主队列中的原因。
另一个有趣的部分是,添加到我们的队列中的一个任务调用了sleep函数。正如你所知,新的队列在分离的线程上执行,下一个任务不会在当前任务完成之前运行,所以使用这个函数是完全可以的。
如果你之前没有在视图控制器上使用过通知,这里有一个示例。别忘了通知只能在 iOS 7 或更高版本上使用,并且在 iOS 8 上,用户必须接受发送通知的权限。
还有更多……
我们仍然有两个悬而未决的主题:第一个是使用并发队列,另一个是防止两个任务同时更改相同的对象。这两个主题都将在下一个菜谱中揭晓。
检查我们网站的链接
在本章中,我们创建了一个应用程序,可以帮助我们通过检查其单词频率来定位我们的网站,但正如你可能知道的,SEO 不仅仅是关于计数单词,它还涉及到网站链接。
在这个菜谱中,我们将检查网站的链接;在这种情况下,因为我们正在使用网络,所以我们可以并行执行任务。
准备工作
创建一个名为Chapter 5 weblinks的项目,并添加一个名为LinkChecker.swift的文件。检查你的模拟器或设备上是否有互联网连接。
如何操作……
一旦你确认你的设备或模拟器有互联网连接,请按照以下步骤创建 Link Checker 应用程序:
-
在我们开始创建
LinkChecker类之前,我们需要创建一个辅助类,它将存储LinkChecker类型对象之间的公共信息。这个类将被命名为UrlManager,它需要存储队列、一个用于记录找到的 URL 的文件处理器、一个包含 URL 的数组以及一个表示我们想要的最大链接数量的常量(一些网站有很多链接):private class UrlManager { enum UrlAddStatus { case OK, FULL, REPEATED, WRONG_URL } private var _queue = dispatch_queue_create("concurrentqueue", DISPATCH_QUEUE_CONCURRENT) var queue:dispatch_queue_t{ return _queue } private var fileHandle:NSFileHandle? private lazy var _urls = [String]() let LIMIT = 10 -
现在,让我们创建一些辅助函数来帮助这个对象知道一个 URL 是否应该被存储。其中之一将告诉它我们的列表中是否已经存储了该 URL,另一个将告诉我们列表是否已满,第三个将检查 URL 是否有效,最后一个将记录 URL 到文件中:
func contains(url:String) -> Bool{ objc_sync_enter(self._urls) for u in self._urls { if u == url { objc_sync_exit(self._urls) return true } } objc_sync_exit(self._urls) return false } var full:Bool { return self._urls.count >= self.LIMIT } private func validUrl(url:String) -> Bool{ var error:NSError? var regex = NSRegularExpression(pattern:"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$", options: .CaseInsensitive, error: &error) let matches = regex?.matchesInString(url, options: nil, range: NSMakeRange(0, count(url))) return matches?.count > 0 } private func writeMessage(message:String){ objc_sync_enter(self.fileHandle) self.fileHandle?.writeData(message.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!) objc_sync_exit(self.fileHandle) } -
现在,让我们编写初始化器;在这种情况下,我们只需要打开日志文件:
init(){ var path:String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! String let fullpath = (path as NSString).stringByAppendingPathComponent("application.log") NSFileManager.defaultManager().createFileAtPath(fullpath, contents: nil, attributes: nil) self.fileHandle=NSFileHandle(forWritingAtPath:fullpath)! } -
为了完成这个类,我们必须创建主方法,即
addUrl。这个函数将返回是否能够添加 URL:func addUrl(url:String) -> UrlAddStatus { if full { // WRITE FULL self.writeMessage("Couldn't store the url \(url). Buffer is full") return .FULL } if self.contains(url){ self.writeMessage("Couldn't store the url \(url). Already on buffer") return .REPEATED } if !self.validUrl(url) { self.writeMessage("Couldn't store the url \(url). Invalid url") return .WRONG_URL } objc_sync_enter(self._urls) self._urls.append(url) objc_sync_exit(self._urls) self.writeMessage("Url \(url) successfully stored") return .OK } } // Class end -
在同一个文件中,我们将创建一个名为
LinkChecker的类。为了做到这一点,我们再次将使用XMLParser。小贴士
在这个菜谱中,我们使用
XMLParser是因为它建立在 Swift 标准库之上,但如果你想要一个更好的 HTML 解析器,你可以在互联网上搜索一个特定的库,例如 NDHpple,github.com/ndavidsson/NDHpple/。 -
让我们从属性开始。对于这个类的实现,我们需要一个
NSXMlParser,一个UrlManager,以及一个闭包,每次我们找到 URL 时都会执行:class LinkChecker:NSObject, NSXMLParserDelegate { private var xmlParser:NSXMLParser private var urlManager:UrlManager var foundAction: (String) -> (Void) = { (url) -> Void in } -
下一步是创建初始化器。在这种情况下,我们将开发两个初始化器。第一个是从外部(视图控制器)调用的那个,另一个是由同一个
LinkChecker创建的,它接收作为参数的同一个UrlManager,这就是为什么它是一个私有初始化器的原因:init(url:String) { self.xmlParser = NSXMLParser(contentsOfURL: NSURL(string: url)!)! self.urlManager = UrlManager() super.init() self.xmlParser.delegate = self self.urlManager.addUrl(url) } private init(url:String, urlManager:UrlManager) { self.xmlParser = NSXMLParser(contentsOfURL: NSURL(string: url)!)! self.urlManager = urlManager super.init() self.xmlParser.delegate = self } -
下一个函数是
start方法,它将创建第一个任务:func start(){ dispatch_async(urlManager.queue, { () -> Void in self.xmlParser.parse() return }) } -
这个
LinkChecker类的最后一个方法是NSXMLParserDelegate协议的解析函数。在这里,我们将检查是否找到了一个由aHTML 标签表示的链接:func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [NSObject : AnyObject]){ if elementName.lowercaseString == "a" { let href = "href" var newUrl = attributeDict["href"] as! String if let range = newUrl.rangeOfString("#", options: .CaseInsensitiveSearch, range: nil, locale: nil){ newUrl = newUrl.substringToIndex(range.startIndex) } print("found link \(newUrl)") if self.urlManager.addUrl(attributeDict[href] as! String) == .OK { self.foundAction(attributeDict[href] as! String) dispatch_async(urlManager.queue, { () -> Void in self.xmlParser.parse() return }) } } } } // class end -
现在,点击 storyboard 并创建一个带有文本字段来写入 URL、一个用于开始分析它的按钮以及一个用于显示结果的表格视图的屏幕。
![如何操作…]()
-
现在,从
UIViewController继承UITableViewDataSource并添加文本字段、tableview、一个用于存储 URL 的字符串数组和一个LinkChecker作为属性:class ViewController: UIViewController, UITableViewDataSource { @IBOutlet let urlTextField: UITextField! @IBOutlet let tableView: UITableView! private var _urls = [String]() private var linkChecker:LinkChecker? -
一旦完成,我们可以在按钮上添加一个事件来开始分析 URL:
@IBAction func analyze(sender: UIButton) { linkChecker = LinkChecker(url:self.urlTextField.text) self.linkChecker?.foundAction = { (url) -> Void in self._urls.append(url) dispatch_async(dispatch_get_main_queue(), {() -> Void in self.tableView.reloadData() }) } linkChecker?.start() self.urlTextField.resignFirstResponder() } -
为了完成这个类,让我们实现
tableview方法,这些方法将显示_urls属性的内容:func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return self._urls.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{ var cell = self.tableView.dequeueReusableCellWithIdentifier(_urls[indexPath.row]) as? UITableViewCell if cell == nil { cell = UITableViewCell(style: .Default, reuseIdentifier: _urls[indexPath.row]) cell?.textLabel?.text = _urls[indexPath.row] } return cell! } -
现在应用程序已经完成,测试你的网站并看看你能从中获得多少链接。你应该看到类似以下的结果:
![如何操作…]()
它是如何工作的…
从网络上请求某些东西将需要时间。然而,在此期间,应用程序可以处理下一个任务,因此在这种情况下,我们需要创建一个并发队列而不是串行队列。
考虑到这一点,我们在处理一些变量时必须小心,主要是因为当应用程序尚未完成对一个变量的处理时,它可以从一个任务切换到另一个任务。为了防止任务重叠相同的属性或变量,我们应该使用objc_sync_enter和objc_sync_exit来控制它。
注意
不幸的是,Swift 还没有某种互斥锁,因此它需要使用 Objective-C 的@synchronize,这也是这些函数前缀的原因。
还有更多...
正如你所见,有时与并发任务一起工作很困难,开发者需要考虑很多细节;修复问题也可能非常困难,因为有时很难重现它。
在下一章中,我们将学习如何在不需要运行整个应用程序的情况下测试我们的代码。
第六章。使用 playground
在本章中,我们将介绍以下食谱:
-
创建第一个 playground
-
观看一些图形
-
观察颜色变化下的温度
-
调整图像大小
-
使用 pangrams 美化你的文本
-
接收 JSON
-
创建我们自己的类表示
-
丰富的文档
-
在 playground 中导航页面
简介
如果你曾经使用过 Python、Perl、Ruby 或 JavaScript 等解释型语言进行编程,你可能已经注意到这些语言与 C、Objective-C 或 C++ 等本地语言相比的一个优势是,可以在不添加额外代码到项目的情况下测试代码。
有时候开发者需要在将代码添加到项目之前对其进行测试,尤其是在学习 Swift 的工作方式时。即使你对 Swift 非常熟悉,也可能会遇到新的想法,你可能在将它们编码到项目中之前需要检查这些想法是否有效。
对于之前提到的情况,苹果推出了 playground,这是一个你可以在这里玩转代码、测试代码、可视化代码,并在将其添加到应用程序之前对代码做出决定的地方。
很令人印象深刻的是,Swift 是一种编译型语言,但你可以在 playground 中边编写边测试你的代码,就像在使用解释型语言一样,可以实时查看结果。
创建第一个 playground
在这个食谱中,你将熟悉Xcode playground,在这里你将学习这个 Xcode 功能的一些基础知识,然后你将能够使用这个新朋友测试你自己的代码。
准备中
打开你的 Xcode,但这次,你不需要创建一个新的项目,而是选择使用 playground 开始:

完成后,你会看到下一个对话框非常简单,只会询问项目名称和平台(iOS、Mac OS 或 tvOS)。命名为 Chapter 6 First Playground 并选择 iOS:

然后你会看到著名的对话框,询问你将应用程序存储在哪里。选择你想要的文件夹。如果有疑问,请选择文档文件夹。
在我们开始使用 playground 之前,打开保存项目的文件夹对应的Finder窗口,右键单击(或按住控制键单击)你的 playground 项目,并选择显示包内容。
现在你可以看到那里有三个文件:contents.xcplayground、results.playgrounddata和section-1.swift。现在你可以想象这些文件的作用,但我想要强调的是,随着 playground 的增长,文件的数量也会增加。记住,playground 有自己的包,你可以在其中添加图片和其他内容。
如何做到这一点...
按照以下步骤创建第一个 playground:
-
回到你的 Xcode,看看你的游乐场。它开始导入 UIKit 和一个字符串变量,该变量显示Hello, playground。你还可以在右侧看到这个变量的结果。
-
让我们开始定义另一个变量,称为
name,在str变量之前,并将str变量更改为具有插值,就像以下代码:var name = "Cecil Costa" var str = "Hello, \(name)"现在你将看到在你的右侧你有处理过的两个变量:
![如何做……]()
-
一旦你理解了这一点,让我们用
if语句来完成这个代码,所以在之前的代码之后,添加一个像这样的if语句:var myCity = "Cambridge" var yourCity = "New York" if myCity == yourCity { print("We live in the same city") }else { print("We live in different cities") } -
编写代码后,确保你有一个语句的结果,但没有另一个语句的结果:
![如何做……]()
-
是时候尝试一个循环了,让我们计算,例如,著名的斐波那契函数:
var fib_n = 1 var fib_n_1 = 1 for i in 3...10 { var sum = fib_n + fib_n_1 fib_n_1 = fib_n fib_n = sum } fib_n现在,你可以以不同的方式欣赏结果,而不是显示变量的值,你可以看到指令被执行的次数。如果你将鼠标指针移到每个指令的结果上,你会看到出现两个小图标。
-
第一个,就像一只眼睛,是用来观察变量结果表示的,另一个是用来时间线的,它显示了每次循环迭代中分配给变量的值。我们将在后面的食谱“观看一些图形”和“使用 Pangrams 美化你的文本”中讨论这一点:
![如何做……]()
我们在代码末尾单独写下变量
fib_n的原因是因为这是你可以可视化变量最终值的方式,在这种情况下,我们可以看到代码的结果是55。 -
现在我们可以将这段代码与等效的递归函数进行比较,然后我们可以决定在应用程序中使用哪种代码:
func fib (n:Int) -> Int{ if n == 1 || n == 2 { return 1 } return fib(n-1) + fib(n-2) } fib(10) -
编写代码后,你可以检查两个重要的信息点;第一个是结果,与之前的代码相同。第二个是函数被调用的次数,正如你所见,在这个例子中,递归函数调用了 54 次,而交互式函数调用了 8 次。
它是如何工作的……
如你所见,每次你编写代码,它都会被重新编译和执行;优势是它给你关于代码的信息,你可以用它来比较不同的代码,还可以检查你心中的想法是否适合你的应用程序。
还有更多……
现在你已经对如何使用游乐场有了基本的了解。接下来我们要学习的是如何以图形方式可视化和跟踪信息及其进展。
观看一些图形
知道循环迭代 8 次而不是 54 次是可以的。这能给你一个好的选择最佳算法的思路,但有时我们需要在循环迭代过程中可视化变量的值。对于这种情况,苹果公司创建了时间线。
准备工作
您可以打开您的 Xcode 并点击开始使用游乐场,或者如果您已经打开了 Xcode,您可以点击文件菜单,然后点击新建选项,然后点击游乐场。
将您的项目命名为第六章时间线并将其保存在您的项目文件夹中。
如何操作…
-
首先,让我们编写一段代码来打印一些输出,在这种情况下,我们将打印一个由星号组成的三角形:
for i in 1...5 { for j in 1...i { print("*") } print() } -
一旦您输入了这段代码,您可能还不会欣赏到三角形。转到看起来像两个重叠圆圈的图标并点击它:
![如何操作…]()
-
看看 Xcode 如何分成两部分。右侧的新部分被称为时间线。现在您应该看到控制台输出,它应该类似于以下截图:
![如何操作…]()
小贴士
如果出于任何原因您关闭了控制台输出,您可以通过重新运行您的代码来重新打开它。
-
现在,让我们看看变量的值;在这种情况下,我们没有很多选项,所以我们将检查
j变量的值。要做到这一点,只需在打印指令后输入j,如下面的代码所示:for j in 1...i { print("*") j // ← Yes, only the letter "j" } -
现在点击与
j变量同一级别的圆形图标:![如何操作…]()
-
现在,您可以在以下图形中看到
j的值,根据迭代进行变化:![如何操作…]()
-
如果您想更清楚地看到变量的值,您可以滑动位于 Xcode 屏幕底部的红色条:
![如何操作…]()
-
如果出于任何原因您有很多图表,您可以关闭它们以节省空间,但如果您想保留一些图表,您可以双击它们的标题来折叠它们,当然您也可以通过再次双击来展开它们:
![如何操作…]()
如您所见,在这种情况下,您只有标题j // <- 只是有字母"j"而不是整个图表。
工作原理…
时间线有三个不同的编辑器:标准编辑器,它为我们提供了更多的空间来编写和查看我们的代码流程;辅助编辑器,它为我们提供了更多关于我们的代码执行的信息,以及版本编辑器,它显示了我们的游乐场所做的修改。
辅助编辑器为我们提供了关于变量演变及其值的更多准确性。使用图表有助于我们了解变量值是如何变化的。
控制台输出也很重要;请注意,如果您只尝试可视化打印指令,您将看到很多星号,每个星号都在不同的行上。这类信息对开发者来说并不有用。
更多内容…
游乐场不仅限于显示图形;您还可以添加一些颜色。在下一个菜谱中,您将在您的游乐场中处理颜色。
用颜色观察温度
当施乐工程师基于 Smalltalk 创建窗口系统时,他们为计算机世界带来了一个重大概念,即在数字设备(计算机)上的模拟表示(思想)。
这意味着对于我们人类来说,通过观察进度条而不是屏幕上的百分比来了解一个过程将花费多长时间更容易理解。这里我们将观察一些颜色而不是数字。
在这个菜谱中,我们将观察温度的颜色而不是观察它的值;这样你可以更好地了解寒冷。
准备工作
打开你的 Xcode,创建一个新的 playground,命名为第六章 颜色,移除str字符串但保留 UIKit 导入。
如何做…
-
首先,让我们创建一个包含温度的数组,在这种情况下,温度是以摄氏度来衡量的:
var temperatures = [15, 8, 3, -1, 2, 3, 3, 9, 14, 18, 23, 27, 30, 34, 20, 30, 35, 39, 41] -
下一步是遍历数组。对于每次迭代,我们将结果存储到一个名为 color 的变量中:
for temp in temperatures { var color:UIColor if temp <= 0 { color = UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) } else if temp >= 40 { color = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) }else { var proportional = CGFloat(temp) / CGFloat(40) color = UIColor(red: proportional, green: 0, blue: CGFloat(1) - proportional, alpha: 1.0) } color // Temperature representation } -
现在你必须将上一个循环句子中的颜色变量添加到时间线中;你可以看到以下类似的结果:
![如何做…]()
它是如何工作的…
时间线不仅关乎显示数字,还关乎提供信息,让我们能更好地理解代码。观察像 13 摄氏度这样的数字并不能很好地让我们了解温度,但观察颜色可以更好地告诉我们它是冷还是热,尤其是如果你使用华氏值的话。
如你所知,你的电脑屏幕使用三种原色:红色、绿色和蓝色。UIColor 还有一个第四个因素,即 alpha 因素,零表示透明,一表示不透明。
在这个菜谱中,我们始终将 alpha 设为 1,绿色设为 0,但对于寒冷的日子,我们假设我们需要高水平的蓝色和低水平的红色。对于温暖的日子,我们必须创建一个红色水平高、蓝色水平低的颜色。这意味着非常蓝色的颜色表示温度非常低,而非常红色的颜色表示温度非常高。
那中间的温度怎么办?在这种情况下,我们将有一个紫色,这是由 50%红色和 50%蓝色混合而成的颜色。
看看这种度量在 playground 中比在应用程序中使用更有用。大概在真实的应用程序中,你不会存储颜色,你会存储实际的温度,但在编码之前,你可以了解你拥有的值是否满意。
更多…
在这种情况下,我们使用了颜色来更好地了解我们的值,但我们还能用什么呢?在下一个菜谱中,你将学习如何使用图片。
展开图片
有时候,需要多个图片样本才能看到哪个比例是我们想要的。让我们想象一下,我们有一个消息,但我们不想展示给用户,我们只想让他知道有东西,应用程序稍后会显示正确的比例。
对于这种情况,你可以使用UIImage和ImageView,并在 playground 中可视化它。
准备工作
创建一个名为 Chapter 6 Stretching Image 的新游乐场,并且为这个食谱准备一张图片,我建议下载图片 secret_message.png,它包含在这本书的资源文件中。
如何操作…
-
关闭 Xcode 并打开一个 Finder 窗口,转到你保存游乐场的文件夹,点击你创建的项目,然后右键(或按住控制键点击)它。在菜单中,选择我们在这章开头学到的 Show Package Contents 选项:
![如何操作…]()
-
现在,创建一个名为
Resources的文件夹,并将你的图片文件复制进去。之后,重新打开你的游乐场。 -
删除
str变量,因为我们不会使用它。让我们创建一个新的类,其中包含ImageView,其大小以及ImageView被拉伸的次数:class ImageStretcher{ var imageView:UIImageView var times = 0 var currentFrame:CGRect init(){ let image = UIImage(named: "secret_message")! imageView = UIImageView(image: image) currentFrame = imageView.frame } func stretch(){ currentFrame.size.height *= CGFloat(1.1) currentFrame.size.width *= CGFloat(0.9) imageView.frame = currentFrame times++ } } -
一旦你编写了代码,我们需要创建这个类型的对象,所以请将以下内容添加到你的游乐场中:
let imageStretcher = ImageStretcher() -
好的,现在你有一个对象了,但图片仍然是原始大小。如果你调用
stretch方法,你会看到图片只被拉伸一次,所以让我们重复这个过程,以便更好地了解我们应该将图片拉伸多少:for i in 1...15 { imageStretcher.stretch() } -
为了可视化不同的结果,你可以点击值历史图标,然后你可以看到很多示例。
![如何操作…]()
-
为了更好地了解
ImageView,你可以点击样本行的右侧,那里有一个眼睛图标。例如,点击数字 10,看看你得到了什么:![如何操作…]()
它是如何工作的…
视图可以在游乐场中使用;例如,你可以预先可视化你的 UIImageView、UILabel、UITextField 等将如何绘制。在这种情况下,我们不得不重复几次;这意味着只有 ImageView 不会给我们完整的信息。
最好的方法是创建我们自己的类,并存储我们需要的所有信息,例如我们拉伸 ImageView 的次数以及 ImageView 的当前大小。
在结果的右侧,正如你所看到的,有一个小眼睛图标(快速查看),它将显示视图在那个时刻的状态。
注意
当你想在图片上使用过滤器(如隐藏秘密信息)时,通常会使用一个名为 CoreImage 的框架,不幸的是,这个框架目前还不能与游乐场一起使用。
还有更多…
如果你需要将信息存储在你的游乐场中,你可能想打印出常量 XCPSharedDataDirectoryPath。它将显示游乐场存储这些信息的位置。
有时候我们可能想隐藏信息,有时候我们可能想让它更容易阅读。在下一个食谱中,我们将学习如何使用不同的字体预先可视化文本。
使用 Pangrams 美化你的文本
你有没有见过这样的短语“The quick brown fox jumps over the lazy dog”?为什么它这么有名?原因是它是一个英语 Pangram;这意味着一个包含字母表中每个字母的短语。
Pangrams 在你想要可视化字体属性时很有用,比如大小、颜色或粗体。在这个菜谱中,我们将使用NSAttributedString和游乐场来检查不同的字体。
准备工作
创建一个新的游乐场,命名为第六章 文本,并移除默认出现的字符串。
如何做到这一点…
-
首先,让我们创建我们的属性字符串:
let string = NSMutableAttributedString(string: "The quick brown fox jumps over the lazy dog") -
之后你将看到一个带有字母a的新图标出现。这意味着游乐场已经将其识别为属性字符串。点击快速查看图标,你会看到当前字符串及其属性:
![如何做到这一点…]()
-
现在让我们为我们的文本选择一些字体大小和颜色:
let fontSizes = [CGFloat(14.0), CGFloat(18.0), CGFloat(24.0)] let colors = [UIColor.blackColor(), UIColor.blueColor(), UIColor.redColor()] -
由于我们还想选择一些不同的字体,我们需要创建另一个数组;然而,它不能是一个简单的字符串数组,因为我们将要调用不同的方法。所以我们需要创建一个闭包数组:
let fontSelectors = [ {(size: CGFloat) -> Void in string.addAttribute(NSFontAttributeName , value: UIFont.systemFontOfSize(size), range: NSMakeRange(0, string.length)) }, {(size: CGFloat) -> Void in string.addAttribute(NSFontAttributeName , value: UIFont(name: "HelveticaNeue-Bold", size: size)!, range: NSMakeRange(0, string.length)) },{(size: CGFloat) -> Void in string.addAttribute(NSFontAttributeName , value: UIFont(name: "HelveticaNeue-Italic", size: size)!, range: NSMakeRange(0, string.length)) } ] -
一旦我们完成了,我们的游乐场就准备好了,可以执行主要部分。为了做到这一点,让我们创建三个嵌套循环,每个循环将遍历我们的数组之一:
for fontSize in fontSizes { for color in colors { for selector in fontSelectors { string.addAttribute(NSForegroundColorAttributeName, value: color, range: NSMakeRange(0, string.length)) selector(fontSize) string } } } -
如果你愿意,你不必使用
for循环,你可以使用一种更快捷的方式来遍历数组,使用map闭包:fontSizes.map { (fontSize)-> Bool in colors.map { (color) -> Bool in fontSelectors.map { (selector) -> Bool in string.addAttribute(NSForegroundColorAttributeName, value: color, range: NSMakeRange(0, string.length)) selector(fontSize) string return true } return true } return true } -
无论你选择了哪种程序,点击只包含单词
string的行的值历史。现在你可以欣赏到属性字符串的样本。![如何做到这一点…]()
它是如何工作的…
属性字符串被游乐场接受,当你需要为你的应用程序选择配置而不运行完整应用程序时,这个程序非常有用。
如果我们使用 Objective-C 而不是 Swift,我们可能已经创建了一个选择器数组而不是闭包。然而,Swift 不再遵循这种哲学,现在performSelector函数要求一个延迟时间来启动。
使用块或闭包更加灵活,同时也使得代码更容易维护。你可以在这里看到的一个限制是,你不能从快速查看中复制函数数组。更好的说法是,你可以这样做,但你将要复制的唯一文本是“函数”,这并不太有用。
还有更多…
在这里我们有我们自己的文本,但如果我们需要从远程服务器获取一些文本怎么办?在接下来的菜谱中,我们将学习如何在游乐场中处理 HTTP 请求。
接收 JSONs
请求远程信息在今天是非常常见的。如果你的游乐场在你收到服务器响应之前就结束了,会发生什么?在这个菜谱中,我们将学习如何处理这个问题。
准备工作
创建一个新的游乐场,命名为第六章 请求 JSONs,并且以防万一,检查你的互联网连接。
如何做到这一点…
-
首先,我们需要找到一个可以返回更多网站的 URL。在这种情况下,我们将使用这个 URL:
api.github.com/users/mralexgray/repos。 -
创建一个包含之前提到的 URL 的常量:
let url = NSURL(string: "https://api.github.com/users/mralexgray/repos")! -
点击快速查看图标并检查你是否收到了 JSON 响应。
小贴士
你可以使用 NSURL 进行快速查看以检查网站。如果你正在测试修改网站的内容,你可以使用快速查看来检查新的网站外观。
-
现在让我们为我们的 URL 创建一个请求:
let request = NSURLRequest(URL: url) -
下一步是请求 URL 的内容:
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue(), completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in if error != nil { error.description }else { data } }) -
检查沙盒的结果。不幸的是,沙盒在收到响应之前就结束了。回到你的沙盒开始处,添加以下行:
import XCPlayground XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true) -
现在我们将能够收到响应,然而正如你所看到的,响应并不是我们等待的那个。解决这个问题需要我们实现一个新的类并将其设置为
NSURLConnectionDelegate。在发送异步请求之前放置以下代码:class HttpDelegate: NSObject, NSURLConnectionDelegate { func connection(connection: NSURLConnection, canAuthenticateAgainstProtectionSpace protectionSpace: NSURLProtectionSpace) -> Bool{ return true } func connection(connection: NSURLConnection, didReceiveAuthenticationChallenge challenge: NSURLAuthenticationChallenge){ challenge.sender.useCredential(NSURLCredential(trust: challenge.protectionSpace.serverTrust!), forAuthenticationChallenge: challenge) } } let delegate = HttpDelegate() var total = 0 XCPCaptureValue("total",total) let connection = NSURLConnection(request: request, delegate: delegate, startImmediately: true)! -
total变量将在之后使用,现在你唯一需要做的就是检查我们是否能够按照预期收到响应。只有一个请求可能不足以进行基准测试,在这种情况下,我们需要转到检查data参数的行,并添加以下代码:total++ XCPCaptureValue("total",total) var err: NSError? var firstResponse = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as [[String:AnyObject]]? for response in firstResponse!{ for (key, value) in response{ if (key as NSString).containsString("_url") && value is String{ let valueString = value as String if !(valueString as NSString).containsString("{"){ let _url = NSURL(string: value as String)! let _request = NSURLRequest(URL: _url) NSURLConnection.sendAsynchronousRequest(_request, queue: NSOperationQueue(), completionHandler: { (_response, _data, err) -> Void in total = total + 1 total XCPCaptureValue("total",total) }) } } } } -
好的,现在正如你所看到的,我们得到了一个更可接受的结果。如果你想为基准测试得到更好的结果,通过将右下角的秒数更改为3秒来减少沙盒的运行时间。
![如何做…]()
-
现在检查你收到了多少次回复,这就是你的真实统计数据。
它是如何工作的…
沙盒有一个自己的框架,称为XCPlayground。它是为了根据你的测试需求添加一些功能而创建的。在这种情况下,我们开始使用函数XCPSetExecutionShouldContinueIndefinitely,它使沙盒即使在达到最后一行代码后也能继续运行。
XCPlayground框架还帮助我们使用了XCPCaptureValue函数。这个函数允许我们从代码的不同部分存储一个值,并为它创建一个单独的图表,就像我们对total变量所做的那样。
我们还必须解决一个关于连接的问题,它被 HTTPS 协议拒绝。在这种情况下,我们尝试创建连接,我们可以看到它被拒绝,这是在将代码编入你的应用之前使用沙盒的一个很好的理由,这样我们可以更快地解决这个问题。
最后,我们可以更改我们的沙盒执行时间限制,默认情况下它从 30 秒开始,但对于这种情况可能太长了。减少这个时间让我们能够更好地了解我们的应用每秒能处理多少请求。
还有更多…
如你所见,playground 有其自己的框架。它允许我们更好地控制 playground 并获取更好的结果。为了完成它,在下一个菜谱中,我们将学习如何创建个性化的快速查看。
创建我们自己的类表示
快速查看是一个很好的工具,它帮助我们可视化对象当前的状态,但有时当我们创建具有自己逻辑的自己的类时,快速查看可能无法在没有你的帮助的情况下绘制代表对象的任何东西。
在这个菜谱中,我们将学习如何创建我们自己的类表示,为此我们将创建一个代表国际象棋棋盘的类。
准备工作
创建一个新的名为第六章国际象棋的 playground,并选择 iOS 选项。如果你选择 Mac OS,请记住导入 Cocoa 并将一些类型替换为等效类型,例如用NSBezierPath代替UIBezierPath。
如何做到这一点…
-
让我们开始创建一个从
NSObject继承的类:class CheckersBoard:NSObject { -
在我们继续之前,我们需要定义每个方格的状态;正如你所知,它可能有一个黑子、一个白子,或者它可能是空的:
enum BoardSpace { case FREE, WHITE, BLACK } -
现在让我们定义棋盘。在这种情况下,它是一个二维数组属性,并且每个方格的初始值必须是
.FREE:var board = [[BoardSpace]](count:8 , repeatedValue:BoardSpace) -
由于我们永远不知道棋盘是否有正确的大小,我们将创建一个表示正方形大小的常量。如果你认为棋盘太大或太小,你可以更改这个值:
let squareSize = 24 -
下一步是创建一个名为
debugQuickLookObject的方法。这个名字不是随意的,它必须这样命名:func debugQuickLookObject() -> AnyObject? {小贴士
在你的应用程序中使用
debugQuickLookObject方法,Swift 和 Objective-C 调试器也使用这个方法来给你一个关于你对象的想法。 -
开始创建一个与国际象棋棋盘大小相同的图像上下文:
UIGraphicsBeginImageContext(CGSizeMake(CGFloat(squareSize * 8), CGFloat(squareSize * 8))) -
创建两个循环,将绘制棋盘的每个方格:
for row in 0...7 { for col in 0...7 { -
在这些循环中,我们需要计算当前位置:
let offsetx = CGFloat(col * squareSize) let offsety = CGFloat(row * squareSize) -
之后,我们需要知道我们将绘制黑色方格还是白色方格。为此,
row和col变量将帮助我们:if col % 2 == row % 2 { UIColor.grayColor().setFill() }else { UIColor.blackColor().setFill() } -
现在,用当前颜色填充方格:
var bezier = UIBezierPath(rect: CGRectMake(offsetx, offsety, CGFloat(squareSize), CGFloat(squareSize))) bezier.fill() -
一旦我们画好了方格,我们将画一个代表玩家棋子的圆圈:
switch board[row][col] { case .WHITE: UIColor.whiteColor().setFill() case .BLACK: UIColor.brownColor().setFill() default: continue } bezier = UIBezierPath(arcCenter: CGPointMake(offsetx + CGFloat( squareSize / 2 ) , offsety + CGFloat(squareSize / 2)), radius: CGFloat(squareSize / 3), startAngle: CGFloat(0.0), endAngle: CGFloat(360), clockwise: true) bezier.closePath() bezier.fill() -
循环完成了,我们现在可以完成它们,返回图像,并完成棋盘类:
} } let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } } -
如你所想,我们需要通过实例化类、在棋盘上添加一些棋子并可视化它来测试我们的类:
var board = CheckersBoard() board.board[2][5] = .WHITE board.board[2][3] = .BLACK board.board[4][3] = .WHITE board.board[2][1] = .BLACK board.board[0][5] = .WHITE board.board[1][0] = .BLACK board.board[7][6] = .WHITE board.board[5][6] = .BLACK board -
现在点击棋盘的快速查看(最后一行),你应该看到以下结果:
![如何做到这一点…]()
它是如何工作的…
为了显示自定义的快速查看,你必须创建一个从NSObject继承的类并实现debugQuickLookObject方法。尽管这个方法返回一个AnyObject类型的对象,但在其声明中,它应该返回 playground 可以表示的任何东西,例如颜色、贝塞尔路径、视图等等。
在这个示例中,我们使用了一个贝塞尔路径,它与传统视图有不同的坐标系。贝塞尔路径的初始点(x = 0 和 y = 0)位于左下角。由于我们只使用了正方形和圆形,这并没有影响我们,但请记住这个问题,因为可能会得到错误的结果。
UIColor 有一些方法可以指示在当前上下文中应使用的颜色来填充形状(setFill),以及绘制其边框(setStroke)。这些颜色将在调用相应的动作来使用它们时应用,例如 bezier.fill()。
还有更多...
有时候我们需要指示在游乐场中显示一个视图,为此我们有一个名为 XCPShowView 的函数,所以这是可视化对象的另一种方式。在下一章中,我们将学习如何调试我们的应用程序;当我们有太多代码且无法在游乐场中测试时,这非常有用。
丰富的文档
提供清晰简洁的文档将直接揭示代码的结构和功能。这对与你代码合作的同事以及你在回顾之前所写的代码时非常有帮助。此外,Xcode 可以利用这些文档,并在代码使用的任何地方提供即时快速查看信息,这样你就不必在多个文件之间切换以进行参考。
在这个示例中,我们将使用 Xcode 7 中 Swift 的新文档语法来对一个通用类声明进行文档化,并记录其方法和属性。
准备工作
创建一个新的游乐场,并将其命名为 Documentation。这个示例不需要项目文件,所以不用担心任何项目设置。
如何操作...
-
将以下代码复制到你的游乐场中作为我们的起点:
import UIKit class SomeSimpleClass: NSObject { var someVar: NSString? var someOtherVar: NSString? func doSomething() { print("Doing something...") } func doSomethingWithStuff(parameterOne: String, parameterTwo: Bool) -> String { print("This is parameter one: \(parameterOne)") print("This is parameter two: \(parameterTwo)") return "Some kind of string." } }如你所见,这个类包含几个变量和两个方法;一个带有参数和返回值,另一个则没有。
-
首先,让我们给类本身添加文档。在类声明上方添加以下代码:
/// A simple class that does some stuff you want it to do.注意
///表示单行注释。这对于不需要任何参数或返回值的文档来说很方便。Xcode 会识别///并解析信息以供快速查看访问。为了测试它,按住选项键点击
SomeSimpleClass,你应该会看到以下截图:![如何操作…]()
-
在
doSomething()方法正上方的一行添加以下代码,并按住选项键点击以验证其是否正常工作:/// Does something; in this case, prints out a message letting the user know that something is happening. -
现在我们已经覆盖了单行文档,让我们给
doSomethingWithStuff()方法添加文档。在方法声明上方添加以下代码:/** This method does something, but accepts parameters and also return a String value. Lets add some explicit documentation for these items. - Parameter parameterOne: The first parameter to be passed in. - Parameter parameterTwo: The second parameter to be passed in. - Returns: A string value */ -
对于多行文档,我们希望在
/** */之间放置所有文档。Xcode 将相应地解析这些行以快速查看。此外,我们还可以显式地定义参数和返回类型。点击doSomethingWithStuff()方法的选项,可以看到以下快速查看视图:![如何操作…]()
-
将上一步中的代码块更新为以下代码:
/** This method does something, but accepts parameters and also return a String value. Lets add some explicit documentation for these items. - Parameters: - parameterOne: The first parameter to be passed in. - parameterTwo: The second parameter to be passed in. - Returns: A string value */当处理多个参数时,指定参数部分而不是每个单独的参数是一个好的做法(并且更容易阅读)。
-
Swift 还允许添加额外的描述字段。将以下代码添加到
doSomethingWithStuff()文档块中:- Author: Kyle Begeman - Version: 1.04 - Note: This is a simple class - Warning: This class doesn't actually do anything! - ToDo: Add some actual functionality to this class您应该看到以下屏幕:
![如何操作…]()
小贴士
要查看可用的字段列表,请访问以下 Apple 文档:
developer.apple.com/library/ios/documentation/Xcode/Reference/xcode_markup_formatting_ref/ -
Xcode 还允许使用标题、格式化和有序/无序列表。将以下代码添加到文档块中:
# Lists You can apply *italic*, **bold**, or `code` inline styles to any text. ## Unordered Lists - Some item - Another item - The last item ## Ordered Lists 1\. Some item 2\. Another item 3\. The last itemXcode 支持三种标题:
#代表一级标题;##和###分别代表二级和三级标题:![如何操作…]()
它是如何工作的…
Xcode 将自动解析添加的文档,并根据文档块中使用的关键字对其进行组织。这不需要额外的设置,并且将在运行 Xcode 7 的任何计算机上工作。
更多内容…
有时文档可能需要以类似说明书的方式格式化,以便有多个页面以更好地组织。利用游乐场的功能,您可以提供具有多个页面的详细文档,并在它们之间建立链接以便于访问。这将在下一个食谱中介绍。
在游乐场中导航页面
游乐场正变得越来越强大,随着 Apple 不断添加功能。一个很棒的新特性是能够在单个游乐场中导航页面。这样做允许您以更组织化的方式格式化文档。此外,您甚至可以将文档和代码结构化得类似于数字书,并允许读者轻松导航。
准备工作
创建一个新的游乐场,并将其命名为Paging。本食谱不需要项目文件,因此请不用担心任何项目设置。接下来,选择文件 | 新建 | 游乐场 页面。在右侧的导航器中,您应该看到两个页面:未命名页面和未命名页面 2。分别将它们重命名为Intro和Start Now。
如何操作…
-
将以下代码添加到 Intro 页面的底部:
//: NextXcode 会将
//:识别为类似于///的标记命令。页面之间的导航使用标准的格式链接标题。在这个例子中,我们命名链接为Next,并使用标准的@next选项让 Xcode 知道要导航到下一页(基于项目顺序)。 -
从顶部菜单选择编辑器 | 显示渲染标记。为了查看我们的导航,我们需要 Xcode 根据提供的标记进行渲染。您应该看到类似以下内容:
![如何操作…]()
-
选择下一个按钮将自动带您到之前创建的
Start页面。您会注意到新页面的样板代码会自动生成其自己的上一页和下一页链接。选择编辑器 | 显示原始标记来查看代码。所有内容都是相同的,除了使用@previous作为上一页的链接位置。 -
返回到
Intro页面,并将以下代码添加到文件末尾://: Start Page通过指定页面名称作为位置,我们可以直接导航到指定的页面。
注意
注意,带有空格的页面名称需要使用
%20ASCII 空格代码才能正常工作。
它是如何工作的…
就像所有其他文档和标记一样,Xcode 会自动将文本解析为正确的命令,并正确链接所有内容。
参见
- 虽然我们已经涵盖了所有基础知识,但苹果公司仍提供了额外的功能来处理文档和游乐场。更多信息,请访问此链接:
developer.apple.com/library/ios/documentation/Xcode/Reference/xcode_markup_formatting_ref/
第七章. 使用 Xcode 进行 Swift 调试
在本章中,我们将介绍以下食谱:
-
验证值 - 开发税收收入模拟应用程序
-
使用 Xcode 和 Swift 进行调试 - 最佳检查运动
-
使用 LLDB 进行调试
-
应用程序分析
-
Swift 2.0 中的错误处理
-
Swift 2.0 中的自定义错误处理
-
Swift 2.0 中的可用性检查
简介
让我们面对现实,没有人会编写一个没有任何问题的完整程序。即使是最好的程序员也会遇到一些未被考虑的情况。有时找到错误很容易,有时却非常困难,主要是因为你有一个多线程应用程序。
在本章中,我们将逐步学习如何调试应用程序,这将使你更容易找出问题所在。
验证值 - 开发税收收入模拟应用程序
在编写代码和开发应用程序时,我们知道函数中的一些变量不应该包含某些值,但你确定吗?我们如何开发和检查一切是否具有正确的值?
在这个食谱中,我们将学习如何在开发阶段检查值是否正确。为了模拟这种情况,让我们创建一个应用程序,我们确信会有一些人试图篡改值;在这种情况下,让我们创建一个计算税收收入的程序。
准备工作
创建一个名为 Chapter 7 Tax Income 的项目,并确保你处于调试模式;要做到这一点,请点击项目方案并选择 编辑方案…,如下截图所示:

点击左侧的 运行 选项,然后确保选择 信息 选项卡,最后确保 构建 选项在 调试 上。之后我们将有一些解释:

一旦检查了这些步骤,我们需要打开项目设置并创建一个名为 DEBUG_MODE 的宏,仅在调试配置中。

如何做到这一点...
-
首先,让我们创建一个名为
Assertions.swift的新文件。在这里,我们将添加一些函数,所有这些函数都以assert_开头:// element must be one of the elements in the set func assert_in<T:Comparable>(element: @autoclosure () -> T, set: [T], message: @autoclosure () -> String){ #if DEBUG_MODE if set.count == 0 { println("warning: comparing with an empty set") } assert(set.filter({(currentElement) -> Bool in return currentElement == element() }).count > 0, message) #endif } // element must be greater or equal to the other value func assert_ge<T:Comparable>(value:T, otherValue:T, message: @autoclosure () -> String){ #if DEBUG_MODE assert(value >= otherValue , message) #endif } // element can't be nil func assert_not_nil<T>(element:@autoclosure () -> T?, message: @autoclosure () -> String){ if element() == nil { #if DEBUG_MODE assertionFailure(message) #endif } } -
现在我们可以创建另一个文件,该文件将包含一个能够计算所得税的类。创建一个名为
IncomeTaxCalculator.swift的新文件;以下是一个类头:class IncomeTaxCalculator: CustomStringConvertable { -
下一步是添加其属性;正如你可以想象的,我们必须为它存储一些值:
var title:String? var name:String? lazy var grossIncome:Double = 0.0 lazy var netIncome:Double = 0.0 lazy var children:Int = 0 lazy var education:Double = 0.0 -
如你所见,所有这些都是在初始化或作为选项;在这种情况下,你不需要创建初始化器,除非你正在使用 Swift 的早期版本之一:
init(){} -
如果你注意到了,这个类必须实现
Printable协议。因此,我们需要向这个类添加描述:var description: String { assert_not_nil(self.title, "Title cant be nil") assert_not_nil(self.name, "Name cant be nil") assert_in(self.title!, ["Mr", "Dr", "Miss", "Mrs"], "Wrong title") return "\(self.title!) \(self.name!) - \(self.calculate())" } -
为了完成这个类,我们需要一个方法来根据之前的属性计算所得税。当然,这是一个虚构的情况;不要使用这个应用程序来计算你的所得税:
func calculate() -> Double { assert_ge(self.grossIncome, 0.0, "Gross income can't be negative") assert_ge(self.netIncome, 0.0, "Net income can't be negative") assert_ge(self.grossIncome, self.netIncome, "Net income cant be negative") let totalAlreadyPaid = self.grossIncome - self.netIncome var percentage:Double if self.grossIncome <= 9000.0 { percentage = 0.0 } else if self.grossIncome <= 18000.0 { percentage = 0.15 } else { percentage = 0.40 } let childrenBonus = Double(self.children) * 100.0 // 10 percent of education up to 1000 per child var educationBonus:Double var educationLimit = Double(self.children) * 1000.0 if 0.1 * self.education < educationLimit { educationBonus = 0.1 * self.education }else { educationBonus = educationLimit } return self.grossIncome * percentage - childrenBonus - educationBonus - totalAlreadyPaid } } -
一旦完成,我们必须创建我们应用程序的图形部分,所以转到故事板并创建一个包含六个文本框、一个按钮和六个标签的布局;类似于以下的一个:
![如何操作…]()
-
如你所想,我们需要创建一些属性来与文本框链接:
@IBOutlet var titleTextField: UITextField! @IBOutlet var nameTextField: UITextField! @IBOutlet var lastYearIncomeTextField: UITextField! @IBOutlet var numberOfChildrenTextField: UITextField! @IBOutlet var LastYearNetIncome: UITextField! @IBOutlet var educationTextField: UITextField! -
为了完成我们的图形部分,我们需要为按钮添加一个事件:
@IBAction func calculateAction(sender: UIButton) { assert(countElements(self.nameTextField.text) >= 5, "Your name looks too short") var error:NSError? let regex = NSRegularExpression(pattern: "^[0-9]+[.[0-9]+]?$", options: .CaseInsensitive, error: &error)! if regex.matchesInString(self.lastYearIncomeTextField.text, options: nil, range: NSMakeRange(0, countElements(self.lastYearIncomeTextField.text))).count == 0{ assertionFailure("Gross Income tax: wrong format") } let income = (self.lastYearIncomeTextField.text as NSString).doubleValue let incomeTaxCalculator:IncomeTaxCalculator = IncomeTaxCalculator() incomeTaxCalculator.title = self.titleTextField.text incomeTaxCalculator.name = self.nameTextField.text incomeTaxCalculator.grossIncome = (self.lastYearIncomeTextField.text as NSString).doubleValue incomeTaxCalculator.netIncome = (self.LastYearNetIncome.text as NSString).doubleValue incomeTaxCalculator.education = (self.educationTextField.text as NSString).doubleValue incomeTaxCalculator.children = self.numberOfChildrenTextField.text.toInt()! UIAlertView(title: "Income Tax", message: incomeTaxCalculator.description, delegate:nil, cancelButtonTitle:"Ok").show() } -
最后一步是测试我们的应用程序并观察断言的工作情况。按播放键,当应用程序出现时,按计算键,不要在文本框中添加任何信息。你应该看到应用程序停止运行,Xcode 应该显示在哪里:
![如何操作…]()
-
如果你注意的话,日志控制台会打开并显示发生了什么,打印出你写入的消息:
![如何操作…]()
-
让我们完成这个配方,将配置从Debug更改为Release;再次按播放键,你会看到第一个断言被忽略了。
它是如何工作的…
断言就像在找到意外值时中断应用程序的函数。当然,当程序发布时,它需要接受这些值,这意味着开发者不应该永远依赖断言的力量,他必须修复值,或者至少中断进程。
Swift 只提供了两个断言函数:
-
assert:此函数有两个参数。第一个是一个布尔元素,如果为假,将停止你的程序并显示下一个参数(消息)给开发者。 -
assertionFailure:此函数在未检查任何条件的情况下停止程序执行。此函数用于应用程序不应该通过的情况。想象一下,你有一个switch语句,理论上你的程序不应该进入默认情况,因为它没有被考虑,在这种情况下你需要添加一个断言。
另一个令人烦恼的问题是:autoclosure是什么意思?原因如下——assert、assertionFailure以及我们的断言函数实际上不接受值作为参数,原因是惰性。这意味着 Swift 在进入函数之前不会评估值。
Swift 将你的参数转换为一个函数,并且当断言函数调用它时,参数将在其中被评估。为什么?原因在于断言不应该在编译配置为 Release 而不是 Debug 时工作。记住,断言会停止你的程序,这在开发时是好事,但对于用户来说却不是好感觉。这就是为什么有时我们在#ifdef NDEBUG和#endif之间调用 autoclosure 函数的原因。
小贴士
在 Xcode 6 beta 5 之前,@autoclosure通常写作@auto_closure。如果你打算在网上搜索有关此修饰符的信息,尝试两种写法都试一试。
如果断言在发布模式下不起作用,为什么它们还有用?原因在于断言用于检测开发错误,你应该追踪错误的来源并修改它,以确保来源不会给你一个错误值。
我们为我们的断言函数创建了一个不同的文件,原因是你可能在这个文件之间共享项目;当然,你可以为所有事情使用 assert 函数,但我建议创建可以为我们节省工作的断言函数,比如我们用来检查元素是否在数组中的那个。
还有更多…
在这道菜谱中,我们学习了如何使用 Swift 断言,它是 Objective-C 中的 NSAssert 的等价物。如果你更喜欢使用 Objective-C 基础中的其他断言功能,如 NSParameterAssert 和 NSAssertionHandler,你仍然可以在 Swift 中使用它们。
断言对于找到接收错误值的路径非常有用,然而有时有必要逐步进入代码。我们将在下一道菜谱中看到如何使用 Xcode 和 Swift 来实现。
使用 Xcode 和 Swift 进行调试 – 最佳的棋子移动
如果你有一些编程经验,你就会知道有时我们需要一步一步地进入代码,主要是在我们遇到那种没有人知道为什么会发生的问题时。
在这道菜谱中,我们将学习如何使用 Xcode 和 Swift 进行调试。为了做到这一点,我们将回收我们的棋盘。在这种情况下,我们将使用白棋进行最佳移动。我们还将为这个应用程序的第二个版本做好准备,届时我们可以使用国王。
我们一开始不会创建正确的算法,我们的想法是调试并找到问题所在,然后我们将在之后进行修正。
准备工作
打开你上一章中的游乐场,其中包含棋盘。保持它打开,因为我们将要重用这段代码。一旦打开,创建一个名为 第七章棋盘 的新项目,然后开始编码。
如何操作…
-
创建一个名为
CheckersBoard.swift的新文件,如果尚未导入,请先导入UIKit库:import UIKit -
之后,你可以将游乐场中的类代码粘贴过来。然后,复制游乐场中的最后一部分(变量实例化和设置),并将其粘贴到视图控制器文件中的
viewDidLoad方法上:override func viewDidLoad() { super.viewDidLoad() var board = CheckersBoard() board.board[2][5] = .WHITE board.board[2][3] = .BLACK board.board[4][3] = .WHITE board.board[2][1] = .BLACK board.board[0][5] = .WHITE board.board[1][0] = .BLACK board.board[7][6] = .WHITE board.board[5][6] = .BLACK } -
现在我们来在第三次分配棋子时设置一个断点,通过点击代码左侧的灰色区域:
![如何操作…]()
-
按下播放,等待 Xcode 在你的断点处停止,并将鼠标指针移到棋盘变量上。它可以是任何一个,比如第一个,变量正在被声明,或者其他的,我们在那里分配棋子。
-
在短暂的间隔后,你将看到一个带有两个图标的小对话框。第一个图标是一个带有圆圈的i,它以类似于 JSON 格式的文本格式显示对象的内容:
![如何操作…]()
另一个图标对我们来说非常熟悉,因为它和我们在游乐场看到的图标一样,它是快速查看图标,我们也可以在这里使用它。这样我们就可以以更直观的方式查看跳棋棋盘:
![如何操作…]()
小贴士
不要为每个类创建
debugQuickLookObject方法,只为那些难以可视化且需要频繁调试的类创建。 -
现在我们知道了如何可视化我们的对象,我们需要区分传统棋子和国王。我们的下一个目标是向
BoardSpace枚举添加一个布尔值。用以下枚举替换之前的枚举:enum BoardSpace { case FREE, WHITE(Bool), BLACK(Bool) } -
现在我们需要显示不同的内容,在这种情况下,我们将在棋子上添加字母
K。将debugQuickLookObject中的switch语句替换为以下内容:var isKing = false; switch board[row][col] { case .WHITE(let king): isKing = king UIColor.whiteColor().setFill() case .BLACK(let king): isKing = king UIColor.brownColor().setFill() default: continue } -
好的,现在在填充棋子之后,我们需要添加一个代码块,以处理当前棋子是国王的情况:
... bezier.fill() if isKing { ("K" as NSString).drawAtPoint(CGPointMake(offsetx + CGFloat( squareSize / 3 ) , offsety + CGFloat(squareSize / 5)), withAttributes: nil) } -
现在我们需要改变分配棋子的方式,如下所示:
let board = CheckersBoard() board.board[2][5] = .WHITE(false) board.board[2][3] = .BLACK(false) board.board[4][3] = .WHITE(true) board.board[2][1] = .BLACK(false) board.board[0][5] = .WHITE(false) board.board[1][0] = .BLACK(false) board.board[7][6] = .WHITE(false) board.board[5][6] = .BLACK(false) -
通过按下播放并使用快速查看观察棋盘来重复此操作;现在你可以按步骤覆盖按钮,只需按下F6键。
注意
新款苹果键盘为F6键分配了功能,所以你可能需要同时按下这个键和fn键。
-
现在检查快速查看,看看你的一枚棋子上是否有字母
K:![如何操作…]()
-
好的,现在我们知道了如何可视化棋盘,我们将看看是否可以用一回合赢得游戏。要做到这一点,我们需要选择一个棋子并查看我们所有的可能性。这意味着我们需要频繁地克隆我们的对象。通常我们会为它创建一个结构体,但在这个情况下,我们将为克隆我们的对象创建一个方法。我们稍后会给出解释;只需将此代码添加到你的
CheckersBoard类中:func clone() -> CheckersBoard { let board = CheckersBoard() for i in 0..<8 { for j in 0..<8 { switch self.board[i][j] { case .FREE: continue default: board.board[i][j] = self.board[i][j] } } } return board } -
接下来的步骤是担心如何捕获对手的棋子,要做到这一点,我们需要测试哪些方向是可能的。让我们在我们的
CheckersBoard类中创建一个枚举来帮助我们:enum Direction { case NORTHWEST, NORTHEAST, SOUTHWEST, SOUTHEAST } -
现在我们需要创建一个方法来找到最佳移动;记住我们只使用白棋:
func bestMovementWhite() -> CheckersBoard?{ var boardCandidate:CheckersBoard? for i in 0..<8 { for j in 1..<8 { var result: CheckersBoard? switch self.board[i][j]{ case .WHITE(let king): if king { result = bestMovementKingWhite(i, y: j) }else { result = bestMovementSinglePieceWhite(i, y: j) } default: continue } if let boardFound = result { if let currentBoard = boardCandidate { if currentBoard.countBlack() > boardFound.countBlack() { boardCandidate = boardFound } }else { boardCandidate = boardFound } } } } return boardCandidate } -
如你所见,我们有两个私有方法,一个用于普通棋子的最佳移动,另一个用于国王。在这里,我们将开发单个棋子的功能,其他的方法我将留作你的作业;否则,这将是一个非常庞大的食谱:
private func bestMovementSinglePieceWhite(x:Int, y:Int) -> CheckersBoard { var clonedBoard = self.clone() if clonedBoard.capture(x, y: y, direction: .NORTHWEST) { return clonedBoard.bestMovementSinglePieceWhite(-1, y: -1) } if clonedBoard.capture(x, y: y, direction: .NORTHEAST) { return clonedBoard.bestMovementSinglePieceWhite(1, y: -1) } if clonedBoard.capture(x, y: y, direction: .SOUTHWEST) { return clonedBoard.bestMovementSinglePieceWhite(-1, y: 1) } if clonedBoard.capture(x, y: y, direction: .SOUTHEAST) { return clonedBoard.bestMovementSinglePieceWhite(1, y: 1) } return clonedBoard } private func bestMovementKingWhite(x:Int, y:Int) -> CheckersBoard? { // TODO Homework return nil } -
要完成这个课程,我们需要添加更多方法,一个用于计算黑棋的数量,另一个用于捕获对手的棋子:
func countBlack() -> Int{ var total = 0 for row in self.board { for element in row { switch element { case .BLACK: total++ default: continue } } } return total } private func capture(x: Int, y:Int, direction: Direction) -> Bool { var offset_x:Int var offset_y:Int switch(direction){ case .NORTHWEST: offset_x = -1 offset_y = -1 case .NORTHEAST: offset_x = 1 offset_y = -1 case .SOUTHWEST: offset_x = -1 offset_y = 1 case .SOUTHEAST: offset_x = 1 offset_y = 1 } if x + 2*offset_x >= 0 && y + 2*offset_y >= 0 && x + 2*offset_x < 8 && y + 2*offset_y < 8 { // we are inside the board range switch (board[x + 2*offset_x][y+2*offset_y], board[x + offset_x][y+offset_y]) { case (.FREE, .BLACK): board[x + offset_x][y+offset_y] = .FREE board[x + 2 * offset_x ][ y + 2 * offset_y] = board[x][y] board[x][y] = .FREE return true default: return false } }else { return false } }好的,现在想象一下今天是星期一。我们不得不输入这么多代码,有人(可能是你的老板)告诉我们有一个设置找不到最佳解决方案。审查代码的每一行可能非常无聊,而且很难找到问题所在。
-
让我们从那个不起作用的设置开始调试,所以用这个代码替换设置代码:
let board = CheckersBoard() board.board[0][1] = .WHITE(false) board.board[0][5] = .WHITE(false) board.board[2][7] = .WHITE(false) board.board[3][2] = .WHITE(false) board.board[2][1] = .BLACK(false) board.board[4][3] = .BLACK(false) board.board[4][5] = .BLACK(false) board.board[2][5] = .BLACK(false) board.board[2][3] = .BLACK(false) if let finalBoard = board.bestMovementWhite() { println(finalBoard.debugDescription) }else { println("no solution") }你可能首先注意到的是
debugDescription。这类似于debugQuickLookObject,但它不返回不同类型的对象,而只返回文本,我们稍后会完成这个属性。如你所知,我们必须调试第四个白棋的移动。要做到这一点,转到名为
bestMovementWhite的方法,并在调用bestMovementSinglePieceWhite方法的行上添加一个断点。右键单击此断点,你会看到一个如下所示的菜单:![如何操作…]()
-
选择编辑断点...,正如你所知,我们不想检查前三个棋子的动作,所以说明你想要忽略3次然后停止:
![如何操作…]()
-
有时候我们知道在经过一定次数后必须停止,尝试使用它而不是多次点击播放。在这个相同的方法末尾(
return boardCandidate)添加另一个断点并编辑它。现在我们将添加一个条件使其工作,并将动作更改为声音。不幸的是,大多数可用的声音都太弱了,我通常选择玻璃声,但如果你更喜欢,你可以选择另一个。要完成这个断点,选择在评估动作后自动继续选项:![如何操作…]()
-
现在按播放并等待 Xcode 停止,按步骤进入按钮(指向下的箭头按钮)或按F7,你将进入
bestMovementSinglePieceWhite。现在点击步骤跳过(或按F6)三次,用快速查看检查棋盘,看看我们将返回这个棋盘。一旦这个问题被发现,我们可以用这个算法替换我们的算法:private func bestMovementSinglePieceWhite(x:Int, y:Int) -> CheckersBoard { var clonedBoard = self.clone() var winner:CheckersBoard = self if clonedBoard.capture(x, y: y, direction: .NORTHWEST) { winner = clonedBoard.bestMovementSinglePieceWhite(x-2, y: y-2) } clonedBoard = self.clone() if clonedBoard.capture(x, y: y, direction: .NORTHEAST) { clonedBoard = clonedBoard.bestMovementSinglePieceWhite(x+2, y: y-2) if winner.countBlack() > clonedBoard.countBlack() { winner = clonedBoard } } clonedBoard = self.clone() if clonedBoard.capture(x, y: y, direction: .SOUTHWEST) { clonedBoard = clonedBoard.bestMovementSinglePieceWhite(x-2, y: y+2) if winner.countBlack() > clonedBoard.countBlack() { winner = clonedBoard } } clonedBoard = self.clone() if clonedBoard.capture(x, y: y, direction: .SOUTHEAST) { clonedBoard = clonedBoard.bestMovementSinglePieceWhite(x+2, y: y+2) if winner.countBlack() > clonedBoard.countBlack() { winner = clonedBoard } } return winner } -
现在再次按播放,但不要按步骤进入,而是使用步骤跳过,用快速查看检查器检查棋盘,看看你现在是否有了正确的解决方案。所以,问题解决了,但不要过于庆祝,其他问题还在路上。
它是如何工作的…
Xcode 允许我们逐步调试。使用我们在我们的游乐场中调试的方法(debugQuickLookObject)可以帮助我们可视化当前对象的状态。在这种情况下,我们可以使用 NSString 的drawAtPoint方法改进我们的方法,但目前 Swift 字符串上没有等效的方法。
你可能还注意到,我们创建了自己的方法来克隆棋盘而不是使用结构体。原因是结构体不能继承,在这种情况下我们需要这样做,因为我们的快速查看方法。
当使用 Xcode 时,断点有一些特殊功能。您可以忽略它们几次,这在您知道问题发生在某些重复之后非常有用。想象一下,如果您必须按 50 次继续,然后您必须一次又一次地重复它,直到找到解决方案。您和您的鼠标都会在一天结束时筋疲力尽。
如您所见,您还可以向断点添加一个动作,例如添加一个调试器命令、播放一个声音(这对于知道后台已经完成了一些操作非常有用,但您不想停止),或者记录一条有助于我们分析程序轨迹的消息。
对于调试,有一些命令是开发者必须知道的,例如 step over(跳过),它执行整个代码行并停止在下一行,step into(进入),它进入当前函数,step out(F8)(退出),它从当前函数退出并停在调用它的同一位置,以及 continue(control + command + Y)(继续),它继续执行程序直到下一个断点。
不幸的是,没有正确或错误的方法来找到问题,有时您必须使用您的第六感来解决它。唯一的方法是收集尽可能多的信息,并一步一步地进行。
还有更多…
实际上,Xcode 并不是自己进行调试,现实是它使用另一个调试器来完成这项任务;在下一个菜谱中,我们将使用命令行调试器。
使用 LLDB 进行调试
使用 Xcode 进行调试是不错的,但有时我们受到限制,必须使用更低级别的调试器。为了做到这一点,我们必须知道实际上,Xcode 并没有在调试任何东西,它使用另一个名为 LLDB 的程序。
小贴士
旧版本的 Xcode 以前使用 GDB 而不是 LLDB 进行调试,不要尝试用 Swift 使用它们,因为 GDB 不支持 Swift。
如果您想调试大型程序,强烈建议您了解 LLDB 命令,还有一些情况下您必须连接到另一台机器(例如持续集成),在那里您必须通过 SSH shell 做所有事情。
准备工作
打开检查棋盘程序,检查您是否有任何断点,并删除所有断点。
如何操作…
-
按下播放按钮,当程序开始时,使用组合键 control + command + U 按下暂停按钮。之后,您将看到 LLDB 控制台:
![如何操作…]()
-
现在点击 LLDB 控制台,输入
break s -r bestMovement*。您将看到答案是Breakpoint 1: 4 locations。然后让我们列出这些断点,使用断点列表。在这里,您可能会看到一个丑陋的答案,但不要害怕,这比您想象的要简单。 -
您还可以输入
thread info来获取一些关于当前线程的信息。检查它是否指定了每个帧的语言,有了这些信息,您可以在程序崩溃的情况下更好地了解您的代码发生了什么。 -
使用
thread list列出你的线程,使用thread select 2切换到线程 2,然后再次写入thread info。 -
现在输入
repl并查看提示符是否变为1>。在这种情况下,我们将编写一个新的函数来测试我们的 REPL,所以输入以下代码:func num0(myArray:[Int]) -> Int { return myArray.reduce(0, { if $1 == 0 { return $0 + 1 }else { return $0 }}) } -
一旦这个函数完成,让我们来测试它:
num0([1,3,1,0,0,4,1])
正如你所见,repl可以帮助你在运行时编写函数,就像我们在 playground 中做的那样。
它是如何工作的…
LLDB 是当前的 Xcode 调试器;你可以使用正则表达式设置多个断点。记住,你在 LLDB 上所做的操作不一定会在 Xcode 中反映出来,我们设置的断点是一个很好的例子。
注意,我们的三个断点都有文件名和行号,例如... at CheckersBoard.swift:158, …这意味着你的断点在文件CheckersBoard.swift的第 158 行。
我们还使用了 REPL,这是一个 Swift 命令行,在这里你可以创建函数并测试一些代码。当然,你也可以使用 playground,但有时使用当前的调试器会更快。
参考信息
-
苹果有一些关于 LLDB 的文档值得一看。检查这个 URL:
developer.apple.com/library/ios/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/Introduction.html。 -
对于用户抱怨但难以调试的功能,例如内存、性能或能耗,该怎么办?对于这类问题,你必须使用另一个工具,我们将在下一个菜谱中学习它。
分析应用程序
很常见听到问题,但如果一个应用程序没有任何重要的问题,并不意味着它运行良好。想象一下,你有一个有内存泄漏的程序。你可能在使用 10 分钟内不会发现任何问题,然而,用户可能在几天后就会发现问题。不要认为这种情况不可能发生,记住 iOS 应用程序不会终止,所以如果你有内存泄漏,它将一直保持到你的应用程序崩溃。
性能是另一个常见的话题,如果你的应用程序看起来没问题,但随着时间的推移变得缓慢,该怎么办?我们必须意识到这个问题。这类测试被称为性能分析,Xcode 提供了一个非常好的工具来实现这个操作,它被称为Instruments。
在这种情况下,我们将分析我们的应用程序以可视化应用程序浪费的能量,当然,我们也要尝试减少它。
准备工作
对于这个菜谱,你需要一个物理设备,并且为了在设备上安装你的应用程序,你需要注册 Apple 开发者计划。如果你满足这两个条件,下一步你要做的是创建一个名为第七章 能量的新项目。
如何做…
-
在我们开始编码之前,我们需要将一个框架添加到项目中。点击项目中的构建阶段选项卡,转到链接二进制与库部分,然后按加号:
![如何做…]()
-
一旦 Xcode 打开一个对话框询问要添加的框架,请选择CoreLocation和MapKit。
-
现在转到故事板,放置一个标签和一个 MapKit 视图,你的布局可能类似于以下这个:
![如何操作…]()
-
将 MapKit 视图链接并命名为 map,将
UILabel命名为 label:@IBOutlet var label: UILabel! @IBOutlet var map: MKMapView! -
继续在视图控制器中操作,让我们点击文件开头以添加
CoreLocation和MapKit导入:import CoreLocation import MapKit -
之后,你必须在
viewDidLoad方法中初始化位置管理器对象:override func viewDidLoad() { super.viewDidLoad() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() }在这个时候,你可能遇到错误,因为你的视图控制器没有遵循
CLLocationManagerDelegate协议,所以让我们转到视图控制器类的头文件,并指定它实现了这个协议。我们遇到的另一个错误是locationManager变量,因为它没有被声明,因此我们必须将其创建为一个属性。在我们声明属性的同时,我们将添加地理编码器,它将被稍后使用:class ViewController: UIViewController, CLLocationManagerDelegate { var locationManager = CLLocationManager() var geocoder = CLGeocoder() -
在我们实现接收定位的方法之前,让我们创建另一个方法来检测是否有任何授权错误:
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) { var locationStatus:String switch status { case CLAuthorizationStatus.Restricted: locationStatus = "Access: Restricted" break case CLAuthorizationStatus.Denied: locationStatus = "Access: Denied" break case CLAuthorizationStatus.NotDetermined: locationStatus = "Access: NotDetermined" break default: locationStatus = "Access: Allowed" } NSLog(locationStatus) } -
然后,我们可以实现更新我们位置的方法:
func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject]) { if locations[0] is CLLocation { let location:CLLocation = locations[0] as CLLocation self.map.setRegion(MKCoordinateRegionMakeWithDistance(location.coordinate, 800,800), animated: true) geocoder.reverseGeocodeLocation(location, completionHandler: { (addresses, error) -> Void in let placeMarket:CLPlacemark = addresses[0] as CLPlacemark let curraddress:String = (placeMarket.addressDictionary["FormattedAddressLines"] as [String]) [0] as String self.label.text = "You are at \(curraddress)" }) } } -
在测试应用程序之前,还有另一个步骤要做。在你的项目中,导航到或点击展开“支持文件”,然后点击info.plist。通过右键点击列表并选择添加行来添加一行。
-
在这一新行中,将
NSLocationWhenInUseUsageDescription作为键,在值中输入“权限要求”,如下截图所示:![如何操作…]()
-
选择一个设备并将此应用程序安装到该设备上,并在你街道周围(或者如果你想的话,在地球周围行走)测试应用程序。检查标签是否会改变,同时地图也会显示你的当前位置。
-
现在回到你的电脑上,再次将设备连接上,现在不是按播放键,而是要按住播放键,直到你看到更多选项,然后你必须选择配置文件选项:
![如何操作…]()
-
接下来会发生的事情是,将打开“仪器”,可能弹出一个对话框要求管理员账户。这是由于“仪器”需要使用一些特殊权限来访问一些底层信息:
![如何操作…]()
-
在下一个对话框中,你会看到不同种类的仪器,其中一些是针对 OS X 的,一些是针对 iOS 的,还有一些是针对两者的。如果你选择了错误的平台仪器,记录按钮将会被禁用。对于这个菜谱,请选择能量诊断:
![如何操作…]()
-
一旦打开能量诊断窗口,您就可以按下记录按钮,它位于左上角,并尝试四处移动——是的,您需要保持设备与电脑连接,因此您必须同时移动这两个元素——并使用您的设备执行一些操作,例如按下主页按钮和关闭屏幕。现在您可能有一个类似于这样的屏幕:
![如何操作…]()
-
现在,您可以使用它来分析谁在您的应用上消耗了更多的能量。为了更好地了解这一点,请进入您的代码,将常量
kCLLocationAccuracyBest替换为kCLLocationAccuracyThreeKilometers并检查您是否节省了一些能量。
它是如何工作的…
Instruments 是一个用于分析您应用程序的工具。它提供了有关您的应用程序的信息,这些信息无法通过代码获取,或者至少无法轻松获取。您可以检查您的应用程序是否有内存泄漏,是否正在失去性能,以及如您所见,是否正在浪费大量能量。
在这个食谱中,我们使用了 GPS,因为它是一个需要一些能量的传感器,并且您还可以检查您的仪器底部的表格,以查看是否进行了网络请求,如果您非常频繁地这样做,这也会快速耗尽您的电池。
您可能会问的问题之一是:为什么我们必须更改info.plist?自 iOS 8 以来,一些传感器需要用户权限;GPS 就是其中之一,因此您需要报告将要显示给用户的消息。
相关内容
-
建议您了解仪器的工作方式,主要是您将要使用的那部分。请查看 Apple 关于仪器的文档以获取更多详细信息:
developer.apple.com/library/watchos/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/index.html。 -
如果您有 Objective-C 的经验,您可能会问如何使用 Swift 回收代码,在下一章中,我们将看到一些关于这个的食谱。
Swift 2.0 中的错误处理
随着 Swift 2.0 的引入,Apple 提供了一种全新的错误处理方式,类似于其他编程语言。您不仅可以在自己的类中利用这一点,Apple 还更新了其所有类以使用新的错误处理方法。
准备工作
创建一个新的单视图项目,并将其命名为Error Handling Chapter 7。我们将为此食谱使用模板项目设置。
如何操作…
-
打开
ViewController.swift并添加以下函数:func performAnErrorProneTask() { var error: NSError? var someString: String = "string" var someURL: NSURL = NSURL(string: "http://www.someurl.com")! let success = someString.writeToURL(someURL, atomically: true, encoding: NSUTF8StringEncoding, error: &error) if !success { print("Error writing to URL: \(error!)") } }注意
运行此代码后,您应该收到一个错误
调用中多余的参数 'Error'。在 Swift 2.0 中,您不再需要传递错误对象的内存地址,因此错误参数不再需要,并且已从 Apple 标准库中删除。 -
更新方法调用中的代码如下:
let someString: String = "string" let someURL: NSURL = NSURL(string: "http://www.someurl.com")! do { let success = try someString.writeToURL(someURL, atomically: true, encoding: NSUTF8StringEncoding) } catch let error as NSError { print(error.localizedDescription) }
它是如何工作的…
与 C++ 类似,Swift 2.0 现在也使用 do-try-catch 方法进行错误处理。在 do 块中,我们尝试执行可能引发错误的某些任务,并在前面加上 try 关键字。这会让 Xcode 知道这个方法可能会失败,并在发生失败时寻找 catch 块。
您可以通过查看方法声明来了解哪些方法需要这种错误处理。所有具有错误处理的方法将在声明末尾包含一个 throws 关键字。我们将在下一个食谱中学习如何定义自己的方法,这些方法包含错误处理。
此外,try 关键字可以与 ? 和 ! 一起使用以提供额外的功能。例如,使用 try? 将返回值包装在可选值中。您不必执行 do-while 循环并捕获错误来将值设置为 nil,只需简单地替换 try?,值现在将作为具有标准 nil 处理的可选值工作。当使用 try! 时,您明确忽略错误处理。这仅在您知道值不会产生错误时使用;例如,加载与应用程序一起分发的图像。
还有更多…
您通常会需要一些代码在出现错误的情况下也能运行。Swift 2.0 添加了一个非常适合这种场景的新功能:defer 关键字。任何在 defer 块内提供的代码将在到达包含作用域的末尾时自动执行。这个功能可以在代码的任何地方工作。
Swift 2.0 中的自定义错误处理
Swift 2.0 中的新错误处理不仅限于 Apple 标准库。您可以将此新语法包含在自己的类和方法中。这样做可以构建能够适当处理所有错误的应用程序,通常会导致更好的用户体验和更少的崩溃。此外,这种新的错误处理可以用于更复杂的调试。
准备工作
创建一个新的单视图项目,并将其命名为 Custom Error Handling Chapter 7。我们将为此食谱使用样板项目设置。如果您遵循了本章前面的食谱,您可以使用那个项目继续。
如何做到这一点...
-
导航到
ViewController.swift文件,并将以下代码添加到文件底部:enum CustomError: ErrorType { case Minimal case Bad case Explosion } -
让我们实现一个使用我们刚刚创建的
CustomError枚举的自定义方法。在我们的枚举声明下方添加以下代码:func performTaskWithString(taskString: String) throws -> String { // Do something that will result in a success or error. // If a minimal error occurs, throw a minimal error if (taskString.isEmpty) { throw CustomError.Minimal } // If a bad error occurs, throw a bad error if (taskString.containsString("Oops")) { throw CustomError.Bad } // If an explosixe error occurs, throw an explosion error if (taskString.containsString("ABORT ABORT ABORT")) { throw CustomError.Explosion } return "No errors!!" }
它是如何工作的...
我们最初创建一个自定义枚举来存储任何所需的自定义错误类型。这些类型可以是特定于您的应用程序的,也可以足够通用以用于框架。Swift 2.0 使用 throws 关键字放在方法返回值之前,以标识具有错误处理的方法。
在我们的自定义方法中,我们想要执行任何可能失败的任务,例如 URL 请求。接下来,我们验证可能的错误,并使用错误类型调用 throw。如果没有抛出错误,我们的检查之后的所有内容都将正常运行。
Swift 2.0 中的可用性检查
Swift 2.0 引入了一种新的方法来检查代码中的操作系统可用性。这是通过 @available 形式实现的,并允许我们检查对调用方法和创建我们自己的方法的操作系统支持。使用这种新的可用性检查功能,您可以编写向后兼容的代码,而无需担心运行时错误。Xcode 7 与 @available 集成良好,为程序员提供了更好的体验,而无需许多警告。此外,您可以根据操作系统版本更清晰地定义任务,而无需使用宏。
准备工作
创建一个新的单视图项目,并将其命名为 Availability Chapter 7。我们将为此食谱使用模板项目设置。如果您遵循了本章前面的食谱,您可以使用该项目继续操作。
如何操作…
-
要测试此功能,我们将使用苹果标准库中的注册通知的调用。iOS 8 引入了一种新的方法,因此我们只想针对 iOS 8 及以上版本。
-
导航到项目设置,并将最低部署目标更改为 iOS 7.1 或以下。因为通知方法在 8.0 及以上版本中受支持,所以我们想使用 7.1 作为我们的目标。
-
导航到项目的
AppDelegate.swift文件,并在didFinishLaunchingWithOptions方法中添加以下代码:if #available(iOS 8.0, *) { let types = UIUserNotificationType([UIUserNotificationType.Alert, UIUserNotificationType.Sound, UIUserNotificationType.Badge]) let settings = UIUserNotificationSettings(forTypes: types, categories: nil) application.registerUserNotificationSettings(settings) }
工作原理…
我们使用新的 #available(版本号) 语法来指定运行此代码块所需的版本。* 仅表示所有高于指定版本的版本。此外,此语法支持添加多个平台,例如 OS X 和 watchOS。
更多内容…
使用 @available(版本号) 语法,您可以指定单个函数和类仅在特定版本中可用。只需在函数或类声明上方直接包含此行即可。
参考以下内容…
- 苹果公司提供了许多可以与
@available一起使用的属性。访问以下链接以了解有关属性的更多信息:developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Attributes.html
第八章。与 Objective-C 集成
在本章中,我们将涵盖以下菜谱:
-
叫车
-
租赁货车
-
将代码从一个语言移植到另一个语言
-
替换用户界面类
-
升级应用程序代理
-
创建你自己的自定义框架
简介
Swift 2 已经发布,我们可以看到它迟早会取代 Objective-C 在 iOS 开发中的应用,然而你应该如何迁移你的 Objective-C 应用程序?是否需要重新编写一切?
当然,你不必从头开始用 Swift 重新编写整个应用程序,你可以逐步迁移它。想象一下,一个由 10 个开发者开发的四岁应用程序,重新编写将花费很长时间。
实际上,你已经看到我们在这本书中使用的一些代码具有某种“旧的 Objective-C 风格”。原因是即使是苹果电脑也无法将整个 Objective-C 代码迁移到 Swift。
本章将帮助你逐步从 Objective-C 迁移到 Swift。
叫车
让我们想象一下,我们为一家出租车公司工作,该公司已经有一个应用程序,允许客户从该应用程序中叫出租车。正如你可能想象的那样,公司可以开始提供额外的服务,而不仅仅是使用传统的汽车,例如,让我们想象一下,现在他们想要为携带大量行李的人提供租赁货车的服务。
在这个菜谱中,我们将从一个纯 Objective-C 应用程序开始,然后我们将对其进行修改,以便为将来添加 Swift 代码做好准备。
准备就绪
让我们从创建一个名为 Chapter 8 Cab 的项目开始,但在这个案例中,选择 Objective-C 而不是 Swift 作为编程语言:

如何做到这一点…
-
首先,在你的项目中创建一个新文件,选择右上角的 iOS 源中的 Cocoa Touch Class:
![如何做到这一点…]()
-
然后创建一个名为
Car的文件,它应该是NSObject的子类:![如何做到这一点…]()
你会看到创建了两个文件,一个是
Car.h,另一个是Car.m。 -
点击头文件(
Car.h)并添加以下代码:#import <Foundation/Foundation.h> @interface Car : NSObject{ float fare; } @property (assign) float distance; @property (assign) int pax; -(id) init; -(id) initWithFare:(float) fare; -(float) getFare; @end -
一旦创建了类接口,我们必须实现这个类,因此点击文件
Car.m并输入以下代码:#import "Car.h" @implementation Car -(id) init{ self = [super init]; if(self){ self->fare = 0.2; self.pax = 4; self.distance = 0; } return self; } -(id) initWithFare:(float) fare{ self = [super init]; if(self){ self->fare = fare; self.pax = 4; self.distance = 0; } return self; } -(float) getFare{ return self->fare; } @end模型部分已完成,现在我们需要创建视图部分。
-
让我们做一些非常直接的事情,只需将一个表格视图添加到故事板中,然后将其与视图控制器作为属性、数据源和代理链接。
-
由于你必须修改头文件(通常称为
ViewController.h),请添加一个名为vehicles的辅助属性,其类型为NSArray:@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>{ NSArray * vehicles; IBOutlet UITableView *tableView; } @end -
要向此应用程序添加一些功能,请转到消息文件(通常称为
ViewController.m)。小贴士
从头文件切换到其实施或反之亦然可以使用 + ^ + 上箭头。
-
首先,让我们导入
Car.h文件:#import "Car.h" -
然后,添加数据源内容:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return [vehicles count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell * cell; cell = [self->tableView dequeueReusableCellWithIdentifier:@"vehiclecell"]; if(cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"vehiclecell"]; } Car * currentCar = [self->vehicles objectAtIndex:indexPath.row]; cell.textLabel.text = [NSString stringWithFormat:@"Distance %.0f, pax: %d, fare: %.2f", currentCar.distance * 1000, currentCar.pax, [currentCar getFare] ]; return cell; } -
现在,你必须初始化车辆的属性。当然,在这里,我们只是使用一些硬编码,否则我们会有一大堆代码:
- (void)viewDidLoad { [super viewDidLoad]; Car * car1 = [[Car alloc] init]; car1.distance = 1.2; Car * car2 = [[Car alloc] init]; car2.distance = 0.5; Car * car3 = [[Car alloc] init]; car3.distance = 5; Car * car4 = [[Car alloc] initWithFare:0.25]; car4.distance = 4; vehicles = @[car1, car2, car3, car4]; [self->tableView reloadData]; } -
我们最后需要做的是为用户创建一个事件,让他可以选择预订的汽车:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ Car * currentCar = [self->vehicles objectAtIndex:indexPath.row]; // let's suppose that the current traffic allows us to drive at 50 km/h float time = currentCar.distance / 50.0 * 60.0; [[[UIAlertView alloc] initWithTitle:@"Car booked" message:[NSString stringWithFormat:@"The car will arrive in %.0f minutes", time] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil] show]; } -
现在测试这个应用程序,只是为了看看它是否在正常工作。下一步是为这个应用程序准备接收一些 Swift 代码。在我们添加任何 Swift 文件之前,我们需要更新这段代码;幸运的是,Xcode 提供了一个自动执行此操作的选项,所以点击 编辑 菜单,然后选择 重构,最后选择 转换为现代 Objective-C 语法…:
![如何操作…]()
小贴士
在开始更新你的代码之前,如果你使用版本控制系统,提交你的代码是一个好主意。
-
当出现一个带有一些介绍文本的对话框时,按 下一步,然后在下一个对话框中检查出现的每个目标,通常它们是勾选的,但以防万一,请确保这一点。在下一个屏幕上,你会看到一些用于现代化你的 Objective-C 代码的选项,你可以保留所有选项的默认值,但更好的做法是确保每个选项都标记为“是”
![如何操作…]()
-
之后,你会看到一个显示原始代码和将要更新的代码之间差异的另一个对话框。通过点击左侧的文件名来检查每个文件的差异:
![如何操作…]()
小贴士
避免在不检查旧代码和新代码之间差异的情况下更新你的代码,有时你会发现一些概念上不正确的修改。
-
按 保存,你将看到一个新对话框询问你是否想启用自动快照:
![如何操作…]()
现在检查你的项目代码已更改,并且仍然像以前一样工作。
它是如何工作的…
当你将你的 Objective-C 应用程序迁移到 Swift 时,你需要做的第一件事是将你的代码转换为现代 Objective-C 语法。多亏了这一点,你的代码将准备好与 Swift 集成兼容。
你可以看到,在更新你的代码后,代码的这一部分被修改了,例如,ID 被替换为 instancetype,初始化器接收 NS_DESIGNATED_INITIALIZER 修饰符,并且一些函数被转换为属性,主要是那些以 get 或 set 开头的函数。
通常这种改变被认为是一次巨大的改变,这当然有需要回滚到之前状态的风险。这就是为什么 Xcode 建议你创建一个快照。
参考信息
- 在这个菜谱中,我们学习了如何获取现有的 Objective-C 应用程序并为其准备使用 Swift 代码。当然,有时你可以自动完成它,有时你必须手动更改它。查看现代 Objective-C 文档是个好主意。你可以通过访问以下网站来完成此操作:
developer.apple.com/library/ios/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html.
招聘一辆货车
在这个菜谱中,我们将为驾驶舱应用程序添加更多功能,在这种情况下,我们将假设应用程序不仅会调用汽车,还会提供货车服务。在这种情况下,货车需要指定其容量,因为我们正在更新代码,我们将使用 Swift 来完成。当然,你总是需要在 Objective-C 中输入一些代码,请记住这一点。
准备工作
在这个菜谱中,我们将继续使用之前的应用程序,所以复制之前的菜谱并打开项目副本。如果你想将应用程序从 第八章 汽车 重命名为 第八章 车辆,这将有助于区分它们。
如何操作…
-
首先,点击项目导航器,然后点击包含源代码的组,添加一个名为
Van.swift的新 Swift 文件。检查在尝试添加后,是否出现一个新对话框询问你是否想要创建一个桥接文件。点击 是,否则你将不得不自己创建一个头文件:![如何操作…]()
-
一旦你接受了它,前往构建设置,在搜索字段中输入
bridging并检查是否在选项 Objective-C Bridging Header 上设置了一个文件:![如何操作…]()
-
之后,检查项目导航器上是否有一个名为
Chapter 8 Vehicles-Bridge-Header.h的新文件,并且其内容基本上是空的(只有一些注释),所以让我们导入文件Car.h:#import "Car.h" -
现在,点击
Car.h并添加最后的修改,创建一个名为image的属性,这样我们就可以区分一辆汽车和一辆货车:@property (strong) UIImage * image; -
你将收到一个错误,因为
Car.h没有导入 UIKit,所以请前往此文件顶部并添加以下导入指令:#import <UIKit/UIKit.h> -
之后,你必须点击
Car.m来初始化这个新属性,所以在两个初始化器中添加以下代码:self.image = [UIImage imageNamed:@"car.png"]; -
现在,我们可以点击回 Swift 文件并创建一个代表货车的类。在这种情况下,我们将添加一个名为
capacity的属性,它将表示空间,以平方米为单位。正如你可能想象的那样,这个类将是汽车的一个子类:class Van: Car { var capacity:Int; override init(){ self.capacity = 10 super.init() self.image = UIImage(named: "van.png") } override init(fare: Float){ self.capacity = 10 super.init(fare: fare) self.image = UIImage(named: "van.png") } } -
正如你所见,我们需要两张图片来帮助用户可视化何时是汽车,何时是货车。将相应的图片从本书的
Resources文件夹拖动到 Supporting Files 组中。 -
很明显,我们不能止步于此,我们必须更改视图控制器,以便让新类能够以特定的信息表示。点击
ViewController.m文件并做出以下更改:首先更改方法cellForRowAtIndexPath,它将以更完整的方式显示:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell * cell; cell = [self->tableView dequeueReusableCellWithIdentifier:@"vehiclecell"]; if(cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"vehiclecell"]; cell.textLabel.numberOfLines = 1; } Car * currentCar = self->vehicles[indexPath.row]; cell.textLabel.text = [NSString stringWithFormat:@"Distance %.3f meters", currentCar.distance]; NSString * detailText = detailText = [NSString stringWithFormat:@"Pax: %d Fare: %.2f", currentCar.pax, [currentCar getFare] ]; if ([currentCar isKindOfClass:[Van class]]) { detailText = [NSString stringWithFormat:@"%@, Volume: %ld",detailText, (long)[(Van*)currentCar capacity]]; } cell.detailTextLabel.text = detailText; cell.imageView.image = currentCar.image; return cell; } -
现在编译器将会对
Van类提出抱怨,原因是你需要导入它。我们如何在 Objective-C 中导入一个 Swift 文件?答案是极其简单,只需导入一个与你的项目名称相同并附加后缀-Swift.h的文件。如果你的项目名称中包含空格,请将其替换为下划线:#import "Chapter_8_Vehicles-Swift.h" -
然后你必须向车辆数组中添加一个额外的元素,所以前往
viewDidLoad并在第 4 辆车之后添加一个货车对象:- (void)viewDidLoad { [super viewDidLoad]; Car * car1 = [[Car alloc] init]; car1.distance = 1.2; Car * car2 = [[Car alloc] init]; car2.distance = 0.5; Car * car3 = [[Car alloc] init]; car3.distance = 5; Car * car4 = [[Car alloc] initWithFare:0.25]; car4.distance = 4; Van * van = [[Van alloc] initWithFare:0.32]; van.distance = 3.8; vehicles = @[car1, car2, car3, car4, van]; [self->tableView reloadData]; } -
现在我们的第一次迁移已经完成,按播放键,你会看到你的应用程序仍然在运行,但有了雇佣货车的可能性:
![如何操作…]()
它是如何工作的…
当你混合 Swift 和 Objective-C 时,如果你在 Objective-C 部分使用 Swift 代码,你必须创建一个桥接文件。通常你将由向导引导,它会创建桥接文件并将其设置在构建设置中,但请记住,如果向导没有出现,你可能需要自己完成这项工作。
你必须导入你想要在 Swift 中使用的每个头文件。在这种情况下,你必须导入Car.h。
Swift 类可以继承自 Objective-C 类,然而,反之则不允许,这意味着如果你开始在项目中添加 Swift 代码,你需要确保你不会继续使用 Objective-C 开发类。
正如你所看到的,你可以将 Swift 类视为 Objective-C 类,即使你需要使用isKindOfClass等方法,而且 Objective-C 也被转换为 Swift,遵循其哲学,例如创建一个初始化器,如init(fare:Float),在 Objective-C 中其原始名称是initWithFare。
将你的代码从一种语言迁移到另一种语言
在之前的食谱中,我们学习了如何将新代码添加到现有的 Objective-C 项目中,然而,你不仅应该添加新代码,而且尽可能地将你的旧代码迁移到新的 Swift 语言中。
如果你希望保持你的应用程序核心为 Objective-C,那没问题,但请记住,新的功能将被添加到 Swift 中,并且保持同一项目中的两种语言将变得困难。
在这个食谱中,我们将把部分代码从 Objective-C 迁移到 Swift。
准备工作
复制之前的食谱。如果你在使用任何版本控制系统,现在是提交你的更改的好时机。如果你没有使用版本控制系统,查看第一章,使用 Xcode 和 Swift 入门,了解如何将其添加到你的项目中。
如何操作…
-
打开项目并添加一个名为
Setup.swift的新文件,在这里我们将添加一个具有相同名称的新类(Setup):class Setup { class func generate() -> [Car]{ var result = [Car]() for distance in [1.2, 0.5, 5.0] { var car = Car() car.distance = Float(distance) result.append(car) } var car = Car() car.distance = 4 var van = Van() van.distance = 3.8 result += [car, van] return result } } -
现在我们有了这个汽车数组生成器,我们可以在
viewDidLoad方法中调用它,替换之前的代码:- (void)viewDidLoad { [super viewDidLoad]; vehicles = [Setup generate]; [self->tableView reloadData]; } -
再次,按播放并检查应用程序是否仍然工作。
它是如何工作的…
我们必须创建一个类而不是创建一个函数的原因是,你只能导出 Objective-C 类、协议、属性和索引。如果你在用两种语言开发时,请记住这一点。
如果你想要将类导出为 Objective-C,你有两种选择。第一个是继承自 NSObject,另一个是在你的类、协议、属性或索引之前添加 @objc 属性。
如果你注意到了,我们的方法返回的是一个转换成 NSArray 的 Swift 数组,但正如你可能知道的,它们是不同类型的数组。首先,因为 Swift 数组是可变的,而 NSArray 不是,另一个原因是它们的方法不同。
我们能否在 Swift 中使用 NSArray?答案是肯定的,但我建议避免使用它;想象一下,一旦完成迁移到 Swift,你的代码仍然遵循旧的方式,这将是一次另一次的迁移。
还有更多…
从 Objective-C 迁移是一件需要谨慎处理的事情。不要试图一次性改变整个应用程序,记住一些 Swift 对象的行为与 Objective-C 不同,例如,Swift 中的字典指定了键和值的类型,但在 Objective-C 中它们可以是任何类型。
替换用户界面类
到目前为止,你知道如何迁移应用程序的模型部分,然而在现实生活中,我们还需要替换图形类。这样做并不复杂,但可能会有些细节。
准备工作
继续使用之前的配方,复制它或只是提交你的更改,然后继续我们的迁移。
如何做到这一点…
-
首先创建一个名为
MainViewController.swift的新文件,并开始导入 UIKit:import UIKit -
下一步是创建一个名为
MainViewController的类。这个类必须继承自UIViewController并实现UITableViewDataSource和UITableViewDelegate协议:class MainViewController:UIViewController,UITableViewDataSource, UITableViewDelegate { -
然后,添加我们在之前的视图控制器中拥有的属性,保持你之前使用的相同名称:
private var vehicles = [Car]() @IBOutlet var tableView:UITableView! -
接下来,我们需要实现方法;让我们从表格视图数据源方法开始:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return vehicles.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{ var cell:UITableViewCell? = self.tableView.dequeueReusableCellWithIdentifier("vehiclecell") if cell == nil { cell = UITableViewCell(style: .Subtitle, reuseIdentifier: "vehiclecell") } var currentCar = self.vehicles[indexPath.row] cell!.textLabel?.numberOfLines = 1 cell!.textLabel?.text = "Distance \(currentCar.distance * 1000) meters" var detailText = "Pax: \(currentCar.pax) Fare: \(currentCar.fare)" if currentCar is Van{ detailText += ", Volume: \( (currentCar as Van).capacity)" } cell!.detailTextLabel?.text = detailText cell!.imageView?.image = currentCar.image return cell! }注意,这种转换并不完全等效。例如,费用不会以两位数的精度显示。稍后会有解释为什么我们现在不修复这个问题。
-
下一步是添加事件;在这种情况下,我们必须在用户选择一辆车时执行操作:
func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? { var currentCar = self.vehicles[indexPath.row] var time = currentCar.distance / 50.0 * 60.0 UIAlertView(title: "Car booked", message: "The car will arrive in \(time) minutes", delegate: nil, cancelButtonTitle: "OK").show() return indexPath }正如你所见,我们只需要再进行一步就可以完成我们的代码。在这种情况下,它是视图的
didLoad方法。请注意,Objective-C 和 Swift 之间的另一个区别是,在 Swift 中你必须指定你正在重载一个现有方法:override func viewDidLoad() { super.viewDidLoad() vehicles = Setup.generate() self.tableView.reloadData() } } // end of class -
我们的代码已经完成,但当然我们的应用程序仍然在使用旧的代码。要完成这个操作,点击故事板;如果文档大纲没有显示,点击编辑菜单,然后点击显示文档大纲:
![如何操作…]()
-
现在你可以看到文档大纲了,点击旁边带有方形黄色圆圈的视图控制器:
![如何操作…]()
-
然后在右侧,点击身份检查器,转到自定义类,将类的值从ViewController更改为MainViewController:
![如何操作…]()
-
然后,按播放并检查你的应用程序是否正在运行,选择一辆车,并检查它是否工作。确保它使用你的新 Swift 类工作,注意票价值,在这种情况下,它没有以两位数的精度显示。
![如何操作…]()
-
事情都完成了吗?我会说还没有,现在是保存更改的好时机。最后,删除原始的 Objective-C 文件,因为你不再需要它们了。
它是如何工作的…
如你所见,用 Swift 替换旧的视图控制器并不难;你需要做的第一件事是创建一个新的视图控制器类及其协议。保持你旧代码中与IBActions链接的属性和方法相同的名称,这将使切换变得非常直接;否则,你可能需要重新链接。
请记住,你需要确保你的更改已经应用并且正在工作,但有时拥有不同的东西是个好主意,否则你的应用程序可能会继续使用旧的 Objective-C,而你却没有意识到这一点。
小贴士
尝试使用 Swift 的方式而不是旧的 Objective-C 风格来现代化你的代码,例如,现在使用插值而不是使用stringWithFormat更受欢迎。
我们还了解到,如果你保持相同的名称,你不需要重新链接任何操作或出口。如果你想更改任何东西的名称,你可能首先保留其原始名称,测试你的应用程序,然后你可以按照传统的分解步骤重构它。
小贴士
在你确信等效的 Swift 文件在所有功能上都能正常工作之前,不要删除原始的 Objective-C 文件。
还有更多…
这个应用程序只有一个视图控制器,然而应用程序通常有多个视图控制器。在这种情况下,最好的更新方式是逐个更新它们,而不是同时更新所有。
升级应用程序代理
如你所知,有一个控制应用程序事件的对象,这被称为应用程序代理。通常你这里不应该有太多代码,但你可能有一些。例如,当你的应用程序进入后台时,你可能需要禁用相机或 GPS 请求,当应用程序返回活动状态时,重新激活它们。
即使你在这个文件上没有添加任何新代码,更新这个文件也是一个好主意,这样将来就不会有问题。
准备工作
如果你使用版本控制系统,请从上一个菜谱提交你的更改,或者如果你更喜欢,只需复制你的应用程序。
如何操作…
-
打开上一个应用程序菜谱,创建一个名为
ApplicationDelegate.swift的新 Swift 文件,然后创建一个具有相同名称的类。 -
就像我们之前的类一样,我们在应用程序代理中没有代码,所以我们可以通过在日志控制台上打印来区分它。在你的 Swift 文件中添加这个传统的应用程序代理:
class ApplicationDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { print("didFinishLaunchingWithOptions") return true } func applicationWillResignActive(application: UIApplication) { print("applicationWillResignActive") } func applicationDidEnterBackground(application: UIApplication) { print("applicationDidEnterBackground") } func applicationWillEnterForeground(application: UIApplication) { print("applicationWillEnterForeground") } func applicationDidBecomeActive(application: UIApplication) { print("applicationDidBecomeActive") } func applicationWillTerminate(application: UIApplication) { print("applicationWillTerminate") } } -
现在转到你的项目导航器,展开 支持文件 组。之后,点击
main.m文件:![如何操作…]()
-
在这个文件中,我们将导入魔法文件,Swift 头文件:
#import "Chapter_8_Vehicles-Swift.h" -
之后,我们必须指定应用程序代理是我们拥有的新类,所以将
UIApplicationMain调用中的AppDelegate类替换为ApplicationDelegate。你的主函数应该是这样的:int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([ApplicationDelegate class])); } } -
是时候按播放键检查应用程序是否正常工作了。按主页按钮,或者如果你使用的是模拟器,按 shift + command + H 组合键,然后再次打开你的应用程序。检查你的日志控制台是否有消息:
![如何操作…]()
-
现在你已经确认你的 Swift 代码正在工作,请从
main.m中移除原始的应用程序代理及其导入。测试你的应用程序以确认无误。 -
你可以考虑我们已经完成了这一部分,但实际上我们还有另一个步骤要做:移除
main.m文件。现在,这非常简单:只需点击ApplicationDelegate.swift文件,在类声明之前添加属性@UIApplicationMain,然后右键点击main.h并选择删除它。测试它,你的应用程序就完成了。
它是如何工作的…
应用程序代理类始终在应用程序开始时指定。在 Objective-C 中,它遵循 C 的起点,即一个名为 main 的函数。在 iOS 中,你可以指定你想要用作应用程序代理的类。
注意
如果你为 OS X 编程,程序不同;你必须转到你的 nib 文件,并将其类名更改为新的一个。
为什么我们必须更改主函数然后消除它?原因是你应该避免大规模的更改。如果出了问题,你不知道失败的具体步骤,所以你可能不得不再次回滚所有内容。如果你一步一步地进行迁移,确保它仍然工作,这意味着在发现错误的情况下,解决它将更容易。
提示
避免在项目中进行大规模更改;逐步更改将更容易解决问题。
还有更多…
在这个菜谱中,我们学习了如何将应用程序从 Objective-C 迁移到 Swift 代码的最后一步,然而我们必须记住,编程不仅仅是关于应用程序;你还可以有一个框架。在下一个菜谱中,我们将学习如何创建与 Swift 和 Objective-C 兼容的自定义框架。
创建你自己的自定义框架
如你所知,有时我们有一些需要在应用程序之间共享的代码,而最好的方式是通过创建一个框架。在这种情况下,我们将创建一个包含自定义视图的框架。
对于这个菜谱,我们只添加一个视图。这个视图将以渐变的形式绘制;这样你可以轻松地更改应用程序的背景。
准备中
创建一个名为CustomViewsFramework的新项目。要做到这一点,请点击文件 | 新建 | 项目,然后选择框架和库部分;之后选择Cocoa Touch 框架选项:

选择Swift作为项目语言。
如何操作…
-
开始向你的项目中添加一个新文件,在这种情况下,你可以从源部分选择Cocoa Touch 类选项:
![如何操作…]()
-
之后,你必须编写新的类名;让我们称它为
CVGradientView。同时,确保它被选为UIView的子类,并且其语言是 Swift:![如何操作…]()
-
文件创建并打开后,你可以移除默认的注释,然后必须在类声明之前添加属性
@IBDesignable:@IBDesignable public class CVGradientView: UIView { -
现在我们需要添加一些属性;在这种情况下,我们将使用带有观察者的属性,每次属性发生变化时都会刷新渐变视图:
@IBInspectable public var color1: UIColor = UIColor.redColor() { didSet{ refresh() } } @IBInspectable public var color2: UIColor = UIColor.blackColor(){ didSet{ refresh() } } @IBInspectable public var roundCorners: CGFloat = 1.0 { didSet{ refresh() } } @IBInspectable public var horizontal: Bool = false { didSet{ refresh() } } -
正如你所注意到的,我们必须实现
refresh()方法:private func refresh(){ let colors:Array = [color1.CGColor, color2.CGColor] gradientLayer.colors = colors gradientLayer.cornerRadius = roundCorners if (horizontal){ gradientLayer.endPoint = CGPoint(x: 1, y: 0) }else{ gradientLayer.endPoint = CGPoint(x: 0, y: 1) } self.setNeedsDisplay() } -
然后,我们需要指定一些关于梯度层的信息;这些信息是按照核心动画框架进行的:
var gradientLayer: CAGradientLayer { return layer as CAGradientLayer } override public class func layerClass()->AnyClass{ return CAGradientLayer.self } -
现在,我们需要这个方法的最后一部分,即初始化器:
override init(frame: CGRect) { super.init(frame: frame) refresh() } required public init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) refresh() } } -
完成后,我们需要通过访问我们的目标构建设置并搜索单词
module来识别我们的模块。现在,将模块标识符更改为CustomViewsFrameWork:![如何操作…]()
-
类已经完成,所以使用 + b生成项目。现在,我们需要检查它是否正常工作,所以让我们创建另一个名为
Chapter 8 Testing Views的项目。创建后,点击项目导航器,然后点击目标Chapter 8 Testing Views。 -
在这里,你必须选择构建设置选项卡,并将字段嵌入式内容包含 Swift 代码更改为是:
![如何操作…]()
-
接下来,选择常规选项卡,转到嵌入的二进制文件部分,点击加号按钮,当对话框出现时,只需点击带有短语添加其他…的按钮。在这里,它正在询问你的框架,所以转到你的框架的构建产品(它应该在名为
DerivedData/CustomsViewsFramework/Build/Products/Debug-iphonesimulator/的文件夹中),选择它(名为CustomsViewsFramework.framework的文件),然后按确定。检查链接的框架部分也会显示它:![如何操作…]()
-
现在既然你的应用已经了解了这个框架,你可以点击故事板,然后点击你拥有的唯一视图(不是视图控制器),转到身份检查器,将类名称字段更改为CVGradientView,并将其模块名称更改为CustomViewsFramework:
![如何操作…]()
-
按播放键并检查你的背景是否已更改。如果你想,甚至可以程序化地更改你的背景;只需点击视图控制器文件并开始导入你的框架:
import CustomViewsFramework -
之后,例如,更改视图加载后的背景颜色:
override func viewDidLoad() { super.viewDidLoad() (self.view as CVGradientView).color1 = UIColor.blueColor() (self.view as CVGradientView).color2 = UIColor.purpleColor() } -
再次按播放键,你会看到你的背景已经不同了。
它是如何工作的…
我们将这个项目命名为与其他项目不同的原因是因为框架的名称中不能有空格。在创建名称中包含空格的项目的情况下,你必须转到构建设置,并将产品名称更改为没有空格的名称。
属性@IBDesignable让界面构建器知道它应该直接在画布中渲染视图,但请记住,只有在你开发框架时才能使用此属性;它不会在传统应用上工作。
另一个属性(@IBInspectable)意味着这个属性可以通过界面构建器查看和设置。
注意,我们不得不将我们的类和一些属性和方法标记为公共的。原因是我们希望它们可以被外部模块访问。如果我们不这样做,这意味着只有我们的框架可以访问这个类。
第九章。处理其他语言
在本章中,我们将涵盖:
-
使用我的旧地址簿
-
压缩信息
-
使用汇编代码与 Swift
-
与 Swift 共享 C++代码
简介
Swift 是在已经完成的库的世界中的新语言。有时你需要其他语言的帮助,否则你可能需要花费大量时间来创建你想要的功能。
如你所知,自 70 年代以来,C 一直是无论你想要在哪个平台上开发,默认的语言。有成千上万甚至数百万的库是用 C 编写的,你可以找到很多开源的,这使得它们更容易移植到你的 iOS 或 Mac OS 项目中。
在本章中,我们将学习如何在 Swift 项目中使用外部语言。在这里,我们将看到如何使用 C、C++甚至汇编代码。
使用我的旧地址簿
在这个菜谱中,我们将学习如何在 Swift 项目中使用 C 代码。在这种情况下,让我们假设我们想要回收一个用 C 编写的地址簿的链表,这样你就可以从设备地址簿中读取联系人并将它们存储在文件中。将结构存储在文件中的优点是,你可以使用没有 Swift 的其他平台打开相同的文件,例如 Linux 或 Windows。
准备工作
创建一个名为Chapter 9 Address Book的新项目;确保这个项目是一个 Swift 项目。
我们将检查在应用程序子文件夹内是否创建了一个新文件。正如你所知,没有使用第三方应用程序,我们无法看到任何设备应用程序文件夹。在这种情况下,我们将下载一个可以探索设备文件的应用程序,称为iFunBox。这个应用程序可以从www.i-funbox.com/免费下载,但如果你更喜欢,还有其他免费应用程序和商业应用程序,如 iBrowser 和 iPad Folder。
小贴士
只有当你打算使用物理设备时,下载 iFunBox 才是必要的,如果你打算只使用模拟器,你可以使用传统的Finder窗口。
如何操作…
-
我们需要采取的第一个步骤是添加一个新文件;在这种情况下,你应该选择一个 C 文件:
![如何操作…]()
-
然后,它会询问文件名;让我们称它为
AddressBook.c并确保已选中也创建一个头文件选项:![如何操作…]()
-
选择与项目源代码存储相同的文件夹来存储这个新文件。之后,你会看到它会询问你是否要创建一个 Objective-C 桥接文件;点击是:
![如何操作…]()
-
现在,你可以看到有三个新的文件:
AddressBook.c、AddressBook.h和Chapter 9 Address Book-Bridging-Header.h:![如何操作…]()
-
在编码之前,首先点击桥接文件并导入
AddressBook.h文件:#import "AddressBook.h" -
现在转到
AddressBook.h。在这里我们需要做的第一件事是移除stdio.h的包含。之后,您必须添加将在我们的应用程序中使用的结构体:struct Contact { char name[60]; char phone[20]; struct date { int day; int month; int year; } birthday; }; struct ContactList { struct Contact contact; struct ContactList * next; }; -
一旦定义了结构和其他类型,我们将添加可以对我们联系人列表执行的操作的函数头:创建一个新的联系人,初始化列表,销毁列表,添加一个新的联系人,并将联系人列表保存到文件中:
struct Contact createContact(); void initializeContactList(struct ContactList ** ); void insertContact(struct ContactList **, struct Contact); void saveContactList(struct ContactList *, const char *); void destroyContactList(struct ContactList **); -
头文件已完成。如果您希望添加更多操作,例如在列表中查找联系人或删除联系人,请随意操作。现在让我们转到实现文件(
AddressBook.c)并编写操作代码。如果您不理解这段代码,请不要担心;通常当您在 Swift 中使用 C 代码时,您只需要通过阅读其头文件来理解操作的功能:struct Contact createContact(){ struct Contact newContact; strcpy(newContact.name, ""); strcpy(newContact.phone, ""); newContact.birthday.day = 0; newContact.birthday.month = 0; newContact.birthday.year = 0; return newContact; } void initializeContactList(struct ContactList ** contactList ){ *contactList = NULL; } void insertContact(struct ContactList ** contactList, struct Contact contact){ struct ContactList * newContactList = malloc(sizeof(struct ContactList)); newContactList->next = *contactList; newContactList->contact = contact; *contactList = newContactList; } void saveContactList(struct ContactList * contactList, const char * filename){ FILE * file = fopen(filename, "wb"); if (file) { while (contactList) { fwrite(&contactList->contact, sizeof(struct Contact), 1, file); contactList = contactList->next; } fclose(file); } } void destroyContactList(struct ContactList ** contactList){ struct ContactList * aux; while ((aux = *contactList)) { *contactList = (*contactList)->next; free(aux); } } -
C 部分已完成,现在您可以点击故事板并添加两个按钮来返回我们创建应用的方式;一个用于加载联系人,另一个用于将联系人保存到文件。将第一个按钮链接到名为
fillContacts的函数,将第二个按钮链接到名为saveContacts的函数。目前不需要实现任何内容,我们很快将返回这些函数。 -
由于我们需要从设备地址簿中读取联系人,我们需要将其添加到我们的项目中,因此点击项目导航器中的项目,确保目标
Chapter 9 AddressBook的 General Info 选项卡被选中。在这里,您需要向下滚动到 Linked Frameworks and Libraries 并添加框架 AddressBook:![如何操作…]()
-
返回到视图控制器,向上滚动到开头,并导入地址簿:
import AddressBook -
在视图控制器类内部,我们将从属性开始,在这种情况下,我们只需要一个指向
ContactList的列表:var list:UnsafeMutablePointer<ContactList> = nil -
即使我们已经通过分配 nil 值初始化了列表,我们仍然需要使用 C 特定的函数来初始化它,我们可以在
viewDidLoad方法中这样做:override func viewDidLoad() { super.viewDidLoad() initializeContactList(&list) } -
如您可能已经注意到的,还有一个销毁列表的函数,这意味着我们需要在析构器中调用它:
deinit{ destroyContactList(&list) } -
现在我们可以实现按钮事件,让我们从从设备地址簿中加载联系人的按钮开始:
@IBAction func fillContacts(sender: UIButton) { let status = ABAddressBookGetAuthorizationStatus() switch status { case .Authorized: // When the user has already authorized previously. self.readContacts() case .NotDetermined: // this case happens when it is the first time the user opens the app, so we have to request his permission var ok = false ABAddressBookRequestAccessWithCompletion(nil) { (granted:Bool, err:CFError!) in if granted { self.readContacts() } } case .Restricted: fallthrough case .Denied: // These cases are when for any reason the app can't access the contacts UIAlertView(title: "Not authorized", message: "This app isn't authorized for reading your contacts", delegate: nil, cancelButtonTitle: "OK").show() } }注意
记住,在 iOS 和 OS X 中,没有用户的许可,您不能读取地址簿。
-
如您所见,有两个对方法
readContacts的调用,所以这就是我们现在需要实现的方法:private func readContacts(){ var err : Unmanaged<CFError>? = nil var myAddressBook: ABAddressBook = ABAddressBookCreateWithOptions(nil, &err).takeRetainedValue() let myContacts = ABAddressBookCopyArrayOfAllPeople(myAddressBook).takeRetainedValue() as NSArray as [ABRecord] for aContact in myContacts { var newContactRecord:Contact = createContact(); // Retrieving name var nameString = ABRecordCopyCompositeName(aContact).takeRetainedValue() as String copyIntoCString(&newContactRecord.name, swiftString:nameString) // Retrieving phone var phones:ABMultiValue = ABRecordCopyValue(aContact, kABPersonPhoneProperty).takeRetainedValue() as ABMultiValue if(ABMultiValueGetCount(phones) > 0){ var phoneString = ABMultiValueCopyValueAtIndex(phones, 0).takeRetainedValue() as String copyIntoCString(&newContactRecord.phone, swiftString:phoneString) } // Retrieving birthday if let date = ABRecordCopyValue(aContact, kABPersonBirthdayProperty).takeRetainedValue() as? NSDate { var calendar = NSCalendar.currentCalendar().components([.Day, .Month, .Year], fromDate: date) newContactRecord.birthday.day = Int32(calendar.day) newContactRecord.birthday.month = Int32(calendar.month) newContactRecord.birthday.year = Int32(calendar.year) } insertContact(&list, newContactRecord) } UIAlertView(title: nil, message: "The contacts were loaded", delegate: nil, cancelButtonTitle: "OK").show() } -
现在我们来实现保存联系人到文件的按钮;这个比较简单,因为我们只需要设置存储文件的完整路径并调用保存联系人的函数:
@IBAction func saveContacts(sender: UIButton) { let documentDir:NSString = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString var filename:NSString = documentDir.stringByAppendingPathComponent("contacts.dat") as NSString saveContactList(list, filename.UTF8String) UIAlertView(title: "Contacts saved", message: "contacts.dat was saved.", delegate: nil, cancelButtonTitle: "Ok").show() } -
应用程序看起来已经完成了,但是如果你尝试编译它,你会得到一个错误,因为缺少实现:函数
copyIntoCString。它是一个辅助函数,我们必须创建它。既然你可能需要在其他项目中使用它,让我们在新的文件中实现它。创建一个名为CstringUtils.swift的新文件,并添加以下代码:func copyIntoCString<T>(inout cstring: T, swiftString: String){ withUnsafeMutablePointer(&cstring, { (cstr) -> Void in let fullSwinftString = swiftString + String(UnicodeScalar(0)) let newCString = fullSwinftString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)! newCString.getBytes(cstr, length: sizeofValue(cstring)) }) } -
好的,现在应用程序已经完成,但是你仍然需要测试它。按播放键,当请求时接受每个权限,并按按钮从地址簿检索联系人。当你收到联系人已加载的对话框时,你可以按保存按钮。在屏幕上,你可能只有如下对话框的结果:
![如何操作…]()
如果你认为你没有获得很多视觉信息,你完全正确,因为这个应用程序不是为了做视觉上的事情而创建的;相反,它是为了生成文件。
-
如果你正在使用物理设备,请打开您在食谱开始时下载的 iFunBox 应用程序(请注意,在撰写本文时,iFunBox 无法在 8.3 或更高版本上运行),展开 用户应用程序 部分,然后点击您的应用(第九章 联系人簿)。在右侧,双击文档文件夹,你应该会看到一个名为
contacts.dat的文件:![如何操作…]()
-
如果你想要复制这个文件,你也可以通过在另一个平台上读取这个文件来进行相反的操作,但这个任务将留作作业。
它是如何工作的...
虽然 Xcode 会询问你是否要创建一个 Objective-C 桥接文件,但实际上它创建了一个也可以作为 C 桥接文件的文件。一旦你的 C 头文件被导入到这里,你就可以在 Swift 中使用它们。正如你可能已经注意到的,C 不与对象一起工作;你能做的唯一一件事是创建一个结构体(在 C 中没有方法)并通过一些函数将其作为参数传递。
C 类型在其名称前加上 C 前缀就有 Swift 中的等效类型,例如 CChar、CInt、CFloat,而结构体则保持其名称不变,不带 struct 这个词,就像你在 ContactList 和 Contact 中看到的那样。
另一个你必须考虑的特性是 C 使用指针。指针是一种信息引用,就像我们在联系人列表中拥有的那样。当你有 C 中的指针时,它们会被转换为 Swift 中的 UnsafeMutablePointer,双指针会被转换为 UnsafeMutablePointer 的 UnsafeMutablePointer,依此类推;这是 C 通过引用接收函数参数的风格。
当你想调用一个需要通过引用传递参数的 C 函数时,你必须使用 & 操作符。这意味着你正在发送该变量的内存地址。然而,Swift 声明通过引用传递参数的方式不同:你必须在参数名称之前添加 inout 属性。
在 Swift 中使用某些 C 类型有时需要一些技巧,例如,C 没有字符串类型;最相似的是字符数组,它有时被转换为[CChar],有时被转换为UInt8的元组。如果你需要使用 C 变量而不通过获取其内存地址进行类型转换,你必须使用withUnsafeMutablePointer函数。
压缩消息
即使你只在一个内部项目中工作,你可能也需要使用库。有时它是一个 Swift 框架,但有时它是一个 C 库。使用 C 库现在非常普遍,主要是因为有很多这样的库。
对于这个菜谱,我们将使用一个非常有用的库:BCL。这个简单的库可以很容易地在 Xcode 中编译,并且每次你的项目需要压缩任何信息时,你都可以使用它。
准备工作
在我们开始编码这个项目之前,让我们下载 BCL 库。它可以在bcl.comli.eu/找到,并且下载源代码是免费的。解压下载的文件并保持其查找窗口打开。
如果你打算将此应用安装到物理设备上,你需要下载之前提到的 iFunBox。
对于这个菜谱,不是创建项目,而是开始创建一个工作区,并将其命名为Chapter 9 Compressing Workspace。
如何操作…
-
首先,你需要创建一个项目,但在这个情况下,不要创建单个视图应用,而是从框架和库部分选择Cocoa Touch 静态库并按下一步:
![如何操作…]()
注意
选择Cocoa Touch 静态库意味着以 C、C++或 Objective-C 进行开发。Swift 没有静态库。
-
现在将你的项目命名为
Chapter 9 BCL。注意,它并没有询问它是否是一个 Swift 项目:![如何操作…]()
-
在下一屏,你必须选择项目将要存储的目标文件夹,但在按下创建按钮之前,确保此项目属于你的工作区:
![如何操作…]()
-
确认此项目以两个源代码文件开始:
Chapter_9_BCL.h和Chapter_9_BCL.m。删除它们,因为它们不是必需的。在删除时,你可以将它们移动到垃圾桶而不是仅仅删除引用:![如何操作…]()
-
现在将 BCL 库(位于
src文件夹中)中以.c和.h结尾的文件拖到你的源代码组中:![如何操作…]()
注意
实际上,你不需要复制文件
bcltest.c和bcl.c,因为它们属于 BCL 作为一个程序而不是作为库,但我们正在复制它们以简化操作。 -
虽然您已经有了构建库所需的文件,但您仍然需要指示外部应使用的头文件。这是您每次构建用 C、C++或 Objective-C 编写的库时都必须执行的程序。要执行此操作,请从目标Chapter 9 BCL中选择构建阶段选项卡;之后,展开复制文件部分:
![如何操作…]()
-
现在,按此部分底部的加号键;将出现一个新对话框,显示可以导出的文件。仅选择头文件,如
huffman.h、shannonfano.h和rle.h:注意
记住,您可以通过按住 command 键并单击文件来选择多个文件。
![如何操作…]()
-
库已完成,您需要通过按command + b来检查一切是否正常;您应该看到一个消息,表明构建已成功。
-
本食谱的下一部分是创建一个压缩用户消息的应用。为此,不要关闭您的项目并创建一个新的项目;这次,不要选择静态库,而选择一个用 Swift 完成的单视图应用,并将其命名为
Chapter 9 BCLApp。非常重要的一点是,您必须将此项目添加到工作区并分组为Chapter 9 Compressing Workspace。 -
注意,现在我们有两个项目。确保通过单击它来选择应用,然后单击目标
Chapter 9 BCLApp并选择常规信息选项卡。向下滚动到链接框架和库部分,如有必要展开它,然后按加号键。 -
将会出现一个对话框,但这次将有一个新的组称为工作区;在这里,您必须选择libChapter 9 BCL.a:
注意
静态库的名称总是以
lib为前缀,并以.a为扩展名。![如何操作…]()
-
一旦添加了库,您还需要向项目中添加一个新的头文件;让我们称这个文件为
BridgeHeader.h。 -
在我们开始编码此文件之前,让我们将此文件设置为应用的桥接文件;因此,您必须转到您应用的构建设置,在搜索框中输入桥接,一旦找到字段Objective-C 桥接头,请输入
Chapter 9 BCLApp/BridgeHeader.h:![如何操作…]()
-
让我们回到
BridgeHead.h并添加一些包含语句以使用压缩库:#include <Chapter 9 BCL/huffman.h> #include <Chapter 9 BCL/lz.h> #include <Chapter 9 BCL/rice.h> #include <Chapter 9 BCL/rle.h> #include <Chapter 9 BCL/shannonfano.h> #include <Chapter 9 BCL/systimer.h> -
应用现在已链接到库,这意味着从现在起,您可以返回到 Swift 编程。转到故事板,添加一个文本视图和两个按钮。通常,文本视图默认带有文本;如果您想,您可以保留它,这样在测试时会更方便。将文本视图链接,命名为
textView:@IBOutlet weak var textView: UITextView! -
之后,您必须添加按钮的动作。对于第一个按钮,将其标签更改为 Huffman,并链接到以下动作:
@IBAction func huffman(sender: UIButton) { let text = self.textView.text let textIn:UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>((text as NSString).UTF8String) let textOut:UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.alloc(countElements(text) * 101 / 100 + 320) let outsize = Huffman_Compress(textIn, textOut, UInt32( countElements(text))) save("huffman.dat", data: textOut, dataSize: Int(outsize)) } -
使用第二个按钮,您可以执行相同的操作;将其标签更改为
lz,并用以下类似代码进行链接:@IBAction func lz(sender: UIButton) { let text = self.textView.text let textIn:UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>((text as NSString).UTF8String) let textOut:UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.alloc(countElements(text) * 257 / 256 + 1) let outsize = LZ_Compress(textIn, textOut, UInt32( countElements(text))) save("lz.dat", data: textOut, dataSize: Int(outsize)) } -
如你所想,最后一步是实现保存功能,这可以通过以下代码轻松实现:
private func save(filename: String, data: UnsafePointer<UInt8>, dataSize: Int){ let nsData = NSData(bytes: data, length: dataSize) let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String + "/\(filename)" NSFileManager.defaultManager().createFileAtPath(path, contents: nsData, attributes: nil) } -
应用程序已经完成,然而,仍然有必要对其进行测试。为此,你必须确保在方案组合中选择应用,否则当你按播放键时,什么都不会发生:
![如何做…]()
-
现在按播放键,在文本字段中写一条消息,然后按
huffman按钮,接着按lz按钮。与之前的食谱类似,你不会看到任何视觉上吸引人的东西,但如果你在 iFunBox 中展开应用并打开文档文件夹,你会看到文件大小。如果你不想复制文件就查看列表视图:![如何做…]()
它是如何工作的…
当你有一个大项目时,你可以将其分解成小组件;为此,你可以将你的项目分解成小项目。这样做的一个好方法是创建工作区。
工作区就像项目之间的连接,使得将一个项目(在这个例子中是 BCL 静态库)的使用应用到另一个项目(在这个例子中是应用)变得更容易。
小贴士
避免创建一个巨大的项目,相反,尝试将其分解成小项目;这将使你的项目更容易维护,并简化问题解决方案的查找,甚至创建单元测试。
静态库是直接复制到你的项目中的东西,这意味着你的项目会增大,但你不会担心库更新可能会破坏你的应用。每次你创建一个静态库,你必须导出头文件,这些文件包含可以公开使用的函数。
记住,即使你有一个静态库在同一工作区,你仍然需要在你的项目中链接它并创建一个桥接文件。
你还可以看到我们使用了新的方法:alloc。alloc方法用于创建具有特定大小的 C 数组;记住,C 中的数组是不可调整大小的,如果你要在它们上存储任何东西,你必须为它分配足够的内存。
也有一种新的类型:UnsafePointer。这样做的原因是 C 函数可以接收常量或变量作为参数,例如,接收const char *与char *不同;第一个被认为是UnsafePointer,第二个是UnsafeMutablePointer。不安全的可变指针可以隐式地转换为不安全的指针,但反过来则不然。
还有更多…
如你所见,这个库中有更多压缩类型;你可以尝试使用其中几个,看看哪个是最好的。
在下一个食谱中,我们将学习如何使用汇编代码与 Swift 结合。如果你真的需要性能,这个特性非常有用。
使用汇编代码与 Swift
在这个菜谱中,我们将学习如何使用 Swift 与汇编代码。当然,如今没有人仅使用汇编代码进行开发,但在需要性能的部分使用汇编代码是非常常见的。例如,图像处理程序使用汇编代码,因为它在通过硬件处理某些内容时比通过软件要快得多。
显然,使用汇编语言编程有其缺点,第一个缺点是您可能需要为不同的处理器重写源代码,例如,如果您为旧款 iPhone(32 位 ARM 处理器)编写了汇编代码,您可能需要为新设备(64 位 ARM 处理器)重写它们,即使有了这两段代码,如果您希望看到您的应用程序在 iPhone 模拟器上运行(Intel 处理器),您可能还需要第三次编写代码。
这次我们将使用一个非常简单的代码,因为本书的范围不包括教授 ARM 架构。在这种情况下,我们将创建一个简单的变量交换函数。
准备工作
对于这个菜谱,建议使用物理设备,但这不是强制性的,因为您可以从命令行编译,指定您想要编译的架构。
创建一个名为 Chapter 9 Assembly 的新 Swift 单视图应用程序,添加一个名为 AssemblyCode.c 的新文件,当 Xcode 询问是否创建桥接文件时,点击 是。
如何操作…
-
首先,您必须点击桥接头文件(
Chapter 9 Assembly-Bridging-Header.h)并包含文件AssemblyCode.h:#include "AssemblyCode.h" -
然后,转到
AssemblyCode.h并添加以下头文件:void swap(int * firstnumber, int * secondnumber); -
定义完成后,您必须在
AssemblyCode.c中编写实现代码。这里我们将使用一个非常简单的代码,它可以用于 32 位和 64 位,但请注意,在更复杂的代码中,您可能需要将它们分开:void swap(int * firstnumber, int * secondnumber){ #if defined __arm64__ || defined __arm__ asm volatile ( "EOR %[first],%[first], %[second] \n\t" "EOR %[second],%[second], %[first] \n\t" "EOR %[first],%[first], %[second] \n\t" : /* outputs */ [first]"=r"(*firstnumber), [second]"=r"(*secondnumber) : /* inputs */ [first]"r"(*firstnumber), [second]"r"(*secondnumber) ); #else #error "Architecture not allowed" #endif } -
汇编部分已完成;点击故事板并创建一个包含两个标签、两个文本字段和一个按钮的布局。将标签文本更改为 第一个数字 和 第二个数字;在文本字段中用一些示例数字替换占位符,并将按钮文本更改为 交换数字。最终结果应类似于以下内容:
![如何操作…]()
-
点击故事板上的每个文本字段,并在属性检查器中将键盘类型更改为 数字和标点符号:
![如何操作…]()
-
在将用户视图放置到故事板后,您必须将文本字段与视图控制器链接,并命名为
firstNumberTextField和secondNumberTextField:@IBOutlet weak var firstNumberTextField: UITextField! @IBOutlet weak var secondNumberTextField: UITextField! -
然后,将按钮与以下操作链接:
@IBAction func swapNumbers(sender: UIButton) { var number1 = Int32(Int(self.firstNumberTextField.text!)) var number2 = Int32(Int(self.secondNumberTextField.text!)) swap(&number1, &number2) self.firstNumberTextField.text = "\(number1)" self.secondNumberTextField.text = "\(number2)" } -
应用程序已完成,让我们测试它。首先,将设备切换到 iPhone 6 模拟器并按播放:
![如何操作…]()
-
如果收到错误消息,没关系。只需注意消息与您在
AssemblyCode.c文件上写的内容相同:![如何操作…]()
-
现在将你的 Apple 设备连接到电脑上,在活动方案中选择它,然后再次按播放。你应该能看到应用,所以输入两个不同的数字(每个文本字段一个),然后按交换按钮。结果是它们交换了它们的文本字段。
它是如何工作的……
在 C 或 Objective-C 层编写汇编代码是你需要做的事情,这意味着你仍然需要知道这些语言是如何转换为 Swift 的。使用as volatile语句可以让你编写汇编代码。在这个语句内部,你有最多四个部分,由冒号分隔:
-
第一部分是一个字符串(只有一个)是你的汇编模板;我们没有写多个字符串。注意,在 C 和 Objective-C 中,如果你写两个常量字符串,它们被视为一个。在这里,你可以使用
%[assembly variable name]或%0, %1等指定变量。 -
第二部分是输出变量;你可以通过使用方括号给你的汇编模板命名,并且在括号内你可以指定等效的 C 变量。
-
第三部分是输入变量,它们的工作方式与输出变量类似。
-
最后的部分(我们还没有使用)被称为“覆盖”。覆盖寄存器是指在汇编代码块中其值被修改的寄存器。编译器将知道不要期望保留旧的寄存器值。
小贴士
注意编写大量的汇编代码,因为调试它可能非常困难。
如前所述,汇编代码是平台相关的,这意味着在不同的平台上使用时可能会有所不同,比如在模拟器上、在旧款 Apple 设备上,或者在使用新的 64 位设备上。为了区分它们,你可以使用宏__arm__用于 32 位设备,以及__arm64__用于新的 ARM 设备。
通常你会看到包含汇编代码的函数,并带有inline关键字。这样做是因为当性能真正需要时,汇编代码会被使用,程序员试图告诉编译器将函数代码复制到它被调用的地方,而不是跳转到函数实现。不幸的是,为了做到这一点,你必须在头文件中实现函数,而 Swift 不接受这种方式。
还有更多……
当你需要调试在发布配置下编译的应用程序时,汇编代码也是非常有趣的东西。有一个很好的网站可以学习 ARM 汇编代码:www.peter-cockerell.net/aalp/html/frames.html。
你还可以研究 NEON 来了解如何处理向量、双字寄存器等,如果你还想了解更多,你可以研究内联函数,这些是调用 ARM 指令的 C 函数。
如果你自己在想,如果汇编代码非常复杂,那些需要高性能的游戏是如何开发的,答案是使用 C++。在下一个菜谱中,我们将使用它与 Swift 结合。
与 Swift 共享 C++代码
如果你有一些用 C++ 或 Objective-C++ 编写的代码或外部库,当你收到苹果的通知,说你不能像使用 C 或 Objective-C 一样直接在 Swift 中使用 C++,你可能会感到非常惊讶。
仍然有一个解决方案,那就是创建你自己的包装器。有一些尝试,比如 SwiftPP (github.com/sandym/swiftpp),但它们仍然非常不成熟。在这个菜谱中,我们将看到如何将你的 C++ 类包装起来以便在 Swift 中使用。
在这个菜谱中,我们将包装一个用于 Swift 的 C++ XML 创建器。如果你不知道 C++,不要担心,这个菜谱的目的是让你知道如何创建这种类型的代理类。
准备工作
对于这个菜谱,我们需要下载一个用于创建 XML 文件的纯 C++ 库;在这种情况下,选择的库是 pugixml。所以在我们开始之前,打开你的网络浏览器,转到 pugixml.org/(或者直接在 Google 中搜索 pugixml)并解压它。之后,创建一个新的 Swift 单视图项目,命名为 第九章 Xml 包装器。
如何操作…
-
首先,我们将创建两个项目组,所以右键单击源代码组,选择 新建组 并将其命名为
Pugi。重复此操作,将第二个组命名为PugiWrapper。 -
现在返回到 Pugi 源代码查找器窗口。在这里,你需要打开
src文件夹,然后使用快捷键 command + a 选择所有文件并将它们拖到Pugi组中。当需要创建桥接文件时接受创建:![如何操作…]()
-
现在转到
PugiWrapper组,创建一个新文件;这次你必须从 iOS 源 部分选择 Cocoa Touch Class。这个文件应该命名为PugiBase。它必须是NSObject的子类,并确保选择了 Objective-C 语言:![如何操作…]()
-
点击刚刚创建的文件
PugiBase.h,开始将pugixml.hpp文件包含进来,以防它来自 C++ 文件:#ifdef __cplusplus #include "pugixml.hpp" #endif Now we can define this class with the following code. @interface PugiBase : NSObject @property (nonatomic, assign) void * element; -(instancetype) init; @end Once this class interface is defined we can implement it, in this case only the initializer is necessary by setting the property to NULL. @implementation PugiBase -(instancetype) init{ self = [super init]; if(self){ self.element = NULL; } return self; } @end小贴士
现在你有了 MinGW 和 MSYS,你不再需要羡慕那些有 Linux 安装的用户了,因为他们已经在你的系统中实现了 Linux 开发环境的最重要部分。
-
这个类已经完成了,所以下一步是创建一个将继承自
PugiBase的类。为此,向PugiWrapper组添加一个新的 Cocoa Touch Class 并将其命名为PugiNodeAttribute。对于这个类,我们将要创建的唯一方法是setValue。了解这一点后,转到PugiNodeAttribute.h文件,并添加以下代码:#import "PugiBase.h" @interface PugiNodeAttribute : PugiBase -(void) setValue:(NSString *) value; @end -
实现这个类很简单,但是有一个重要的细节:这个类的实现将是 C++;这意味着在输入任何内容之前,将这个文件从
PugiNoteAttribute.m重命名为PugiNodeAttribute.mm(Objective-C++ 扩展):![如何操作…]()
-
点击重命名的文件,并使用以下代码完成类的实现:
@implementation PugiNodeAttribute -(void) setValue:(NSString *) value{ if(self.element){ reinterpret_cast<pugi::xml_attribute *>(self.element)->set_value([value UTF8String]); } } @end -
现在属性已经完成,我们可以重复操作为节点(XML 标签),因此创建一个名为
PugiNode的 Cocoa Touch 类文件,将.m重命名为.mm,进入头文件并添加以下代码:#import "PugiBase.h" #import "PugiNodeAttribute.h" @interface PugiNode : PugiBase -(PugiNode*) appendChild:(NSString *) name; -(PugiNodeAttribute *) appendAttribute:(NSString *) name; @end For the implementation just add this code. #import "PugiNode.h" #import "PugiNodeAttribute.h" @implementation PugiNode -(PugiNode*) appendChild:(NSString *) name{ if (self.element) { PugiNode * newNode = [PugiNode new]; newNode.element = new pugi::xml_node(reinterpret_cast<pugi::xml_node*>(self.element)->append_child([name UTF8String])); return newNode; } return nil; } -(PugiNodeAttribute *) appendAttribute:(NSString *) name{ if (self.element){ PugiNodeAttribute * newAttribute = [PugiNodeAttribute new]; newAttribute.element = new pugi::xml_attribute(reinterpret_cast<pugi::xml_node*>(self.element)->append_attribute(name.UTF8String)); return newAttribute; } return nil; } @end -
为了完成封装类,我们只需要创建另一个代表 XML 文档的类。再次创建一个新的 Cocoa Touch 类,这次命名为
PugiDocument。将实现文件从.m重命名为.mm,并将以下代码添加到.h文件中:#import "PugiBase.h" #import "PugiNode.h" @interface PugiDocument : PugiBase -(instancetype) init; -(PugiNode *) appendChild:(NSString *) name; -(void) saveFile:(NSString *) path; @end On the implementation file (PugiDocument.mm) add the following code. @implementation PugiDocument -(instancetype) init{ self = [super init]; if(self){ self.element = new pugi::xml_document; } return self; } -(PugiNode *) appendChild:(NSString *) name{ if(self.element){ PugiNode * newNode = [PugiNode new]; newNode.element = new pugi::xml_node(reinterpret_cast<pugi::xml_document *>(self.element)->append_child([name UTF8String])); return newNode; } return nil; } -(void) saveFile:(NSString *) path{ if (self.element) { reinterpret_cast<pugi::xml_document *>(self.element)->save_file(path.UTF8String); } } @end -
封装步骤已经完成,因此现在我们可以在我们的应用中使用这些类,但我们仍然需要创建另一个类,这个类将用于我们的应用并在创建 XML 文件之前存储用户信息。这个类将是一个纯 Swift 类,所以将一个名为
Task.swift的新 Swift 文件添加到项目中,并添加以下代码:class Task { var description:String var important :Bool init(description:String, important: Bool){ self.description = description self.important = important } } -
在我们开始编写视图控制器代码之前,别忘了你必须将封装类的头文件导入到桥接文件中。所以点击桥接文件(
Chapter 9 Xml Wrapper-Bridging-Header.h)并添加以下行:#import "PugiDocument.h" #import "PugiNode.h" #import "PugiNodeAttribute.h" -
现在,通过点击故事板文件并添加三个标签、两个按钮、一个文本框和一个 UISwitch 来模拟我们的视图,就像以下图像所示:
![如何做…]()
-
现在,将文本框与视图控制器连接,并命名为
taskTextField,而UISwitch应该命名为importantSwitch。生成的代码应该像这样:@IBOutlet var taskTextField: UITextField! @IBOutlet var importantSwitch: UISwitch! -
现在我们可以添加一个新的属性来存储用户任务:
var tasks = [Task]() -
我们需要做的最后一件事是为按钮创建事件。将添加按钮与以下代码连接:
@IBAction func addTask(sender: AnyObject) { tasks.append(Task(description: taskTextField.text, important: importantSwitch.on)) taskTextField.text = "" importantSwitch.on = false } -
好的,现在我们可以通过以下操作将保存按钮最终化:
@IBAction func saveXml(sender: AnyObject) { var document = PugiDocument()! var mainNode = document.appendChild("tasks") var path = (NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString).stringByAppendingPathComponent("tasks.xml") as String; for task in tasks { let node = mainNode.appendChild("task") let attributeDescription = node.appendAttribute("description") attributeDescription.setValue(task.description) let attributeImporant = node.appendAttribute("important") attributeImporant.setValue(task.important ? "yes" : "no") } document.saveFile(path) } -
恭喜,应用完成了!按播放键并添加一些任务,例如 打扫房间 或 努力学习 Swift。一旦完成,你可以按 保存 按钮。发生了什么?打开你的 iFunBox 或等效应用程序,检查应用的
Documents文件夹。
它是如何工作的…
pugixml 是一个易于使用的极简 XML 库。在这种情况下,我们只封装了必要的最小代码,即标签属性、XML 标签(称为 node)以及完整的 XML 文档。当然,这个库中还有更多类,如果你想的话可以完成代码。
如前所述,C++ 不能直接在 Swift 中使用,但你可以创建一个调用 C++ 的 Objective-C 类。这里有一个重要的细节:你可以创建 Objective-C 类,但不能创建 Objective-C++ 类,这意味着类接口不能包含任何 C++ 对象。
我们如何解决这个问题?每个 C++ 对象(如属性或属性)都必须声明为 void*(指向任何内容的非安全指针)并且参数或返回值必须是另一个封装类。
提示
为了更好地兼容 Swift 和 Objective-C,尽量使用两种语言都通用的类型,而不是 C 或 C++ 类型,例如使用 NSString 而不是 char*。
第十章。数据访问
在本章中,我们将涵盖以下菜谱:
-
创建 SQLite 数据库
-
检查你的 IP 地址来自哪里
-
跟踪你的手机活动
-
控制你的股票
-
使用 CouchDB 设计投票设备
简介
如你所知,如今很难想象一个不将任何内容存储在硬盘上的应用程序。像计算器或指南针这样的简单应用程序可能不需要存储任何信息,但通常你需要创建具有更复杂功能的应用程序,并且即使在设备重启的情况下也需要保留信息。
当你需要存储最小量的信息,如简单的日期或当前应用程序版本时,你可以使用文件,就像我们在本书的前几章中所做的那样,但当你需要存储具有不同数据结构的几个记录时,你需要数据库的帮助。
在本章中,我们将学习如何在 Swift 中使用数据库,你将看到每种方法的优点。
创建 SQLite 数据库
通常,在移动应用程序中存储信息是通过本地数据库完成的。为此,使用 SQLite 非常常见,因为尽管它有些限制,但这个数据库有一些优点,例如它是一个无服务器数据库,它是零配置的,并且它是内置在 iOS 和 Mac OS X 中的。
准备工作
对于这个菜谱,我们将下载一个 SQLite 文件;因此你需要一个 SQLite 客户端来读取这个文件。所以除了在上一章中下载的 iFunBox,你还需要下载一个程序,如 SQLiteBrowser (sqlitebrowser.org/)。
创建一个名为 Chapter 10 SQLite 的新项目;请记住你保存此菜谱的位置,因为我们将在下一个菜谱中完成它。
如何实现…
-
当项目创建完成后,点击 常规 选项卡,向下滚动直到你到达 链接框架和库 部分点击加号以添加新的库,并选择
libsqlite3.tbd:![如何做…]()
-
之后,让我们创建一个新的桥接文件,通过添加一个名为
BridgingHeader.h的新头文件;检查它是否在 构建设置 中设置为桥接头。在这个文件中,包含sqlite3.h:#include <sqlite3.h> -
现在添加一个名为
SQLite.swift的新文件;在这里,我们将开始编写一个名为SQLite的类,并在其中定义一个名为status的枚举:public class SQLite { public enum Status { case CONNECTED, DISCONNECTED } -
现在,让我们创建两个属性,一个用于与数据库的连接,另一个用于当前连接状态:
private var _connection:COpaquePointer = nil private var _status = SQLite.Status.DISCONNECTED -
下一步是创建一个只读的计算属性,它返回当前连接状态:
public var status:SQLite.Status { return _status } -
之后,你可以创建一个方法来打开与数据库的连接。对于这个方法,我们只需要一个文件名作为参数;然而,我们将将其存储在文档文件夹中:
public func connect(filename:String)-> Bool{ let documentsPath = (NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString).stringByAppendingPathComponent(filename) let error = sqlite3_open(documentsPath,&self._connection) if error == SQLITE_OK { // Adding a table just in case let statement = "CREATE TABLE IF NOT EXISTS ips " + "(ipstart text, ipend text, " + "iipstart integer, iipend integer, " + "country text);" as NSString var errmessage:UnsafeMutablePointer<CChar> = nil if sqlite3_exec(self._connection, statement.UTF8String, nil, nil, &errmessage) == SQLITE_OK { self._status = .CONNECTED return true } return false return false } -
这个类的最后一个细节是析构器,它应该关闭数据库连接:
deinit { switch self._status { case .CONNECTED: sqlite3_close(self._connection) default: break; } } } -
模型部分已完成;现在让我们通过点击故事板并添加一个标签、一个文本框和一个按钮来创建视图部分,布局类似于这个:
![如何操作…]()
-
现在将文本框与视图控制器连接,并命名为
databaseNameTextField:@IBOutlet var databaseNameTextField: UITextField! -
一旦完成,你就可以为唯一的按钮创建一个动作;在这种情况下,我们将打开数据库连接,创建一个表并检查是否一切操作都成功完成:
@IBAction func createDatabase(sender: AnyObject) { var database = SQLite() if self.databaseNameTextField.text == "" { let alert = UIAlertController(title: "No database name", message: "You must introduce a database name", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) return } let dbname = self.databaseNameTextField.text + ".sqlite" if database.connect(dbname) { UIAlertView(title: nil, message: "Database was created", delegate: nil, cancelButtonTitle: "OK").show() let alert = UIAlertController(title: nil, message: "Database was created", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) }else { let alert = UIAlertController(title: nil, message: "Failed creating the database", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } } -
这个应用完成了。更确切地说,这个第一个版本完成了;现在按播放按钮,为你的数据库输入一个文件名,例如
mydatabase,然后按创建数据库的按钮。 -
现在让我们检查实际的应用程序,所以打开你的 iFunBox 并搜索应用程序
第十章 SQLite;打开其Document文件夹,选择这个文件夹中的唯一文件,然后按 复制到 Mac 按钮并将其保存在你的本地文档文件夹中:![如何操作…]()
-
一旦你有了空数据库,让我们检查我们是否能够打开它。打开你的 Sqlite 浏览器(仅支持 8.3 及以下版本)并点击 打开 数据库 按钮:
![如何操作…]()
-
选择你下载的数据库文件并打开它。之后,你应该在主面板和数据库模式中看到一个名为
ips的表,这意味着你的数据库已成功创建,并且可以无错误地创建一个表:![如何操作…]()
它是如何工作的…
SQLite 不是一个框架;而是一个库,它使用传统的 C 函数而不是 Objective-C 或 Swift 对象。
如你所知,使用 C 函数意味着使用 C 类型;这就是我们为什么必须使用 UnsafeMutablePointer<CChar> 来表示错误信息,以及 COpaquePointer 来表示数据库句柄的原因。
注意
COpaquePointer 表示一个 C 指针,例如 UnsafeMutablePointer,但它在你无法在 Swift 中表示指针类型时使用,例如一些结构体,例如。
在这个菜谱中,我们使用了三个 SQLite 函数:
-
sqlite3_open:这个函数打开一个数据库。如果不存在,它将创建一个新的数据库文件。这个函数接收文件名和一个句柄指针作为参数。小贴士
如果你不想创建数据库文件,你可以使用内存数据库;查看
www.sqlite.org/inmemorydb.html。 -
sqlite3_close:这个函数关闭与数据库的连接并释放用于此连接的资源。请注意,C 没有对象,没有垃圾回收器,也没有自动引用计数器;因此,如果你不释放资源,它们将一直存在,直到你的应用程序结束或崩溃。 -
sqlite3_exec:这个函数执行一个 SQL 语句;在这种情况下,这并不是真的必要,因为我们只是想检查文件是否已创建;然而,有时如果你不使用数据库,SQLite 只会创建一个空文件。
参见
- SQLite 有很多选项和函数;你可以在官方网站上了解更多信息:
www.sqlite.org/cintro.html。现在你知道如何创建数据库了,让我们开始使用它,创建登记和查询结果。
检查你的 IP 地址来源
有时候你需要从远程数据库进行查询,但正如你所知,SQLite 与本地数据库一起工作,这意味着在查询之前你必须填充它。在这个菜谱中,我们将把 CSV 文件转换为 SQLite 数据库,然后查询一些结果。
准备工作
对于这个菜谱,我们需要一个包含每个国家 IP 地址范围的 CSV 文件。有一些网站会提供或出售给你。你可以从db-ip.com/db/download/country免费下载它。解压它,并将其添加到你的 SQLite 应用程序中。
注意
目前这个文件名为dbip-country-2014-12.csv,但每个月都会更改其名称,所以将提到的文件名替换为你拥有的那个。
如何操作…
-
让我们开始通过添加两个额外的方法来完善 SQLite 类,其中一个用于执行不返回任何结果的操作,例如插入、删除和更新查询。要做到这一点,请点击文件
SQLite.swift,并在 SQLite 类中添加以下代码:func exec(statement: String) -> Bool { var errmessage:UnsafeMutablePointer<CChar> = nil return sqlite3_exec(self._connection, (statement as NSString).UTF8String, nil, nil, &errmessage) == SQLITE_OK } func query(statement:String) -> [[String]]? { var sqliteStatement:COpaquePointer = nil if sqlite3_prepare_v2(self._connection, (statement as NSString).UTF8String , -1, &sqliteStatement, nil) != SQLITE_OK { return nil } var result = [[String]]() while sqlite3_step(sqliteStatement) == SQLITE_ROW { var row = [String]() for i in 0..<sqlite3_column_count(sqliteStatement) { row.append(String.fromCString(UnsafePointer<CChar>(sqlite3_column_text(sqliteStatement, i)))!) } result.append(row) } return result } -
现在,让我们在你的项目中添加一个名为
Functions.swift的新文件。这个文件将包含一些辅助函数。我们将要创建的第一个函数是读取 CSV 文件并返回其内容的双字符串数组:func csv2array(filename: String) -> [[String]]? { var error: NSErrorPointer = nil var url = NSBundle.mainBundle().URLForResource(filename, withExtension: "csv") if let fileContent = String(contentsOfURL: url!, encoding: NSUTF8StringEncoding, error: error){ var records = [[String]] () fileContent.enumerateLines({ (line, _) -> () in var fields:[String] = line.componentsSeparatedByString(",").map({ (field:String) -> String in return field.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: "\"")) }) if isIPv4(fields[0]) { records.append(fields) } }) return records }else { return nil } } -
现在,我们需要两个与 IP 字符串相关的额外函数;第一个将检查输入字符串是否具有 IPv4 格式:
func isIPv4(ip:String) -> Bool { var error: NSErrorPointer = nil return try NSRegularExpression(pattern: "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$", options: .CaseInsensitive, error: error)!.matchesInString(ip, options: nil, range:NSMakeRange(0, countElements(ip))).count > 0 } -
下一个函数将把 IP 从字符串转换为无符号整数;这将允许我们比较 IP 是否在某个范围内:
func ip2int(ip:String) -> UInt32 { return CFSwapInt32(inet_addr((ip as NSString).UTF8String)) } -
我们已经完成了辅助函数,现在我们必须更新故事板。转到你的故事板,并添加两个额外的按钮,一个带有“Populate”这个词,另一个带有“Search”这个词,类似于以下截图:
![如何操作…]()
-
现在,我们需要将这些按钮默认设置为隐藏,因为我们不会在创建数据库之前插入任何登记。要做到这一点,请点击这些新按钮中的一个,转到属性检查器,勾选“隐藏”选项,然后对另一个按钮重复此操作:
![如何操作…]()
-
一旦故事板完成,我们就需要更新视图控制器。让我们先从将标签和三个按钮与视图控制器连接开始:
@IBOutlet var inputLabel: UILabel! @IBOutlet var createButton: UIButton! @IBOutlet var populateButton: UIButton! @IBOutlet var searchButton: UIButton! -
我们还需要另一个属性,即数据库连接,它在创建数据库的动作中,所以我们要做的是将声明和初始化从
createDatabase方法内部移动到外部:var database = SQLite() @IBAction func createDatabase(sender: AnyObject) { if self.databaseNameTextField.text == "" { -
由于我们正在
createDatabase方法附近工作,我们可以利用它并更新它,通过隐藏一些不需要的视图,只显示填充按钮:@IBAction func createDatabase(sender: AnyObject) { if self.databaseNameTextField.text == "" { let alert = UIAlertController(title: "No database name", message: "You must introduce a database name", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) return } let dbname = self.databaseNameTextField.text + ".sqlite" if database.connect(dbname) { let alert = UIAlertController(title: nil, message: "Database was created", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) self.createButton.hidden = true self.populateButton.hidden = false self.inputLabel.text = "" self.databaseNameTextField.text = "" self.databaseNameTextField.hidden = true }else { let alert = UIAlertController(title: nil, message: "Failed creating database", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } } -
之后,我们可以将填充按钮与一个新的动作连接起来。这个动作将被命名为
populate,它将在我们的数据库上调用一个insert语句,然后显示搜索按钮:@IBAction func populate(sender: AnyObject) { if let data = csv2array("dbip-country-2014-12"){ print("total \(data.count)") var statements = data.map{ record -> String in return "INSERT INTO ips (ipstart, ipend, iipstart, iipend, country) VALUES " + "('\(record[0])', '\(record[1])', \(ip2int(record[0])), \(ip2int(record[1]))," + "'\(record[2])' )" } database.exec("delete from ips;") for statement in statements { database.exec(statement) } self.searchButton.hidden = false self.populateButton.hidden = true self.inputLabel.text = "Enter an IP" self.databaseNameTextField.text = "" self.databaseNameTextField.hidden = false } else { let alert = UIAlertController(title: nil, message: "Unable to parse the file", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } } -
如你所想,我们需要开发搜索动作。首先,它需要检查输入是否正确,然后它将查找它所属的 IP 范围。如果它能找到,它将显示 IP 所属的国家,否则它将显示国家未找到:
@IBAction func search(sender: AnyObject) { let iptext = self.databaseNameTextField.text if !isIPv4(iptext) { UIAlertView(title: "Error", message: "Wrong format", delegate: nil, cancelButtonTitle: "OK").show() return } let ipnumber = ip2int(iptext) let sql = "SELECT country FROM ips where \(ipnumber) between iipstart and iipend" if let result = database.query(sql){ if result.count > 0 && result[0].count > 0 { let alert = UIAlertController(title: "Found", message: " This ip belongs to \(result[0][0])", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) }else { let alert = UIAlertController(title: "Not found", message: "No result was found", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } }else { let alert = UIAlertController(title: "Error", message: "Failed to execute your query", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } } -
再次,我们完成了一个应用程序,我们需要对其进行测试。按播放,在测试字段中写入
mydatabase,然后按创建数据库。现在你应该只看到屏幕上的填充按钮,点击它——它需要一段时间才能完成。小贴士
如果你想在开发时知道应用程序何时开始写入磁盘,你可以点击调试器导航器,然后点击磁盘报告,你应该会看到一些表示磁盘写入活动的条形图,如下面的图像所示:
![如何做…]()
-
最后一步是查找特定 IP 的国家,所以当文本字段出现时,在文本字段中输入一个 IP,例如
74.125.230.52,然后按搜索。你应该会收到一个警报,显示这个 IP 来自哪里。
它是如何工作的…
SQL 有两种类型的查询:
-
有一些查询会修改数据结构,如
CREATE TABLE和ALTER TABLE,或者修改数据内容,如INSERT、DELETE或UPDATE,这些将在下一节中展示。这些查询通常不会返回任何数据。对于这类查询,你可以使用sqlite3_exec函数。 -
查询信息,如
SELECT。在这里,你需要以不同的方式工作;你需要使用sqlite3_prepare_v2函数来执行查询,并使用sqlite3_step来检索每条记录。
注意
如果你检查 SQLite API 文档,你会看到sqlite3_exec也可以从SELECT语句返回结果;但是它需要一个指向每个接收到的记录的函数的指针(称为回调函数)。
如果你用 C 开发了你的函数,你可以通过使用类型为CFunctionPointer的变量来拥有它的指针,但请记住,这种数据类型不能用于指向 Swift 函数。确保 SQLite 有自己的常量,例如SQLITE_OK用于指示操作已成功完成,SQLITE_ROW告诉我们它可以从语句结果中接收记录,或者SQLITE_DONE表示该语句结果上没有更多记录。
在这种情况下,我们遇到了将文本文件转换为数据库记录的常见情况。当你需要执行类似任务时,请详细检查。首先我们在一个数组中添加了每个记录;当我们完成插入记录时,大约需要 80 兆字节的 RAM。这不是问题,因为即使是低端设备如 iPhone 4S 也拥有 512 兆字节的 RAM,但如果你决定加载城市的 IP 数据库,它可能会消耗大量内存,因此你可能需要分割文件或将每个记录直接存储在数据库中,而不是使用中间数组。
另一个细节是我们没有使用事务,这意味着如果我们有任何原因需要在插入过程中停止,我们需要删除之前插入的每个记录;否则我们可能会出现重复记录或尝试重新插入所有内容时的错误。
另一个好问题是:为什么我们使用 IP 范围而不是每个 IP 一个记录?原因很简单:空间。如果我们为每个 IP 使用一个记录,我们需要 4,294,967,296 个记录;如果每个记录占用 1 千字节磁盘空间,它将需要 4,398,046,511,104 字节(约 4 太字节),这在我们任何苹果移动设备上都没有,除了苹果电脑,但为了仅一个 IP 表浪费这样的空间是不值得的。
另一个好问题是:为什么我们必须将 IP 字符串转换为无符号整数?即使你将 IP 视为一个数字序列,实际上它是一个 32 位无符号整数,这允许我们比较 IP 是否在某个范围内;否则我们将进行字符串比较,这是不正确的。
参见
-
SQLite 有很多函数,其中一些比其他函数使用得更频繁,一些比其他函数更专业;因此,检查
www.sqlite.org/c3ref/intro.html上的可用 C 函数和www.sqlite.org/lang.html上的 SQL 语句是个好主意。 -
在这里你学习了如何在应用程序中使用 SQLite,然而这给我们带来了一些工作要做,因为 SQLite 不是一个框架,它是一个 C 库。在下一个菜谱中,我们将学习如何使用 SQLite 的“预煮”类来节省开发时间。
跟踪你的手机活动
假设你想要通过记录人头顶靠近手机的时间来追踪你的手机接收(或拨打)的电话,因此你想要创建一个应用程序,每次当接近传感器检测到有东西靠近手机的前脸时,都会记录当前时间和手机坐标。
虽然我们还将使用 SQLite,但我们不会编写任何 SQL 语句。这次我们将使用一个仅使用 Swift 类型和对象的框架;这样我们就不必担心将类型从 Swift 转换为 C,再从 C 转换回 Swift。
准备工作
对于这个菜谱,我们需要下载一个名为 SQLite.Swift 的外部框架。要做到这一点,打开你喜欢的浏览器,并转到 github.com/stephencelis/SQLite.swift。一旦网站打开,点击下载 ZIP 图标。如果你使用了 Safari,下载的文件可能已经被解压。如果你使用的是其他网络浏览器,请通过在查找器窗口的文件图标上双击来自动解压它。使用此框架需要 Xcode 6.1 或更高版本,所以请确保你使用的是 Xcode 的更新版本。
由于这个菜谱将使用接近传感器,因此必须使用物理手机使用它,否则你将无法创建任何记录。
如果你已经准备好开始,创建一个名为 Chapter 10 Activity Recording 的新项目。
如何做…
-
首先,我们需要将
SQLite.Swift添加到我们的项目中。要做到这一点,只需将SQLite.SwiftXcode 项目文件(SQLite.xcodeproj)拖入你的项目;你现在应该在项目导航器中看到两个项目:![如何做…]()
-
现在点击你的项目以添加此框架,因此点击目标的目标通用选项卡,滚动到链接的框架和库,然后点击加号。当对话框出现时,你可以检查是否有名为工作区的部分,其中包含两个同名框架;选择为 iOS 准备的那个:
![如何做…]()
-
再次按加号,并选择另一个框架:
CoreLocation框架。 -
因为在这个菜谱中,我们只将开发视图控制器,所以我们不会添加任何新的文件。因此,直接点击故事板,向你的视图中添加一个文本字段和一个按钮。删除你的文本字段中的文本,并将按钮标签更改为打印记录。现在将文本字段与视图控制器连接,并命名为
textView:@IBOutlet var textView: UITextView! -
将按钮与一个名为
printRecords的新操作连接起来;我们现在不会开发它,只是将其留空以供将来使用:@IBAction func printRecords(sender: AnyObject) { } -
现在点击视图控制器文件,并将提示放置在此文件的开始处。在这里,我们需要导入 UIKit 之外的
SQLite和CoreLocation两个框架:import SQLite import CoreLocation -
在我们开始开发视图控制器代码之前,我们将为我们的公共变量创建一个辅助类型:
typealias activityTuple = (activity: Query, id:Expression<Int>, latitude: Expression<Double?>, longitude:Expression<Double?>, time:Expression<String>, away:Expression<Bool>) -
由于我们将需要使用核心位置来接收当前位置,我们需要将视图控制器实现为核心位置代理:
class ViewController: UIViewController, CLLocationManagerDelegate { -
现在我们可以添加属性,我们需要一个数据库连接用于位置管理器,另一个用于保存接收到的最后位置:
var database:Database? var locationManager = CLLocationManager() var lastLocation: CLLocation? -
是时候开发方法了,从
viewDidLoad开始;在这里,我们需要设置一切:override func viewDidLoad() { super.viewDidLoad() if !openDatabase(){ let alert = UIAlertController(title: "Error", message: "Cant open the database", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) return } if !createStructure() { UIAlertView(title: "Error", message: "Can't create database structure", delegate: nil, cancelButtonTitle: "OK").show() return } setLocationManager() setProximitySensor() } -
如你所见,我们有几个方法需要实现。让我们从
openDatabase开始,它创建数据库并处理连接:private func openDatabase() -> Bool{ let documentsPath = (NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString).stringByAppendingPathComponent("database.sqlite") database = Database(documentsPath) return database != nil } -
下一步是创建数据库结构;它将创建一个名为
activity的表,并包含以下列:private func createStructure() -> Bool { var actVars = self.activityVars() var result = database!.create(table: actVars.activity, ifNotExists: true) { t in // Autoincrement means that we don't have to set this value because it will be automatic. t.column(actVars.id, primaryKey: .Autoincrement) t.column(actVars.latitude) t.column(actVars.longitude) t.column(actVars.time, unique: true) t.column(actVars.away) } return !result.failed } -
数据库初始化已完成;我们需要通过设置核心位置来开始接收设备位置信息:
private func setLocationManager(){ locationManager.delegate = self locationManager.distanceFilter = kCLDistanceFilterNone locationManager.desiredAccuracy = kCLLocationAccuracyBest if (UIDevice.currentDevice().systemVersion as NSString).floatValue >= 8 && CLLocationManager.authorizationStatus() != CLAuthorizationStatus.AuthorizedAlways { locationManager.requestAlwaysAuthorization() } locationManager.startUpdatingLocation() } -
如你所知,我们需要实现更新当前位置的方法:
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!){ if locations.count > 0 { lastLocation = locations[0] as? CLLocation } } -
现在我们需要从接近传感器获取通知;为了做到这一点,我们需要使用通知中心:
private func setProximitySensor(){ var device = UIDevice.currentDevice() device.proximityMonitoringEnabled = true if device.proximityMonitoringEnabled { NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("proximity:"), name: UIDeviceProximityStateDidChangeNotification, object: device) } } -
如你所见,当我们收到接近传感器变化时,
proximity方法将被调用;那就是我们必须在数据库中存储活动开始或结束的时刻:func proximity(notification:NSNotification){ var device: AnyObject? = notification.object var latitude:Double? var longitude:Double? if lastLocation != nil { latitude = lastLocation!.coordinate.latitude longitude = lastLocation!.coordinate.longitude } let dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" // superset of OP's format let dateString:String = dateFormatter.stringFromDate(NSDate()) let actVars = self.activityVars() if let id = actVars.activity.insert(actVars.away <- !device!.proximityState!, actVars.time <- dateString, actVars.latitude <- latitude, actVars.longitude <- longitude ) { print("Register inserted with id \(id)") } } -
现在我们需要创建一个返回与活动表相关的变量的方法:
private func activityVars() -> activityTuple{ return (activity:database!["activity"], id:Expression<Int>("id"), latitude: Expression<Double?>("latitude"), longitude:Expression<Double?>("longitude"), time:Expression<String>("time"), away:Expression<Bool>("away") ) } -
开发的最后一部分是按钮事件,直到现在都是空的。我们只需要从活动表中检索数据并将其添加到文本视图中:
@IBAction func printRecords(sender: AnyObject) { self.textView.text = "" let actVars = activityVars() for record in actVars.activity { textView.text = textView.text + "id: \(record[actVars.id]), time: \(record[actVars.time]), away: \(record[actVars.away])" if record[actVars.latitude] != nil { textView.text = textView.text + ", latitude: \(record[actVars.latitude]!), longitude: \(record[actVars.longitude]!)" } textView.text = textView.text + "\n" } } -
你可能还需要执行另一个步骤:点击你的项目的 Info.plist 文件并添加一个新行。在这个行中,将键设置为
NSLocationAlwaysUsageDescription,将值设置为This app needs GPS。这样做的原因是,在 iOS 8 中,当你请求使用 GPS(核心位置)的权限时,如果没有消息,它会被忽略。有些人说这是一个 bug,并且它应该很快得到修复。 -
再次强调,应用已经完成,我们需要对其进行测试,所以按播放按钮,并将你的手机放在你的头部旁边,就像你在用它通话一样。重复几次,然后通过按按钮检查表内容。你的结果应该类似于这个截图:
![如何做到这一点…]()
它是如何工作的…
SQLite.Swift是 SQLite 库的一个良好封装。你可以通过创建数据库类型的对象来创建数据库。你可以使用一个名为Expression的泛型类来定义表字段。这个类与整数(Int)、双精度(Double)、字符串和布尔类型(Bool)一起工作。所有这些都可以声明为可选的,这意味着数据库可以在其中存储 null 值。
你可以使用如insert或update这样的方法来添加或更改记录;这两个方法都接收一个名为 setter 的类型,它类似于字段和其值之间的关系。要创建 setter 类型的对象,你必须使用操作符<-。
对于检索结果,你可以像使用for循环一样遍历一个查询对象,并使用表达式作为索引通过下标访问其字段。
小贴士
你可以使用过滤方法来检索符合特定标准的记录,例如 SQL 语言中的 WHERE 子句。
还有更多…
正如你所看到的,我们不需要编写 SQL 语句;然而SQLite.Swift允许你在需要时使用 SQL 语句。为此,你可以使用如run或prepare这样的方法。
SQLite.Swift的另一个优秀特性是它支持事务。事务方法及其使用事务的方式。
苹果公司还有一个在其自身层上使用数据库的解决方案,称为Core Data。在下一个菜谱中,我们将学习如何使用它。
控制你的库存
一个应用程序与数据库相连的想法源于需要保留一些数据,即使应用程序已经结束。然而,SQL 是另一种语言,你必须重复一些开发工作,例如,在类中添加一个新字段,在数据库中也是一个新字段。
当施乐公司开发基于 Smalltalk 的第一个窗口系统时,它没有使用任何类型的数据库。它的论点是,如果一个应用程序没有结束,数据总是会留在 RAM 内存中。
这个想法非常聪明,然而我们知道现实世界并不总是这样运作。应用程序会崩溃并结束,设备有时需要重启。除了这些事实之外,你还需要考虑,直到今天,RAM 内存仍然比硬盘更贵。例如,新的 iPhone 6 只有 1GB 的 RAM 和至少 32GB 的永久存储。
基于上述提到的问题,苹果推荐使用其自己的 ORM(对象关系映射)框架,称为Core Data。ORM 是一种框架,允许你的对象存储在永久存储系统中,而无需浪费时间编写 SQL 语句。实际上,Core Data 为我们编写了 SQL。
在这个菜谱中,我们将通过模拟仓库的产品控制来开发一个小应用程序,使用 Core Data。
准备工作
开始创建一个名为第十章库存控制的新应用程序,但请确保已选中如下截图所示的使用 Core Data选项:

如何操作…
-
首先,我们需要创建一个数据库模型。在创建此模型时,不要考虑 SQLite,因为这里会有一些不同类型。所以点击文件
Chapter_10_Stock_Control.xcdatamodeld;你应该看到一个不同的布局和一些空字段。让我们先按一下位于文本添加实体上方的符号:![如何操作…]()
-
将新实体重命名为
Product:![如何操作…]()
-
之后,为这个实体添加三个属性,一个名为
name,类型为字符串,一个名为price,类型为双精度浮点数,还有一个名为units,类型为 32 位整数:![如何操作…]()
-
数据部分就到这里。现在我们需要点击故事板文件。通过点击它来选择你拥有的唯一视图控制器,然后转到编辑菜单,展开嵌入在选项并选择导航控制器:
![如何操作…]()
-
由于我们不希望这个应用程序有导航栏,你可以点击刚刚创建的导航控制器,并在属性检查器中取消选中显示导航栏选项:
![如何操作…]()
-
现在在我们拥有的唯一视图控制器上添加一个带有文本新产品的按钮;在其下方添加一个表格视图。你应该有一个类似于以下布局:
![如何操作…]()
-
现在转到视图控制器并导入
CoreData:import CoreData -
之后,你必须将
UITableViewDataSource协议添加到视图控制器中;这样我们就可以在表格视图中显示结果:class ViewController: UIViewController, UITableViewDataSource { -
现在将表格视图与视图控制器作为数据源和属性连接起来。因为我们正在添加一个属性,我们也可以添加一个新属性,它将包含我们的记录:
@IBOutlet var tableView: UITableView! var products = [NSManagedObject]() -
一旦完成,我们就必须实现更新表格视图信息的那些方法。在这种情况下,记录的数量是数组的长度,但它们的值是
NSManagedObject的一些键的值:func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return products.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell? if (cell == nil) { cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "cell") } cell!.textLabel?.text = products[indexPath.row].valueForKey("name") as? String var units = products[indexPath.row].valueForKey("units") as? Int cell!.detailTextLabel?.text = "\(units!) units" return cell! } -
想象一下,当我们创建一个新记录时,重新加载表格视图数据是必要的。我们可以创建一个刷新按钮,但这样并不直观,所以我们能做的最好的方式是,当视图再次出现时,我们可以刷新数据以及更新产品数组:
override func viewWillAppear(animated: Bool) { var appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate var moc: NSManagedObjectContext = appDelegate.managedObjectContext! var request = NSFetchRequest(entityName: "Product") request.returnsObjectsAsFaults = false do { try products = moc.executeFetchRequest(request) as [NSManagedObject] } catch { let alert = UIAlertController(title: "Error", message: "Error fetching the data.", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } self.tableView.reloadData() } -
好的,按下播放,会发生什么?什么都没有!原因是我们可以列出记录,但不能插入它们,所以是时候给新产品按钮添加功能了。返回到故事板,添加一个新的视图控制器,将其放在上一个视图控制器的右侧,并按住键点击新产品按钮,将其拖到新的视图控制器中。
-
按下播放,当然你不会看到任何记录,但如果你点击按钮,你可以看到一个新视图被调用。在这个时候,我们需要开发这个视图,然而,在我们继续之前,它需要一个视图控制器文件;在这种情况下,你必须添加一个新文件,类型为 Cocoa Touch Class,并将其命名为
NewProductViewController。确保它继承自UIViewController:![如何做…]()
-
返回到你的故事板,并在身份检查器中将此视图控制器的类更改为新创建的文件:
![如何做…]()
-
现在让我们为这个视图创建一个布局。我们必须添加三个文本字段(每个实体字段一个),三个标签来解释这些字段的内容,一个用于保存记录的按钮,以及一个仅用于显示视图标题的标签(因为我们没有导航栏):
![如何做…]()
-
一旦布局完成,我们必须将文本字段连接到新的产品视图控制器:
@IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var priceTextField: UITextField! @IBOutlet weak var unitsTextField: UITextField! -
我们需要做的最后一部分是保存按钮的开发。因为它将要使用几个
CoreData类,所以你必须先导入它们:import CoreData -
现在将你的
save按钮连接到一个名为save的动作,并通过检索文本字段的数据并使用核心数据保存它来开发它:@IBAction func save(sender: UIButton) { var appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate var moc: NSManagedObjectContext = appDelegate.managedObjectContext! var newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: moc) as NSManagedObject newProduct.setValue(self.nameTextField.text, forKey: "name") newProduct.setValue( (priceTextField.text as NSString).floatValue, forKey: "price") newProduct.setValue(unitsTextField.text.toInt()!, forKey: "units") var err:NSError? moc.save(&err) if let error = err { print(error.localizedDescription) }else{ print(newProduct) } self.navigationController?.popViewControllerAnimated(true) } -
最后一步是测试你的应用程序。按下播放,添加一些产品,如电脑、土豆和汽车,并检查它们是否出现在你的表格视图中。
它是如何工作的…
我们必须非常感谢苹果公司,因为旧版本的 Xcode 没有创建 CoreData 应用程序的选项,这意味着在 AppDelegate 类上执行的初始化代码必须手动完成。它所做的就是创建一些属性,例如 managedObjectModel,这些属性读取实体模型并对其进行解释;然后 persistentStoreCoordinator 将核心数据转换为持久化系统(默认为 SQLite,但可以配置为 XML 文件等),还有一个非常重要的一点:managedObjectContext;这个对象控制属于 CoreData 的对象/记录。
因此,每次我们需要使用 CoreData 中的某些东西时,我们都需要几次访问 AppDelegate 类。如果你愿意,你可以创建自己的单例类并将 CoreData 代码转移到那里。
当我们需要一个新的记录时,我们需要一个 NSManagedObject 类型的对象。如果它是一个新记录,我们可以请求 NSEntityDescription 类的 insertNewObjectForEntityForName 方法来帮忙。访问其字段非常简单;我们只需要使用 valueForKey 来获取其值或 setValue 来修改它。
如果你需要访问对象,你必须使用 NSFetchRequest,它就像一个 SQL 语句。managedObjectContext 是可以执行此请求并返回其数据的对象。
还有更多...
如果你想要过滤一些数据,你可以使用 NSPredicate,它类似于 SQL 的 where 子句。
我们已经看到了一些将数据存储到本地设备的方法,但关于网络数据库呢?在下一个菜谱中,我们将使用集中式数据库来存储我们的数据。
使用 CouchDB 设计投票设备
正如我们之前所学的,通常在移动应用中,我们访问本地数据库,但有时我们只需要使用一个与许多连接的设备共享的数据库。例如,当你进行 Google 搜索时,你不会将整个 Google 指数下载到你的手机上,你只是请求一些信息并检索结果。
在这个菜谱中,我们将学习如何使用集中式数据库,在这种情况下,我们将使用一个名为 CouchDB 的数据库。
准备中
对于这个菜谱,你需要下载一个 CouchDB 服务器。我假设你正在 Mac 计算机上开发,所以它将以 Mac OS X 进行演示。如果你更喜欢使用其他平台,如 Linux 或 Windows,请随意使用。
从 couchdb.apache.org 下载 CouchDB。一旦下载并解压,右键单击其图标,从菜单中选择 显示包内容 选项:

现在按照路径Contents/Resources/couchdbx-core/etc/couchdb前进;在这里你应该看到一个名为default.ini的文件。打开它,搜索变量bind_address。这里你必须将127.0.0.1更改为0.0.0.0。现在返回 CouchDB 应用所在的文件夹,通过双击打开应用。你的网络浏览器应该会打开,显示 CouchDB 前端(称为Futon);保持它打开,以便稍后检查结果。
一个你必须牢记的重要细节是,如果你打算使用一个物理设备,它必须使用与你的电脑相同的 Wi-Fi 网络;否则,你的设备将无法找到你的数据库服务器。
返回你的 Xcode,创建一个名为Chapter 10 Voting的新项目。在这里,你不需要CoreData。
如何做到这一点…
-
第一步是设置数据库。打开你的网络浏览器,点击创建数据库的按钮。如果你因为任何原因关闭了浏览器,只需输入 URL
http://127.0.0.1:5984/_utils/。当网站要求输入数据库名称时,写下voting:![如何做到这一点…]()
-
在这个数据库中,我们需要添加一些文档。所以点击名为新建文档的按钮。
注意
CouchDB 中的文档是 JSON 字典。
-
对于第一个文档,点击源并按照以下代码完成。不要修改 _id 值,保留 CouchDB 给你的那个:
{ "_id": "608d7c9174f7caba4ab618d6810004cf", "question": "What is your favorite computer programming language?", "answers": [ { "answer": "Swift", "votes": [] }, { "answer": "Objective-C", "votes": [] }, { "answer": "C", "votes": [] } ] } -
好的,现在点击保存文档,然后让我们用另一个文档重复这个操作;这样我们可以确保问题是从数据库接收到的:
{ "_id": "608d7c9174f7caba4ab618d681001467", "_rev": "3-41c057820d6cbd595e7db2bdf05610fe", "question": "What is your favorite book?", "answers": [ { "answer": "Swift cookbook", "votes": [] }, { "answer": "Divine Comedy", "votes": [] }, { "answer": "Oxford dictionary", "votes": [] } ] } -
现在我们需要创建一个更新处理程序,它就像一个接收一些数据并为我们完成文档的函数。点击新建文档,这次我们需要更改文档 ID,因为它将被命名为
_design/voting:{ "_id": "_design/voting", "updates": { "addvote": "function(doc, req) { var json = JSON.parse(req.body); doc.answers[parseInt(json.answer)].votes.push(json.uid); return [doc, toJSON(doc)]}" } } -
数据库部分已经完成;因此,你必须回到你的 Xcode 项目中。
注意
解释 CouchDB 的工作原理超出了本书的范围;如果你想学习如何使用这个数据库,有关于它的优秀书籍和教程。
-
一旦你回到了你的 Xcode 项目,点击故事板,并在视图中添加两个标签和三个按钮。在视图顶部放置第一个标签,上面写着
TODAY'S QUESTION。在其下方放置另一个标签,它将包含接收到的提问,然后在其下方放置三个按钮。不用担心这些组件的文本,它们将通过编程方式更改。 -
如同往常,我们必须将一些组件与视图控制器链接起来;在这种情况下,我们必须连接问题标签和三个按钮:
@IBOutlet var questionLabel: UILabel! @IBOutlet var answer1button: UIButton! @IBOutlet var answer2button: UIButton! @IBOutlet var answer3button: UIButton! -
此外,我们还将添加两个属性,一个用于文档 ID,另一个用于请求 URL 的公共部分。记住,如果你正在使用设备,你必须将 IP 从
127.0.0.1更改为你的电脑的 IP:var documentId:String? let baseurl = "http://127.0.0.1:5984/voting/" -
将三个按钮连接到同一个名为
vote的动作。现在暂时留这个动作为空:@IBAction func vote(sender: UIButton) { } -
当应用程序启动时,我们需要接收数据库中当前的问题,所以让我们在
viewDidLoad方法中调用一个函数来帮我们完成这个任务:override func viewDidLoad() { super.viewDidLoad() self.chooseQuestion() } -
如你所想,我们必须实现
chooseQuestion方法。这个方法将调用_all_docs动作,它返回数据库中每个文档的 ID,包括特殊文档_design/voting。一旦我们收到文档,我们就可以选择其中一个并请求其数据:private func chooseQuestion(){ var url = NSURL(string: baseurl + "_all_docs")! var task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { data, response, error -> Void in if error != nil { print(error.localizedDescription) } var jsonResult = [:] do { jsonResult = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as NSDictionary } catch { let alert = UIAlertController(title: "Error", message: "Error parsing JSON", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } var invalid = true var docid:String = "" while invalid { let rows = jsonResult.valueForKey("rows") as NSArray srandom(UInt32(time(nil))) var choosenRow:Int = random() % rows.count docid = (rows[choosenRow] as NSDictionary).valueForKey("id") as String invalid = startsWith(docid, "_") } self.getQuestion( docid ) }) task.resume() } -
一旦应用程序决定它想要哪个文档,我们就可以请求它的数据。机制与之前类似,但不同之处在于当我们收到数据时,我们必须更新 UI。正如你所知,这必须在主线程上完成:
private func getQuestion(id:String){ self.documentId = id var url = NSURL(string: baseurl + id)! var task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { data, response, error -> Void in if error != nil { print(error.localizedDescription) } var jsonResult = [:] do { jsonResult = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as NSDictionary } catch { let alert = UIAlertController(title: "Error", message: "Error parsing JSON", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } dispatch_async(dispatch_get_main_queue(), { self.questionLabel.text = jsonResult.valueForKey("question") var answers = jsonResult.valueForKey("answers") as [NSDictionary] self.answer1button.setTitle(answers[0].valueForKey("answer") as? String, forState: .Normal) self.answer2button.setTitle(answers[1].valueForKey("answer") as? String, forState: .Normal) self.answer3button.setTitle(answers[2].valueForKey("answer") as? String, forState: .Normal) }) }) task.resume() } -
如果你现在按下播放按钮,你会看到一个像下面的屏幕。这意味着你从数据库中收到了一个问题及其可能的答案。然而,如果你选择任何答案,什么也不会发生:
![如何操作…]()
-
现在是时候实现投票动作了。想法是检查哪个按钮被按下,然后将这个信息连同设备 ID 一起发送到数据库。这样我们的投票就会被记录:
@IBAction func vote(sender: UIButton) { var answer:Int switch sender { case answer1button: answer = 0 case answer2button: answer = 1 case answer3button: answer = 2 default: return } // input var params = ["answer":"\(answer)", "uid":UIDevice.currentDevice().identifierForVendor.UUIDString] as Dictionary<String, String> var request = NSMutableURLRequest(URL: NSURL(string: baseurl + "_design/voting/_update/addvote/\(documentId!)")!) request.HTTPMethod = "POST" do { request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(params, options: nil) } catch { let alert = UIAlertController(title: "Error", message: "Error getting HTTP header", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") var task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in UIAlertView(title: nil, message: "Thanks for voting", delegate: nil, cancelButtonTitle: "OK").show() }) task.resume() } -
应用程序已经完成,所以现在我们必须检查它与数据库的正确通信。再次按下播放按钮并选择一个答案。你应该收到一个感谢你的警报视图,这是一个好兆头。之后,再次打开网页浏览器并打开所选文档;检查你的设备 ID 是否在所选答案中:
![如何操作…]()
它是如何工作的…
如果你曾经与远程 SQL 数据库合作过,你可能知道通常你需要一个驱动器/连接器,它应该与你的平台兼容,也可能被某些防火墙阻止。
CouchDB 是一个通过 http 协议交换 JSON 消息工作的 NoSQL 数据库,这使得我们的生活更加容易,因为我们不需要添加任何控制器,只需接收和发送 JSON 消息。由于这个数据库的工作方式,我们不得不在客户端开发一个用于更新/插入新投票的函数;记住,Swift 不是 JavaScript:将新元素添加到 JSON 数组中可能会给我们带来比开发一个简单的函数更多的工作。
为什么我们不得不几次将NSArray和NSDictionary转换为类型?原因是NSJSONSerialization是在 Objective-C 时代创建的,这意味着它仍然不是 100%为 Swift 准备的。
诚然,我们工作的方式可能组织得更好。理想的方式是创建一个 CouchDB 层,比如一个框架或库,然而这项任务将被留作家庭作业。
还有更多…
在这一章中,我们学习了在 Swift 中使用数据库的不同方法;其中一些非常直接,而另一些则需要更好的分析。
在下一章中,我们将学习一些新的技巧,主要是与新的 Xcode 6 和 iOS 8 相关的新技巧。
第十一章. 扩展、照片及其他
在本章中,我们将涵盖以下菜谱:
-
开发最酷的键盘
-
是时候服用你的药片了
-
为你的照片添加效果
-
成为电影评论家
-
留下痕迹
-
创建货币转换器应用程序
-
Swift 中的方法交换
-
Swift 中的关联对象
简介
这是本书的最后一章。在这里,我们将学习前几章未提及的不同主题,主要是 Xcode 6 的新功能。
开发最酷的键盘
应用扩展是一个新特性,其中应用程序可以附带一些插件,这些插件甚至可以与其他应用程序交互。
在这种情况下,我们将为极客开发一个键盘。这个键盘将只包含两个键:键 0 和键 1。当你输入八个键的组合时,你会得到一个新的字符。
准备工作
对于这个菜谱,请确保您有 iOS 8,无论您使用的是模拟器还是物理设备。自定义键盘功能仅在 iOS 8 上可用。
创建一个名为 Chapter 11 Geekboard 的新单视图应用程序,让我们开始编码。
如何操作…
-
此菜谱的主要开发基于自定义键盘的应用扩展。然而,由于我们需要一个视图来测试我们的键盘,让我们先点击故事板,并在我们的视图中添加一个文本字段。将此文本字段与名为
inputTextField的视图控制器链接:@IBOutlet weak var inputTextField: UITextField! -
现在,让我们使这个文本字段成为即将启动的应用程序的首个响应者。您不需要点击字段来显示键盘:
override func viewDidLoad() { super.viewDidLoad() self.inputTextField.becomeFirstResponder() } -
这是我们必须使用此视图控制器编写的所有内容;其他所有内容都将由应用程序扩展完成。下一步是打开菜单,并向我们的项目添加一个新目标。在这种情况下,从 应用程序扩展 部分中选择 自定义键盘:
![如何操作…]()
-
将此目标命名为
Geekboard,当对话框询问激活 Geekboard 方案时按 是:![如何操作…]()
-
在编码之前,让我们先向此目标添加一个新视图;因此,从菜单中选择新文件,并在 用户界面 部分中选择 视图:
![如何操作…]()
-
将此视图命名为
Geekboard(再次),但在按下 创建 按钮之前,确保此文件属于扩展目标,如下面的截图所示:![如何操作…]()
-
创建完成后,点击新文件(
Geekboard.xib),选择它拥有的唯一视图,然后通过点击 属性检查器 来更改一些属性。在这里,您需要将大小更改为 自由形式,状态栏更改为 无,背景颜色更改为银色:![如何操作…]()
-
太好了!之后,选择 大小检查器,将视图大小更改为 320 x 160:
![如何操作…]()
-
视图属性已完成,现在我们需要设置文件的拥有者类。要做到这一点,请单击文件的拥有者图标(黄色立方体),选择身份检查器,并将类名更改为
KeyboardViewController:![如何操作…]()
-
在这个 XIB 文件中,我们还需要做一件事:我们必须为此布局添加一些组件。添加一个标签,让用户知道所制作的二进制组合,以及两个按钮:一个代表数字 0,另一个代表数字1。它应该看起来类似于以下截图:
![如何操作…]()
-
当然,标签将会被更改,按钮不需要不同的动作,因为唯一的区别是数字值;因此,我们将为两个按钮创建相同的动作并通过检查发送者来区分它们。总结一下,根据以下代码将标签和按钮与
KeyboardViewController链接:@IBOutlet var button0: UIButton! @IBOutlet var button1: UIButton! @IBOutlet var label:UILabel!注意
不要删除
KeyboardViewController上由 Xcode 完成的任何代码。如果需要删除任何代码,它将明确写出。 -
将两个按钮与一个名为
addBit的空动作链接。不用担心它的内容,我们稍后会开发它:@IBAction func addBit(sender: UIButton){ } -
在
KeyboardViewController中,我们还将添加两个用于控制当前键盘状态的属性:var currentBinaryText:String = "" var currentBinaryNumber:Int = 0 -
现在我们需要设置视图,所以转到
viewDidLoad方法,在super.viewDidLoad之后和苹果预编译代码之前添加一些代码行:override func viewDidLoad() { super.viewDidLoad() // Perform custom UI setup here var geekNib = UINib(nibName: "Geekboard", bundle: nil) self.view = geekNib.instantiateWithOwner(self, options: nil)[0] as UIView self.label.text = currentBinaryText self.nextKeyboardButton = UIButton.buttonWithType(.System) as UIButton … -
打完这些后,我们可以通过开发按钮事件来完成我们的应用程序:
@IBAction func addBit(sender: UIButton){ var number: Int switch sender{ case button0: number = 0 case button1: number = 1 default: return } currentBinaryText += "\(number)" currentBinaryNumber = currentBinaryNumber * 2 + number if countElements(currentBinaryText) == 8 { var proxy = textDocumentProxy as UITextDocumentProxy proxy.insertText(String(UnicodeScalar(currentBinaryNumber))) currentBinaryNumber = 0 currentBinaryText = "" } self.label.text = currentBinaryText } -
应用程序已经完成,让我们来测试它。按下播放键,当应用程序启动时,显示的键盘不是我们的键盘!!!发生了什么?原因是您必须以与在设备上添加另一种语言键盘相同的方式添加此键盘。
-
考虑到这一点,按住主页按钮,进入设置,选择通用,然后键盘,然后键盘选项中的另一个选项,最后选择添加新键盘...。
-
您应该会看到一些建议的键盘和另一个包含第三方键盘的部分。从该部分选择Geekboard:
![如何操作…]()
-
返回您的应用程序(第十一章 Geekboard),您将看到键盘还没有出现。因此,您必须轻触地球图标,直到您获得键盘,然后,哇!它开始工作了。例如,键入这个二进制消息:
01001000 01000101 01001100 01001100 01001111,但如果您真的很酷,您可以去邮件应用程序,只用这个键盘写一封电子邮件。你接受挑战吗?
它是如何工作的…
自定义键盘是一个称为应用扩展的功能。它有一些限制,例如,它不能用于密码和其他文本字段类型,如电话联系人。它也不能在其顶部显示任何内容。
创建一个自定义键盘意味着创建一个 UIInputViewController 类型的控制器,这是一个继承自 UIViewController 的类,这意味着如果需要,你可以使用 UIViewController 的方法。
为了使键盘开发更简单,我们添加了一个新的 XIB 文件,这使得我们可以直观地创建布局。一些开发者认为,由于故事板被整合,XIB 文件已被从 Xcode 中移除,然而你可以看到这并不正确;你仍然可以使用 XIB 文件来定制一些视图,例如键盘或表格单元格。
将文本提交到文本字段非常简单:你只需要创建一个 UITextDocumentProxy 对象并使用 insertText 方法。它将神奇地知道活动文本字段。
还有更多…
自定义键盘的通信有点有限,因为它默认不能使用网络或与包含的应用程序共享任何文件。如果你希望使用这些功能,你必须进入它的 Info.plist 并将选项 RequestsOpenAccess 设置为 yes。
在下一个菜谱中,我们将学习一些不同的事情:我们将为 Apple Watch 开发。
现在是吃药的时候了
在这个世界上,谁从未生病过?让我们面对现实,迟早我们都会生病,我们必须遵循医生的处方。如果你像我一样,在吃药的时候经常看手表,也许我们需要的是一个可以告诉我们这个信息的应用程序,这次它将是一个 Apple Watch 应用程序。
准备工作
对于这个菜谱,你需要 Xcode 6.2 或更高版本,因为我们将要使用 WatchKit,这在之前的版本中不可用。
按照惯例,只需创建一个单独的视图 iOS 应用程序,并将其命名为 第十一章 红丸。
如何操作…
-
我们需要做的第一步是创建一个新的目标,但这次你必须从 WatchKit 部分的 WatchKit 应用 中添加:
![如何操作…]()
-
在 下一步 对话框中取消选中通知、快速查看和复杂功能选项;这将使项目更简洁:
![如何操作…]()
-
之后,将出现一个请求激活 WatchKit 应用的对话框;通过按下 激活 按钮接受它:
![如何操作…]()
-
你可以看到在你的项目中有两个新的组:一个用于 WatchKit 扩展,另一个用于 WatchKit 应用。打开你的扩展组,添加一个名为
FrequencyData.swift的新 Swift 文件。这里你只需要输入以下简单的代码:class FrequencyData: CustomStringConvertable { var description: String { switch self.time { case 0 ..< 60: return "Every \(self.time) minutes" case 60 ..< (24 * 60): return "Every \(self.time/60) hours" default: return "Every \(self.time/60/24) days" } } var time:Int // minutes init(time:Int){ self.time = time } } -
现在转到你的 WatchKit 应用组,展开它,并点击故事板。这里你有一个类似于视图控制器的东西,但在这里它被称为界面。在你的界面中添加一个标签,并尝试让它适合整个屏幕。将其与界面控制器作为 IBOutlet 连接:
@IBOutlet var label:WKInterfaceLabel! -
创建一个新的界面,放置另一个标签;你不必连接它,只需将其文本更改为
现在是吃药的时候了。如果它不适合,将标签的行数更改为两行。 -
现在点击第一个界面,按住控制键并将其拖动到第二个界面。现在您的 Storyboard 可能看起来像以下这样:
![如何操作…]()
-
选择第二个界面,并转到其属性检查器。将标识符设置为
its_time:![如何操作…]()
-
返回到扩展组,并打开
InterfaceController文件。像往常一样,我们将从添加必要的属性开始:var timer:NSTimer? var remainingTime:Int? var context:AnyObject? var options = [FrequencyData(time: 2), FrequencyData(time:4 * 60), FrequencyData(time: 8 * 60), FrequencyData(time: 24 * 60)] -
现在在
awakeWithContext方法上初始化上下文:override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) self.context = context } -
之后,我们必须请求用户选择他需要服用药片的时间。在
willActivate方法中执行此操作:override func willActivate() { super.willActivate() let texts = options.map({ (freq) -> String in return freq.description }) self.presentTextInputControllerWithSuggestions(texts, allowedInputMode: WKTextInputMode.Plain, completion: { selections in var index = find(texts, selections[0] as String)! var frequency = self.options[index] self.timer?.invalidate() self.remainingTime = frequency.time * 60 self.timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("tick"), userInfo: nil, repeats: true) }) } -
如您所见,我们需要一个名为
tick的方法,它将每秒被调用一次。按照这种方式编写代码:func tick(){ var rt = remainingTime! let formatter = NSDateComponentsFormatter() formatter.unitsStyle = .Short let components = NSDateComponents() components.second = rt % 60 rt = rt / 60 components.minute = rt % 60 rt = rt / 60 components.hour = rt % 24 rt = rt / 24 components.day = rt if components.hour > 6 { label.setText("Still have time") }else if components.hour == 0 && components.minute == 0{ label.setText("A few secs: \(components.second)") }else { label.setText( formatter.stringFromDateComponents(components)) } remainingTime!-- if(remainingTime == 0){ presentControllerWithName("its_time", context: self.context) timer?.invalidate() } } -
应用程序已完成,让我们通过将当前方案更改为
第十一章红丸 WatchKit 应用程序并按播放来测试它。您应该看到一个像以下这样的对话框:![如何操作…]()
注意
选择 2 分钟,这只是为了测试,等待直到您收到警报。
它是如何工作的...
如您所见,组件类不同;我们有的不是UIViewController,而是WKInterfaceController,我们有的不是UILabel,而是WKInterfaceLabel。一些方法也不同,例如,界面控制器在awakeWithContext方法上初始化其属性,而不是在viewDidLoad方法上。
还有更多...
有一个名为WKInterfaceTimer的组件,它的工作方式就像我们使用NSTimer一样。在这个菜谱中,我们使用了WKInterfaceLabel与NSTimer,因为它更灵活,您可以自定义组件上的文本。
注意
WatchKit 具有更多功能,如通知和快速查看。请尝试查看官方文档:developer.apple.com/library/ios/documentation/General/Conceptual/WatchKitProgrammingGuide/index.html#//apple_ref/doc/uid/TP40014969
在下一个菜谱中,我们将回到 iOS,学习如何使用相机拍照。
为您的照片添加效果
令人难以置信的是,手机已经取代了传统的照相机。我记得我们过去只会在特殊事件时携带相机,而现在我们的相机无处不在。我们可以这样说,手机已经更进一步:您可以在手机上拍照,编辑它,并与您的朋友和家人分享。
在这个菜谱中,我们将学习如何以非常简单的方式使用手机拍照并编辑它。
准备工作
由于我们将使用设备相机进行此菜谱,因此您需要一个物理设备来测试此应用程序。如果您使用的是图库中的照片,则可以更改它,但您还需要将一些图片上传到模拟器中。
创建一个名为第十一章照片效果的项目,然后继续前进。
如何操作…
-
打开你的项目,点击目标配置的 通用设置 并添加一个名为 CoreImage 的框架。之后进入故事板,在其下添加一个图像视图和四个按钮。将按钮标签更改为
Take photo、Sepia、Blur和Dots。 -
将图片与视图控制器作为属性连接并命名为
imageView。现在为每个按钮创建一个动作,分别命名为takePhoto、sepia、blur和dots。现在不必担心它们的实现内容,我们稍后会填充它们:@IBOutlet var imageView: UIImageView! @IBAction func takePhoto(sender: UIButton) { } @IBAction func sepia(sender: AnyObject) { } @IBAction func blur(sender: AnyObject) { } @IBAction func dots(sender: AnyObject) { } -
点击视图控制器源代码,让我们通过添加一个名为
image的可选类型UIImage的新属性来开始完善它:var image:UIImage? -
UIImagePickerController需要一个代理,并且只接受也是导航控制器代理的对象,因此将这些协议附加到视图控制器定义中:class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { -
几乎是时候开始编写函数了,但我们仍然需要添加一个细节:我们必须在文件顶部导入核心图像:
import CoreImage -
好的,到目前为止,我们可以开始编写视图控制器方法。首先在
viewDidLoad方法中检查你的设备是否有相机:override func viewDidLoad() { super.viewDidLoad() if !UIImagePickerController.isSourceTypeAvailable(.Camera){ UIAlertView(title: "Error", message: "There is no camera", delegate: nil , cancelButtonTitle: "OK").show() } } -
下一步是完善
takePhoto方法。此方法初始化图片选择器并调用相机视图:@IBAction func takePhoto(sender: UIButton) { let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.allowsEditing = true imagePicker.sourceType = .Camera self.presentViewController(imagePicker, animated: true, completion: nil) } -
如你所想,代理至少需要有一个方法;在这种情况下,我们需要一个方法来接收用户拍摄的图片,另一个方法是在取消的情况下:
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]){ image = info[UIImagePickerControllerEditedImage] as? UIImage self.imageView.image = image; picker.dismissViewControllerAnimated(true, completion: nil) } func imagePickerControllerDidCancel(picker: UIImagePickerController){ picker.dismissViewControllerAnimated(true, completion: nil) } -
一旦我们完成这些,我们只需要完善效果代码;它们非常相似但并不相同,所以这里它们是:
@IBAction func sepia(sender: AnyObject) { if image != nil { var ciImage = CIImage(image: image) let filter = CIFilter(name: "CISepiaTone") filter.setValue(ciImage, forKey: kCIInputImageKey) filter.setValue(0.8, forKey: "inputIntensity") ciImage = filter.outputImage self.imageView.image = UIImage(CIImage: ciImage) } } @IBAction func dots(sender: AnyObject) { if image != nil { var ciImage = CIImage(image: image) let filter = CIFilter(name: "CIDotScreen") filter.setValue(ciImage, forKey: kCIInputImageKey) ciImage = filter.outputImage self.imageView.image = UIImage(CIImage: ciImage) } } @IBAction func blur(sender: AnyObject) { if image != nil { var ciImage = CIImage(image: image) let filter = CIFilter(name: "CIGaussianBlur") filter.setValue(ciImage, forKey: kCIInputImageKey) ciImage = filter.outputImage self.imageView.image = UIImage(CIImage: ciImage) } } -
应用程序已完成。现在按播放,拍照,并选择你最喜欢的效果。
它是如何工作的…
UIImagePickerController 是为了方便使用相机而创建的;这样我们就不必使用复杂的相机设置,也不必担心它的不同状态。要使用 UIImagePickerController,你需要一个代理,在其方法中你可以检索用户拍摄的图片作为 UIImage。
在接收到图片后,你可以使用 Core Image 添加一些效果;但是你需要将 UIImage 转换为 CIImage。如果你从本地文件加载了 UIImage,你可以通过调用一个属性 CIImage 来轻松转换;然而,这种情况并不适用,因为这张图片是从相机加载的,所以你需要创建一个新的对象并将你的 UIImage 作为参数传递。
现在你可以使用相应的值使用你想要的过滤器。当你使用 CIFilter 时,你必须检查它接受的属性;有时你可以使用默认值,有时你可能想更改它们。
在使用过滤器后,你可以使用 outputImage 属性检索你的图片,然后你可以使用生成的 CIImage 构造一个新的 UIImage。
参见
-
CoreImage有很多过滤器,所以现在没有必要寻找修改我们图片的库或算法。检查developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP40004346上可用的过滤器,并用不同的属性测试它们。 -
在下一个菜谱中,我们将学习如何将信息从 iPhone 发送到我们的 Mac。
成为电影评论家
有时候,应用程序需要从您的设备传输信息到电脑,反之亦然;例如,您可能在回家的路上在 iPad 上观看电影(假设您不是司机),然后您将在电脑上继续观看。
对于这种类型的场景,苹果创造了一种名为Handoff的新技术。想法很简单:在另一台设备上继续您正在进行的任务。
在这个菜谱中,我们将创建一个应用程序,用户可以在一台设备上开始撰写他对电影的看法,并在 Mac 应用程序中查看。这个菜谱将被分成更小的部分,以便更容易消费。
准备中
Handoff 框架有软件和硬件要求。软件要求是 Xcode 6、iOS 8 和 OS X Yosemite(10.10);因此请确保您的硬件能够使用所有这些软件版本(或更高版本)。不幸的是,Handoff 不能与模拟器一起使用。另一个软件要求是,两台设备(电脑和您的苹果移动设备)必须登录到同一个 iCloud 账户,并且它们必须配对。
硬件要求是它具有蓝牙 LE 4.0。检查您的 iPhone 或 iPad 是否可以使用此功能的简单方法是打开设置,进入通用,检查是否有名为Handoff & Suggested Apps的选项:

确保 Handoff 选项已开启,如下面的截图所示:

一旦您检查了设备要求,就是时候检查电脑是否符合要求了。在您的 Mac 电脑上,打开系统偏好设置,然后打开通用选项,确认 Handoff 选项已勾选:

注意
在所有要求中,最复杂的是这项技术需要由团队(或开发者)签名的应用程序,这意味着需要在每个平台上的苹果开发计划中注册。这意味着如果您打算在移动设备和 mac 电脑之间使用这项技术,您需要两个订阅。
在这个菜谱中,我们将使用这两个平台,但如果您只有 iOS 订阅,将 Mac 应用更改为 iOS 应用非常简单。
现在我们可以开始编码项目;在这种情况下,开始创建一个名为 Chapter 11 Films 的工作空间。
如何做这件事...
我们将食谱分成三个小食谱,以便更好地理解。
创建工作空间
-
在名为
Common Code的组中创建工作空间;在这里,您需要添加一个名为FilmData.swift的新 Swift 文件,我们将添加以下简单的类:class FilmData { var name:String var year:Int? var director:String? var score:Int? var opinion = "" init(name:String){ self.name = name } } -
让我们创建一个名为
Chapter 11 Films iOS的新 iOS 项目。确保它将被添加到您的组合框中的工作空间:![创建工作空间]()
-
使用名为
Chapter 11 Films MacOSX的 Mac OS X Cocoa 应用程序重复此过程,但请注意,您必须将其添加到工作空间中,并且组也必须在工作空间中:![创建工作空间]()
-
一旦我们有了这两个项目,让我们将我们在本食谱开头创建的第一个文件(
FilmData.swift)添加到它们中。这样我们就不必为每个项目重复代码:![创建工作空间]()
-
一旦我们有了这些通用部分,我们将继续进行 Mac 应用程序的开发。因此,点击 Mac 项目,转到 General Settings,将签名部分更改为 Developer ID,并选择您的团队账户:
![创建工作空间]()
-
现在点击位于 Supporting Files 组中的
info.plist文件,添加一个名为 NSUserActivityTypes 的新键。将其类型更改为数组,尝试展开它,但如您所见,没有项目,因此点击加号并写入值com.packtpub.editingfilm:![创建工作空间]()
-
下一步是点击 XIB 文件,并将五个标签添加到我们唯一的窗口中。将它们一个接一个地放置,然后从第一个开始连接到
AppDelegate.swift:@IBOutlet var titleLabel: NSTextField! @IBOutlet var directorLabel: NSTextField! @IBOutlet var yearLabel: NSTextField! @IBOutlet var scoreLabel: NSTextField! @IBOutlet var opinionLabel: NSTextField! -
之后,我们只需将以下代码添加到
AppDelegate中:func application(application: NSApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { return userActivityType == "com.packtpub.editingfilm" } func application(application: NSApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]!) -> Void) -> Bool { func setField (fieldName:String, uiField:NSTextField) { if let value = userActivity.userInfo![fieldName] as? String { uiField.stringValue = value }else if let value = userActivity.userInfo![fieldName] as? Int { uiField.stringValue = String(value) } else{ uiField.stringValue = "-" } } setField("title", titleLabel) setField("director", directorLabel) setField("year", yearLabel) setField("score", scoreLabel) setField("opinion", opinionLabel) return true } -
好的,Mac 应用程序已完成;在我们按播放之前,请记住您必须登录到 iCloud 才能这样做,因此您必须打开 系统偏好设置,然后转到 iCloud 并登录。
注意
记住,这个账户必须与您将在移动设备上使用的账户相同。完成后,返回您的应用程序并按播放。您应该看到一个带有一些标签的窗口——现在不用担心它们,因为我们稍后会检查它们。
开发应用程序的 iOS 部分
-
在这个时候,我们已经准备好开发应用程序的 iOS 部分。首先,您必须在主项目目标上设置您的团队,并在
info.plist上添加键NSUserActivityTypes。就像我们在 Mac 应用程序中所做的那样,将其类型更改为 Array,并添加值com.packtpub.editingfilm,就像我们在 Mac 应用程序中所做的那样。 -
点击故事板,就像往常一样,你可能只会看到一个视图控制器。点击它,进入编辑器菜单,向下移动到选项
嵌入并选择导航控制器。正如你所预期的那样,我们稍后会添加第二个视图控制器。现在只需通过点击导航控制器,选择属性检查器,取消选择选项显示导航栏:![开发应用的 iOS 部分]()
-
现在返回到原始视图控制器,并在其中简单地添加一个表格视图。在这种情况下,我们需要显示一些带有内容的单元格,并且当它们被选中时还需要做一些事情。这意味着我们需要将视图控制器设置为
UITableViewDelegate和UITableViewDatasource:class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { -
之后,将表格视图与视图控制器作为数据源和代理绑定。作为数据源,我们需要一个
FilmData数组的数组,它将是一个使用私有函数初始化的属性:let movies = createBasicMovieArray() -
当然,我们在这里会收到一个错误,因为我们需要实现这个函数,所以请在类外部实现它:
private func createBasicMovieArray() -> [FilmData] { var movieArray = [FilmData]() var filmData = FilmData(name: "A Clockwork Orange") filmData.year = 1971 filmData.director = "Stanley Kubrick" movieArray.append(filmData) filmData = FilmData(name: "Monty Python and the Holy Grail") filmData.year = 1975 filmData.director = "Terry Gilliam" movieArray.append(filmData) filmData = FilmData(name: "Kill Bill") filmData.year = 2003 filmData.director = "Quentin Tarantino" movieArray.append(filmData) filmData = FilmData(name: "Ghost Busters") filmData.year = 1984 movieArray.append(filmData) return movieArray } -
之后,我们需要在视图控制器类中完成数据源方法:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return movies.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{ var cell = tableView.dequeueReusableCellWithIdentifier("filmcell") as? UITableViewCell if (cell == nil) { cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "filmcell") } let currentFilm = movies[indexPath.row] cell!.textLabel?.text = currentFilm.name let unknown = "????" cell!.detailTextLabel?.text = "\(currentFilm.year != nil ? String(currentFilm.year!) : unknown) - \(currentFilm.director != nil ? currentFilm.director! : unknown)" return cell! } -
即使如此,应用还没有完成。你应该按播放并测试它,你会看到一个类似于以下视图。别忘了确保你已经选择了正确的模式;否则,它将重新启动应用:
![开发应用的 iOS 部分]()
-
一旦这个阶段完成,我们就可以回到我们的视图控制器并实现最后一个方法。当你得到一些编译器错误时,不要担心,它们很快就会被修复:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { var filmDetailViewController = self.storyboard?.instantiateViewControllerWithIdentifier("film_detail") as FilmDetailViewController filmDetailViewController.film = movies[indexPath.row] self.navigationController?.pushViewController(filmDetailViewController, animated: true) } -
这个视图控制器已经完成,下一步是创建一个新的继承自
UIViewController的 Cocoa Touch 类,称为FilmDetailViewController。取消选择XIB选项:![开发应用的 iOS 部分]()
-
一旦你有了新的 Swift 文件,你就可以回到故事板,添加一个新的视图控制器。在这个故事板中添加五个标签、一个步进器、一个文本视图和一个按钮。将文本视图的背景改为灰色。你应该有一个类似于以下布局:
![开发应用的 iOS 部分]()
-
新的视图控制器需要知道它的类不是默认视图控制器,因此选择视图控制器,进入其身份检查器,将其类更改为FilmDetailViewController。利用这一点,我们还应该将Storyboard ID设置为
film_detail:![开发应用的 iOS 部分]()
-
现在将标签、步进器和文本视图与视图控制器链接起来:
@IBOutlet var movieTitle: UILabel! @IBOutlet var director: UILabel! @IBOutlet var year: UILabel! @IBOutlet var score: UILabel! @IBOutlet var opinion: UITextView! @IBOutlet var stepper: UIStepper! -
一旦我们有了这些属性,我们就可以设置动作。唯一需要协议的是文本视图;然后开始向类头中添加
UITextViewDelegate:class FilmDetailViewController: UIViewController, UITextViewDelegate { -
之后,我们可以将视图控制器作为文本视图代理连接起来,为按钮创建一个名为 done 的动作,并为步进器值变化事件创建一个名为
changeScore的动作:@IBAction func done(sender: AnyObject) { } @IBAction func changeScore(sender: UIStepper) { }
编写类代码
-
太好了,现在我们可以编写之前的类而不用担心故事板了。让我们从属性
film开始,它将包含正在屏幕上的电影信息:var film:FilmData? -
然后,我们可以初始化屏幕上的视图以及一个继承属性
userActivity。记住,在这里我们假设属性film已经设置:override func viewDidLoad() { super.viewDidLoad() movieTitle.text = film?.name director.text = film?.director year.text = film?.year != nil ? "\(film!.year!)" : "???" if let score = film?.score { self.score.text = String(score) self.stepper.value = Double(score) }else { self.score.text = "" self.stepper.value = 1 } self.opinion.text = film?.opinion self.userActivity = NSUserActivity(activityType: "com.packtpub.editingfilm") self.userActivity!.userInfo = [NSObject: AnyObject]() } -
然后,我们可以完成视图事件;这里我们将添加一个新的事件,称为
textViewDidChange,它属于UITextViewDelegate:@IBAction func done(sender: AnyObject) { self.userActivity!.invalidate() self.navigationController?.popViewControllerAnimated(true) } @IBAction func changeScore(sender: UIStepper) { self.film?.score = Int(sender.value) self.score.text = String(self.film!.score!) self.updateUserActivityState(self.userActivity!) } func textViewDidChange(textView: UITextView) { film?.opinion = self.opinion.text self.updateUserActivityState(self.userActivity!) } -
代码的最后一部分是获取我们想要传输的所有信息并更新
userActivity状态:override func updateUserActivityState(activity: NSUserActivity) { self.userActivity!.userInfo!["title"] = film?.name self.userActivity!.userInfo!["year"] = film?.year self.userActivity!.userInfo!["director"] = film?.director self.userActivity!.userInfo!["score"] = film?.score self.userActivity!.userInfo!["opinion"] = film?.opinion super.updateUserActivityState(activity) }
测试应用程序
-
应用程序已经完成,正如你所知,我们必须对其进行测试。在按下播放按钮之前,确保已选择正确的模式,并且应用程序将被安装在你的移动设备上,而不是在模拟器上。
-
按下播放按钮,选择你最喜欢(或最讨厌)的电影,为它设置评分,并写下你的评论。返回到你的 Mac(记住你没有停止 Mac 应用程序)并检查你的 dock 是否有一个新的图标。这意味着它检测到了可以读取的用户活动。点击此图标:
![测试应用程序]()
-
你会看到你的 Mac 应用程序在前台,并且它会显示从你的设备接收到的信息:
![测试应用程序]()
它是如何工作的…
Handoff 与一个名为 Activity 的功能一起工作。活动是关于用户当前正在做什么的一些信息,例如撰写电子邮件、编辑视频等等。
当使用 Handoff 时,你必须为你的活动规划三个阶段:创建活动、更新活动和销毁活动。我们在 viewDidLoad 方法中创建了它,每次用户更改分数或意见文本时都更新它,当用户按下完成按钮时销毁它。
如果你想在你的类中使用 Handoff,你必须添加一个 NSUserActivity 对象。这个类的一个好特点是它在 AppKit(OS X)和 UIKit(iOS)中都可以使用。一些类已经将这个对象作为属性,例如 NSDocument、UIDocuments、NSResponder 和 UIResponder。
由于 UIViewController 继承自 UIResponder,我们可以使用现有的属性 userActivity。每次我们认为活动需要更新时,都会调用 updateUserActivityState 方法。在这里,它应该设置应该传输的所有信息,即使信息没有变化,例如电影标题、导演或制作年份,因为更新状态后,userInfo 字典将会清空。
小贴士
不要过度加载用户信息字典;苹果建议存储最多 3k 的信息,超过这个量可能会影响应用程序性能。
接收所需信息需要两个步骤:第一步是通过在应用代理上实现方法 willContinueUserActivityWithType 来检查应用是否接受该时刻的活动;第二步是实现应用代理方法 continueUserActivity 以获取信息并将其发送到相应的视图或对象。
一个重要的细节是,Handoff 是用于同一公司或开发者之间的应用之间,并且仅与同一用户;这就是为什么你必须使用相同的团队签名,并且用户必须登录。
还有更多…
在这种情况下,我们创建了一个编辑你的电影观点的应用,另一个可以接收它的应用,然而你可以修改 Mac 应用程序,使其也可以编辑并将其重新传输到设备应用。试着作为家庭作业来做。
参考信息
- 苹果有一个关于如何使用 Handoff 与照片的示例;查看
developer.apple.com/library/ios/samplecode/PhotoHandoff/Listings/README_md.html下载它。
留下痕迹
你是否曾经去过某个地方,并开始怀疑你的路线是否最佳?有时,当我们到达目的地后,我们想要回顾我们的旅程。通常,当旅程非常漫长时,我们会这样做。在这个菜谱中,我们将创建一个用于记录我们步伐的应用,然后我们可以检查我们所走的路径。
准备中
创建一个名为 Chapter 11 Breadcrumbs 的新单视图应用,添加 Core Location 框架和 Map Kit 框架。你可以使用模拟器或物理设备来测试这个应用,然而如果你和我一样懒惰,使用模拟器会更好,这样你就不需要站起来走动来测试它:

如何做…
一旦添加了框架,你只需遵循以下步骤来创建应用:
-
打开故事板,在顶部添加一个标签,在其下方添加一个按钮,再在其下方添加一个地图视图。结果可以类似于以下截图:
![如何做…]()
-
如同往常,开始将标签和地图视图连接到视图控制器:
@IBOutlet var mapView: MKMapView! @IBOutlet var positionLabel: UILabel! -
现在你可以点击视图控制器,并首先导入核心定位和地图框架,当然不要移除 UIKit:
import UIKit import MapKit import CoreLocation -
之后,通过添加
CLLocationManagerDelegate和MKMapViewDelegate协议来完成ViewController类,如下所示:class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate { -
下一步是添加视图控制器属性。在这种情况下,我们需要位置管理器来接收当前位置、我们经过的位置数组以及一个布尔属性来在地图上跟随用户:
var manager = CLLocationManager() var locationsStack = [CLLocation]() var follow = true -
现在我们需要初始化管理属性和地图视图来完成它,所以我们将使用
viewDidLoad方法:override func viewDidLoad() { super.viewDidLoad() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest manager.requestAlwaysAuthorization() manager.startUpdatingLocation() mapView.delegate = self mapView.mapType = .Standard mapView.showsUserLocation = true } -
然后,我们需要在每次接收到新的位置时更新地图视图和
locationStack:func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject]) { var currentLocation = locations[0] as CLLocation positionLabel.text = "\(currentLocation.coordinate.latitude), \(currentLocation.coordinate.longitude)" locationsStack.append(currentLocation) if follow { var currentRegion = MKCoordinateRegion(center: mapView.userLocation.coordinate, span: MKCoordinateSpanMake(0.01, 0.01)) mapView.setRegion(currentRegion, animated: true) } if locationsStack.count > 1{ var destination = locationsStack.count - 2 let sourceCoord = locationsStack.last!.coordinate let destinationCoord = locationsStack[destination].coordinate var coords = [sourceCoord, destinationCoord] let polyline = MKPolyline(coordinates: &coords, count: coords.count) mapView.addOverlay(polyline) } } -
应用程序仍然没有显示路径;原因是我们需要通过编写地图视图方法
renderForOverlay来绘制它:func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! { if overlay is MKPolyline { var polylineRenderer = MKPolylineRenderer(overlay: overlay) polylineRenderer.strokeColor = UIColor.blueColor() polylineRenderer.lineWidth = 3 return polylineRenderer } return nil } -
现在应用程序正在运行;然而,检查我们的旅程可能相当困难,因为它总是在更新,所以是时候添加按钮事件了:
@IBAction func followAction(sender: UIButton) { follow = !follow if follow { sender.setTitle("Stop following", forState: .Normal) }else { sender.setTitle("Resume following", forState: .Normal) } } -
应用程序几乎完成了。还有一项细节需要你设置:iOS 9 上的权限。所以请前往你的
info.plist文件,然后添加一个新的记录,键为Required background modes,在Item 0中写入字符串值App registers for location updates:![如何操作…]()
-
现在应用程序已经完成,如果你使用的是物理设备,请按播放并四处走动;如果你使用的是模拟器,请点击调试,然后向下滚动到位置并选择高速公路驾驶。
它是如何工作的…
核心定位框架允许我们检索当前设备位置,但当然它需要一个代理;这就是我们为什么必须实现CLLocationManagerDelegate协议的原因。该协议通过didUpdateLocations方法接收位置。
一旦我们收到它,我们就可以将位置存储到locationStack数组中。实际上,如果你不想保留整个旅程的信息,你只需存储最后一个位置即可。
存储了新的位置后,我们可以创建一条折线,这就像是我们旅程的一部分。这些信息被提交到地图视图。
地图视图需要使用MKMapViewDelegate的rendererForOverlay方法来渲染它。这样做的原因是你可以自由地在地图上绘制你想要的任何内容,并且你可以创建圆形、正方形等形状来突出显示一个区域。
还有更多…
在地图视图中绘制路线是非常常见的,尤其是如果你想要使用方向。看看 MKDirections,它可能非常有用。
创建货币转换器应用程序
现在,我们的应用程序必须准备好在各个地方执行;因此,你的应用程序应该尽可能包含多种语言。考虑到国际化对于使用不同的语言或不同的数字格式非常重要。
在这个菜谱中,我们将创建一个应用程序,它将显示货币汇率,但更重要的是,它将适应当前位置。
准备工作
创建一个名为Chapter 11 Currency Converter的单视图应用程序,并将两张旗帜图片放在images.xcassets中。这些图片可以从书籍资源中下载。
如何操作…
按照以下步骤创建货币转换器应用程序:
-
首先,点击支持文件组并添加一个新文件。在这种情况下,前往资源部分并选择字符串文件:
![如何操作…]()
-
在这个文件中,添加以下键及其对应值:
"Rate" = "Rate: %@"; "Total" = "Total: %@"; "Choose Currency" = "Choose a currency"; "flagicon" = "english"; "Cancel" = "Cancel"; -
现在,前往故事板并添加四个标签、两个按钮、一个文本框和一个图像视图在底部,类似于以下截图:
![如何操作…]()
-
一旦完成布局,请使用以下名称将 UI 组件(除了标题标签)与视图控制器连接:
@IBOutlet var fromButton: UIButton! @IBOutlet var toButton: UIButton! @IBOutlet var amountTextField: UITextField! @IBOutlet var dateLabel: UILabel! @IBOutlet var rateLabel: UILabel! @IBOutlet var totalLabel: UILabel! @IBOutlet var flagImage: UIImageView! -
在属性检查器中将文本字段的键盘类型更改为数字和标点符号:
![如何操作…]()
-
将协议
UITextFieldDelegate添加到视图控制器:class ViewController: UIViewController, UITextFieldDelegate { -
现在,将视图控制器设置为文本字段代理,并将
Amount of money作为文本字段占位符写入。 -
另一个重要操作是将视图类从
UIView更改为UIControl,这样我们就可以轻松地隐藏键盘:![如何操作…]()
-
前往视图控制器并添加以下属性:
let currencies = ["AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HRK", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", "RUB", "SEK", "SGD", "THB", "TRY", "USD", "ZAR"] let baseurl = "http://api.fixer.io/latest" var fromCurrency = "USD" var toCurrency = "EUR" -
到目前为止,我们可以开始编写视图控制器方法;从开始我们将实现
viewDidLoad方法:override func viewDidLoad() { super.viewDidLoad() flagImage.image = UIImage(named: NSLocalizedString("flagicon", comment: "")) setup() } -
如您所见,有一个名为
setup的私有方法,我们现在将实现它。此方法负责从互联网检索货币汇率并计算用户输入的金额的价值:private func setup(){ fromButton.setTitle(fromCurrency, forState: .Normal) toButton.setTitle(toCurrency, forState: .Normal) var session = NSURLSession.sharedSession() var url = NSURL(string: "\(baseurl)?base=\(fromCurrency)&symbols=\(toCurrency)")! session.dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in var json = {} do { json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as [String:AnyObject] } catch { let alert = UIAlertController(title: "Error", message: "Error parsing JSON", preferredStyle:.Alert) self.presentViewController(alert, animated: true, completion: nil) } let dateComponentsArray = (json["date"] as String).componentsSeparatedByString("-") let dateComponents = NSDateComponents() dateComponents.year = dateComponentsArray[0].toInt()! dateComponents.month = dateComponentsArray[1].toInt()! dateComponents.day = dateComponentsArray[2].toInt()! let rates = json["rates"] as [String:Double] let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents) let ratio = rates[self.toCurrency] let amount = self.amountTextField.text == "" ? 1.0 : (self.amountTextField.text as NSString).doubleValue NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in let dateFormatter = NSDateFormatter() dateFormatter.dateStyle = .LongStyle self.dateLabel.text = dateFormatter.stringFromDate(date!) let currencyFormatter = NSNumberFormatter() currencyFormatter.currencyCode = self.toCurrency currencyFormatter.numberStyle = .CurrencyStyle self.rateLabel.text = String(format: NSLocalizedString("Rate", comment: ""), arguments: [currencyFormatter.stringFromNumber(ratio!)!]) let total = amount * ratio! self.totalLabel.text = String(format: NSLocalizedString("Total", comment: ""), arguments: [currencyFormatter.stringFromNumber(total)!]) }) }).resume() } -
我们将实现按钮的事件。这两个按钮的事件是相同的,因此将触摸事件与以下方法连接:
@IBAction func chooseCurrency(sender: UIButton) { let alertController = UIAlertController(title: NSLocalizedString("Choose Currency", comment: ""), message: nil, preferredStyle: .ActionSheet) let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .Cancel) { (action) in } alertController.addAction(cancelAction) for currency in currencies { let currAction = UIAlertAction(title: currency, style: .Default) { (action) in if sender == self.fromButton { self.fromCurrency = action.title }else { self.toCurrency = action.title } self.setup() } alertController.addAction(currAction) } self.presentViewController(alertController, animated: true, completion: nil) } -
将主视图(我们将其更改为
UIControl)的触摸事件与视图控制器创建的名为touchup的方法连接:@IBAction func touchup(sender: UIControl) { self.amountTextField.resignFirstResponder() self.setup() } -
现在我们可以使用最后一个方法完成视图控制器,这个方法允许我们在按下回车键时隐藏键盘:
func textFieldShouldReturn(textField: UITextField) -> Bool { self.touchup(self.view as UIControl) return true } -
应用程序基本上已经完成,然而我们只能说它已准备好本地化,但是除了货币格式外,我们可以说没有其他可以证明这一点的东西。因此,点击项目导航器中的项目,转到项目的Info标签页。确保你选择了项目而不是目标。滚动到Locations部分并点击加号。选择Spanish,一种新语言:
![如何操作…]()
-
现在展开你的故事板并选择Main.strings (Spanish):
![如何操作…]()
-
将标题从
Currency Converter更改为Conversor de monedas,并将文本字段占位符从Amount of money更改为Cantidad de dinero。修改后的行应类似于以下行:/* Class = "UILabel"; text = "Currency Converter"; ObjectID = "hFW-19-dID"; */ "hFW-19-dID.text" = "Conversor de Monedas"; /* Class = "UITextField"; placeholder = "Amount of money"; ObjectID = "vzh-tY-rr3"; */ "vzh-tY-rr3.placeholder" = "Cantidad de dinero";小贴士
在将布局翻译成其他语言之前,尝试设计整个布局;有时向视图中添加组件会让你再次翻译一切。
-
返回
Localizable.strings,在文件检查器中点击位于Localization部分的Localize…按钮:![如何操作…]()
-
将此文件移动到
lproj文件夹的对话框将出现。选择基本语言:![如何操作…]()
-
如您所见,本地化部分已用一些语言选项替换了旧按钮;检查Spanish选项:
![如何操作…]()
-
展开
Localizable.strings并点击西班牙语选项:![如何操作…]()
-
现在更新值,将它们翻译成西班牙语,如下所示:
"Rate" = "Tasa de conversión: %@"; "Total" = "Total: %@"; "Choose Currency" = "Elija la moneda"; "flagicon" = "spanish"; "Cancel" = "Cancelar"; -
是时候测试我们的应用程序了,按播放并检查应用程序是否运行得很好,然后按主页按钮,进入设置,进入通用部分,点击(或单击)语言与地区,将地区更改为西班牙,语言更改为西班牙语。现在返回到你的应用程序,你应该看到它使用西班牙文文本;数字应以西班牙格式表示:
![如何做到这一点…]()
它是如何工作的…
当你想将你的应用程序翻译成其他语言时,你首先必须使用基础语言(默认语言)创建它,但请记住,每个文本都可以被翻译;因此,而不是使用硬编码的文本,你必须从Localizable.strings文件中检索它。
使用NSLocalizedString从本地化文件中检索字符串。你还可以获取格式字符串并在字符串格式初始化器中使用它们。
你也可以使用NSDateFormatter和NSNumberFormatter来使用日期和数字格式化;这样你就不必担心本地日期和数字了。
也可以翻译你的故事板,因此不需要在视图加载时设置标签和占位符。
更多…
你还可以翻译其他文件,例如启动屏幕和Info.plist。例如,你可以使用Bundle display name键(CFBundleDisplayName)根据语言更改应用程序名称。
NSLocalizedString有其他选项,允许你在复杂的应用程序中使用翻译。
Swift 中的方法交换
方法交换是支持动态方法调度的编程语言中的一种常见做法。这在 Objective-C 中也非常常见。通过方法交换,你可以在运行时交换一个方法实现为另一个实现。建议谨慎使用方法交换,并且仅在没有替代方案(可能是协议或扩展)时使用。
准备工作
创建一个名为Swizzling的游乐场。我们在这个食谱中不会使用项目,所以不用担心项目设置。
如何做到这一点…
-
我们将以
UIViewController为例。将以下代码添加到你的游乐场中:extension UIViewController { public override static func initialize() { struct Static { static var token: dispatch_once_t = 0 } dispatch_once(&Static.token) { let originalSelector = Selector("viewWillAppear:") let swizzledSelector = Selector("ViewWillDefinitelyAppear:") let originalMethod = class_getInstanceMethod(self, originalSelector) let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) if didAddMethod { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } } } -
我们还想要确保当我们期望父类时,我们不会从子类中进行交换。在初始化静态结构后添加以下代码:
// Verify this is not a subclass if self !== UIViewController.self { return }
它是如何工作的…
对于这个例子,我们希望为每个UIViewController执行额外的操作;然而,我们需要保留viewWillAppear的原始功能。这只能通过方法交换来完成。尽管 Swift 在方法调度方面采取了更静态的方法,但你仍然可以在运行时交换方法。
我们将每个调用包裹在 dispatch_once 块中,以确保这仅在运行时发生一次。我们定义每个选择器,包括现有的和新的一。一旦我们调用 class_addMethod,我们检查它是否成功,如果是,则交换实现(在运行时发生)。
如果你熟悉 Objective-C,你会注意到通常方法交换发生在加载方法中,这是在类定义加载时保证会被调用的。考虑到这是一个 Objective-C 方法,我们只在初始化方法中调用交换代码,这是在调用任何类方法之前发生的。
还有更多...
除了交换系统方法之外,你还可以交换自定义 Swift 类中创建的方法。然而,有一些额外的考虑。类必须扩展 NSObject,并且期望的方法定义中也必须包含动态属性。使用 @objc 会使你的代码通过 Objective-C 运行时运行(从而支持动态方法分发);然而,它并不保证属性或方法的动态分发。
Swift 中的关联对象
除了方法交换之外,我们还可以利用另一种称为关联对象的运行时过程。这与 Swift 中的扩展类似;然而,扩展不允许你向现有类添加新属性。让我们给所有 UIViewControllers 添加一个描述性名称属性。
准备工作
创建一个名为 Associated Objects 的游乐场。我们在这个食谱中不会使用项目,所以不用担心项目设置。如果你使用了之前的食谱,你可以继续使用相同的游乐场文件。
如何实现...
-
将以下代码添加到您的游乐场文件中:
extension UIViewController { private struct AssociatedKeys { static var DescriptiveName = "nsh_DescriptiveName" } var descriptiveName: String? { get { return objc_getAssociatedObject(self, &AssociatedKeys.DescriptiveName) as? String } set { if let newValue = newValue { objc_setAssociatedObject( self, &AssociatedKeys.DescriptiveName, newValue as NSString?, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } } } }
它是如何工作的...
首先,我们创建一个私有的静态结构体来存储引用我们的对象的键。在这种情况下,它将只包含 DescriptiveName 对象。现在我们为 UIViewController 定义一个新的变量,并在获取和设置方法中执行所需的方法。
我们使用 objc_getAssociatedObject() 来返回正确的对象,并将其转换为 String 对象。对于设置,我们调用 objc_setAssociatedObject() 方法。在这里,我们传递 self 以让运行时知道我们正在向 UIViewController 添加内容,然后引用我们静态结构体中的对象,我们想要将其与类关联。最后,我们设置属性以保留并设置为非原子。
结果是每个 UIViewController 都将包含一个新的属性,DescriptiveName,可以在代码的任何地方访问。
































































































































































































浙公网安备 33010602011771号