Swift-精要第二版-全-

Swift 精要第二版(全)

原文:zh.annas-archive.org/md5/0a9cbaf528e0988ad3fcf081b72a4f54

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Swift 基础知识提供了 Swift 语言及其编写 iOS 应用程序所需的工具的概述。从使用 Swift 开源版本的简单命令行命令,到在 OS X 上使用 Xcode Playground 编辑器交互式测试图形内容,Swift 语言和语法通过示例进行介绍。

本书还通过展示如何创建一个简单的 iOS 应用程序,然后介绍如何使用故事板和自定义视图来构建更复杂的网络应用程序,介绍了在 OS X 上使用 Xcode 进行端到端 iOS 应用程序开发的流程。

本书通过提供一个从头开始构建的示例,构建了一个 iOS GitHub 仓库浏览器,以及一个 Apple Watch 应用程序,作为结尾。

本书涵盖的内容

第一章,探索 Swift,展示了带有 Swift Read-Evaluate-Print-Loop (REPL)的开源 Swift 版本,并通过标准数据类型、函数和循环的示例介绍了 Swift 语言。

第二章,玩转 Swift,展示了 Swift Xcode Playgrounds 作为交互式玩转 Swift 代码并查看图形结果的一种方式。它还介绍了 playground 格式,并展示了如何对 playground 进行文档化。

第三章,创建 iOS Swift 应用程序,展示了如何使用 Xcode 在 Swift 中创建和测试 iOS 应用程序,并概述了 Swift 类、协议和枚举。

第四章,使用 Swift 和 iOS 的 Storyboard 应用程序,介绍了 Storyboard 作为创建多屏幕 iOS 应用程序的手段,并展示了如何将 Interface Builder 中的视图连接到 Swift 的输出和动作。

第五章,在 Swift 中创建自定义视图,涵盖了使用自定义表格视图、布局嵌套视图以及绘制自定义图形和分层动画的 Swift 自定义视图。

第六章,解析网络数据,展示了 Swift 如何使用 HTTP 和自定义基于流的协议与网络服务进行通信。

第七章,构建仓库浏览器,使用本书中描述的技术构建了一个可以显示用户 GitHub 仓库信息的仓库浏览器。

第八章, 添加手表支持,介绍了 Apple Watch 的功能,并展示了如何为 iOS 应用构建扩展以在手表上直接提供数据。

附录, Swift 相关网站、博客和知名推特用户参考,提供了额外的参考和资源,以继续学习 Swift。

您需要为本书准备

本书中的练习是为 Swift 2.1 编写和测试的,它包含在 Xcode 7.2 中,并与 Swift 2.2 的开发版本进行了验证。要实验 Swift,您需要一个符合 swift.org/download/ 所示要求的 Mac OS X 或 Linux 计算机。

要运行第 2-8 章中涉及 Xcode 的练习,您需要一个运行 10.9 或更高版本且安装了 Xcode 7.2 或更高版本的 Mac OS X 计算机。如果发布了新的 Swift 版本,请检查本书的 GitHub 仓库或 Packtpub 的勘误页以获取可能影响本书内容的任何更改的详细信息。

注意

Swift 演示场(在第二章 [https://part0023_split_000.html#LTSU2-d7e55eb5242648e89c396442afe4f84b "第二章。与 Swift 玩耍"] 中描述),与 Swift 玩耍,仅作为 OS X 上 Xcode 的一部分提供,不是 Swift 开源版本的一部分。

此外,iOS 和 watchOS 开发(第 3-8 章)只能在 OS X 上使用 Xcode 进行;在其他平台上无法创建 iOS 或 watchOS 应用程序。iOS 开发所需的大部分库和模块在 Swift 的开源版本中不可用。

Xcode 可以通过 App Store 免费下载安装;在搜索框中搜索 Xcode。或者,您可以从 developer.apple.com/xcode/downloads/ 下载 Xcode,该链接来自 iOS 开发者中心 developer.apple.com/devcenter/ios/

Xcode 安装完成后,可以从 /Applications/Xcode.app 或 Finder 中启动。要运行基于命令行的练习,可以从 /Applications/Utilities/Terminal.app 启动终端,如果 Xcode 安装成功,可以通过运行 xcrun swift 启动 swift

iOS 应用可以在 iOS 模拟器中开发和测试,该模拟器包含在 Xcode 中。编写或测试代码时不需要 iOS 设备。如果您想在您的 iOS 设备上运行代码,则需要 Apple ID 进行登录,但应用程序将仅限于直接连接的设备。同样,手表应用程序也可以在本地模拟器或本地设备上进行测试。

将应用程序发布到 AppStore 需要您加入苹果开发者计划。更多信息请参阅developer.apple.com/programs/

本书面向的对象

本书旨在帮助对学习 Swift 编程语言感兴趣的开发者,无论是使用 Linux 上的 Swift 开源版本,还是使用 OS X 上 Xcode 打包的版本。然而,在第一章之后,即“探索 Swift”,剩余章节使用 Xcode 功能或包含仅能在 OS X 上使用 Xcode 的 iOS 示例。这些章节展示了如何使用 Swift 在 OS X 上编写 iOS 应用程序。虽然不假设有 iOS 编程经验,但预期读者具备动态或静态类型编程语言的基本编程经验。读者应熟悉导航和使用 Mac OS X,在需要终端命令的情况下,开发者应有简单 shell 命令的经验,或者可以从提供的示例中快速掌握。

熟悉 Objective-C 的开发者将了解许多提到的框架和库;然而,对 Objective-C 及其框架的现有知识既不是必需的,也不被假设。

源代码存储在 GitHub 仓库中,网址为github.com/alblue/com.packtpub.swift.essentials/,您可以使用仓库中的标签在章节内容之间切换。如果您想在不同版本之间导航,了解 Git 知识会有所帮助;或者,您也可以使用 GitHub 的基于网页的界面。强烈建议读者熟悉 Git,因为它是 Xcode 的标准版本控制系统,也是开源项目的既定标准。如果读者不熟悉且想了解更多,请访问作者的博客alblue.bandlem.com/Tag/git/

商标

GitHub 是 GitHub Inc. 的商标,本书中的示例未经 GitHub Inc. 批准、审查或认可。Mac 和 OS X 是 Apple Inc. 的商标,在美国和其他国家注册。iOS 是 Cisco 在美国和其他国家的商标或注册商标,并在此许可下使用。

术语

在本书中,您将找到许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“"hello".hasPrefix("he")方法在 OS X 和 iOS 上编译和运行成功。”

代码块设置如下:

> var shopping = [ "Milk", "Eggs", "Coffee", ]
shopping: [String] = 3 values {
  [0] = "Milk"
  [1] = "Eggs"
  [2] = "Coffee"
}

当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

func setupView() {
 contentMode = .Redraw
}

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“Xcode 文档可以通过导航到帮助 | 文档和 API 参考来搜索。”

注意事项

警告或重要注意事项以这样的框形式出现。

小贴士

技巧和窍门以这样的形式出现。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。

要发送给我们一般性的反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给你。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择你的书籍,点击错误提交表单链接,并输入你的错误清单详情来报告它们。一旦你的错误清单得到验证,你的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误部分下的现有错误清单中。

要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍的名称。所需信息将出现在错误清单部分下。

侵权

互联网上版权材料的侵权是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果你在互联网上遇到任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您的帮助,以保护我们的作者和为您提供有价值的内容的能力。

问题和建议

如果您对本书的任何方面有问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章:探索 Swift

苹果公司在 2014 年的 WWDC 上宣布 Swift 作为一种新的编程语言,它结合了 Objective-C 平台的经验以及过去几十年动态和静态类型语言的进步。在 Swift 之前,大多数为 iOS 和 OS X 应用程序编写的代码都是 Objective-C,它是 C 编程语言的面向对象扩展集。Swift 旨在建立在 Objective-C 的模式和框架之上,但具有更现代的运行时和自动内存管理。2015 年 12 月,苹果在 swift.org 上开源了 Swift,并为 Linux 以及 OS X 提供了二进制文件。本章中的内容可以在 Linux 或 OS X 上运行,但本书的其余部分要么是 Xcode 特定的,要么依赖于非开源的 iOS 框架。开发 iOS 应用程序需要 Xcode 和 OS X。

本章将介绍以下主题:

  • 如何使用 Swift REPL 来评估 Swift 代码

  • Swift 文字字面量的不同类型

  • 如何使用数组和字典

  • 函数和不同类型的函数参数

  • 从命令行编译和运行 Swift

开源 Swift

苹果公司在 2015 年 12 月将 Swift 作为开源项目发布,托管在 github.com/apple/swift/ 以及相关仓库中。有关 Swift 开源版本的信息可在 swift.org 网站上找到。从运行时角度来看,Swift 的开源版本在 Linux 和 OS X 上相似;然而,两个平台可用的库集合不同。

例如,Objective-C 运行时最初并未包含在 Swift for Linux 的初始版本中;因此,一些委托给 Objective-C 实现的方法不可用。"hello".hasPrefix("he") 在 OS X 和 iOS 上编译和运行成功,但在 Linux 的第一个 Swift 版本中是编译错误。除了缺少函数外,两个平台之间还有不同的模块(框架)。OS X 和 iOS 上的基本功能由 Darwin 模块提供,但在 Linux 上,基本功能由 Glibc 模块提供。Foundation 模块,它提供了许多在基础集合库之外的数据类型,在 OS X 和 iOS 上用 Objective-C 实现,但在 Linux 上是一个干净的 Swift 重实现。随着 Swift 在 Linux 上的发展,更多的这些功能将被填补,但如果有跨平台功能的需求,特别值得在 OS X 和 Linux 上进行测试。

最后,尽管 Swift 语言和核心库已经开源,但这并不适用于 iOS 库或其他 Xcode 中的功能。因此,无法从 Linux 编译 iOS 或 OS X 应用程序,并且构建 iOS 应用程序和编辑用户界面必须在 OS X 上的 Xcode 中完成。

开始使用 Swift

Swift 提供了一个运行时解释器,用于执行语句和表达式。Swift 是开源的,可以从 swift.org/download/ 下载针对 OS X 和 Linux 平台的预编译二进制文件。正在对其他平台和操作系统进行端口移植,但这些移植不受 Swift 开发团队的支持。

Swift 解释器的名称为 swift,在 OS X 上可以通过在 Terminal.app 终端中运行 xcrun 命令来启动:

$ xcrun swift
Welcome to Swift version 2.2!  Type :help for assistance.
>

xcrun 命令允许执行工具链命令;在这种情况下,它找到 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftswift 命令与其他编译工具(如 clangld)并列,允许在同一台机器上使用多个版本的命令和库而不会冲突。

在 Linux 上,只要 swift 二进制文件及其依赖库位于合适的位置,就可以执行 swift 二进制文件。

Swift 提示符显示 > 用于新语句,显示 . 用于续行。输入到解释器的语句和表达式将被评估并显示。匿名值被赋予引用,以便随后使用:

> "Hello World"
$R0: String = "Hello World"
> 3 + 4
$R1: Int = 7
> $R0
$R2: String = "Hello World"
> $R1
$R3: Int = 7

数值字面量

Swift 中的数值类型可以表示大小为 8、16、32 或 64 位的有符号和无符号整数值,以及有符号的 32 或 64 位浮点值。数字可以包含下划线以提高可读性;因此,68_040 与 68040 相同:

> 3.141
$R0: Double = 3.141
> 299_792_458
$R1: Int = 299792458
> -1
$R2: Int = -1
> 1_800_123456
$R3: Int = 1800123456

数字也可以用 二进制八进制十六进制 表示,使用前缀 0b0o(零和字母 "o")或 0x。请注意,Swift 不像 Java 和 JavaScript 那样继承 C 使用前导零 (0) 来表示八进制值。示例包括:

> 0b1010011
$R0: Int = 83
> 0o123
$R1: Int = 83
> 0123
$R2: Int = 123
> 0x7b
$R3: Int = 123

浮点字面量

Swift 中提供了三种使用 IEEE754 浮点标准的浮点类型。Double 类型表示 64 位数据,而 Float 存储了 32 位数据。此外,Float80 是一种特殊类型,存储了 80 位数据(Float32Float64 分别作为 FloatDouble 的别名,尽管在 Swift 程序中不常用)。

一些 CPU 在内部使用 80 位精度进行数学运算,而 Float80 类型允许在 Swift 中使用这种精度。并非所有架构都原生支持 Float80,因此应谨慎使用。

默认情况下,Swift 中的浮点值使用 Double 类型。由于浮点表示无法精确表示某些数字,一些值将显示为舍入误差;例如:

> 3.141
$R0: Double = 3.141
> Float(3.141)
$R1: Float = 3.1400003

浮点值可以用十进制或十六进制指定。十进制浮点使用 e 作为 10 的底数的指数,而十六进制浮点使用 p 作为 2 的底数的指数。一个 AeB 的值是 A*10^B,而一个 0xApB 的值是 A*2^B。例如:

> 299.792458e6
$R0: Double = 299792458
> 299.792_458_e6
$R1: Double = 299792458
> 0x1p8
$R2: Double = 256
> 0x1p10
$R3: Double = 1024
> 0x4p10
$R4: Double = 4096
> 1e-1
$R5: Double = 0.10000000000000001
> 1e-2
$R6: Double = 0.01> 0x1p-1
$R7: Double = 0.5
> 0x1p-2
$R8: Double = 0.25
> 0xAp-1
$R9: Double = 5

字符串字面量

字符串可以包含转义字符、Unicode 字符和插值表达式。转义字符以反斜杠 () 开头,可以是以下之一:

  • \\:这是一个文字斜杠 \

  • \0:这是一个空字符

  • \':这是一个文字单引号 '

  • \":这是一个文字双引号 "

  • \t:这是一个制表符

  • \n:这是一个换行符

  • \r:这是一个回车符

  • \u{NNN}:这是一个 Unicode 字符,例如欧元符号 \u{20AC} 或笑脸 \u{1F600}

一个 插值字符串 包含一个嵌入的表达式,该表达式将被评估,转换为 String 并插入到结果中:

> "3+4 is \(3+4)"
$R0: String = "3+4 is 7"
> 3+4
$R1: Int = 7
> "7 x 2 is \($R1 * 2)"
$R2: String = "7 x 2 is 14"

变量和常量

Swift 区分变量(可以修改)和常量(赋值后不能更改)。标识符以下划线或字母字符开头,后跟下划线或字母数字字符。此外,还可以使用其他 Unicode 字符点(如表情符号),尽管不允许使用方框线和箭头;有关允许的 Unicode 字符的完整集合,请参阅 Swift 语言指南。通常,不允许使用 Unicode 私用区域,并且标识符不能以下划线字符(如重音符号)开头。

变量使用 var 关键字定义,常量使用 let 关键字定义。如果未指定类型,则自动推断:

> let pi = 3.141
pi: Double = 3.141
> pi = 3

error: cannot assign to value: 'pi' is a 'let' constant
note: change 'let' to 'var' to make it mutable
> var i = 0
i: Int = 0
> ++i
$R0: Int = 1

可以显式指定类型。例如,要将 32 位浮点值存储,变量可以显式定义为 Float

> let e:Float = 2.718
e: Float = 2.71799994

类似地,要将值存储为无符号 8 位整数,显式声明类型为 UInt8

> let ff:UInt8 = 255
ff: UInt8 = 255

可以使用类型初始化器或分配给不同类型变量的文字将数字转换为不同类型,前提是它不会下溢或溢出:

> let ooff = UInt16(ff)
ooff: UInt16 = 255
> Int8(255)
error: integer overflows when converted from 'Int' to 'Int8'
Int8(255)
^
> UInt8(Int8(-1))
error: negative integer cannot be converted to unsigned type 'UInt8'
UInt8(Int8(-1))
^

集合类型

Swift 有三种集合类型:数组字典集合。它们是强类型和泛型的,这确保了分配给它们的类型值与元素类型兼容。使用 var 定义的集合是可变的;使用 let 定义的集合是不可变的。

数组的文字语法使用 [] 来存储逗号分隔的列表:

> var shopping = [ "Milk", "Eggs", "Coffee", ]
shopping: [String] = 3 values {
  [0] = "Milk"
  [1] = "Eggs"
  [2] = "Coffee"
}

文字字典使用逗号分隔的 [key:value] 格式定义条目:

> var costs = [ "Milk":1, "Eggs":2, "Coffee":3, ]
costs: [String : Int] = {
  [0] = { key = "Coffee" value = 3 }
  [1] = { key = "Milk"   value = 1 }
  [2] = { key = "Eggs"   value = 2 }
}

提示

为了可读性,数组和字典的文字可以有一个尾随逗号。这允许初始化跨越多行,并且如果最后一个元素以尾随逗号结尾,添加新项目不会导致与上一行的 SCM 差异。

可以使用可重新分配和添加的下标运算符索引数组和字典:

> shopping[0]
$R0: String = "Milk"
> costs["Milk"]
$R1: Int? = 1
> shopping.count
$R2: Int = 3
> shopping += ["Tea"]
> shopping.count
$R3: Int = 4
> costs.count
$R4: Int = 3
> costs["Tea"] = "String"
error: cannot assign a value of type 'String' to a value of type 'Int?'
> costs["Tea"] = 4
> costs.count
$R5: Int = 4

集合类似于字典;键是无序的,可以高效地查找。然而,与字典不同,键没有关联的值。因此,它们没有数组索引,但具有 insertremovecontains 方法。它们还有高效的集合交集方法,如 unionintersect。如果类型已定义,可以从数组字面量创建它们,或者直接使用集合初始化器:

> var shoppingSet: Set = [ "Milk", "Eggs", "Coffee", ]
> // same as: shoppingSet = Set( [ "Milk", "Eggs", "Coffee", ] )
> shoppingSet.contains("Milk")
$R6: Bool = true
> shoppingSet.contains("Tea")
$R7: Bool = false
> shoppingSet.remove("Coffee")
$R8: String? = "Coffee"
> shoppingSet.remove("Tea")
$R9: String? = nil
> shoppingSet.insert("Tea")
> shoppingSet.contains("Tea")
$R10: Bool = true

小贴士

在创建集合时,请使用显式的 Set 构造函数,否则类型将被推断为 Array,这将具有不同的性能特征。

可选类型

在前面的示例中,costs["Milk"] 的返回类型是 Int? 而不是 Int。这是一个 可选类型;可能有一个 Int 值,也可能为空。对于包含类型为 T 的元素的字典,对字典进行索引将具有 Optional<T> 类型,可以简写为 T?。如果字典中不存在该值,则返回的值将是 nil。其他面向对象的语言,如 Objective-C、C++、Java 和 C#,默认具有可选类型;任何对象值(或指针)都可以是 null。通过在类型系统中表示可选性,Swift 可以确定值是否确实存在或可能是 nil

> var cannotBeNil: Int = 1
cannotBeNil: Int = 1
> cannotBeNil = nil
error: cannot assign a value of type 'nil' to a value of type 'Int'
cannotBeNil = nil
              ^
> var canBeNil: Int? = 1
canBeNil: Int? = 1
> canBeNil = nil
$R0: Int? = nil

可选类型可以使用 Optional 构造函数显式创建。给定一个类型为 X 的值 x,可以使用 Optional(x) 创建一个可选 X? 值。可以通过将其与 nil 进行比较来测试值是否存在,然后使用 opt! 等展开它,例如:

> var opt = Optional(1)
opt: Int? = 1
> opt == nil
$R1: Bool = false
> opt!
$R2: Int = 1

如果展开了一个 nil 值,将发生错误:

> opt = nil
> opt!
fatal error: unexpectedly found nil while unwrapping an Optional value
Execution interrupted. Enter Swift code to recover and continue.
Enter LLDB commands to investigate (type :help for assistance.)

尤其是在处理基于 Objective-C 的 API 时,通常会将值声明为可选,尽管始终期望它们返回一个值。可以将此类变量声明为 隐式展开的可选值;这些变量的行为类似于可选值(它们可能包含 nil),但在访问值时,它们会根据需要自动展开:

> var implicitlyUnwrappedOptional:Int! = 1
implicitlyUnwrappedOptional: Int! = 1
> implicitlyUnwrappedOptional + 2
3
> implicitlyUnwrappedOptional = nil
> implicitlyUnwrappedOptional + 2
fatal error: unexpectedly found nil while unwrapping an Optional value

小贴士

通常应避免隐式展开的可选值,因为它们很可能会导致错误。它们主要用于与已知具有实例值的现有 Objective-C API 进行交互:

Nil 合并运算符

Swift 有一个 nil 合并运算符,类似于 Groovy 的 ?: 运算符或 C# 的 ?? 运算符。这提供了一种在表达式为 nil 时指定默认值的方法:

> 1 ?? 2
$R0: Int = 1
> nil ?? 2
$R1: Int = 2

nil 合并运算符也可以用来展开可选值。如果可选值存在,它将被展开并返回;如果不存在,则返回表达式的右侧值。类似于 || 短路和 && 运算符,除非必要,否则右侧不会进行评估:

> costs["Tea"] ?? 0
$R2: Int = 4
> costs["Sugar"] ?? 0
$R3: Int = 0

条件逻辑

Swift 中有三种主要的条件逻辑类型(在语法中称为分支语句):if语句、switch语句和guard语句。与其他语言不同,if的主体必须用大括号{}包围;如果在使用解释器时输入,则大括号{必须与if语句在同一行上。guard语句是一个用于函数的特殊if语句,将在本章后面的函数部分介绍。

If 语句

条件性地解包可选值如此常见,以至于 Swift 创建了一个特定的模式可选绑定来避免对表达式进行两次评估:

> var shopping = [ "Milk", "Eggs", "Coffee", "Tea", ]
> var costs = [ "Milk":1, "Eggs":2, "Coffee":3, "Tea":4, ]
> var cost = 0
> if let cc = costs["Coffee"] {
.   cost += cc
. }
> cost
$R0: Int = 3

只有当可选值存在时,if块才会执行。cc常量的定义仅存在于if块的主体中,并且在该作用域之外不存在。此外,cc是一个非可选类型,因此它保证不会是nil

注意

Swift 1 只允许在if块中有一个let赋值,导致嵌套的if语句形成金字塔。Swift 2 允许在单个if语句中使用多个以逗号分隔的let赋值。

> if let cm = costs["Milk"], let ct = costs["Tea"] {
.   cost += cm + ct
. }
> cost
$R1: Int = 8

如果找不到项目,可以使用else块来执行替代块:

> if let cb = costs["Bread"] {
.   cost += cb
. } else {
.   print("Cannot find any Bread")
. }
Cannot find any Bread

其他布尔表达式可以包括truefalse字面量,以及任何符合BooleanType协议的表达式,==!=相等运算符,===!==身份运算符,以及<<=>>=比较运算符。is type运算符提供了一个测试,以查看元素是否为特定类型。

小贴士

等于运算符和身份运算符之间的区别对于类或其他引用类型是相关的。等于运算符询问“这两个值是否彼此等效?”,而身份运算符询问“这两个引用是否彼此相等?”

Swift 有一个特定的布尔运算符,即~=模式匹配运算符。尽管名称如此,但这与正则表达式无关;相反,它是一种询问模式是否与特定值匹配的方法。这在switch块的实现中使用,下一节将介绍。

除了if语句外,还有一个类似于其他语言的三元 if 表达式。在条件之后,使用一个问号(?),然后是一个当条件为真时使用的表达式,然后是一个冒号(😃,后面是当条件为假时使用的表达式:

> var i = 17
i: Int = 17
> i % 2 == 0 ? "Even" : "Odd"
$R0: String = "Odd"

Switch 语句

Swift 有一个类似于 C 和 Java 的 switch 语句。然而,它在两个方面有所不同。首先,case 语句不再有默认的穿透行为(因此不会因为缺少 break 语句而引入错误),其次,case 语句的值可以是表达式而不是值,进行类型和范围的模式匹配。在相应的 case 语句的末尾,评估将跳转到 switch 块的末尾,除非使用了 fallthrough 关键字。如果没有 case 语句匹配,则执行 default 语句。

注意

当情况列表不是详尽无遗的时候,需要有一个 default 语句。如果不是,编译器将给出错误,指出列表不是详尽无遗的,并且需要 default 语句。

> var position = 21
position: Int = 21
> switch position {
.   case 1: print("First")
.   case 2: print("Second")
.   case 3: print("Third")
.   case 4...20: print("\(position)th")
.   case position where (position % 10) == 1:
.     print("\(position)st")
.   case let p where (p % 10) == 2:
.     print("\(p)nd")
.   case let p where (p % 10) == 3:
.     print("\(p)rd")
.   default: print("\(position)th")
. }
21st

在前面的例子中,如果位置是 123,表达式将分别打印出 FirstSecondThird。对于 420(包含)之间的数字,它将打印出带有 th 后缀的位置。否则,对于以 1 结尾的数字,它将打印 st;对于以 2 结尾的数字,它将打印 nd;对于以 3 结尾的数字,它将打印 rd。对于所有其他数字,它将打印 th

case 语句中的 4...20 范围表达式代表一个模式。如果表达式的值与该模式匹配,则将执行相应的语句:

> 4...10 ~= 4
$R0: Bool = true
> 4...10 ~= 21
$R1: Bool = false

Swift 中有两种范围运算符:包含或 闭范围 和排除或 半开范围。闭范围用三个点指定;因此 1...12 将给出介于 1 和 12 之间的整数列表。半开范围用两个点和小于运算符指定;因此 1..<10 将提供从 1 到 9 的整数,但不包括 10。

switch 块中的 where 子句允许评估任意表达式,前提是模式匹配。这些表达式按顺序评估,即它们在源文件中的顺序。如果 where 子句评估为 true,则将执行相应的语句集。

可以使用 let 变量语法来定义一个常量,该常量引用 switch 块中的值。这个局部常量可以在 where 子句或对应的具体情况的语句中使用。或者,也可以使用周围作用域中的变量。

注意

如果需要匹配多个 case 语句的相同模式,它们可以用逗号作为表达式列表分隔。或者,可以使用 fallthrough 关键字来允许为多个 case 语句使用相同的实现。

迭代

可以使用范围来迭代固定次数,例如,for i in 1...12。要打印这些数字,可以使用如下循环:

> for i in 1...12 {
.   print("i is \(i)")
. }

如果数字不是必需的,则可以使用下划线 (_) 作为占位符来充当废弃值。下划线可以被赋值但不能被读取:

> for _ in 1...12 {
.   print("Looping...")
. }

然而,更常见的是使用 for in 模式遍历集合的内容。这会遍历集合中的每个项目,并且 for 循环的主体会针对每个项目执行:

> var shopping = [ "Milk", "Eggs", "Coffee", "Tea", ]
> var costs = [ "Milk":1, "Eggs":2, "Coffee":3, "Tea":4, ]
> var cost = 0
> for item in shopping {
.   if let itemCost = costs[item] {
.     cost += itemCost
.   }
. }
> cost
cost: Int = 10

要遍历字典,可以提取键或值并将它们作为数组处理:

> Array(costs.keys)
$R0: [String] = 4 values {
  [0] = "Coffee"
  [1] = "Milk"
  [2] = "Eggs"
  [3] = "Tea"
}
> Array(costs.values)
$R1: [Int] = 4 values {
  [0] = 3
  [1] = 1
  [2] = 2
  [3] = 4
}

注意

字典中键的顺序是不保证的;随着字典的变化,顺序可能会改变。

将字典的值转换为数组将导致创建数据的副本,这可能导致性能不佳。由于底层 keysvaluesLazyMapCollection 类型,它们可以直接遍历:

> costs.keys
$R2: LazyMapCollection<[String : Int], String> = {
  _base = {
    _base = 4 key/value pairs {
      [0] = { key = "Coffee" value = 3 }
      [1] = { key = "Milk"   value = 1 }
      [2] = { key = "Eggs"   value = 2 }
      [3] = { key = "Tea"    value = 4 }
    }
  _transform =}
}

要打印出字典中的所有键,可以使用 keys 属性与 for in 循环一起使用:

> for item in costs.keys {
.   print(item)
. }
Coffee
Milk
Eggs
Tea

遍历字典中的键和值

遍历字典以获取所有键,然后随后查找值将导致在数据结构中搜索两次。相反,可以使用 元组 同时遍历键和值。元组类似于固定大小的数组,但它允许一次分配一对(或更多)值:

> var (a,b) = (1,2)
a: Int = 1
b: Int = 2

可以使用元组对字典的键和值进行成对迭代:

> for (item,cost) in costs {
.   print("The \(item) costs \(cost)")
. }
The Coffee costs 3
The Milk costs 1
The Eggs costs 2
The Tea costs 4

ArrayDictionary 都遵守 SequenceType 协议,这使得它们可以用 for in 循环进行迭代。实现 SequenceType 的集合(以及其他对象,如 Range)有一个 generate 方法,它返回一个 GeneratorType,允许遍历数据。自定义 Swift 对象可以实现 SequenceType 以允许它们在 for in 循环中使用。

使用 for 循环进行迭代

虽然在 Swift 中 for 运算符最常见的使用是在 for in 循环中,但在 Swift 1 和 2 中也可以使用更传统的 for 循环形式。这有一个初始化部分,一个在每个循环开始时测试的条件,以及一个在每个循环结束时评估的步进操作。虽然 for 循环周围的括号是可选的,但代码块的括号是强制性的。

注意

有提议说,传统的 for 循环和增量/减量运算符都应该从 Swift 3 中移除。建议尽可能避免这些循环形式。

计算介于 1 和 10 之间的整数的和可以不使用范围运算符:

> var sum = 0
. for var i=0; i<=10; ++i {
.   sum += i
. }
sum: Int = 55

如果需要在 for 循环中更新多个变量,Swift 有一个 表达式列表,它是一组以逗号分隔的表达式。要遍历两个变量的集合,可以使用以下方法:

> for var i = 0,j = 10; i<=10 && j >= 0; ++i,--j {
.   print("\(i), \(j)")
. } 
0, 10
1, 9
…
9, 1
10, 0

小贴士

Apple 推荐使用 ++i 而不是 i++(以及相反的,--i 而不是 i--),因为它们会在操作后返回 i 的值,这可能是预期的值。如前所述,这些运算符可能在 Swift 的未来版本中被移除。

断言和继续

break语句会提前退出最内层的循环,并将控制权跳转到循环的末尾。continue语句会将执行权带到最内层循环的顶部和下一个项目。

要从嵌套循环中跳出继续,可以使用一个标签。Swift 中的标签只能应用于循环语句,如whilefor。标签通过一个标识符和一个冒号在循环语句之前引入:

> var deck = [1...13, 1...13, 1...13, 1...13]
> suits: for suit in deck {
.   for card in suit {
.     if card == 3 {
.       continue // go to next card in same suit
.     }
.     if card == 5 {
.       continue suits // go to next suit
.     } 
.     if card == 7 {
.       break // leave card loop
.     }
.     if card == 13 {
.       break suits // leave suit loop
.     }
.   } 
. }

函数

可以使用func关键字创建函数,它包含一组参数和一组语句。可以使用return语句来退出函数:

> var shopping = [ "Milk", "Eggs", "Coffee", "Tea", ]
> var costs = [ "Milk":1, "Eggs":2, "Coffee":3, "Tea":4, ]
> func costOf(items:[String], _ costs:[String:Int]) -> Int {
.   var cost = 0
.   for item in items {
.     if let ci = costs[item] {
.       cost += ci
.     }
.   }
.   return cost
. }
> costOf(shopping,costs)
$R0: Int = 10

函数的返回类型在参数之后指定,后面跟着一个箭头(->)。如果省略,则函数不能返回值;如果存在,则函数必须返回该类型的值。

注意

costs参数前面的下划线(_)是必需的,以避免它成为一个命名参数。Swift 函数中的第二个及以后的参数是隐式命名的。为了确保它被当作位置参数处理,需要在参数名称前加上下划线_

带有位置参数的函数可以通过括号调用,例如costOf(shopping,costs)调用。如果一个函数没有参数,则仍然需要括号。

foo()表达式调用没有参数的foo函数。foo表达式代表函数本身,因此一个表达式,如let copyOfFoo = foo,会导致函数的一个副本;因此,copyOfFoo()foo()具有相同的效果。

命名参数

Swift 还支持命名参数,可以使用变量的名称或使用外部参数名称定义。为了修改函数以支持使用basketprices作为参数名称进行调用,可以执行以下操作:

> func costOf(basket items:[String], prices costs:[String:Int]) -> Int {
.   var cost = 0
.   for item in items {
.     if let ci = costs[item] {
.       cost += ci
.     }
.   }
.   return cost
. }
> costOf(basket:shopping, prices:costs)
$R1: Int = 10

此示例为函数定义了外部参数名称basketprices。函数签名通常被称为costOf(basket:prices:),当参数的作用不明确时(尤其是如果它们是同一类型)非常有用。

可选参数和默认值

Swift 函数可以通过在函数定义中指定默认值来具有可选参数。当函数被调用时,如果缺少可选参数,则使用该参数的默认值。

注意

可选参数是可以省略的函数调用参数,而不是必须的参数,它接受一个可选值。这种命名是不幸的。将其视为默认参数而不是可选参数可能会有所帮助。

默认参数值在函数签名中的类型之后指定,后面跟着一个等号(=)然后是表达式。每次函数被调用而没有相应的参数时,此表达式都会重新评估。

costOf示例中,而不是每次传递costs的值,它可以定义为一个默认参数:

> func costOf(items items:[String], costs:[String:Int] = costs) -> Int {
.   var cost = 0
.   for item in items {
.     if let ci = costs[item] {
.       cost += ci
.     }
.   }
.   return cost
. }
> costOf(items:shopping)
$R2: Int = 10
> costOf(items:shopping, costs:costs)
$R3: Int = 10

请注意,捕获的costs变量在函数定义时被绑定。

注意

要在函数中将命名参数用作第一个参数,必须重复参数名称。Swift 1 使用哈希(#)来表示隐式参数名称,但这个特性在 Swift 2 中被移除。

守卫

函数通常需要满足某些条件的参数才能成功运行。例如,可选值必须有值,或者整型参数必须在某个范围内。

通常,实现此模式的模式是有一系列if语句,在顶部跳出函数,或者有一个if块包裹整个方法主体:

if card < 1 || card > 13 {
  // report error
  return
}

// or alternatively:

if card >= 1 && card <= 13 {
  // do something with card
} else {
  // report error
}

这两种方法都有缺点。在第一种情况下,条件已经被否定;不是寻找有效值,而是在检查无效值。这可能会导致微妙的错误悄悄出现;例如,card < 1 && card > 13永远不会成功,但它可能会无意中通过代码审查。还有如果块没有returnbreak会发生什么的问题;它可能是完全有效的 Swift 代码,但仍然包含错误。

在第二种情况下,函数的主体在if语句的主体中至少缩进一个级别。当需要多个条件时,可能会有许多嵌套的if语句,每个都有自己的错误处理或清理要求。如果需要新的条件,则代码的主体可能需要进一步缩进,导致即使只有空白发生变化,仓库中的代码也会发生 churn。

Swift 2 添加了guard语句,从概念上讲与if语句相同,但它只有一个else子句体。此外,编译器检查else块是否从函数返回,无论是通过返回还是抛出异常:

> func cardName(value:Int) -> String {
.   guard value >= 1 && value <= 13 else {
.     return "Unknown card"
.   }
.   let cardNames = [11:"Jack",12:"Queen",13:"King",1:"Ace",]
.   return cardNames[value] ?? "\(value)"
. }

Swift 编译器检查guard``else块是否离开函数,如果没有,则报告编译错误。在guard语句之后的代码可以保证值在1...13范围内,而无需进行进一步测试。

guard块也可以用来执行可选绑定;如果guard条件是一个执行可选测试的let赋值,那么guard语句之后的代码可以使用该值而无需进一步展开:

> func firstElement(list:[Int]) -> String {
.   guard let first = list.first else {
.     return "List is empty"
.   }
.   return "Value is \(first)"
. }

由于数组的第一元素是可选值,这里的guard测试获取了该值并展开了它。当它在函数的后续部分使用时,展开的值可用于使用,而无需进一步展开。

多重返回值和参数

到目前为止,函数的示例都只返回单一类型。如果一个函数有多个返回结果会发生什么?在面向对象的语言中,答案是返回一个类;然而,Swift 有元组,可以用来返回多个值。元组的类型是其组成部分的类型:

> var pair = (1,2)
pair: (Int, Int) ...

这可以用来从函数中返回多个值;而不是只返回一个值,可以返回一个值的元组。

注意

Swift 还有输入输出参数,这将在第六章的处理错误部分中看到,解析网络数据

分别,也可以接受可变数量的参数。一个函数可以轻松地使用[]接受值数组,但 Swift 提供了一个机制,允许使用可变参数调用,这表示在类型后面的省略号(…)。然后可以将该值用作函数中的数组。

注意

Swift 1 只允许可变参数作为最后一个参数;Swift 2 放宽了这一限制,允许在函数参数中任何位置出现单个可变参数。

结合这两个特性,可以创建一个minmax函数,它从整数列表中返回最小值和最大值:

> func minmax(numbers:Int…) -> (Int,Int) {
.   var min = Int.max
.   var max = Int.min
.   for number in numbers {
.     if number < min {
.       min = number
.     }
.     if number > max {
.       max = number
.     }
.   }
.   return (min,max)
. }
> minmax(1,2,3,4)
$R0: (Int, Int) = {
  0 = 1
  1 = 4
}

numbers:Int…参数表示可以传递多个参数到函数中。在函数内部,它被处理为一个普通数组;在这种情况下,使用for in循环迭代。

注意

Int.max是一个表示最大Int值的常量,而Int.min是一个表示最小Int值的常量。对于其他整数类型,也存在类似的常量,例如UInt8.maxInt64.min

如果没有传入参数呢?如果在 64 位系统上运行,那么输出将是:

> minmax()
$R1: (Int, Int) = {
  0 = 9223372036854775807
  1 = -9223372036854775808
}

这可能对minmax函数没有意义。而不是返回错误值或默认值,可以使用类型系统。通过使元组为可选,如果不存在,则可以返回nil值,如果存在,则返回元组:

> func minmax(numbers:Int...) -> (Int,Int)? {
.   var min = Int.max
.   var max = Int.min
.   if numbers.count == 0 {
.     return nil
.   } else {
.     for number in numbers {
.       if number < min {
.         min = number
.       }
.       if number > max {
.         max = number
.       }
.     }
.     return(min,max)
.   }
. }
> minmax()
$R2: (Int, Int)? = nil
> minmax(1,2,3,4)
$R3: (Int, Int)? = (0 = 1, 1 = 4)
> var (minimum,maximum) = minmax(1,2,3,4)!
minimum: Int = 1
maximum: Int = 4

返回一个可选值允许调用者确定在最大值和最小值不存在的情况下应该发生什么。

小贴士

如果一个函数不总是有一个有效的返回值,使用可选类型将这种可能性编码到类型系统中。

返回结构化值

元组是有序数据集。元组中的条目是有序的,但很快就会变得不清楚存储了哪些数据,尤其是如果它们是相同类型的话。在minmax元组中,不清楚哪个值是最小值,哪个值是最大值,这可能导致后续的微妙编程错误。

结构体(struct)就像一个元组,但是具有命名值。这允许通过名称而不是位置来访问成员,从而减少错误并提高透明度。命名值也可以添加到元组中;本质上,具有命名值的元组是匿名结构体。

小贴士

结构体以值复制的方式传递,就像元组一样。如果两个变量被分配了相同的结构体或元组,那么对一个的更改不会影响另一个的值。

使用 struct 关键字定义 struct 并在主体中包含变量或值:

> struct MinMax {
.   var min:Int
.   var max:Int
. }

这定义了一个 MinMax 类型,它可以替代迄今为止看到的任何类型。它可以在 minmax 函数中使用来返回一个 struct 而不是元组:

> func minmax(numbers:Int...) -> MinMax? {
.   var minmax = MinMax(min:Int.max, max:Int.min)
.   if numbers.count == 0 {
.     return nil
.   } else {
.     for number in numbers {
.       if number < minmax.min {
.         minmax.min = number
.       }
.       if number > minmax.max {
.         minmax.max = number
.       }
.     }
.     return minmax
.   }
. }

struct 使用类型初始化器进行初始化;如果使用 MinMax(),则每个结构类型默认值(基于结构定义)将被给出,但如果需要,可以使用 MinMax(min:-10,max:11) 明确覆盖这些值。例如,如果 MinMax 结构定义为 struct MinMax { var min:Int = Int.max; var max:Int = Int.min },则 MinMax() 将返回一个填充了适当的最小和最大值的结构。

注意

当结构初始化时,所有非可选字段都必须被分配。它们可以作为命名参数传递给初始化器或在结构定义中指定。

Swift 还具有类;这些将在下一章的 Swift 类部分中介绍。

错误处理

在 Swift 的原始版本中,错误处理由函数结果返回的 Bool 或可选值组成。这通常与 Objective-C 的工作不一致,Objective-C 在各种调用中使用可选 NSError 指针,如果发生条件,则设置该指针。

Swift 2 添加了一个类似异常的错误模型,它允许以更紧凑的方式编写代码,同时确保错误得到相应处理。尽管这与 C++ 异常处理的方式不完全相同,但错误处理的语义非常相似。

可以使用新的 throw 关键字创建错误,错误存储为 ErrorType 的子类型。尽管 Swift 的 enum 值(在第三章,创建 iOS Swift 应用)中经常用作错误类型,但也可以使用 struct 值。

可以通过在类型名称后附加超类型来创建 ErrorType 的子类型作为异常类型:

> struct Oops:ErrorType {
.   let message:String
. }

使用 throw 关键字和创建异常类型实例来抛出异常:

> throw Oops(message:"Something went wrong")
$E0: Oops = {
  message = "Something went wrong"
}

注意

REPL 使用 $E 前缀显示异常结果;普通结果使用 $R 前缀显示。

抛出错误

函数可以使用返回类型前的 throws 关键字声明返回错误,如果有的话。之前的 cardName 函数,如果参数超出范围则返回一个虚拟值,可以通过在返回类型前添加 throws 关键字并将 return 改为 throw 来升级为抛出异常:

> func cardName(value:Int) throws -> String {
.   guard value >= 1 && value <= 13 else {
.     throw Oops(message:"Unknown card")
.   }
.   let cardNames = [11:"Jack",12:"Queen",13:"King",1:"Ace",]
.   return cardNames[value] ?? "\(value)"
. }

当函数使用实际值调用时,返回结果;当传递无效值时,将抛出异常:

> cardName(1)
$R1: String = "Ace"
> cardName(15)
$E2: Oops = {
  message = "Unknown card"
}

当与 Objective-C 代码交互时,接受NSError**参数的方法在 Swift 中自动表示为抛出异常的方法。一般来说,任何参数以NSError**结尾的方法在 Swift 中都被视为抛出异常。

注意

在 C++和 Objective-C 中抛出异常的性能不如 Swift 中的异常处理,因为 Swift 不执行栈回溯。因此,从性能角度来看,Swift 中的异常抛出等同于处理返回值。预计 Swift 库在未来会朝着基于throws的错误检测方式发展,并远离 Objective-C 使用**NSError指针的方式。

捕获错误

异常处理的另一半是能够在错误发生时捕获错误。与其他语言一样,Swift 现在有一个try/catch块,可以用来处理错误条件。与其他语言不同,语法略有不同;没有try/catch块,而是有一个do/catch块,并且每个可能抛出错误的表达式都带有自己的try语句:

> do { 
.   let name = try cardName(15)
.   print("You chose \(name)")
. } catch {
.   print("You chose an invalid card")
. }

当执行前面的代码时,它将打印出通用的错误消息。如果给出不同的选择,则将运行成功的路径。

可以捕获错误对象并在catch块中使用它:

. } catch let e {
.   print("There was a problem \(e)")
. }

小贴士

如果没有指定,默认的catch块将绑定到一个名为error的变量

这两个先前的示例都将捕获从代码主体抛出的任何错误。

注意

如果类型是一个使用模式匹配的enum,则可以显式地基于类型来捕获错误,例如catch Oops(let message)。然而,由于这不能用于结构体值,因此在这里无法进行测试。第三章,创建 iOS Swift 应用程序介绍了enum类型。

有时代码总是可以正常工作,并且没有失败的可能性。在这些情况下,如果已知问题永远不会发生,那么需要将代码包裹在do/try/catch块中是很麻烦的。Swift 提供了一个简短的快捷方式,使用try!语句来捕获和过滤异常:

> let ace = try! cardName(1)
ace: String = "Ace"

如果表达式确实失败,那么它将转换为运行时错误并停止程序:

> let unknown = try! cardName(15)

Fatal error: 'try!' expression unexpectedly raised an error: Oops(message: "Unknown card")

小贴士

使用try!通常不推荐;如果发生错误,程序将崩溃。然而,它通常与用户界面代码一起使用,因为 Objective-C 有许多可选方法和值,传统上被认为是非nil的,例如对封装窗口的引用。

更好的方法是使用try?,它将表达式转换为可选值:如果评估成功,则返回一个包含值的可选;如果评估失败,则返回一个nil值:

> let ace = try? cardName(1)
ace: String? = "Ace"
> let unknown = try? cardName(15)
unknown: String? = nil

这在if letguard let构造中使用时很方便,可以避免需要将代码包裹在do/catch块中:

> if let card = try? cardName(value) {
.   print("You chose: \(card)")
. }

清理错误后的代码

通常,有一个需要在函数返回之前执行一些清理工作的函数,无论函数是否成功完成。一个例子是与文件一起工作;在函数开始时文件可能被打开,而在函数结束时应该再次关闭,无论是否发生错误。

处理这种情况的传统方法是用可选值来持有文件引用,并在方法末尾如果它不是 nil,则关闭文件。然而,如果在方法执行过程中可能发生错误,则需要 do/catch 块来确保正确调用清理,或者一组嵌套的 if 语句,只有当文件成功时才会执行。

这种方法的缺点是代码的实际主体通常在每个方法末尾都缩进几次,每次都有不同级别的错误处理和恢复。资源获取和清理之间的语法分离可能导致错误。

Swift 有一个 defer 语句,可以用来注册一个在函数调用结束时运行的代码块。这个块无论函数是否正常返回(使用 return 语句)或发生错误(使用 throw 语句)都会运行。延迟块按执行顺序的相反顺序执行,例如:

> func deferExample() {
.   defer { print("C") }
.   print("A")
.   defer { print("B") }
. }
> deferExample()
A
B
C

请注意,如果 defer 语句未执行,则该块不会在方法末尾执行。这允许 guard 语句提前退出函数,同时执行已添加的 defer 语句:

> func deferEarly() { 
.   defer { print("C") } 
.   print("A") 
.   return 
.   defer { print("B") } // not executed
. }    
> deferEarly()
A
C

命令行 Swift

由于 Swift 可以被解释,因此可以在 shell 脚本中使用它。通过使用 hashbang 将解释器设置为 swift,脚本可以执行而无需单独的编译步骤。或者,Swift 脚本可以编译成原生可执行文件,可以在没有解释器开销的情况下运行。

解释型 Swift 脚本

将以下内容保存为 hello.swift

#!/usr/bin/env xcrun swift
print("Hello World")

小贴士

在 Linux 中,第一行应指向 swift 可执行文件的位置,例如 #!/usr/bin/swift

保存后,通过运行 chmod a+x hello.swift 使文件可执行。然后可以通过输入 ./hello.swift 来运行程序,并看到传统的问候语:

Hello World

参数可以通过命令行传递,并在过程中使用 Process 类的 arguments 常量进行查询。与其他 Unix 命令一样,第一个元素(0)是进程可执行文件名;从命令行传递的参数从一(1)开始。

可以使用 exit 函数来终止程序;然而,这个函数是在操作系统库中定义的,因此需要导入才能调用此函数。Swift 中的模块对应于 Objective-C 中的框架,并提供对模块中定义为公共 API 的所有函数的访问。从模块中导入所有元素的语法是 import module,尽管也可以使用 import func module.functionName 来导入单个函数。

注意

并非所有的基础库都在 Linux 上实现,这导致了一些行为上的差异。此外,iOS 和 OS X 上的基本功能的基础模块是 Darwin,而在 Linux 上是 Glibc。这些也可以通过 import Foundation 访问,这将包括适当的操作系统模块。

一个用于将参数打印为大写的 Swift 程序可以作为一个脚本实现:

#!/usr/bin/env xcrun swift
import func Darwin.exit
# import func Glibc.exit # for Linux
let args = Process.arguments[1..<Process.arguments.count]
for arg in args {
  print("\(arg.uppercaseString)")
}
exit(0)

使用 hello world 运行此代码的结果如下:

$ ./upper.swift hello world
HELLO
WORLD

通常,Swift 程序的入口点是名为 main.swift 的脚本。如果在 Xcode 中启动基于 Swift 的命令行应用程序项目,将自动创建一个 main.swift 文件。脚本不需要有 .swift 扩展名;例如,前面的示例可以命名为 upper,它仍然可以工作。

编译后的 Swift 脚本

虽然解释的 Swift 脚本对于实验和编写代码很有用,但每次启动脚本时,它都会使用 Swift 编译器进行解释,然后执行。对于简单的脚本(如将参数转换为大写),这可能占脚本执行时间的大部分。

要将 Swift 脚本编译成原生可执行文件,请使用带有 -o 输出标志的 swiftc 命令来指定要写入的文件。这将生成一个与解释脚本完全相同的可执行文件,但运行速度要快得多。可以使用 time 命令来比较解释和编译版本的运行时间:

$ time ./upper.swift hello world    # Interpreted
HELLO
WORLD
real  0m0.145s
$ xcrun swiftc -o upper upper.swift # Compile step
$ time ./upper hello world          # Compiled
HELLO
WORLD
real  0m0.012s

当然,数字会有所不同,初始步骤只发生一次,但 Swift 的启动非常轻量级。这些数字并不是指它们的绝对值,而是指它们之间的相对值。

编译步骤也可以用来将许多单独的 Swift 文件链接成一个可执行文件,这有助于创建一个更有组织的项目;Xcode 也会鼓励使用多个 Swift 文件。

摘要

Swift 解释器是学习 Swift 编程的绝佳方式。它允许创建和测试表达式、语句和函数,同时提供带有编辑支持的命令行历史记录。介绍了基本集合类型(如数组和集合)、标准数据类型(如字符串和数字)、可选值和结构体。还介绍了控制流和具有位置、命名和可变参数的函数,以及默认值。最后,还演示了如何编写 Swift 脚本并在命令行中运行它们。

下一章将探讨在 OS X 上使用 Swift 代码的另一种工作方式,即通过 Xcode 演示场。

第二章. 玩转 Swift

Xcode 随带一个命令行解释器(在 第一章 中介绍过),探索 Swift 和一个名为 playground 的图形界面,可以用来原型设计和测试 Swift 代码片段。在 playground 中输入的代码将被编译并交互式执行,这允许流畅的开发风格。此外,用户界面可以显示变量的图形视图以及时间轴,可以显示循环的执行情况。最后,playground 可以混合和匹配代码和文档,从而有可能提供作为 playground 的示例代码,并使用 playground 来学习如何使用现有的 API 和框架。

本章将介绍以下主题:

  • 如何创建 playground

  • 在时间轴中显示值

  • 使用快速查看展示对象

  • 运行异步代码

  • 使用 playground 的实时文档

  • 在 playground 中创建多个页面

  • playground 的限制

开始使用 playground

当 Xcode 启动时,将显示带有各种选项的欢迎屏幕,包括创建 playground 的能力。可以通过 Command + Shift + 1 显示欢迎屏幕,或通过导航到 窗口 | 欢迎使用 Xcode

开始使用 playground

创建 playground

可以通过 Xcode 欢迎屏幕(可以通过导航到 窗口 | 欢迎使用 Xcode 打开)或通过导航到 文件 | 新建 | Playground 来创建位于合适位置的 MyPlayground,目标为 iOS。在 桌面 上创建 playground 将允许轻松访问测试 Swift 代码,但它可以位于文件系统的任何位置。

playground 可以针对 OS X 应用程序或 iOS 应用程序。这可以在创建 playground 时进行配置,或者通过导航到 视图 | 实用工具 | 显示文件检查器 或按 Command + Option + 1 并将下拉菜单从 OS X 更改为 iOS 或反之亦然来切换到 实用工具 视图:

创建 playground

初始创建时,playground 将包含如下所示的代码片段:

// Playground - noun: a place where people can play
import UIKit
var str = "Hello, playground"

小贴士

针对 OS X 的 playground 将读取 import Cocoa

在右侧,将显示每行代码执行时的值。通过抓住 Swift 代码和输出之间的垂直分隔线,可以调整输出的大小以显示文本的完整值:

创建 playground

或者,通过将鼠标移至 playground 的右侧,将出现 快速查看 图标(眼睛符号)。如果点击它,将显示一个弹出框,显示完整详情:

创建 playground

查看控制台输出

控制台输出可以在调试区域中查看。这可以通过按Command + Shift + Y或通过导航到查看 | 调试区域 | 显示调试区域来实现。这将显示代码中执行的任何print语句的结果。

在游乐场中添加一个简单的for循环:

for i in 1...12 {
  print("I is \(i)")
}

输出显示在下面的调试区域中:

查看控制台输出

查看时间线

时间线显示了特定时间点的值。在之前显示的打印循环的情况下,输出显示在调试区域中。然而,可以使用游乐场来检查表达式在行上的值,而无需直接显示它。此外,结果可以绘制成图表,以显示这些值随时间的变化。图表的值与源代码在同一行显示,这与 Xcode 的早期版本不同,后者将它们显示在右侧。

print语句上方添加另一行来计算表达式(i-6)*(i-7)的执行结果,并将其存储在j常量中。

在变量定义旁边的行上,点击添加变量历史(+)符号,该符号位于右侧列(当鼠标移过该区域时可见)。点击后,它将变为(o)符号,并在右侧显示图表。这也可以应用于print语句:

for i in 1...12 {
  let j = (i-7) * (i-6)
  print("I is \(i)")
}

查看时间线

通过在实用工具区域中选择显示时间线复选框,可以在窗口底部显示时间线滑块。这将添加一个带有红色标记的时间线滑块在底部,并且可以使用它来滑动垂直条以查看特定点的确切值:

查看时间线

要同时显示多个值,请使用额外的变量来保存这些值,并在时间线中显示它们:

for i in 1...12 {
  let j = (i-7) * (i-6)
  let k = i
  print("I is \(i)")
}

查看时间线

当拖动时间线滑块时,两个值将同时显示。

使用快速查看显示对象

游乐场时间线可以显示对象以及数字和简单的字符串。使用类,如UIImage(或在 OS X 上的NSImage)可以在游乐场中加载和查看图像。这些被称为快速查看支持的对象,默认包括:

  • 字符串(有归属和无归属)

  • 视图

  • 类和结构体类型(成员显示)

  • 颜色

小贴士

通过实现返回数据图形视图的debugQuickLookObject方法,可以将对自定义类型的支持构建到 Swift 中。

显示彩色标签

要显示彩色标签,首先需要获取颜色。当针对 iOS 构建时,这将使用UIColor;但当针对 OS X 构建时,它将使用NSColor。这两个方法类型在很大程度上是等效的,但本章将演示使用 iOS 类型。

可以通过初始化器或使用 Swift 中暴露的预定义颜色之一来获取颜色,如下所示:

import UIKit // AppKit for OS X
let blue = UIColor.blueColor() // NSColor.blueColor() for OS X

小贴士

在 Xcode 7.1 及以上版本中,可以从颜色选择器中直接拖动颜色到 Swift 代码中,它将被转换为具有特定硬编码颜色值的颜色初始化器。

颜色可以用作 UILabeltextColor,它以特定的大小和颜色显示文本字符串。UILabel 需要一个大小,这由一个 CGRect 表示,并且可以使用 xy 位置以及 widthheight 来定义。xy 位置对于游乐场来说不相关,因此可以将其保留为零:

let size = CGRect(x:0,y:0,width:200,height:100)
let label = UILabel(frame:size)// NSLabel for OS X

最后,文本需要以蓝色和更大的字体大小显示:

label.text = str // from the first line of the code
label.textColor = blue
label.font = UIFont.systemFontOfSize(24) // NSFont for OS X

当游乐场运行时,颜色和字体将在时间轴上显示,并且可供快速查看。尽管显示的是相同的 UILabel 实例,但时间轴和快速查看值显示了对象在每个点的状态快照,这使得查看变化之间发生的事情变得容易:

显示带颜色的标签

显示图片

可以使用 UIImage 构造函数(或在 OS X 上的 NSImage)创建和加载到游乐场中。两者都接受一个 named 参数,该参数用于从游乐场的 Resources 文件夹中查找和加载具有给定名称的图像。

要将文件复制到游乐场的 Resources 文件夹,首先下载一个图像,例如 http://alblue.bandlem.com/images/AlexHeadshotLeft.png,并将其保存为 alblue.png 在一个合适的位置,如 桌面。为了将其添加到游乐场,需要使用 Command + 1 或通过导航到 视图 | 导航器 | 显示项目导航器 打开项目导航器。一旦打开,文件可以拖放到树中的 Resources 元素:

显示图片

小贴士

Xcode 7.1 允许直接将图片拖动到源代码区域。它将填充一个 UIImage(或 NSImage),并将其复制到资源区域。Xcode 7.0 及以下版本如果拖动图片,则仅复制源文件的完整路径。

或者,要使用命令行下载徽标,打开 Terminal.app 并运行以下命令:

$ mkdir MyPlayground.playground/Resources
$ curl http://alblue.bandlem.com/images/AlexHeadshotLeft.png > MyPlayground.playground/Resources/alblue.png

现在可以使用以下方式在 Swift 中创建图像:

let alblue = UIImage(named:"alblue")

小贴士

与游乐场关联的 Resources 的位置可以在 文件检查器 工具视图中查看,该工具可以通过按 Command + Option + 1 打开。

创建的图像可以使用 快速查看 或将其添加到值历史记录中显示:

显示图片

小贴士

可以通过创建一个 NSURL 对象 NSURL(string:"http://...")! 来使用 URL 获取图像,然后使用 NSData(contentsOfURL:)! 加载 URL 的内容,最后使用 UIImage(data:) 将其转换为图像。然而,由于 Swift 会反复执行代码,URL 在单个调试会话中会被多次访问而不会缓存。建议在 playground 中避免使用 NSData(contentsOfURL:) 和类似的网络类。

高级技巧

playground 有自己的 XCPlayground 框架,可以用来执行某些任务。例如,可以在循环中捕获单个值以供后续分析。它还允许在 playground 完成运行后继续执行异步代码。

显式捕获值

可以通过导入 XCPlayground 框架并使用 XCPlaygroundPage.currentPage,然后调用 captureValue 方法来显式地向时间轴添加值,该方法需要一个标识符,该标识符既用作标题,也用于在相同系列中分组相关的数据值。当选择值历史按钮时,它实际上会插入一个调用 captureValue 的调用,其中表达式的值作为标识符。

例如,要自动将徽标添加到时间轴上:

import XCPlayground
let page = XCPlaygroundPage.currentPage
let alblue = UIImage(named:"alblue")
page.captureValue(alblue, withIdentifier:"Al Blue")

打开 辅助编辑器 将会显示时间轴以及记录的值:

显式捕获值

可以使用标识符来分组循环中显示的数据,标识符代表值的类别。例如,要显示 16 之间所有偶数和奇数的列表,可以使用以下代码:

for n in 1...6 {
  if n % 2 == 0 {
    page.captureValue(n,withIdentifier:"even")
    page.captureValue(0,withIdentifier:"odd")
  } else {
    page.captureValue(n,withIdentifier:"odd")
    page.captureValue(0,withIdentifier:"even")
  }
}

执行后,结果将看起来像:

显式捕获值

运行异步代码

默认情况下,当执行到达当前 playground 页面的末尾时,执行会停止。在大多数情况下这是期望的,但当涉及异步代码时,即使主代码已经执行完成,执行可能还需要继续运行。这可能涉及网络数据或存在多个需要同步结果的任务。

例如,将之前的偶数/奇数分割包裹在一个异步调用中,将不会显示任何数据:

dispatch_async(dispatch_get_main_queue()) {
  for n in 1...6 {
    // as before
  }
}

小贴士

这使用了 Swift 的一种语言特性:dispatch_async 方法,它实际上是一个接受队列和块类型的两个参数的方法。然而,如果最后一个参数是块类型,那么它可以表示为一个尾随闭包而不是一个参数。

要允许 playground 在达到末尾后继续执行,请添加以下赋值:

page.needsIndefiniteExecution = true

虽然这表明执行将永远运行,但它被限制在 30 秒的运行时间内,或者屏幕右下角显示的任何值。可以通过输入新值或使用+按钮来增加/减少一秒来更改此超时。此外,可以通过点击窗口左下角的方块图标来停止执行:

运行异步代码

游乐场与文档

游乐场可以包含代码和文档的混合。这允许将一组代码、示例和说明与游乐场本身混合在一起。

使用游乐场学习

由于游乐场可以包含代码和文档的混合体,这使得它们成为查看注释代码片段的理想格式。实际上,Apple 的 Swift Tour 手册可以作为一个游乐场文件打开。

可以通过导航到帮助 | 文档和 API 参考或按Command + Shift + 0来搜索 Xcode 文档。在显示的搜索对话框中,键入Swift Tour并选择第一个结果。Swift Tour 手册应该在 Xcode 的帮助系统中显示,如下所示:

使用游乐场学习

在第一部分提供了一个下载并作为游乐场打开文档的链接;如果下载,它可以在 Xcode 中以独立游乐场的形式打开。这提供了相同的信息,但它允许代码示例是动态的,并在窗口中显示结果:

使用游乐场学习

通过基于游乐场文档的学习方式的一个关键优势是代码可以进行实验。在文档的简单值部分,其中myVariable被分配,游乐场的右侧显示了值。如果更改了字面数字,新的值将被重新计算并在右侧显示。

理解游乐场格式

游乐场是一个OS X bundle,这意味着它看起来像一个单独的文件。如果在TextEdit.appFinder中选择游乐场,它看起来就像一个普通文件:

理解游乐场格式

在幕后,它实际上是一个目录:

$ ls -F
MyPlayground.playground/

在目录内,有一些文件:

$ ls -1 MyPlayground.playground/*
MyPlayground.playground/Contents.swift
MyPlayground.playground/Resources
MyPlayground.playground/contents.xcplayground
MyPlayground.playground/playground.xcworkspace
MyPlayground.playground/timeline.xctimeline

文件如下:

  • Contents.swift文件,这是创建新游乐场时默认创建的 Swift 文件,它包含任何新游乐场内容的输入代码

  • 之前创建的Resources目录,用于存储标志图像

  • contents.xcplayground文件,是构成游乐场的文件的 XML 目录表

  • playground.xcworkspace,用于在 Xcode 中存储项目元数据

  • timeline.xctimeline文件,包含执行 Swift 文件时由运行时生成的执行时间戳文件,当时间线打开时

目录文件定义了正在针对哪个运行时环境(例如,iOS 或 OS X)以及时间线文件的引用:

<playground version='5.0' target-platform='ios' requires-full-environment='true' timelineScrubberEnabled='true' display-mode='raw'>
  <timeline fileName='timeline.xctimeline'/>
</playground>

小贴士

当在 Xcode 中进行更改时,Xcode 的 playground 目录会被删除并重新创建。在该目录中打开的任何 Terminal.app 窗口将不再显示任何文件。因此,使用外部工具或就地编辑文件可能会导致更改丢失。此外,使用过时的控制系统版本,如 SVN 和 CVS,可能会在保存之间丢失版本控制元数据。Xcode 随带行业标准的 Git 版本控制系统,应优先使用。

添加页面

默认情况下,Xcode playground 只打开一个页面。然而,对于更全面的文档示例,可能更倾向于多个单独的页面。例如,与其创建一个包含子标题的非常长的页面(这可能需要一些时间来解释和执行),不如添加额外的页面,每个页面都有其特定的示例。这也具有这样的优势,即可以交互式地实验代码,因为只有页面上的示例需要重新计算。

要向现有的 playground 添加新页面,在项目导航器中右键单击 MyPlayground 顶级元素,并选择 New Playground Page 菜单项。或者,导航到 文件 | 新建 | Playground 页面 或其快捷键,Command + Option + N。完成此操作后,第一个页面变为 未命名页面,新添加的页面变为 未命名页面 2

添加页面

可以通过在左侧的项目导航器中拖放页面来重新排序页面。还可以通过选择页面,然后单击它以显示可以重命名的文本字段来重命名页面。这与在 Finder 中重命名文件类似。文档的 @previous@next 链接允许读者在以下部分中描述的页面上导航。

注意

当与具有页面的 playground 一起工作时,contents.xcplayground 文件的版本号更新为 6.0,并创建一个新的 Pages 目录,该目录位于 Resources 顶级文件夹旁边。在 Pages 目录中,每个页面都表示为其自己的 .xcplaygroundpage 文件夹,其中包含一个 Contents.swift 文件和一个单独的 timeline.xctimeline 文件。

代码文档化

Swift 2 采用了新的文档标记方案,用于与 playground 一起使用,同时也用于文档化 Swift 代码。因此,文档注释被描述为适用于 Playground 注释符号文档

游乐场注释以 //: 开头用于单行注释,并使用 /*:*/ 用于块级注释。这些在游乐场中作为内联文档呈现,并取代了先前版本的 Xcode 中存在的嵌套 HTML。标记默认显示为原始文本,但可以通过导航到 编辑器 | 显示渲染标记 来查看渲染内容。要将它切换回显示原始标记并允许编辑文本,请导航到 编辑器 | 显示原始标记。此设置也保存在 xcplayground 文件中,带有 display-mode='rendered'display-mode='raw' 属性。

符号文档以 /// 开头用于单行注释,并使用 /***/ 用于块级注释。符号文档适用于变量和常量、函数和类型。符号定义上方只能有一个类型的符号文档注释(单行或多行,但不能同时存在)。多个连续的单行注释将被合并为一个块。

小贴士

符号文档可以通过在标识符上按下 Command + Control + ? 来揭示,或者通过在 Xcode 中按下 Alt 并单击标识符来揭示。

无论是游乐场还是符号文档,都允许使用一些标记来格式化文本。此外,还有一些可以指定为带有短横线的符号 格式命令,后跟命令名称和冒号。这些用于引入文档,例如,对于函数的特定参数、返回类型或抛出的错误。

游乐场导航文档

在多页游乐场中,可以在页面之间创建导航链接。每个页面都有一个名称(最初为 未命名页面未命名页面 2 等),但可以在项目导航器中重命名。

要重命名页面,请使用 Command + 1 打开项目导航器,然后在导航视图中选择页面。可以通过双击页面名称使其可编辑,从而将其转换为文本字段:

Playground 导航文档

通过链接到特定页面来执行,链接表示为 链接名称。例如,要创建到刚刚显示的第一个页面的链接,可以使用以下内容:

//: Go back to the first page

小贴士

页面名称中的空格需要使用 URL 转义,因此空格表示为 %20。使用 + 是无效的。

由于页面名称可能很脆弱,建议使用 下一页上一页 链接。这些可以使用 @next@previous 特殊标识符来表示页面名称,如下所示:

//: Go back to the previous page,
//: or move forward to the next page.

建议使用 @next@previous 来连接多个页面,因为这允许在不修改内容的情况下重新排序页面。可以通过在项目导航器中拖放项目上下来重新排序页面。

注意

页面导航仅在游乐场中可用。

文本格式化

游戏场和符号文档可以使用多种不同的格式化样式,使用标记语言来表示不同类型的文本。以下是一些:

  • 列表项,使用*+字符之一作为项目符号,后跟一个空格和文本

  • 编号列表,使用数字,后跟一个点,一个空格和文本

  • 水平线,使用四个破折号----在文本中生成水平线

  • 引用块,每行以>后跟一个空格开始

  • 块代码,要么从开头缩进四个空格,要么以swift`` ```` swift``

  • Headings, using #, ##, or ### for level 1, 2, or 3 headings, respectively. Alternatively, heading level 1 can use a === underneath the title and heading level 2 can use --- underneath the title

Tip

Exactly a single space is required between the end of the list delimiter (the bullet or the period) and the following text; otherwise it will not be rendered as expected

In addition to the block-level formatting, it is possible to use in-line formatting elements:

  • Code can be represented with `backticks` around the words
  • Text can be emphasized in italics _like this_ or *like this*
  • Text can be marked as bold using __this__ or **this**

Images and links can also be used in documentation:

  • Images are represented with ![Alternate Text](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/url "hover text")
  • Links are represented with link text
  • Links can also be declared with [link title]: url "hover text", and then referred to later with [link title]

For example, here is a markup block consisting of many single-line comments, which will be concatenated into a single documentation block:


//: # 示例文档

//: 导航到上一页或下一页页面

//: ----

//: 列表项:

//: 1\. 第一项

//: 2\. 第二项

//: 3\. 第三项

//:

//: 列表项:

//: * 第一项

//: * 第二项

//:   + 子项

//:   + 子项

//: * 第三项

//:

//: 如何使用`for`在 Swift 中做循环:

//:

//:     for i in 1...12 {

//:       print("Looping \(i)")

//:     }

//:

//: > 这是一个引用块

//: > 这被合并在一起

//: > 使用斜体或**粗体**

//:

//: 链接到[AlBlue 的博客](http://alblue.bandlem.com)

//: ![AlBlue 的图片](http://alblue.bandlem.com/images/AlexHeadshotLeft.png "AlBlue")

```swift

When viewed in a rendered markup view, it will look like:

![Text formatting](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00021.jpeg)

### Symbol documentation

When writing documentation for symbols (variables, constants, functions, and so on), additional commands can be used to indicate particular values. These are represented with a `–` dash, followed by a command name, and then a colon. These include the following:

*   `- parameter name: description`: This is the `description` of a `parameter` called `name` in the function or method
*   `- parameters:`: This is a group of related `parameter` elements
*   `- returns: description`: This is the `description` of the return result
*   `- throws: description`: This is the `description` of any errors that may occur

There are also a number of field commands that can be used with symbols. These are all represented with `– name: description`, and they all have exactly the same effect but with a different text name in the symbol reference. These include the following:

*   `author`: This is the name of the author who wrote this section.
*   `authors`: This is a series of paragraphs with one author name per paragraph.
*   `bug`: This is a description of a known bug.
*   `complexity`: This is a description of the complexity of the function, such as `O(1)` or `O(N)`. Use `\*` to represent an escaped `*` character or to denote higher orders, for example, `O(N\*N)`.
*   `copyright`: This is a copyright statement.
*   `date`: This is a date reference; please note that the text field is not parsed in any way.
*   `experiment`: This is a block-denoting experiment.
*   `important`: This marks something as important.
*   `invariant`: This describes an invariant of the function.
*   `note`: This introduces a note.
*   `precondition`: This describes what must be true for the function that has to be called.
*   `postcondition`: This describes what must be true after the function returns.
*   `remark`: This adds general notes to the symbol.
*   `requires`: This notes what is required, such as module dependencies, or a minimum version of the operating system.
*   `seealso`: This adds a **See Also** link to the documentation.
*   `since`: This is documentation indicating when the functionality first arrived.
*   `todo`: This adds a note to do later.
*   `version`: This documents the version number of the location.
*   `warning`: This adds a warning note.

The following combines a number of these documentation examples into a multi-line documentation block:

/**

返回大写字母的字符串

  • 参数 input:输入字符串

  • 作者:Alex Blewitt

  • 返回值:输入字符串,但为大写

  • 抛出:不抛出错误

  • 注意:请不要大喊大叫

  • 参考信息:String.uppercaseString

  • 自:2015

*/

func shout(input:String) -> String {

return input.uppercaseString

}


当鼠标悬停在`shout`函数上时,将看到以下文档:

![符号文档](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00022.jpeg)

# 游戏场的限制

虽然游戏场可以非常强大地与代码交互,但也有一些值得注意的限制。游戏场中没有调试支持,因此无法添加断点并使用调试器来找出值。

由于 UI 允许跟踪值——并且只需添加新行并跟踪值就非常容易——这并不是什么大问题。游戏场的其他限制包括以下内容:

+   只有模拟器可以用于执行基于 iOS 的游戏场。这防止了使用可能仅在设备上存在的特定硬件功能。

+   游戏场脚本的性能主要基于执行了多少行代码以及调试器保存了多少输出。它不应用于测试对性能敏感的代码的性能。

+   虽然游戏场非常适合展示用户界面组件,但不能用于用户输入。

+   在当前写作时,需要权限(如应用内购买或访问 iCloud)的功能在游戏场中是不可能的。

# 摘要

本章介绍了游乐场,这是一种运行 Swift 代码的创新方式,它通过图形化展示值和运行代码的检查来呈现。表达式和时序都被展示为显示程序在任何时刻状态的方式,以及使用快速查看(Quick Look)图形化检查对象。`XCPlayground` 框架还可以用来记录特定值,并允许异步代码执行。

下一章将探讨如何使用 Swift 创建 iOS 应用程序。


# 第三章。创建 iOS Swift 应用程序

2014 年发布 Xcode 6 之后,已经可以构建适用于 iOS 和 OS X 的 Swift 应用程序,并将它们提交到 App Store 进行发布。本章将介绍单视图应用程序和主从视图应用程序,并使用这些应用程序来解释 iOS 应用程序背后的概念,以及介绍 Swift 中的类。

本章将介绍以下主题:

+   iOS 应用程序的架构

+   单视图 iOS 应用程序

+   在 Swift 中创建类

+   Swift 中的协议和枚举

+   使用 `XCTest` 测试 Swift 代码

+   主从视图 iOS 应用程序

+   `AppDelegate` 和 `ViewController` 类

# 理解 iOS 应用程序

iOS 应用程序是一个编译后的可执行文件,以及一个包含在包中的支持文件集。应用程序包被打包成一个存档文件,用于安装到设备或上传到 App Store。

### 小贴士

Xcode 可以用于在模拟器中运行 iOS 应用程序,以及在本地设备上进行测试。将应用程序提交到 App Store 需要一个开发者签名密钥,该密钥包含在 Apple 开发者计划中,请访问 [`developer.apple.com`](https://developer.apple.com)。

到目前为止,大多数 iOS 应用程序都是用 Objective-C 编写的,它是 C 和 Smalltalk 的交叉语言。随着 Swift 的出现,许多开发者可能会将他们应用程序的至少部分迁移到 Swift,以实现性能和维护。

虽然 Objective-C 可能会存在一段时间,但很明显,Swift 是 iOS 开发的未来,也许也是 OS X 的未来。应用程序包含多种不同类型的文件,这些文件在编译时和运行时都会使用。这些文件包括以下内容:

+   `Info.plist` 文件,其中包含有关应用程序本地化语言的信息,应用程序的身份,以及配置要求,例如支持的界面类型(iPad、iPhone 和通用),以及方向(纵向、颠倒、横屏左和横屏右)

+   零个或多个扩展名为 `.xib` 的 *界面构建器* 文件,这些文件包含用户界面屏幕(这取代了之前的 `.nib` 文件)

+   零个或多个扩展名为 `.xcassets` 的 *图像资源* 文件,这些文件存储不同尺寸的相关图标组,例如应用程序图标或用于屏幕显示的图形(这取代了之前的 `.icns` 文件)

+   零个或多个扩展名为 `.storyboard` 的 *故事板* 文件,这些文件用于协调应用程序中不同的屏幕

+   一个或多个包含应用程序代码的 `.swift` 文件

# 创建单视图 iOS 应用程序

单视图 iOS 应用程序是指应用程序在一个屏幕上展示,没有任何过渡或其他视图。本节将展示如何创建一个不使用故事板的单视图应用程序。(关于使用 Swift 和 iOS 的故事板应用程序,请参阅第四章,*使用 Swift 和 iOS 的故事板应用程序*。)

当 Xcode 启动时,它显示一个包含创建新项目能力的欢迎信息。此欢迎信息可以通过导航到**窗口** | **欢迎使用 Xcode**或通过按*Command* + *Shift* + *1*在任何时候重新显示。

使用欢迎对话框中的**创建一个新的 Xcode 项目**选项,或通过导航到**文件** | **新建** | **项目...**,或通过按*Command* + *Shift* + *N*,创建一个新的**iOS**项目,以**单视图应用程序**作为模板,如图所示:

![创建单视图 iOS 应用程序](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00023.jpeg)

当按下**下一步**按钮时,新项目对话框将要求提供更多详细信息。这里的产品名称是`SingleView`,**组织名称**和**标识符**有适当的值。确保选定的语言是**Swift**,设备类型是**通用**:

![创建单视图 iOS 应用程序](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00024.jpeg)

### 注意

**组织标识符**是组织的反向域名表示,**捆绑标识符**是**组织标识符**与**产品名称**的连接。发布到 App Store 需要**组织标识符**由发布者拥有,并在[`developer.apple.com/membercenter/`](https://developer.apple.com/membercenter/)的在线开发者中心中进行管理。

当按下**下一步**按钮时,Xcode 将询问项目保存的位置以及是否创建仓库。所选位置将用于创建产品目录,并将提供创建 Git 仓库的选项。

### 提示

在 2014 年,Git 成为了最广泛使用的版本控制系统,超过了所有其他分布式和集中式版本控制系统。在创建新的 Xcode 项目时不创建 Git 仓库是愚蠢的。

当按下**创建**按钮时,Xcode 将创建项目,设置模板文件,然后在本地上或共享服务器上初始化 Git 仓库。

在 Xcode 的左上角按下三角形的播放按钮以启动模拟器:

![创建单视图 iOS 应用程序](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00025.jpeg)

如果一切设置正确,模拟器将以白色屏幕启动,并在屏幕顶部显示时间和电池:

![创建单视图 iOS 应用程序](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00026.jpeg)

## 移除故事板

单视图应用的默认模板包含一个*故事板*。这将为第一个(唯一)屏幕创建视图,并在幕后执行一些额外的设置。为了理解发生了什么,故事板将被移除,并用代码代替。

### 注意

大多数应用都是使用一个或多个故事板构建的。这里移除它只是为了演示目的;有关如何使用故事板的更多信息,请参阅第四章,*使用 Swift 和 iOS 的故事板应用*。

可以通过前往项目导航器,找到`Main.storyboard`文件,然后按*Delete*键或从上下文相关菜单中选择**Delete**来删除故事板。当显示确认对话框时,选择**Move to Trash**选项以确保文件被删除,而不是仅仅从 Xcode 所知的文件列表中移除。

### 小贴士

要查看项目导航器,请按*Command* + *1*或导航到**View** | **Navigators** | **Show Project Navigator**。

一旦删除了`Main.storyboard`文件,它需要从`Info.plist`中移除,以防止 iOS 在启动时尝试打开它。打开位于`SingleView`文件夹下的`Supporting`文件夹中的`Info.plist`文件。将显示一组键值对;点击**Main storyboard file base name**行将显示(**+**)和(**-**)选项。点击删除图标(**-**)将移除以下行:

![移除故事板](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/swift-ess-2e/img/00027.jpeg)

现在当应用启动时,将显示一个黑色屏幕。

### 小贴士

Xcode 模板创建了多个`Info.plist`文件;一个文件用于实际应用,而其他文件用于在运行测试时构建的测试应用。测试将在本章后面的*Swift 中的子类和测试*部分中介绍。

## 设置视图控制器

*视图控制器*负责在激活时设置视图。通常,这是通过故事板或界面文件来完成的。由于这些已经被移除,因此需要手动实例化窗口和视图控制器。

当 iOS 应用启动时,`application:didFinishLaunchingWithOptions:`会在相应的`UIApplicationDelegate`上被调用。可选的`window`变量在从界面文件或故事板加载时自动初始化,但如果用户界面是通过代码实现的,则需要显式初始化。

在`AppDelegate`类中实现`application:didFinishLaunchingWithOptions:`方法如下:

```swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  func application(application: UIApplication,
   didFinishLaunchingWithOptions launchOptions:
   [NSObject:AnyObject]?) -> Bool {
    window = UIWindow()
    window?.rootViewController = ViewController()
    window?.makeKeyAndVisible()
    return true
  }
}

小贴士

要按名称打开一个类,请按Command + Shift + O并输入类名。或者,导航到File | Open Quickly...

最后一步是创建视图的内容,这通常在ViewController类的viewDidLoad方法中完成。作为一个示例用户界面,将创建并添加一个UILabel到视图中。每个视图控制器都有一个关联的view属性,可以通过addSubview方法添加子视图。为了使视图突出,将视图的背景改为黑色,并将文字颜色改为白色:

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
 view.backgroundColor = UIColor.blackColor()
 let label = UILabel(frame:view.bounds)
 label.textColor = UIColor.whiteColor()
 label.textAlignment = .Center
 label.text = "Welcome to Swift"

 view.addSubview(label)
  }
}

这将创建一个标签,其大小为整个屏幕的大小,文字颜色为白色,文字对齐方式为居中。运行时,屏幕上会显示欢迎使用 Swift

注意

通常,视图将在它们自己的类中实现,而不是直接内联到视图控制器中。这允许视图在其他控制器中重用。这种技术将在下一章中演示。

当屏幕旋转时,标签将旋转出屏幕。在实际应用中,需要添加逻辑来处理视图控制器中的旋转变化,例如willRotateToInterfaceOrientation,并使用视图的transform属性适当地添加旋转。通常,会使用界面构建器文件或故事板来自动处理这一点。

Swift 类、协议和枚举

几乎所有的 Swift 应用程序都将面向对象。第一章,探索 Swift,以及第二章,玩转 Swift,都展示了函数式和过程式 Swift 代码。使用来自CoreFoundation框架的Process类,以及来自UIKit框架的UIColorUIImage类来展示如何在应用程序中使用类。本节描述了如何在 Swift 中创建类、协议和枚举。

Swift 中的类

在 Swift 中使用class关键字创建类,并使用花括号括起来类体。类体可以包含称为属性的变量,以及称为方法的函数,它们统称为成员。实例成员对每个实例都是唯一的,而静态成员在该类的所有实例之间共享。

类通常定义在以类命名的文件中;因此,GitHubRepository类通常定义在GitHubRepository.swift文件中。可以通过导航到文件 | 新建 | 文件…并选择iOS下的Swift 文件选项来创建一个新的 Swift 文件。确保将其添加到测试UI 测试目标中。创建后,按照以下方式实现类:

class GitHubRepository {
  var id:UInt64 = 0
  var name:String = ""
  func detailsURL() -> String {
    return "https://api.github.com/repositories/\(id)"
  }
}

此类可以按如下方式实例化和使用:

let repo = GitHubRepository()
repo.id = 1
repo.name = "Grit"
repo.detailsURL() // returns https://api.github.com/repositories/1

可以创建静态成员,这些成员对于类的所有实例都是相同的。在GitHubRepository类中,api URL 对于所有调用可能都是相同的,因此它可以重构为一个static属性:

class GitHubRepository {
  // does not work in Swift 1.0 or 1.1
  static let api = "https://api.github.com"
  …
  class func detailsURL(id:String) -> String {
    return "\(api)/repositories/\(id)"
  }
}

现在,如果需要更改api URL(例如,为了支持模拟测试或支持内部 GitHub Enterprise 服务器),只需在一个地方进行更改。在 Swift 2 之前,可能会显示一个错误信息类变量尚未支持

在 Swift 版本 2 之前使用静态变量,必须使用不同的方法。可以定义计算属性,这些属性不是存储的,而是在需要时计算。它们有一个getter(也称为访问器)和可选的setter(也称为修改器)。前面的例子可以重写如下:

class GitHubRepository {
  class var api:String {
    get {
      return "https://api.github.com"
    }
  }
  func detailsURL() -> String {
    return "\(GitHubRepository.api)/repositories/\(id)"
  }
}

虽然这在逻辑上是一个只读常量(没有相关的set块),但无法使用访问器定义let常量。

要引用类变量,使用类型名——在这个例子中是GitHubRepository。当GitHubRepository.api表达式被评估时,会调用 getter 的主体。

Swift 中的子类和测试

没有显式父类的简单 Swift 类被称为基类。然而,Swift 中的类经常通过在类名后指定一个超类来继承另一个类。这种语法的格式是class SubClass:SuperClass{...}

Swift 中的测试使用XCTest框架编写,该框架默认包含在 Xcode 模板中。这允许应用程序在本地编写和执行测试,以确认没有引入错误。

小贴士

XCTest 取代了之前的测试框架 OCUnit。

XCTest框架有一个基类叫做XCTestCase,所有测试都继承自这个类。在测试用例类中,以test开头(且不带参数)的方法在运行测试时会被自动调用。测试代码可以通过调用XCTAssert*函数,如XCTAssertEqualsXCTAssertGreaterThan,来指示成功或失败。

GitHubRepository类的测试通常存在于相应的GitHubRepositoryTest类中,它将是XCTestCase的子类。通过导航到文件 | 新 | 文件...并选择类别下的iOS中的Swift 文件来创建一个新的 Swift 文件。确保选择TestsUITests目标,但不选择应用程序目标。它可以实现如下:

import XCTest
class GitHubRepositoryTest: XCTestCase {
  func testRepository() {
    let repo = GitHubRepository()
    repo.id = 1
    repo.name = "Grit"
    XCTAssertEqual(
      repo.detailsURL(),
      "https://api.github.com/repositories/1",
      "Repository details"
    )
  }
}

确保将GitHubRepositoryTest类添加到测试目标中。如果文件创建时没有添加,可以通过选择文件并按Command + Option + 1来显示文件检查器。测试目标旁边的复选框应该被选中。测试永远不会添加到主目标中。GitHubRepository类应该添加到两个测试目标中:

Swift 中的子类和测试

GitHubDetails协议可以在与现有 Swift 类型相同的位置用作类型,例如变量类型、方法返回类型或参数类型。

注意

总是检查失败的测试是否会导致构建失败;这将确认测试实际上正在运行。例如,在GitHubRepositoryTest类中,修改 URL 以从前面删除https并检查是否显示测试失败。没有比正确实现但从未运行过的测试更无用的了。

在 Swift 中需要理解的最后一种概念是枚举,或简称为enum。枚举是一组封闭的值,例如NorthEastSouthWest,或者UpDown

协议在 Swift 中被广泛使用,以允许来自框架的回调,否则这些框架不知道特定的回调处理程序。如果需要超类,则单个类不能用于实现多个回调。常见的协议包括UIApplicationDelegatePrintableComparable

注意

只有当协议被标记为@objc属性时,才支持可选协议方法。这表示该类将由NSObject类支持,以便与 Objective-C 进行互操作性。纯 Swift 协议不能有可选方法。

在 Swift 中需要理解的最后一种概念是枚举,或简称为enum。枚举是一组封闭的值,例如NorthEastSouthWest,或者UpDown

protocol GitHubDetails {
  func detailsURL() -> String
  // protocol needs @objc if using optional protocols
  // optional doNotNeedToImplement()
}

当一个类既有超类又有一个或多个协议时,必须首先列出超类。

协议不能有默认参数的函数。除非使用了@objc类属性,否则协议可以与structclassenum类型一起使用;在这种情况下,它们只能用于 Objective-C 类或枚举。

Swift 中的协议

小贴士

当通过按Command + U或导航到产品 | 测试来运行测试时,测试的结果将显示出来。更改实现或预期的测试结果将演示测试是否正确执行。

class GitHubRepository: GitHubDetails {
  func detailsURL() -> String {
    // implementation as before
  }
}

协议在其他语言中类似于接口;它是一个具有方法签名但没有方法实现的命名类型。类可以实现零个或多个协议;当它们这样做时,它们被称为采用符合协议。协议可能有一系列方法,这些方法是必需的(默认)或可选的(用optional关键字标记)。

注意

类通过在类名后列出协议名称来符合协议,类似于超类。

小贴士

Swift 中的枚举

Swift 中的枚举

enum Suit {
  case Clubs, Diamonds, Hearts // many on one line
  case Spades // or each on separate lines
}

与 C 不同,枚举值默认没有特定类型,因此它们通常不能转换为整数值。枚举可以定义具有原始值,允许转换为整数值。枚举值使用类型名和enum名称分配给变量:

var suit:Suit = Suit.Clubs

然而,如果已知表达式的类型,则不需要显式指定类型前缀;在 Swift 代码中,以下形式更为常见:

var suit:Suit = .Clubs

原始值

对于具有特定意义的 enum 值,可以从不同的类型扩展 enum,例如 Int。这些被称为原始值

enum Rank: Int {
  case Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten
  case Jack, Queen, King, Ace
}

可以使用 rawValue 属性和可失败初始化器 Rank(rawValue:) 将原始值 enum 转换为其原始值,如下所示:

Rank.Two.rawValue == 2
Rank(rawValue:14)! == .Ace

提示

可失败初始化器返回一个可选的 enum 值,因为等效的 Rank 可能不存在。例如,表达式 Rank(rawValue:0) 将返回 nil

关联值

enum 还可以有关联值,例如其他语言中的值或案例类。例如,可以将 SuitRank 的组合组合起来形成 Card

enum Card {
  case Face(Rank, Suit)
  case Joker
}

可以通过将值传递给 enum 初始化器来创建实例:

var aceOfSpades: Card = .Face(.Ace,.Spades)
var twoOfHearts: Card = .Face(.Two,.Hearts)
var theJoker: Card = .Joker

一个 enum 实例的关联值不能被提取(如 struct 的属性那样),但可以通过 switch 语句中的模式匹配来访问 enum 值:

var card = aceOfSpades // or theJoker or twoOfHearts ...
switch card {
  case .Face(let rank, let suit): 
    print("Got a face card \(rank) of \(suit)");
  case .Joker: 
    print("Got the joker card")
}

Swift 编译器将要求 switch 语句是详尽的。由于 enum 只包含这两种类型,因此不需要 default 块。如果将来在 Card 中添加了另一个 enum 值,编译器将在该 switch 语句中报告错误。

创建 master-detail iOS 应用程序

在了解了 Swift 中如何定义类、协议和 enum 之后,可以创建一个更复杂的 master-detail 应用程序。master-detail 应用程序是一种特定的 iOS 应用程序,它最初显示主表视图,当选择一个单独的元素时,将显示一个次要的详细信息视图,显示有关所选项目的更多信息。

使用欢迎屏幕上的创建新 Xcode 项目选项,或通过导航到文件 | 新建 | 项目…或按Command + Shift + N,创建一个新项目,并从iOS 应用类别中选择Master-Detail 应用程序

创建 master-detail iOS 应用程序

在随后的对话框中,输入项目的适当值,例如名称(MasterDetail)、组织标识符(通常基于反向 DNS 名称),确保语言下拉菜单读取为Swift,并且它针对通用设备:

创建 master-detail iOS 应用程序

当项目创建时,一个包含由向导本身创建的所有文件的 Xcode 窗口将打开,包括 MasterDetail.appMasterDetailTests.xctest 产品。MasterDetail.app 是一个由模拟器或连接的设备执行的包,而 MasterDetailTests.xctestMasterDetailsUITests.xctest 产品用于执行应用程序代码的单元测试。

可以通过按 Xcode 左上角的三角形播放按钮或按 Command + R 来启动应用程序,这将针对当前选定的目标运行应用程序。

创建一个主详情 iOS 应用程序

在简短的编译和构建周期之后,iOS 模拟器将打开一个包含空表的首页,如下面的截图所示:

创建一个主详情 iOS 应用程序

默认的 MasterDetail 应用程序可以通过点击屏幕右上角的添加(+)按钮来向列表中添加项目。这将向列表中添加一个新的带时间戳的条目。

创建一个主详情 iOS 应用程序

当点击此项目时,屏幕将切换到详情视图,在这种情况下,屏幕中央显示时间:

创建一个主详情 iOS 应用程序

这种主详情应用在 iOS 应用程序中很常见,用于显示顶层列表(如购物清单、一组联系人、待办笔记等),同时允许用户点击查看详情。

主详情应用程序中有三个主要类:

  • AppDelegate 类定义在 AppDelegate.swift 文件中,它负责启动应用程序并设置初始状态

  • MasterViewController 类定义在 MasterViewController.swift 文件中,它用于管理第一个(主)屏幕的内容和交互

  • DetailViewController 类定义在 DetailViewController.swift 文件中,它用于管理第二个(详情)屏幕的内容

为了更详细地了解这些类的作用,接下来的三个部分将依次介绍它们。

提示

本节生成的代码是从 Xcode 7.0 创建的,因此如果使用不同版本的 Xcode,模板可能会有所不同。可以从 Packt 网站或本书的 GitHub 仓库 github.com/alblue/com.packtpub.swift.essentials/ 获取相应的代码的精确副本。

AppDelegate

AppDelegate 类是应用程序的主要入口点。当一组 Swift 源文件被编译时,如果存在 main.swift 文件,它将作为应用程序的入口点运行该代码。然而,为了简化 iOS 应用程序的设置,存在一个 @UIApplicationMain 特殊属性,它将生成 main 方法并设置关联的类作为应用程序代理。

iOS 的 AppDelegate 类扩展了 UIResponder 类,它是 iOS 上所有 UI 内容的父类。它还采用了两个协议,UIApplicationDelegateUISplitViewControllerDelegate,用于在发生某些事件时提供回调:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,
   UISplitViewControllerDelegate {
  var window: UIWindow?
  ...
}

注意

在 OS X 上,AppDelegate 类将是 NSApplication 的子类,并采用 NSApplicationDelegate 协议。

合成的 main 函数调用 UIApplicationMain 方法,该方法读取 Info.plist 文件。如果存在 UILaunchStoryboardName 键,并且指向一个合适的文件(在这种情况下是 LaunchScreen.xib 接口文件),它将在进行任何进一步工作之前显示为启动画面。在应用程序的其余部分加载完毕后,如果存在 UIMainStoryboardFile 键,并且指向一个合适的文件(在这种情况下是 Main.storyboard 文件),则启动故事板并显示初始视图控制器。

故事板引用了 MasterViewControllerDetailViewController 类。window 变量被分配给故事板的窗口。

当应用程序启动后,会调用 application:didFinishLaunchingWithOptions 方法。该方法通过一个指向 UIApplication 实例的引用和一个字典传递,字典通知应用程序是如何启动的:

func application(
 application: UIApplication,
 didFinishLaunchingWithOptions launchOptions:
  [NSObject: AnyObject]?) -> Bool {
  // Override point for customization after application launch.
  ...
}

在示例 MasterDetail 应用程序中,application:didFinishLaunchingWithOptions 方法从显式解包的 window 中获取对 splitViewController 的引用,并将 AppDelegate 设置为其代理:

let splitViewController = 
 self.window!.rootViewController as! UISplitViewController
splitViewController.delegate = self

小贴士

… as! UISplitViewController 语法执行类型转换,以便可以将泛型 rootViewController 赋值给更具体的类型;在这种情况下,UISplitViewController。一个替代版本 as? 提供了运行时检查的转换,并返回一个可选值,该值要么包含正确转换的类型值,要么在否则返回 nil。与 as! 的区别是,如果项目不是正确的类型,将发生运行时错误。

最后,从 splitViewController 获取一个 navigationController,它存储了一个 viewControllers 数组。这允许 DetailView 在必要时在左侧显示一个按钮以展开详细视图:

let navigationController = splitViewController.viewController
 [splitViewController.viewControllers.count-1]
 as! UINavigationController
navigationController.topViewController
 .navigationItem.leftBarButtonItem =
 splitViewController.displayModeButtonItem()

这唯一的区别在于在宽屏设备上运行时,例如 iPhone 6 Plus 或 iPad,在横向模式下视图并排显示。这是 iOS 8 应用程序中的新功能。

AppDelegate 类

否则,当设备处于纵向模式时,它将渲染为一个标准的后退按钮:

AppDelegate 类

该方法以 return true 结束,以让操作系统知道应用程序已成功打开。

MasterViewController

MasterViewController 类负责协调在第一屏(当设备处于纵向模式时)或屏幕左侧(当大设备处于横向模式时)显示的数据。这是通过 UITableView 渲染的,并且数据通过父 UITableViewController 类进行协调:

class MasterViewController: UITableViewController {
  var detailViewcontroller: DetailViewController? = nil
  var objects = [AnyObject]()
  override func viewDidLoad() {…}
  func insertNewObject(sender: AnyObject) {…}
  …
}

viewDidLoad 方法用于在视图加载后设置或初始化视图。在这种情况下,创建了一个 UIBarButtonItem,以便用户可以向表中添加新条目。UIBarButtonItem 在 Objective-C 中使用 @selector,在 Swift 中被视为可转换为字符串字面量的(因此 "insertNewObject:" 将导致调用 insertNewObject 方法)。一旦创建,按钮就被添加到右侧的导航中,使用标准的 .Add 类型,它将在屏幕上显示为 + 号:

override func viewDidLoad() {
  super.viewDidLoad()
  self.navigationItem.leftBarButtonItem = self.editButtonItem()
  let addButton = UIBarButtonItem(
    barButtonSystemItem: .Add, target: self, 
    action: "insertNewObject:")
  self.navigationItem.rightBarButtonItem = addButton
  if let split = self.splitViewController {
    let controllers = split.viewControllers
    self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}

这些对象是 NSDate 值,作为 AnyObject 元素的数组存储在类中。当按下 + 按钮时调用 insertNewObject 方法,它创建一个新的 NSDate 实例,然后将其插入到数组中。sender 事件作为 AnyObject 类型的参数传递,它将是一个对 UIBarButtonItem 的引用(尽管在这里不需要或使用它):

func insertNewObject(sender: AnyObject) {
  objects.insertObject(NSDate.date(), atIndex: 0)
  let indexPath = NSIndexPath(forRow: 0, inSection: 0)
  self.tableView.insertRowsAtIndexPaths(
   [indexPath], withRowAnimation: .Automatic)
}

备注

在 iOS 设备上可用 blocks 之前,就创建了 UIBarButtonItem 类,因此它使用较旧的 Objective-C @selector 机制。iOS 的未来版本可能提供一个接受 block 的替代方案,在这种情况下,可以传递 Swift 函数。

父类包含对 tableView 的引用,该引用由 storyboard 自动创建。当插入一个项目时,tableView 被通知有一个新的对象可用。使用标准的 UITableViewController 方法从数组中访问数据:

override func numberOfSectionsInTableView(
 tableView: UITableView) -> Int {
  return 1
}
override func tableView(tableView: UITableView,
 numberOfRowsInSection section: Int) -> Int {
  return objects.count
}
override func tableView(tableView: UITableView,
 cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
  let cell = tableView.dequeueReusableCellWithIdentifier(
   "Cell", forIndexPath: indexPath)
  let object = objects[indexPath.row] as! NSDate
  cell.textLabel!.text = object.description
  return cell
}
override func tableView(tableView: UITableView,
 canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
  return true
}

在这个例子中,numberOfSectionsInTableView 函数返回 1,但 tableView 可以有多个分区;例如,为了允许联系人应用程序有 A、B、C 到 Z 的不同分区。numberOfRowsInSection 方法返回每个分区的元素数量;在这种情况下,因为只有一个分区,所以数组中的对象数量。

备注

每个方法之所以被称为 tableView 并接受一个 tableView 参数,是因为 UIKit 的 Objective-C 继承的结果。Objective-C 中的约定将方法名作为第一个命名参数,因此原始方法是 [delegate tableView:UITableView, numberOfRowsInSection:NSInteger]。因此,第一个参数的名称被重用作 Swift 中方法的名称。

cellForRowAtIndexPath 方法预期返回一个 UITableViewCell 对象。在这种情况下,通过使用 dequeueReusableCellWithIdentifier 方法(该方法在单元格离开屏幕时缓存单元格以节省对象实例化)从 tableView 中获取一个单元格,然后使用对象的 description(这是对象的 String 表示形式;在这种情况下,是日期)填充 textLabel

这足以在表中显示元素,但为了允许编辑(或只是删除,如示例应用程序中所示),还需要一些额外的协议方法:

override func tableView(tableView: UITableView,
 canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
  return true
}
override func tableView(tableView: UITableView,
 commitEditingStyle editingStyle: UITableViewCellEditingStyle,
 forRowAtIndexPath indexPath: NSIndexPath) {
  if editingStyle == .Delete {
    objects.removeObjectAtIndex(indexPath.row)
    tableView.deleteRowsAtIndexPaths([indexPath],
     withRowAnimation: .Fade)
  }
}

canEditRowAtIndexPath 方法如果行可编辑则返回 true;如果所有行都可以编辑,则对于所有值都将返回 true

commitEditingStyle 方法接受一个表格、一个路径和一个样式,这是一个枚举,表示发生了哪个操作。在这种情况下,传入 UITableViewCellEditingStyle.Delete 以从底层对象数组以及从 tableView 中删除项目。(枚举可以缩写为 .Delete,因为 editingStyle 的类型已知为 UITableViewCellEditingStyle。)

DetailViewController

MasterViewController 中选择元素时,将显示详细视图。过渡由故事板控制器管理;视图通过 segue(发音为 seg-way;基于意大利语中 follows 一词的 segue 一词的产物)连接。

要在控制器之间传递选定的项目,DetailViewController 类中存在一个名为 detailItem 的属性。当值更改时,将运行额外的代码,这通过 didSet 属性通知实现:

class DetailViewController: UIViewController {
  var detailItem: AnyObject? {
    didSet {
      self.configureView()
    }
  }
  … 
}

DetailViewController 设置了 detailItem 时,将调用 configureView 方法。在值更改后,didSet 主体将运行,但在设置器返回调用者之前。这是由 MasterViewController 中的 segue 触发的:

class MasterViewController: UIViewController {
  …
  override func prepareForSegue(
   segue: UIStoryboardSegue, sender: AnyObject?) {
    super.prepareForSegue(segue, sender: sender)
    if segue.identifier == "showDetail" {
      if let indexPath = 
       self.tableView.indexPathForSelectedRow() {
        let object = objects[indexPath.row] as! NSDate
        let controller = (segue.destinationViewController 
         as! UINavigationController)
         .topViewController as! DetailViewController
        controller.detailItem = object
        controller.navigationItem.leftBarButtonItem =
         self.splitViewController?.displayModeButtonItem()
        controller.navigationItem.leftItemsSupplementBackButton =
         true
      }
    } 
  }
}

当用户在表格中选择一个项目时,将调用 prepareForSegue 方法。在这种情况下,它从表格中获取选定的行索引,并使用此索引获取选定的日期对象。搜索导航控制器层次结构以获取 DetailViewController,一旦获取到,就使用 controller.detailItem = object 设置选定的值,这触发了更新。

标签最终通过 configureView 方法在 DetailViewController 中显示,该方法将对象的 description 打印到中心的 label 上:

class DetailViewController {
  ...
  @IBOutlet weak var detailDescriptionLabel: UILabel!
  function configureView() {
    if let detail: AnyObject = self.detailItem {
      if let label = self.detailDescriptionLabel {
        label.text = detail.description
      }
    }
  }
}

detailItem 发生变化或首次加载视图时,将调用 configureView 方法。如果 detailItem 未设置,则此操作没有效果。

实现引入了一些新的概念,这些概念值得强调:

  • @IBOutlet 属性表示该属性将在界面构建器中公开,并可以连接到对象实例。这将在第四章、使用 Swift 和 iOS 的 Storyboard 应用程序和第五章、在 Swift 中创建自定义视图中更详细地介绍。

  • weak 属性表示该属性不会存储对象的 strong 引用;换句话说,详情视图不会拥有该对象,而只是引用它。通常,所有 @IBOutlet 引用都应该声明为 weak 以避免循环依赖引用。

  • 类型定义为 UILabel!,这是一个 隐式解包的可选类型。当访问时,它会对可选值执行显式解包;否则,@IBOutlet 将被连接为一个 UILabel? 可选类型。隐式解包的可选类型用于变量在运行时已知永远不会是 nil 的情况,这对于 @IBOutlet 引用通常是这种情况。通常,所有 @IBOutlet 引用都应该使用隐式解包的可选类型。

摘要

本章介绍了两个 iOS 应用程序的示例;一个是通过编程创建 UI 的,另一个是从故事板中加载 UI 的。结合对类、协议和枚举的概述,以及解释 iOS 应用程序是如何启动的,本章为理解经常用于启动新项目的 Xcode 模板提供了一个起点。

下一章,使用 Swift 和 iOS 的故事板应用,将更详细地介绍故事板的创建方式以及如何从头开始构建一个应用程序。

第四章. 使用 Swift 和 iOS 的 Storyboard 应用

Storyboard 首先在 Xcode 4.2 和 iOS 5.0 中引入。Storyboard 解决了在 iOS 应用程序中图形化展示屏幕流程的问题,并且还提供了一种方法,可以在一个地方而不是多个单独的 xib 文件中编辑这些屏幕的内容。Storyboard 与 Swift 的工作方式与 Objective-C 相同,Swift 和 storyboard 部分展示了如何将 Swift 代码与 storyboard 切换集成。

本章将介绍以下主题:

  • 如何创建 storyboard 项目

  • 创建多个场景

  • 使用切换在场景之间导航

  • 编写自定义视图控制器

  • 在 Swift 中将视图连接到输出口

  • 使用自动布局排列视图

  • 使用约束构建可调整大小的视图

Storyboard、场景和切换

默认情况下,Xcode 7 为新创建的 iOS 项目创建一个 Main.storyboard 文件,而不是 MainWindow.xib 文件。Info.plist 文件中的 UIMainStoryboardFile 键指向应用程序的主 storyboard 名称(不带扩展名)。当应用程序启动时,将加载 Main.storyboard 文件而不是 NSMainNib 条目。Xcode 的早期版本允许开发者选择是否使用 storyboard,但 Xcode 7 中,storyboard 是默认选项,开发者无法轻易选择退出。尽管如此,仍然可以使用 xib 文件为应用程序的各个部分或用于加载原型表格单元格的自定义类。此外,Xcode 7 创建一个 LaunchScreen.storyboard 文件,在应用程序加载时显示为启动画面(在 iOS 8 及更高版本上),优先于固定分辨率的预渲染屏幕。这允许具有许多不同分辨率的设备(包括未来未宣布的设备)渲染像素完美的启动画面,而无需为每个新设备尺寸渲染不同分辨率的画面。

Storyboard 是由 segues(发音为 seg-ways)连接的 场景(独立的屏幕)集合。每个场景由一个 视图控制器 表示,它有一个相关的 视图。切换通过可定制的用户界面过渡(如滑动或淡入淡出)在不同场景之间进行转换,并且可以从 UI 控件或以编程方式触发。

创建 storyboard 项目

由于 Xcode 7 的默认模板默认使用故事板,因此任何模板都将工作。实际上,每个应用程序模板都设置了一个特定的视图控制器和模板代码。最简单且易于定制的工作模板是单视图应用程序,可以通过导航到文件 | 新建 | 项目…来选择。创建一个名为 Storyboards 的项目,使用单视图应用程序,以实验本章内容。(有关如何创建新应用程序的更多详细信息,请参阅第三章中的创建单视图 iOS 应用程序部分,创建 iOS Swift 应用。)

场景和视图控制器

标准视图控制器可以用来构建应用程序,包括以下内容:

  • 使用 UISplitViewController 类的分割视图,该类可以包含以下任何一种,但不能嵌入到任何其他视图控制器中

  • 使用 UITabBarController 类的标签视图,该类可以包含以下任何一种,但只能嵌入到分割视图中或用作根控制器

  • 可以使用 UINavigationController 类向现有控制器添加导航控件,该类可以包含以下任何一种,并且可以嵌入到前面的任何一种或用作根视图控制器

  • 使用 UIPageViewController 类的翻页视图,该类提供滑动和翻页显示选项

  • 使用 UITableViewController 类的表格视图

  • 使用 UICollectionViewController 类的网格视图

  • 使用 AVPlayerViewController 类的音频-视频内容

  • 使用 GLKViewController 类的 OpenGL ES 内容

  • 使用 UIViewController 类或自定义子类创建自定义控制器内容

这些类可以混合使用,但必须遵循明确的顺序以满足苹果的人类界面指南(也称为HIG)。这些都是可选的,但如果组合使用,则需要遵守此顺序:

场景和视图控制器

除了标准视图控制器类之外,还可以使用自定义子类。这将在本章后面的“自定义视图控制器”部分中更详细地介绍。

向场景添加视图

可以通过在项目导航器中单击文件来打开 Main.storyboard 文件。将打开一个编辑器,它将故事板显示为一系列场景,同时在左侧显示文档大纲。在单页应用程序中,只有一个视图控制器存在。

向场景添加视图

视图控制器左侧的箭头表示此场景是 初始视图控制器。这也可以通过选择场景中的 视图控制器 并导航到 属性检查器(转到 视图 | 实用工具 | 显示属性检查器,或按 Command + Option + 4)来设置。也可以通过拖放箭头指向不同的场景来将初始视图控制器更改为不同的场景。

通过从 Xcode 右下角的 对象库 中拖放来添加视图。可以通过导航到 视图 | 实用工具 | 显示对象库,或者按 Command + Option + Control + 3 来显示对象库。点击一个视图,例如 标签,然后将其拖放到视图中:

将视图添加到场景

可以通过在视图中双击标签并输入或通过选择对象并在属性检查器中编辑文本属性来修改标签的文本内容:

将视图添加到场景

当元素被拖动时,可能会显示蓝色引导线。它们建议视图的位置;标准做法是在视图和屏幕边缘之间保持 20pt 的间隔,在相邻视图之间保持 8pt 的间隔。

欢迎使用 Swift 标签拖放到场景的左上角,然后从对象库中拖放一个 按钮 到场景中。将按钮的标题重命名为 按我。此按钮应与标签保持标准空间(8pt)的距离,并对齐在基线(文本自然坐落的水平)上。

将视图添加到场景

注意

在此阶段,视图中的文本在用户界面文件中是硬编码的,对齐是手动的,这意味着如果修改父视图,视图将不会调整大小。这些问题将在本章后面的 将视图连接到 Swift 中的出口使用自动布局 部分中解决。

要在模拟器中查看故事板,请点击顶部的 播放 按钮,或按 Command + R 来运行应用程序。应该会显示一个包含 欢迎使用 Swift按我 的窗口。在此阶段,按按钮将没有效果,这将在下一节中修复。

转场

转场 是在故事板中转到不同场景的过渡。转场可以连接到屏幕上的视图,也可以通过代码触发。最常见的过渡是当用户在用户界面中选择了视图,例如按钮、表格行或详情图标时,会显示新的场景。

为了演示过渡,需要一个新场景。从对象库中拖动一个View Controller并将其拖放到故事板中。视图控制器的确切位置无关紧要,但传统上,场景按照它们将被查看的顺序从左到右组织,因此建议将其拖放到现有视图控制器的右侧,如下面的截图所示:

过渡

一旦添加了View Controller,将标签拖到左上角并将文本更改为请不要再按此按钮。这将提供一个视觉线索,表明当跟随过渡时屏幕已更改。

现在,选择Press Me按钮,在拖动鼠标的同时按住Control键到新创建的视图控制器。当鼠标按钮释放时,将显示一个弹出菜单,其中包含多个选项,这些选项被分组为动作过渡非自适应动作过渡。前者是首选的;后者仅用于向后兼容,未来可能会被移除。

过渡

小贴士

或者,可以从左侧的文档大纲中选择该对象,并将其拖到文档大纲下的对象。可以从编辑器区域中的视图拖动到文档大纲中的对象,反之亦然。将拖动到文档大纲有时更快、更准确,尤其是在故事板中有多个场景时。可以通过导航到编辑器 | 显示文档大纲来显示文档大纲,如果它不可见,或者通过点击编辑器左下角的图标。

选择显示选项,将在两个视图之间创建一个过渡。这表示为连接它们的箭头和文档大纲中的另一个对象。圆形过渡线内的图标显示了将发生的过渡类型;推送将有一个指向左边的箭头,而模态显示将表示为一个方形框。弹出类型将在过渡中显示一个小弹出图标。

过渡

在模拟器中运行应用程序并点击Press Me按钮。应该会弹出一个窗口并显示第二条消息。

注意

将无法关闭或退出第二个屏幕。这是故意的,将在下一节中修复。

添加导航控制器

当有多个屏幕需要显示时,需要一个父控制器来跟踪当前显示哪个屏幕以及下一步(或上一步)是什么。这就是导航控制器的目的;尽管它没有直接的可视表示,但在故事板中它被表示为一个场景,并且可以影响故事板中各个元素的位置布局。

要将初始场景嵌入到导航控制器中,选择初始视图,然后转到编辑器 | 嵌入 | 导航控制器。这将创建一个新的导航控制器视图并将其放置在第一个场景的左侧。它还将初始视图控制器更改为导航控制器,并在导航控制器和第一个场景(由一个类似百分符号的图标表示,但线条方向相反)之间设置名为root view controller关系转换

添加导航控制器

有必要将标签和按钮移动到新添加的导航栏下方,以便它们仍然可见。这可以在引入导航控制器之前完成,或者通过选择重叠的对象来完成。

要暂时隐藏导航栏,删除导航控制器和欢迎场景之间的关系转换,导航栏将消失。这将允许暂时选择并移动对象到其他地方,以便重新定位。要再次添加它,按住Control键并将鼠标光标从导航控制器拖动到欢迎场景,并在关系转换下选择root view controller;或者,在属性检查器中将顶栏属性设置为

或者,要选择重叠的对象,首先在文档大纲中选择对象,以便显示拖动框的位置。然后,按住Shift键并右键单击它,以在任何深度显示鼠标位置下的对象弹出菜单。从这里,可以选择对象,然后使用箭头键将其移动到其他位置。

添加导航控制器

现在当应用程序运行并点击Press Me按钮时,消息将再次显示,但还会显示一个< 返回导航菜单项,如下所示:

添加导航控制器

命名场景和视图

当处理许多场景时,将它们全部称为视图控制器场景没有帮助。为了区分它们,可以在故事板编辑器中重命名控制器。

要更改场景的名称,请在文档大纲中选择其视图控制器,然后转到视图 | 实用工具 | 显示属性检查器,或者按Command + Option + 3,然后钻到文档部分,其中标签提示将显示为文档标签。输入另一个值,例如Press MeMessageInitial,将重命名文档大纲中的视图控制器和场景:

命名场景和视图

小贴士

默认情况下,文档大纲中元素的名称取自元素的文本值或如果没有文本值则取类型。这意味着标签或按钮文本的更新将自动反映在大纲中。然而,可以在文档大纲中的任何视图中添加文档标签。

Swift 和故事板

到目前为止,本章中的故事板内容不涉及任何 Swift 或其他编程内容——它使用了故事板编辑器的拖放功能。幸运的是,使用自定义视图控制器(custom view controller)集成 Storyboard 和 Swift 非常容易。

自定义视图控制器

每个标准视图控制器都有一个相应的超类(在本章之前提到的场景和视图控制器部分中列出)。这可以被替换为自定义子类,然后它就有能力影响和改变用户界面中发生的事情。要替换Message Scene中的消息,创建一个名为MessageViewCotroller.swift的新文件,并包含以下内容:

import UIKit
class MessageViewController: UIViewController {
}

创建了这个类之后,可以通过在故事板中选择它,然后通过导航到View | Utilities | Show Identity Inspector或按Command + Option + 3来切换到身份检查器来将其与视图控制器关联。在Custom Class部分,Class将显示UIViewController作为提示。在这里输入MessageViewController将自定义控制器与视图控制器关联:

自定义视图控制器

这将对消息场景没有明显的影响;运行应用程序将与之前相同。要显示差异,创建一个带有override关键字的viewDidLoad方法,然后创建一个随机的背景颜色,如下所示:

override func viewDidLoad() {
  super.viewDidLoad()
  let red = CGFloat(drand48())
  let green = CGFloat(drand48())
  let blue = CGFloat(drand48())
  view.backgroundColor = UIColor(
    red:red,
    green:green,
    blue:blue,
    alpha:1.0
  )
}

运行应用程序并按下Press Me按钮会导致每次创建不同颜色的视图。

提示

这并不展示良好的用户体验,但在这里使用它来展示每次发生转场时都会调用viewDidLoad的事实。它通常用于在向用户显示视图之前设置视图状态。

在 Swift 中将视图连接到出口

每个视图控制器都有一个与其视图的隐式关系,每个视图都有自己的backgroundColor属性。这个例子将适用于任何视图。如果视图控制器需要以某种方式与视图的内容交互呢?视图控制器可以程序性地遍历视图,寻找特定类型的视图或具有特定标识符的视图,但有一种更好的方法来做这件事。

接口构建器和故事板都有出口(outlets)的概念,它们是在类中的一个预定义点,可以公开并可以在 UI 和代码之间建立连接。在 Objective-C 中,这是通过IBOutlet限定符来实现的。在 Swift 中,这是通过@IBOutlet属性来实现的。实际上,它们是可以绑定到 UI 的变量。

注意

当定义一个具有@IBOutlet属性的类时,@objc属性也会隐式添加,标记这个 Swift 类使用 Objective-C 运行时。由于所有UIKit类已经是 Objective-C 类型,这并不重要;但对于不应该使用 Objective-C 运行时的类型,在添加属性时,如@IBOutlet,应小心谨慎。@objc属性也可以用于需要使用 Objective-C 运行时的非 UI 类。

创建 Swift 视图控制器中的输出需要以下步骤:

  1. 在视图控制器代码中使用@IBOutlet weak var定义一个可选类型的输出。

  2. 通过按Control并从视图拖动鼠标光标到输出,将视图控制器中的输出连接到视图。

要这样做,请按Command + Option + Enter或转到视图 | 辅助编辑器 | 显示辅助编辑器来打开辅助编辑器。这将显示关联源文件的并排视图。这对于显示故事板中选定的视图的关联自定义视图控制器非常有用(或接口文件)。

一旦显示辅助编辑器,从故事板中打开消息场景,按Control并从消息标签拖动鼠标光标到辅助编辑器,并在类声明后释放鼠标。

在 Swift 中将视图连接到输出

一个弹出对话框将询问如何命名字段并显示一些其他信息;确保选择输出,命名为message,并确保它具有存储类型:

在 Swift 中将视图连接到输出

这将在MessageViewController类中添加以下行,并将标签连接到属性如下:

class MessageViewController: UIViewController {
  @IBOutlet weak var message: UILabel!
  … 
}

@IBOutlet属性(在UIKit中定义)允许界面构建器绑定到属性。存储类型——可以在弹出对话框中更改——表示此类不会持有对象的强引用,因此当视图被关闭时,控制器不会拥有它。

小贴士

通常,所有@IBOutlet连接都应标记为weak,因为故事板或xib文件是对象的拥有者,而不是控制器。从界面构建器分配属性时,所有权不会传递。将其更改为其他类型可能导致循环引用。由于 Swift 使用引用计数方法来确定对象何时不再被引用,强引用之间的循环引用可能导致内存泄漏。

类型 UILabel! 末尾的感叹号表示它是一个隐式展开的可选类型。这个属性以可选类型存储,但在使用时访问器代码会自动展开它。由于视图控制器在初始化时不会有对 message 的引用,它将是 nil,因此必须存储为可选类型。然而,由于在视图加载后知道值不是 nil,隐式展开的可选类型节省了每次使用时都会使用的 ?. 调用。

注意

隐式展开的可选类型在底层仍然是一个可选值;每次访问值时在使用的点展开它是语法糖。当视图加载时,但在调用 viewDidLoad 方法之前,出口的值将被连接到屏幕上的实例化视图。

这些连接可以在连接检查器中看到,可以通过选择消息标签并按 Command + Option + 6 或通过导航到 视图 | 实用工具 | 显示连接检查器 来显示检查器。检查器还可以用来删除现有连接或添加新的连接。

在 Swift 中将视图连接到出口

现在消息视图和自定义控制器之间已经建立了连接,而不是更改视图的背景颜色,而是更改 message 的背景颜色,如下所示:

message.backgroundColor = UIColor(...)

运行应用程序,每次场景显示时消息的背景颜色都会改变:

在 Swift 中将视图连接到出口

从界面构建器调用动作

与界面构建器中的出口变量一样,动作是可以从界面构建器中的视图触发的函数/方法。@IBAction 属性用于注释可以连接的方法或函数。

注意

@IBOutlet 类似,在函数上使用 @IBAction 会导致编译器隐式地向类添加一个 @objc 属性,以强制它使用 Objective-C 运行时。

要在按钮被调用时更改消息,需要一个合适的 changeMessage。从历史上看,动作方法的签名是一个返回 void、标记为 IBAction 并接受 sender 参数的方法,该参数可以是任何对象。在 Swift 中,这个签名转换为以下形式:

@IBAction func changeMessage(sender:AnyObject) { … }

然而,在 Swift 中,sender 已不再是必需的参数。因此,可以绑定以下签名的动作:

@IBAction func changeMessage() { … }

如果更改签名,必须删除并重新创建任何现有绑定,否则将报告错误。

提示

将不带参数的 func 转换为带参数的 func 是困难的。有一个带参数但不必需的 func 更容易。如果不清楚,请选择接受发送者对象的函数签名,然后只需忽略它即可。

changeMessage函数可以随机选择一条消息并设置标签上的文本,如下所示:

let messages = [
  "Ouch, that hurts",
  "Please don't do that again",
  "Why did you press that?",
]
@IBAction func changeMessage() {
  message.text = messages[
    Int(arc4random_uniform(
      UInt32(messages.count)))]
}

当函数被调用时,消息文本将更改为数组中定义的值。要调用该函数,需要在故事板编辑器中将其连接起来。从对象库中添加一个新的按钮到消息场景,并带有Change Message标签。要连接到操作,按Control并从消息场景中的更改消息按钮拖动鼠标光标,并将其放在顶部的消息视图控制器上:

从界面构建器调用操作

然后将显示一个弹出菜单,列出可以连接到的出口和操作。从列表中选择changeMessage

从界面构建器调用操作

小贴士

如果changeMessage没有列出,请检查视图控制器是否定义为MessageViewController,并验证是否已将@IBAction属性添加到changeMessage函数中。

现在当应用程序运行并按下更改消息按钮时,标签将更改为预定义的值之一。

注意

消息标签的大小不会改变,因为与之关联的视图没有自动布局。本章中“使用自动布局”部分解释了如何解决这个问题。

使用代码触发 segues

如果需要额外的设置或需要从一个视图控制器传递数据参数到另一个视图控制器(例如当前选定的对象),可以使用代码来触发 segues。

Segues 有命名的segue 标识符,在代码中用于触发特定的 segues。为了测试这一点,从库中拖动一个新的视图控制器(通过按Command + Option + Control + 3或通过导航到视图 | 实用工具 | 显示对象库)到主故事板,并将其命名为About。拖动一个标签并给它输入文本:About this App

接下来,通过按Control并拖动鼠标光标在消息场景和新的场景之间创建一个 segues。可以通过属性检查器设置命名标识符为about(通过按Command + Option + 4或通过导航到视图 | 实用工具 | 显示属性检查器):

使用代码触发 segues

最后,将一个新的按钮拖到更改消息场景,并命名为About。而不是直接调用 segues,创建一个新的@IBAction名为about。当这个按钮被按下时,将运行以下代码:

@IBAction func about(sender: AnyObject) {
  performSegueWithIdentifier("about", sender: sender)
}

当按下关于按钮时,将显示关于屏幕。

通过 segues 传递数据

通常,在主从应用程序中,需要将数据从一个场景传递到下一个场景。这可能是指当前选定的对象,或者可能需要传递额外的信息以进行处理。当调用 segue 时,会调用视图控制器的 prepareForSegue 方法,并带有目的地 segue 和发送对象。这允许将视图控制器任何内部状态传递给新的 segue

UIStoryboardSegue 包含一个标识符,该标识符在上一节中已设置。由于 prepareForSegue 方法可能对 MessageViewController 的任意数量的 segue 进行调用,因此通常会在标识符上使用 switch 语句,以便采取正确的操作。对于单个 segue,可以使用以下 if 语句:

override func prepareForSegue(segue: UIStoryboardSegue,
  sender: AnyObject?) {
  if segue.identifier == "about" {
      let dest = segue.destinationViewController as UIViewController
      dest.view.backgroundColor = message.backgroundColor
  }
}

在这里,使用 segue 调用 prepareForSegue 方法,其中包含目的地(场景)和标识符。if 语句确保匹配正确的标识符。在这种情况下,消息标签的背景颜色(在视图加载时随机选择)被传递到目的地视图的背景颜色;然而,可以在这里设置视图控制器或视图上的任何属性。

使用自动布局

自动布局(Auto Layout)已经成为了 Xcode 的几个版本的一部分,并且它的加入是为了支持从之前预 dating Mac OS X 的弹簧和支柱方法向其演变。首次在 iOS 6.0 中发布,它已经发展到可以创建默认的无尺寸依赖的显示。

理解约束

在 Xcode 5 中,界面构建器首次默认启用自动布局。当将标签拖动到父视图的顶部或底部时,一条虚线蓝色的线会指示标签被正确地间隔,并且会生成一个 约束

然而,在许多情况下,约束没有被正确创建或者产生了不期望的效果。例如,将按钮放置在顶部中央的位置可能不会保持位置,这取决于添加的约束是绝对位置(距离右侧 200 像素)还是相对位置(屏幕中央)。在这两种情况下,按钮可能看起来被正确地定位,但设备屏幕方向旋转或在不同尺寸的屏幕上运行时可能会失败。

在 Xcode 6 中,尽管指南在视图移动时仍然以视图的形式显示,但不会创建相对约束。相反,每个视图都被赋予了一个确切的硬编码位置,这个位置不会随着屏幕旋转或显示尺寸的改变而改变。

在 Xcode 7 中,自动布局是创建应用的推荐方式,视图会隐式选择自动布局。此外,可以为不同的大小类别创建单独的用户界面,这使得像计算器和邮件这样的应用能够根据设备的旋转提供不同的用户界面。在具有将应用并排停靠能力的大屏幕设备上,大小类别用于确定每个应用的外观和行为。

为了恢复正确的行为,必须手动将约束添加到视图中,并且随着手动约束的添加,绝对约束将被移除。

添加约束

在示例应用中,“欢迎使用 Swift”标签和“按我”按钮相邻,距离顶部一小段距离。然而,当在模拟器中旋转屏幕时,通过按下命令键和左右箭头键,标签与顶部之间的间距不会改变,因此标签看起来更远。

所期望的结果是标签与左上角保持标准距离,按钮与标签的基线对齐。

需要对标签应用两个独立的约束:

  • 与父视图的顶部保持标准垂直距离

  • 与父视图的左侧保持标准水平距离

需要对按钮应用以下两个约束:

  • 与标签的基线对齐

  • 与标签保持标准垂直距离

添加约束有不同的方法,以下章节将进行介绍。

使用拖放添加约束

添加约束的一个快捷方法是按下控制键并从视图拖动鼠标光标到容器的顶部。根据拖动的方向,将显示不同的选项。垂直向上拖动将显示垂直对齐选项:

使用拖放添加约束

垂直间距到顶部布局指南选项将在导航栏和标签之间插入一个推荐的间隔。还有一个在容器中水平居中选项,这也是一种垂直分隔,但在此情况下不适用。

其他激活的类型——等宽等高宽高比——允许多个视图相对于彼此进行尺寸调整。

水平拖动将显示顶部的一组不同选项,包括到容器边距的领先空间在容器中垂直居中

使用拖放添加约束

如果鼠标以一定角度拖动,将显示两组选项,如下所示:

使用拖放添加约束

向“按我”场景添加约束

要设置欢迎标签的约束,请按Control键并从标签拖动鼠标光标到左侧,并选择到容器边距的领先空间。将出现一条橙色线,并显示一个橙色轮廓:

向“按我”场景添加约束

注意

橙色线表示一个模糊的约束,这意味着已经向视图添加了一些约束,但不足以唯一地定位标签。在这种情况下,标签从容器的左侧定位,但它相对于屏幕的顶部或底部可以是任何位置。红色虚线显示了自动布局算法将放置具有当前指定约束的视图的位置。

要解决这个问题,请按Control键并将鼠标指针从标签拖动到顶部并选择到顶部布局指南的垂直间距。完成此操作后,将显示两个蓝色的约束,代表关于对象的约束:

向“按我”场景添加约束

提示

如果标签周围有一个橙色框,并显示警告信息在运行时标签的框架将不同,这可以通过下一节中讨论的更新框架选项来修复。

约束也可以在左侧的文档大纲中看到:

向“按我”场景添加约束

如果现在运行应用程序并旋转,标签将正确重新定位,但按钮不会:

向“按我”场景添加约束

添加缺失的约束

要找出哪些视图没有约束,请逐个在文档大纲中点击视图,并检查大小检查器(可以通过按Command + Option + 5或导航到视图 | 实用工具 | 显示大小检查器来查看)。对于已设置约束的视图,将在约束部分下显示内容:

添加缺失的约束

如果一个视图没有与之关联的约束,那么本节将是空的。界面构建器有一个选项可以为选定的视图创建缺失的约束,可以通过导航到编辑器 | 解决自动布局问题 | 添加缺失的约束或从底部的解决自动布局问题菜单(看起来像两条垂直线之间的三角形)来访问。

当选中时,上半部分的选项仅适用于选定的视图,而下半部分的选项作用于选定视图控制器中的所有视图:

添加缺失的约束

选项包括:

  • 更新框架:这是基于当前约束;它自动重新定位和调整视图的大小,以对应运行时的情况

  • 更新约束:这是基于对象的当前位置,并尝试重新计算现有的约束(但不创建新的约束)

  • 添加缺失约束:这是基于组件的大致位置,添加创建相同结果的约束

  • 重置为建议的约束:这相当于清除与视图相关联的所有约束,然后读取缺失的约束

  • 清除约束:这会移除与视图相关联的所有约束

要向Press Me按钮添加约束,请点击视图,然后导航到编辑器 | 解决自动布局问题 | 所选视图 | 添加缺失约束。应该添加两个约束:与标签的基线对齐,以及到标签的水平间距。

要查看更新框架操作的效果,请将标签和按钮移动到视图控制器中的不同位置。将显示橙色线条和虚线轮廓,指示存在模糊的约束。导航到选择编辑器 | 解决自动布局问题 | 视图控制器中的所有视图 | 更新框架,视图将自动移动到正确的位置并调整大小。

注意

视图的大小设置为它们的固有大小,即刚好适合内容的大小。例如,标签的固有大小是文本可以适应当前字体空间的大小。这可以用来固定消息场景中标签的大小;通过添加约束,文本的变化将导致固有大小重新计算,背景色将正确调整大小。

现在,运行应用程序并旋转设备,通过按Command键和左右箭头键来查看视图是否正确调整大小。

摘要

本章介绍了故事板的概念,它是一系列通过转场连接的场景,这些场景可以是与 GUI 有线连接,也可以是程序驱动。最后,可以使用自动布局来构建能够响应屏幕方向或大小变化的以及响应视图大小或其他属性变化的程序。

下一章将介绍如何在 Swift 中创建自定义视图。

第五章. 使用 Swift 创建自定义视图

用户界面可以通过 Interface Builder、Storyboard 编辑器或自定义代码通过组合标准视图和视图控制器来构建。然而,最终可能需要将用户界面分解成更小、可重用且易于测试的片段。这些被称为 自定义视图

本章将介绍以下主题:

  • 自定义表格视图

  • 构建和布局自定义视图子类

  • 使用 drawRect 绘制图形视图

  • 使用动画创建分层图形

UIView 概述

所有 iOS 视图都基于一个名为 UIView 的 Objective-C 类,该类来自 UIKit 框架/模块。UIView 类代表一个可能关联到 UIWindow 或用于表示离屏视图的矩形空间。执行用户交互的视图通常是 UIControl 的子类。UIViewUIViewController 都继承自 UIResponder 类,而 UIResponder 类又继承自 NSObject

UIView 概述

在 Mac OS X 上,视图基于 NSView,来自 AppKit 框架。否则,这两个实现非常相似。将使用一个新的 Xcode 项目来创建自定义视图类。创建一个基于 标签应用 模板的名为 CustomViews 的新项目。要从一个空白表单开始,请从 Main.storyboard 中删除生成的视图控制器及其相关的 FirstViewControllerSecondViewController 类。

使用 Interface Builder 创建新视图

创建自定义视图最简单的方法是使用 Interface Builder 拖放内容。这通常是通过 UITableView原型表格单元格 来完成的。

创建表格视图控制器

从对象库中将 Table View Controller 拖放到主故事板中,然后从标签栏控制器拖放到新创建的表格视图控制器中,以创建一个名为 view controllers 的关系转换。(转换在 第四章 的 Storyboards转换场景 部分有更详细的介绍,Storyboard Applications with Swift and iOS。)

默认情况下,表格视图控制器将具有 动态属性内容——也就是说,它能够显示可变数量的行。这定义在 表格视图 部分的 属性检查器 中,可以通过从场景导航器中选择 表格视图 并按 Command + Option + 4 来显示:

创建表格视图控制器

注意

表格有一个选项可以具有静态内容;表格中的固定行数。这在创建可以分割成块的可滚动内容时有时很有用,即使它看起来不像表格。iOS 设置中的大多数元素都表示为固定大小的表格视图。在表格视图的顶部有一个或多个原型单元格。这些用于定义表格项的外观和感觉。默认情况下,使用UITableViewCell,它有一个标签和一个图像,但可以使用原型单元格向条目添加更多数据。

创建表格视图控制器

可以使用原型单元格提供额外的信息或视图。例如,可以将两个标签拖入视图中;一个标签可以居中在顶部,并使用标题字体显示,而第二个可以左对齐。

从对象库中拖动两个UILabel到原型单元格中,并使用Auto Layout适当地排列它们。

要更改标签的字体,在编辑器中选择标签,然后转到属性检查器。在标签部分,单击字体选择器图标并选择标题副标题,根据需要:

创建表格视图控制器

完成后,原型单元格将类似于以下截图:

创建表格视图控制器

当应用程序运行时,将看到一个空表格。这是因为表格目前没有显示任何条目。下一节将展示如何向表格添加数据,以便将其绑定并显示到原型单元格中。

在表格中显示数据

UITableViewUITableViewDataSource获取数据。UITableViewController类已经实现了UITableViewDataSource协议,因此只需要实现少量方法来为表格提供数据。

小贴士

由于UITableView最初是用 Objective-C 实现的,因此协议中定义的方法接受一个tableView。因此,Swift 中的所有UITableViewDataSource代理方法最终都以tableView结尾,并带有不同的参数。

创建一个新的SampleTable类,该类继承自UITableViewController。按照以下方式实现该类:

import UIKit
class SampleTable: UITableViewController {
  var items = [
    ("First", "A first item"),
    ("Second", "A second item"),
  ]
  required init?(coder:NSCoder) {
    super.init(coder:coder)
  }
  override func tableView(tableView: UITableView,
    numberOfRowsInSection section:Int) -> Int {
    return items.count
  }
  override func tableView(tableView: UITableView,
    cellForRowAtIndexPath indexPath: NSIndexPath)
     -> UITableViewCell {
    let cell = tableView.
     dequeueReusableCellWithIdentifier("prototypeCell")!

    // configure labels
    return cell
  }
}

实现数据源方法后,需要配置标签以显示数组中的数据。需要完成三件事:从xib文件中获取原型单元格;提取标签;最后,将表格视图控制器与自定义的SampleTable类关联。

首先,cellForRowAtIndex函数需要一个可重用单元格的标识符。标识符在主故事板中的原型单元格上设置。要设置此标识符,请选择原型单元格,然后转到属性检查器。在表格视图单元格部分的标识符中输入prototypeCell

在表格中显示数据

标识符用于tableViewdequeueReusableCellWithIdentifier方法中。当使用xib来加载单元格时,返回值将要么是之前已经离开屏幕的单元格,要么是从xib中实例化出的新单元格。

每个标签都可以分配一个非零整数Tag,这样就可以使用viewWithTag方法从原型单元格中提取标签:

let titleLabel = cell.viewWithTag(1) as! UILabel
let subtitleLabel = cell.viewWithTag(2) as! UILabel

要为视图分配标签,选择Heading Label,导航到Attributes Inspector,并将Tag更改为1。对Subheading Label做同样的事情,将Tag设置为2

在表中显示数据

现在,可以设置行的文本值:

let (title,subtitle) = items[indexPath.row]
titleLabel.text = title
subtitleLabel.text = subtitle

最后,需要将SampleTable与表格视图控制器关联起来。点击表格,进入Identity Inspector,在Custom Class部分输入SampleTable

在表中显示数据

当应用程序运行时,将显示以下视图:

在表中显示数据

小贴士

要隐藏状态栏,在Info.plist文件中添加或更改Status bar is initially hiddenYES,并将View controller-based status bar appearance设置为NO。请注意,Xcode 7 在使用这些选项时显示CGContextRestoreGState: invalid context 0x0错误消息,这是一个已知问题,可能在后续版本中修复。

在 xib 文件中定义视图

可以使用Interface Builder创建视图,将其保存为xib文件,然后按需实例化。这就是UITableView背后的操作——存在一个registerNib:forCellReuseIdentifier:方法,它接受一个xib文件和一个标识符(在先前的例子中对应于prototypeCell)。

通过导航到File | New | File | iOS | User Interface | View创建一个名为CounterView.xib的新接口文件来表示视图。打开后,它将显示为一个空视图,没有内容,并且在一个 600 x 600 的正方形中。要将大小更改为更合理的尺寸,转到Attributes Inspector并将大小从Inferred更改为Freeform。同时,将Status BarTop BarBottom Bar更改为None。然后切换到Size Inspector并修改视图的Frame Rectangle300 x 50

在 xib 文件中定义视图

这应该会调整视图的大小,使其显示为 300 x 50 而不是之前的 600 x 600,并且状态栏和其他栏不应可见。现在,通过从对象库中拖动到视图的左侧添加一个Stepper,并将一个Label拖到右侧。调整大小并添加缺失的约束,使视图看起来类似于以下截图:

在 xib 文件中定义视图

连接自定义视图类

创建一个新的 CounterView 类,它扩展了 UIView,并为 label 定义一个 @IBOutlet,以及一个接受 sender@IBAction change 方法。

打开 CounterView.xib 文件并选择视图。将 Custom Class 更改为 CounterView。将步进器的 valueChanged 事件连接到 change 方法,并连接到 label 输出:

连接自定义视图类

实现 change 函数,以便在选择步进器时更改标签文本:

import UIKit
class CounterView: UIView {
  @IBOutlet weak var label:UILabel!
  @IBAction func change(sender:AnyObject) {
    let count = (sender as! UIStepper).value
    label.text = "Count is \(count)"
  }
}

CounterView 将被添加到 SampleTable表头 中。每个 UITableViewController 都有一个对其关联的 UITableView 的引用,每个 UITableView 都有一个用于整个表格的可选 headerView(和 footerView)。

注意

UITableView 还具有 sectionHeadersectionFooter,它们用于分隔表格的不同部分。一个表格可以有多个部分——例如,每个月一个部分——并且每个部分都可以使用单独的头部和尾部。

要创建一个 CounterView,必须加载 xib 文件。这是通过使用 nibNamebundle 实例化一个 UINib 来实现的。最合适的地方是在 SampleTable 类的 viewDidLoad 方法中完成这个操作:

class SampleTable: UITableViewController {
  override func viewDidLoad() {
    let xib = UINib(nibName:"CounterView", bundle:nil)
    // continued

一旦加载 xib,就必须创建视图。instantiateWithOwner 方法允许将 xib 中的对象反序列化。

注意

xib 文件中可以存储多个对象(例如,为了定义一个适合小显示设备而不是大显示设备的单独视图);但通常,xib 文件只包含一个视图。

所有权传递给视图,以便可以将任何连接连接到界面的文件所有者。这通常是 self,如果没有连接则为 nil

    // continued from before
    let objects = xib.instantiateWithOwner(self, options:nil)
    // continued

这返回一个 AnyObject 实例的数组,因此将第一个元素强制转换为 UIView 是一个常见的步骤。

小贴士

可以使用 objects[0],但如果数组为空,这将导致失败。相反,使用 objects.first 来获取包含第一个元素的可选值。

使用 as? 强制类型转换,可以将可选值转换为更具体的类型,并从这一点进行到 tableHeaderView 的赋值:

    // continued from before
    let counter = objects.first as? UIView
    tableView.tableHeaderView = counter
  }

当在这个模拟器中运行此应用程序时,可以在表格顶部看到以下头部:

连接自定义视图类

拥有一个 xib 来表示用户界面的一个优点是,它可以使用单个定义在许多地方重用。例如,可以使用相同的 xib 来实例化另一个视图作为表格的底部,如下所示:

tableView.tableFooterView = 
  xib.instantiateWithOwner(self,options:nil).first as? UIView

当应用程序现在运行时,计数器会在表格的顶部和底部创建:

连接自定义视图类

处理固有大小

当一个视图被添加到一个使用 Auto Layout 管理的视图中时,它将使用其 固有内容大小。不幸的是,在 Interface Builder 中定义的视图没有方法可以程序化地设置其固有大小或在 Interface Builder 中指定它。大小检查器 允许更改此值,但正如 Xcode 所注明的,这在运行时没有任何效果:

处理固有大小

如果一个自定义类与视图相关联,则可以定义一个适当的固有大小。向 CounterView 添加一个方法,重写 intrinsicContentSize 方法并返回一个 CGSize,允许一些 .xib 自定义,并返回标签的固有大小和某个值(如 (300,50))的最大值:

override func intrinsicContentSize() -> CGSize {
  let height = max(50,label.intrinsicContentSize().height)
  let width = max(300,label.intrinsicContentSize().width)
  return CGSize(width: width, height: height)
}

现在当视图被添加到一个由 Auto Layout 管理的视图中时,它将有一个适当的初始大小,尽管它可以变得更大。

注意

大小应该考虑到包含在其中的各种视图的大小,以及任何可能改变视图的字体大小或主题。使用标签的 intrinsicSize 来计算最大值是一个好主意。

通过子类化 UIView 创建新视图

虽然.xib 文件提供了一个自定义类的机制,但标准框架之外的大多数 UIKit 视图都是通过自定义代码实现的。这使得推理固有大小应该是什么以及接收代码补丁和理解版本控制系统的 diffs 变得更容易。这种方法的一个缺点是,在使用 Auto Layout 时,编写约束可能是一个挑战,固有大小通常报告错误或返回未知值:(-1,-1)

自定义视图可以作为一个 UIView 的子类来实现。UIView 的子类通常应该有两个初始化器,一个接受 frame:CGRect 参数,另一个接受 coder:NSCoder 参数。frame 通常在代码中使用,它指定了屏幕上的位置(0,0 是左上角)以及宽度和高度。coder 在从 xib 文件反序列化时使用。

为了允许自定义子类既可以在 Interface Builder 中使用,也可以从代码中实例化,一个好的做法是确保两个初始化器都创建了必要的视图。这可以通过一个名为 setupView 的第三个方法来完成,它从两个地方调用。

创建一个名为 TwoLabels 的类,该类在视图中包含两个标签:

import UIKit
class TwoLabels: UIView {
  var left:UILabel = UILabel()
  var right:UILabel = UILabel()
  required init?(coder:NSCoder) {
    super.init(coder:coder)
    setupView()
  }
  override init(frame:CGRect) {
    super.init(frame:frame)
    setupView()
  }
  // ...
}

setupView 调用将子视图添加到视图中。这里放入的代码应该只执行一次。没有标准的名称,通常示例代码会将设置放在一个或另一个 init 方法中。

通常有一个单独的方法,例如 configureView,用于用当前的数据集填充 UI。这可以根据系统的状态重复调用;例如,一个字段可能根据某些条件被启用或禁用。此代码应该是可重复的,以便它不会修改视图层次结构:

func setupView() {
  addSubview(left)
  addSubview(right)
  configureView()
}
func configureView() {
  left.text = "Left"
  right.text = "Right"
}

在显式设置大小环境中(其中文本标签正在设置并放置在特定位置),有一个layoutSubviews方法被调用以请求正确布局视图。然而,有一种更好的方法,那就是使用自动布局和约束。

自动布局和自定义视图

自动布局在第四章的使用自动布局部分中介绍,使用 Swift 和 iOS 的 Storyboard 应用。在显式创建用户界面时,必须适当地设置和管理工作视图。管理这些视图的最简单方法是使用自动布局,这需要添加约束来设置视图。

可以在updateConstraints方法中添加或更新约束。这通常在调用setNeedsUpdateConstraints之后进行。如果视图变得可见或数据发生变化,可能需要更新约束。通常,这可以通过在setupView方法末尾放置一个调用来实现,如下所示:

func setupView() {
  // addSubview etc
  setNeedsUpdateConstraints()
}

updateConstraints方法需要执行几个操作。为了防止自动调整大小掩码被转换为约束,每个视图都需要调用setTranslatesAutoresizingMaskIntoConstraints并传递参数false

提示

为了便于在弹簧和支柱(也称为自动调整大小掩码)之间以及自动布局之间进行转换,可以配置视图将弹簧和支柱转换为自动布局约束。默认情况下,对所有视图启用此功能,以提供对现有视图的向后兼容性,但在实现自动布局时应将其禁用。

可以增量更新约束或删除现有约束。removeConstraints方法允许首先删除现有约束,如下所示:

override func updateConstraints() {
  translatesAutoresizingMaskIntoConstraints = false
  left.translatesAutoresizingMaskIntoConstraints = false
  right.translatesAutoresizingMaskIntoConstraints = false
  removeConstraints(constraints)
  // add constraints here
}

可以使用NSLayoutConstraint类编程添加约束。在 Interface Builder 中添加的约束也是NSLayoutConstraint类的实例。

约束表示为一个方程;两个对象的属性以以下形式的等式(或不等式)相关联:

// object.property = otherObject.property * multiplier + constant

要声明两个标签具有相同的宽度,可以在updateConstraints方法中添加以下内容:

// left.width = right.width * 1 + 0
let equalWidths = NSLayoutConstraint(
  item: left,
  attribute: .Width,
  relatedBy: .Equal,
  toItem: right,
  attribute: .Width,
  multiplier: 1,
  constant: 0)
addConstraint(equalWidths)

约束和视觉格式语言

虽然添加单个约束为我们提供了最大的灵活性,但通过编程设置可能会很繁琐。可以使用视觉格式语言向视图添加多个约束。这是一种基于 ASCII 的表示,允许视图在位置上相互关联,并扩展成约束数组。

约束可以水平(默认)或垂直应用。|字符可以用来表示包含的父视图的开始或结束,而-用来表示在[]中命名的视图之间的空间,这些视图在字典中引用。

要约束在视图中相邻的两个标签,可以使用 H:|-[left]-[right]-|。这可以读作一个水平(H:)从左边距(|-)开始,然后是左视图([left]),一个间隔(-),一个右视图([right]),最后是一个从右边距的间隔(-|)。同样,可以使用 V: 前缀添加垂直约束。

NSLayoutConstraint 类上的 constraintsWithVisualFormat 方法可以用来解析视觉格式约束。它接受一组 optionsmetrics 以及一个包含在视觉格式中引用的 views 字典。返回一个约束数组,这些约束可以被传递到视图的 addConstraints 方法中。

要添加确保 leftright 视图具有相等宽度、它们之间有空间以及视图顶部和标签之间有垂直空间的约束,可以使用以下代码:

override func updateConstraints() {
  // …
  let options = NSLayoutFormatOptions()
  let namedViews = ["left":left,"right":right]
  addConstraints(NSLayoutConstraint.
    constraintsWithVisualFormat("H:|-[left]-[right]-|",
      options: options, metrics: nil, views: namedViews))
  addConstraints(NSLayoutConstraint.
    constraintsWithVisualFormat("V:|-[left]-|",
      options: options, metrics: nil, views: namedViews))
  addConstraints(NSLayoutConstraint.
    constraintsWithVisualFormat("V:|-[right]-|",
      options: options, metrics: nil, views: namedViews))
  super.updateConstraints()
}

注意

如果存在模糊的约束,则在视图显示时将在控制台打印错误。包含 NSAutoresizingMaskLayout 约束的消息表明视图尚未禁用自动将自动调整大小掩码转换为约束的自动转换。

将自定义视图添加到表格中

可以通过将其添加到之前创建的 SimpleTable 的页脚来测试 TwoLabels 视图。页脚是一个特殊类,UITableViewHeaderFooterView,需要创建并添加到 tableView 中。然后可以将 TwoLabels 视图添加到页脚的 contentView

let footer = UITableViewHeaderFooterView()
footer.contentView.addSubview(TwoLabels(frame:CGRect.zero))
tableView.tableFooterView = footer

现在当应用程序在模拟器中运行时,将看到自定义视图:

将自定义视图添加到表格中

使用 drawRect 绘制自定义图形

UIView 的子类可以通过提供一个实现自定义绘图例程的 drawRect 方法来实现它们自己的自定义图形。drawRect 方法接受一个 CGRect 参数,它指示要绘制的区域。然而,实际的绘图命令是在 Core Graphics 上下文中执行的,该上下文由 CGContext 类表示,可以通过调用 UIGraphicsGetCurrentContext 获取。

Core Graphics 上下文表示 iOS 中的一个可绘制区域,它用于打印以及绘制图形。每个视图都有绘制自己的责任;矩形将是整个区域(例如,视图第一次绘制时)或可能是区域的一个子集(例如,当对话框显示然后随后被移除时)。

核心图形 是一个基于 C 的接口(而不是基于 Objective-C),因此 API 以 UIGraphics 前缀开始的一组函数的形式公开。与其他绘图 API 一样,程序可以设置当前绘图颜色、绘制线条、设置填充颜色、填充矩形等。

要测试这个,创建一个名为 SquaresView 的类,它是 UIView 的新子类,在一个新的 Swift 文件中。

所有视图都有标准的init方法;将它们委托给超类的实现。最后,创建一个接受CGRectdrawRect方法。这将是在自定义绘图发生的地方。其骨架如下所示:

import UIKit
class SquaresView: UIView {
  required init?(coder: NSCoder) {
    super.init(coder:coder)
    setupView()
  }
  override init(frame: CGRect) {
    super.init(frame:frame)
    setupView()
  }
  func setupView() {
  }
  override func drawRect(rect: CGRect) {
    // drawing code goes here
  }
}

打开Main.storyboard,拖入另一个UIViewController,并在Identity Inspector中将视图的定制类设置为SquaresView。在标签视图控制器和新视图控制器之间拖入一个关系切换,并将标签栏项设置为Squares,这将允许测试移动到不同的视图。如果运行应用程序,将在Squares标签中看到一个空白视图。

在 drawRect 中绘制图形

要在视图中绘制图形,需要获取一个CGContext并设置一个绘图(描边)颜色。可以获取一个UIColor并将其转换为CGColor,以便能够在图形上下文中设置它。

最后,可以使用CGContextStrokeRect绘制一个矩形:

override func drawRect(rect: CGRect) {
  let context = UIGraphicsGetCurrentContext()
  let red = UIColor.redColor().CGColor
  CGContextSetStrokeColorWithColor(context, red)
  CGContextStrokeRect(context, 
    CGRect(x:50, y:50, width:100, height:100))
}

当在模拟器中运行时,将在Squares标签上显示一个红色矩形。

要在中间绘制一个带有黑色轮廓的绿色正方形,首先需要绘制一个填充的绿色正方形,然后绘制一个黑色正方形。 (以相反的顺序绘制它们将导致实心绿色正方形消除黑色正方形。)

在 Core Graphics 上下文中有两种不同的颜色:描边颜色,用于绘制线条和路径,以及填充颜色,用于创建填充路径。尽管存在CGContextSetFillColorWithColor函数,但在 Swift 中,使用UIColorsetFillsetStroke方法直接设置要简单得多。以下代码将创建一个带有黑色边框的绿色正方形:

UIColor.greenColor().setFill()
UIColor.blackColor().setStroke()
CGContextFillRect(context,
  CGRect(x:75, y:75, width:50, height:50))
CGContextStrokeRect(context,
  CGRect(x:75, y:75, width:50, height:50))

现在当应用程序运行时,将看到以下内容:

在 drawRect 中绘制图形

响应方向变化

当屏幕旋转时,视图被拉伸和挤压,导致正方形变成矩形。当视图改变方向时,不会调用drawRect调用;现有的显示会自动被挤压和拉伸。

为了防止这种情况,可以更改视图的内容模式。有一个名为UIViewContentMode的枚举,可以指定以引起不同的行为。使用Redraw将在方向改变或边界大小改变时调用drawRect

注意

其他enum值在UIViewContentMode类型中有所记录,包括缩放选项以及居中或附着到其中一个边缘或角落。

正方形可以居中在屏幕上;而不是从位置50,50开始,可以通过访问视图的center属性来找出位置。按照以下方式修改代码:

func setupView() {
  contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
  let context = UIGraphicsGetCurrentContext()
  let red = UIColor.redColor().CGColor
  CGContextSetStrokeColorWithColor(context,red)
  CGContextStrokeRect(context,
    CGRect(x:center.x-50, y:center.y-50, width:100, height:100))
  UIColor.greenColor().setFill()
  UIColor.blackColor().setStroke()
  CGContextFillRect(context,
    CGRect(x:center.x-25, y:center.y-25, width:50, height:50))
  CGContextStrokeRect(context,
    CGRect(x:center.x-25, y:center.y-25, width:50, height:50))
}

现在当应用程序运行时,正方形将居中在屏幕上。如果屏幕旋转,drawRect将被再次调用,并且显示将被重新绘制。

使用图层进行自定义图形

通过重写 drawRect 来绘制图形并不太高效,因为所有的绘图例程都是在 CPU 上执行的。将图形绘制任务卸载到 GPU 上既更高效,也更节能。

iOS 有一个层的概念,它们是 Core Graphics 优化的绘图内容。在 上组成的操作,包括添加 路径,可以转换为可以在 GPU 上执行并高效渲染的代码。此外,可以使用 Core Animation 高效地动画化层上的变化。Core AnimationQuartzCore 框架/模块提供;这两个术语可以互换使用。它更普遍地被称为 Core Animation。

在 iOS 中,可以将下载进度图标重新创建为一个包含层(用于圆形轮廓)、一个层(用于中间的方形停止按钮)和一个层(用于进度弧)的 ProgressView。最终的视图将组合这三个层以提供完成后的视图。

每个 UIView 都有一个隐式关联的层,可以向其添加子层。与视图一样,新添加的层会覆盖现有层。可以使用几个 核心动画层 类,它们是 CALayer 的子类,具体如下:

  • CAEAGLLayer 类提供了一种将 OpenGL 内容嵌入到视图中的方法

  • CAEmitterLayer 类提供了一种生成发射效果(如烟雾和火焰)的机制

  • CAGradientLayer 类提供了一种创建具有渐变颜色的背景的方法

  • CAReplicatorLayer 类提供了一种通过不同的变换复制现有层的方法,这允许显示如反射和 coverflow 这样的效果

  • CAScrollLayer 类提供了一种执行滚动的方法

  • CAShapeLayer 类提供了一种绘制和动画化单个路径的方法

  • CATextLayer 类允许显示文本

  • CATiledLayer 类提供了一种在不同缩放级别生成平铺内容的方法,如地图

  • CATransformLayer 类提供了一种将层转换为 3D 视图(如 coverflow 风格的图像动画)的方法

通过层创建 ProgressView

创建另一个名为 ProgressView 的视图类,它扩展了 UIView。使用默认的 init 方法、setupView 方法和 configureView 方法来设置它:

import UIKit
class ProgressView: UIView {
  required init?(coder: NSCoder) {
    super.init(coder:coder)
    setupView()
  }
  override init(frame: CGRect) {
    super.init(frame:frame)
    setupView()
  }
  func setupView() {
    configureView()
  }
  func configureView() {
  }
}

通过从对象库中将 UIViewController 拖动到 Main.storyboard 中来创建一个新的 Layers Scene。通过拖动一个关系 segue 到新创建的 layers 视图控制器来将其连接到标签栏控制器。通过从对象库中拖动一个 View 并将其 Custom Class 设置为 ProgressView 来添加 ProgressView。将其大小调整到屏幕中间的大致位置。

现在向 ProgressView 类添加一个实例变量 circle 并创建一个新的 CAShapeLayer 实例。在 setupView 中,将 strokeColor 设置为 blackfillColor 设置为 nil。最后,将 circle 层添加到视图的层中,以便显示:

let circle = CAShapeLayer()
func setupView() {
  circle.strokeColor = UIColor.blackColor().CGColor
  circle.fillColor = nil
  self.layer.addSublayer(circle)
  configureView()
}

CAShapeLayer 有一个 path 属性,用于执行所有绘图操作。使用它的最简单方法是创建一个 UIBezierPath,然后使用 CGPath 访问器将其转换为 CGPath

注意

贝塞尔曲线 是在两点之间表示平滑曲线以及一个或多个控制点的一种方法。这些曲线可以精确缩放,并且在图形卡上易于计算。UIBezierPath 提供了一种表示一个或多个贝塞尔路径的方法,从而实现平滑且高效的曲线生成。

UIGraphics* 方法不同,没有单独的 draw*fill* 操作;相反,要么设置 fillColorstrokeColor,然后填充或绘制路径(绘制)。UIBezierPath 可以通过添加段来构建,但有几个初始化器可以用来绘制特定形状。例如,可以使用 ovalInRect 初始化器绘制圆形:

func configureView() {
  let rect = self.bounds
  circle.path = UIBezierPath(ovalInRect: rect).CGPath
}

现在当应用程序运行时,将在 图层 选项卡上看到一个小的黑色圆圈:

从图层创建 ProgressView

添加停止正方形

可以通过创建另一个图层来添加停止正方形。这将允许根据需要打开或关闭停止按钮。(例如,在下载过程中,可以显示停止按钮,当下载完成后,可以将其动画化消失。)

添加一个名为 square 的新常量,类型为 CAShapeLayer。它将有助于创建一个常量 black,因为它将在本类的其他地方再次使用:

class ProgressView: UIView {
  let square = CAShapeLayer()
  let circle = CAShapeLayer()
  let black = UIColor.blackColor().CGColor
}

现在可以将 setupView 方法更新以处理额外的图层。由于通常以相同的方式设置它们,因此使用循环是设置多个图层的快速方法,如下所示:

func setupView() {
  for layer in [square, circle] {
    layer.strokeColor = black
    layer.fillColor = nil
    self.layer.addSublayer(layer)
  }
  configureView()
}

使用 UIBezierPathrect 初始化器可以创建 square 的路径。为了创建一个位于圆内的矩形,请使用 insetBy 方法并设置一个合适的值:

func configureView() {
  let rect = self.bounds
  let sq = rect.insetBy(dx: rect.width/3, dy: rect.height/3)
  square.fillColor = black
  square.path = UIBezierPath(rect: sq).CGPath
  circle.path = UIBezierPath(ovalInRect: rect).CGPath
}

现在当应用程序运行时,将看到以下内容:

添加停止正方形

添加进度条

进度条可以绘制为表示已下载数据量的弧线。在其他 iOS 应用程序中,进度条从 12 点位置开始,然后顺时针移动。

有两种方法可以实现这一点:使用绘制到某个特定数量的弧线,或者通过设置表示整个圆的单一路径,然后使用 strokeStartstrokeEnd 来定义应该绘制路径的哪一段。使用 strokeStartstrokeEnd 的优点是它们是 可动画属性,这允许一些动画效果。

需要从顶部绘制弧线,顺时针移动到右边,然后再向上。strokeStartstrokeEnd是介于 0 和 1 之间的CGFloat值,因此它们可以用来表示下载的进度。

小贴士

就像π一样简单

虽然圆通常被分成 360 度(主要是因为 360 有很多因数,很容易被分成不同的数字),但计算机倾向于使用弧度。一个圆中有2pi弧度;所以半个圆是pi,四分之一圆是pi/2

有一个UIBezierPath便利初始化器可以绘制弧线;centerradius被指定,以及一个startAngleendAngle点。起点和终点都指定为弧度,0 是 3 点钟位置,顺时针或逆时针,根据指定进行:

添加进度条

要从圆的顶部开始绘制进度,必须将起点指定为-pi/2。从这里顺时针绘制整个圆到达-pi/2 + 2pi,即3 * pi/2

小贴士

计算机大量使用π,定义在usr/include/math.h中,它通过Darwin模块间接包含在UIKit中。常数:M_PIM_PI_2(π/2),和M_PI_4(π/4),以及它们的倒数:M_1_PI(1/π),和M_2_PI(2/π),都是可用的。

图表中间可以通过访问self.center来计算,圆的radius将是widthheight的最小值的一半。要添加路径,创建一个新的CAShapeLayer,命名为progress,将其添加到层数组中,并可选地给它不同的widthcolor以区分背景:

class ProgressView: UIView {
  let progress = CAShapeLayer()
  var progressAmount: CGFloat = 0.5
  …
  func setupView() {
    for layer in [progress, square, circle] {
      …
    }
    progress.lineWidth = 10
    progress.strokeColor = UIColor.redColor().CGColor
    configureView()
  }
  func configureView() {
    …
    let radius = min(rect.width, rect.height) / 2
    let center = CGPoint(x:rect.midX, y:rect.midY)
    progress.path = UIBezierPath(
      arcCenter: center,
      radius: radius,
      startAngle: CGFloat(-M_PI_2),
      endAngle: CGFloat(3*M_PI_2),
      clockwise: true
    ).CGPath
    progress.strokeStart = 0
    progress.strokeEnd = progressAmount
  }
}

当运行时,进度条将显示在圆的后面:

添加进度条

视图裁剪

进度线的麻烦在于它延伸到了进度视图的圆形边界之外。一个简单的方法可能是尝试计算从半径到半宽度的距离,然后重新绘制圆,但这很脆弱,因为线宽的变化可能会导致未来的图表看起来不正确。

更好的方法是遮罩图形区域,这样绘图就不会超出特定形状之外。通过指定遮罩,任何在遮罩内的绘图都会显示出来;绘制在遮罩外的图形则不会显示。

遮罩可以被定义为矩形区域或填充层的输出结果。创建圆形遮罩需要创建一个新的遮罩层,然后设置一个圆形路径,就像我们之前做的那样。

注意

遮罩只能被单个层使用。如果需要为多个层使用相同的遮罩,要么需要复制遮罩层,要么可以将遮罩设置在公共父层上。

创建一个新的CAShapeLayer,用于遮罩,并创建一个基于UIBezierPathovalInRectpath。然后可以将遮罩分配给progress层的mask层:

class ProgressView: UIView {
  let mask = CAShapeLayer()
  func configureView() {
    … 
    mask.path = UIBezierPath(ovalInRect:rect).CGPath
    progress.mask = mask
  }
}

现在当显示出现时,进度条不会溢出边缘:

裁剪视图

在 Xcode 中测试视图

要直接在 Interface Builder 中测试视图,可以将类标记为 @IBDesignable。这允许 Xcode 实例化和运行视图,以及更新任何所做的更改。如果类被标记为 @IBDesignable,则 Xcode 将尝试加载视图并在故事板和 xib 文件中显示它。

然而,当类加载时,UI 不会正确显示,因为框架大小需要正确初始化。重写 layoutSubviews 方法来调用 configureView,这确保了视图在视图大小改变或首次显示时被正确重绘:

@IBDesignable class ProgressView: UIView {
  … 
  override func layoutSubviews() {
    setupView()
  }
}

现在当 ProgressView 被添加或显示在 Interface Builder 中时,它将就地渲染。构建项目后,打开 Main.storyboard,点击 Progress View;经过短暂延迟后,它将被绘制。

Xcode 还可以用于编辑 Interface Builder 中对象的属性。这允许在不运行应用程序的情况下测试视图。

要允许 Interface Builder 编辑属性,可以将它们标记为 @IBInspectable

@IBDesignable class ProgressView: UIView {
  @IBInspectable var progressAmount: CGFloat = 0.5 
  …
}

构建项目后,打开故事板,选择 Progress View 并转到 Attributes Inspector。在 View 部分上方将有一个 Progress View 部分,其中包含基于同名 @IBInspectable 字段的 Progress Amount 字段:

在 Xcode 中测试视图

响应变化

如果将 UISlider 添加到 Layers View,可以通过添加 @IBAction 来触发更改,允许 valueChanged 事件将值传播给调用者。

创建一个 @IBAction 函数,名为 setProgress,它接受一个发送者,然后根据发送者的类型提取一个值:

@IBAction func setProgress(sender:AnyObject) {
  switch sender {
    case let slider as UISlider: progressAmount =
      CGFloat(slider.value)
    case let stepper as UIStepper: progressAmount = 
      CGFloat(stepper.value)
    default: break
  }
}

小贴士

使用基于类型的 switch 语句允许将来添加更多视图。

现在可以将 UISlider 上的 valueChanged 事件连接到 ProgressViewsetProgess

仅分配 progressAmount 值没有可见效果,因此可以使用属性观察器在字段修改时触发显示更改。属性观察器 是在属性更改之前(willSet)或之后(didSet)被调用的代码块:

@IBInspectable var progressAmount: CGFloat = 0.5 {
  didSet {
    setNeedsLayout()
  }
}

现在当应用程序运行并移动滑块值时,视图中的下载量将更新:

响应变化

小贴士

如果滑块值改变时图像没有更新,请检查 progressAmount 上的 didSet 是否触发了 setNeedsLayout 调用,以及 layoutSubviews 函数是否正确调用了 configureView

注意到 progressAmount 的更改会自动动画化,所以如果快速将滑块从一个端点移动到另一个端点,下载弧将平滑地动画化。

小贴士

属性观察者使用setNeedsLayout来触发对layoutSubviews的调用,以实现显示上的变化。由于只有在尺寸发生变化或属性被更改时才需要检测变化,因此这比实现其他方法(如每次显示需要更新时都会被调用的drawRect)更高效。

摘要

在本章中,我们探讨了在 iOS 中创建视图的几种不同方法。首先,我们使用 Interface Builder 图形化地构建视图并分析了一些可能引起的问题。然后,我们探讨了通过子类化UIView并添加其他视图来构建自定义视图的方法。最后,我们介绍了两种绘制自定义图形的不同方式;首先是通过drawRect,然后是通过层。下一章将向您展示如何使用 iOS 中的网络 API 下载网络数据。

第六章:解析网络数据

许多 iOS 应用程序需要与其他服务器或设备进行通信。本章介绍了 Swift 中的 HTTP 和非 HTTP 网络以及如何从 JSON 或 XML 中解析数据。它首先演示了如何有效地从 URL 加载数据,然后是如何流式传输较大的数据响应。最后,它总结了如何执行除 HTTP 之外的协议的同步和异步网络请求。

本章将介绍以下主题:

  • 从 URL 加载数据

  • 从后台线程更新用户界面

  • 解析 JSON 和 XML 数据

  • 基于流的连接

  • 异步数据通信

从 URL 加载数据

从远程网络源加载数的最常见方式是使用形式为 raw.githubusercontent.com/alblue/com.packtpub.swift.essentials/master/CustomViews/CustomViews/SampleTable.json 的 HTTP(或 HTTPS)URL。

可以使用来自 Foundation 模块(该模块通过 UIKit 模块间接导入)的 NSURL 类来操作 URL。主要的 NSURL 初始化器接受一个带有完整 URL 的 String 初始化器,尽管存在其他初始化器用于创建相对 URL 或对文件路径的引用。

NSURLSession 类通常用于执行与 URLs 相关的操作,可以通过初始化器创建单个会话,或者可以使用标准的共享会话。在旧版本的 iOS 和 Mac OS X 中使用了 NSURLConnection 类。在某些教程中仍可以看到对该类的引用,或者如果需要支持 Mac OS X 10.8 或 iOS 6,则可能需要该类;否则,应首选 NSURLSession 类。

NSURLSession 类提供了一种创建任务的方法。这些包括:

  • 数据任务:这可以用于以编程方式处理网络数据

  • 上传任务:这可以用于将数据上传到远程服务器

  • 下载任务:这可以用于将数据下载到本地存储或恢复先前的或部分下载

任务是从 NSURLSession 类的方法创建的,可以接受一个 URL 参数和一个可选的完成处理程序。完成处理程序类似于代理,但它可以针对每个任务进行自定义,通常表示为一个函数。

任务可以被暂停恢复以停止和启动过程。默认情况下,任务处于暂停状态,因此它们必须最初恢复以开始处理。

当数据任务完成时,完成处理程序会回调三个参数:一个表示返回数据的 NSData 对象,一个表示远程 URL 服务器响应的 NSURLResponse 对象,以及一个可选的 NSError 对象,如果请求过程中发生任何错误。

在此基础上,上一章中创建的 SampleTable 可以通过获取会话、启动数据任务然后恢复来从网络 URL 加载数据。当数据可用时,完成处理程序会被调用,这可以用来将内容添加到表格中。

修改 SampleTable 类的 viewDidLoad 方法,通过在方法末尾添加以下代码来加载 SampleTable.json 文件:

let url = NSURL(string: "https://raw.githubusercontent.com/
  alblue/com.packtpub.swift.essentials/master/
  CustomViews/CustomViews/SampleTable.json")!
let session = NSURLSession.sharedSession()
let encoding = NSUTF8StringEncoding
let task = session.dataTaskWithURL(url,completionHandler:
 {data,response,error -> Void in
  let contents = String(data:data!,encoding:encoding)!
  self.items += [(url.absoluteString,contents)]
 // table data won't reload – needs to be on ui thread
  self.tableView.reloadData()
})
task.resume()

这将创建一个 NSURL 和一个 NSURLSession,然后创建数据、任务并立即恢复。内容下载完成后,会调用完成处理程序,将数据作为 NSData 对象传递。使用 String 初始化器从 NSData 对象中解码 UTF8 文本,并将其显式转换为 String 类型,以便将其添加到 items 数组中。

小贴士

NSURLSession 类还提供了其他工厂方法,包括一个接受配置参数的方法,该参数包括选项,例如是否应该缓存响应、网络连接是否应该通过蜂窝网络进行,以及是否应该将任何 cookie 或其他头信息与任务一起发送。

最后,将项目添加到 items 中,并重新加载 tableView 以显示新数据。请注意,如果不在主 UI 线程上运行,则不会立即生效;表格需要旋转或移动以重新绘制显示。在 UI 线程上运行将在本章后面的 网络和用户界面 部分进行介绍。

处理错误

错误是生活中不可避免的事实,尤其是在网络连接间歇性的移动设备上。完成处理程序会调用第三个参数,它表示在操作过程中引发的任何错误。如果这是 nil,则表示操作成功;如果不为 nil,则可以使用 errorlocalizedDescription 属性来通知用户。

为了测试目的,如果检测到错误,请将 localizedDescription 添加到列表中的 items。按照以下方式修改 viewDidLoad 方法:

let task = session.dataTaskWithURL(url, completionHandler:
 {data,response,error -> Void in
  if error == nil {
    let contents = String(data:data!,encoding:encoding)!
    self.items += [(url.absoluteString,contents)]
  } else {
 self.items += [("Error",error!.localizedDescription)]
 }
 // table data won't reload – needs to be on UI thread
  self.tableView.reloadData()
})

可以通过在 URL 中使用不存在的域名或未知协议来模拟错误。

处理缺失内容

如果无法联系远程服务器,例如当域名不正确或服务器关闭时,会报告错误。如果服务器正在运行,则不会报告错误;但仍然有可能请求的文件找不到,或者服务器在处理请求时遇到错误。这些错误会通过 HTTP 状态码报告。

注意

如果找不到 HTTP URL,服务器会返回一个 404 状态码。客户端可以使用这个状态码来判断是否应该访问不同的文件或者是否应该查询另一个服务器。例如,浏览器通常会请求一个 favicon.ico 文件,并使用它来显示一个小图标;如果这个文件缺失,则显示一个通用的页面图标。一般来说,4xx 响应是客户端错误,而 5xx 响应是服务器错误。

NSURLResponse对象没有 HTTP 状态码的概念,因为它可以用于任何协议,包括ftp。然而,如果请求使用了 HTTP,那么响应很可能是 HTTP,因此它可以转换为一个NSURLHttpResponse,它有一个statusCode属性。这可以用来在文件找不到时提供更具体的反馈。按照以下方式修改代码:

if error == nil {
  let httpResponse = response as! NSHTTPURLResponse
 let statusCode = httpResponse.statusCode
 if (statusCode >= 400 && statusCode < 500) {
 self.items += [("Client error \(statusCode)",
 url.absoluteString)]
 } else if (statusCode >= 500) {
 self.items += [("Server error \(statusCode)",
 url.absoluteString)]
 } else {
    let contents = String(data:data!,encoding:encoding)!
    self.items += [(url.absoluteString,contents)]
  }
} else {...}

现在,如果服务器响应但表示客户端提出了一个错误请求或服务器遇到了问题,用户界面将相应地更新。

嵌套的 if 和 switch 语句

有时,错误处理逻辑可能会因为处理不同的案例而变得复杂,尤其是当需要测试不同值时。在前面的章节中,需要检查NSError和 HTTP 的statusCode

使用带有where子句的switch语句是另一种方法。这些可以用来测试多个不同的条件,并显示正在测试的条件部分。尽管switch语句需要一个单一的表达式,但可以使用元组将多个值组合成一个单一表达式。

使用元组的一个优点是它允许根据类型进行情况匹配。在网络案例中,一些 URL 基于httphttps,这意味着响应将是一个NSHTTPURLResponse类型。然而,如果 URL 是不同类型(例如fileftp协议),那么它将是NSURLResponse的不同子类型。使用as无条件地转换到NSHTTPURLResponse在这些情况下将失败并导致崩溃。

测试可以重写为一个switch块,如下所示:

switch (data,response,error) {
  case (_,_,let e) where e != nil:
    self.items += [("Error",e.localizedDescription)]
  case (_,let r as NSHTTPURLResponse,_) 
   where r.statusCode >= 400 && r.statusCode < 500:
    self.items += [("Client error \(r.statusCode)",
     url.absoluteString)]
  // see note below
  case (_,let r as NSHTTPURLResponse,_) 
   where r.statusCode >= 500:
    self.items += [("Server error \(r.statusCode)",
      url.absoluteString)]
  default:
    let contents = String(data:data!,encoding:encoding)!
    self.items += [(url.absoluteString,contents)]
}

在这个例子中,default块用于执行成功条件,而之前的case语句用于匹配错误条件。

case (_,_,let e) where e != nil情况是一个条件模式匹配的例子。在 Swift 中,下划线被称为通配符模式(在其他语言中也称为空槽),它可以匹配任何值。第三个参数let e是一个值绑定模式,在这个情况下相当于let e = error。最后,where子句添加了测试以确保只有当e不是nil时,此情况才会发生。

小贴士

case语句中,可以使用标识符error而不是let e,使用case (_,_,_) where error != nil将产生相同的效果。然而,在switch语句外部捕获值用于情况匹配是不良的做法,因为如果error变量被重命名,那么case语句可能会变得无效。通常,在case语句中使用let模式以确保匹配正确的表达式值。

第二和第三个情况同时执行一个let赋值和一个类型测试/转换。当case (_,let r as NSHTTPURLResponse,_)匹配时,不仅元组中该部分的值被赋给常量r,它还被转换为NSHTTPURLResponse类型。如果该值不是NSHTTPURLResponse类型,则自动跳过该情况语句。这相当于一个带有is表达式的if测试,后面跟着一个使用as的转换。

虽然两种模式相同,但where子句不同。第一个where子句寻找r.statusCode为 400 或更大但小于 500 的情况,而第二个匹配的是r.statusCode为 500 或更大的情况。

小贴士

无论使用嵌套的if语句还是switch语句,执行测试的代码可能非常相似。这通常取决于开发者的偏好,但更多的开发者可能更熟悉嵌套的if语句。在 Swift 中,switch语句比其他语言更强大,因此这种模式可能变得更加流行。

在 Swift 2 中,可以使用guard语句来确保如果发生某些错误条件,可以采取适当的行动。guard语句类似于if语句,但没有true块,并且false块必须始终离开函数。例如,代码可以被重写为:

guard error == nil else {
  self.items += [("Error",error!.localizedDescription)]
  return
}
let statusCode = (response as! NSHTTPURLResponse).statusCode

guard statusCode < 500 else {
  self.items += [("Server error \(statusCode)",
    url.absoluteString)]
  return
}

guard statusCode < 400 else {
  self.items += [("Client error \(statusCode)",
   url.absoluteString)]
  return
}

let contents = String(data:data!,encoding:encoding)!
self.items += [(url.absoluteString,contents)]

请注意,guard块必须退出调用函数;因此,如果需要额外的操作,要么将实现体的主体移动到不同的函数中,要么使用switchif块。本章后面的示例假设使用if块以保持简单。

网络和用户界面

当前回调方法的一个突出问题是无法保证回调函数一定会在主线程中被调用。因此,用户界面操作可能无法正确执行或引发错误。正确的解决方案是使用主线程设置另一个调用。

在 Swift 中访问主线程的方式与在 Objective-C 中相同:使用Grand Central DispatchGCD)。可以使用dispatch_get_main_queue访问主队列,这是所有 UI 更新应使用的线程。后台任务通过dispatch_async提交到队列。要在主线程上调用reloadData,可以这样包装:

dispatch_async(dispatch_get_main_queue(), {
  self.tableView.reloadData()
})

这种调用方式对 Objective-C 和 Swift 都有效(尽管 Objective-C 使用^(尖括号)作为块前缀)。然而,Swift 为接受块的函数有特殊的语法;块可以被提升出函数的参数,并作为尾随参数留下。这被称为尾随闭包

dispatch_async(dispatch_get_main_queue()) {
  self.tableView.reloadData()
}

虽然这只是微小的差异,但它使得dispatch_async看起来更像是一个关键字,例如ifswitch,它接受一段代码块。这可以用于任何最终参数是函数的函数;在函数定义中不需要特殊语法。此外,相同的技巧也适用于在 Swift 外部定义的函数;在dispatch_async的情况下,该函数被定义为 C 语言函数,并且可以透明地以可移植的方式使用。

在主线程上运行函数

每当 UI 需要更新时,更新必须在主线程上运行。这可以通过使用之前的模式来完成,因为它们将始终在线程中执行。然而,每次需要时都记住这样做可能会很痛苦。

可以构建一个 Swift 函数,该函数接受另一个函数并在主线程上自动运行。NSThread.isMainThread可以用来确定当前线程是否是 UI 线程;因此,要在主线程上运行代码块,无论它是否在主线程上,可以使用以下方法:

func runOnUIThread(fn:()->()) {
  if NSThread.isMainThread() {
    fn()
  } else {
    dispatch_async(dispatch_get_main_queue(), fn)
  }
}

这允许使用以下方式将代码提交到后台线程:

self.runOnUIThread(self.tableView.reloadData)

小贴士

由于缺少括号,reloadData函数没有被调用,但它被作为函数指针传递。它在runOnUIThread函数内部被调度到正确的线程。

如果需要调用多个函数,可以创建一个内联块。由于这可以作为runOnUIThread方法的尾随闭包传递,因此括号是可选的:

self.runOnUIThread {
  self.tableView.backgroundColor = UIColor.redColor()
  self.tableView.reloadData()
  self.tableView.backgroundColor = UIColor.greenColor()
}

解析 JSON

在网络上发送结构化数据最流行的机制是将数据编码为JSON,代表JavaScript 对象表示法。这提供了一个分层树形数据结构,可以存储简单的数字、逻辑和基于字符串的类型,以及数组和字典表示。

Mac OS X 和 iOS 都内置了 JSON 文档的解析器,在NSJSONSerialization类中。这提供了一种解析数据对象并返回包含 JSON 对象键/值对的NSDictionary或表示 JSON 数组的NSArray的方法。其他字面量被解析,并作为NSNumberNSString值表示。

JSON 解析器使用JSONObjectWithData从包含字符串的NSData对象创建一个对象。这通常是网络 API 返回的格式,并且可以使用dataUsingEncoding与内置编码类型之一(如NSUTF8StringEncoding)从现有字符串创建。

一个简单的数字 JSON 数组可以按以下方式解析:

let array = "[1,2,3]".dataUsingEncoding(NSUTF8StringEncoding)!
let parsed = try? NSJSONSerialization.JSONObjectWithData(
  array, options:.AllowFragments)

这个返回类型是一个可选的AnyObject。可选性表示数据内容可能不是有效的 JSON 数据。可以使用as关键字将其转换为适当类型;如果解析失败,则会抛出错误。

可以使用 options 来指示返回类型是否应该是可变的。可变数据允许调用者在解析函数返回后添加或删除项目;如果没有指定,返回值将是不可变的。NSJSONReadingOptions 选项包括 MutableContainers(包含数据结构是可变的)、MutableLeaves(子叶是可变的)和 AllowFragments(允许解析非对象、非数组值)。

SampleTable.json 文件(在 viewDidLoad 方法中引用)存储一个条目数组,每个条目的 titlecontent 字段包含文本数据:

[{"title":"Sample Title","content":"Sample Content"}]

要解析 JSON 文件并将条目添加到表中,将 SampleTable 中的 default 子句替换为以下内容:

default:
  let parsed = try? NSJSONSerialization.JSONObjectWithData(
    data!, options:.AllowFragments) as! NSArray
  for entry in parsed {
    self.items += 
      [(entry["title"] as! String,
        entry["content"] as! String)]
  }

运行应用程序将在表中显示 样本标题样本内容 条目,这些条目已从书籍的 GitHub 仓库加载并解析。

处理错误

如果解析 JSON 数据时出现问题,则 try? JSONObjectWithData 函数的返回类型将为 nil 值。如果类型是隐式解包的,则访问元素将导致错误:

do {
 let parsed = try NSJSONSerialization.JSONObjectWithData(data!,
 options:.AllowFragments) {
  // do something with parsed
} catch let error as NSError {
  self.items += [("Error", 
   "Cannot parse JSON \(error.localizedDescription)")]
  // show message to user
}

parsed 值的类型将是 AnyObject?,尽管 let 块会隐式解包该值,这被称为 可选绑定。在上一节中,代码被直接转换为 NSArray,但如果返回的结果包含不同类型(例如,NSDictionary 或片段类型 NSNumberNSString),则尝试转换为与运行时类型不兼容的类型将导致失败。

可以使用 if [object] is [type] 来测试对象的类型。然而,由于下一步通常是将它转换为不同的类,使用 as,一个简写形式 as? 可以在一步中完成测试和转换:

 if let array = parsed as? NSArray {
  for entry in array {
    // process elements
  }
} else {
  self.items += [("Error", "JSON is not an array")]
}

switch 语句可以同时检查多个值的类型。由于这些值是可选的 AnyObject 对象,因此在 Swift 中使用之前需要将它们转换为 String

for entry in array {
  switch (entry["title"], entry["content"]) {
    case (let title as String, let content as String):
      self.items += [(title,content)]
    default:
      self.items += [("Error", "Missing unknown entry")]
  }
}

现在当应用程序运行时,任何错误都会被检测和处理,而不会导致应用程序崩溃。

解析 XML

虽然 JSON 更常用,但仍有许多基于 XML 的网络服务。幸运的是,iOS 自 5 以来就存在 XML 解析,在 NSXMLParser 类中,并且从 Swift 中访问很简单。例如,一些数据源(如博客文章)使用 XML 文档,如 Atom 或 RSS。

NSXMLParser 是一个面向流的解析器;也就是说,它会按顺序报告看到的各个元素。解析器调用 delegate 通知元素被看到和完成。当看到元素时,解析器还会包括任何存在的属性;对于文本节点,包括字符串内容。解析 XML 文件涉及解析器中的某些状态管理。本节中使用的示例将解析 Atom(新闻源)文件,其(简化后的)结构如下所示:

<feed >
  <title>AlBlue's Blog</title>
  <link href="http://alblue.bandlem.com/atom.xml" rel="self"/>
  <entry>
    <title type="html">QConLondon and Swift Essentials</title>
    <link href="http://alblue.bandlem.com/2015/01/qcon-swift-essentials.html"/>
    ... 
  </entry>
  ...
</feed> 

在这个情况下,目标是提取出所有来自源的数据元素entry,特别是titlelink。这会带来一些挑战,这些挑战将在稍后变得明显。

创建解析代理

解析 XML 文件需要创建一个遵守NSXMLParserDelegate协议的类。为此,创建一个新的类FeedParser,它扩展NSObject并遵守NSXMLParserDelegate协议。

它应该有一个init方法,该方法接受一个NSData,并且有一个items属性,用于在解析后获取结果:

class FeedParser: NSObject, NSXMLParserDelegate {
  var items:[(String,String)] = []
  init(_ data:NSData) {
    // parse XML
  }
}

小贴士

NSXMLParserDelegate协议要求对象也遵守NSObjectProtocol。最简单的方法是子类化NSObject。首先提到的超类型是父类;第二个和后续的超类型必须是协议。

下载数据

XML 解析器可以解析下载时的数据流,或者它可以接受之前已下载的NSData对象。在成功下载后,可以使用FeedParser解析NSData实例并返回项目列表。

虽然可以给单个表达式分配与上次相似的临时值,但可以将语句写在一行中(尽管请注意,这里没有错误处理)。将以下内容添加到SampleTable类的viewDidLoad方法末尾:

session.dataTaskWithURL(
  NSURL(string:"https://alblue.bandlem.com/Tag/swift/atom.xml")!,
  completionHandler: {data,response,error -> Void in
    if let data = data {
      self.items += FeedParser(data).items
      self.runOnUIThread(self.tableView.reloadData)
    }
}).resume()

这将下载作者博客中 Swift 文章的 Atom XML 源,博客地址为alblue.bandlem.com。目前,数据尚未解析,因此在此步骤中不会向表中添加任何内容。

小贴士

确保下载操作和解析都在主线程之外处理,因为这两个操作可能需要一些时间。一旦数据下载完成,就可以解析它,解析完成后,可以通知 UI 重新显示内容。

解析数据

为了处理下载的 XML 文件,需要解析数据。这涉及到编写一个解析代理来监听titlelink元素。然而,titlelink元素既存在于单个entry级别,也存在于博客的顶级。因此,在解析器中需要表示某种状态,以便检测解析器是否在entry元素内部,从而允许使用正确的值。

元素通过parser:didStartElement:方法和parser:didEndElement:方法报告。这可以用来确定解析器是否在entry元素内部,通过在entry元素开始时设置一个布尔值,并在元素结束时重置它。将以下内容添加到FeedParser类中:

var inEntry:Bool = false
func parser(parser: NSXMLParser,
 didStartElement elementName: String,
 namespaceURI: String?, qualifiedName: 
 String?, attributes: [String:String]) {
  switch elementName {
    case "entry":
      inEntry = true
    default: break
  }
}

link将元素的引用值存储在元素的href属性中。这会在调用开始元素时传递,因此存储是微不足道的。在此点,标题可能未知,因此link的值必须存储在可选字段中:

var link:String?
...
// in parser:didStartElement method
case "entry":
  inEntry = true
case "link":
 link = attributes["href"]
default break;

title 将其数据存储为文本节点,需要使用另一个布尔标志来指示解析器是否在 title 节点内部。文本节点通过 parser:foundCharacters: 代理方法报告。将以下内容添加到 FeedParser

var title:String?
var inTitle: Bool = false
...
// in parser:didStartElement method
case "entry":
  inEntry = true
case "title":
 inTitle = true
case "link":
...
func parser(parser: NSXMLParser, foundCharacters string:String) {
 if inEntry && inTitle {
 title = string
 }
}

当看到 entry 元素的末尾时,将 titlelink 存储为可选字段,可以将这些字段附加到 items 列表中,然后重置解析器的状态:

func parser(parser: NSXMLParser,
 didEndElement elementName: String,
 namespaceURI: String?, qualifiedName: String?) {
  switch elementName {
    case "entry":
      inEntry = false
      if title != nil && link != nil {
        items += [(title!,link!)]
      }
      title = nil
      link = nil
    case "title":
      inTitle = false
    default: break
  }
}

最后,在实现了回调方法之后,接下来的步骤是从之前传递的数据中创建一个 NSXMLParser,设置 delegate(以及可选的命名空间处理),然后调用解析器:

init(_ data:NSData) {
  let parser = NSXMLParser(data: data)
  parser.shouldProcessNamespaces = true
  super.init()
  parser.delegate = self
  parser.parse()
}

小贴士

在调用 super.init 之后才能将 self 赋值给 delegate

现在当应用程序运行时,将显示一组新闻源项目。

小贴士

如果在 iOS 9 目标上运行并从 http 网站下载,控制台可能会看到 App Transport Security 已阻止明文 HTTP 资源加载 的消息。修复此问题的解决方案是在 Info.plist 文件中添加一个异常,允许通过 HTTP 进行连接,无论是显式域名还是所有域名。在第一个 <dict> 元素之后添加以下内容到 Info.plist

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

现在当应用程序运行时,错误应该不再出现。

直接网络连接

虽然大多数应用程序网络将涉及通过标准协议(如 HTTP(S))下载内容,并使用标准表示,但有时需要特定的数据流协议。在这种情况下,导向的过程将允许读取或写入单个字节,或者可以使用数据报数据包导向的过程来发送单个数据包。

有网络库支持这两种情况;一个基于 Objective-C 的高级 NSStream 类提供了一个驱动基于流的响应的机制,尽管使用 CoreFoundationPOSIX 层可以实现低级别的数据包连接,但使用 MultipeerConnectivity 模块进行本地多人游戏通常是合适的。

注意

使用 MultipeerConnectivity 模块进行本地网络涉及创建一个 MCSession,然后通过 sendData 发送 NSData 对象到已连接的节点,并使用 MCSessionDelegate 从已连接的节点接收数据。这通常用于同步世界的状态,例如玩家的当前位置或健康。

打开基于流的连接

流是一个可靠、有序的字节序列,大多数互联网协议都使用流。可以使用 NSStream 类的 getStreamsToHostWithName 方法从网络主机和端口创建流。这允许同时获取 NSInputStreamNSOutputStream

注意

由于这是一个现有的 Objective-C API,流通过 输入输出参数 返回。在 Swift 中,这表示参数通过 ampersand (&) 返回,并将变量声明为可选的。

然后,可以使用这些输入/输出流异步或同步地发送数据。异步机制涉及在应用程序的运行循环上调度数据处理,这在 异步读写 部分有所介绍。同步机制使用 readwrite 来接收或发送数据缓冲区。

小贴士

一旦获取了流,它们需要被 打开 以接收或发送数据。忘记这一步将导致没有网络数据被发送。

为了简化获取流,可以将以下内容作为 NSStream 类的扩展来创建。扩展使方法看起来来自原始类,但实际上是在类外部实现的。在 CustomViews 项目中添加一个 StreamExtensions.swift 文件,内容如下:

extension NSStream {
  class func open(host:String,_ port:Int)
   -> (NSInputStream, NSOutputStream)? {
    var input:NSInputStream?
    var output:NSOutputStream?
    NSStream.getStreamsToHostWithName(
      host, port: port, 
      inputStream: &input,
      outputStream: &output)
    guard let i = input, o = output else {
      return nil
    }
    o.open()
    i.open()
    return (i,o)
  }
}

通过调用 NSStream.open(host,port) 可以获得对远程主机的连接,该方法返回一个打开的输入/输出流对。

同步读写

NSInputStreamread 方法允许同步地从流中读取字节,而 NSOutputStreamwrite 方法允许将字节写入流。它们接受不同的类型,但在 Swift 中最常见的方法是创建一个字节数组 [UInt8] 作为缓冲区,然后使用 UnsafeMutablePointer(在 C 中相当于 ampersand)读取到或从中。

readwrite 方法都返回读取/写入的字节数。这可能为负(错误情况),为零,或者为正(已处理字节的情况)。这两个调用都接受一个缓冲区和最大长度,尽管不能保证会处理完整的最大长度。

小贴士

总是检查 writeread 的返回值,因为可能只有缓冲区的一部分被写入。对于同步连接,最佳实践是使用 while 循环包装调用或具有其他形式的 retry,以确保所有数据都被写入。

向 NSOutputStream 写入数据

为了更容易将 NSData 内容写入流,可以在 NSOutputStream 上创建一个扩展方法,该方法根据数据的大小执行完整写入:

extension NSOutputStream {
  func writeData(data:NSData) -> Int {
    let size = data.length
    var completed = 0
    while completed < size {
      let wrote = write(UnsafePointer(data.bytes) +
       completed, maxLength:size - completed)
      if wrote < 0 {
        return wrote
      } else {
        completed += wrote
      }
    }
    return completed
  }
}

此代码接受一个 NSData 并将其写入底层流,返回写入的字节数(如果有问题,则返回负值)。检查 write 方法的返回值,如果值为负,则直接返回给调用者。否则,使用写入的字节数增加 completed 计数器。

如果写入的字节数达到请求的数据大小,则返回该值。否则,循环递归,这次从上次离开的点开始。

注意

虽然在 Swift 中不常见,但可以通过获取 data.bytes 数组的 UnsafePointer 来执行指针算术,然后通过已写入的字节数增加它。剩余字节的长度通过 size-completed 计算。

从 NSInputStream 读取

可以使用类似的方法从 NSInputStream 读取完整缓冲区,通过创建一个返回已知大小字节数组的 readBytes 方法,以及将其转换为 NSData 以便于处理/解析的方法:

extension NSInputStream {
  func readBytes(size:Int) -> [UInt8]? {
    let buffer = Array<UInt8>(count:size,repeatedValue:0)
    var completed = 0
    while completed < size {
      let read = self.read(
       UnsafeMutablePointer(buffer) + completed,
       maxLength: size - completed)
      if read < 0 {
        return nil
      } else {
        completed += read
      }
    }
    return buffer
  }
  func readData(size:Int) -> NSData? {
    if let buffer = readBytes(size) {
      return NSData(
       bytes: UnsafeMutablePointer(buffer),
       length: buffer.count)
    } else {
      return nil
    }
  }
}

readData 方法返回一个 NSData,而 readBytes 方法返回一个 UInt8 值的数组。使用 NSData 方法在某些情况下很有用(尤其是,从返回的数据中创建一个 String),而在其他情况下,能够直接处理字节很有用(例如,解析二进制格式)。两者都存在使得可以根据需要使用任一方法。

提示

同步读取可能会永远阻塞;如果客户端应用程序请求恰好 10 个字节,但服务器只发送了 9 个字节,那么它将永久挂起,直到发送了第十个字节。最佳实践是使用异步读取,这样就不会以这种方式阻塞。

读取和写入十六进制和 UTF8 数据

能够将数据作为 UTF8 值或十六进制值处理在某些协议中可能很有用。尽管 NSStringNSData 都提供了转换到和从 UTF8 的方法,但其语法过于冗长,因为它基于现有的 Objective-C 方法。

为了便于转换,可以创建扩展方法以提供简单地将转换到和从 UTF8 表示的方法。除了类和实例函数之外,还可以使用扩展向现有对象添加动态属性。这可以通过在文件 Extensions.swift 中添加扩展来实现,如下所示:

extension NSData {
  var utf8string:String {
    return String(data:self,
     encoding:NSUTF8StringEncoding)!
  }
}
extension String {
  var utf8data:NSData {
    return self.dataUsingEncoding(
      NSUTF8StringEncoding, allowLossyConversion: false)!
  }
}

这允许使用更紧凑的表达式,例如 data.utf8stringstring.utf8data。每次表达式被评估时,相关的 getter 函数将被调用。

提示

在本书编写时,Swift 中没有命名扩展的标准约定。如果有对单一类型数据的扩展——例如之前提到的流——那么文件可以命名为 [Type]Extensions.swift。或者,可以使用该名称来命名被调用的方法类型;例如,在这种情况下,可以使用 UTF8Extensions.swift

可以将解析十六进制数据从字符串和整数添加到 StringInt 类型中,如下所示:

extension String {
  func fromHex() -> Int {
    var result = 0
    for c in self.characters {
      result *= 16
      switch c {
      case "0":result += 0      case "1":result += 1
      case "2":result += 2      case "3":result += 3
      case "4":result += 4      case "5":result += 5
      case "6":result += 6      case "7":result += 7
      case "8":result += 8      case "9":result += 9
      case "a","A":result += 10 case "b","B":result += 11
      case "c","C":result += 12 case "d","D":result += 13
      case "e","E":result += 14 case "f","F":result += 15
      default: break
      }
    }
    return result;
  }
}
extension Int {
  func toHex(digits:Int) -> String {
    return String(format:"%0\(digits)x",self)
  }
}

这允许使用 int.toHexstring.fromHex 创建十六进制值。

实现 Git 协议

可以编写一个客户端,使用 git:// 协议查询远程 git 服务器,以确定远程标签/分支/引用的哈希值。

注意

git:// 协议通过发送带有 ASCII 码前缀的四位十六进制数字的数据包行来工作,这些数字表示其余数据(包括这四个初始数字)的长度。发送 git-upload-pack 请求将返回远程仓库上的引用列表。

由于 git:// 协议使用数据包行,因此创建一个 PacketLineExtensions.swift 文件,内容如下:

extension NSOutputStream {
  func writePacketLine(message:String = "") -> Int {
    let data = message.utf8data
    let length = data.length
    if length == 0 {
      return writeData("0000".utf8data)
    } else {
      let prefix = (length + 4).toHex(4).utf8data
      return self.writeData(prefix) + self.writeData(data)
    }
  }
}

当传递一个空的 NSData 对象时,将写入特殊的包行 0000,表示对话结束。当写入非空的 NSData 时,数据长度以十六进制值(包括用于长度的 4 个字节)表示,然后是数据本身。

注意

这将导致以下协议对话:

> 004egit-upload-pack /alblue/com.packtpub.swift.essentials.git\0host=github.com\0
< 00dfadaa46b98ce211ff819f0bb343395ad6a2ec6ef1 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master agent=git/2:2.1.1+github-611-gd89bd9f
< 003fadaa46b98ce211ff819f0bb343395ad6a2ec6ef1 refs/heads/master
> 0000
< 0000

读取数据包行类似:

extension NSInputStream {
  func readPacketLine() -> NSData? {
    if let data = readData(4) {
      let length = data.utf8string.fromHex()
      if length == 0 {
        return nil
      } else {
        return readData(length - 4)
      }
    } else {
      return nil
    }
  }
  func readPacketLineString() -> NSString? {
    if let data = self.readPacketLine() {
      return data.utf8string
    } else {
      return nil
    }
  }
}

在这种情况下,读取前 4 个字节以确定剩余长度。如果它是零,则返回 nil 值以指示流结束。如果它不是零,则读取数据(减去用于数据包行长度头的 4 个字节)。还提供了一个 readPacketLineString 以便于轻松创建作为 NSString 的数据包行。

远程列出 git 引用

要远程查询 git 仓库的引用,需要发送 git-upload-pack 命令,并附带要查询的仓库的引用,以及可选的主机。为了提供一种程序化查询的 API,创建一个 RemoteGitRepository 类,该类具有一个初始化器,用于存储主机、端口和仓库,以及一个 lsRemote 函数,该函数返回引用的值:

class RemoteGitRepository {
  let host:String
  let repo:String
  let port:Int
  init(host:String, repo:String, _ port:Int = 9418) {
    self.host = host
    self.repo = repo
    self.port = port
  }
  func lsRemote() -> [String:String] {
    var refs = [String:String]()
    // load the data
    return refs
  }
}

要从仓库加载数据,需要在默认端口(在这种情况下,9418git:// 协议的默认端口)上连接到远程主机。一旦流被打开,发送 git-upload-pack [repository]\0host=[host]\0 数据包行,然后可以读取形式为 hash reference 的行。将以下内容添加到 lsRemote 函数中:

// load the data
if let (input,output) = NSStream.open(host,port) {
  output.writePacketLine(
   "git-upload-pack \(repo)\0host=\(host)\0")
  while true {
    if let response = input.readPacketLineString() {
      let hash = String(response.substringToIndex(41))
      let ref = String(response.substringFromIndex(41))
      if ref.hasPrefix("HEAD") {
        continue
      } else {
        refs[ref] = hash
      }
    } else {
      break
    }
  }
  output.writePacketLine()
  input.close()
  output.close()
}

在一个具有适当的 hostrepoRemoteGitRepository 实例上调用 lsRemote 函数将返回一个通过引用的哈希列表。

将网络调用集成到 UI 中

由于网络可能会引入延迟,甚至可能导致完全失败,因此网络调用不应在 UI 线程上执行。以前,SampleTable 被用来引入一个 runOnUIThread 函数。可以使用类似的方法在后台线程上运行函数。将以下内容添加到 SampleTable 类中:

func runOnBackgroundThread(fn:()->()) {
  dispatch_async(
   dispatch_get_global_queue(
    DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
   ,fn)
}

这将允许 viewDidLoad 调用一个调用,以从仓库查询远程引用并将其添加到表中。与之前一样,必须从 UI 线程调用更新表的调用。将以下内容添加到 viewDidLoad 方法的末尾:

runOnBackgroundThread {
  let repo = RemoteGitRepository(host: "github.com", 
   repo: "/alblue/com.packtpub.swift.essentials.git")
  for (ref,hash) in repo.lsRemote() {
    self.items += [(ref,hash)]
  }
  self.runOnUIThread(self.tableView.reloadData)
}

现在当应用程序启动时,应将远程仓库中对应的分支和标签条目添加到表中。

异步读写

除了同步的读写操作,还可以执行异步读写操作。应用程序不需要在while循环中旋转,而是可以使用在应用程序的运行循环上安排的回调。

要接收回调,必须创建一个实现了NSStreamDelegate的类并将其分配给流的delegate字段。当事件发生时,stream方法会根据事件类型和关联的流被调用。

流通过scheduleInRunLoop(使用NSRunLoop.mainRunLoop()NSDefaultRunLoopMode模式)进行注册。最后,流可以被打开。

小贴士

如果在设置代理或调度到运行循环之前打开流,则不会传递事件。

事件在NSStreamEvent类中定义,包括HasSpaceAvailable(用于输出流)和HasBytesAvailable(用于输入流)。通过响应回调,应用程序可以异步处理结果。

小贴士

当使用 Swift 时,NSStreamDelegate在输入流或输出流上被视为一个weak代理。当使用内联类提供输入解析时,这会引发问题;这样做会导致EXC_BAD_ACCESS,因为代理会被运行时自动回收。这可以通过在初始化器中存储对self的强循环引用并在流关闭时将其赋值为nil来避免。

从 NSInputStream 异步读取数据

这对于异步协议特别有用,例如 XMPP,它可能在任意时间发送额外的消息。这也允许电池供电的设备在远程服务器慢或挂起时不需要旋转 CPU。

要异步接收数据,代理必须实现NSStreamDelegate方法stream(stream:handleEvent)。当数据可用时,会发送HasBytesAvailable事件,然后可以相应地读取数据。

为了将之前的示例转换为异步形式,需要做一些更改。首先,在打开流连接部分创建的open扩展方法需要增加一个connect方法,但不立即执行open

class func open(host:String,_ port:Int)
 -> (NSInputStream, NSOutputStream)? {
  if let (input,output) = connect(host,port) {
    input.open()
    output.open()
    return (input,output)
  } else {
    return nil
  }
}
class func connect(host:String,_ port:Int)
  -> (NSInputStream, NSOutputStream)? {
    var input:NSInputStream?
    var output:NSOutputStream?
    NSStream.getStreamsToHostWithName(
      host, port: port, 
      inputStream: &input,
      outputStream: &output)
    guard let i = input, o = output else {
      return nil
    }
    return (i,o)
  } 
}

小贴士

为了异步接收事件,必须在流打开之前设置代理并将流调度到运行循环中。

创建流代理

要创建一个流代理,创建一个名为PacketLineParser.swift的文件,并包含以下内容:

class PacketLineParser: NSObject, NSStreamDelegate {
  let output:NSOutputStream
  let callback:(NSString)->()
  var capture:PacketLineParser?
  init(_ output:NSOutputStream, _ callback:(NSString) -> ()) {
    self.output = output
    self.callback = callback
    super.init()
    capture = self
  }
  func stream(stream: NSStream, handleEvent: NSStreamEvent) {
    let input = stream as! NSInputStream
    if handleEvent == NSStreamEvent.HasBytesAvailable {
      if let line = input.readPacketLineString() {
        callback(line)
      } else {
        output.writePacketLine()
        input.close()
        output.close()
        capture = nil
      }
    }
  }
}

此解析器有一个回调,对于读取的每个数据包行都会被调用;当发送HasBytesAvailable事件时,行会被读取(使用与之前相同的同步机制)然后传递给回调。与同步方法不同,这里没有while循环——当数据可用时,它触发数据的解析。

小贴士

由于这将分配给一个输入流代理(它持有弱引用),因此需要使用 capture = self 来捕获自身的循环引用,以避免运行时从实例中移除。当流关闭时,capture 将被设置为 nil,从而释放实例。

readPacketLine 返回 nil 以指示错误或完成的流;在这种情况下,发送一个空的数据包行(以告知远程服务器不需要进一步交互),然后关闭两个流。

处理错误

在流内容成功和发生通信错误时,都需要清理流并将它们从运行循环中移除。除了 HasBytesAvailable 事件外,还有在遇到流的末尾或发生错误时发送的事件。

这些应该以连接自然结束时相同的方式进行处理;资源应该被整理,特别是流应该从运行循环处理中移除。最后,应该移除循环引用,以便可以移除 delegate 对象。

可以将现有的 close 代码移动到它自己的单独函数中,并且当流结束或发生错误时,可以执行相同的清理操作:

func stream(stream: NSStream, handleEvent: NSStreamEvent) {
  let input = stream as! NSInputStream
  if handleEvent == NSStreamEvent.HasBytesAvailable {
    if let line = input.readPacketLineString() {
      callback(line)
    } else {
      closeStreams(input,output)
    }
  }
  if handleEvent == NSStreamEvent.EndEncountered 
 || handleEvent == NSStreamEvent.ErrorOccurred {
 closeStreams(input,output)
 }
}
func closeStreams(input:NSInputStream,_ output:NSOutputStream) {
  if capture != nil {
    capture = nil
    output.removeFromRunLoop(NSRunLoop.mainRunLoop(),
     forMode: NSDefaultRunLoopMode)
    input.removeFromRunLoop(NSRunLoop.mainRunLoop(),
     forMode: NSDefaultRunLoopMode)
    input.delegate = nil
    output.delegate = nil
    if output.streamStatus != NSStreamStatus.Closed {
      output.writePacketLine()
      output.close()
    }
    if input.streamStatus != NSStreamStatus.Closed {
      input.close()
    }
  }
}

异步列出引用

要异步提供引用列表,代理需要设置一个合适的回调来解析返回的数据。而不是方法返回一个字典(这将需要同步阻塞),将传递一个回调,可以在找到引用时调用。

注意

请注意,有两个独立的回调:PacketLineParser 回调(它读取网络数据,并在每个数据包行的基础上返回 NSString 实例),以及引用解析回调(它将 NSString 转换为 (String,String) 元组)。

要开始这个过程,需要在同步发送 git-upload-pack 之后异步处理后续响应。这可以通过在 RemoteGitRepository 类中创建一个新的方法 lsRemoteAsync 来实现,该方法接受一个用于 (String,String) 元组的回调函数:

func lsRemoteAsync(fn:(String,String) -> ()) {
  if let (input,output) = NSStream.connect(host,port) {
    input.delegate = PacketLineParser(output) {
    (response:NSString) -> () in
      let hash = String(response.substringToIndex(41))
      let ref = String(response.substringFromIndex(41))
      if !ref.hasPrefix("HEAD") {
        fn(ref,hash)
      }
    }
    input.scheduleInRunLoop(NSRunLoop.mainRunLoop(), 
     forMode: NSDefaultRunLoopMode)
    input.open()
    output.open()
    output.writePacketLine(
     "git-upload-pack \(repo)\0host=\(host)\0")
  }
}

这将创建一个连接(但不会打开流),设置 delegate,并为输入流安排运行循环,最后打开两个流以进行交互。完成这些操作后,就像之前一样发送初始的 git-upload-pack 消息。此时,lsRemoteAsync 方法返回,随后在从服务器接收到输入数据时发生后续事件。

当通过 PacketLineParser 回调接收到一行时,它将被分割成一个引用和一个哈希值,然后把结果交给最初在参数中传入的回调。

注意

异步编程通常涉及许多回调。与看起来像 A;B;C; 的同步程序不同,异步程序通常看起来像 A(callback:B(callback:C))。当发生输入触发——网络请求、用户交互或定时器触发时,可以通过这些嵌套回调发生一系列操作。

由于在 while 循环中阻塞会浪费 CPU 能量直到条件满足,因此出于电池性能的原因,通常更喜欢异步管道。

在 UI 中显示异步引用

要在屏幕上显示异步数据,必须修改回调以允许单个元素更新 GUI。

SampleTable 中,不要调用 repo.lsRemote(它执行同步查找),而应使用 repo.lsRemoteAsync。这需要一个回调,它可以用来更新表格数据并导致视图重新加载内容:

// for (ref,hash) in repo.lsRemote() {
//   self.items += [(ref,hash)]
// }
repo.lsRemoteAsync() { (ref:String,hash:String) in
  self.items += [(ref,hash)]
  self.runOnUIThread(self.tableView.reloadData)
}

现在当应用程序运行时,引用将被异步更新,UI 不会被缓慢或挂起的服务器阻塞。

将数据异步写入 NSOutputStream

除非需要大文件上传,否则异步发送不如异步读取有用。如果有大量数据,那么在单个 write 调用中不太可能同步写入。最好异步执行任何额外的写入。

要异步写入数据,需要将 completed 计数存储为函数外的变量。write 方法可以用来替换之前的 while 循环,通过在流方法的每次迭代中写入数据的一个片段。尽管在这个例子中不需要代码,代码可能看起来像这样:

…
self.data = data
// initial write to kick off subsequent events
completed = output.write(UnsafePointer(data.bytes), 
 maxLength: data.length
…
var completed:Int
var data:NSData?
func stream(stream: NSStream, handleEvent: NSStreamEvent) {
  let output = stream as! NSOutputStream
  if handleEvent == NSStreamEvent.HasSpaceAvailable 
   && data != nil {
    let size = data!.length
    completed += output.write(
     UnsafePointer(data!.bytes) + completed,
     maxLength: size – completed)
    if completed == size {
      completed = 0
      data = nil
    }
  }
}

异步数据始终以同步写入数据的调用开始。如果并非所有数据都被写入(换句话说,completed < size),则后续回调将在 NSStreamDelegate 上发生。然后可以使用与同步情况类似的技术来继续 data 值,但不使用 while 循环。而不是迭代阻塞以写入整个数据值,流调用将被多次调用(实际上替换了 while 循环的每次迭代)。在最后一次运行时,当 completed == size 时,数据被释放,完成计数器被重置。

注意

流回调被调用足够多次以写入所有数据。如果没有数据被写入,则事件不再被调用。只有当传递了额外的值时,才会写入新数据。在从不同线程写入数据时必须小心,因为数据值被处理为一个实例变量,覆盖它可能会导致数据丢失。读者被邀请将单个元素数据扩展到未完成数据元素的数组中,以便它们可以适当地排队。

摘要

本章介绍了在基于 Swift 的应用程序中处理网络数据时常用的技术,特别关注如何使用异步技术访问数据来最大化便携设备的电池使用。

由于大多数网络请求很可能会通过 HTTP(S)提供基于 JSON 或 XML 的表示,本章的第一节涵盖了使用NSURLSession和异步的dataTask操作从远程服务器拉取数据。然后第二和第三部分介绍了如何根据所需格式从 JSON 或 XML 中解析这些数据。

最后一节展示了如何直接建立网络连接来处理除 HTTP 之外的协议;作为一个例子,展示了如何执行远程git命令以找出远程 git 仓库中可用的引用。这以两种形式呈现:首先作为一个同步 API(以展示如何与流一起工作的技术,并解释 git 协议),然后转换为异步 API,这可以用来最小化 CPU 周期,从而降低电池使用,以便将来执行其他此类转换。

下一章将展示如何将本书中涵盖的所有想法整合到一个 iOS 应用程序中,以显示 GitHub 仓库。

第七章:构建仓库浏览器

在介绍了如何集成构建应用程序所需的组件后,本章将创建一个仓库浏览器,允许使用 GitHub API 显示用户仓库。

本章将介绍以下主题:

  • GitHub API 概述

  • 使用 Swift 与 GitHub API 通信

  • 创建仓库浏览器

  • 在视图控制器之间保持选择

GitHub API 概述

GitHub API 提供了一个基于 REST 的接口,使用 JSON 返回有关用户和仓库的信息。API 的第 3 版在 developer.github.com/v3/ 有文档说明,并且是本书中使用的版本。

小贴士

API 有速率限制;在撰写本文时,匿名请求每小时最多可以发出六十次,而登录用户有更高的限制。本书的代码仓库有用于测试和开发目的的示例响应。

根端点

GitHub 的主要入口点是 根端点。对于主 GitHub 网站,这是 api.github.com,而对于 GitHub Enterprise 安装,它将是 https://hostname.example.org/api/v3/ 的形式,并带有用户凭据。端点提供了一组可用于查找特定资源的 URL:

{
 ...
  "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
  "issues_url": "https://api.github.com/issues",
  "repository_url": "https://api.github.com/repos/{owner}/{repo}","user_url": "https://api.github.com/users/{user}"  "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", }

这些服务是 URI 模板。花括号 {} 中的文本将在需要时用参数的值替换;以 {?a,b,c} 开头的文本如果存在,则展开为 ?a=&b=&c=,否则不展开。例如,对于 useralblue 的情况,用户资源在 https://api.github.com/users/{user} 上的 user_url 变为 https://api.github.com/users/alblue

用户资源

特定用户的用户资源包含有关其仓库 (repos_url)、姓名以及其他信息,例如位置和博客(如果提供)。此外,avatar_url 提供了一个可以用来显示用户头像的 URL。例如,https://api.github.com/users/alblue 包含:

{
  ...
  "login": "alblue",
  "avatar_url": "https://avatars.githubusercontent.com/u/76791?v=2",
  "repos_url": "https://api.github.com/users/alblue/repos",
  "name": "Alex Blewitt",
  "blog": "http://alblue.bandlem.com",
  "location": "Milton Keynes, UK",
  ...
}

可以使用 repos_url 链接来查找用户的仓库。这是在根端点报告的 user_repositories_url,其中 {user} 已经替换为用户名。

仓库资源

用户仓库可以通过 repos_urluser_repositories_url 引用访问。这返回一个包含信息的 JSON 对象数组,例如:

[{ 
  "name": "com.packtpub.e4.swift.essentials",
  "html_url":
    "https://github.com/alblue/com.packtpub.swift.essentials",
  "clone_url":
    "https://github.com/alblue/com.packtpub.swift.essentials.git",
  "description": "Swift Essentials",
},{
  "name": "com.packtpub.e4",
  "html_url":
    "https://github.com/alblue/com.packtpub.e4",
  "clone_url":
    "https://github.com/alblue/com.packtpub.e4.git",
  "description":
    "Eclipse Plugin Development by Example: Beginners Guide",
},{
  "name": "com.packtpub.e4.advanced",
  "html_url":
    "https://github.com/alblue/com.packtpub.e4.advanced",
  "clone_url":
    "https://github.com/alblue/com.packtpub.e4.advanced.git",
  "description":
    "Advanced Eclipse plug-in development",
}...]

仓库浏览器项目

RepositoryBrowser 客户端将从 主从模板 创建。这设置了一个空的应用程序,可以在大设备上使用分割视图控制器,或在小型设备上使用导航视图控制器。此外,还会创建添加条目的操作。

要创建带有测试的项目,确保在创建项目时选择 包含单元测试 选项:

仓库浏览器项目

要构建显示内容所需的 API,需要几个实用类:

  • URITemplate 类使用一组键/值对处理 URI 模板

  • Threads 类允许函数在后台或主线程中运行

  • NSURLExtensions 类提供了从 URL 解析 JSON 对象的简便方法

  • DictionaryExtensions 类提供了一种从 JSON 对象创建 Swift 字典的方法

  • GitHubAPI 类提供了对 GitHub 远程 API 的访问

URI 模板

URI 模板在 RFC 6570 中定义,见 tools.ietf.org/html/rfc6570。它们可以用来替换 URI 中由 {} 包围的文本序列。尽管 GitHub 的 API 使用可选值 {?...},但本章中展示的示例客户端不需要使用这些,因此在本实现中可以忽略它们。

模板类使用字典中的值替换参数。要创建 API,首先编写一个测试用例是有用的,遵循测试驱动开发。可以通过导航到 文件 | 新建 | 文件… | iOS | | 单元测试用例类 来创建一个 XCTestCase 的子类,在 Swift 中创建一个测试用例类。测试代码将类似于:

import XCTest
class URITemplateTests: XCTestCase {
  func testURITemplate() {
    let template = "http://example.com/{blah}/blah/{?blah}"
    let replacement = URITemplate.replace(
     template,values: ["blah":"foo"])
    XCTAssertEqual("http://example.com/foo/blah/",
     replacement,"Template replacement")
  }
}

提示

不要忘记确保将 URITemplateTests.swift 文件添加到必要的测试目标中。

replace 函数需要字符串处理。虽然该函数可以是类函数或 String 的扩展,但将其作为一个单独的类可以使测试更容易。函数签名看起来像:

import Foundation
class URITemplate {
  class func replace(template:String, values:[String:String])
   -> String {
    var replacement = template
    while true {
      // replace until no more {…} are present
    }
    return replacement
  }
}

提示

确保将 URITemplate 类也添加到测试目标中;否则,测试脚本将无法编译。

参数是通过正则表达式进行匹配的,例如 {[^}]}。要从字符串中搜索或访问这些,涉及到 String.Index 值的 Range。这些类似于字符串中的整数索引,但与通过字节偏移量引用字符不同,索引是一个抽象表示(某些字符编码,如 UTF8,使用多个字节来表示单个字符)。

rangeOfString 方法接受一个字符串或正则表达式,如果存在匹配项则返回一个范围(如果没有匹配项则返回 nil)。这可以用来检测是否存在模式或从 while 循环中退出:

// replace until no more {…} are present
if let parameterRange = replacement.rangeOfString(
  "\\{[^}]*\\}",
  options: NSStringCompareOptions.RegularExpressionSearch) {
  // perform a replacement of parameterRange
} else {
  break
}

parameterRange 包含一个表示 {} 字符位置的 startend 索引。可以使用 replacement.substringWithRange(parameterRange) 提取参数值。如果它以 {? 开头,则替换为空字符串:

// perform a replacement of parameterRange
var value:String
let parameter = replacement.substringWithRange(parameterRange)
if parameter.hasPrefix("{?") {
  value = ""
} else {
  // substitute with real replacement
}
replacement.replaceRange(parameterRange, with: value)

最后,如果替换的形式是 {user},则从字典中获取 user 的值并将其用作替换值。要获取参数的名称,startIndex 必须前进到 successor,而 endIndex 必须反转到 predecessor 以考虑 {} 字符:

// substitute with real replacement
let start = parameterRange.startIndex.successor()
let end = parameterRange.endIndex.predecessor()
let name = replacement.substringWithRange(
 Range<String.Index>(start:start,end:end))
value = values[name] ?? ""

现在当测试通过导航到 产品 | 测试 或按 Command + U 运行时,字符串替换将通过。

注意

?? 是一个可选测试,用于在存在时返回第一个参数,如果它是 nil,则返回第二个参数。

后台线程

后台线程允许函数在适当的 UI 线程或后台线程上轻松启动。这已在 第六章,解析网络数据,在 网络和用户界面 部分中解释过。将以下内容作为 Threads.swift 添加:

import Foundation
class Threads {
  class func runOnBackgroundThread(fn:()->()) {
    dispatch_async(dispatch_get_global_queue(
     DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),fn)
  }
  class func runOnUIThread(fn:()->()) {
    if NSMainThread.isMainThread() {
      fn()
    } else {
      dispatch_async(dispatch_get_main_queue(), fn)
    }
  }
}

可以使用以下测试用例测试 Threads 类:

import XCTest
class ThreadsTest: XCTestCase {
  func testThreads() {
    Threads.runOnBackgroundThread {
      XCTAssertFalse(NSThread.isMainThread(), 
       "Running on background thread")
      Threads.runOnUIThread {
        XCTAssertTrue(NSThread.isMainThread(),
         "Running on UI thread")
      }
    }
  }
}

当测试使用 Command + U 运行时,测试应该通过。

解析 JSON 字典

由于许多网络响应以 JSON 格式返回,为了使 JSON 解析更容易,可以向 NSURL 类添加扩展,以方便从网络位置获取和解析内容。而不是设计一个同步扩展,它会阻塞直到数据可用,使用回调函数是最佳实践。创建一个名为 NSURLExtensions.swift 的文件,并包含以下内容:

import Foundation
extension NSURL {
  func withJSONDictionary(fn:[String:String] -> ()) {
    let session = NSURLSession.sharedSession()
    session.dataTaskWithURL(self) {
      data,response,error -> () in
      if let json = try? NSJSONSerialization.JSONObjectWithData(
        data!, options: .AllowFragments) as? [String:AnyObject] {
        fn(json!) // will give a compile time error
      } else {
        fn([String:String]())
      }
    }.resume()
  }
}

这为 NSURL 提供了一个扩展,以提供 JSON 字典。然而,从 JSONObjectWithData 方法返回的数据类型是 [String:AnyObject],而不是 [String:String]。尽管可能期望它可以直接转换为正确的类型,但 as 将执行一个测试,如果存在混合值(如数字或 nil),则整个对象将被视为无效。相反,必须将 JSON 数据结构转换为 [String:String] 类型。将以下内容作为独立函数添加到 NSURLExtensions.swift

func toStringString(dict:[String:AnyObject]) -> [String:String] {
  var result:[String:String] = [:]
  for (key,value) in dict {
    if let valueString = value as? String {
      result[key] = valueString
    } else {
      result[key] = "\(value)"
    }
  }
  return result
}

这可以用于将 JSON 函数中的 [String:AnyObject] 转换:

fn(toStringString(json!)) // fixes compile time error

可以通过使用 data: 协议并通过传递表示 JSON 数据的 base64 编码字符串来测试该函数,使用测试类。为了创建一个 base64 表示,创建一个字符串,将其转换为 UTF8 数据对象,然后使用 data: 前缀将其转换回字符串表示:

import XCTest
class NSURLExtensionsTest: XCTestCase {
  func testNSURLJSON() {
    let json = "{\"test\":\"value\"}".
     dataUsingEncoding(NSUTF8StringEncoding)!
    let base64 = json.base64EncodedDataWithOptions(     .EncodingEndLineWithLineFeed)
    let data = String(data: base64, 
     encoding: NSUTF8StringEncoding)!
    let dataURL = NSURL(string:"data:text/plain;base64,\(data)")!
    dataURL.withJSONDictionary {
      dict in
      XCTAssertEqual(dict["test"] ?? "", "value",
       "Value is as expected")
    }
    sleep(1)
  }
}

请注意,sleep(1) 是必需的,因为解析响应必须在后台线程中发生,因此可能不会立即可用。通过向函数添加延迟,它为断言执行提供了机会。

解析字典数组

可以使用类似的方法来解析字典数组(例如,由列出存储库资源返回的数组)。这里的区别在于类型签名(它有一个额外的 [] 来表示数组),以及正在使用 map 处理列表中的元素:

func withJSONArrayOfDictionary(fn:[[String:String]] -> ()) {
  … 
  if let json = try? NSJSONSerialization.JSONObjectWithData(
   data, options: .AllowFragments) as? [[String:AnyObject]] {
    fn(json!.map(toStringString))
  } else {
    fn([[String:String]]())
  }

测试也可以扩展:

let json = "[{\"test\":\"value\"}]".
 dataUsingEncoding(NSUTF8StringEncoding)!
…
dataURL.withJSONArrayOfDictionary {
  dict in XCTAssertEqual(dict[0]["test"] ?? "", "value",
 "Value is as expected")
}

创建客户端

现在工具已经完成,可以创建 GitHub 客户端 API。一旦完成,就可以将其与用户界面集成。

与 GitHub API 通信

将会创建一个 Swift 类来与 GitHub API 通信。这将连接到根端点主机并下载服务 URL 的 JSON,以便后续的网络连接可以建立。

为了确保网络请求不被重复,将使用 NSCache 来保存响应。当应用程序在内存压力下时,这将自动清空:

import Foundation
class GitHubAPI {
  let base:NSURL
  let services:[String:String]
  let cache = NSCache()
  class func connect() -> GitHubAPI? {
    return connect("https://api.github.com")
  }
  class func connect(url:String) -> GitHubAPI? {
    if let nsurl = NSURL(string:url) {
      return connect(nsurl)
    } else {
      return nil
    }
  }
  class func connect(url:NSURL) -> GitHubAPI? {
    if let data = NSData(contentsOfURL:url) {
      if let json = try? NSJSONSerialization.JSONObjectWithData(
       data,options:.AllowFragments) as? [String:String] {
        return GitHubAPI(url,json!)
      } else {
       return nil
      }
    } else {
      return nil
    }
  }
  init(_ base:NSURL, _ services:[String:String]) {
    self.base = base
    self.services = services
  }
}

这可以通过在项目的根级别创建一个 api 目录并将 GitHub API 主站点的响应保存到 api/index.json 文件中来测试,从终端提示符运行 curl https://api.github.com > api/index.json。在 Xcode 中,通过导航到 文件 | 将文件添加到项目… 或按 Command + Option + Aapi 目录添加到项目中,并确保它与测试目标相关联。

然后,可以使用 NSBundle 来访问:

import XCTest
class GitHubAPITests: XCTestCase{
  func testApi() {
    let bundle = NSBundle(forClass:GitHubAPITests.self)
 if let url = bundle.URLForResource("api/index",
 withExtension:"json") {
      if let api = GitHubAPI.connect(url) {
        XCTAssertTrue(true,"Created API \(api)")
      } else {
        XCTAssertFalse(true,"Failed to parse \(url)")
      }
    } else {
      XCTAssertFalse(true,"Failed to find sample API")
    }
  }
}

小贴士

模拟 API 不应该成为主应用程序的目标,而应该是测试目标的一部分。因此,而不是使用 NSBundle.mainBundle 来获取应用程序的包,使用 NSBundle(forClass)

返回用户的仓库

从服务查找返回的 API 包括 user_repositories_url,这是一个可以实例化为特定用户的模板。可以向 GitHubAPI 类添加一个方法 getURLForUserRepos,它将返回用户仓库的 URL。由于它将被频繁调用,结果应该使用 NSCache 来缓存:

func getURLForUserRepos(user:String) -> NSURL {
  let key = "r:\(user)"
  if let url = cache.objectForKey(key) as? NSURL {
    return url
  } else {
    let userRepositoriesURL = services["user_repositories_url"]!
    let userRepositoryURL = URITemplate.replace(
     userRepositoriesURL, values:["user":user])
    let url = NSURL(string:userRepositoryURL, relativeToURL:base)!
    cache.setObject(url, forKey:key)
    return url
  }
}

一旦知道了 URL,就可以使用异步回调函数解析数据,该函数在数据准备好时通知:

func withUserRepos(user:String, fn:([[String:String]]) -> ()) {
  let key = "repos:\(user)"
  if let repos = cache.objectForKey(key) as? [[String:String]] {
    fn(repos)
  } else {
    let url = getURLForUserRepos(user)
    url.withJSONArrayOfDictionary {
      repos in
      self.cache.setObject(repos,forKey:key)
      fn(repos)
    }
  }
}

这可以通过向 GitHubAPITests 类添加一个简单的测试来验证:

api.withUserRepos("alblue") {
  array in
  XCTAssertEqual(24,array.count,"Number of repos")
}

注意

样本数据包含以下文件中的 24 个仓库,但 GitHub API 在未来可能为该用户提供不同的值:

raw.githubusercontent.com/alblue/com.packtpub.swift.essentials/master/RepositoryBrowser/api/users/alblue/repos.json

通过 AppDelegate 访问数据

当构建一个管理数据的 iOS 应用程序时,决定在哪里声明变量是必须做出的第一个决定。在实现视图控制器时,通常将特定于视图的数据与该类关联;但如果数据需要在多个视图控制器之间使用,就有更多的选择。

一种常见的方法是将所有内容封装到一个 单例 中,这是一个只实例化一次的对象。这通常通过实现类中的 private var 和一个返回(或按需实例化)单例的 class func 来实现。

小贴士

Swift 中的private关键字确保变量仅在当前源文件中可见。默认的可视性是internal,这意味着代码仅在当前模块中可见;public关键字表示它也可以在模块外部可见。

另一种方法是使用AppDelegate本身。实际上,它已经是一个单例,可以通过UIApplication.sharedApplication().delegate来访问,并且在任何其他对象访问它之前就已经设置好了。

小贴士

应该避免过度使用AppDelegate来存储数据。而不是添加过多的属性,考虑创建一个单独的类或结构来保存值。

AppDelegate将用于存储对GitHubAPI的引用,这可能使用偏好存储或其他外部方式来定义要连接的实例,以及用户列表和仓库缓存:

class AppDelegate {
  var api:GitHubAPI!
  var users:[String] = []
  var repos:[String:[[String:String]]] = [:]
  func application(application: UIApplication,
   didFinishLaunchingWithOptions: [NSObject: AnyObject]?)
   -> Bool {
    api = GitHubAPI.connect()
    users = ["alblue"]
    return true
  }
}

为了方便从视图控制器加载仓库,可以在AppDelegate中添加一个函数来为特定用户提供仓库列表:

func loadRepoNamesFor(user:String, fn:([[String:String]])->()) {
  repos[user] = []
  api.withUserRepos(user) {
    results in
    self.repos[user] = results
    fn(results)
  }
}

从视图控制器访问仓库

MasterViewController(从主从模板创建或创建一个新的UITableViewController子类)中,定义一个实例变量AppDelegate,并在viewDidLoad方法中分配它:

class MasterViewController:UITableViewController {
  var app:AppDelegate!
  override func viewDidLoad() {
    app = UIApplication.sharedApplication().delegate
     as? AppDelegate
    …
  }
}

表视图控制器在多个部分和行中提供数据。numberOfSections方法将返回具有用户名(通过用户列表索引)作为部分标题的用户数量:

override func numberOfSectionsInTableView(tableView: UITableView)
 -> Int {
  return app.users.count
}
override func tableView(tableView: UITableView,
 titleForHeaderInSection section: Int) -> String? {
  return app.users[section]
}

numberOfRowsInSection函数被调用以确定每个部分中有多少行。如果数量未知,可以在运行后台查询以找到正确答案的同时返回0

override func tableView(tableView: UITableView,
 numberOfRowsInSection section: Int) -> Int {
  let user = app.users[section]
  if let repos = app.repos[user] {
    return repos.count
  } else {
    app.loadRepoNamesFor(user) { _ in
      Threads.runOnUIThread {
        tableView.reloadSections(
         NSIndexSet(index: section),
         withRowAnimation: .Automatic)
      }
    }
    return 0
  }
}

小贴士

记得在 UI 线程上重新加载部分内容;否则,更新将无法正确显示。

最后,需要在单元格的值中显示仓库名称。如果使用默认的UITableViewCell,则可以在textLabel上设置值;如果是从 Storyboard 原型单元格加载的,则可以使用标签适当地访问内容:

override func tableView(tableView: UITableView,
 cellForRowAtIndexPath indexPath: NSIndexPath)
 -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier(
   "Cell", forIndexPath: indexPath)
  let user = app.users[indexPath.section]
  let repo = app.repos[user]![indexPath.row]
  cell.textLabel!.text = repo["name"] ?? ""
  return cell
}

当应用程序运行时,将按用户分组显示仓库列表:

从视图控制器访问仓库

添加用户

在此时刻,用户列表被硬编码到应用程序中。最好移除这个硬编码的列表,并允许按需添加用户。在AppDelegate类中创建一个addUser函数:

func addUser(user:String) {
  users += [user]
  users.sortInPlace({ $0 < $1 })
}

这允许详细控制器调用addUser函数并确保用户列表按字母顺序排列。

注意

$0$1sort函数期望的匿名参数。这是users.sort({ user1, user2 in user1 < user2})的简写形式。也可以使用数组本身的<函数通过users.sortInPlace(<)来对数组进行排序。

可以在MasterViewControllerviewDidLoad方法中创建添加按钮,以便在点击时调用insertNewObject方法:

override func viewDidLoad() {
  super.viewDidLoad()
  let addButton = UIBarButtonItem(barButtonSystemItem: .Add,
   target: self, action: "insertNewObject:")
  self.navigationItem.rightBarButtonItem = addButton
  …
}

当选择添加按钮时,可以通过UIAlertController对话框显示一系列操作,这些操作将调用以添加用户。

MasterViewController中添加(或替换)insertNewObject,如下所示:

func insertNewObject(sender: AnyObject) {
  let alert = UIAlertController(
   title: "Add user",
   message: "Please select a user to add",
   preferredStyle: .Alert)
  alert.addAction(UIAlertAction(
   title: "Cancel", style: .Cancel, handler: nil))
  alert.addAction(UIAlertAction(
   title: "Add", style: .Default) {
    alertAction in
    let username = alert.textFields![0].text
    self.app.addUser(username!)
    Threads.runOnUIThread {
      self.tableView.reloadData()
    }
  })
  alert.addTextFieldWithConfigurationHandler {
    textField -> Void in
    textField.placeholder = "Username";
  }
  presentViewController(alert, animated: true, completion: nil)
}

现在,用户可以通过在应用程序右上角的添加+)按钮上点击来在 UI 中添加。每次应用程序启动时,用户数组将为空,用户可以重新添加。

添加用户

小贴士

用户可以使用NSUserDefaults.standardUserDefaultssetObject:forKey以及stringArrayForKey方法在启动之间持久化。此实现的细节留给读者。

实现详细视图

最后一步是实现详细视图,以便在从主屏幕选择存储库时显示每个存储库的信息。在从主屏幕选择存储库时,已知用户名和存储库名。可以使用这些信息从存储库中提取更多信息并将项目添加到详细视图中。

更新故事板中的视图,添加四个标签和四个标签标题,用于用户名、存储库名、监视者数量和开放问题数量。将这些连接到DetailViewController的输出:

@IBOutlet weak var userLabel: UILabel?
@IBOutlet weak var repoLabel: UILabel?
@IBOutlet weak var issuesLabel: UILabel?
@IBOutlet weak var watchersLabel: UILabel?

要在详细视图中设置内容,userrepo将作为(可选的)字符串存储,而额外的data将存储在字符串键/值对中。当它们发生变化时,应调用configureView方法以重新显示内容:

var user: String? { didSet { configureView() } }
var repo: String? { didSet { configureView() } }
var data:[String:String]? { didSet { configureView() } }

在调用viewDidLoad方法之后,还需要调用configureView方法以确保 UI 按预期设置:

override func viewDidLoad() { configureView() }

configureView方法中,标签可能尚未设置,因此在设置内容之前需要使用if let语句进行测试:

func configureView() {
  if let label = userLabel { label.text = user }
  if let label = repoLabel { label.text = repo }
  if let label = issuesLabel {
    label.text = self.data?["open_issues_count"]
  }
  if let label = watchersLabel {
    label.text = self.data?["watchers_count"]
  }
}

如果使用标准模板,需要在详细视图修改后,将AppDelegatesplitViewController更改为返回true

func splitViewController(
 splitViewController: UISplitViewController,
 collapseSecondaryViewController 
  secondaryViewController:UIViewController!,
 ontoPrimaryViewController
  primaryViewController:UIViewController!) -> Bool {
  return true
}

注意

splitViewController:collapseSecondaryViewController方法确定显示的第一页是主页(true)还是详细页(false)。

在主视图和详细视图之间切换

主视图和详细视图之间的连接是通过MasterViewController中的showDetail切换来触发的。这可以用来从表中提取所选行,然后可以用来提取所选行和部分:

override func prepareForSegue(segue: UIStoryboardSegue,
 sender: AnyObject?) {
  if segue.identifier == "showDetail" {
    if let indexPath = self.tableView.indexPathForSelectedRow {
      // get the details controller
      // set the details
    }
  }
}

可以从切换的目的控制器访问详细控制器——除了目的控制器是导航控制器,因此需要进一步解包:

// get the details controller
let controller = (segue.destinationViewController as!
 UINavigationController).topViewController
 as! DetailViewController
// set the details

接下来,需要传递详细内容,这些内容可以从indexPath中提取,如应用程序的前几部分所示:

let user = app.users[indexPath.section]
let repo = app.repos[user]![indexPath.row]
controller.repo = repo["name"] ?? ""
controller.user = user
controller.data = repo

最后,为了确保应用程序在SplitViewController的分割模式下正常工作,如果处于分割模式,则需要显示返回按钮:

controller.navigationItem.leftBarButtonItem =
 self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true

现在运行应用程序将显示一系列仓库,当选择其中一个时,将显示其详细信息:

在主视图和详细视图之间切换

提示

如果在显示详细视图时出现崩溃,请检查Main.storyboard中不存在字段的连接器是否未定义。否则可能会看到类似此类对于键 detailDescriptionLabel 不遵守键值编码的错误,这是由于 Storyboard 运行时试图在代码中分配缺失的出口而引起的。打开Main.storyboard,转到连接检查器,并删除到缺失出口的连接。

在主视图和详细视图之间切换

加载用户的头像

用户可能有一个上传到 GitHub 的头像或图标。这些信息存储在用户信息中,可以通过 GitHub API 的单独查找访问。每个用户的头像都将作为参考存储在用户信息文档中的avatar_url下,例如api.github.com/users/alblue,它将返回类似以下的内容:

{
  … 
  "avatar_url": "https://avatars.githubusercontent.com/u/76791?v=2",
  … 
}

此 URL 代表一个可用于用户仓库头部的图像。

为了支持这一点,需要将用户信息添加到GitHubAPI类中:

func getURLForUserInfo(user:String) -> NSURL {
  let key = "ui:\(user)"
  if let url = cache.objectForKey(key) as? NSURL {
    return url
  } else {
    let userURL = services["user_url"]!
    let userSpecificURL = URITemplate.replace(userURL,
     values:["user":user])
    let url = NSURL(string:userSpecificURL, relativeToURL:base)!
    cache.setObject(url,forKey:key)
    return url
  }
}

这从 GitHub API 中查找user_url服务,返回以下 URI 模板:

  "user_url": "https://api.github.com/users/{user}",

这可以通过用户实例化,然后异步加载图像:

import UIKit
...
func withUserImage(user:String, fn:(UIImage -> ())) {
  let key = "image:\(user)"
  if let image = cache.objectForKey(key) as? UIImage {
    fn(image)
  } else {
    let url = getURLForUserInfo(user)
    url.withJSONDictionary {
      userInfo in
      if let avatar_url = userInfo["avatar_url"] {
        if let avatarURL = NSURL(string:avatar_url,
         relativeToURL:url) {
          if let data = NSData(contentsOfURL:avatarURL) {
            if let image = UIImage(data: data) {
              self.cache.setObject(image,forKey:key)
              fn(image)
} } } } } } }

一旦实现了加载用户头像的支持,就可以将其添加到视图的头部,以在用户界面中显示它。

提示

这里的嵌套if语句集表明可能最好重构为 Swift 的guard语句。这将确保在每次条件检查时不会增加缩进。重构留给读者作为练习。

显示用户的头像

通过用户展示仓库信息的表格视图可以被修改,以便除了用户的名称外,同时显示他们的头像。目前,这是在tableView:titleForHeaderInSection方法中完成的,但有一个等效的tableView:viewForHeaderInSection方法,它提供了更多的自定义选项。

尽管方法签名表明返回类型是UIView,但实际上它必须是UITableViewHeaderFooterView的子类型。不幸的是,Storyboard 中没有支持编辑或自定义这些视图,因此必须通过编程实现。

要实现viewForHeaderInSection方法,像以前一样获取用户名,并将其设置为新建的UITableViewHeaderFooterViewtextLabel。然后,在异步图像加载器中,创建一个具有相同原点但正方形大小的图像框架,然后创建并添加图像作为头部视图的子视图。该方法看起来像这样:

override func tableView(tableView: UITableView,
 viewForHeaderInSection section: Int) -> UIView? {
  let cell = UITableViewHeaderFooterView()
  let user = app.users[section]
  cell.textLabel!.text = user
  app.api.withUserImage(user) {
    image in
    let minSize = min(cell.frame.height, cell.frame.width)
    let squareSize = CGSize(width:minSize, height:minSize)
    let imageFrame = CGRect(origin:cell.frame.origin,
     size:squareSize)
    Threads.runOnUIThread {
      let imageView = UIImageView(image:image)
      imageView.frame = imageFrame
      cell.addSubview(imageView)
      cell.setNeedsLayout()
      cell.setNeedsDisplay()
    }
  }
  return cell
}

现在当应用程序运行时,头像将显示在用户的仓库上叠加:

显示用户的头像

摘要

本章展示了如何将本书中创建的主题集成,以便将它们整合成一个功能应用,用于与远程网络服务(如 GitHub)交互,并能以表格形式展示这些信息。

通过确保所有网络请求都在后台线程中实现,并且返回的数据在 UI 线程上更新,应用程序将保持对用户输入的响应。可以创建图形和自定义视图来提供标题,或者可以将 Storyboard 修改为包含每个仓库的更多图形。

第八章:添加手表支持

苹果在 2015 年 4 月发布 Apple Watch 的同时发布了 watchOS。然而,随着 2015 年 9 月 watchOS 2 的发布,开发者能够编写在手表本身上运行的扩展,而不是依赖于可用的配套 iOS 设备。本章将展示如何将手表支持添加到现有的仓库浏览器应用(在第七章 Building a Repository Browser 中创建),即构建仓库浏览器

本章将介绍以下主题:

  • 将手表扩展添加到现有项目

  • 手表界面的类型

  • 使用表格、文本和图像

  • 如何在选中上下文之间切换屏幕

  • 智能手表应用的最佳实践

手表应用

手表应用由可以在手表本身上执行代码组成。手表应用是用 Swift 开发的,并以手表扩展手表应用的形式运行。对于 watchOS 2,两者都在手表上运行。(在 watchOS 1 中,手表扩展在配套的 iPhone 上运行。)本章将假设使用 watchOS 2 来直接在手表上运行 Swift 编译的代码。

注意

由于第一版 watchOS 不允许在手表上执行代码,代码被打包成一个手表扩展,作为 iPhone 上配套应用的组成部分运行。手表应用包含资源和其他图像,这些图像直接在手表上显示。随着 watchOS 2 的发布,这种分离变得不那么相关。Xcode 或 watchOS 的将来版本可能会导致这两个概念合并。

添加手表目标

要为现有应用添加手表支持,必须为手表创建一个新的目标。打开现有的仓库浏览器应用,导航到文件 | 新建 | 目标,并在watchOS部分选择WatchKit App

添加手表目标

创建完成后,将询问手表应用的名称。这个名字不能与包含的项目同名,因此可以将其命名为RepositoryBrowserWatch。语言应该是Swift;其他用户界面元素(** complicationGlance通知**)与本项目无关,因此可以取消选择:

添加手表目标

当按下完成按钮时,项目中将创建以下新元素:

  • RepositoryBrowserWatch:这是手表应用,它提供了应用的界面描述

  • RepositoryBrowserWatch Extension:这是对应手表应用可执行代码的内容

  • InterfaceController.swift:这是对应自动创建的用户界面元素的 Swift 文件

  • ExtensionDelegate.swift:这是对应整个用户应用的 Swift 文件(类似于传统 iOS 应用中的AppDelegate

将 GitHubAPI 添加到监视目标

为了允许手表应用程序使用在 第七章 中开发的 GitHubAPI构建仓库浏览器,应将以下代码添加到 ExtensionDelegate

var api:GitHubAPI!
var users:[String] = []
var repos:[String:[[String:String]]] = [:]
func loadReposFor(user:String, fn:([[String:String]])->()) {
  repos[user] = []
  api.withUserRepos(user) {
    results in
    self.repos[user] = results
    fn(results)
  }
}
func addUser(user:String) {
  users += [user]
  users.sortInPlace({ $0 < $1 })
}

这将最初生成一个编译时错误,因为 GitHubAPI 类(以及相关的类)目前没有与监视目标关联。为了解决这个问题,选择 GitHubAPIThreadsNSURLExtensionsURITemplate Swift 文件,并通过按 Command + Option + 1 或通过导航到 视图 | 工具 | 显示文件检查器 打开文件检查器。确保这些文件通过选择相应的复选框添加到 RepositoryBrowserWatch 扩展 目标中:

将 GitHubAPI 添加到监视目标

现在当监视目标构建并运行时,手表模拟器将显示一个黑色屏幕,并在应用程序右上角显示时间。如果未显示,请验证所选的目标是否为手表应用程序:

将 GitHubAPI 添加到监视目标

创建监视接口

手表的用户界面是以类似于 iOS 应用程序的方式构建的,除了用户工具包是使用 WatchKit 而不是 UIKit 构建的。与 UITableView 等类存在的方式相同,也存在相应的类,如 WKInterfaceTable。有一些细微的差异;例如,UITableView 将在显示时动态填充元素,但 WKInterfaceTable 需要事先知道有多少行以及这些行是什么。

将用户列表添加到监视列表

与提供分组行标题的 UITableView 不同,WKInterfaceTable 只允许一个项目列表。相反,应用程序将被设计成第一个屏幕将显示用户列表,然后第二个屏幕将显示所选用户的仓库。

为了测试目的,将以下内容添加到 ExtensionDelegate 类的 applicationDidFinishLaunching 方法中:

api = GitHubAPI.connect()
addUser("alblue")

这将允许其他类查询 ExtensionDelegate 属性 users 来显示一些内容。与 iOS 应用程序的 AppDelegate 一样,有一个全局单例可以访问。将以下内容添加到 InterfaceController

let delegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate

要显示用户列表,界面本身必须有一个表格。每一行表格都有一个自己的控制器类,这可以是一个简单的 NSObject 子类。要显示用户名列表,创建一个 UserRowController 类,它包含一个单独的标签。由于这是 InterfaceController 的私有实现细节,将其包含在同一个文件中是有意义的:

class UserRowController: NSObject {
  @IBOutlet weak var name: WKInterfaceLabel!
}

将以下内容添加到 InterfaceController 类中,该类稍后将连接到界面:

  @IBOutlet weak var usersTable: WKInterfaceTable!

现在,可以在awakeWithContext方法中填充表格。这涉及到设置行数和行的类型。添加以下内容:

let users = delegate.users
usersTable.setNumberOfRows(users.count, withRowType: "user")
for (index,user) in users.enumerate() {
  let controller = usersTable.rowControllerAtIndex(index) as! UserRowController
  controller.name.setText(user)
}

如果此时运行应用程序,将发生几个错误,因为IBOutlet引用尚未连接,行类型user尚未与UserRowController类关联。

连接界面

生成用户内容后,界面必须连接到实现细节。在RepositoryBrowserWatch文件夹中打开Interface.storyboard,并转到接口控制器场景。这将显示一个带有时钟和底部显示任何屏幕尺寸的黑色手表。像 iOS 应用程序界面一样,它们可以有不同的尺寸(在撰写本文时为 38mm 或 42mm)。

通过按Command + Option + Control + 3键或通过导航到视图 | 工具 | 显示对象库打开对象库。在搜索字段中输入table,然后将它拖动到手表界面中:

连接界面

从左侧文档大纲中的接口控制器,按Control键并向下拖动到表格中,创建一个连接到接口控制器中定义的usersTable出口:

连接界面

InterfaceController实例化时,usersTable将连接到出口。然而,行之间还没有连接。为此,将一个标签拖动到带有表格行占位符的虚线区域。为了确保标签占用所有可用空间,将大小设置为相对于容器,宽度和高度的因素均为1

连接界面

为了将标签的文本与UserRowController连接起来,必须做两件事。首先,行的类型必须设置为与UserRowController类相对应,这将允许标签连接到名称出口。其次,行必须被赋予标识符user,以便它可以与之前章节中指定的rowType连接。

要设置行控制器的类,通过按Command + Option + 3键或通过导航到视图 | 工具 | 显示身份检查器打开身份检查器。从下拉菜单中选择UserRowController,这应该也会设置模块名称RepositoryBrowserWatch_Extension。完成此操作后,用户控制器可以通过按Control键并拖动到标签,然后选择name出口来连接到标签:

连接界面

要设置行控制器的类型,通过按Command + Option + 4键或通过导航到视图 | 工具 | 显示属性检查器切换到属性检查器,输入之前使用的rowType,即user

连接界面

现在当应用程序运行时,应该可以看到用户列表,其中包括alblue

连接界面

添加图片

可以使用现有的 API 为用户返回一张图片,并且可以使用WKInterfaceImage以类似文本名称的方式显示它。首先,需要在UserRowController中创建一个出口,以便将其连接到界面:

class UserRowController: NSObject {
  @IBOutlet weak var name: WKInterfaceLabel!
  @IBOutlet weak var icon: WKInterfaceImage!
}

界面现在需要更新以添加图片。这可以通过在对象库中搜索图片,然后将其拖入用户行来完成。

手表更喜欢提前知道图片大小,因此可以将图片大小固定为 32x32 像素,这对于大尺寸和小尺寸的手表都足够。将图片标记为填充将确保图片不会被不恰当地调整大小,并且整个图片都将显示。

小贴士

可以点击+图标旁边的大小,然后为两个不同的手表指定不同的尺寸。

将图片右对齐和居中对齐会给手表的两种尺寸带来相同的印象。将对齐方式更改为右对齐居中对齐将允许显示适应不同的尺寸。也可能有必要将用户名称的宽度从相对于容器更改为大小适应,但这不是严格必要的。最后,使用控制和拖动鼠标将来自用户行的出口与图片连接,然后选择图标出口。结果的用户界面将看起来像这样:

添加图片

创建并连接好图片后,最后一步是填充数据。在InterfaceController方法awakeFromContext中,在设置用户名称后,添加对 API 的调用以获取图片,类似于上一章中的DetailViewController

controller.name.setText(user) // from before
delegate.api.withUserImage(user) {
  image in controller.icon.setImage(image)
}

现在当应用程序运行后,经过短暂的暂停,将看到用户的头像:

添加图片

响应用户交互

通常,手表用户界面会向用户展示信息,或者让他们以某种方式选择或操作它。当项目以表格形式展示时,自然会让用户轻触行以显示后续屏幕。手表应用程序使用segues以类似于 iOS 应用程序的方式从一个屏幕移动到另一个屏幕。

第一步将涉及创建一个新的控制器文件,名为RepositoryListController.swift。这将用于存储RepositoryListControllerRepositoryRowController类,与现有的InterfaceController非常相似。与其他视图一样,将有一个表格来存储行,并且每一行都将有一个name标签:

class RepositoryRowController: NSObject {
  @IBOutlet weak var name: WKInterfaceLabel!
}
class RepositoryListController: WKInterfaceController {
  let delegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
  @IBOutlet weak var repositoriesTable: WKInterfaceTable!
}

小贴士

不要忘记将RepositoryListController.swift文件添加到RepositoryBrowserWatch Extension目标中,否则将无法将其用作实现类。

一旦创建了这些类,就可以打开 Interface.storyboard 并从对象库中拖入一个新的 Interface Controller。这将创建一个空白的屏幕,可以添加其他对象。

小贴士

确保选择的是 Interface Controller,而不是 Glance Interface ControllerNotification Interface Controller,因为这些用于不同的目的。

一旦创建了界面控制器,从对象库中拖动一个 Table 到界面控制器,然后以与上一个界面控制器示例相同的方式,从对象库中拖动一个 Label 到行占位符。

界面控制器需要更新以指向 RepositoryListController 类;这可以通过选择界面控制器并像之前一样转到 Identity Inspector 来完成。一旦定义了 RepositoryListController 实现,按 Control 并从界面控制器图标拖动到表格,并将其连接到 repositoriesTable

小贴士

这些连接与上一节中 usersTable 的连接方式相同。

行占位符的类可以通过在文档大纲中选择 Repositories Table 下的占位符,然后在 Attributes Inspector 中将行控制器的身份设置为 repository 来定义。这将允许仓库行占位符将名称属性连接到场景中的标签。

最后一个连接是将从用户屏幕到仓库屏幕添加一个 segue。按 Control 并从 Users Table 中的 user 行拖动到仓库列表控制器,并在弹出窗口中选择 Push Segue

最后的连接将看起来像这样:

响应用户交互

当用户在第一个屏幕中被选中时,第二个屏幕应该滑过。目前这将是空的,但在下一节中会填充仓库。

添加上下文并显示仓库

要从一个屏幕传递数据到另一个屏幕,需要设置一个上下文。每个 WKInterface 屏幕都有一个 awakeWithContext 函数,可以在屏幕显示时将任意对象传递到屏幕中。这可以用来提供一个用户对象,进而可以用来查找一组仓库。

第一个元素是在从屏幕过渡出去时设置上下文对象。在 InterfaceController 类中添加一个新方法 contextForSegueWithIdentifier,如下所示:

override func contextForSegueWithIdentifier(
 segueIdentifier: String,
 inTable table: WKInterfaceTable,
 rowIndex: Int) -> AnyObject? {
  return delegate.users[rowIndex]
}

现在当 RepositoryListController 显示时,当前选定的用户将被传递。为了接收对象,在 RepositoryListController 类中创建一个 awakeWithContext 方法,如下所示:

override func awakeWithContext(context: AnyObject?) {
  super.awakeWithContext(context)
  if let user = context as? String {
    print("Showing user \(user)")
  }
}

小贴士

这将允许在此处调试代码,以验证对象是否按预期传递。

显示仓库列表需要使用 API 生成数据列表,创建适当数量的行,然后设置行内容,就像之前一样。这可以通过更新RepositoryListController中的awakeWithContext方法来实现,如下所示:

if let user = context as? String {
  delegate.loadReposFor(user) {
    result in
    self.repositoriesTable.setNumberOfRows(
     result.count, withRowType: "repository")
    for (index,repo) in result.enumerate() {
      let controller = self.repositoriesTable
       .rowControllerAtIndex(index) as! RepositoryRowController
      controller.name.setText(repo["name"] ?? "")
  }
}

现在当手表应用程序运行时,如果用户选择了一个仓库,第二个屏幕应该填充了仓库列表:

添加上下文和显示仓库

添加详细屏幕

手表应用程序的最后一部分是创建一个类似于 iOS 应用程序中DetailViewController的模态屏幕。当用户选择一个仓库时,应该以模态方式展示仓库的详细信息。

这将通过一个新的RepositoryController.swift文件来实现,该文件将包含一个WKInterfaceController并具有四个可以在界面中连接的标签:

class RepositoryController: WKInterfaceController {
  @IBOutlet weak var repo: WKInterfaceLabel!
  @IBOutlet weak var issues: WKInterfaceLabel!
  @IBOutlet weak var watchers: WKInterfaceLabel!
  @IBOutlet weak var forks: WKInterfaceLabel!
}

小贴士

不要忘记将RepositoryController.swift文件添加到RepositoryBrowserWatch Extension目标中,否则无法将其用作实现类。

要添加屏幕,打开Interface.storyboard,从对象库中拖动另一个界面控制器到画布上。在身份检查器中,将RepositoryController设置为类型,这将允许随后连接标签。

将四个标签对象拖入观察界面。它们将自动排成一行,一个接一个。这些可以赋予占位文本为RepoIssuesWatchersForks——尽管这些内容将程序化地更改。通过从仓库控制器拖动并放置到每个标签上,为输出连接设置线路,以便它们可以程序化控制。

最后,连接从仓库列表控制器到过渡,以便当在仓库表下选择仓库行控制器时,选择模态选择过渡。在 Xcode 中,完成的连接应如下所示:

添加详细屏幕

到目前为止,应用程序可以进行测试,选择一个仓库应该过渡到新屏幕,尽管正确的内容尚未显示。

填充详细屏幕

要在详细屏幕中连接标签,需要遵循与上一个屏幕类似的过程:需要从过渡屏幕设置上下文,然后将数据填充到接收屏幕中。

RepositoryListController中,需要通过contextForSegueWithIdentifier方法传递所选仓库的信息。然而,与users列表(在ExtensionDelegate中持久化)不同,没有这样的存储仓库数据列表。因此,当屏幕唤醒时,有必要持久化仓库的临时副本。

修改RepositoryListController类的awakeWithContext方法,以便将条目存储在repos属性中,以便在选中时可以用于在退出屏幕时设置上下文:

var repos = []
override func awakeWithContext(context: AnyObject?) {
  super.awakeWithContext(context)
  if let user = context as? String {
    delegate.loadReposFor(user) {
      // as before
      self.repos = result
    }
  } else {
    repos = []
  }
}
override func contextForSegueWithIdentifier(
 segueIdentifier: String,
  inTable table: WKInterfaceTable,
  rowIndex: Int) -> AnyObject? {
  return repos[rowIndex]
}

现在当选择存储库时,键/值对将通过之前缓存的內容传递。

填写详细屏幕的最后一步是使用此上下文对象来设置标签。在RepositoryController类中,添加一个awakeWithContext方法,该方法接收键/值字典,并使用字段显示有关存储库的信息:

override func awakeWithContext(context: AnyObject?) {
  if let data = context as? [String:String] {
    repo.setText(data["name"])
    issues.setText(data["open_issues_count"])
    watchers.setText(data["watchers_count"])
    forks.setText(data["forks_count"])
  }
}

现在当运行应用时,用户应该能够逐个浏览三个屏幕以查看内容。

填充详细屏幕

手表应用的最佳实践

由于手表是低功耗设备,网络功能有限,因此应尽可能减少网络使用。这里展示的示例应用(使用多个基于 REST 的调用到后端服务器)发送和接收的数据比所需的多;如果这是一个定制应用,那么应该最小化协议以避免不必要的传输。

示例应用还以文本数据列表的形式展示了用户信息,这可能不是展示数据的最佳方式。考虑在适当的情况下使用其他机制以更图形化的方式展示信息。

UI 线程注意事项

在主线程上执行任何网络操作,如 API 查找和用户仓库查询,通常是不良的做法。相反,查找应该在后台线程中运行,在必要时切换回 UI 线程以执行更新。

例如,在连接的 API 查找中,connect 方法看起来是这样的:

class func connect(url:NSURL) -> GitHubAPI? {
  if let data = NSData(contentsOfURL:url) {
    ...
  }
}

这使用可选初始化器来返回GitHubAPI,无论网络连接是否成功。但这意味着在可以使用之前,调用必须阻塞。这意味着在applicationDidFinishLaunching中调用的GitHubAPI()初始化器会阻塞应用的启动,这不是良好的用户体验。相反,最好是这样做:

Threads.runOnBackgroundThread() {
  if let data = NSData(contentsOfURL:url) {
    …
    Threads.runOnUIThread() {
      // update the UI as before
    }
  }
}

添加后台线程会增加复杂性,但这也意味着应用启动会更快。可能需要更新 UI 初始化逻辑,以便将 API 调用推迟到网络服务可用,或者显示其他加载进度指示器,以向用户提供反馈,表明有操作正在进行。

存储数据

示例应用中的用户列表仅存储一个变量,该变量硬编码到应用中。通常情况下不会是这样,但手表并未设置用于数据输入。相反,应使用配套的 iOS 应用来定义用户列表(带有适当的错误检查和界面),然后与手表应用进行通信。

有两种实现方式。最好的方法是使用 iCloud 基础设施,并在 iOS 设备上更新文档,然后自动镜像到手表。这将使用户在未来过渡到新的 iOS 设备或手表时无需重新创建列表。

另一种方法是使用WatchConnectivity模块和WCSession类型在手表和 iOS 设备之间发送消息。这提供了一个单例,可以通过WCSession.defaultSession()访问,可以用来在 iOS 设备和配对的手表之间发送和接收消息。请注意,会话可能不受支持,因此应首先使用session.isSupported()进行检查;如果是的话,则必须在发送或接收任何消息之前使用session.activate()激活它。传入的消息会被路由到相关的delegate

手表还可以使用会话的watchDirectoryURL持久化数据,该 URL 返回可以写入临时数据的位置。这可以用来添加在启动时加载的附加信息。例如,GitHubAPI 可以在最初检索后缓存 API,然后用于后续请求,并在必要时自动重新加载。

合理使用复杂功能和快速查看

手表的界面主要使用不同类型的控件来处理不同的交互。一个复杂功能是一个显示在手表表盘屏幕上的小型实用控件(例如,升起的太阳或计时器)。一个通知是一个简短的信息更新(类似于 iOS 上的通知,如收到的消息),可以用来执行简单的操作(如回答是/否/可能)或启动完整的应用程序。一个快速查看是一个简单的基于位置的项目,当用户抬起手腕时,可能会告诉用户附近有什么东西。

根据创建的应用程序类型,可能会有适当的方法来使用这些功能,以便在需要时向用户提供特定信息。然而,它们不应该仅仅为了使用而使用;如果它们不会提供任何有用的信息,则不应使用。

与应用程序交互还有其他方式;例如,watchOS 2 支持直接与数字表冠和强推进行交互。有关更多信息,请参阅 Apple Watch 人机界面指南。

摘要

与 iOS 设备上的运行方式相同,手表应用程序也可以运行代码,尽管它们上传到手表的方式略有不同。在模拟器上运行代码与在真实设备上运行非常不同;网络和处理器比桌面级机器(甚至 iOS 设备)预期的要有限得多。因此,在实际设备上进行测试对于测试完整体验是必不可少的。

本章介绍了如何构建手表应用和扩展,如何将它们打包成手表扩展和手表应用的形式,以及它们如何与父应用共享代码以避免代码重复。手表界面演示了如何使用 segues 在屏幕之间切换,以实现上一章创建的 iOS 应用的手表扩展。

附录 A. 对 Swift 相关网站、博客和知名 Twitter 用户的引用

学习任何语言最初都集中在语言的语法和语义上,但很快就会转向学习标准库和附加库的套件,这些库使程序员能够高效地工作。一本书不可能列出所有可能需要的库;这本书旨在成为学习旅程的开始。

为了进一步阅读,本附录提供了一些额外的资源,这些资源可能对读者继续这一旅程有所帮助。此外,请注意 Packt Publishing 出版的其他书籍,这些书籍展示了 Swift 的不同方面。这个资源列表必然是不完整的;在本书出版后,新的资源将会出现,但您可以通过关注这里提供的资源的订阅和帖子来找到新的发展。

语言

Swift 语言由苹果公司开发,可以在developer.apple.com/swift/的 Swift 开发者页面上找到许多文档。这包括语言参考指南和标准库的介绍:

Swift 语言在 2015 年 12 月开源,并在swift.org有一个新的家,同时还有一个新的 Swift 博客在swift.org/blog/

Twitter 用户

有很多活跃的 Twitter 用户使用 Swift;在许多情况下,帖子会被标记为#swift标签,可以在twitter.com/search?q=%23swift找到。作者关注的流行用户包括(按 Twitter 昵称字母顺序):

  • @AirspeedSwift: 这个推特有关于 Swift 相关主题的好选择和转推

  • @ChrisEidhof: 这是《Functional Swift》书籍的作者和@objcio的作者

  • @CodeWithChris: 这个推特是关于 iOS 编程教程的集合

  • @CodingInSwift: 这个推特包含了一系列 Swift 资源的跨帖

  • @CompileSwift: 这个推特包含关于 Swift 的文章

  • @cwagdev: 克里斯·沃格德撰写了一些与雷·温德利希合作的 iOS 教程

  • @FunctionalSwift: 这是一系列功能片段的选择,以及一本《Functional Swift》书籍

  • @LucasDerraugh: 这是 YouTube 上视频教程的创作者

  • @NatashaTheRobot: 这个推特包含了对正在发生的事情的精彩总结,以及新闻简报和交叉引用

  • @nnnnnnnn: 内特·库克,他审阅了本书的早期版本,并提供了刚刚提到的 Swifter 列表

  • @PracticalSwift: 这是一个关于 Swift 语言的博客文章好集合

  • @rwenderlich: 雷·温德利希有许多与 iOS 开发相关的文章;信息量丰富,最近还涉及 Swift 话题

  • @SketchyTech: 这是关于 Swift 的博客文章集合

  • @SwiftCastTV: 这些是 Swift 的视频教程

  • @SwiftEssentials: 这是本书的推特动态

  • @SwiftLDN: 这个推特发布基于伦敦的 Swift 聚会,也邀请了一些优秀的 Swift 演讲者和演示者

除了专注于 Swift 的推特用户外,还有许多其他 Cocoa(Objective-C)开发者定期在 iOS 和 OS X 平台相关主题上写博客。鉴于任何 Objective-C 框架都可以集成到 Swift 应用中(反之亦然),阅读这些帖子通常会提供有用的信息:

  • @Cocoanetics: 奥利弗·德罗尼克撰写关于 iOS 并提供培训

  • @CocoaPods: CocoaPods 是一个 Objective-C 框架(库)的依赖管理系统,现在正在扩展到 Swift 领域

  • @Mattt: 马特·汤普森撰写关于许多 iOS 主题的文章,是 AFNetworking 和 AlamoFire 网络库的作者,后来加入了苹果公司编写 Swift 包管理器

  • @MikeAbdullah: 迈克·阿卜杜拉撰写关于通用 iOS 开发的文章

  • @MikeAsh: 迈克·阿什知道所有的事情,不知道的事情他会去了解

  • @MZarra: 马库斯·S·扎拉写过很多关于 Core Data 和同步的内容

  • @NSHipster: 这是马特·汤普森整理的 iOS 和 Cocoa 文章集合

  • @objcio: 这是一个关于 Objective-C 话题的月度出版物,其中包含一些 Swift 内容

  • @PerlMunger: 马特·朗撰写关于 Swift、Cocoa 和 iOS 的文章

本书的审稿人包括:

  • @AnilVrgs: 安尼尔·瓦格谢斯

  • @Ant_Bello: 安东尼奥·贝洛

  • @ArvidGerstmann: 阿维德·格斯特曼

  • @jiaaro: 詹姆斯·罗伯特

  • @nnnnnnnn: 内特·库克

作者的个人和书籍推特账号是:

  • @AlBlue 是作者的推特账号

  • @SwiftEssentials 是这本书的推特账号

类似于 @SwiftLdn 的聚会跟踪有趣的 Swift 作者在 Twitter 列表 twitter.com/SwiftLDN/lists/swift-writers/members 中,这可能比本节有更更新的推荐,以及 Ray Wenderlich 团队 twitter.com/rwenderlich/lists/raywenderlich-com-team/members

博客和教程网站

有许多博客涵盖了 Swift 和相关技术。以下是一些你可能感兴趣的精选:

Meetups

在 Swift 创建之前,存在许多本地的 iOS 开发者小组;它们随后被 Swift 特定的群体所取代。这些小组当然会因地理位置而异,但存在一些聚会网站,例如 EventBrite 在 www.eventbrite.co.uk,以及 Meetup 在 www.meetup.com

你附近可能也有 Twitter 群组或聚会;例如,在伦敦,有@SwiftLDNtwitter.com/SwiftLDN,他们定期在www.meetup.com/swiftlondon/列出会议。在纽约,www.meetup.com/NYC-Swift-Developers/群组相当活跃。在旧金山,www.meetup.com/swift-language/www.meetup.com/San-Francisco-SWIFT-developers/都是活跃的。

后记

千里之行,始于足下。你编写优秀的 Swift 应用程序的旅程才刚刚开始。就像任何旅程一样,旅伴可以提供支持、帮助和鼓励;这里提到的许多旅伴可以为你提供通往更多资源的连接。我希望你享受你的旅程。

第九章:索引

A

  • AppDelegate 类

    • 关于 / AppDelegate 类
  • 苹果人机界面指南(HIG) / 场景和视图控制器

  • 助理编辑器 / 在 Swift 中将视图连接到输出端口

  • 异步读写

    • 关于 / 异步读写

    • 数据,从 NSInputStream 读取 / 异步从 NSInputStream 读取数据

    • 流代理,创建 / 创建流代理

    • 错误,处理 / 处理错误

    • 参考,列出 / 异步列出参考

    • 参考,在 UI 中显示 / 在 UI 中显示异步参考

    • 数据,写入 NSOutputStream / 异步写入 NSOutputStream 数据

  • 自动布局

    • 关于 / 使用自动布局

    • 使用 / 使用自动布局

    • 约束 / 理解约束

    • 约束,添加 / 添加约束

    • 约束,通过拖放添加 / 通过拖放添加约束

    • 约束,添加到 Press Me 场景 / 将约束添加到 Press Me 场景

    • 缺失约束,添加 / 添加缺失约束

B

  • 最佳实践,监视应用

    • 关于 / 手表应用的最佳实践

    • UI 线程注意事项 / UI 线程注意事项

    • 存储数据 / 存储数据

    • 表盘组件的使用 / 适当地使用表盘组件和快速查看

    • 快速查看的使用 / 适当地使用表盘组件和快速查看

C 语言

    • 关于 / Swift 中的类
  • 客户端

    • 创建 / 创建客户端

    • GitHub API,与之对话 / 与 GitHub API 对话

    • 用户返回的仓库 / 为用户返回仓库

    • 通过 AppDelegate 访问数据 / 通过 AppDelegate 访问数据

  • 集合类型

    • 关于 / 集合类型
  • 命令行 Swift

    • 关于 / 命令行 Swift

    • 解释后的 Swift 脚本 / 解释后的 Swift 脚本

    • 编译后的 Swift 脚本 / 编译后的 Swift 脚本

  • 编译后的 Swift 脚本

    • 关于 / 编译后的 Swift 脚本
  • 表盘组件 / 适当地使用表盘组件和快速查看

  • 条件逻辑

    • 关于 / 条件逻辑

    • if 语句 / if 语句

    • switch 语句 / switch 语句

  • 常量

    • 关于 / 变量和常量
  • 核心动画图层类

    • 关于 / 使用图层进行自定义图形

    • CAEAGLLayer 类 / 使用图层自定义图形

    • CAEmitterLayer 类 / 使用图层自定义图形

    • CAGradientLayer 类 / 使用图层自定义图形

    • CAReplicatorLayer 类 / 使用图层自定义图形

    • CAScrollLayer 类 / 使用图层自定义图形

    • CAShapeLayer 类 / 使用图层自定义图形

    • CATextLayer 类 / 使用图层自定义图形

    • CATiledLayer 类 / 使用图层自定义图形

    • CATransformLayer 类 / 使用图层自定义图形

  • Core Graphics 上下文 / 使用 drawRect 自定义图形

  • 自定义图形,使用 drawRect

    • 关于 / 使用 drawRect 自定义图形

    • 绘图 / 在 drawRect 中绘制图形

    • 方向变化,响应 / 响应方向变化

  • 自定义图形,使用图层

    • 关于 / 使用图层自定义图形

    • ProgressView,从图层创建 / 从图层创建 ProgressView

    • 停止方块,添加 / 添加停止方块

    • 进度条,添加 / 添加进度条

    • 视图,裁剪 / 裁剪视图

    • 视图,在 Xcode 中测试 / 在 Xcode 中测试视图

    • 变化,响应 / 响应变化

  • 自定义视图,通过子类化 UIView 创建

    • 关于 / 通过子类化 UIView 创建新视图

    • 自动布局,使用 / 自动布局和自定义视图

    • 约束,添加 / 约束和视觉格式语言

    • 视觉格式语言 / 约束和视觉格式语言

    • 自定义视图,添加到表格 / 将自定义视图添加到表格

  • 自定义视图,使用 Interface Builder 创建

    • 关于 / 使用 Interface Builder 创建新视图

    • 表视图控制器,创建 / 创建表视图控制器

    • 数据,在表格中显示 / 在表格中显示数据

    • 视图,在 xib 文件中定义 / 在 xib 文件中定义视图

    • 自定义视图类,连接 / 连接自定义视图类

    • 内在尺寸,处理 / 处理内在尺寸

D

  • 数据加载,从 URL

    • 关于 / 从 URL 加载数据

    • 错误,处理 / 处理错误

    • 缺失内容,处理 / 处理缺失内容

    • 嵌套 if 和 switch 语句 / 嵌套 if 和 switch 语句

    • 网络 / 网络和用户界面

    • 用户界面 / 网络和用户界面

    • 函数,在主线程上运行 / 在主线程上运行函数

  • DetailViewController 类

    • 关于 / DetailViewController 类
  • DictionaryExtensions 类 / 仓库浏览器项目

  • 直接网络连接

    • 关于 / 直接网络连接

    • 基于流的连接 / 打开基于流的连接

    • 同步读写 / 同步读写

    • 异步读写 / 异步读写

  • 文档,游乐场

    • 关于 / 游乐场和文档

    • 开始 / 使用游乐场学习

    • 游乐场格式 / 理解游乐场格式

    • 页面,添加 / 添加页面

    • 代码,文档化 / 代码文档化

    • 游乐场导航文档 / 游乐场导航文档

    • 文本,格式化 / 文本格式化

    • 符号文档 / 符号文档

  • drawRect

    • 关于 / 使用 drawRect 绘制自定义图形

E

  • 枚举类型

    • 关于 / Swift 中的枚举

    • 原始值 / 原始值

    • 关联值 / 关联值

  • ExtensionDelegate.swift / 添加手表目标

F

  • 浮点字面量

    • 关于 / 浮点字面量
  • 函数

    • 关于 / 函数

    • 命名参数 / 命名参数

    • 可选参数 / 可选参数和默认值

    • 默认值 / 可选参数和默认值

    • guard 语句 / Guards

    • 多重返回值和参数 / 多重返回值和参数

    • 结构化值,返回 / 返回结构化值

    • 错误处理 / 错误处理

    • 错误,抛出 / 抛出错误

    • 错误,捕获 / 捕获错误

    • 清理,错误后执行 / 错误后清理

G

  • GitHub API

    • 关于 / GitHub API 概述

    • 概述 / GitHub API 概述

    • 根端点 / 根端点

    • 用户资源 / 用户资源

    • 仓库资源 / 仓库资源

  • GitHubAPI 类 / 仓库浏览器项目

  • 快速查看 / 适当使用并发症和快速查看

I

  • Interface Builder

    • 自定义视图,使用 / 使用 Interface Builder 创建新视图
  • InterfaceController.swift / 添加手表目标

  • 解释型 Swift 脚本

    • 关于 / 解释型 Swift 脚本
  • iOS 应用

    • 关于 / 理解 iOS 应用
  • 迭代

    • 关于 / 迭代

    • 在字典中遍历键和值 / 在字典中遍历键和值

    • 使用 for 循环 / 使用 for 循环进行迭代

    • break 语句 / Break 和 continue

    • continue 语句 / Break 和 continue

J

  • JSON

    • 解析 / 解析 JSON

    • 错误,处理 / 处理错误

M

  • 主详情 iOS 应用

    • 创建 / 创建主详情 iOS 应用

    • 关于 / 创建主详情 iOS 应用

    • AppDelegate 类 / AppDelegate 类

    • MasterViewController 类 / MasterViewController 类

    • DetailViewController 类 / DetailViewController 类

  • MasterViewController 类

    • 关于 / MasterViewController 类
  • Meetups

    • 参考 / Meetups
  • 成员

    • 关于 / Swift 中的类
  • 方法

    • 关于 / Swift 中的类

N

  • 导航控制器

    • 添加 / 添加导航控制器

    • 场景,命名 / 命名场景和视图

    • 视图,命名 / 命名场景和视图

  • 导航文档,playground

    • 关于 / Playground 导航文档
  • nil 合并运算符

    • 关于 / nil 合并运算符
  • 通知 / 适当使用复杂性和预览

  • NSURLExtensions 类 / 代码库浏览器项目

  • 数字字面量

    • 关于 / 数字字面量

    • 二进制 / 数字字面量

    • 八进制 / 数字字面量

    • 十六进制 / 数字字面量

O

  • 开源 Swift

    • 关于 / 开源 Swift
  • 可选类型

    • 关于 / 可选类型

P

  • 游乐场

    • 开始 / 开始使用游乐场

    • 创建 / 创建游乐场

    • 控制台输出,查看 / 查看控制台输出

    • 时间线,查看 / 查看时间线

    • 文档 / 游乐场和文档

    • 格式 / 理解游乐场格式

    • 限制 / 游乐场的限制

  • 属性

    • 关于 / Swift 中的类
  • 协议

    • 关于 / Swift 中的协议

    • 定义 / Swift 中的协议

Q

  • QuartzCore 框架 / 使用层自定义图形

  • 快速查看

    • 对象,显示 / 使用快速查看显示对象

    • 带颜色的标签,显示 / 显示带颜色的标签

    • 图片,显示 / 显示图片

R

  • 仓库,从视图控制器访问

    • 关于 / 从视图控制器访问仓库

    • 用户,添加 / 添加用户

    • 详情视图,实现 / 实现详情视图

    • 主视图和详情视图,转换 / 在主视图和详情视图之间转换

    • 用户头像,加载 / 加载用户的头像

    • 用户头像,显示 / 显示用户的头像

  • 仓库浏览器项目

    • 关于 / 仓库浏览器项目

    • URI 模板 / URI 模板

    • 背景线程 / 背景线程

    • 字典的 JSON,解析 / 解析字典的 JSON

    • 字典的 JSON 数组,解析 / 解析字典的 JSON 数组

  • RepositoryBrowserWatch / 添加监视目标

  • RepositoryBrowserWatch 扩展 / 添加监视目标

S

  • 场景

    • 关于 / 故事板、场景和转场
  • 转场

    • 关于 / 故事板、场景和转场, 转场

    • 展示 / 转场

  • 单视图 iOS 应用

    • 关于 / 创建单视图 iOS 应用

    • 创建 / 创建单视图 iOS 应用

    • 故事板,移除 / 移除故事板

    • 视图控制器,设置 / 设置视图控制器

  • 故事板集成,使用 Swift

    • 关于 / Swift 和故事板

    • 自定义视图控制器 / 自定义视图控制器

    • 视图,在 Swift 中连接到输出 / 在 Swift 中将视图连接到输出

    • 操作,从界面构建器调用 / 从界面构建器调用操作

    • 转场,使用代码触发 / 使用代码触发转场

    • 数据,通过转场传递 / 通过转场传递数据

  • 故事板项目

    • 创建 / 创建故事板项目

    • 标准视图控制器 / 场景和视图控制器

    • 视图,添加到场景 / 将视图添加到场景

  • 故事板

    • 关于 / 故事板、场景和转场
  • 基于流的连接

    • 关于 / 基于流的连接打开
  • 字符串字面量

    • 关于 / 字符串字面量
  • 子类

    • 关于 / Swift 中的子类和测试
  • Swift

    • URL / 开源 Swift

    • 关于 / Swift 入门

    • 下载链接 / Swift 入门

    • 数字字面量 / 数字字面量

    • 浮点字面量 / 浮点字面量

    • 字符串字面量 / 字符串字面量

    • 变量 / 变量和常量

    • 常量 / 变量和常量

    • 集合类型 / 集合类型

    • 可选类型 / 可选类型

    • 空合并运算符 / 空合并运算符

    • 类 / Swift 中的类、协议和枚举,Swift 中的类

    • 子类 / Swift 中的子类和测试

    • 测试 / Swift 中的子类和测试

    • 协议 / Swift 中的协议

    • 枚举 / Swift 中的枚举

    • 引用 / 语言

    • 博客和教程网站引用 / 博客和教程网站

  • 符号文档

    • 关于 / 符号文档
  • 同步读写

    • 关于 / 同步读写

    • 数据,写入 NSOutputStream / 向 NSOutputStream 写入数据

    • 数据,从 NSInputStream 读取 / 从 NSInputStream 读取

    • 十六进制和 UTF8 数据,读取 / 读写十六进制和 UTF8 数据

    • 十六进制和 UTF8 数据,写入 / 读写十六进制和 UTF8 数据

    • Git 协议,实现 / 实现 Git 协议

    • 远程列出 git 引用 / 远程列出 git 引用

    • 网络调用,集成到 UI / 将网络调用集成到 UI

T

  • 测试,在 Swift 中

    • 关于 / Swift 中的子类和测试
  • 线程类 / 代码库浏览器项目

  • Twitter 用户

    • 参考 / Twitter 用户

U

  • UIView

    • 概述 / UIView 概述
  • URITemplate 类 / 代码库浏览器项目

  • 用户交互,响应用户

    • 关于 / 响应用户交互

    • 上下文,添加 / 添加上下文并显示代码库

    • 代码库,显示 / 添加上下文并显示代码库

    • 详细信息屏幕,添加 / 添加详细信息屏幕

    • 详细信息屏幕,填充 / 填充详细信息屏幕

V

  • 变量

    • 关于 / 变量和常量

W

  • 观看应用

    • 关于 / 监视应用

    • 监视目标,添加 / 添加监视目标

    • GitHubAPI,添加到监视目标 / 将 GitHubAPI 添加到监视目标

    • 最佳实践 / 监视应用的最佳实践

  • 手表界面

    • 创建 / 创建手表界面

    • 用户列表,添加到监视 / 将用户列表添加到监视

    • 连接接口 / 连接接口

    • 图片,添加 / 添加图片

X

  • XCPlayground 框架

    • 关于 / 高级技术

    • 值,显式捕获 / 显式捕获值

    • 异步代码,执行 / 运行异步代码

  • XCTest 框架

    • 关于 / Swift 中的子类和测试
  • XML

    • 解析 / 解析 XML

    • 解析器代理,创建 / 创建解析器代理

    • 数据,下载 / 下载数据

    • 数据,解析 / 解析数据

posted @ 2025-10-24 10:07  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报