IOS18-编程初学者指南-全-
IOS18 编程初学者指南(全)
原文:
zh.annas-archive.org/md5/c79b480579d21d3ef3e2083c44cdee36译者:飞龙
前言
欢迎来到 iOS 18 编程入门。这本书是 iOS 编程入门 系列的第九版,已经完全更新以适应 iOS 18、macOS 15.0 Sequioa 和 Xcode 16。
在这本书中,你将构建一个名为 JRNL 的日记应用。你将从探索 Xcode 开始,这是苹果的编程环境,也被称为其 集成开发环境(IDE)。接下来,你将学习 Swift 的基础知识,Swift 是用于 iOS 应用的编程语言,并了解它是如何用于完成常见的编程任务的。
在你熟练掌握 Swift 的基础知识后,你将开始创建 JRNL 应用的用户界面。在这个过程中,你将使用故事板,并通过 segues 连接应用场景。
在完成用户界面后,你将添加代码来实现应用的功能。首先,你将学习如何使用表格视图显示数据。接下来,你将学习如何向应用中添加数据,以及如何在视图控制器之间传递数据。之后,你将学习如何确定设备位置并在地图上显示注释。然后,你将学习如何使用 JSON 文件持久化应用数据,创建自定义视图,并从相机或照片库添加照片。最后,你将通过实现集合视图来替代表格视图,使你的应用能够在具有更大屏幕的设备上运行,例如 iPad 或 Mac。
你现在拥有了一个完整的应用,但如何添加最新的 iOS 18 功能呢?你将从学习 SwiftData 开始,它允许你使用常规 Swift 代码描述数据模型并操作模型实例。接下来,你将学习如何使用 SwiftUI 开发应用,这是一种为所有苹果平台开发应用的全新方法。之后,你将学习如何使用 Swift Testing 测试你的代码,以及如何将苹果智能功能引入你的应用。
最后,你将学习如何使用内部和外部测试人员测试你的应用,并将其发布到 App Store。
本书面向对象
本书针对那些具有最少编码经验、新接触 Swift 和 iOS 应用开发领域的人士。建议具备基本的编程概念理解。
本书涵盖内容
第一章,探索 Xcode,带你游览 Xcode,并讨论了你在本书中将使用的所有不同部分。
第二章,简单值和类型,讨论了 Swift 语言如何实现值和类型。
第三章,条件语句和可选类型,展示了如何实现 if 和 switch 语句,以及如何实现可能或可能没有值的变量。
第四章,范围运算符和循环,展示了如何在 Swift 中处理范围以及循环的不同实现方式。
第五章,集合类型,涵盖了常见的集合类型,包括数组、字典和集合。
第六章,函数和闭包,涵盖了如何使用函数和闭包将指令分组在一起。
第七章,类、结构和枚举,讨论了在 Swift 中表示包含状态和行为复杂对象的方法。
第八章,协议、扩展和错误处理,讨论了创建复杂数据类型可以采用的协议,扩展现有类型的特性,以及如何在代码中处理错误。
第九章,Swift 并发,介绍了并行和异步编程的概念,并展示了您如何在应用中实现它们。
第十章,设置用户界面,涉及创建JRNL应用并设置用户将看到的初始屏幕。
第十一章,构建用户界面,涵盖了为JRNL应用设置主屏幕。
第十二章,完成用户界面设置,涵盖了为JRNL应用设置剩余的屏幕。
第十三章,修改应用屏幕,是关于在故事板中配置应用的每个屏幕。
第十四章,开始使用 MVC 和表格视图,涵盖了与表格视图一起工作以及如何使用它来显示项目列表。
第十五章,将数据添加到表格视图中,涉及使用数组作为数据源将数据合并到表格视图中。
第十六章,在视图控制器之间传递数据,教您如何将使用视图控制器输入的数据添加到数组中,以及如何将数据从数组传递到另一个视图控制器。
第十七章,开始使用 Core Location 和 MapKit,涉及使用 Core Location 和 MapKit 确定您的设备位置并在地图上添加注释。
第十八章,开始使用 JSON 文件,涉及学习如何使用 JSON 文件存储和检索用户数据。
第十九章,开始使用自定义视图,教您如何创建和使用一个显示星级评分的自定义视图。
第二十章,开始使用相机和照片库,讨论了如何将相机或照片库中的照片添加到您的应用中。
第二十一章,开始使用搜索,教您如何为主屏幕实现搜索栏。
第二十二章,开始使用集合视图,展示了如何实现集合视图以替代表格视图,以适应具有更大屏幕的设备,如 Mac 或 iPad。
第二十三章,开始使用 SwiftData,涉及实现 Apple 的新 SwiftData 框架以在您的应用中持久化数据。
第二十四章,开始使用 SwiftUI,介绍了使用 Apple 的新 SwiftUI 技术构建应用。
第二十五章,开始使用 Swift 测试,教您如何使用 Swift 测试来测试您的代码。
第二十六章,开始使用 Apple 智能功能,展示了如何将 Apple 智能功能添加到您的应用中。
第二十七章,测试并将您的应用提交到 App Store,涉及如何测试并将您的应用提交到 App Store。
为了充分利用这本书
本书已完全修订以适应 iOS 18、macOS 15.0 Sequioa、Xcode 16 和 Swift 6。本书的 第四部分 还涵盖了 Apple 在 2024 年 WWDC 上推出的最新技术,包括 SwiftData、SwiftUI、Swift 测试和 Apple 智能技术。
要完成本书中的所有练习,您将需要:
-
运行 macOS 14.0 Sonoma、macOS 15.0 Sequioa 或更高版本的 Mac 计算机
-
Xcode 16.0 或更高版本
要检查您的 Mac 是否支持 macOS 15.0 Sequioa,请查看此链接:www.apple.com/my/macos/macos-sequoia-preview/。如果您的 Mac 支持,您可以通过 系统偏好设置 中的 软件更新 来更新 macOS。
要获取 Xcode 的最新版本,您可以从 Apple App Store 下载。大多数练习可以在没有 Apple 开发者账户的情况下完成,并使用 iOS 模拟器。如果您希望在实际的 iOS 设备上测试您正在开发的 App,您将需要一个免费或付费的 Apple 开发者账户。
以下章节需要付费的 Apple 开发者账户:第二十七章,测试并将您的 App 提交到 App Store。如何获取付费 Apple 开发者账户的说明已包含在内。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还提供其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们!
代码实战
访问以下链接查看代码运行的视频:
www.youtube.com/playlist?list=PLeLcvrwLe185EJSoURfHhSHfbPFkiZl6m
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781836204893。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“所以,这是一个非常简单的函数,名为 serviceCharge()。”
代码块设置为以下格式:
class ClassName {
property1
property2
property3
method1() {
code
}
method2() {
code
}
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
let cat = Animal()
**cat.****name****=****"****Cat"**
**cat.****sound****=****"Mew"**
**cat.****numberOfLegs****=****4**
**cat.****breathesOxygen****=****true**
print(cat.name)
粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的词汇在文本中显示如下。以下是一个示例:“启动 Xcode 并点击 创建一个新的 Xcode 项目:”
重要提示
显示如下。
提示
显示如下。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果你对这本书的任何方面有疑问,请在邮件主题中提及书名,并通过客户关怀@packtpub.com 给我们发邮件。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这一点。请访问www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com 与我们联系,并提供材料的链接。
如果你有兴趣成为作者: 如果你有一个你擅长的主题,并且你对撰写或为书籍做出贡献感兴趣,请访问authors.packtpub.com。
留下评论!
感谢您从 Packt Publishing 购买这本书——我们希望您喜欢它!您的反馈对我们来说是无价的,它帮助我们改进和成长。一旦您阅读完毕,请花一点时间在亚马逊上留下评论;这只需一分钟,但对像您这样的读者来说意义重大。

扫描下面的二维码或访问链接以获得你选择的免费电子书。

下载这本书的免费 PDF 副本
感谢您购买这本书!
你喜欢在路上阅读,但无法携带你的印刷书籍到处走?
你的电子书购买是否与你的选择设备不兼容?
别担心,现在每购买一本 Packt 书籍,你都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从你喜欢的技术书籍中搜索、复制和粘贴代码到你的应用程序中。
优惠不会就此停止,你还可以获得独家折扣、时事通讯和每日免费内容的邮箱访问权限。
按照以下简单步骤获取这些好处:
- 扫描下面的二维码或访问以下链接:

packt.link/free-ebook/9781836204893
-
提交你的购买证明。
-
就这些!我们将直接将你的免费 PDF 和其他好处发送到你的邮箱。
第一部分
Swift
欢迎来到本书的第一部分。在本部分,你将首先探索 Xcode,这是苹果的编程环境,也被称为集成开发环境(IDE)。之后,你将开始学习 Swift 6 的基础知识,这是 iOS 应用程序中使用的编程语言,并了解它是如何用于完成常见编程任务的。
本部分包括以下章节:
-
第一章,探索 Xcode
-
第二章,简单值和类型
-
第三章,条件语句和可选类型
-
第四章,范围操作符和循环
-
第五章,集合类型
-
第六章,函数和闭包
-
第七章,类、结构和枚举
-
第八章,协议、扩展和错误处理
-
第九章,Swift 并发
在本部分的结尾,你将了解创建应用程序并在模拟器或设备上运行的过程,并且你将具备使用 Swift 编程语言完成常见编程任务的实际知识。这将为你学习下一章做好准备,并使你能够创建自己的 Swift 程序。让我们开始吧!
第一章:探索 Xcode
欢迎来到 iOS 18 编程入门。我希望你会发现这是一本关于在 App Store 上创建和发布 iOS 18 应用的有用介绍。
在本章中,你将在你的 Mac 上下载并安装 Xcode。然后,你将探索 Xcode 用户界面。之后,你将创建你的第一个 iOS 应用 并在 模拟器 中运行它。最后,你将在 iOS 设备 上运行你的应用。
到本章结束时,你将了解如何创建 iOS 应用,如何在模拟器中运行它,以及如何在 iOS 设备上运行它。
本章将涵盖以下主题:
-
从 App Store 下载并安装 Xcode
-
探索 Xcode 用户界面
-
在模拟器中运行你的应用
-
在 iOS 设备上运行你的应用
技术要求
要完成本章的练习,你需要以下内容:
-
运行 macOS 14 Sonoma 或 macOS 15 Sequoia 的 Apple Mac 计算机(Apple Silicon 或 Intel)
-
Apple 账户(如果你没有,你将在本章中创建一个)
-
可选,运行 iOS 18 的 iOS 设备
本章的 Xcode 项目位于本书代码包的 Chapter01 文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,看看代码的实际运行情况:
在下一节中,你将开始从 App Store 下载 Xcode,这是苹果为开发 iOS 应用而提供的 集成开发环境(IDE)。
下载的总大小非常大(Xcode 为 2.98 GB,iOS 18 模拟器为 8.36 GB),因此下载可能需要一些时间。在下载之前,请确保你有足够的磁盘空间。
从 App Store 下载并安装 Xcode
Xcode 是苹果为开发 macOS、iOS、iPadOS、watchOS、tvOS 和 visionOS 应用而提供的 IDE。在编写你的第一个应用之前,你需要在你的 Mac 上下载并安装 Xcode。按照以下步骤操作:
-
在你的 Mac 上,从 Apple 菜单中选择 App Store。
-
在右上角的搜索框中,输入
Xcode并按 回车 键。 -
你会在搜索结果中看到 Xcode。点击 获取 然后点击 安装。
-
如果你有一个 Apple 账户,请在文本框中输入它,并在提示时输入你的密码。如果没有,点击 创建 Apple 账户 按钮,按照逐步说明创建一个:

图 1.1:创建 Apple 账户对话框
你可以通过此链接了解如何使用此链接创建 Apple 账户的更多信息:https://support.apple.com/en-us/108647#appstore。
- 一旦 Xcode 安装完成,启动它。你会看到一个许可协议屏幕。点击 同意:

图 1.2:许可协议屏幕
- 你将被提示输入你的 Mac 的管理员 用户名 和 密码。一旦完成,点击 确定:

图 1.3:提示输入管理员用户名和密码
- 您将看到一个显示可用的开发平台的屏幕。现在您只需要 macOS 和 iOS。勾选 iOS 18.0,取消勾选所有其他选项,并点击 下载并安装:

图 1.4:开发平台屏幕
- Xcode 将提示您重新启动以使用更新的框架。点击 重新启动 Xcode:

图 1.5:重新启动 Xcode 提示
- 您将看到一个 Xcode 新功能 屏幕。点击 继续:

图 1.6:Xcode 新功能屏幕
- 您将看到 欢迎使用 Xcode 屏幕。在左侧面板中点击 创建新项目...:

图 1.7:欢迎使用 Xcode 屏幕
- Xcode 将自动开始下载 iOS 18.0 模拟器。请注意,在此过程完成之前,您将无法在模拟器上运行任何应用程序:

图 1.8:模拟器下载进度条
- 您将看到以下新项目屏幕。在 为您的项目选择模板 部分,选择 iOS。然后选择 应用程序 并点击 下一步:

图 1.9:新项目屏幕
- 您将看到 为您的项目选择选项 屏幕:

图 1.10:为您的项目选择选项屏幕
按以下选项进行配置:
-
产品名称:您的应用程序的名称。在文本框中输入
JRNL。 -
组织标识符:用于在 App Store 上为您的应用程序创建一个唯一的标识符。现在输入
com.myname。这被称为反向域名命名格式,是 iOS 开发者常用的格式。 -
界面:创建应用程序用户界面的方法。将其设置为 Storyboard。
-
测试系统:您将使用的测试系统。您将在 第二十五章,Swift 测试入门 中了解它。现在将其设置为 无。
将其他设置保留为默认值。完成后点击 下一步。
- 您将看到一个 保存 对话框。选择一个位置来保存您的项目,例如 桌面 或 文档 文件夹,然后点击 创建:

图 1.11:保存对话框
-
您将看到一个显示 Git 仓库创建失败 的对话框。点击 修复。
您看到此对话框的原因是因为在 保存 对话框中勾选了 源代码控制 复选框。Apple 建议开启 源代码控制。源代码控制 不在本书的范围之内,但如果您想了解更多关于版本控制和 Git 的信息,请参阅此链接:
git-scm.com/video/what-is-version-control。 -
您将看到以下 源代码控制 屏幕:

图 1.12:源代码控制首选项屏幕
输入以下信息:
-
作者姓名:您的名字
-
作者电子邮件:您的电子邮件地址
完成后,通过点击左上角的关闭按钮关闭 源代码控制 屏幕。Xcode 主窗口将出现。
太棒了!您现在已经成功下载并安装了 Xcode,并创建了您的第一个项目。在下一节中,您将了解 Xcode 用户界面。
探索 Xcode 用户界面
您已经成功创建了您的第一个 Xcode 项目!如您所见,Xcode 用户界面被分为几个不同的部分,如下所示:

图 1.13:Xcode 用户界面
让我们更详细地看看每一部分。以下描述对应于前面截图中的数字:
-
工具栏(1):用于构建和运行您的应用程序,并查看运行任务的进度。
-
导航器区域(2):提供快速访问项目各个部分。默认显示 项目导航器。
-
编辑区域(3):允许您编辑源代码、用户界面和其他资源。
-
检查器区域(4):允许您查看和编辑 导航器区域 或 编辑区域 中选定项的信息。
-
调试区域(5) – 包含 调试栏、变量视图 和 控制台。通过按 Shift + Command + Y 切换 调试区域。
接下来,让我们更仔细地检查工具栏。工具栏的左侧如下所示:

图 1.14:Xcode 工具栏(左侧)
让我们更详细地看看每一部分。以下描述对应于前面截图中的数字:
-
导航器按钮(1) – 用于显示和隐藏导航器区域。
-
停止按钮(2) – 仅在应用程序运行时出现在运行按钮旁边。停止当前运行的应用程序。
-
运行按钮(3) – 用于构建和运行您的应用程序。
-
方案菜单(4) – 显示构建您的项目 (JRNL) 的特定方案以及运行您的应用程序的目标设备(iPhone SE(第 3 代)。方案和目标不同。方案指定构建和运行项目设置。目标指定应用程序的安装位置,并存在于模拟器和物理设备上。
-
活动视图(5) – 显示运行任务的进度。
工具栏的右侧如下所示:

图 1.15:Xcode 工具栏(右侧)
让我们更详细地看看每一部分。以下描述对应于前面截图中的数字:
-
Xcode Cloud 按钮(1) – 允许您登录到 Xcode Cloud,这是一个集成到 Xcode 中的持续集成和交付服务。
-
图书馆按钮(2) – 显示用户界面元素、代码片段和其他资源。
-
检查器按钮(3) – 用于显示和隐藏检查器区域。
不要被所有不同的部分吓到,因为你在接下来的章节中会详细了解它们。现在,你已经熟悉了 Xcode 界面,你将在模拟器中运行你刚刚创建的应用,它将显示 iOS 设备的表示。
在模拟器中运行你的应用
模拟器是在安装 Xcode 后下载和安装的。它提供了一个模拟的 iOS 设备,这样你就可以看到你的应用看起来如何以及它的行为,而无需物理 iOS 设备。它可以模拟 iPad 和 iPhone 的所有屏幕尺寸和分辨率,这样你就可以轻松地在多个设备上测试你的应用。
要在模拟器中运行你的应用,请按照以下步骤操作:
- 点击目标弹出菜单以查看模拟设备的列表。从该菜单中选择iPhone SE(第 3 代):

图 1.16:Xcode 目标弹出菜单,已选择 iPhone SE(第 3 代)
在你自己的项目中,你应该选择你需要的任何模拟器。话虽如此,如果你想与本书中的截图完全匹配,请使用iPhone SE(第 3 代)模拟器。这个模拟器还有一个主页按钮,因此更容易回到主页。
-
点击运行按钮来在当前选定的模拟器上安装和运行你的应用。你也可以使用Command + R键盘快捷键。
-
模拟器将启动并显示 iPhone SE(第 3 代)的表示。你的应用显示一个空白屏幕,因为你还没有向项目中添加任何内容:

图 1.17:模拟器显示你的应用
- 切换回 Xcode,点击停止按钮(或按Command + .)来停止当前运行的项目。
你刚刚在模拟器中创建并运行了你的第一个 iOS 应用!做得好!
目标菜单有一个显示连接到你的 Mac 的物理设备的部分,以及一个构建部分。你可能想知道它们有什么用。让我们在下一节中看看。
理解构建部分
在上一节中,你学习了如何在目标菜单中选择模拟设备来运行你的应用。除了模拟设备的列表外,此菜单还有一个显示连接到你的 Mac 的物理设备的部分,以及一个构建部分。
这些功能允许你在实际的 Mac 或 iOS 设备上运行应用,并为提交到 App Store 做准备。
点击工具栏中的目标菜单以查看菜单顶部的物理设备和构建部分:

图 1.18:Xcode 目标菜单显示设备和构建部分
如果你有一台 Apple Silicon Mac,物理设备部分将显示文本说明 我的 Mac(专为 iPad 设计),因为 Apple Silicon Mac 可以运行 iOS 应用。否则,将显示 无设备。如果你插入了一个 iOS 设备,它将出现在此部分,并且你可以运行你为测试而开发的 app。在真实设备上运行你的 app 是推荐的,因为模拟器无法准确反映真实 iOS 设备的性能特征,并且没有真实设备所具有的硬件功能。
构建部分有两个菜单项,任何 iOS 设备(arm64) 和 任何 iOS 模拟器设备 (arm64, x86_64)。这些用于在提交到 App Store 之前存档你的 app。你将在 第二十七章,测试并将你的 app 提交到 App Store 中学习如何这样做。
现在我们来看看如何在真实的 iOS 设备上构建和运行你的 app。尽管这本书中的大多数说明不需要你拥有 iOS 设备,但如果你没有,你可以跳过下一节,直接进入 第二章,简单值和类型。
在 iOS 设备上运行你的 app
虽然你将能够使用模拟器完成这本书中的大多数练习,但建议在真实设备上构建和测试你的 app,因为模拟器无法模拟某些硬件组件和软件 API。
要全面了解模拟器和实际设备之间的所有差异,请参阅此链接:https://help.apple.com/simulator/mac/current/#/devb0244142d。
除了你的设备外,你还需要一个 Apple 账户(用于自动创建免费的 Apple 开发者账户)或付费的 Apple 开发者账户,才能在你的设备上构建和运行你的 app。你可以使用你从 App Store 下载 Xcode 时使用的同一个 Apple 账户。要在 iOS 设备上运行你的 app,请按照以下步骤操作:
-
使用随你的 iOS 设备一起提供的电缆将你的设备连接到你的 Mac,并确保 iOS 设备已解锁。
-
你的 Mac 将显示一个 允许配件连接 提示。点击 允许。
-
你的 iOS 设备将显示一个 信任此计算机 提示。轻触 信任 并在提示时输入你的设备密码。你的 iOS 设备现在已连接到你的 Mac,并将在 Xcode 的目标菜单中显示。
-
在 Xcode 菜单栏中选择 窗口 | 设备和模拟器。你将看到一个显示消息的窗口,说明 开发者模式已禁用:

图 1.19:Xcode 设备和模拟器窗口显示开发者模式已禁用
Apple 在 2022 年的全球开发者大会(WWDC 2022)期间引入了开发者模式,这是在运行 iOS 16 或更高版本的设备上安装、运行和调试你的 app 所必需的。
要观看关于开发者模式的 WWDC 2022 视频,请点击此链接:developer.apple.com/videos/play/wwdc2022/110344/。
- 要在您的 iOS 设备上启用开发者模式,请转到 设置 | 隐私与安全,向下滚动到 开发者模式 项,并点击它:

图 1.20:隐私与安全屏幕显示开发者模式
- 打开 开发者模式 开关:

图 1.21:开发者模式开关
-
将会弹出一个警告,提醒您开发者模式会降低您的 iOS 设备的安全性。点击警告的 重启 按钮。
-
在您的 iOS 设备重新启动并解锁后,通过点击 启用 并输入您的 iOS 设备密码来确认您想要启用 开发者模式。
-
设备和模拟器窗口将显示 准备 iPhone 信息。等待几分钟,然后确认设备和模拟器窗口不再显示 开发者模式已禁用 文本:

图 1.22:Xcode 设备和模拟器窗口显示准备 iPhone 信息
您的 iOS 设备现在已准备好安装和运行来自 Xcode 的应用程序。
-
在 Xcode 中,从目标菜单中选择您的 iOS 设备。
-
通过点击运行按钮(或使用 Command + R)来运行项目。您将在 Xcode 的 签名与能力 面板中遇到以下错误:“JRNL”的签名需要一个开发团队:

图 1.23:Xcode 签名与能力面板
这是因为要在 iOS 设备上运行应用程序需要数字证书,您需要将免费或付费的 Apple 开发者账户添加到 Xcode,以便生成数字证书。
使用 Apple 账户创建免费的开发者账户将允许您在 iOS 设备上测试您的应用程序,但它仅有效期为 7 天。此外,您还需要付费的 Apple 开发者账户才能在 App Store 上分发应用程序。您将在第二十七章 测试和提交您的应用程序到 App Store 中了解更多信息。
证书确保只有您授权的应用程序才能在您的设备上运行。这有助于防止恶意软件。您也可以通过此链接了解更多信息:https://help.apple.com/xcode/mac/current/#/dev60b6fbbc7。
- 点击 添加账户... 按钮:

图 1.24:Xcode 签名与能力面板,已选择 添加账户… 按钮
- Xcode 设置 窗口出现,并选中了 账户 选项卡。输入您的 Apple 账户并点击 下一步:

图 1.25:创建 Apple 账户对话框
注意,如果您愿意,可以使用 创建 Apple ID 按钮创建不同的 Apple 账户。
您也可以通过在 Xcode 菜单中选择 设置 来访问 Xcode 设置。
- 当提示时输入您的密码。几分钟后,账户 选项卡将显示您的账户设置:

图 1.26:Xcode 预设中的账户选项卡
-
完成后,通过点击左上角的红色关闭按钮关闭 设置 窗口。
-
在 Xcode 的编辑区域中,点击签名与功能。确保自动管理签名被勾选,并且从团队下拉菜单中选择个人团队:

图 1.27:Xcode 签名与功能面板,已设置账户
-
如果您在此屏幕上仍然看到错误,请尝试通过在其中输入一些随机字符来更改您的包标识符,例如,
com.myname6712.JRNL。 -
构建并运行您的应用程序。如果您被提示输入密码,请输入您的 Mac 登录密码并点击始终允许。
-
您的应用程序将安装在您的 iOS 设备上。然而,它将无法启动,您将看到以下信息:

图 1.28:无法启动“JRNL”对话框
- 这意味着您需要信任已安装到您设备上的证书。您将在下一节中学习如何做到这一点。
在您的 iOS 设备上信任开发者应用程序证书
开发者应用程序证书是一个特殊文件,它将与应用程序一起安装到您的 iOS 设备上。在您的应用程序可以运行之前,您需要信任它。按照以下步骤操作:
- 在您的 iOS 设备上,点击设置 | 通用 | VPN 与设备管理:

图 1.29:设置中的 VPN 与设备管理设置
- 点击您的 Apple 账户:

图 1.30:设备管理设置中的您的 Apple 账户
- 点击信任:

图 1.31:信任按钮
- 点击允许:

图 1.32:允许对话框
您应该看到以下文本,这表明应用程序现在已被信任:

图 1.33:设备管理部分,带有已信任证书
- 在 Xcode 中点击运行按钮以构建和再次运行。您将看到您的应用程序在 iOS 设备上启动并运行。
恭喜!您已成功在真实 iOS 设备上运行了您的应用程序!
摘要
在本章中,您学习了如何在 Mac 上下载和安装 Xcode。然后,您熟悉了 Xcode 用户界面的不同部分。之后,您创建了您的第一个 iOS 应用程序,选择了一个模拟 iOS 设备,并在模拟器中构建和运行了应用程序。最后,您学习了如何通过 USB 将 iOS 设备连接到 Xcode,以便您可以在其上运行应用程序。
在下一章中,我们将开始使用 Swift Playgrounds 探索 Swift 语言,并学习简单的值和类型如何在 Swift 中实现。
加入我们的 Discord!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会议与作者聊天,等等。扫描二维码或访问链接加入社区。
第二章:简单值和类型
现在你已经在前一章中对 Xcode 进行了简要的浏览,让我们来看看你将用来编写应用的 Swift 编程语言。
首先,你将探索 Swift 游乐场,这是一个交互式环境,你可以在此处输入 Swift 代码,并立即显示结果。然后,你将学习 Swift 如何表示和存储各种类型的数据。之后,你将了解一些酷炫的 Swift 功能,例如 类型推断 和 类型安全,这些功能可以帮助你更简洁地编写代码并避免常见错误。最后,你将学习如何对数据进行常见操作以及如何将消息打印到调试区域以帮助你解决问题。
到本章结束时,你应该能够编写简单的程序来存储和处理字母和数字。
以下主题将涵盖:
-
介绍 Swift 游乐场
-
探索数据类型
-
探索常量和变量
-
理解类型推断和类型安全
-
探索运算符
-
使用
print()语句要了解 Swift 语言最新版本的信息,请访问
docs.swift.org/swift-book/documentation/the-swift-programming-language/。
技术要求
要完成本章的练习,你需要以下内容:
-
运行 macOS 14 Sonoma 或 macOS 15 Sequoia 的苹果 Mac 电脑
-
已安装 Xcode 16(有关安装 Xcode 的说明,请参阅 第一章,探索 Xcode)
本章节的 Xcode 游乐场位于本书代码包的 Chapter02 文件夹中,可在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
观看以下视频以查看代码的实际效果:
在下一节中,你将创建一个新的游乐场,你可以在其中输入本章中展示的代码。
介绍 Swift 游乐场
游乐场是交互式编码环境。你在左侧面板中输入代码,结果会立即在右侧面板中显示。这是一种很好的实验代码和探索 iOS SDK 的方法。
SDK 是软件开发工具包的缩写。要了解更多关于 iOS SDK 的信息,请访问 developer.apple.com/ios/。
让我们从创建一个新的游乐场并检查其用户界面开始。按照以下步骤操作:
- 要创建游乐场,启动 Xcode 并从 Xcode 菜单栏中选择 文件 | 新建 | 游乐场...:

图 2.1:选择 File | New | Playground... 的 Xcode 菜单栏
- 选择一个模板以创建新的游乐场:屏幕出现。iOS 应已选中。选择 空白 并点击 下一步:

图 2.2:为新游乐场选择模板:屏幕
- 将你的游乐场命名为
SimpleValues并保存到任何你喜欢的位置。完成后点击 创建:

图 2.3:保存对话框
- 你会在屏幕上看到游乐场:

图 2.4:Xcode 游乐场用户界面
如你所见,它比 Xcode 项目简单得多。让我们更详细地看看界面:
-
导航按钮(1):这会显示或隐藏导航区域。
-
活动视图(2):这显示了当前的操作或状态。
-
库按钮(3):这会显示代码片段和其他资源。
-
检查器按钮(4):这会显示或隐藏检查器区域。
-
导航区域(5):这提供了快速访问项目各个部分的途径。默认情况下显示项目导航器。
-
编辑器区域(6):你在这里编写代码。
-
结果区域(7):这为你编写的代码提供即时反馈。
-
运行按钮(8):这会从选定的行执行代码。
-
边框(9):这个边框分隔了编辑器和结果区域。如果你发现结果区域显示的结果被截断,可以将边框向左拖动以增加其大小。
-
运行/停止按钮(10):这会执行或停止游乐场中所有代码的执行。
-
调试区域(11):这会显示
print()命令的结果。 -
调试按钮(12):这会显示或隐藏调试区域。
你可能会发现游乐场中的文本太小,难以阅读。让我们看看如何在下一节中将其放大。
自定义字体和颜色
Xcode 提供了广泛的定制选项。你可以在 设置... 菜单中访问它们。如果你发现文本太小,难以看清,请按照以下步骤操作:
-
从 Xcode 菜单中选择 设置... 以显示设置窗口。
-
在设置窗口中,点击 主题 并选择 演示(浅色) 以使文本更大,更容易阅读:

图 2.5:选择主题面板的 Xcode 设置窗口
- 关闭设置窗口返回到游乐场。注意,游乐场中的文本比之前更大。你也可以尝试其他主题。
现在你已经根据自己的喜好自定义了字体和颜色,让我们看看如何在下一节中运行游乐场代码。
运行游乐场代码
你的游乐场中已经包含了一条指令。要执行指令,请按照以下步骤操作:
- 点击指令左侧的运行按钮。几秒钟后,你将在结果区域看到
"``Hello, playground":

图 2.6:游乐场结果显示区域显示“Hello, playground”
你也可以使用左下角的运行/停止按钮,或者使用键盘快捷键 Command + Shift + Return 来运行游乐场中的所有代码。
- 为了准备游乐场在本章剩余部分的使用,请从游乐场中删除
var greeting = "Hello, playground"指令。随着你的操作,将本章中显示的代码输入到游乐场中,并点击最后一行左侧的运行按钮来运行它。
在下一节中,让我们深入了解 Swift 中使用的简单数据类型。
探索数据类型
所有编程语言都可以存储数字、单词和逻辑状态,Swift 也不例外。即使你是一位经验丰富的程序员,你也可能会发现 Swift 对这些值的表示与其他你可能熟悉的语言不同。
更多有关数据类型的信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics.
在下一节中,我们将逐步介绍 Swift 中的 整数、浮点数、字符串 和 布尔值。
表示整数
假设你想要存储以下内容:
-
城市中的餐馆数量
-
飞机上的乘客
-
酒店的房间
你会使用整数,它们是没有分数部分的数字(包括负数)。
Swift 中的整数由 Int 类型表示。
表示浮点数
假设你想要存储以下内容:
-
圆周率(3.14159...)
-
绝对零度(-273.15°C)
你会使用带有分数部分的浮点数。
Swift 中浮点数的默认类型是 Double,它使用 64 位,包括负数。你也可以使用 Float,它使用 32 位,但 Double 是默认表示。
表示字符串
假设你想要存储以下内容:
-
餐馆的名称,例如“孟买宫殿”
-
职位描述,例如“会计”或“程序员”
-
一种水果,例如“香蕉”
你会使用 Swift 的 String 类型,它表示字符序列,并且完全符合 Unicode 标准。这使得表示不同的字体和语言变得容易。
想了解更多关于 Unicode 的信息,请访问此链接:home.unicode.org/basic-info/faq/.
表示布尔值
假设你想要存储简单的是/否问题的答案,例如以下内容:
-
是否在下雨?
-
餐馆是否有空位?
对于此,你使用布尔值。Swift 提供了一个 Bool 类型,可以被分配 true 或 false。
现在你已经了解了 Swift 如何表示这些常见的数据类型,让我们在前面创建的游乐场中尝试它们,下一节将进行操作。
在游乐场中使用常见的数据类型
你在游乐场中输入的任何内容都将被执行,结果将显示在结果区域。让我们看看当你将数字、字符串和布尔值输入到你的游乐场并执行时会发生什么。按照以下步骤操作:
-
将以下代码输入到你的游乐场的编辑器区域:
// SimpleValues 42 -23 3.14159 0.1 -273.15 "hello, world" "albatross" true false
注意,任何以//开头的行都是注释。注释是为自己创建笔记或提醒的好方法,并且会被 Xcode 忽略。
-
点击最后一行左侧的运行按钮来运行你的代码。
-
等待几秒钟。Xcode 将评估你的输入并在结果区域显示结果,如下所示:
42 -23 3.14159 0.1 -273.15 "hello, world" "albatross" true false
注意,注释不会出现在结果区域。
太棒了!你已经创建并运行了你的第一个沙盒。让我们看看如何在下一节中存储不同的数据类型。
探索常量和变量
现在你已经了解了 Swift 支持的基本数据类型,让我们看看如何存储它们,以便你可以在以后对它们进行操作。
你可以使用常量或变量来存储值。两者都是具有名称的容器,但常量的值只能设置一次,一旦设置就不能更改,而变量的值可以在任何时候更改。
在使用之前,你必须声明常量和变量。常量使用let关键字声明,而变量使用var关键字声明。
让我们通过在我们的沙盒中实现它们来探索常量和变量是如何工作的。按照以下步骤操作:
-
将以下代码添加到你的沙盒中,以声明三个常量:
let theAnswerToTheUltimateQuestion = 42 let pi = 3.14159 let myName = "Ahmad Sahar" -
点击最后一行左侧的运行按钮来运行它。在每种情况下,都会创建一个容器并命名,并将分配的值存储在其中。
你可能已经注意到这里显示的常量和变量的名称以小写字母开头,并且如果名称中有多个单词,则每个后续单词的首字母都大写。这被称为驼峰式命名法。强烈建议这样做,因为大多数经验丰富的 Swift 程序员都遵循这个约定。
注意,用双引号括起来的字符序列"Ahmad Sahar"用于为myName分配值。这些被称为字符串字面量。
-
在常量声明之后添加以下代码来声明三个变量并运行它:
var currentTemperatureInCelsius = 27 var myAge = 50 var myLocation = "home"
与常量一样,在每种情况下都会创建一个容器并命名,并将分配的值存储在其中。
存储的值会在结果区域显示。
-
常量的值一旦设置就不能更改。为了测试这一点,在变量声明之后添加以下代码:
let isRaining = true isRaining = false
当你输入第二行代码时,会出现一个带有建议的弹出菜单:

图 2.7:自动完成弹出菜单
使用上箭头键和下箭头键选择isRaining常量,然后按Tab键选择它。这个功能被称为自动完成,有助于在输入代码时防止输入错误。
- 当你完成输入后,等待几秒钟。在第二行,你会看到一个错误通知(一个中间有白色点的红色圆圈)出现:

图 2.8:错误通知
这意味着你的程序中存在错误,Xcode 认为它可以修复。错误出现是因为你试图在设置初始值后为常量分配新值。
- 点击红色圆圈以展开错误消息。你会看到一个带有修复按钮的框:

图 2.9:扩展的错误通知
Xcode 告诉你问题是什么(无法赋值:'isRaining'是一个'let'常量)并建议一个修正(将'let'改为'var'以使其可变)。"可变"的意思是值可以在最初设置后更改。
- 点击修复按钮。你会看到
isRaining常量声明已被更改为变量声明:

图 2.10:应用了修复的代码
由于可以在创建变量后为其分配新值,错误已解决。但是请注意,建议的修复可能不是最佳解决方案。随着你在 iOS 开发方面经验的增加,你将能够确定最佳的行动方案。
如果你查看你输入的代码,你可能想知道 Xcode 是如何知道变量或常量中存储的数据类型的。你将在下一节中了解这一点。
理解类型推断和类型安全
在上一节中,你声明了常量和变量并给它们赋值。Swift 会根据提供的值自动确定常量或变量的类型,这被称为类型推断。你可以通过按住选项键并点击其名称来查看常量或变量的类型。为了看到这一点是如何发生的,请按照以下步骤操作:
-
将以下代码添加到你的游乐场中,以声明一个字符串并运行它:
let cuisine = "American" -
按住选项键并点击
cuisine以显示常量类型。你应该看到以下内容:

图 2.11:显示的类型声明
如你所见,cuisine的类型是String。
如果你想为变量或常量设置一个特定的类型?你将在下一节中看到如何做到这一点。
使用类型注解来指定类型
你已经看到 Xcode 会尝试根据提供的值自动确定变量或常量的数据类型。然而,有时你可能希望指定一个类型而不是让 Xcode 为你做这件事。为此,在常量或变量名称后输入一个冒号(:),然后跟随着期望的类型。这被称为类型注解。
将以下代码添加到你的游乐场中,以声明一个类型为Double的变量restaurantRating,然后点击运行按钮来运行它:
var restaurantRating: Double = 3
在这里,你指定了restaurantRating具有特定的类型,Double。即使你为restaurantRating分配了一个整数,它也会被存储为浮点数。
在下一节中,你将了解 Xcode 如何通过强制类型安全来帮助你减少程序中的错误数量。
使用类型安全来检查值
Swift 是一种类型安全的语言。它会检查你是否将正确类型的值分配给变量,并将不匹配的类型标记为错误。让我们通过以下步骤来了解它是如何工作的:
-
将以下语句添加到你的游乐场中,以将字符串分配给
restaurantRating并运行它:restaurantRating = "Good" -
你会看到一个错误通知(一个带有 x 的红色圆圈)。x 表示 Xcode 无法为此提供修复方案。点击红色圆圈。
-
由于你试图将一个字符串分配给类型为
Double的变量,因此会显示以下错误消息:

图 2.12:无修复方案的扩展错误通知
-
在它前面输入
//来注释掉该行,如下所示:// restaurantRating = "Good"
红色圆圈消失了,因为你的程序中不再有错误。
选择代码行并输入 Command + / 来注释掉它们。
现在你已经知道了如何将数据存储在常量和变量中,让我们看看如何在下一节中对这些数据进行操作。
探索运算符
你可以在 Swift 中执行算术、比较和逻辑运算。算术运算符用于常见的数学运算。比较和逻辑运算符检查表达式的值并返回 true 或 false。
更多关于运算符的信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators。
让我们更详细地查看每种运算符类型。你将在下一节中从算术运算符(加法、减法、乘法和除法)开始。
使用算术运算符
你可以使用这里显示的标准算术运算符对整数和浮点数执行数学运算。

图 2.13:算术运算符
让我们看看这些运算符是如何使用的。按照以下步骤操作:
-
将以下代码添加到你的游乐场中,以添加算术运算:
let sum = 23 + 20 let result = 32 - sum let total = result * 5 let divide = total / 10 -
运行代码。结果区域显示的结果分别是
43,-11,-55和-5。请注意,55 除以 10 返回 5 而不是 5.5,因为这两个数都是整数。 -
运算符只能与相同类型的操作数一起工作。输入以下代码并运行,看看如果操作数是不同类型会发生什么:
let a = 12 let b = 12.0 let c = a + b
你会得到一个错误消息(二进制运算符 '+' 无法应用于类型 'Int' 和 'Double' 的操作数)。这是因为 a 和 b 是不同类型的。请注意,Xcode 无法自动修复此问题,因此不会显示任何修复建议。
-
要修复错误,请按以下方式修改程序:
let c = Double(a) + b
Double(a) 获取存储在 a 中的值,并从中创建一个浮点数。现在两个操作数都是同一类型,现在你可以将 b 中的值加到它上面。存储在 c 中的值是 24.0,而 24 将在结果区域显示。
现在您已经了解了如何使用算术运算符,您将学习下一节中的复合赋值运算符(+=、-=、*= 和 /=)。
使用复合赋值运算符
您可以使用复合赋值运算符(如下所示)对一个值执行操作并将结果赋给一个变量:

图 2.14:复合赋值运算符
让我们看看这些运算符是如何使用的。将以下代码添加到您的游乐场并运行它:
var d = 1
d += 2
d -= 1
d += 2 表达式是 d = d + 2 的简写,因此 d 中的值现在是 1 + 2,并将 3 赋值给 d。同样,d -= 1 是 d = d - 1 的简写,因此 d 中的值现在是 3 - 1,并将 2 赋值给 d。
现在您已经熟悉了复合赋值运算符,让我们看看下一节中的比较运算符(==、/、>、<、>= 和 <=)。
使用比较运算符
您可以使用比较运算符将一个值与另一个值进行比较,结果将是 true 或 false。您可以使用以下比较运算符:

图 2.15:比较运算符
让我们看看这些运算符是如何使用的。将以下代码添加到您的游乐场并运行它:
1 == 1
2 != 1
2 > 1
1 < 2
1 >= 1
2 <= 1
让我们看看这是如何工作的:
-
1 == 1返回true,因为 1 等于 1。 -
2 != 1返回true,因为 2 不等于 1。 -
2 > 1返回true,因为 2 大于 1。 -
1 < 2返回true,因为 1 小于 2。 -
1 >= 1返回true,因为 1 大于或等于 1。 -
2 <= 1返回false,因为 2 不小于或等于 1。
返回的布尔值将在结果区域显示。
如果您想检查多个条件会发生什么?这就是逻辑运算符(AND、OR 和 NOT)发挥作用的地方。您将在下一节中了解它们。
使用逻辑运算符
逻辑运算符在处理两个或多个条件时非常有用。例如,如果您在便利店,如果您有现金或信用卡,您就可以为商品付款。在这种情况下,OR 是逻辑运算符。
您可以使用以下逻辑运算符:

图 2.16:逻辑运算符
要查看这些运算符的使用方法,将以下代码添加到您的游乐场并运行它:
(1 == 1) && (2 == 2)
(1 == 1) && (2 != 2)
(1 == 1) || (2 == 2)
(1 == 1) || (2 != 2)
(1 != 1) || (2 != 2)
!(1 == 1)
让我们看看这是如何工作的:
-
(1 == 1) && (2 == 2)返回true,因为两个操作数都是true,所以true AND true返回true。 -
(1 == 1) && (2 != 2)返回false,因为一个操作数是false,所以true AND false返回false。 -
(1 == 1) || (2 == 2)返回true,因为两个操作数都是true,所以true OR true返回true。 -
(1 == 1) || (2 != 2)返回true,因为一个操作数是true,所以true OR false返回true。 -
(1 != 1) || (2 != 2)返回false,因为两个操作数都是false,所以false OR false返回false。 -
!(1 == 1)返回false,因为1==1是true,所以NOT true返回false。
返回的布尔值将在结果区域显示。
到目前为止,你只处理过数字。在下一节中,你将看到如何使用 Swift 的 String 类型对单词和句子进行操作,这些单词和句子被存储为字符串。
执行字符串操作
如你之前所见,字符串是一系列字符。它们由 String 类型表示,并且完全符合 Unicode 标准。
更多有关字符串的信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters。
让我们学习一些常见的字符串操作。按照以下步骤操作:
-
你可以使用
+运算符将两个字符串连接起来。将以下代码添加到你的游乐场中并运行它:let greeting = "Good" + " Morning"
字符串字面量 "Good" 和 "Morning" 的值被连接起来,并在结果区域显示 "Good Morning"。
-
你可以通过将其他类型的常量或变量也转换为字符串来组合字符串。要将常量
rating转换为字符串,输入以下代码并运行它:let rating = 3.5 var ratingResult = "The restaurant rating is " + String(rating)
rating 常量包含 3.5,这是一个 Double 类型的值。将 rating 放在 String() 的括号中,会获取 rating 中存储的值并基于它创建一个新的字符串 "3.5",然后与 ratingResult 变量中的字符串组合,返回字符串 "The restaurant rating is 3.5"。
-
有一种更简单的方法来组合字符串,称为 字符串插值。字符串插值是通过在字符串中用
"\("和")"包围一个常量或变量的名称来完成的。输入以下代码并运行它:ratingResult = "The restaurant rating is \(rating)"
如前例所示,rating 中的值被用来创建一个新的字符串 "3.5",返回字符串 "The restaurant rating is 3.5"。
到目前为止,你可以在结果区域看到你的指令结果。然而,当你使用 Xcode 编写应用程序时,你将无法访问你在游乐场中看到的结果区域。为了在程序运行时显示变量和常量的内容,你将在下一节中学习如何将它们打印到调试区域。
使用 print() 语句
如你在 第一章,探索 Xcode 中所见,Xcode 项目没有像游乐场那样的结果区域,但项目和游乐场都有调试区域。使用 print() 语句会将括号内的任何内容打印到调试区域。
print() 语句是一个函数。你将在 第六章,函数和闭包 中了解更多关于函数的内容。
将以下代码添加到你的游乐场中,然后点击 运行 按钮来运行它:
print(ratingResult)
你将看到 ratingResult 的值出现在调试区域:

图 2.17:调试区域显示 print() 语句的结果
当你刚开始时,可以随意使用尽可能多的 print() 语句。这是一个真正理解程序中发生情况的好方法。
概述
在本章中,你学习了如何创建和使用游乐场文件,这允许你探索和实验 Swift。
你看到了 Swift 如何表示不同类型的数据,以及如何使用常量和变量。这使你能够在程序中存储数字、布尔值和字符串。
你还学习了类型推断、类型注解和类型安全,这些可以帮助你编写简洁且错误更少的代码。
你了解了如何对数字和字符串进行操作,这让你能够执行简单的数据处理任务。
你学习了如何修复错误,以及如何将输出打印到调试区域,这在尝试查找和修复你编写的程序中的错误时非常有用。
在下一章中,你将学习条件语句和可选参数。条件语句用于在程序中做出逻辑选择,而可选参数用于处理变量可能或可能没有值的情况。
加入我们的 Discord!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,等等。扫描二维码或访问链接加入社区。
第三章:条件语句和可选
在上一章中,你学习了数据类型、常量、变量和操作。到这一点,你可以编写简单的程序来处理字母和数字。然而,程序并不总是按顺序执行。很多时候,你需要根据条件执行不同的指令。Swift 允许你通过使用 条件语句 来做到这一点,你将学习如何使用它们。
你可能还注意到,在上一章中,每个变量或常量都被立即赋予了一个值。如果你需要一个可能最初没有值的变量,你将需要一个方法来创建一个可能或可能没有值的变量。Swift 允许你通过使用 可选 来做到这一点,你也将在这章中了解它们。
到本章结束时,你应该能够编写根据不同条件执行不同操作的程序,并处理可能或可能没有值的变量。
以下内容将涵盖:
-
介绍条件语句
-
介绍可选和可选绑定
请花些时间了解可选。对于新手程序员来说,它们可能有些令人畏惧,但正如你将看到的,它们是 iOS 开发的重要组成部分。
技术要求
本章的 Xcode 演示场位于本书代码包的 Chapter03 文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际运行情况:
创建一个新的演示场,并将其命名为 ConditionalsAndOptionals。你可以一边阅读一边输入并运行本章中的所有代码。你将从学习条件语句开始。
介绍条件语句
有时,你将想要根据特定的条件执行不同的代码块,例如在以下场景中:
-
在酒店中选择不同的房间类型。大房间的价格会更高。
-
在在线商店之间切换不同的支付方式。不同的支付方式会有不同的程序。
-
在快餐店决定要订购什么。每种食品的准备程序都不同。
要做到这一点,你会使用条件语句。在 Swift 中,这是通过使用 if 语句(用于单个条件)和 switch 语句(用于多个条件)来实现的。
更多有关条件语句的信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/controlflow。
让我们看看在下一节中如何使用 if 语句根据条件值执行不同的任务。
使用 if 语句
if语句在条件为true时执行一段代码,如果条件为false,则可选地执行另一段代码。它看起来像这样:
if condition {
code1
} else {
code2
}
让我们实现一个if语句来观察其效果。想象一下,你正在为一家餐厅编写应用程序。该应用程序将允许你检查餐厅是否营业,搜索餐厅,并检查顾客是否超过饮酒年龄限制。按照以下步骤操作:
-
要检查餐厅是否营业,将以下代码添加到你的 playground 中。运行它以创建一个常量,并在常量的值为
true时执行一个语句:let isRestaurantOpen = true if isRestaurantOpen { print("Restaurant is open.") }
首先,你创建了一个常量isRestaurantOpen,并将其赋值为true。接下来,你有一个if语句,它会检查isRestaurantOpen中存储的值。由于值是true,print()语句被执行,并在 Debug 区域打印出Restaurant is open。
-
尝试将
isRestaurantOpen的值改为false并再次运行你的代码。由于当前条件是false,Debug 区域将不会打印任何内容。 -
你也可以在值为
false时执行语句。假设顾客搜索的餐厅不在应用程序的数据库中,因此应用程序应显示一条消息表示餐厅未找到。输入以下代码以创建一个常量,并在常量的值为false时执行一个语句:let isRestaurantFound = false if isRestaurantFound == false { print("Restaurant was not found") }
常量isRestaurantFound被设置为false。接下来,检查if语句。isRestaurantFound == false条件返回true,并在 Debug 区域打印出Restaurant was not found。
你也可以使用!isRestaurantFound代替isRestaurantFound == false来检查条件。
-
尝试将
isRestaurantFound的值改为true。由于当前条件是false,Debug 区域将不会打印任何内容。 -
要在条件为
true时执行一组语句,在条件为false时执行另一组语句,请使用else关键字。输入以下代码,该代码检查酒吧中的顾客是否超过饮酒年龄限制:let drinkingAgeLimit = 21 let customerAge = 23 if customerAge < drinkingAgeLimit { print("Under age limit") } else { print("Over age limit") }
在这里,drinkingAgeLimit被赋值为21,customerAge被赋值为23。在if语句中,检查customerAge < drinkingAgeLimit。由于 23 < 21 返回false,所以执行else语句,并在 Debug 区域打印出Over age limit。如果你将customerAge的值改为19,customerAge < drinkingAgeLimit将返回true,因此将在 Debug 区域打印出Under age limit。
到目前为止,你只处理了单个条件。如果有多个条件怎么办?这就是switch语句的用武之地,你将在下一节中学习它们。
使用switch语句
要理解switch语句,我们先从实现一个带有多个条件的if语句开始。想象一下你正在编写交通灯的代码。交通灯有三种可能的颜色——红色、黄色或绿色——并且你希望根据灯的颜色执行不同的操作。为此,你可以嵌套多个if语句。按照以下步骤操作:
-
将以下代码添加到你的游乐场中,以使用多个
if语句实现交通灯,并运行它:var trafficLightColor = "Yellow" if trafficLightColor == "Red" { print("Stop") } else if trafficLightColor == "Yellow" { print("Caution") } else if trafficLightColor == "Green" { print("Go") } else { print("Invalid color") }
第一个if条件trafficLightColor == "Red"返回false,因此执行else语句。第二个if条件trafficLightColor == "Yellow"返回true,所以在调试区域打印出Caution,并且不再评估更多的if条件。尝试更改trafficLightColor的值以查看不同的结果。
这里使用的代码是有效的,但读起来有点困难。在这种情况下,使用switch语句会更加简洁且易于理解。switch语句看起来是这样的:
switch value {
case firstValue:
code1
case secondValue:
code2
default:
code3
}
值会被检查并与某个case匹配,然后执行该case的代码。如果没有任何一个case匹配,则执行default中的代码。
-
这是如何将前面显示的
if语句写成switch语句的方法。输入以下代码并运行:trafficLightColor = "Yellow" switch trafficLightColor { case "Red": print("Stop") case "Yellow": print("Caution") case "Green": print("Go") default: print("Invalid color") }
与之前的版本相比,这里的代码更容易阅读和理解。trafficLightColor中的值是"Yellow",所以case "Yellow":被匹配,并在调试区域打印出Caution。尝试更改trafficLightColor的值以查看不同的结果。
关于switch语句有两点需要注意:
-
Swift 中的
switch语句默认不会从每个case的底部跌落到下一个case。在前面显示的示例中,一旦case "Yellow":被匹配,case "Red"、case "Green"和default:将不会执行。 -
switch语句必须覆盖所有可能的case。在前面显示的示例中,任何除了"Red"、"Yellow"或"Green"之外的trafficLightColor值都将匹配到default:,并在调试区域打印出Invalid color。
这部分关于if和switch语句的内容到此结束。在下一部分,你将学习可选类型,它允许你创建没有初始值的变量,以及可选绑定,它允许在可选类型有值时执行指令。
介绍可选类型和可选绑定
到目前为止,每次你声明一个变量或常量时,你都会立即给它赋值。但如果你想在声明变量后稍后再赋值呢?在这种情况下,你会使用可选类型。
更多关于可选类型的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics。
让我们学习如何创建和使用可选类型,并看看它们如何在程序中使用。想象你正在编写一个程序,其中用户需要输入他们配偶的名字。当然,如果用户没有结婚,这个值就不存在。在这种情况下,你可以使用可选来表示配偶的名字。
可选可能有两种可能的状态。它可以包含一个值,或者不包含值。如果可选包含一个值,你可以访问它内部的值。访问可选值的这个过程被称为 解包 可选。让我们通过以下步骤看看这是如何工作的:
-
将以下代码添加到你的游乐场中,以创建一个变量并打印其内容:
var spouseName: String print(spouseName) -
由于 Swift 是类型安全的,会出现一个错误,(变量‘spouseName’在使用之前未初始化)。
-
为了解决这个问题,你可以将空字符串赋值给
spouseName。按照以下方式修改你的代码:var spouseName: String **=****""**
这使得错误消失,但空字符串仍然是一个值,而 spouseName 不应该有值。
-
由于
spouseName应该最初没有值,让我们将其设置为可选。要做到这一点,在类型注释后输入一个问号,并删除空字符串赋值:var spouseName: String**?**
你会看到一个警告,因为 spouseName 现在是一个可选的字符串变量,而不是一个常规的字符串变量,而 print() 语句期望的是一个常规字符串变量。

图 3.1:警告通知
即使有警告,现在先忽略它并运行你的代码。spouseName 的值在结果区域显示为 nil,并且在调试区域打印出 nil。nil 是一个特殊的关键字,表示可选变量 spouseName 没有值。
- 警告出现是因为
print语句将spouseName视为Any类型而不是String?类型。点击黄色三角形以显示可能的修复,并选择第一个修复:

图 3.2:突出显示第一个修复的扩展警告通知
语句将变为 print(spouseName ?? default value)。注意 ?? 操作符的使用。这意味着如果 spouseName 不包含值,将使用你提供的默认值在 print 语句中。
- 将默认值占位符替换为
"No value in spouseName",如图所示。警告将会消失。再次运行你的程序,No value in spouseName 将会在调试区域显示:

图 3.3:显示默认值的调试区域
-
让我们给
spouseName赋予一个值。按照以下方式修改代码:var spouseName: String? **spouseName****=****"Nia"** print(spouseName ?? "No value in spouseName")
当你的程序运行时,Nia 将会在调试区域显示。
-
添加一行代码以将
spouseName与另一个字符串连接,如图所示:print(spouseName ?? "No value in spouseName") **let****greeting****=****"Hello, "****+****spouseName**
你会得到一个错误,并且调试区域会显示错误信息和错误发生的位置。这是因为你不能使用 + 操作符将常规字符串变量与可选类型连接。你需要首先解包可选类型。
- 点击红色圆圈以显示可能的修复方案,你会看到以下内容:

图 3.4:带有第二个修复方案高亮的扩展错误通知
第二个修复建议使用强制解包来解决这个问题。强制解包无论可选是否包含值都会解包。如果spouseName有值,它将正常工作,但如果spouseName是nil,你的代码将崩溃。
-
点击第二个修复方案,你会在代码的最后一行
spouseName后面看到一个感叹号,这表示可选被强制解包了:let greeting = "Hello, " + spouseName**!** -
运行你的程序,你会在结果区域看到
Hello, Nia被分配给greeting,如图所示。这意味着spouseName已经成功被强制解包。 -
要查看强制解包包含
nil的变量的效果,将spouseName设置为nil:spouseName = **nil**
你的代码崩溃了,你可以在调试区域中看到导致崩溃的原因:

图 3.5:崩溃的程序及其在调试区域中的详细信息
由于spouseName现在是nil,程序在尝试强制解包spouseName时崩溃了。
处理这个问题的一个更好的方法是使用可选绑定。在可选绑定中,你尝试将可选中的值分配给一个临时变量(你可以随意命名它)。如果分配成功,将执行一个代码块。
-
要查看可选绑定的效果,修改你的代码如下:
spouseName = **"Nia"** print(spouseName ?? "No value in spouseName") **if****let** **spouseTempVar** **=****spouseName** **{** **let** **greeting** **=****"Hello, "****+****spouseTempVar** **print(greeting)** **}**
Hello, Nia将出现在调试区域。以下是它是如何工作的。如果spouseName有值,它将被解包并分配给一个临时常量spouseTempVar,然后if语句将返回true。花括号之间的语句将被执行,常量greeting将被分配值Hello, Nia。然后,Hello, Nia将在调试区域打印出来。注意,临时变量spouseTempVar不是一个可选变量。如果spouseName没有值,无法将值分配给spouseTempVar,if语句将返回false。在这种情况下,花括号中的语句根本不会执行。
-
你也可以以前一个步骤更简单的方式编写代码,如下所示:
spouseName = "Nia" print(spouseName ?? "No value in spouseName") if let **spouseName** { let greeting = "Hello, " + spouseName print(greeting) }
在这里,临时常量使用与可选值相同的名称创建,并将用于花括号之间的语句中。
-
要查看当可选包含
nil时可选绑定的效果,再次将nil分配给spouseName:spouseName = **nil**
你会注意到调试区域中没有出现任何内容,即使spouseName是nil,你的程序也不再崩溃。
这就结束了关于可选和可选绑定的部分,你现在可以创建和使用可选变量了。太棒了!
摘要
你做得很好!你学习了如何使用if和switch语句,这意味着你现在能够编写基于不同条件执行不同操作的程序。
你还学习了可选类型和可选绑定。这意味着你现在可以表示可能具有也可能不具有值的变量,并且只有当变量的值存在时才执行指令。
在下一章中,你将学习如何使用一系列值而不是单个值,以及如何使用循环重复程序语句。
加入我们的 Discord 社群!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第四章:范围运算符和循环
在上一章中,你学习了条件语句,它允许你根据不同的条件执行不同的操作,以及可选类型,它使你能够创建可能或可能没有值的变量。
在本章中,你将了解范围运算符和循环。范围运算符允许你通过指定范围的起始和结束值来表示一系列值。你将了解不同类型的范围运算符。循环允许你重复执行一条指令或一系列指令。你可以重复一个序列固定次数或直到满足某个条件。你将了解用于完成此操作的不同类型的循环。
到本章结束时,你将学会如何使用范围以及创建和使用不同类型的循环(for-in、while和repeat-while)。
本章将涵盖以下主题:
-
探索范围运算符
-
探索循环
技术要求
本章的 Xcode 游乐场位于本书代码包的Chapter04文件夹中,可以从以下链接下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition.
查看以下视频,以查看代码的实际运行情况:
如果你希望从头开始,创建一个新的游乐场并将其命名为RangeOperatorsAndLoops。
你可以边走边输入并运行本章中的所有代码。让我们从使用范围运算符指定数字范围开始。
探索范围运算符
假设你需要编写一个为百货公司编写程序,该程序可以自动向 18 至 30 岁的顾客发送折扣券。如果需要为每个年龄设置if或switch语句,将会非常繁琐。在这种情况下使用范围运算符会方便得多。
范围运算符允许你表示一系列值。假设你想要表示一个从firstNumber开始到lastNumber结束的数字序列。你不需要指定每个值;你只需以这种方式指定范围:
firstNumber...lastNumber
更多关于范围运算符的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators。
让我们在游乐场中尝试一下。按照以下步骤操作:
-
将以下代码添加到你的游乐场中并运行它:
let myRange = 10...20
这将为myRange常量分配一个从10开始到20结束的数字序列,包括这两个数字,这被称为闭区间运算符。myRange的起始和结束值将在结果区域显示。
- 显示在结果区域的结果可能会被截断。点击结果右侧的方块图标。它将在编辑器区域中内联显示:

图 4.1:显示内联结果的编辑器区域
你现在可以在代码行下面的框中看到完整的结果。如果你愿意,可以拖动框的右边缘使其变大。
记住,你可以拖动结果和编辑器区域之间的边界来增加结果区域的大小。
-
如果你不想在范围内包含序列的最后一个数字,将
...替换为..<。在下一行输入并运行以下语句:let myRange2 = 10..<20
这将在myRange2常量中存储从10开始到19结束的序列,并被称为半开区间范围运算符。
还有一种范围运算符类型,即单侧范围运算符,你将在下一章中了解它。
现在你已经知道了如何创建和使用范围,你将在下一节学习循环、不同类型的循环以及如何在程序中使用它们。
探索循环
在编程中,你经常需要重复执行相同的事情。例如,每个月,公司都需要为每位员工生成工资条。如果公司有 10,000 名员工,编写 10,000 条指令来创建工资条将是不高效的。重复一条指令 10,000 次会更好,循环就是为此而用的。
有三种循环类型:for-in循环、while循环和repeat-while循环。for-in循环将重复已知次数,而while和repeat-while循环将在循环条件为true时重复。
关于循环的更多信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/controlflow。
让我们逐一查看每种类型,从for-in循环开始,当你知道循环应该重复多少次时使用。
for-in循环
for-in循环遍历序列中的每个值,并将一组语句(称为循环体)在每次执行时执行。每个值依次分配给一个临时变量,并且可以在循环体内使用这个临时变量。它看起来是这样的:
for item in sequence {
code
}
循环重复的次数由序列中的项目数量决定。让我们先创建一个for-in循环来显示myRange中的所有数字。按照以下步骤操作:
-
将以下代码添加到你的游乐场中并运行它:
for number in myRange { print(number) }
你应该看到序列中的每个数字都在调试区域中显示。请注意,由于myRange包括范围中的最后一个数字,循环内的语句执行了 11 次。
-
让我们尝试相同的程序,但这次使用
myRange2。按照以下方式修改代码并运行:for number in **myRange2** { print(number) }
循环内的语句执行了 10 次,并且在调试区域中打印的最后一个值是19。
-
你甚至可以直接在
in关键字后面使用范围运算符。输入并运行以下代码:for number in 0...5 { print(number) }
从 0 到 5 的每个数字都在调试区域中显示。
-
如果你想序列反向,请使用
reversed()函数。按照以下方式修改代码并运行:for number in **(****0****...****5****).reversed()** { print(number) }
每个从 5 到 0 的数字都会显示在调试区域中。
干得好!让我们在下一节中检查 while 循环,当循环序列应该在条件为 true 时重复时使用。
当前的循环
while 循环包含一个条件和一组在花括号中的语句,称为循环体。首先检查条件;如果为 true,则执行循环体,并且循环会重复,直到条件为 false。以下是一个 while 循环的示例:
while condition == true {
code
}
添加并运行以下代码以创建一个变量,将其增加 5,并且只要变量的值小于 50 就继续这样做:
var x = 0
while x < 50 {
x += 5
print("x is \(x)")
}
让我们逐步分析代码。最初,x 被设置为 0。检查 x < 50 条件并返回 true,因此执行循环体。x 的值增加 5,并在调试区域中打印出 x 是 5。循环重复,再次检查 x < 50。由于 x 现在是 5,并且 5 < 50 仍然返回 true,因此再次执行循环体。这会一直重复,直到 x 的值为 50,此时 x < 50 返回 false,循环停止。
如果 while 循环的条件一开始就是 false,则循环体将永远不会执行。尝试将 x 的值更改为 100 来查看这一点。
在下一节中,你将学习 repeat-while 循环。这些循环首先执行循环体内的语句,然后再检查循环条件。
repeat-while 循环
与 while 循环一样,repeat-while 循环也包含一个条件和循环体,但循环体在检查条件之前先执行。如果条件为 true,则循环会重复,直到条件返回 false。以下是一个 repeat-while 循环的示例:
repeat {
code
} while condition == true
添加并运行以下代码以创建一个变量,将其增加 5,并且只要变量的值小于 50 就继续这样做:
var y = 0
repeat {
y += 5
print("y is \(y)")
} while y < 50
让我们逐步分析代码。最初,y 被设置为 0。执行循环体。y 的值增加 5,因此现在 y 包含 5,并在调试区域中打印出 y 是 5。检查 y < 50 条件,由于它返回 true,因此循环会重复。y 的值增加 5,因此现在 y 包含 10,并在调试区域中打印出 y 是 10。循环会重复,直到 y 包含 50,此时 y < 50 返回 false,循环停止。
即使初始条件为 false,循环体也会至少执行一次。尝试将 y 的值更改为 100 来查看这一点。
你现在已经知道如何创建和使用不同的循环类型。太棒了!
摘要
在本章中,你学习了闭包和半开区间运算符,这些运算符允许你指定一组数字的范围,而不是逐个指定每个单独的数字。
你还学习了三种不同的循环类型:for-in 循环、while 循环和 repeat-while 循环。for-in 循环允许你重复执行一组语句固定次数,而 while 和 repeat-while 循环允许在条件为真时重复执行一组语句。做得很好!
在下一章中,你将学习集合类型,这些类型允许你通过索引、键值对以及无结构的 数据集合来存储数据。
加入我们的 Discord 社群!
与其他用户、专家以及作者本人一起阅读这本书。提问、为其他读者提供解决方案、通过“问我任何问题”的环节与作者聊天,以及更多。扫描二维码或访问链接加入社区。
第五章:集合类型
到目前为止,你已经学到了很多!你现在可以创建一个存储数据在常量或变量中并对其执行操作的程序,并且你可以使用条件语句和循环来控制流程。但到目前为止,你主要存储的是单个值。
在本章中,你将学习存储值集合的方法。Swift 有三种集合类型:数组,它存储一个有序的值列表;字典,它存储一个无序的键值对列表;以及 集合,它存储一个无序的值列表。
到本章结束时,你将学会如何创建数组、字典和集合,以及如何对它们进行操作。
本章将涵盖以下主题:
-
探索数组
-
探索字典
-
探索集合
技术要求
本章的 Xcode 操场位于本书代码包的 Chapter05 文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Eighth-Edition
查看以下视频以查看代码的实际操作:
如果你希望从头开始,创建一个新的操场并将其命名为 CollectionTypes。你可以逐行输入并运行本章中的所有代码。
要了解更多关于数组、字典和集合的信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/collectiontypes。
你将要学习的第一个集合类型是数组,它允许你按有序列表存储信息。
探索数组
假设你想要存储以下内容:
-
便利店购买物品清单
-
每月必须做的家务
数组将非常适合这个。数组按有序列表存储值。它看起来是这样的:

图 5.1:数组表示
值必须是同一类型。你可以通过使用数组索引来访问数组中的任何值,索引从 0 开始。
如果你使用 let 关键字创建一个数组,那么在创建后其内容不能被更改。如果你想创建后更改数组的内容,请使用 var 关键字。
让我们看看如何使用数组。你将在下一节中通过给数组赋值来创建它。
创建数组
在前面的章节中,你通过声明并为其分配一个初始值来创建一个常量或变量。你可以用同样的方式创建一个数组。
想象一下你的配偶要求你从便利店购买一些物品。让我们使用数组来实现购物清单。将以下代码添加到你的操场中并运行它:
var shoppingList = ["Eggs", "Milk"]
此指令创建了一个名为 shoppingList 的数组变量。分配的值 ["Eggs", "Milk"] 是一个 数组字面量。它表示一个包含两个 String 类型元素的数组,其中 "Eggs" 在索引 0,"Milk" 在索引 1。
在这里使用 var 关键字意味着可以修改数组的元素。由于 Swift 使用类型推断,这个数组的元素类型将是 String。
假设您需要检查在商店需要购买多少个项目。在下一节中,您将学习如何确定数组中的元素数量。
检查数组中的元素数量
要找出数组中有多少个元素,请使用 count。输入并运行以下代码:
shoppingList.count
由于 shoppingList 数组包含两个元素,结果区域显示 2。
您可以使用 isEmpty 来检查数组是否为空。输入并运行以下代码:
shoppingList.isEmpty
由于 shoppingList 数组包含两个元素,结果区域显示 false。
您也可以通过使用 shoppingList.count == 0 来检查数组是否为空,但使用 shoppingList.isEmpty 提供更好的性能。
假设您的配偶打电话让您在商店时买油、鱼和鸡肉。在下一节中,您将看到如何向数组的末尾添加元素,以及在指定的数组索引处添加元素。
向数组添加新元素
您可以使用 append(_:) 方法向数组的末尾添加新元素。输入并运行以下代码:
shoppingList.append("Cooking Oil")
"Cooking Oil" 已添加到 shoppingList 数组的末尾,该数组现在包含三个元素 – "Eggs"、"Milk" 和 "Cooking Oil",并且在结果区域显示为 ["Eggs", "Milk", "Cooking Oil"]。
您还可以使用 + 运算符将数组添加到另一个数组中,以下代码示例:
shoppingList = shoppingList + ["Fish"]
您可以使用 insert(_:at:) 方法在指定索引处添加新项目。输入并运行以下代码:
shoppingList.insert("Chicken", at: 1)
这将在索引 1 处插入 "Chicken",因此现在 shoppingList 数组包含 "Eggs"、"Chicken"、"Milk"、"Cooking Oil" 和 "Fish"。注意,"Chicken" 是数组的第二个元素,因为第一个元素位于索引 0。您可以通过点击快速查看图标来看到这一点:

图 5.2:在游乐场中显示的数组内容
假设您已经购买了购物清单上的第一个项目,现在您需要知道下一个项目是什么。在下一节中,您将学习如何使用数组索引访问特定数组元素。
访问数组元素
您可以通过指定数组索引来访问特定元素。输入并运行以下代码:
shoppingList[2]
这将返回存储在索引 2 的数组元素,并且在结果区域显示 “Milk”。
假设您的配偶打电话让您买豆浆而不是牛奶。由于这个数组是使用 var 关键字声明的,您可以修改其中存储的值。您将在下一节中学习如何操作。
将新值分配给指定索引
你可以通过指定索引并为其分配新值来替换现有的数组元素。输入并运行以下代码:
shoppingList[2] = "Soy Milk"
shoppingList
这将替换存储在索引2处的值"Milk"为"Soy Milk"。现在shoppingList数组包含"Eggs"、"Chicken"、"Soy Milk"、"Cooking Oil"和"Fish",如结果区域所示。
注意,使用的索引必须是有效的。例如,你不能使用索引5,因为这里唯一的有效索引是0、1、2、3和4。这样做会导致程序崩溃。
想象一下,你的配偶打电话告诉你冰箱里有鸡肉和鱼,所以你不再需要它们。在下一节中,你将看到两种从数组中移除元素的方法。
从数组中移除元素
你可以通过使用remove(at:)从数组中移除一个元素。输入并运行以下代码:
let oldArrayValue = shoppingList.remove(at: 1)
oldArrayValue
shoppingList
这将从shoppingList数组中移除索引1处的项目"Chicken",因此现在它包含"Eggs"、"Soy Milk"、"Cooking Oil"和"Fish"。被移除的项目存储在removedValue中。你可以在结果区域中看到这一点。
你也可以选择不保留被移除的值。输入并运行以下代码:
shoppingList.remove(at: 3)
shoppingList
这将从shoppingList数组中移除索引3处的项目"Fish",因此现在它包含"Eggs"、"Soy Milk"和"Cooking Oil"。
如果你正在从数组中移除最后一个项目,你可以使用removeLast()代替,并且可以选择将移除的值也分配给一个常量或变量。
想象一下,你已经获得了列表中的每一项,并且你想再次遍历你的列表以确保。你需要依次访问每个数组元素并对每个元素执行操作。你将在下一节中看到如何做到这一点。
遍历数组
记得你在上一章中学到的for-in循环吗?你可以使用它来遍历数组中的每个元素。输入并运行以下代码:
for shoppingListItem in shoppingList {
print(shoppingListItem)
}
这将打印出数组中的每个元素到调试区域。
你也可以使用单侧范围运算符。这些是只有起始值的范围运算符,例如1...。输入并运行以下代码:
for shoppingListItem in shoppingList[1...] {
print(shoppingListItem)
}
这将在从索引1开始的数组元素打印到调试区域。
你现在知道如何使用数组创建有序列表,例如购物清单,以及如何执行数组操作,如访问、添加和移除元素。在下一节中,我们将看看如何使用字典存储无序列表的键值对。
探索字典
假设你正在编写一个联系人应用。你需要存储一个包含姓名和相应联系号码的列表。字典非常适合这个用途,因为它允许你将电话号码与联系人姓名关联起来。
字典以无序列表的形式存储键值对。它看起来是这样的:

图 5.3:字典的表示
所有键必须是同一类型且必须是唯一的。所有值必须是同一类型,但不必必须是唯一的。键和值不必是同一类型的。你使用键来获取相应的值。
如果你使用 let 关键字创建字典,一旦创建,其内容就不能更改。如果你想创建后更改内容,请使用 var 关键字。
让我们看看如何与字典一起工作。你将在下一节中通过为它分配一个值来创建一个字典。
创建字典
假设你正在创建一个 联系人 应用。对于这个应用,你将使用字典来存储你的联系人。就像数组一样,你可以通过声明并为其分配一个初始值来创建一个新的字典。将以下代码添加到你的沙盒中并运行它:
var contactList = ["Shah" : "+60123456789", "Aamir" : "+0223456789"]
此指令创建了一个名为 contactList 的字典变量。分配的值,["Shah" : "+60123456789", "Aamir" : "+0223456789"],是一个 字典字面量。它代表一个包含两个元素的字典。每个元素都是一个键值对,其中联系人名称作为键,联系人电话号码作为值。请注意,由于联系人名称是键字段,它应该是唯一的。
由于 contactList 字典是一个变量,你可以在创建后更改字典的内容。由于类型推断,键和值都是 String 类型。
假设你的应用必须显示联系人的总数。在下一节中,你将学习如何确定字典中的元素数量。
检查字典中的元素数量
要找出字典中有多少个元素,请使用 count。输入并运行以下代码:
contactList.count
由于 contactList 字典中有两个元素,结果区域中显示 2。
你可以使用 isEmpty 来检查字典是否为空。输入并运行以下代码:
contactList.isEmpty
由于 contactList 字典有两个元素,结果区域中显示 false。
也可以通过使用 contactlist.count == 0 来检查字典是否为空,但使用 contactList.isEmpty 提供了更好的性能。
假设你刚刚结束了一个会议,并想在应用中添加一个新的联系人。由于这个字典是使用 var 关键字声明的,你可以向其中添加键值对。你将在下一节中学习如何操作。
向字典中添加新元素
要向字典中添加新元素,提供一个键并为它分配一个值。输入并运行以下代码:
contactList["Meena"] = "+0229876543"
contactList
这将在 contactList 字典中添加一个新的键值对,键为 "Meena",值为 "+0229876543"。现在它包含 "Shah" : "+60126789345","Aamir" : "+0223456789" 和 "Meena" : "+0229876543"。您可以在结果区域中看到这一点。
假设你想拨打你的一个联系人的电话,并需要该联系人的电话号码。在下一节中,你将看到如何通过指定键来访问字典元素以获取所需值。
访问字典元素
你可以指定一个字典键来访问其对应值。输入并运行以下代码:
contactList["Shah"]
这将返回键 "Shah" 的值,并在结果区域显示 +60123456789。
假设你的某个联系人换了一个新电话,因此你必须更新该联系人的电话号码。你可以修改字典中存储的键值对。你将在下一节中学习如何操作。
给现有的键分配新值
你可以给现有的键分配一个新值。输入并运行以下代码:
contactList["Shah"] = "+60126789345"
contactList
这将给键 "Shah" 分配一个新值。现在 contactList 字典包含 "Shah" : "+60126789345","Aamir" : "+0223456789" 和 "Meena" : "+0229876543"。你可以在结果区域看到这一点。
假设你必须从你的应用程序中删除一个联系人。让我们看看下一节中如何从字典中移除元素。
从字典中移除元素
要从字典中移除一个元素,将 nil 分配给现有的键。输入并运行以下代码:
contactList["Aamir"] = nil
contactList
这将从 contactList 字典中移除键为 "Aamir" 的元素,现在它包含 "Shah" : "+60126789345" 和 "Meena" : "+0229876543"。你可以在结果区域看到这一点。
如果你想要保留你正在移除的值,请使用 removeValue(for:Key) 代替。输入并运行以下代码:
var oldDictValue = contactList.removeValue(forKey: "Meena")
oldDictValue
contactList
这将从 contactList 字典中移除键为 "Meena" 的元素,并将其值分配给 oldDictValue。现在 oldDictValue 包含 "+0229876543",而 contactList 字典包含 "Shah" : "+60126789345"。
你也可以选择只移除值,而无需将其分配给常量或变量,如下所示:
contactList.removeValue(forKey: "Meena")
假设你想要给每个联系人打电话,祝他们新年快乐。你必须依次访问每个字典元素并对其执行操作。你将在下一节中看到如何操作。
遍历字典
就像数组一样,你可以使用 for-in 循环遍历字典中的每个元素。输入并运行以下代码:
for (name, contactNumber) in contactList {
print("\(name) : \(contactNumber)")
}
这将打印字典中的每个元素到调试区域。由于字典是无序的,当你再次运行此代码时,你可能会得到不同的结果顺序。
现在你已经知道如何使用字典创建一个无序的键值对列表,例如联系人列表,以及如何执行字典操作。在下一节中,我们将看看如何在一个集合中存储无序的值列表。
探索集合
假设你正在编写一个 Movies 应用程序,并想存储电影类型列表。你可以使用集合来完成这个任务。
集合以无序列表的形式存储值。它看起来是这样的:

图 5.4:集合的表示
所有值都是同一类型。
如果你使用 let 关键字创建一个集合,其内容在创建后不能被更改。如果你想创建后更改内容,请使用 var 关键字。
让我们看看如何处理集合。你将在下一节中通过给它赋值来创建一个集合。
创建集合
想象一下,你正在创建一个 电影 应用,并且你希望在你的应用中存储电影类型。该应用将存储你喜欢的电影类型,并且它可以检查你想要观看的电影是否在其中。与数组相比,集合是无序的并且只包含唯一值,而数组是有序的,按索引排列,可以包含重复值。由于你不需要按顺序存储电影类型,并且每个类型都是唯一的,因此你将使用集合来完成这个目的。
正如你所看到的,对于数组和字典,你可以通过声明它并分配一个新的值来创建一个集合。将以下代码添加到你的游乐场中并运行它:
var movieGenres: Set = ["Horror", "Action", "Romantic Comedy" ]
这个指令创建了一个名为 movieGenres 的集合变量。请注意,分配给它的 集合字面量 ["Horror", "Action", "Romantic Comedy"] 与数组字面量的格式相同,因此你使用类型注解将 movieGenres 的类型设置为 Set。否则,Swift 的类型推断将创建一个数组变量而不是集合变量。
在这里使用 var 关键字意味着集合的内容可以被修改。由于类型推断,这个集合的元素类型将是 String。
想象一下,你需要显示你应用中电影类型的总数。让我们看看如何在下一节中找到集合中元素的数量。
检查集合中的元素数量
要找出集合中有多少个元素,请使用 count。输入并运行以下代码:
movieGenres.count
由于 movieGenres 集合包含三个元素,结果区域显示 3。
你可以使用 isEmpty 来检查一个集合是否为空。输入并运行以下代码:
movieGenres.isEmpty
由于 movieGenres 包含三个元素,结果区域显示 false。
你也可以通过使用 movieGenres.count == 0 来检查集合是否为空,但使用 movieGenres.isEmpty 提供更好的性能。
想象一下,你的应用的用户可以添加更多类型。由于这个集合是使用 var 关键字声明的,你可以向其中添加元素。你将在下一节中学习如何操作。
向集合中添加新元素
你可以通过使用 insert(_:) 来向集合中添加新元素。输入并运行以下代码:
movieGenres.insert("War")
movieGenres
这将一个新的项目 "War" 添加到 movieGenres 集合中,现在它包含 "Horror"、"Romantic Comedy"、"War" 和 "Action"。当你点击快速查看图标时,这一点是可见的。
想象一下,一个用户想知道你的应用中是否提供某种类型的电影。在下一节中,你将学习如何检查一个元素是否在集合中。
检查集合是否包含一个元素
要检查集合是否包含一个元素,请使用 contains(_:)。输入并运行以下代码:
movieGenres.contains("War")
由于 "War" 是 movieGenres 集合中的一个元素,结果区域显示 true。
假设一个用户想要从他们的音乐类型列表中移除一个类型。让我们在下一节中看看如何从集合中移除不再需要的项。
从集合中移除项
要从集合中移除项,请使用 remove(_:)。你正在移除的值可以被丢弃、赋值给变量或常量。如果该值不存在于集合中,则返回 nil。输入并运行以下代码:
var oldSetValue = movieGenres.remove("Action")
oldSetValue
movieGenres
"动作" 从 movieGenres 集合中移除并赋值给 oldSetValue,现在 movieGenres 集合包含 "恐怖"、"浪漫喜剧" 和 "战争"。
要从集合中移除所有元素,请使用 removeAll()。
假设你希望显示你应用中所有音乐类型作为你应用用户的推荐。你可以遍历并操作每个集合元素。让我们在下一节中看看如何做到这一点。
遍历集合
与数组和大纲类似,你可以使用 for-in 循环遍历集合中的每个元素。输入并运行以下代码:
for genre in movieGenres {
print(genre)
}
你应该在调试区域看到每个集合元素。由于集合是无序的,当你再次运行此代码时,你可能会得到不同的结果顺序。
假设你想让你的应用对你喜欢的音乐类型和另一个人喜欢的音乐类型执行操作。在下一节中,你将了解在 Swift 中可以使用集合执行的各种操作。
执行集合操作
执行集合操作,如 并集、交集、减集 和 对称差集 非常简单。输入并运行以下代码:
let movieGenres2: Set = ["Science Fiction", "War", "Fantasy"]
movieGenres.union(movieGenres2)
movieGenres.intersection(movieGenres2)
movieGenres.subtracting(movieGenres2)
movieGenres.symmetricDifference(movieGenres2)
在这里,你正在对两个集合 movieGenres 和 movieGenres2 执行集合操作。让我们看看每个集合操作的结果:
-
union(_:)返回一个新集合,包含两个集合中的所有值,因此当你点击快速查看图标时,会显示 “恐怖”,“浪漫喜剧”,“战争”,“科幻” 和 “奇幻”。 -
intersection(_:)返回一个新集合,其中只包含两个集合共有的值,因此当你点击快速查看图标时,会显示 “战争”。 -
subtracting(_:)返回一个新集合,不包含指定集合中的值,因此当你点击快速查看图标时,会显示 “恐怖” 和 “浪漫喜剧”。 -
symmetricDifference(_:)返回一个新集合,不包含两个集合共有的值,因此当你点击快速查看图标时,会显示 “恐怖”,“浪漫喜剧”,“科幻” 和 “奇幻”。
假设你想让你的应用比较你喜欢的音乐类型和另一个人喜欢的音乐类型。在下一节中,你将学习如何检查一个集合是否等于另一个集合,是否是另一个集合的子集,或者与另一个集合没有任何共同点。
理解集合成员资格和相等性
检查一个集合是否等于另一个集合的 子集、超集 或 不相交 非常简单。输入并运行以下代码:
let movieGenresSubset: Set = ["Horror", "Romantic Comedy"]
let movieGenresSuperset: Set = ["Horror", "Romantic Comedy", "War", "Science Fiction", "Fantasy"]
let movieGenresDisjoint: Set = ["Bollywood"]
movieGenres == movieGenres2
movieGenresSubset.isSubset(of: movieGenres)
movieGenresSuperset.isSuperset(of: movieGenres)
movieGenresDisjoint.isDisjoint(with: movieGenres)
让我们看看这段代码是如何工作的:
-
==操作符检查一个集合的所有成员是否与另一个集合的成员相同。由于movieGenres集合的所有成员并不都与movieGenres2集合的成员相同,结果区域将显示 false。 -
isSubset(of:)检查一个集合是否是另一个集合的子集。由于movieGenresSubset集合的所有成员都在movieGenres集合中,结果区域将显示 true。 -
isSuperset(of:)检查一个集合是否是另一个集合的超集。由于movieGenres集合的所有成员都在movieGenresSuperset集合中,结果区域将显示 true。 -
isDisjoint(with:)检查一个集合是否与另一个集合没有共同的值。由于movieGenresDisjoint集合与movieGenres集合没有共同的成员,结果区域将显示 true。
你现在知道如何使用集合来创建一个无序的值列表,例如电影类型的列表,以及如何执行集合操作。这标志着关于集合类型的章节结束。做得好!
摘要
在本章中,你学习了 Swift 中的集合类型。首先,你学习了有关数组的内容。这些允许你使用有序的值列表来表示如购物清单这样的项目,并对它执行操作。
接下来,你学习了有关字典的内容。这些允许你使用无序的键值对列表来表示如联系人列表这样的项目,并对它执行操作。
最后,你学习了有关集合的知识。这些允许你使用无序的值列表来表示如电影类型列表这样的项目,并对它执行操作。你还学习了为什么在这种情况下使用集合而不是数组可能更合适。
在下一章中,你将学习如何使用函数将一组指令组合在一起。当你想在程序中多次执行一组指令时,这非常有用。
加入我们的 Discord 社区!
与其他用户、专家以及作者本人一起阅读这本书。提问、为其他读者提供解决方案、通过“问我任何问题”的环节与作者聊天,以及更多。扫描二维码或访问链接加入社区。
第六章:函数和闭包
到目前为止,你可以编写相对复杂的程序,这些程序可以做出决策并重复指令序列。你还可以使用集合类型为你的程序存储数据。随着你编写的程序在大小和复杂性上增长,理解它们所做的工作将变得更加困难。
为了使大型程序更容易理解,Swift 允许你创建函数,这让你可以通过调用单个名称来组合多个指令并执行它们。你还可以创建闭包,这让你可以组合多个指令而不命名,并将它们分配给常量或变量。
到本章结束时,你将了解函数、嵌套函数、作为返回类型的函数、作为参数的函数以及guard语句。你还将了解如何创建和使用闭包。
本章将涵盖以下主题:
-
探索函数
-
探索闭包
技术要求
本章的 Xcode 游乐场位于本书代码包的Chapter06文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,看看代码的实际效果:
如果你希望从头开始,创建一个新的游乐场,并将其命名为FunctionsAndClosures。
你可以在进行过程中输入并运行本章中的所有代码。让我们先了解函数。
探索函数
函数对于封装执行特定任务的多个指令非常有用,如下所示:
-
计算餐厅餐费的 10%服务费
-
计算你希望购买的汽车的月供
这就是函数的样子:
func functionName(parameter1: ParameterType, ...) -> ReturnType {
code
}
每个函数都有一个描述性的名称。你可以定义一个或多个作为输入值的函数,这些值被称为参数。你还可以定义函数完成后的输出,这被称为其返回类型。参数和返回类型都是可选的。
你“调用”函数的名称来执行它。这就是函数调用的样子:
functionName(parameter1: argument1, …)
你提供与函数参数类型匹配的输入值(称为参数)。
要了解更多关于函数的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/functions/。
在下一节中,我们将看看如何创建一个函数来计算服务费。
创建函数
在其最简单的形式中,函数只是执行一些指令,没有任何参数或返回类型。你将通过编写一个计算餐费服务费的函数来了解这是如何工作的。服务费应该是餐费的 10%。
将以下代码添加到你的游乐场中,创建并调用此函数并运行它:
func serviceCharge() {
let mealCost = 50
let serviceCharge = mealCost / 10
print("Service charge is \(serviceCharge)")
}
serviceCharge()
你刚刚创建了一个名为serviceCharge()的非常简单的函数。它所做的只是计算价值 50 美元的餐费的 10%服务费,即50 / 10,返回5。然后你使用其名称调用此函数。你将在调试区域看到服务费是 5的显示。
这个函数不是很实用,因为每次调用这个函数时mealCost总是50。此外,结果只打印在调试区域,不能在程序的其他地方使用。让我们给这个函数添加一些参数和一个返回类型,使其更有用。
按照以下所示修改你的代码:
func serviceCharge(**mealCost****:** **Int****) ->** **Int**{
**return** **mealCost** **/****10**
}
**let****serviceChargeAmount****=****serviceCharge****(****mealCost****:** **50****)**
**print(****serviceChargeAmount****)**
这样就更好了。现在,你可以在调用serviceCharge(mealCost:)函数时设置餐费,并将结果分配给一个变量或常量。尽管这样看起来有点别扭。你应该尝试使 Swift 中的函数签名读起来像英语句子,因为这被认为是一种最佳实践。让我们看看如何在下一节中做到这一点,你将使用自定义标签使你的函数更符合英语风格,更容易理解。
使用自定义参数标签
注意,serviceCharge(mealCost:)函数不太符合英语风格。你可以给参数添加一个自定义标签,使函数更容易理解。
按照以下所示修改你的代码:
func serviceCharge(**forMealPrice** mealCost: Int) -> Int {
mealCost / 10
}
let serviceChargeAmount = serviceCharge(**forMealPrice**: 50)
print(serviceChargeAmount)
函数的工作方式与之前相同,但调用它时,你使用serviceCharge(forMealPrice:)。这听起来更像是英语,更容易弄清楚函数的作用。此外,注意如果你的函数体只包含一个语句,则return关键字是可选的。
在下一节中,你将学习如何在其他函数体内使用几个较小的函数,这些被称为嵌套函数。
使用嵌套函数
在另一个函数体内有一个函数,这些被称为嵌套函数。这允许你将多个相关函数放在一起,使封装函数更容易理解。
嵌套函数可以使用封装函数的变量。让我们通过编写一个计算贷款月供的函数来了解嵌套函数是如何工作的。
输入并运行以下代码:
func calculateMonthlyPayments(carPrice: Double, downPayment: Double, interestRate: Double, paymentTerm: Double) -> Double {
func loanAmount() -> Double {
carPrice - downPayment
}
func totalInterest() -> Double {
interestRate * paymentTerm
}
func numberOfMonths() -> Double {
paymentTerm * 12
}
return ((loanAmount() + (loanAmount() *
totalInterest() / 100 )) / numberOfMonths())
}
calculateMonthlyPayments(carPrice: 50000, downPayment: 5000, interestRate: 3.5, paymentTerm: 7.0)
这里,在calculateMonthlyPayments(carPrice:downPayment:interestRate:paymentTerm:)中有三个函数。让我们来看看它们:
-
第一个嵌套函数
loanAmount()通过从carPrice中减去downPayment来计算总贷款金额。它返回50000 - 5000=45000。 -
第二个嵌套函数
totalInterest()通过将interestRate乘以paymentTerm来计算支付期限产生的总利息金额。它返回3.5 * 7=24.5。 -
第三个嵌套函数
numberOfMonths()通过将paymentTerm乘以12来计算支付期限的总月份数。它返回7 * 12=84。
注意,三个嵌套函数都使用了封装函数的变量。返回的值是 ( 45000 + ( 45000 * 24.5 / 100 ) ) / 84 = 666.96,这是您为了购买这辆车每月必须支付的金额。
如您所见,Swift 中的函数与其他语言中的函数类似,但有一个酷炫的特性。在 Swift 中,函数是一等类型,因此它们可以用作参数和返回类型。让我们在下一节中看看这是如何实现的。
使用函数作为返回类型
一个函数可以作为其返回类型返回另一个函数。输入并运行以下代码以创建一个函数,该函数使用两种可能的方法之一生成π的值:
func approximateValueOfPi1() -> Double {
3.14159
}
func approximateValueOfPi2() -> Double {
22.0 / 7.0
}
func pi() -> (() -> Double) {
approximateValueOfPi1
// approximateValueOfPi2
}
pi()()
approximateValueOfPi1() 和 approximateValueOfPi2() 都是没有任何参数并返回π近似值的函数。pi() 函数的返回类型是一个没有参数并返回 Double 类型的函数。这意味着它可以返回 approximateValueOfPi1(如所示)或 approximateValueOfPi2,因为这两个函数都符合预期的返回类型。
pi()() 调用了返回 3.14159 的函数 approximateValueOfPi1。3.14159 在结果区域中显示。
让我们看看在下一节中如何将一个函数用作另一个函数的参数。
使用函数作为参数
一个函数可以将另一个函数作为参数。输入并运行以下代码以创建一个函数,该函数用于判断一个满足特定条件的数字是否存在于数字列表中:
func isThereAMatch(listOfNumbers: [Int], condition: (Int) -> Bool) -> Bool {
for number in listOfNumbers {
if condition(number) {
return true
}
}
return false
}
func numberIsOdd(number: Int) -> Bool {
(number % 2) > 0
}
func numberIsEven(number: Int) -> Bool {
(number % 2) == 0
}
let numbersList = [1, 3, 5, 7]
isThereAMatch(listOfNumbers: numbersList, condition: numberIsOdd)
isThereAMatch(listOfNumbers:condition:) 有两个参数:一个整数数组和函数。提供的作为参数的函数必须接受一个整数值并返回一个布尔值。
numberIsOdd(number:) 和 numberIsEven(_:) 都接受一个整数并返回一个布尔值,这意味着任一函数都可以作为第二个参数的参数。包含奇数的数组 numbersList 用作第一个参数的参数。当 numberIsOdd 作为第二个参数的参数时,isThereAMatch(listOfNumbers:condition:) 被调用时会返回 true。尝试使用 numberisEven 作为第二个参数的参数。
函数作为参数和返回类型可能难以理解,但在您学习旅程的这个阶段相对较少,所以如果您一开始不理解,请不要担心。随着您经验的积累,这会变得更容易理解。
在下一节中,您将了解如果使用的参数不合适,如何从函数中提前退出。
使用守卫语句提前退出函数
假设您需要一个函数用于在线购物终端。这个函数将在您购买东西时计算借记卡或信用卡的剩余余额。您想要购买的物品价格输入在一个文本字段中。
文本框中的值被转换为整数,这样你就可以计算剩余的卡余额。如果输入数据有问题,能够提前退出函数是非常有用的。
输入并运行以下代码:
func buySomething(itemValueEntered itemValueField: String, cardBalance: Int) -> Int {
guard let itemValue = Int(itemValueField) else {
print("Error in item value")
return cardBalance
}
let remainingBalance = cardBalance - itemValue
return remainingBalance
}
print(buySomething(itemValueEntered: "10", cardBalance: 50))
print(buySomething(itemValueEntered: "blue", cardBalance: 50))
你应该在调试区域看到这个结果:
40
Error in item value
50
让我们看看这个函数是如何工作的。函数体内的第一行是一个guard语句。这个语句检查一个条件是否为true;如果不是,它将退出函数。在这里,它被用来检查用户是否在在线购买终端输入了有效的价格。
如果是这样,该值可以成功转换为整数,你可以计算剩余的卡余额。否则,guard语句中的else子句将被执行。错误信息将被打印到调试区域,并且返回未更改的卡余额。
对于print(buySomething(itemValueEntered: "10", cardBalance: 50)),项目价格成功从卡余额中扣除,并返回40。
对于print(buySomething(itemValueEntered: "blue", cardBalance: 50)),guard语句的条件失败,其else子句被执行,导致错误信息被打印到调试区域,并返回50。
现在,你已经知道了如何创建和使用函数。你还看到了如何使用自定义参数标签、嵌套函数、函数作为参数或返回类型,以及guard语句。
现在,让我们看看闭包。与函数一样,闭包允许你组合多个指令,但闭包没有名字,并且可以被分配给一个常量或变量。你将在下一节中看到它们是如何工作的。
探索闭包
闭包,就像函数一样,包含一系列指令,可以接受参数并返回值。然而,闭包没有名字。闭包中的指令序列被大括号({ })包围,in关键字将参数和返回类型与闭包体分开。
闭包可以被分配给一个常量或变量,所以当你需要在程序内部传递它们时,它们非常方便。例如,假设你有一个从互联网下载文件的应用程序,并且你需要在文件下载完成后对文件进行一些操作。你可以在闭包内部放置一个处理文件的指令列表,并在文件下载完成后让程序执行它。
要了解更多关于闭包的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/。
你现在将编写一个闭包,该闭包将对数字数组中的每个元素应用计算。将以下代码添加到你的游乐场中,然后点击运行按钮来运行它:
var numbersArray = [2, 4, 6, 7]
let myClosure = { (number: Int) -> Int in
let result = number * number
return result
}
let mappedNumbers = numbersArray.map(myClosure)
这将一个计算数字的平方的闭包赋值给 myClosure。然后 map() 函数将此闭包应用于 numbersArray 中的每个元素。每个元素都乘以自身,并在结果区域出现 [4, 16, 36, 49]。
您可以以更简洁的方式编写闭包,您将在下一节中看到如何做到这一点。
简化闭包
新开发者遇到困难的一件事是经验丰富的 Swift 程序员编写闭包时使用的非常简洁的方法。考虑以下示例中的代码:
var testNumbers = [2, 4, 6, 7]
let mappedTestNumbers = testNumbers.map({
(number: Int) -> Int in
let result = number * number
return result
})
print(mappedTestNumbers)
在这里,您有 testNumbers,一个数字数组,您使用 map(_:) 函数依次将闭包映射到数组的每个元素。闭包中的代码将数字乘以自身,生成该数字的平方。结果 [4, 16, 36, 49] 然后打印到调试区域。您将看到,闭包代码可以写得更加简洁。
当闭包的类型已知时,您可以删除参数类型、返回类型或两者。单行闭包隐式返回其唯一语句的值,这意味着您可以删除 return 语句。因此,您可以按如下方式编写闭包:
let mappedTestNumbers = testNumbers.map({ number in
number * number
})
当闭包是函数的唯一参数时,您可以省略包围闭包的括号,如下所示:
let mappedTestNumbers = testNumbers.map { number in
number * number
}
您可以使用表示它们在参数列表中相对位置的数字来引用参数,而不是使用名称,如下所示:
let mappedTestNumbers = testNumbers.map { $0 * $0 }
因此,闭包现在确实非常简洁,但对于新开发者来说可能难以理解。请随意以您舒适的方式编写闭包。
您现在已经知道如何创建和使用闭包,以及如何更简洁地编写它们。太棒了!
概述
在本章中,您学习了如何将语句分组到函数中。您学习了如何使用自定义参数标签、函数内的函数、函数作为返回类型、函数作为参数以及 guard 语句。当您需要在程序的不同点完成相同任务时,这将很有用。
您还学习了如何创建闭包。当您需要在程序中传递代码块时,这将很有用。
在下一章中,我们将研究类、结构和枚举。类和结构允许创建可以存储状态和行为复杂对象,枚举可以用来限制可以分配给变量或常量的值,从而减少出错的可能性。
加入我们的 Discord!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第七章:类、结构和枚举
在上一章中,你学习了如何使用函数和闭包来分组指令序列。
是时候考虑如何在代码中表示复杂对象了。例如,考虑一辆车。你可以使用一个String常量来存储车名,以及一个Double变量来存储车价,但它们之间并没有关联。你已经看到你可以通过分组指令来创建函数和闭包。在本章中,你将学习如何使用类和结构将常量和变量组合成一个单一实体,以及如何操作它们。你还将学习如何使用枚举来分组一组相关值。
到本章结束时,你将学会如何创建和初始化一个类,从现有类创建子类,创建和初始化一个结构,区分类和结构,以及创建枚举。
本章将涵盖以下主题:
-
理解类
-
理解结构
-
理解枚举
技术要求
本章的 Xcode 游乐场位于本书代码包的Chapter07文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Eighth-Edition
查看以下视频,看看代码的实际效果:
如果你希望从头开始,创建一个新的游乐场,并将其命名为ClassesStructuresAndEnumerations。你可以一边阅读一边输入并运行本章中的所有代码。让我们从学习什么是类以及如何声明和定义它开始。
理解类
类对于表示复杂对象非常有用,例如:
-
公司的个别员工信息
-
在电子商务网站上出售的物品
-
你家中用于保险目的的物品
下面是一个类声明和定义的例子:
class ClassName {
property1
property2
property3
method1() {
code
}
method2() {
code
}
}
每个类都有一个描述性的名称,它包含用于表示对象的变量或常量。与类关联的变量或常量称为属性。
类还可以包含执行特定任务的函数。与类关联的函数称为方法。
一旦你声明并定义了一个类,你就可以创建该类的实例。想象一下,你正在为动物园创建一个应用程序。如果你有一个Animal类,你可以使用该类的实例来表示动物园中的不同动物。这些实例的每个属性都将有不同的值。
要了解更多关于类的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures。
让我们看看如何使用类。你将学习如何声明和定义类,根据类声明创建实例,并操作这些实例。你将从下一节开始创建一个表示动物的类。
创建类声明
让我们声明并定义一个可以存储关于动物详细信息的类。将以下代码添加到你的游乐场中:
class Animal {
var name: String = ""
var sound: String = ""
var numberOfLegs: Int = 0
var breathesOxygen: Bool = true
func makeSound() {
print(sound)
}
}
你刚刚声明了一个非常简单的名为 Animal 的类。惯例规定,类名应以大写字母开头。这个类有属性来存储动物的名字、它发出的声音、它有多少条腿以及它是否呼吸氧气。这个类还有一个名为 makeSound() 的方法,它会将产生的噪音打印到调试区域。
现在你有了 Animal 类,让我们在下一节中使用它来创建一个表示动物的实例。
创建类的实例
一旦你声明并定义了一个类,你就可以创建该类的实例。现在,你将创建一个表示猫的 Animal 类的实例。按照以下步骤操作:
-
要创建
Animal类的实例,列出所有属性并调用它的makeSound()方法;在你的类声明之后输入以下代码并运行:let cat = Animal() print(cat.name) print(cat.sound) print(cat.numberOfLegs) print(cat.breathesOxygen) cat.makeSound()
你可以通过在实例名称后键入一个点,然后跟随着你想要的属性或方法来访问实例属性和方法。你将在调试区域看到实例属性和方法的值。由于这些值是在创建类时分配的默认值,因此 name 和 sound 包含空字符串,numberOfLegs 包含 0,breathesOxygen 包含 true,而 makeSound() 方法打印一个空字符串。
-
让我们给这个实例的属性分配一些值。按照以下所示修改你的代码:
let cat = Animal() **cat****.****name****=****"Cat"** **cat****.****sound****=****"Mew"** **cat****.****numberOfLegs****=****4** **cat****.****breathesOxygen****=****true** print(cat.name)
现在,当你运行程序时,以下内容将在调试区域显示:
Cat
Mew
4
true
Mew
所有实例属性值和 makeSound() 方法的输出都将打印到调试区域。
注意,在这里,你首先创建实例,然后分配值给该实例。也可以在创建实例时分配值,这可以通过在类声明中实现一个 初始化器 来完成。
-
初始化器负责确保在创建类时所有实例属性都有有效的值。让我们为
Animal类添加一个初始化器。按照以下所示修改你的类定义:class Animal { var name: String var sound: String var numberOfLegs: Int var breathesOxygen: Bool **init****(****name****:** **String****,** **sound****:** **String****,** **numberOfLegs****:** **Int****,** **breathesOxygen****:** **Bool****) {** **self****.****name****=** **name** **self****.****sound****=** **sound** **self****.****numberOfLegs****=** **numberOfLegs** **self****.****breathesOxygen****=** **breathesOxygen** **}** func makeSound() { print(sound) } }
如你所见,初始化器使用了 init 关键字,并有一个参数列表,这些参数将用于设置属性值。请注意,self 关键字区分了属性名和参数。例如,self.name 指的是属性,而 name 指的是参数。
在初始化过程结束时,类中的每个属性都应该有一个有效的值。
-
在这个阶段,你会在代码中看到一些错误,因为函数调用没有参数。你需要更新你的函数调用以解决这个问题。按照以下所示修改你的代码并运行:
func makeSound() { print(sound) } } let cat = Animal(**name****:** **"Cat"****,** **sound****:** **"Mew"****,** **numberOfLegs****:** **4****,** **breathesOxygen****:** **true**) print(cat.name)
结果与步骤 2中的相同,但你在一个指令中创建了实例并设置了其属性。太棒了!
现在有不同类型的动物,如哺乳动物、鸟类、爬行动物和鱼类。你可以为每种类型创建一个类,但你也可以基于现有类创建一个子类。让我们在下一节中看看如何做到这一点。
创建子类
一个类的子类继承了一个现有类的所有方法和属性。如果你愿意,你还可以向其中添加额外的属性和方法。例如,对于一家 IT 公司,你可以有CustomerSupportAgent作为Employee类的子类。这个类将具有Employee类的所有属性,以及客户支持角色所需的额外属性。
现在,你将创建Mammal,它是Animal类的子类。按照以下步骤进行:
-
要声明
Mammal类,在Animal类声明之后键入以下代码:class Mammal: Animal { let hasFurOrHair: Bool = true }
在类名后面键入: Animal会使Mammal类成为Animal类的子类。它具有在Animal类中声明的所有属性和方法,以及一个额外的属性:hasFurOrHair。由于Animal类是Mammal类的父类,你可以将其称为Mammal类的超类。
-
按照以下所示修改创建你的类实例的代码,然后运行它:
let cat = **Mammal**(name: "Cat", sound: "Mew", numberOfLegs: 4, breathesOxygen: true)
cat现在是一个Mammal类的实例,而不是Animal类的实例。正如你所看到的,调试区域显示的结果与之前相同,没有错误。不过,hasFurOrHair的值尚未显示。让我们修复这个问题。
-
在你的游乐场中的所有其他代码之后键入以下代码以显示
hasFurOrHair属性的值并运行它:print(cat.hasFurOrHair)
由于Animal类的初始化器没有参数来分配hasFurOrHair的值,将使用默认值,调试区域将显示true。
你已经看到子类可以具有额外的属性。子类还可以具有额外的属性和方法,子类中的方法实现可以与超类实现不同。让我们在下一节中看看如何做到这一点。
覆盖超类方法
到目前为止,你一直在使用多个print()语句来显示类实例的值。你将实现一个description()方法来在调试区域显示所有实例属性,因此不再需要多个print()语句。按照以下步骤进行:
-
按照以下所示修改你的
Animal类声明以实现description()方法:class Animal { var name: String var sound: String var numberOfLegs: Int var breathesOxygen: Bool = true init(name: String, sound: String, numberOfLegs: Int, breathesOxygen: Bool) { self.name = name self.sound = sound self.numberOfLegs = numberOfLegs self.breathesOxygen = breathesOxygen } func makeSound() { print(sound) } **func****description****() ->** **String** **{** **"****name:** **\(****name****)****sound:** **\(****sound****)** **numberOfLegs:** **\(****numberOfLegs****)** **breathesOxygen:** **\(****breathesOxygen****)****"** **}** } -
按照以下所示修改你的代码,用
description()方法代替多个print()语句,然后运行程序:let cat = Mammal(name: "Cat", sound: "Mew", numberOfLegs: 4, breathesOxygen: true) **print(****cat****.****description****())** cat.makeSound()
你将在调试区域看到以下内容:
name: Cat sound: Mew numberOfLegs: 4 breathesOxygen: true
Mew
如您所见,尽管description()方法在Mammal类中没有实现,但它却在Animal类中实现了。这意味着它将被Mammal类继承,并且实例属性将被打印到调试区域。请注意,hasFurOrHair属性的值缺失,并且您不能将其放入description()方法中,因为Animal类中没有hasFurOrHair属性。
-
您可以更改
Mammal类中description()方法的实现,以显示hasFurOrHair属性的值。将以下代码添加到您的Mammal类定义中并运行它:class Mammal: Animal { let hasFurOrHair: Bool = true **override****func****description****() ->** **String** **{** **super****.****description****()** **+****" hasFurOrHair:** **\(****hasFurOrHair****)"** **}** }
override关键字在这里用于指定实现的description()方法将替换超类实现。super关键字用于调用超类的description()实现。然后,hasFurOrHair的值被添加到super.description()返回的字符串中。
您将在调试区域看到以下内容:
name: Cat sound: Mew numberOfLegs: 4 breathesOxygen: true hasFurOrHair: true
Mew
hasFurOrHair属性的值在调试区域中显示,表明您正在使用Mammal子类的description()方法实现。
您已经创建了类和子类声明,并为两者创建了实例。您还为两者添加了初始化器和方法。太酷了!让我们在下一节中看看如何声明和使用结构体。
理解结构体
与类一样,结构体也组合了用于表示对象和执行特定任务的属性和方法。还记得您创建的Animal类吗?您也可以使用结构体来完成相同的事情。不过,类和结构体之间还是有区别的,您将在本章后面了解更多。
下面是结构体声明和定义的示例:
struct StructName {
property1
property2
property3
method1() {
code
}
method2(){
code
}
}
如您所见,结构体与类非常相似。它也有一个描述性的名称,可以包含属性和方法。您也可以创建结构体的实例。
想要了解更多关于结构体的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures。
让我们看看如何与结构体一起工作。您将学习如何声明和定义结构体,基于结构体创建实例,并对其进行操作。您将在下一节中创建一个结构体来表示爬行动物。
创建结构体声明
继续动物主题,让我们声明并定义一个可以存储关于爬行动物详细信息的结构体。在您的游乐场中所有其他代码之后添加以下代码:
struct Reptile {
var name: String
var sound: String
var numberOfLegs: Int
var breathesOxygen: Bool
let hasFurOrHair: Bool = false
func makeSound() {
print(sound)
}
func description() -> String {
"Structure: Reptile name: \(name)
sound: \(sound)
numberOfLegs: \(numberOfLegs)
breathesOxygen: \(breathesOxygen)
hasFurOrHair: \(hasFurOrHair)"
}
}
如你所见,这几乎与你之前所做的Animal类声明相同。结构体的名称也应该以大写字母开头,并且这个结构体有属性来存储动物的名字、它发出的声音、它有多少条腿、它是否呼吸氧气以及它是否有毛皮或毛发。这个结构体还有一个makeSound()方法,它会将发出的声音打印到调试区域。
现在你有了Reptile结构体的声明,让我们在下一节中使用它来创建一个表示蛇的实例。
创建结构体的实例
与类一样,你可以从结构体声明中创建实例。现在,你将创建一个表示蛇的Reptile结构体的实例,打印出该实例的属性值,并调用makeSound()方法。在你的游乐场中的所有其他代码之后输入以下内容并运行它:
var snake = Reptile(name: "Snake", sound: "Hiss",
numberOfLegs: 0, breathesOxygen: true)
print(snake.description())
snake.makeSound()
注意,你不需要实现一个初始化器;结构体自动为其所有属性获得一个名为成员初始化器的初始化器。真方便!以下内容将在调试区域显示:
Structure: Reptile name: Snake sound: Hiss numberOfLegs: 0 breathesOxygen: true hasFurOrHair: false
Hiss
尽管结构体声明与类声明非常相似,但类和结构体之间有两个区别:
-
结构体不能从另一个结构体继承
-
类是引用类型,而结构体是值类型
让我们看看下一节中值类型和引用类型之间的区别。
比较值类型和引用类型
类是引用类型。这意味着当你将类实例赋值给变量时,你是在变量中存储原始实例的内存位置,而不是实例本身。
结构体是值类型。这意味着当你将结构体实例赋值给变量时,该实例被复制,你对原始实例所做的任何更改都不会影响副本。
现在,你将创建一个类的实例和一个结构体的实例,并观察它们之间的差异。按照以下步骤操作:
-
你将首先创建一个包含结构体实例的变量,并将其赋值给第二个变量,然后更改第二个变量中属性的值。输入以下代码并运行它:
struct SampleValueType { var sampleProperty = 10 } var a = SampleValueType() var b = a b.sampleProperty = 20 print(a.sampleProperty) print(b.sampleProperty)
在这个例子中,你声明了一个包含一个属性sampleProperty的结构体SampleValueType。然后,你创建了该结构体的一个实例并将其赋值给变量a。之后,你将a赋值给一个新的变量b。然后,你将b的sampleProperty值更改为20。
当你打印出a的sampleProperty值时,调试区域将显示10,这表明对b的sampleProperty值的任何更改都不会影响a的sampleProperty值。这是因为当你将a赋值给b时,a的一个副本被赋值给b,因此它们是独立的实例,不会相互影响。
-
接下来,你将创建一个包含类实例的变量,并将其分配给第二个变量,然后更改第二个变量的属性值。输入以下代码并运行它:
class SampleReferenceType { var sampleProperty = 10 } var c = SampleReferenceType() var d = c c.sampleProperty = 20 print(c.sampleProperty) print(d.sampleProperty)
在这个例子中,你声明了一个包含一个属性 sampleProperty 的类 SampleReferenceType。然后,你创建了该类的实例并将其分配给一个变量 c。之后,你将 c 分配给一个新的变量 d。接下来,你将 d 的 sampleProperty 值更改为 20。
当你打印出 c 的 sampleProperty 值时,在调试区域中会打印出 20,这表明对 c 或 d 的任何更改都会影响相同的 SampleReferenceType 实例。
现在,问题是,你应该使用类还是结构体?让我们在下一节中探讨这个问题。
在类和结构体之间做出选择
你已经看到你可以使用类或结构体来表示复杂对象。那么,你应该使用哪一个呢?
除非你需要类才能实现某些功能,例如子类,否则建议使用结构体。这有助于防止由于类是引用类型而可能发生的某些微妙错误。
太棒了!现在你已经了解了类和结构体,让我们看看枚举,它允许你将相关值分组,在下一节中。
理解枚举
枚举允许你将相关值分组,例如以下内容:
-
指南针方向
-
交通信号灯颜色
-
彩虹的颜色
为了理解为什么枚举对于这个目的来说非常理想,让我们考虑以下示例。
想象你正在编写交通信号灯的代码。你可以使用一个整型变量来表示不同的交通信号灯颜色,其中 0 代表红色,1 代表黄色,2 代表绿色,如下所示:
var trafficLightColor = 2
虽然这是一种表示交通信号灯的方法,但当将 3 分配给 trafficLightColor 时会发生什么?这是一个问题,因为 3 并不代表有效的交通信号灯颜色。因此,如果我们能将 trafficLightColor 的可能值限制为它可以显示的颜色,那就更好了。
下面是一个枚举声明和定义的例子:
enum EnumName {
case value1
case value2
case value3
}
每个枚举都有一个描述性名称,其主体包含该枚举的关联值。
要了解更多关于枚举的信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations。
让我们看看如何使用枚举。你将学习如何创建和操作它们。你将从下一节创建一个表示交通信号灯颜色的枚举开始。
创建一个枚举
让我们创建一个枚举来表示交通信号灯。按照以下步骤操作:
-
将以下代码添加到您的游乐场中并运行它:
enum TrafficLightColor { case red case yellow case green } var trafficLightColor = TrafficLightColor.red
这创建了一个名为 TrafficLightColor 的枚举,它将红色、黄色和绿色值分组在一起。trafficLightColor 变量的值限制为 red、yellow 和 green;设置任何其他值将生成错误。
-
就像类和结构体一样,枚举也可以包含方法。让我们给
TrafficLightColor添加一个方法。按照以下所示修改你的代码,使TrafficLightColor返回一个表示交通灯颜色的字符串,并运行它:enum TrafficLightColor { case red case yellow case green **func****description****() ->** **String** **{** **switch****self** **{** **case****.red****:** **"red"** **case****.yellow****:** **"yellow"** **case .green:** **"green"** **}** **}** } var trafficLightColor = TrafficLightColor.red **print(****trafficLightColor****.****description****())**
description() 方法返回一个字符串,取决于 trafficLightColor 的值。由于 trafficLightColor 的值为 TrafficLightColor.red,红色将出现在调试区域。
你已经学会了如何创建和使用枚举来存储分组值,以及如何向它们添加方法。做得好!
摘要
在本章中,你学习了如何使用类声明复杂对象,如何创建类的实例,如何创建子类,以及如何重写类方法。你还学习了如何声明结构体,创建结构体的实例,以及理解引用类型和值类型之间的区别。最后,你学习了如何使用枚举来表示一组特定的值。
现在,你知道了如何使用类和结构体来表示复杂对象,以及如何在你的程序中使用枚举将相关值分组在一起。
在下一章中,你将学习如何使用协议在类和结构体中指定常见特性,如何使用扩展扩展内置类的功能,以及如何在程序中处理错误。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读这本书。提问,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第八章:协议、扩展和错误处理
在上一章中,你学习了如何使用类或结构体来表示复杂对象,以及如何使用枚举将相关值分组在一起。
在本章中,你将了解协议、扩展和错误处理。协议定义了一个方法、属性和其他要求的蓝图,这些可以由类、结构体或枚举采用。扩展使你能够为现有的类、结构体或枚举提供新功能。错误处理涵盖了如何响应和从程序中的错误中恢复。
到本章结束时,你将能够编写自己的协议以满足你应用程序的需求,使用扩展为现有类型添加新功能,并在你的应用程序中处理错误条件而不会崩溃。
本章将涵盖以下主题:
-
探索协议
-
探索扩展
-
探索错误处理
技术要求
本章的 Xcode 游乐场位于本书代码包的Chapter08文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,看看代码的实际应用:
如果你希望从头开始,创建一个新的游乐场,并将其命名为ProtocolsExtensionsAndErrorHandling。你可以一边输入一边运行本章中的所有代码。让我们从协议开始,这是一种指定类、结构体或枚举应该具有的属性和方法的方式。
探索协议
协议就像蓝图,决定了对象必须拥有的属性或方法。在你声明了一个协议之后,类、结构体和枚举可以采用它,并为所需的属性和方法提供自己的实现。
这就是协议声明的样子:
protocol ProtocolName {
var readWriteProperty1 {get set}
var readOnlyProperty2 {get}
func methodName1()
func methodName2()
}
就像类和结构体一样,协议名称以大写字母开头。属性使用var关键字声明。如果你想有一个可读可写的属性,你使用{get set},如果你想有一个只读属性,你使用{get}。请注意,你只需指定属性和方法名称;实现是在采用类、结构体或枚举内部完成的。
更多关于协议的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols。
为了帮助你理解协议,想象一个快餐店使用的应用程序。管理层已经决定显示正在提供的餐点的卡路里含量。该应用程序目前有以下类、结构体和枚举,它们都没有实现卡路里含量:
-
一个
Burger类 -
一个
Fries结构体 -
一个
Sauce枚举
将以下代码添加到您的游乐场中,以声明Burger类、Fries结构和Sauce枚举:
class Burger {
}
struct Fries {
}
enum Sauce {
case chili
case tomato
}
这些代表应用中现有的类、结构和枚举。不用担心空定义,因为它们对于本课程不是必需的。如您所见,它们目前都没有卡路里计数。让我们学习如何创建一个指定实现卡路里计数所需属性和方法协议。您将在下一节中声明此协议。
创建协议声明
让我们创建一个协议,该协议指定一个必需的属性calories和一个方法description()。在类、结构和枚举声明之前,将以下内容输入到您的游乐场中:
protocol CalorieCountable {
var calories: Int { get }
func description() -> String
}
此协议命名为CalorieCountable。它指定了采用它的任何对象必须有一个属性calories,该属性包含卡路里计数,以及一个返回字符串的方法description()。{ get }表示您只需能够读取存储在calories中的值,而无需写入它。请注意,description()方法的定义没有指定,因为这将在类、结构或枚举中完成。您要采用协议只需在类名后键入一个冒号,然后是协议名,并实现所需的属性和方法。
要使Burger类符合此协议,请按以下方式修改您的代码:
class Burger**:** **CalorieCountable** {
**let****calories****=****800**
**func****description****() ->** **String** **{**
**"This burger has** **\(****calories****)** **calories"**
**}**
}
如您所见,calories属性和description()方法已添加到Burger类中。尽管协议指定了一个变量,但您在这里可以使用一个常量,因为协议只要求您获取calories的值,而不需要设置它。
让我们让Fries结构也采用此协议。按以下方式修改您的Fries结构代码:
struct Fries**:** **CalorieCountable**{
**let****calories****=****500**
**func****description****() ->** **String** **{**
**"****These fries have** **\(****calories****)** **calories"**
**}**
}
添加到Fries结构的代码与添加到Burger类的代码类似,并且它现在也符合CalorieCountable协议。
您也可以以相同的方式修改Sauce枚举,但让我们使用扩展来实现。扩展可以扩展现有类、结构或枚举的功能。您将在下一节中使用扩展将CalorieCountable协议添加到Sauce枚举中。
探索扩展
扩展允许您在不修改原始对象定义的情况下向对象提供额外的功能。您可以在 Apple 提供的对象上使用它们(您无法访问对象定义的地方)或当您希望将代码分离以提高可读性和易于维护时。以下是一个扩展的示例:
class ExistingType {
property1
method1()
}
extension ExistingType : ProtocolName {
property2
method2()
}
在这里,扩展被用来向现有类提供额外的属性和方法。
更多关于扩展的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/extensions。
让我们看看如何使用扩展。您将首先通过在下一节中使用扩展使 Sauce 枚举符合 CalorieCountable 协议。
通过扩展采用协议
目前,Sauce 枚举不符合 CalorieCountable 协议。您将使用扩展来添加使其符合所需的属性和方法。在 Sauce 枚举声明之后输入以下代码:
enum Sauce {
case chili
case tomato
}
**extension****Sauce****:** **CalorieCountable** **{**
**var****calories****:** **Int** **{**
**switch****self** **{**
**case** **.****chili****:**
**20**
**case** **.****tomato****:**
**15**
**}**
**}**
**func****description****() ->** **String** **{**
**"This sauce has** **\(****calories****)** **calories"**
**}**
**}**
如您所见,对 Sauce 枚举的原定义没有进行任何更改。如果您想扩展现有的 Swift 标准类型的功能,如 String 和 Int,这也很有用。
枚举实例不能像结构体和类那样在属性中存储值,因此使用 switch 语句根据枚举的值返回卡路里数。description() 方法与 Burger 类和 Fries 结构体中的相同。
所有三个对象都有一个 calories 属性和一个 description() 方法。太棒了!
让我们看看如何在下一节中将它们放入数组中,并执行操作以获取餐点的总卡路里数。
创建不同类型对象的数组
通常,数组的元素必须是同一类型。然而,由于 Burger 类、Fries 结构体和 Sauce 枚举都符合 CalorieCountable 协议,您可以创建一个包含符合此协议的元素的数组。按照以下步骤操作:
-
要将
Burger类的实例、Fries结构体和Sauce枚举的实例添加到数组中,在文件中的所有其他代码之后输入以下代码:let burger = Burger() let fries = Fries() let sauce = Sauce.tomato let foodArray: [CalorieCountable] = [burger, fries, sauce] -
要获取总卡路里数,在创建
foodArray常量之后的行中添加以下代码:let totalCalories = foodArray.reduce(0, {$0 + $1.calories}) print(totalCalories)
reduce 方法用于从 foodArray 数组的元素中生成一个单一值。此方法的第一参数是初始值,设置为 0。第二个参数是一个闭包,它将初始值与一个元素的 calories 属性中存储的值组合。这将对 foodArray 数组中的每个元素重复进行,并将结果分配给 totalCalories。总数量,1315,将在调试区域显示。
您已经学习了如何创建一个协议,并使类、结构体或枚举符合它,无论是通过类定义还是通过扩展。让我们接下来看看错误处理,并了解如何在程序中响应或恢复错误。
探索错误处理
当您编写应用程序时,请记住可能会发生错误条件,错误处理是您的应用程序如何响应和从这些条件中恢复的方式。
首先,你创建一个符合 Swift 的Error协议的类型,这使得该类型可用于错误处理。枚举通常被使用,因为你可以为不同类型的错误指定关联值。当发生意外情况时,你可以通过抛出错误来停止程序执行。你使用throw语句来完成此操作,并提供一个符合Error协议的类型的实例,并带有适当的值。这允许你看到出了什么问题。
当然,如果你能在不停止程序的情况下响应错误,那就更好了。为此,你可以使用do-catch块,它看起来像这样:
do {
try expression1
statement1
} catch {
statement2
}
在这里,你尝试使用try关键字在do块中执行代码。如果抛出错误,catch块中的语句将被执行。你可以有多个catch块来处理不同类型的错误。
更多关于错误处理的信息,请访问docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling。
例如,假设你有一个需要访问网页的应用程序。然而,如果该网页所在的服务器宕机,你需要编写代码来处理错误,例如尝试使用备用网页服务器或通知用户服务器已宕机。
让我们创建一个符合Error协议的枚举,当发生错误时使用throw语句停止程序执行,并使用do-catch块来处理错误。按照以下步骤操作:
-
将以下代码输入到你的 playground 中:
enum WebsiteError: Error { case noInternetConnection case siteDown case wrongURL }
这声明了一个符合Error协议的枚举WebsiteError,它涵盖了三种可能的错误条件:没有互联网连接、网站宕机或 URL 无法解析。
-
在
WebsiteError定义之后输入以下代码以声明一个函数,该函数在WebpageError声明之后检查网站是否可用:func checkWebsite(siteUp: Bool) throws -> String { if !siteUp { throw WebsiteError.siteDown } return "Site is up" }
如果siteUp为true,则返回"Site is up"。如果siteUp为false,程序将停止执行并抛出错误。
-
在
checkWebsite(siteUp:)函数定义之后输入以下代码并运行你的程序:let siteStatus = true try checkWebsite(siteUp: siteStatus)
由于siteStatus为true,Site is up将出现在结果区域。
-
将
siteStatus的值更改为false并运行你的程序。你的程序会崩溃,并在调试区域显示以下错误消息:Playground execution terminated: An error was thrown and was not caught: error: error: -
当然,如果你能在不使程序崩溃的情况下处理错误,那就更好了。你可以通过使用
do-catch块来实现这一点。按照以下所示修改你的代码并运行它:let siteStatus = false **do** **{** **print(**try checkWebsite(siteUp: siteStatus)**)** **}** **catch** **{** **print(****error****)** **}**
do块尝试执行checkWebsite(siteUp:)函数,并在成功时打印状态。如果有错误发生,而不是崩溃,catch块中的语句将被执行,错误消息siteDown将出现在调试区域。
你可以通过实现多个catch块来让你的程序处理不同的错误条件。有关详细信息,请参阅此链接:docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling。
你已经学会了如何在应用程序中处理错误而不会导致其崩溃。太棒了!
摘要
在本章中,你学习了如何编写协议以及如何使类、结构和枚举符合这些协议。你还学习了如何通过使用扩展来扩展类的能力。最后,你学习了如何使用do-catch块来处理错误。
这些内容现在可能看起来相当抽象且难以理解,但在本书的第三部分中,你将看到如何使用协议在程序的各个部分实现常见功能,而不是一遍又一遍地编写相同的程序。你将看到扩展在组织代码方面的有用性,这使得代码易于维护。最后,你将看到良好的错误处理如何使定位你在编写应用程序时犯的错误变得容易。
在下一章中,你将了解Swift 并发,这是在 Swift 中处理异步操作的新方法。
加入我们的 Discord 频道!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,还有更多。扫描二维码或访问链接加入社区。
第九章:Swift 并发
在 WWDC21 上,Apple 引入了 Swift 并发,它为 Swift 5.5 添加了对结构化异步和并行编程的支持。它允许您编写更易读、更易于理解的并发代码。在 WWDC24 上,Apple 引入了 Swift 6,它通过在编译时诊断 数据竞争 来简化并发编程。
目前,不建议为大型现有项目启用严格并发,因为它可能会生成多个错误和警告。然而,鉴于这是 Apple 的未来方向,您将在本章和本书的 第三部分 中为项目启用它,以便您可以学习和获得相关经验。
在本章中,您将学习 Swift 并发的基本概念。接下来,您将检查一个没有并发的应用程序,并探讨其问题。然后,您将使用 async/await 在应用程序中实现并发。最后,您将通过使用 async-let 使您的应用程序更加高效。
到本章结束时,您将了解 Swift 并发的工作原理以及如何更新您自己的应用程序以使用它。
以下内容将涵盖:
-
理解 Swift 并发
-
检查没有并发功能的 app
-
使用
async/await更新应用程序 -
使用
async-let提高效率
技术要求
我们将使用一个示例应用程序,BreakfastMaker,来理解 Swift 并发的概念。
本章完成的 Xcode 项目位于本书代码包的 Chapter09 文件夹中,您可以通过以下链接下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际运行情况:
让我们从学习下一节中的 Swift 并发开始。
理解 Swift 并发
在 Swift 5.5 中,Apple 添加了对以结构化方式编写 异步 和 并行 代码的支持。
异步代码允许您的应用程序暂停和恢复代码。并行代码允许您的应用程序同时运行多个代码片段。这使得您的应用程序能够在执行如从互联网下载数据等操作的同时更新用户界面。
您可以在 WWDC21 期间找到所有 Apple 的 Swift 并发视频链接,请访问 developer.apple.com/news/?id=2o3euotz。
您可以在 developer.apple.com/news/?id=2o3euotz 阅读 Apple 的 Swift 并发文档。
在 WWDC24 上,Apple 发布了 Swift 6。使用 Swift 6 语言模式,编译器现在可以保证并发程序没有数据竞争。这意味着你的应用程序的一部分代码不能再访问另一部分代码正在修改的同一内存区域。然而,当你创建一个新的 Xcode 项目时,它默认使用 Swift 5 语言模式,你必须打开 Swift 6 语言模式才能启用此功能。
要查看 Apple 的 WWDC24 视频关于将你的应用程序迁移到 Swift 6,请点击此链接:developer.apple.com/videos/play/wwdc2024/10169/
要查看 Apple 关于将你的应用程序迁移到 Swift 6 的文档,请点击此链接:www.swift.org/migration/documentation/migrationguide/
为了让你了解 Swift 并发的工作方式,想象一下你正在为早餐做水煮蛋和烤面包。这里有一种做法:
-
将两片面包放入烤面包机中。
-
等待两分钟,直到面包烤熟。
-
在平底锅中放入两个鸡蛋,并盖上锅盖。
-
等待七分钟,直到鸡蛋煮熟。
-
上菜并享用早餐。
总共需要九分钟。现在,思考一下这个事件序列。你只是盯着烤面包机和平底锅发呆吗?你可能会在面包在烤面包机中,鸡蛋在平底锅中时使用手机。换句话说,你可以在准备烤面包和鸡蛋的同时做其他事情。因此,事件序列更准确地描述如下:
-
将两片面包放入烤面包机中。
-
用你的手机计时两分钟,直到面包烤熟。
-
在一个装有沸水的大平底锅里放两个鸡蛋,并盖上锅盖。
-
用你的手机计时七分钟,直到鸡蛋煮熟。
-
上菜并享用早餐。
在这里,你可以看到你与烤面包机和平底锅的交互可以被暂停,然后恢复,这意味着这些操作是异步的。操作仍然需要九分钟,但你在这段时间里可以做其他事情。
还有一个需要考虑的因素。你不需要等到面包烤熟后再把鸡蛋放入平底锅。这意味着你可以修改步骤的顺序如下:
-
将两片面包放入烤面包机中。
-
当面包正在烤制时,将两个鸡蛋放入一个装有沸水的大平底锅中,并盖上锅盖。
-
用你的手机计时七分钟。在这段时间里,面包会烤熟,鸡蛋会煮熟。
-
上菜并享用早餐。
烤面包和煮鸡蛋现在是并行进行的,这可以为你节省两分钟。太棒了!然而,请注意,你还有更多的事情需要关注。
现在你已经理解了异步和并行操作的概念,让我们在下一节研究没有并发的应用程序存在的问题。
检查没有并发的应用程序
你已经看到了异步和并行操作如何帮助你更快地准备早餐,并允许你在做这件事的同时使用手机。现在,让我们看看一个模拟准备早餐过程的示例应用。最初,这个应用没有实现并发,这样你可以看到它对应用的影响。按照以下步骤操作:
-
如果你还没有这样做,请在此链接下载本书的代码包中的
Chapter09文件夹:github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Eighth-Edition。 -
打开
Chapter09文件夹,你会看到两个文件夹,BreakfastMaker-start和BreakfastMaker-complete。第一个文件夹包含你将在本章中修改的应用,第二个文件夹包含完成的应用。 -
打开
BreakfastMaker-start文件夹,然后打开BreakfastMakerXcode 项目。在项目导航器中点击Main故事板文件。你应该在视图控制器场景中看到四个标签和一个按钮,如图所示:

图 9.1:主故事板文件显示视图控制器场景
应用将显示一个屏幕,显示吐司和鸡蛋的状态,以及上菜和上桌所需的时间。应用还将显示一个按钮,你可以使用它来测试用户界面的响应性。
如果你对其中一些概念不熟悉,不要担心。你将在下一章,即第十章,设置用户界面中学习如何使用故事板为你的应用构建用户界面。
-
在项目导航器中点击ViewController文件。你应该在编辑器区域看到以下代码:
import UIKit class ViewController: UIViewController { @IBOutlet var toastLabel: UILabel! @IBOutlet var eggLabel: UILabel! @IBOutlet var plateAndServeLabel: UILabel! @IBOutlet var elapsedTimeLabel: UILabel! override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let startTime = Date().timeIntervalSince1970 toastLabel.text = "Making toast..." toastLabel.text = makeToast() eggLabel.text = "Boiling eggs..." eggLabel.text = boilEggs() plateAndServeLabel.text = plateAndServe() let endTime = Date().timeIntervalSince1970 elapsedTimeLabel.text = "Elapsed time is \(((endTime - startTime) * 100).rounded() / 100) seconds" } func makeToast() -> String { sleep(2) return "Toast done" } func boilEggs() -> String { sleep(7) return "Eggs done" } func plateAndServe() -> String { return "Plating and serving done" } @IBAction func testButton(_ sender: UIButton) { print("Button tapped") } }
如你所见,这段代码模拟了之前章节中描述的制作早餐的过程。让我们来分解它:
@IBOutlet var toastLabel: UILabel!
@IBOutlet var eggLabel: UILabel!
@IBOutlet var plateAndServeLabel: UILabel!
@IBOutlet var elapsedTimeLabel: UILabel!
这些输出连接到Main故事板文件中的四个标签。当你运行应用时,这些标签将显示吐司和鸡蛋的状态,上菜和上桌,以及完成过程所需的时间。
override func viewDidAppear(_ animated: Bool) {
这个方法在视图控制器的视图出现在屏幕上时被调用。
let startTime = Date().timeIntervalSince1970
这个语句将 startTime 设置为当前时间,这样应用就可以稍后计算制作餐点所需的时间。
toastLabel.text = "Making toast..."
这个语句使得 toastLabel 显示文本“制作吐司....”
toastLabel.text = makeToast()
这个语句调用了 makeToast()方法,该方法等待两秒钟来模拟制作吐司所需的时间,然后返回文本“吐司完成”,该文本将由 toastLabel 显示。
eggLabel.text = "Boiling eggs..."
这个语句使得 eggLabel 显示文本“正在煮鸡蛋....”
eggLabel.text = boilEggs()
这个语句调用了 boilEggs()方法,该方法等待七秒钟来模拟煮两个鸡蛋所需的时间,然后返回文本“鸡蛋完成”,该文本将由 eggLabel 显示。
plateAndServeLabel.text = plateAndServe()
这个语句调用了 plateAndServe()方法,该方法返回文本“上菜和上桌完成”,该文本将由plateAndServeLabel显示。
let endTime = Date().timeIntervalSince1970
此语句将endTime设置为当前时间。
elapsedTimeLabel.text = "Elapsed time is
\(((endTime - startTime) * 100).rounded()
/ 100) seconds"
该语句计算经过的时间(大约八秒),这将通过elapsedTimeLabel显示。
@IBAction func testButton(_ sender: UIButton) {
print("Button tapped")
}
此方法每次屏幕上的按钮被点击时,在调试区域显示按钮点击。
构建并运行应用,并在用户界面出现时立即点击按钮:

图 9.2:iOS 模拟器运行 BreakfastMaker 应用,显示要点击的按钮
您应该注意到以下问题:
-
初始时点击按钮没有效果,您只能在约九秒后在调试区域看到按钮点击。
-
制作吐司... 和 煮鸡蛋... 从不显示,而吐司完成和鸡蛋完成仅在约九秒后出现。
这种情况发生的原因是,当makeToast()和boilEggs()方法运行时,您的应用代码没有更新用户界面。您的应用确实注册了按钮点击,但在makeToast()和boilEggs()完成执行后,才能够处理这些点击并更新标签。这些问题不会为您的应用提供良好的用户体验。
您现在已经体验了没有实现并发的应用所呈现的问题。在下一节中,您将使用async/await修改应用,使其可以在makeToast()和boilEggs()方法运行时更新用户界面。
使用 async/await 更新应用
如您之前所见,当makeToast()和poachEgg()方法运行时,应用无响应。为了解决这个问题,您将在应用中使用 async/await。
在方法声明中写入async关键字表示该方法是非同步的。这看起来是这样的:
func methodName() async -> returnType {
在方法调用前写入await关键字标记了一个可能挂起执行的点,从而允许其他操作运行。这看起来是这样的:
await methodName()
您可以观看 Apple 的 WWDC21 视频,讨论 async/await,链接为developer.apple.com/videos/play/wwdc2021/10132/。
您将修改您的应用以使用 async/await。这将使其能够挂起makeToast()和poachEgg()方法以处理按钮点击,更新用户界面,然后之后继续执行这两个方法。您还将通过开启 Swift 6 语言模式来为您的应用启用严格的并发检查。请按照以下步骤操作:
- 在项目导航器中,点击顶部的BreakfastMaker图标,然后点击BreakfastMaker目标。在构建设置选项卡中,将Swift 语言版本更改为Swift 6:

图 9.3:BreakfastMaker 项目,Swift 语言版本设置为 Swift 6
这使得对您的应用进行严格的并发检查成为可能。
-
在项目导航器中点击ViewController文件。修改
makeToast()和boilEggs()方法,如下所示,以使它们体内的代码异步:func makeToast() -> String { **try?****await****Task****.sleep(for: .seconds(2))** return "Toast done" } func boilEggs() -> String { **try?****await****Task****.sleep(for: .seconds(7))** return "Eggs done" }
Task代表异步工作的一个单元。它有一个静态方法sleep(for:),该方法可以暂停执行指定的时间长度,以秒为单位。由于这个方法是一个抛出异常的方法,你将使用try?关键字来调用它,而不需要实现do-catch块。await关键字表示此代码可以被挂起,以允许其他代码运行。
使用try?将导致任何错误被抑制或忽略。在这种情况下这是可以接受的,因为睡眠 2 秒或 7 秒不太可能产生错误。在其他情况下,这可能不可接受,在这些情况下,do-catch块是一个更好的解决方案。你可能需要重新阅读第八章,协议、扩展和错误处理,以获取有关如何实现do-catch块的信息。
makeToast()和boilEggs()都会出现错误。点击任一错误图标以显示错误信息:

图 9.4:点击错误图标时的错误信息
错误显示出来是因为你在不支持并发的方法中调用异步方法。你需要将async关键字添加到方法声明中,以表明它是异步的。
-
对于每个方法,点击修复按钮以将
async关键字添加到方法声明中。 -
确认你完成后的代码如下所示:
func makeToast() **async** -> String { try? await Task.sleep(for: .seconds(2)) return "Toast done" } func boilEggs() **async** -> String { try? await Task.sleep(for: .seconds(2)) return "Eggs done" } -
makeToast()和poachEgg()方法中的错误应该已经消失,但在viewDidAppear()方法中会出现新的错误。点击其中一个错误图标以查看错误信息,该信息将与你在步骤 2中看到的相同。这是因为你在不支持并发的方法中调用异步方法。 -
点击修复按钮,将出现更多错误。
-
目前忽略方法声明中的错误,并点击
makeToast()方法调用旁边的错误以查看错误信息:

图 9.5:点击错误图标时的错误信息
这个错误信息显示出来是因为你在调用异步函数时没有使用await。
-
点击修复按钮以在方法调用之前插入
await关键字。 -
对于
boilEggs()方法调用旁边的错误,重复步骤 7和步骤 8。await关键字也将被插入到boilEggs()方法调用中。 -
点击
viewDidAppear()方法声明中的错误图标以查看错误信息:

图 9.6:错误图标被突出显示的错误
这个错误显示出来是因为你不能使用async关键字使viewDidAppear()方法异步,因为这个功能在超类中不存在。
-
要解决这个问题,你需要移除
async关键字,并将super.viewDidAppear()之后的全部代码放在一个Task块中,这将允许它在同步方法中异步执行。按照以下方式修改你的代码:override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) **Task** **{** let startTime = Date().timeIntervalSince1970 toastLabel.text = "Making toast..." toastLabel.text = await makeToast() eggLabel.text = "Boiling eggs..." eggLabel.text = await boilEggs() plateAndServeLabel.text = plateAndServe() let endTime = Date().timeIntervalSince1970 elapsedTimeLabel.text = "Elapsed time is \(((endTime - startTime) * 100).rounded() / 100) seconds" **}** }
构建并运行应用,一旦看到用户界面就立即点击按钮。注意,按钮已点击现在会立即显示在调试区域,标签也会相应更新。这是因为应用现在能够挂起makeToast()和boilEggs()方法以响应用户点击,更新用户界面,并在稍后恢复方法执行。太棒了!
然而,如果你查看经过的时间,你会发现应用准备早餐的时间比之前稍微长了一些:

图 9.7:iOS 模拟器运行 BreakfastMaker 应用,显示经过的时间
这部分原因是异步/等待挂起和恢复方法所需的额外处理,但还有一个因素在起作用。尽管makeToast()和boilEggs()方法现在是异步的,但boilEggs()方法只有在makeToast()方法执行完毕后才开始执行。在下一节中,你将看到如何使用async-let来并行运行makeToast()和boilEggs()方法。
使用 async-let 提高效率
尽管你的应用现在对按钮点击有响应,并且可以在makeToast()和boilEggs()方法运行时更新用户界面,但这两个方法仍然按顺序执行。这里的解决方案是使用async-let。
在定义常量时在let语句前写上async,然后在访问常量时写上await,允许异步方法的并行执行,如下所示:
async let temporaryConstant1 = methodName1()
async let temporaryConstant2 = methodName2()
await variable1 = temporaryConstant1
await variable2 = temporaryConstant1
在这个例子中,methodName1()和methodName2()将并行运行。
你将修改你的应用以使用async-let来使makeToast()和poachEgg()方法并行运行。在ViewController文件中,按照以下方式修改Task块中的代码:
Task {
let startTime = Date().timeIntervalSince1970
toastLabel.text = "Making toast..."
**async****let** **tempToast** **=****makeToast****()**
eggLabel.text = "Boiling eggs..."
**async****let** **tempEggs** **=****boilEggs****()**
**await****toastLabel****.****text****=** **tempToast**
**await****eggLabel****.****text****=** **tempEggs**
plateAndServeLabel.text = plateAndServe()
let endTime = Date().timeIntervalSince1970
elapsedTimeLabel.text = "Elapsed time is
\(((endTime - startTime) * 100).rounded()
/ 100) seconds"
}
构建并运行应用。你会发现经过的时间现在比之前短:

图 9.8:iOS 模拟器运行 BreakfastMaker 应用,显示经过的时间
这是因为使用async-let允许makeToast()和poachEgg()方法并行运行,并且poachEgg()方法不再等待makeToast()方法完成后再开始执行。酷!
关于 Swift 并发还有很多东西要学习,比如结构化并发和 actors,但这超出了本章的范围。你可以在developer.apple.com/videos/play/wwdc2021/10134/了解更多关于结构化并发的信息,你可以在developer.apple.com/videos/play/wwdc2021/10133/了解更多关于 actors 的信息。
你已经在你的应用中成功实现了异步代码。太棒了!关于 Swift 并发还有很多东西要学习,比如结构化并发和 actors,但这超出了本章的范围。
给自己鼓掌吧;你已经完成了这本书的第一部分!
摘要
在本章中,您了解了 Swift 并发及其在BreakfastMaker应用程序中的实现方法。
您从学习 Swift 并发的基本概念开始。然后,您检查了一个没有并发的应用程序,并探讨了它的问题。之后,您开启了严格的并发检查,并在应用程序中实现了并发,使用了async/await。最后,您通过使用async let使您的应用程序更加高效。
您现在已经了解了 Swift 并发的基础知识,并将能够在自己的应用程序中使用async/await和async-let。
在下一章中,您将通过创建它的屏幕,使用故事板来编写您的第一个 iOS 应用程序,这允许您在不输入大量代码的情况下快速原型化应用程序。
留下您的评价!
感谢您从 Packt Publishing 购买此书——我们希望您喜欢它!您的反馈对我们来说无价,它帮助我们改进和成长。一旦您阅读完毕,请花一点时间在亚马逊上留下评价;这只需一分钟,但对像您这样的读者来说意义重大。扫描下面的二维码或访问链接,以获得您选择的免费电子书。
第二部分
设计
欢迎来到本书的第二部分。到这个时候,你已经熟悉了 Xcode 的用户界面,并且对使用 Swift 有了坚实的基础。在这一部分,你将开始创建一个名为JRNL的日记应用的用户界面。你将使用 Interface Builder 来构建应用将使用的屏幕,向它们添加按钮、标签和字段等元素,并通过 segues 将它们连接起来。正如你将看到的,你可以用最少的编码来完成这些工作。
本部分包括以下章节:
-
第十章,设置用户界面
-
第十一章,构建用户界面
-
第十二章,完成用户界面
-
第十三章,修改应用屏幕
到这一部分的结尾,你将能够在 iOS 模拟器中导航你应用的各个屏幕,并且将知道如何原型化你自己的应用的用户界面。让我们开始吧!
第十章:设置用户界面
在本书的第一部分中,你学习了 Swift 语言及其工作原理。现在你对这门语言有了很好的了解,你可以学习如何开发 iOS 应用程序。在这一部分,你将构建一个名为JRNL的日记应用程序的用户界面(UI)。你将使用 Xcode 的界面构建器来完成这项工作,并且代码将保持最小化。
你将从这个章节开始学习 iOS 开发中广泛使用的一些有用术语。接下来,你将游览JRNL应用程序中使用的屏幕,并了解用户如何使用该应用程序。最后,你将开始使用界面构建器重新创建应用程序的 UI,从允许用户在日记列表和地图屏幕之间选择的标签栏开始。你将在两个屏幕的顶部添加导航栏并配置标签栏按钮。
到本章结束时,你将学会 iOS 应用开发中常用的术语,了解你的应用程序的流程,以及如何使用界面构建器添加和配置 UI 元素。
本章将涵盖以下主题:
-
学习 iOS 开发中的有用术语
-
游览JRNL应用程序
-
修改你的 Xcode 项目
-
设置标签栏控制器场景
技术要求
你将修改在第一章,“探索 Xcode”中创建的JRNL Xcode 项目。
本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter10文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
观看以下视频,看看代码的实际应用:
在你开始项目之前,你将学习一些 iOS 开发中常用的术语。
学习 iOS 开发中的有用术语
当你开始你的 iOS 应用开发之旅时,你将遇到特殊的术语和定义。以下是一些最常用的术语和定义。现在只需阅读它们即可。即使你现在可能并不完全理解,但随着你的深入,一切都会变得清晰:
- 视图:视图是
UIView类或其子类的实例。你屏幕上看到的所有内容(按钮、文本字段、标签等)都是视图。你将使用视图来构建你的 UI。
类在第七章,“类、结构和枚举”中有所介绍。
-
堆叠视图:堆叠视图是
UIStackView类的实例,它是UIView的子类。它用于将视图组合成水平或垂直堆叠,这使得它们更容易使用自动布局(稍后在本节中讨论)在屏幕上定位。自动布局是一种布局方式,它允许开发者通过指定视图之间的相对位置和大小来创建用户界面。 -
视图控制器:视图控制器是
UIViewController类的一个实例。每个视图控制器都有一个view属性,它包含对一个视图的引用。它决定了视图向用户显示的内容以及用户与视图交互时会发生什么。
视图控制器将在第十四章“开始使用 MVC 和表格视图”中详细讨论。
- 表格视图控制器:表格视图控制器是
UITableViewController类的一个实例,它是UIViewController类的一个子类。它的view属性包含对一个UITableView实例(表格视图)的引用,该实例显示一列UITableViewCell实例(表格视图单元格)。
设置应用以表格视图的形式显示你的设备设置:

图 10.1:设置应用
正如你所见,所有不同的设置(通用、辅助功能、隐私等)都在表格视图的单元格内显示。
- 集合视图控制器:集合视图控制器是
UICollectionViewController类的一个实例,它是UIViewController类的一个子类。它的view属性包含对一个UICollectionView实例(集合视图)的引用,该实例显示一个UICollectionViewCell实例(集合视图单元格)的网格。
照片应用在集合视图中显示照片:

图 10.2:照片应用
正如你所见,缩略图图片在集合视图中显示在集合视图的单元格内。
- 导航控制器:导航控制器是
UINavigationController类的一个实例,它是UIViewController类的一个子类。它有一个viewControllers属性,该属性包含一个视图控制器数组。数组中最后一个视图控制器的视图会显示在屏幕上,同时屏幕顶部还有一个导航栏。
在设置应用中,表格视图控制器嵌入在导航控制器中,你可以看到表格视图上方的导航栏:

图 10.3:设置应用中的导航栏
当你点击一个设置时,该设置的视图控制器会被添加到分配给viewControllers属性的视图控制器数组中。用户会看到该视图控制器从右侧滑入。注意屏幕顶部的导航栏,它可以包含标题和按钮。一个< 设置按钮出现在导航栏的左上角。点击此按钮会返回上一个屏幕,并从viewControllers属性分配的视图控制器数组中移除该设置的视图控制器。
- 标签栏控制器:标签栏控制器是
UITabBarController类的一个实例,它是UIViewController类的一个子类。它有一个viewControllers属性,包含一个视图控制器数组。数组中第一个视图控制器的视图显示在屏幕上,同时还有一个带有按钮的标签栏在底部。最左边的按钮对应于数组中的第一个视图控制器,并且已经选中。当你点击另一个按钮时,相应的视图控制器将被加载,其视图将显示在屏幕上。
Fitness应用使用标签栏控制器来导航到不同的屏幕:

图 10.4:健身应用中的标签栏
正如你所见,这个应用的不同屏幕(所有照片、为你推荐、相册和搜索)可以通过点击相应的标签栏按钮来访问。
-
模型-视图-控制器(MVC):这是在 iOS 应用开发中非常常见的一个设计模式。用户与屏幕上的视图进行交互。应用数据存储在数据模型对象中。控制器管理视图和数据模型对象之间的信息流。
MVC 将在第十四章,开始使用 MVC 和表格视图中详细讨论。
-
故事板文件:故事板文件包含用户看到的视觉表示。应用中的每一屏幕都由一个故事板场景表示。
打开你在第一章,探索 Xcode中创建的JRNL项目,并点击主故事板文件。

图 10.5:显示主故事板文件的 JRNL Xcode 项目
你将看到一个场景,当你运行你的应用在模拟器中时,这个场景的内容将显示在屏幕上。你可以在故事板文件中有一个以上的场景。
-
转场:如果你在一个应用中有多个场景,你使用转场来从一个场景移动到另一个场景。JRNL项目没有转场,因为它的故事板文件中只有一个场景,但你将在本章的后面部分看到它们。
-
自动布局:作为一名开发者,你必须确保你的应用在不同屏幕尺寸的设备上看起来都很好。自动布局帮助你根据你指定的约束来布局你的 UI。例如,你可以设置一个约束来确保按钮在屏幕上居中,无论屏幕大小如何,或者当设备从纵向旋转到横向时,使文本字段扩展到屏幕的宽度。
现在你已经熟悉了在 iOS 应用开发中使用的术语,让我们来游览一下你将要构建的应用。
JRNL 应用的游览
让我们快速浏览一下你将要构建的应用程序。JRNL应用程序是一个日记应用程序,允许用户编写自己的个人日记,并为每个日记条目提供存储照片或地图位置的选择。用户还可以查看显示靠近用户当前位置的条目位置的地图。你将在下一节中看到应用程序中使用的所有屏幕及其整体流程。
使用“期刊列表”屏幕
当应用程序启动时,你会看到“期刊列表”屏幕:

图 10.6:期刊列表屏幕
让我们研究一下这个屏幕的不同部分。
屏幕底部的UITabBar实例(标签栏)显示了期刊和地图按钮。期刊按钮被选中,你可以看到一个表格视图,在表格视图中显示期刊条目的列表。一个UISearchController实例在屏幕顶部显示一个搜索栏。这允许你搜索特定的期刊条目。
要添加新的期刊条目,你点击屏幕顶部的+按钮。这会显示“添加新期刊条目”屏幕。
使用“添加新期刊条目”屏幕
当你在“期刊列表”屏幕顶部点击+按钮时,你会看到“添加新期刊条目”屏幕:

图 10.7:添加新期刊条目屏幕
让我们研究一下这个屏幕的不同部分。
屏幕顶部的导航栏包含取消和保存按钮。一个堆叠视图显示自定义评分控件、开关、条目标题文本字段、正文文本视图和占位符照片。点击评分控件可以为此条目分配 0 到 5 星。打开开关将获取你的当前位置。
你可以在条目标题文本字段中输入期刊条目的标题,并在正文文本视图中输入详细信息。你还可以点击占位符照片,使用设备相机拍照。一旦你点击保存,你将返回到“期刊列表”屏幕,然后新的条目将在表格视图中可见。你也可以点击取消,不创建新的期刊条目就返回到“期刊列表”屏幕。
要查看特定期刊条目的详细信息,点击列表中的条目,然后你会看到“期刊条目详情”屏幕。
使用“期刊条目详情”屏幕
点击“期刊列表”屏幕上的任何一条期刊条目将显示相应的“期刊条目详情”屏幕:

图 10.8:期刊条目详情屏幕
让我们研究一下这个屏幕的不同部分。
屏幕顶部的导航栏包含一个返回按钮。一个表格视图在表格视图中显示期刊条目的日期、评分、标题文本、正文文本、照片和位置地图。
你可以点击< 期刊按钮返回到“期刊列表”屏幕。
使用“地图”屏幕
在标签栏中点击地图按钮会显示地图屏幕:

图 10.9:地图屏幕
让我们研究一下这个屏幕的不同部分。
屏幕底部的标签栏显示了Journal和Map按钮。Map按钮被选中,你可以看到一个MKMapView实例(地图视图)在屏幕上显示地图,图钉指示期刊条目。
点击一个图钉将显示注释,点击注释中的按钮将显示该条目的期刊条目详情屏幕。
这完成了对该应用的浏览。现在,是时候开始构建它的 UI 了!
修改你的 Xcode 项目
现在你已经知道了应用屏幕的外观,你可以开始构建它了。如果你还没有这样做,打开你在第一章 探索 Xcode中创建的JRNL项目:

图 10.10:JRNL 项目
确认从目标菜单中选择了iPhone SE (第 3 代)。构建并运行你的应用。你会看到一个空白的白色屏幕。如果你在项目导航器中点击Main故事板文件,你会看到它包含一个包含空白视图的单个场景。这就是为什么当你运行应用时,你只看到一个空白的白色屏幕。
要配置 UI,你将使用 Interface Builder 修改Main故事板文件。Interface Builder 允许你添加和配置场景。每个场景代表用户将看到的屏幕。你可以在场景中添加 UI 对象,如视图和按钮,并按需配置它们,使用属性检查器。
有关如何使用 Interface Builder 的更多信息,请访问此链接:help.apple.com/xcode/mac/current/#/dev31645f17f。
现在,你将在标签栏中嵌入现有的场景,并向其中添加另一个场景。标签栏场景将在屏幕底部显示一个标签栏,其中包含两个按钮。点击一个按钮将显示与之关联的屏幕。这些屏幕对应于应用浏览中显示的期刊列表和地图屏幕。让我们看看如何在下一节中完成这个操作。
设置标签栏控制器场景
正如你在应用浏览中看到的,JRNL应用在屏幕底部有一个标签栏,其中包含两个按钮,用于显示期刊列表和地图屏幕。你将在标签栏中嵌入现有的视图控制器场景,并向其中添加第二个视图控制器场景。按照以下步骤操作:
- 在项目导航器中点击Main故事板文件:

图 10.11:选择 Main 故事板文件的项目导航器
Main故事板文件的内容显示在编辑器区域。
- 如果它不存在,点击文档大纲按钮以显示文档大纲:

图 10.12:显示文档大纲按钮的编辑器区域
- 在文档大纲中选择View Controller:

图 10.13:选择 ViewController 的文档大纲
- 你将在标签栏控制器场景中嵌入现有的视图控制器场景。从编辑器菜单中选择嵌入 | 标签栏控制器:

图 10.14:选中嵌入 | 标签栏控制器的编辑器菜单
你将在编辑器区域看到一个新标签栏控制器场景出现。
- 点击窗口右上角的+按钮以显示库:

图 10.15:显示+按钮的工具栏
库允许你选择要添加到场景中的 UI 对象。
- 在库的过滤器字段中输入
view con。一个View Controller对象将出现在结果列表中:

图 10.16:选中视图控制器对象的库
- 将View Controller对象拖动到故事板中,以添加一个新的视图控制器场景,并将其放置在现有的视图控制器场景下方:

图 10.17:添加了视图控制器场景的主故事板文件
- 点击-按钮以缩小视图,并在故事板中重新排列场景,以便同时显示标签栏控制器场景和视图控制器场景:

图 10.18:显示缩放按钮的编辑区域
如果–和+按钮不可见,尝试放大 Xcode 窗口。你也可以尝试使用导航器和检查器按钮隐藏导航器和检查器区域。
- 在文档大纲中选择Tab Bar Controller。按Ctrl键并从Tab Bar Controller拖动到新添加的视图控制器场景:

图 10.19:显示拖动目的地的编辑区域
- 将出现一个切换弹出菜单。从该菜单中选择视图控制器:

图 10.20:切换弹出菜单
将出现一个连接标签栏控制器场景到视图控制器场景的切换。
- 在编辑器区域重新排列场景,使其看起来像下面的截图:

图 10.21:重新排列场景的编辑区域
- 在模拟器中构建和运行你的应用,你将在屏幕底部看到带有两个按钮的标签栏:

图 10.22:显示两个按钮的标签栏的模拟器
你已成功将标签栏添加到项目中,但正如你所见,按钮标题目前都命名为Item。在下一节中,你将将它们更改为Journal和Map。
设置标签栏按钮标题和图标
你的应用现在在屏幕底部显示了一个标签栏,但按钮标题和图标与应用之旅中显示的不匹配。为了使它们匹配,你将在属性检查器中将按钮标题配置为读取Journal和Map,并配置它们的图标。按照以下步骤操作:
- 在项目导航器中点击Main故事板文件。如果未显示,点击文档大纲按钮以显示文档大纲。点击文档大纲中的第一个Item Scene:

图 10.23:显示已选择第一个项目场景的文档大纲
- 在项目场景下点击项目按钮。然后,点击属性检查器按钮:

图 10.24:已选择属性检查器
- 在条目项下将标题设置为
Journal,图像设置为person.fill:

图 10.25:属性检查器,标题设置为 Journal,图像设置为 person.fill
- 在第二个项目场景中点击项目按钮,并在条目项下将标题设置为
Map,图像设置为map:

图 10.26:属性检查器,标题设置为 Map,图像设置为 map
- 在模拟器中构建并运行你的应用。你会看到按钮的标题已分别更改为Journal和Map,并且每个按钮都有一个自定义图标:

图 10.27:显示带有自定义按钮标题和图标的标签栏的模拟器
点击Journal和Map按钮将显示期刊列表和地图屏幕的场景。
person.fill和map图标是苹果的SF Symbols库的一部分。要了解更多信息,请访问此链接:developer.apple.com/design/human-interface-guidelines/sf-symbols。
正如你在应用浏览中看到的,一些屏幕在导航栏中有标题和按钮。在下一节中,你将学习如何将导航栏添加到你的屏幕上,以便你可以根据需要稍后添加按钮和标题。
在导航控制器中嵌入视图控制器
正如你在应用浏览中看到的,期刊列表和地图屏幕的顶部都有导航栏。要为两个屏幕添加导航栏,你需要在导航控制器中嵌入期刊和地图场景的视图控制器。这样,当显示期刊列表和地图屏幕时,导航栏将出现在屏幕顶部。按照以下步骤操作:
- 在文档大纲中点击期刊场景:

图 10.28:显示已选择期刊场景的文档大纲
- 从编辑菜单中选择嵌入 | 导航控制器:

图 10.29:显示已选择嵌入到导航控制器中的编辑菜单
- 验证是否在标签栏控制器场景和期刊场景之间出现了一个导航控制器场景:

图 10.30:显示已添加导航控制器场景的编辑区域
- 在文档大纲中点击地图场景并重复步骤 2。

图 10.31:显示已添加导航控制器场景的编辑区域
现在,期刊列表屏幕和地图屏幕都拥有导航栏,但由于它们的颜色与背景相同,所以在屏幕上并不明显。你将为每个场景的导航项设置标题,以便区分它们。
- 在文档大纲中选择第一个视图控制器场景的导航项。在属性检查器中,在导航项下,将标题设置为
日历:

图 10.32:标题设置为“日历”的属性检查器
- 在文档大纲中选择第二个视图控制器场景的导航项。在属性检查器中,在导航项下,将标题设置为
地图:

图 10.33:标题设置为“地图”的属性检查器
- 构建并运行你的应用,并点击每个标签栏按钮以显示相应的屏幕。注意每个屏幕在导航栏中显示一个标题。
在导航控制器中嵌入视图控制器会将该视图控制器添加到导航控制器的viewControllers数组中。然后导航控制器在屏幕上显示视图控制器的视图。导航控制器还会在屏幕顶部显示一个带有标题的导航栏。
恭喜!你刚刚为你的应用配置了标签栏和导航控制器!
你可能已经注意到 Interface Builder 中显示的屏幕与你在目标菜单中选择的 iPhone 型号不匹配,你可能还会发现最小地图显示会妨碍你在应用中排列屏幕。让我们进行一些额外的 Interface Builder 配置来解决这个问题。
配置 Xcode
尽管你已经配置了模拟器使用 iPhone SE(第 3 代)作为你的应用设备,但在 Interface Builder 中显示的场景是为不同的 iPhone 型号。你可能还希望隐藏最小地图显示。让我们配置 Interface Builder 中的场景以使用 iPhone SE(第 3 代)并隐藏最小地图显示。按照以下步骤操作:
- 应该仍然选择主故事板文件。要配置在 Interface Builder 中场景的外观,请点击设备配置按钮:

图 10.34:显示设备配置按钮的编辑区域
将显示不同设备屏幕的弹出窗口。
- 从此弹出窗口中选择iPhone SE(第 3 代),然后点击编辑区域中的任何位置以关闭它:

图 10.35:选择 iPhone SE(第 3 代)的设备弹出窗口
故事板中场景的外观将更改为反映 iPhone SE(第 3 代)的屏幕。
- 如果你希望隐藏最小地图,从编辑菜单中选择最小地图以取消选中它。

图 10.36:突出显示最小地图的编辑菜单
- 验证在
主故事板文件中是否有以下场景:

图 10.37:显示完成的主故事板文件的编辑区域
- 构建并运行你的应用。它应该和之前一样工作。
你已经为你的应用创建了日历列表和地图屏幕!做得好!
摘要
在本章中,你学习了在 iOS 应用开发中使用的某些有用术语。这将使你更容易理解本书的其余部分,以及关于该主题的其他书籍或在线资源。
然后,你还了解了 JRNL 应用中使用的不同屏幕以及用户如何使用该应用。当你从头开始重新创建应用的 UI 时,你能够将你所做的工作与实际应用的外观进行比较。
最后,你学习了如何使用 Interface Builder 和 storyboards 将标签栏控制器场景添加到你的应用中,配置按钮标题和图标,并为 JournalList 和 Map 屏幕添加导航控制器。这将使你熟悉为你的应用添加和配置 UI 元素。
在下一章中,你将继续设置你应用的 UI,并熟悉更多 UI 元素。你将为你的应用添加和配置剩余的屏幕。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第十一章:构建你的用户界面
在上一章中,你修改了一个现有的 Xcode 项目,向你的应用添加了一个标签栏,允许用户在日记列表和地图屏幕之间进行选择,并配置了标签栏按钮的标题和图标。当你的应用启动时,显示的是日记列表屏幕,但它目前是空的。
正如你在第十章设置用户界面中的应用游览中看到的那样,日记列表屏幕应显示一个表格视图,显示表格视图单元格中的日记条目列表。
在本章中,你将使日记列表屏幕显示一个包含 10 个空表格视图单元格的表格视图,以及一个按钮,当点击时将显示一个表示添加新日记条目屏幕的视图。你还将配置一个取消按钮来关闭此视图并返回到日记列表屏幕。
你将在你的应用中添加少量代码,但不用担心太多——你将在本书的下一部分中了解更多关于它的内容。
到本章结束时,你将学会如何向故事板场景添加视图控制器,将视图控制器中的输出连接到场景,设置表格视图单元格,并以模态方式呈现视图控制器。这在你设计自己的应用的用户界面时将非常有用。
本章将涵盖以下主题:
-
将表格视图添加到日记列表屏幕
-
将故事板元素连接到视图控制器
-
为表格视图配置数据源方法
-
以模态方式呈现视图
技术要求
你将继续在上一章中创建的 JRNL Xcode 项目上工作。
本章的完成 Xcode 项目位于本书代码包的 Chapter11 文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际效果:
让我们从向日记列表屏幕添加表格视图开始,该表格视图最终将显示日记条目列表。
将表格视图添加到日记列表屏幕
正如你在应用游览中看到的那样,JRNL 应用在表格视图中显示日记条目。表格视图是 UITableView 类的一个实例。它显示一列单元格。表格视图中的每个单元格都是一个表格视图单元格,它是 UITableViewCell 类的一个实例。在本节中,你将首先在 Main 故事板文件中为日记列表屏幕的视图控制器场景添加一个表格视图,然后你将添加自动布局约束使其填充整个屏幕。
有关自动布局及其使用方法的更多信息,请访问 developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/。
打开您在前一章中创建的JRNL项目并运行应用程序,以确保一切仍然按预期工作,然后按照以下步骤操作:
- 在项目导航器中点击主故事板文件,选择代表期刊列表屏幕的视图控制器场景,然后点击库按钮:

图 11.1:显示库按钮的工具栏
- 库将出现。在过滤器字段中输入
table。表格视图对象将作为结果之一出现。将其拖动到期刊列表屏幕视图控制器场景的中间:

图 11.2:选择表格视图对象的库
表格视图已添加,但它只占据了屏幕的一小部分。如前一章中的应用程序导游所示,它应该填充整个屏幕。
- 您将使用自动布局添加新约束按钮将表格视图的边缘绑定到其封装视图的边缘。确保表格视图被选中,然后点击自动布局添加新约束按钮:

图 11.3:选择表格视图的视图控制器场景
- 在顶部、左侧、右侧和底部边缘约束字段中输入
0,然后点击所有浅红色支柱。确保所有支柱都已变为亮红色。点击添加 4 个约束按钮:

图 11.4:添加新约束的自动布局弹出对话框
这将设置表格视图边缘与封装视图边缘之间的空间为 0,将表格视图的边缘绑定到封装视图的边缘。现在表格视图将填充屏幕,无论设备方向如何。
- 确认表格视图的四面现在都已绑定到屏幕边缘,如下面的截图所示:

图 11.5:表格视图填充屏幕的视图控制器场景
您已将表格视图添加到期刊列表屏幕视图控制器场景的视图中,并使用自动布局约束使其填充整个屏幕,但构建并运行您的应用程序时,期刊列表屏幕仍然会保持空白。
在下一节中,您将实现JournalListViewController类的代码,并将此类的出口连接到期刊列表屏幕上的 UI 元素。这将使JournalListViewController类的一个实例能够控制期刊列表屏幕显示的内容。
将故事板元素连接到视图控制器
您已将表格视图添加到期刊列表屏幕,但它目前还没有显示任何内容。您需要修改现有的视图控制器以在期刊列表屏幕中管理表格视图。当您创建JRNL项目时,Xcode 自动创建了ViewController文件。
它包含了名为 ViewController 的 UIViewController 子类的声明和定义,并且这个类目前被设置为 Journal List 屏幕的视图控制器。你需要在 ViewController 文件中将类的名称更改为 JournalListViewController,并为之前添加到视图控制器场景中的表格视图创建一个出口。按照以下步骤操作:
- 在项目导航器中点击 ViewController 文件。在编辑区域,右键点击类名,选择 重构 | 重命名...

图 11.6:编辑区域显示带有重命名...高亮的弹出菜单
- 将类名更改为
JournalListViewController并点击 重命名:

图 11.7:显示 ViewController 类新名称的编辑区域
- 确认类名和文件名都已更改为 JournalListViewController:

图 11.8:文件名和类名都更改为 JournalListViewController
- 在项目导航器中点击 Main 故事板文件,并在文档大纲中选择第一个 Journal Scene(包含表格视图的那个)。

图 11.9:文档大纲显示已选择第一个 Journal Scene
- 点击身份检查器按钮,并确认在 自定义类 下,类 设置为 JournalListViewController:

图 11.10:身份检查器中类设置为 JournalListViewController
这意味着 Journal List 屏幕的内容由 JournalListViewController 类的一个实例管理。
- 点击导航器和检查器按钮以隐藏导航器和检查器区域,以便有更多空间工作:

图 11.11:工具栏显示导航器和检查器按钮
- 点击调整编辑器选项按钮,并从弹出菜单中选择 辅助:

图 11.12:调整编辑器选项菜单中已选择辅助
这将在辅助编辑器中显示与此场景关联的任何 Swift 文件。如你所见,Main 故事板文件的内容显示在编辑区域的左侧,而 JournalListViewController 类的定义显示在右侧。
- 查看代码上方的小条,确认 JournalListViewController.swift 已被选中:

图 11.13:显示已选择的 JournalListViewController.swift
如果你没有看到 JournalListViewController.swift 被选中,点击条并从弹出菜单中选择 JournalListViewController.swift。
- 要将 Journal 场景中的表格视图连接到
JournalListViewController类中的出口,从表格视图 Ctrl + 拖动 到类名声明下方的JournalListViewController文件:

图 11.14:显示拖拽目标位置的编辑区域
你也可以从文档大纲中的表格视图拖动。
- 将会弹出一个小的弹出对话框。在 名称 文本框中输入出口名称,
tableView,将 存储 设置为 强,然后点击 连接:

图 11.15:出口创建的弹出对话框
- 验证
tableView出口声明是否已自动添加到JournalListViewController类。完成此操作后,点击 x 关闭辅助编辑器窗口:

图 11.16:显示 tableView 出口的编辑区域
JournalListViewController 类现在在 Journal List 屏幕中有一个出口,tableView。这意味着一个 JournalListViewController 实例可以管理表格视图显示的内容。
使用 Ctrl + 拖动 从故事板场景中的元素拖动到文件时,经常会出错。如果你在这个过程中出错,这可能会导致应用启动时崩溃。为了检查表格视图和 JournalListViewController 类之间的连接是否存在错误,请按照以下步骤操作:
-
点击导航器和检查器按钮以显示导航器和检查器区域。
-
在 Journal Scene 中选择 Journal,然后点击连接检查器按钮:

图 11.17:选择连接检查器
连接检查器显示你的 UI 对象和代码之间的链接。你将在 出口 部分看到 tableView 出口连接到表格视图。
- 如果你看到一个微小的黄色警告图标,点击 x 来断开连接:

图 11.18:显示带有黄色警告图标的 tableView 出口的连接检查器
- 在 出口 下,从 tableView 出口拖动到表格视图以重新建立连接:

图 11.19:显示要连接的表格视图的编辑区域
如果你需要在创建后更改代码中的出口名称,右键单击出口名称,然后从弹出菜单中选择 重构 | 重命名 而不是手动更改,以避免错误。
你已经在 JournalListViewController 类中成功创建了一个用于表格视图的出口。做得好!
要在屏幕上显示表格视图单元格,你需要通过向 JournalListViewController 类添加一些代码来实现表格视图的数据源方法。你将在下一节中这样做。
配置表格视图的数据源方法
当您的应用运行时,JournalListViewController 类的一个实例充当 Journal 列表屏幕的视图控制器。它负责加载和显示该屏幕上的所有视图,包括您之前添加的表格视图。表格视图需要知道要显示多少个表格视图单元格以及每个单元格中要显示什么。通常,视图控制器负责提供这些信息。苹果为这个目的创建了一个协议,UITableViewDataSource。您需要做的只是将表格视图的 dataSource 属性设置为 JournalListViewController 类,并实现此协议的必需方法。
表格视图还需要知道如果用户点击表格视图单元格时应该做什么。同样,表格视图的视图控制器负责此事,而苹果为这个目的创建了 UITableViewDelegate 协议。您将设置表格视图的 delegate 属性为 JournalListViewController 类,但您目前不会实现此协议中的任何方法。
协议在 第八章,协议、扩展和错误处理 中有所介绍。
您在本章中需要输入少量代码。不用担心它的含义;您将在本书的 第三部分 中学习更多关于表格视图控制器及其相关协议的内容。
在下一节中,您将使用连接检查器将表格视图的 dataSource 和 delegate 属性分配给 JournalListViewController 类中的输出。
设置表格视图的委托和数据源属性
JournalListViewController 类的一个实例将提供表格视图将显示的数据,以及当用户与表格视图交互时将执行的方法。为了使这生效,您需要将表格视图的 dataSource 和 delegate 属性连接到 JournalListViewController 类中的输出。按照以下步骤操作:
-
如果您还没有这样做,请点击导航器和检查器按钮以再次显示导航器和检查器区域。
-
主故事板文件仍然被选中。在文档大纲中选择 Journal Scene 的 表格视图,然后点击连接检查器按钮。在 输出 部分,您将在 dataSource 和 delegate 输出旁边看到两个空圆圈。从每个空圆圈拖动到文档大纲中的 Journal 图标:

图 11.20:连接检查器显示 dataSource 和 delegate 输出
- 验证表格视图的
dataSource和delegate属性是否已连接到JournalListViewController类中的输出:

图 11.21:已设置 dataSource 和 delegate 输出的连接检查器
在下一节中,你将添加一些代码,使 JournalListViewController 类符合 UITableViewDataSource 协议,并在运行你的应用时配置表格视图以显示 10 个表格视图单元格。
采用 UITableViewDataSource 和 UITableViewDelegate 协议
到目前为止,你已经使 JournalListViewController 类成为表格视图的数据源和代理。下一步是使其采用 UITableViewDataSource 和 UITableViewDelegate 协议并实现任何必需的方法。你还将更改表格视图单元格的颜色,使它们在屏幕上可见。按照以下步骤操作:
- 在文档大纲中点击 表格视图 并点击属性检查器按钮。在 表格视图 下,将 原型单元格 的数量更改为
1:

图 11.22:属性检查器显示原型单元格设置为 1
- 在文档大纲中点击 表格视图 旁边的 > 按钮,以显示 表格视图单元格:

图 11.23:文档大纲显示 > 按钮
这表示表格视图将显示的表格视图单元格。
- 在文档大纲中点击 表格视图单元格。在 表格视图单元格 下的 属性检查器 中,将 标识符 设置为
journalCell并按 Enter 键:

图 11.24:属性检查器中已设置标识符
文档大纲中的 表格视图单元格 名称将更改为 journalCell。
- 在 视图 下的属性检查器中,将 背景 设置为 系统青色,这样在运行应用时可以轻松看到表格视图单元格:

图 11.25:属性检查器中表格视图单元格背景色已设置
-
在项目导航器中点击 JournalListViewController 文件。在类声明之后输入以下代码,使
JournalListViewController类采用UITableViewDataSource和UITableViewDelegate协议:class JournalListViewController: UIViewController**,** **UITableViewDataSource****,** **UITableViewDelegate** {
几秒钟后,将出现一个错误:

图 11.26:编辑区域显示错误
- 点击它以显示错误信息。错误信息显示 类型‘JournalListViewController’不符合协议‘UITableViewDataSource’。添加符合性的占位符:

图 11.27:显示错误信息的编辑区域
这意味着你需要实现 UITableViewDataSource 协议的必需方法,以使 JournalListViewController 符合该协议。
-
点击 修复 以自动将必需方法的占位符添加到
JournalListViewController类中。 -
验证两个必需的
UITableViewDataSource协议方法占位符已自动插入到JournalListViewController类中,如图所示:

图 11.28:显示 UITableViewDataSource 方法占位符的编辑区域
第一个方法告诉表格视图显示多少个单元格,而第二个方法告诉表格视图在每个表格视图单元格中显示什么。
- 将第一个方法中的占位符文本替换为
10(如果只是一行代码,则return关键字是可选的)。这告诉表格视图显示 10 个单元格:

图 11.29:显示显示 10 个表格视图单元格的代码的编辑区域
-
将第二个方法中的占位符文本替换为以下代码:
tableView.dequeueReusableCell(withIdentifier: "journalCell", for: indexPath)

图 11.30:显示为每行显示一个表格视图单元格的代码的编辑区域
不要担心这现在意味着什么,因为您将在第三部分“表格视图”中学习更多关于表格视图的内容。
- 构建并运行您的应用。模拟器将显示 10 个青色表格视图单元格的列,如图所示:

图 11.31:显示 10 个表格视图单元格的模拟器
正如您在第十章“设置用户界面”的应用程序游览中看到的那样,屏幕右上角应该有一个+按钮。您将在下一节中添加此按钮。
以模态方式呈现视图
期刊列表屏幕的导航栏可以配置为显示标题和按钮。您已经在第十章“设置用户界面”中配置了标题。现在您将向导航栏添加并配置一个栏按钮项。当点击时,此按钮将显示一个表示添加新期刊条目屏幕的视图。此视图将来自一个嵌入在导航控制器中的新视图控制器场景,您将将其添加到项目中。视图将以模态方式呈现,这意味着在它被关闭之前,您将无法执行其他任何操作。
要关闭它,您将在视图的导航栏中添加一个取消按钮。您还将添加一个保存按钮,但您将在第十六章“在视图控制器之间传递数据”中实现其功能。让我们先在下一节中将库中的栏按钮项添加到导航栏中。
向导航栏添加栏按钮
正如第十章“设置用户界面”的应用程序游览中所示,屏幕右上角有一个+按钮。为了实现这一点,您将向期刊列表屏幕的导航栏添加一个栏按钮项。按照以下步骤操作:
- 在项目导航器中单击Main故事板文件。确保在文档大纲中选择第一个Journal Scene。单击库按钮以显示库:

图 11.32:显示库按钮的工具栏
- 在过滤器字段中键入
bar b。一个栏按钮项对象将出现在结果中。将栏按钮对象拖到导航栏的右侧:

图 11.33:选择栏按钮项对象的库
- 在选择栏按钮后,单击属性检查器按钮。在栏按钮项下,将系统项设置为添加:

图 11.34:将系统项设置为添加的属性检查器
你现在在导航栏中有一个 + 按钮。在下一节中,你将添加一个视图控制器场景来表示当按钮被点击时出现的添加新日记条目屏幕。
添加新的视图控制器场景
如 第十章 中应用程序浏览所示,在 设置用户界面 时,当你点击导航栏中的 + 按钮时,将显示添加新日记条目屏幕。你将在项目中添加一个新的视图控制器场景来表示此屏幕。按照以下步骤操作:
- 点击库按钮以显示库,并在过滤器字段中输入
view con。视图控制器 对象将在搜索结果中。将 视图控制器 对象拖放到故事板中:

图 11.35:库中已选择视图控制器对象
- 将视图控制器放置在 日记 场景的右侧:

图 11.36:显示视图控制器场景与日记场景并排的编辑区域
- 选择新添加的视图控制器场景。在文档大纲中,点击此场景的 视图控制器 图标:

图 11.37:选择视图控制器的文档大纲
-
你将需要为 取消 和 保存 按钮留出空间,因此你将在这个视图控制器场景中嵌入导航控制器以提供一个可以放置按钮的导航栏。从 编辑 菜单中选择 嵌入 | 导航控制器。
-
验证是否在视图控制器场景左侧出现了一个导航控制器场景:

图 11.38:显示嵌入在导航控制器中的视图控制器场景的编辑区域
- 在文档大纲中点击新视图控制器场景的 导航项。在属性检查器中,在 导航项 下,将 标题 设置为
新条目:

图 11.39:属性检查器中标题设置为“新条目”
导航项的名称将更改为 新条目。
- Ctrl + 拖动从按钮到导航控制器场景:

图 11.40:显示拖动目标的编辑区域
- 将出现切换弹出菜单。选择 以模态方式呈现:

图 11.41:选择“以模态方式呈现”的切换弹出菜单
当按钮被点击时,这会使视图控制器视图从屏幕底部向上滑动。在此视图关闭之前,你无法与其他任何视图进行交互。
- 验证是否有一个切换将 日记 场景和 导航控制器 场景连接在一起:

图 11.42:显示从日记场景到导航控制器场景的切换的编辑区域
- 构建并运行你的应用。点击 + 按钮,新的视图控制器视图将从屏幕底部向上滑动:

图 11.43:模拟器显示新的视图控制器视图
目前您只能通过向下拖动来取消此视图。在下一节中,您将在导航栏中添加一个取消按钮,并编程使其取消视图。您还会添加一个保存按钮,但暂时不会对其进行编程。
在导航栏中添加取消和保存按钮
如您之前所见,将视图控制器嵌入导航控制器的一个好处是屏幕顶部的导航栏。您可以在其左右两侧放置按钮。按照以下步骤将取消和保存按钮添加到导航栏:
- 点击文档轮廓中新条目场景的导航项。点击图书馆按钮:

图 11.44:显示图书馆按钮的工具栏
- 在过滤器字段中输入
bar b,并将栏按钮项对象拖到导航栏的每一侧:

图 11.45:选中栏按钮项对象的图书馆
- 点击右侧的项目按钮。在栏按钮项的属性检查器下,将样式设置为完成,并将系统项设置为保存:

图 11.46:将样式设置为完成并将系统项设置为保存的属性检查器
- 点击左侧的项目按钮,并将系统项设置为取消:

图 11.47:将系统项设置为取消的属性检查器
请记住,导航控制器有一个属性,viewControllers,它包含一个视图控制器数组。当您在期刊列表屏幕上点击+按钮时,新的视图控制器将被添加到viewControllers数组中,其视图从屏幕底部出现,覆盖期刊列表屏幕,唯一取消视图的方法是向下拖动。
-
要使取消按钮能够取消视图,您需要将取消按钮链接到场景退出,并在
JournalListViewController类中实现一个方法,该方法将在期刊列表屏幕重新出现时执行。在项目导航器中,点击JournalListViewController文件,并在文件底部在最后的闭合花括号之前添加以下方法:@IBAction func unwindNewEntryCancel(segue: UIStoryboardSegue) { } -
在项目导航器中,点击主故事板文件,并在新条目场景中点击取消按钮。在文档轮廓中,Ctrl + 拖动从取消按钮到场景退出图标,并从弹出菜单中选择unwindNewEntryCancelWithSegue::

图 11.48:文档轮廓显示正在设置取消按钮动作
当您的应用运行时,点击取消按钮将从导航控制器的viewControllers数组中移除视图控制器,取消所呈现的模态视图,并执行unwindNewEntryCancel(segue:)方法。请注意,此方法目前没有任何操作。
- 构建并运行你的应用程序,点击 Journal List 屏幕导航栏中的+按钮。新视图将出现。当你点击取消按钮时,新视图将消失:

图 11.49:模拟器显示的取消按钮
恭喜!你已经完成了 Journal List 屏幕的基本结构!
摘要
在本章中,你将表格视图添加到主 Storyboard 文件中的 Journal List 屏幕,并修改现有的视图控制器类以实现JournalListViewController类。然后,你修改了JournalListViewController类,使其在 Storyboard 中有一个表格视图出口,并使其成为表格视图的数据源和代理。最后,你添加了一个按钮来显示第二个视图,并配置了一个取消按钮来关闭它。
到目前为止,你应该已经熟练使用 Interface Builder 向 Storyboard 场景添加视图和视图控制器,将视图控制器出口链接到 Storyboard 中的 UI 元素,设置表格视图,以及以模态方式显示视图。当你设计自己的应用程序的用户界面时,这将非常有用。
在下一章中,你将实现应用程序的 Journal Entry Detail 屏幕,并为 Map 屏幕实现一个地图视图。
加入我们的 Discord!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,以及更多。扫描二维码或访问链接加入社区。
第十二章:完成用户界面
在上一章中,你配置了期刊列表屏幕以显示表格视图中的 10 个空表格视图单元格,向导航栏中添加了一个条形按钮项以模态方式呈现表示添加新期刊条目屏幕的视图,并添加了取消和保存按钮。
在本章中,你将添加第十章中应用程序之旅中显示的剩余屏幕。你将添加期刊条目详情屏幕,当在期刊列表屏幕中轻按表格视图单元格时将显示此屏幕。你将配置此屏幕以显示具有固定数量表格视图单元格的表格视图。你还将使地图屏幕显示地图。
到本章结束时,你将学会如何将具有固定单元格数的表格视图添加到故事板场景中,如何实现当在期刊列表屏幕中轻按单元格时显示屏幕的 segue,以及如何将地图视图添加到场景中。你的应用程序的基本用户界面将完整,你将能够在模拟器中遍历所有屏幕。所有屏幕都不会显示数据,但你将在本书的第三部分中完成它们的实现。
本章将涵盖以下主题:
-
实现期刊条目详情屏幕
-
将地图视图添加到地图屏幕
技术要求
你将继续在上一章中创建的JRNL项目中工作。
本章的完成 Xcode 项目位于本书代码包的Chapter12文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际效果:
首先,你将向故事板添加一个新的表格视图控制器场景来表示期刊条目详情屏幕。当在期刊列表屏幕中轻按单元格时,将显示此屏幕。你将在下一节中这样做。
实现期刊条目详情屏幕
如第十章中应用程序之旅所示,在设置用户界面时,当你轻按期刊列表屏幕中的期刊条目时,将出现包含该期刊条目详细信息的期刊条目详情屏幕。在本节中,你将在故事板中添加一个新的表格视图控制器场景来表示期刊条目详情屏幕。请按照以下步骤操作:
-
在项目导航器中单击主故事板文件,然后单击库按钮。
-
在过滤器字段中输入
table,并将表格视图控制器对象拖动到故事板中地图场景旁边:

图 12.1:显示表格视图控制器对象的库
这将代表期刊条目详情屏幕。
- 验证是否已添加表格视图控制器场景:

图 12.2:编辑区域显示地图场景旁边的表格视图控制器场景
注意,它已经包含了一个表格视图,因此您不需要在场景中添加表格视图,就像您在上一章中所做的那样。
- 要在期刊列表屏幕中的表格视图单元格被点击时显示期刊条目详情屏幕,请从期刊场景下的文档大纲中的journalCell(在journalCell中)拖动到表格视图控制器场景,以在它们之间添加导航:

图 12.3:显示期刊单元的文档大纲
- 在弹出菜单中,在选择导航下选择显示:

图 12.4:弹出菜单中已选择显示
这样,当期刊列表屏幕中的单元格被点击时,期刊条目详情屏幕将从右侧滑入。
- 验证两个场景之间是否出现了导航:

图 12.5:编辑区域显示期刊场景和表格视图控制器场景之间的导航
您可以在故事板中重新排列场景,以便更容易看到导航。
- 期刊条目详情屏幕将始终显示固定数量的单元格。在文档大纲中,点击表格视图控制器场景下的表格视图:

图 12.6:在文档大纲中选中表格视图
- 点击属性检查器按钮,将内容设置为静态单元格,以便使期刊条目详情屏幕显示固定数量的单元格。

图 12.7:内容设置为静态单元格的属性检查器
- 构建并运行您的应用程序。在期刊列表屏幕中点击一个单元格以显示期刊条目详情屏幕:

图 12.8:模拟器显示期刊条目详情屏幕
- 点击< 期刊按钮返回到期刊列表屏幕。
您已成功实现了期刊条目详情屏幕!太棒了!
如果您的应用程序需要在列表中显示项目的详细信息,可以使用此方法。例如,这是您 iPhone 上的联系人应用程序和设置应用程序的示例。
在下一节中,您将使地图屏幕显示地图。
实现地图屏幕
当您启动应用程序时,将显示期刊列表屏幕。在标签栏中轻触地图按钮将使地图屏幕出现,但它为空。要使地图屏幕显示地图,您需要在地图屏幕的视图控制器场景中添加一个地图视图。按照以下步骤操作:
- 在编辑区域中选择地图屏幕的视图控制器场景,这将展开文档大纲中的相应地图场景:

图 12.9:编辑区域显示地图场景的视图控制器场景
- 要使此场景显示地图,请点击库按钮,并在过滤器字段中输入
map。一个地图工具包视图对象作为结果之一出现。将其拖动到视图控制器场景中的视图中:

图 12.10:选中地图工具包视图对象的库
- 要使地图视图填充整个屏幕,请确认它已被选中,然后点击添加新约束按钮:

图 12.11:已选择地图视图的视图控制器场景
- 在所有最近邻居间距字段中输入
0,并确保选择了浅红色支撑(它们将变为鲜红色)。点击添加 4 个约束按钮:

图 12.12:自动布局添加新约束弹出对话框
- 确认地图视图填充了整个屏幕:

图 12.13:地图视图填充屏幕的视图控制器场景
- 构建并运行你的应用。点击地图按钮。你应该看到与这里显示的类似地图:

图 12.14:模拟器显示地图屏幕
- 确认所有为
JRNL应用所需的屏幕已在主故事板文件中创建:

图 12.15:编辑区域显示 Main.storyboard 中的所有场景
- 确认当你在模拟器中运行你的应用时,所有屏幕都显示得如预期。
太棒了!你现在已经完成了应用的基本用户界面!
摘要
在本章中,你完成了应用的基本结构。你为表示日记条目详情屏幕添加了一个新的表格视图控制器场景,为该屏幕配置了一个静态单元格的表格视图,并实现了当在日记列表屏幕中点击单元格时将显示此屏幕的转场。你还为地图屏幕的视图控制器场景添加了地图视图,当点击地图按钮时,它现在会显示地图。
你已成功实现了应用所需的全部屏幕,当你运行模拟器中的应用时,你将能够测试应用流程。你也应该更加熟练地使用 Interface Builder。熟悉从库中使用和定位对象对于你构建自己的应用的用户界面将至关重要。
在下一章中,你将修改日记列表屏幕、添加新日记条目屏幕和日记条目详情屏幕上的单元格,以便它们与在应用游览中显示的设计相匹配。
加入我们的 Discord!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第十三章:修改应用程序屏幕
在第十一章,构建你的用户界面中,你添加了一些应用程序所需屏幕,以匹配应用程序浏览中显示的内容。在第十二章,完成你的用户界面中,你添加了应用程序所需的其他屏幕。现在,当你运行模拟器中的应用程序时,你将能够导航到应用程序的所有屏幕,但屏幕仍然缺少数据输入和数据显示所需的用户界面元素。
在本章中,你将向期刊列表、添加新期刊条目和期刊条目详情屏幕添加和配置缺失的用户界面元素,以匹配应用程序浏览中的设计。
对于期刊列表屏幕,你需要通过向其中添加一个图像视图和两个标签来修改journalCell表格视图单元格,以便它可以显示期刊条目的照片、日期和标题。对于添加新期刊条目屏幕,你需要通过添加自定义视图、开关、文本字段、文本视图和图像视图来修改它,以便你可以输入新期刊条目的详细信息。你还需要配置图像视图以显示默认图像。对于期刊条目详情屏幕,你需要在其中添加文本视图、标签和图像视图,并配置图像视图以显示默认图像,以便屏幕可以显示现有期刊条目的详细信息。在所有用户界面元素就位后,你的应用程序将准备好进行代码实现,这些代码将在本书的第三部分中实现。
到本章结束时,你将更加熟练地添加和定位用户界面元素,并将获得更多使用约束来确定它们相对位置的经验。这将有助于确保与不同屏幕尺寸和方向的兼容性,使你能够轻松地原型化应用程序的外观和流程。
本章将涵盖以下主题:
-
修改期刊列表屏幕
-
修改添加新期刊条目屏幕
-
修改期刊条目详情屏幕
技术要求
你将继续在上一章中修改的JRNL项目中工作。
本章完成的 Xcode 项目位于本书代码包的Chapter13文件夹中,可以通过以下链接下载:
github.com/PacktPublishing/iOS-17-Programming-for-Beginners-Eighth-Edition
查看以下视频以查看代码的实际效果:
让我们从修改期刊列表屏幕上的journalCell表格视图单元格开始。在下一节中,你将添加一些用户界面元素,使其与应用程序浏览中显示的表格视图单元格相匹配。
修改期刊列表屏幕
让我们看看应用程序浏览中期刊列表屏幕的样子:

图 13.1:完成的 JRNL 应用程序的期刊列表屏幕
如您所见,日记列表屏幕上的表视图单元格有一个照片、一个日期和一个日记条目标题。在 第十一章,构建您的用户界面 中,您为 journalCell 表视图单元格设置了青色背景,并配置了表视图以显示 10 个单元格的列。现在您将移除背景颜色,并将用户界面元素添加到 journalCell 表视图单元格中,以匹配应用导览中的设计。您将从下一节开始向其中添加图像视图。
添加到 journalCell 的图像视图
图像视图是 UIImageView 类的一个实例。它可以在您的应用中显示单个图像或一系列动画图像。要将图像视图添加到 journalCell 表视图单元格中,请按照以下步骤操作:
- 为了在将用户界面元素添加到故事板时更容易看到它们,从 编辑器 菜单中选择 画布 | 边界矩形:

图 13.2:选择 Canvas | Bounds Rectangles 的编辑器菜单
这将应用一个薄薄的蓝色轮廓到故事板中的用户界面元素。
- 在项目导航器中点击 主故事板文件。在第一个 日记场景 下,在文档大纲中选择 journalCell:

图 13.3:显示 journalCell 的文档大纲
- 在添加图像视图之前,您需要移除之前设置的背景颜色。在属性检查器中,在 视图 下,将 背景 设置为 默认:

图 13.4:journalCell 的属性检查器设置
- 要将图像视图添加到表视图单元格中,点击 库 按钮。在过滤器字段中输入
imag。一个 图像视图 对象将出现在结果中。将其拖动到原型单元格中:

图 13.5:添加了图像视图的原型单元格
- 为了确保新添加的图像视图的约束可以正确设置,请验证它是否是 journalCell 表视图单元格的 内容视图 的子视图,并且已被选中:

图 13.6:选择 Image View 对象的文档大纲
-
点击 添加新约束 按钮,并输入以下值以设置新添加的图像视图的约束:
-
顶部:
0 -
左侧:
0 -
底部:
0 -
宽度:
90 -
高度:
90
-
完成后,点击 添加 5 个约束 按钮。

图 13.7:添加新约束对话框
这将图像视图的顶部、左侧和底部边缘绑定到 journalCell 表视图单元格的相应边缘,并将其宽度和高度设置为 90 点。它还隐式地将表视图单元格的高度设置为 90 点。
- 在属性检查器中,在 图像视图 下,将 图像 设置为
face.smiling:

图 13.8:将 Image 设置为 face.smiling 的图像视图
您已成功将图像视图添加到表视图单元格中,设置了其默认图像,并应用了约束以确定其相对于封装视图的位置。太酷了!
在下一节中,您将添加用于显示日记条目日期和标题的用户界面元素。
向 journalCell 添加标签
您将使用标签在 journalCell 表视图单元格中显示日期和日记条目标题。标签是 UILabel 类的实例。它可以在您的应用中显示一行或多行文本。
要向 journalCell 表视图单元格添加标签,请按照以下步骤操作:
- 首先,您将添加一个标签来显示日期。点击库按钮,并将一个 标签 对象拖动到您刚刚添加的图像视图和原型单元格右侧之间的空间:

图 13.9: 选择标签对象的库
注意,标签 出现在文档大纲中,并且是 journalCell 表视图单元格的 内容视图 的子视图。
- 在属性检查器中,在 标签 下,使用 字体 菜单将 字体 设置为 标题 1:

图 13.10: 标签属性检查器
-
点击 添加新约束 按钮,并输入以下值以设置标签的约束:
-
顶部:
0 -
左侧:
8 -
右侧:
0
-
约束到边距 应已选中,这会将标准边距 8 点设置为标签顶部和右侧以及表格视图单元格顶部和右侧之间的空间。完成时,点击 添加 3 个约束 按钮。
- 验证标签的位置是否如图下截图所示,以及新添加的约束是否在文档大纲中:

图 13.11: 应用了约束的标签
标签顶部边缘与 journalCell 内容视图顶部边缘之间的空间设置为 0 + 8 点。标签左侧边缘与图像视图右侧边缘之间的空间为 8 点。标签右侧边缘与 journalCell 内容视图右侧边缘之间的空间为 0 + 8 点。标签底部边缘的位置由您之前设置的文本样式自动设置。
接下来,您将添加一个标签来显示日记条目标题。按照以下步骤操作:
- 点击库按钮,并将一个 标签 对象拖动到您刚刚添加的标签和原型单元格底部的空间之间:

图 13.12: 选择标签对象的库
注意,标签 出现在文档大纲中,并且是 journalCell 表视图单元格的 内容视图 的子视图。
- 在属性检查器中,在 标签 下,使用 字体 菜单将 字体 设置为 正文,并将 线条 设置为
2:

图 13.13: 标签属性检查器
将 线条 设置为 2 将使标签在应用运行时显示最多两行文本。
-
点击 添加新约束 按钮,并输入以下值以设置标签的约束:
-
顶部:
0 -
左侧:
8 -
右侧:
0
-
约束到边距应该已经勾选,这会将标准边距8点设置为标签右侧和表格视图单元格右侧之间的空间。完成时,点击添加 3 个约束按钮。
- 确认标签的位置如图下截图所示,并且新添加的约束在文档轮廓中:

图 13.14:应用约束的标签
标签顶部边缘和之前添加的标签底部边缘之间的空间设置为0点。标签左侧边缘和图像视图右侧边缘之间的空间是8点。标签右侧边缘和journalCell内容视图右侧边缘之间的空间是0 + 8点。标签底部边缘的位置由你之前设置的文本样式和行数自动设置。
你可以在文档轮廓中点击一个约束,并在大小检查器中修改它。
- 构建并运行你的应用:

图 13.15:模拟器显示的完成后的 journalCell 表格视图单元格
你已成功添加并配置了标签以显示journalCell表格视图单元格的日期和日记条目标题,并且已添加所有必要的约束。正如你所见,日记列表屏幕现在具有在应用浏览中显示数据所需的所有用户界面元素。太棒了!
在下一节中,你将在“添加新日记条目”屏幕中添加包含用户界面元素的堆叠视图。
修改“添加新日记条目”屏幕
让我们看看在应用浏览中“添加新日记条目”屏幕看起来是什么样子:

图 13.16:完成后的日记应用“添加新日记条目”屏幕
苹果提供了一整套用户界面元素库,你可以在自己的应用中使用。这有助于使所有 iOS 应用具有一致的外观和感觉。正如你所见,添加新日记条目屏幕具有以下元素:
-
一个显示星级评分的自定义视图
-
一个开关,允许你获取当前位置
-
一个用于日记条目标题的文本字段
-
一个用于日记条目主体的文本视图
-
一个用于用手机相机拍照的图片视图
现在,你将修改屏幕以匹配应用浏览中的设计,从下一节开始,通过添加一个允许用户设置星级评分的自定义视图。
将自定义视图添加到新条目场景
正如你在应用浏览中所见,添加新日记条目屏幕有一个显示星级评分的自定义视图。这个自定义视图是水平堆叠视图的子类。你将在本章中添加水平堆叠视图,并在第十九章,开始使用自定义视图中完成自定义视图的实现。
栈视图是UIStackView类的一个实例。它允许你轻松地在一列或一行中排列一组视图。要将栈视图添加到“添加新日志条目”屏幕,请按照以下步骤操作:
- 在主故事板文件中,点击文档大纲中的新条目场景:

图 13.17:编辑区域显示新条目场景
- 要向场景添加水平栈视图,请点击库按钮。在过滤器字段中输入
hori。一个水平栈视图对象将出现在结果中。将其拖动到新条目场景的视图中:

图 13.18:选择水平栈视图对象的库
注意,你刚刚添加的栈视图出现在文档大纲中,并且是新条目场景视图的子视图。
- 点击属性检查器。在栈视图下,如果尚未设置,将间距设置为
8,在视图下,将背景设置为系统青色:

图 13.19:属性检查器显示间距和背景设置
间距值决定了栈视图中元素之间的间距。
- 点击大小检查器。在视图下,将宽度设置为
252,将高度设置为44:

图 13.20:大小检查器显示栈视图的大小
显示星级评分的自定义视图将包含五个按钮。每个按钮的高度为44点,宽度为44点,按钮之间的间距为8点。自定义视图的总宽度将是5 x 44 + 4 x 8,总宽度为252点。
-
点击添加新约束按钮,并输入以下值以设置栈视图的约束:
-
宽度:
252 -
高度:
44
-
完成后,点击添加 2 个约束按钮。
在大小检查器中设置 UI 元素的尺寸,使你稍后添加约束更容易,因为预期的值已经设置在添加新约束对话框中。
- 你会看到栈视图被红色勾勒出来。点击文档大纲中的小红粉箭头。

图 13.21:文档大纲中的箭头
- 你将在文档大纲中看到两个缺少约束错误:

图 13.22:显示缺少约束错误的错误信息
由于缺少约束错误,栈视图被红色勾勒出来。这意味着栈视图相对于其封装视图的位置目前是不明确的。你将在稍后将其嵌入另一个栈视图时修复这个问题。
- 点击< 结构按钮返回。
你已成功将水平栈视图添加到新条目场景。在下一节中,你将向其中添加一个 UI 元素,允许你获取当前位置。
向新条目场景添加开关
如应用程序导游所示,在创建新日记条目时,您可以通过切换开关来获取您的当前位置。开关是UISwitch类的实例。它显示一个提供二元选择的控件,例如开/关。您还将添加一个标签来描述开关的功能,并将这两个对象放入水平堆叠视图中。
按照以下步骤操作:
- 要将开关添加到新条目场景,单击库按钮。在过滤器字段中输入
swi。结果中会出现一个开关对象。将其拖动到水平堆叠视图下的新条目场景视图:

图 13.23:选择开关对象的库
注意,您刚刚添加的开关出现在文档大纲中,并且是新条目场景视图的子视图。
- 要在开关旁边添加标签,从库中拖动一个标签对象并将其放置在开关旁边:

图 13.24:选择标签对象的库
注意,您刚刚添加的标签出现在文档大纲中,并且也是新条目场景视图的子视图。
将出现蓝色线条以帮助您将标签放置在开关的正确距离处。
- 双击标签并更改标签文本为“获取位置”:

图 13.25:标签文本更改为“获取位置”
- 您将把标签和开关都嵌入到水平堆叠视图中。在文档大纲中,按住Shift键,单击开关,然后单击获取位置以选择开关和标签:

图 13.26:显示已选择标签和开关的文档大纲
- 从编辑器菜单中选择嵌入|堆叠视图。

图 13.27:选择“嵌入”|“堆叠视图”的编辑器菜单
开关和标签现在都嵌入在堆叠视图中。
- 单击属性检查器按钮,在堆叠视图下将间距设置为
8:

图 13.28:设置间距为 8 的属性检查器
您已成功将包含开关和标签的水平堆叠视图添加到新条目场景。在下一节中,您将向其中添加 UI 元素,以便用户可以输入日记标题和正文。
将文本字段和文本视图添加到新条目场景
如应用程序导游所示,用户将使用此屏幕输入日记条目的标题和正文文本。要输入文本,您可以使用文本字段或文本视图。文本字段是UITextField类的实例。它显示一个可编辑的文本区域,通常限制为单行。您将使用文本字段输入日记条目的标题。文本视图是UITextView类的实例。它也显示一个可编辑的文本区域,但通常显示多行。您将使用文本视图输入日记条目的正文。
要将文本字段和文本视图添加到新条目场景,请按照以下步骤操作:
- 要将文本字段添加到场景中,点击库按钮。在过滤器字段中输入
text。一个文本字段对象将出现在结果中。将其拖动到包含开关和标签的水平堆叠视图下的新条目场景视图:

图 13.29:选择文本字段对象的库
注意您刚刚添加的文本字段出现在文档大纲中,并且是新条目场景视图的子视图。
- 在属性检查器中,在文本字段下,将占位符设置为
Journal Title:

图 13.30:将占位符设置为期刊标题的属性检查器
- 要将文本视图添加到场景中,点击库按钮。在过滤器字段中输入
text。一个文本视图对象将出现在结果中。将其拖动到您刚刚添加的文本字段下的新条目场景视图:

图 13.31:选择文本字段对象的库
验证您刚刚添加的文本视图是否出现在文档大纲中,并且是新条目场景视图的子视图。如果您愿意,也可以更改默认文本。
- 您将使用约束将文本视图的高度设置为默认值 128 点。选择文本视图后,点击添加新约束按钮并勾选高度约束。然后,点击添加 1 个约束按钮。文本视图现在将看起来像以下截图:

图 13.32:应用了约束的文本视图
注意文本视图周围的红色轮廓,因为它相对于封装视图的位置不明确。现在不用担心,因为您稍后会修复它。
您已成功将文本字段和文本视图添加到新条目场景。在下一节中,您将添加一个 UI 元素,允许用户在其中拍照并显示。
向新条目场景添加图像视图
如应用导览所示,用户可以使用设备相机为日记条目拍照。选定的照片将使用图像视图在添加新日记条目屏幕上显示。在第十一章,构建您的用户界面中,您已将图像视图添加到journalCell表格视图单元格。现在,您将向新条目场景添加图像视图。按照以下步骤操作:
- 要将图像视图添加到场景中,点击库按钮。在过滤器字段中输入
imag。一个图像视图对象将出现在结果中。将其拖动到文本视图下的新条目场景视图:

图 13.33:选择图像视图对象的库
注意您刚刚添加的图像视图出现在文档大纲中,并且是新条目场景视图的子视图。
-
点击大小检查器按钮。在视图下,将宽度和高度都设置为
200。 -
点击属性检查器按钮。在图像视图下,将图像设置为
face.smiling。 -
你将使用约束来设置图像视图的宽度和高度。点击 添加新约束 按钮,勾选 宽度 和 高度 约束(它们的值应已设置为
200)。之后,点击 添加 2 个约束 按钮。图像视图现在将看起来像以下截图:

图 13.34:应用约束的图像视图
注意图像视图周围的红色轮廓,因为它相对于包含视图的位置是不明确的。现在不用担心,因为稍后你会修复它。
新条目场景的所有用户界面元素都已添加。在下一节中,你将把它们全部嵌入到垂直堆叠视图中以解决定位问题。
在堆叠视图中嵌入用户界面元素
新条目 场现在拥有了所有必需的用户界面元素,但元素相对于包含视图的位置是不明确的。你将把所有元素嵌入到垂直堆叠视图中,并使用约束来解决定位问题。按照以下步骤操作:
- 在文档大纲中,选择你之前添加的所有用户界面元素,如图所示:

图 13.35:显示所选元素的文档大纲
-
从 编辑器 菜单中选择 嵌入 | 堆叠视图。
-
在文档大纲中选择 堆叠视图。在属性检查器中,在 堆叠视图 下,将 对齐 设置为 居中,并将 间距 设置为
8。 -
选择堆叠视图后,点击添加新约束按钮。输入以下值以设置堆叠视图的约束:
-
顶部:
20 -
左侧:
20 -
右侧:
20
-
约束到边距 应已勾选,这设置了标准的 8 点边距。完成后,点击 添加 3 个约束 按钮。堆叠视图现在将看起来像以下截图:

图 13.36:应用约束后的堆叠视图
注意所有红色线条都已消失。堆叠视图与包含视图顶部、右侧和左侧边缘之间的空间已设置为 20 + 8 点。堆叠视图底部边缘的位置将自动由它包含的所有元素的高度推导出来。
- 你会看到文本字段没有扩展到堆叠视图的全宽。为了修复这个问题,在文档大纲中选择 日记标题,然后点击添加新约束按钮。将右侧约束设置为
8,然后点击 添加 1 个约束 按钮。文本字段将扩展到几乎堆叠视图的全宽:

图 13.37:应用约束后的堆叠视图中的文本字段
- 注意文本视图也没有扩展到堆叠视图的全宽。在文档大纲中选择 文本视图,然后点击添加新约束按钮。将右侧约束设置为
8,然后点击 添加 1 个约束 按钮。文本视图将扩展到几乎堆叠视图的全宽:

图 13.38:应用约束的堆叠视图中的文本视图
所有定位问题现在都已解决。
- 构建并运行您的应用,然后轻触 + 按钮以显示添加新日记条目屏幕:

图 13.39:模拟器显示添加新日记条目屏幕
您已将所有必需的用户界面元素和约束添加到添加新日记条目屏幕。干得好!在下一节中,您将配置静态表格视图并向日记条目详情屏幕添加用户界面元素。
修改日记条目详情屏幕
让我们看看日记条目详情屏幕在应用导览中的样子:

图 13.40:完成后的日记应用日记条目详情屏幕
向上滚动可显示日记条目详情屏幕的剩余部分:

图 13.41:日记条目详情屏幕的其余部分
如您所见,日记条目详情屏幕具有以下元素:
-
显示日期的标签
-
显示星级评分的自定义视图
-
日记条目标题的标签
-
日记条目正文的标签
-
一个用于您用手机相机拍摄的照片的图像视图
-
显示地图位置的图像视图
此外,您还需要滚动以查看整个屏幕。您现在将修改它以匹配应用导览中的设计,首先在下一节中设置表格视图单元格的数量和大小。
配置静态表格视图单元格的数量和大小
在 第十二章,完成您的用户界面 中,您已将表格视图控制器场景添加到 Main 故事板文件,并配置它使用固定数量的单元格。这将代表日记条目详情屏幕。现在,您将设置单元格的数量和大小以匹配应用导览中的布局。按照以下步骤操作:
- 在 Main 故事板文件中,在文档大纲中单击 Table View Controller Scene,然后单击 Navigation Item:

图 13.42:选择 Table View Controller Scene 的文档大纲
-
单击属性检查器按钮,在 Navigation Item 下,将 Title 设置为
Entry Detail。 -
在文档大纲中选择 Table View Section:

图 13.43:选择 Table View Section 的文档大纲
- 在 Table View Section 下的属性检查器中,将 Rows 设置为
7。

图 13.44:属性检查器显示 7 行
- 在文档大纲中的 Table View Section 下单击第二个 Table View Cell:

图 13.45:显示第二个 Table View Cell 的文档大纲
- 单击大小检查器按钮。在 Table View Cell 下,将 Row Height 设置为
60:

图 13.46:大小检查器显示行高设置为 60
-
在文档大纲中点击表格视图部分下的第四个表格视图单元格。在大小检查器中,在表格视图单元格下,将行高设置为
150。 -
在文档大纲中点击表格视图部分下的第五个表格视图单元格。在大小检查器中,在表格视图单元格下,将行高设置为
316。 -
重复之前的步骤,对第六个表格视图单元格进行操作。
你已经设置了单元格的数量和大小以匹配应用导览中显示的布局。在下一节中,你将为每个单元格添加用户界面元素。
向静态表格视图单元格添加用户界面元素
在上一节中,你使用了一个堆叠视图来帮助你管理添加新日志条目屏幕中的多个用户界面元素。这里,你将使用带有静态表格视图单元格的表格视图。使用静态表格视图的优势在于内置了视图滚动,因此它可以容纳比设备屏幕更高的视图。按照以下步骤操作:
-
在文档大纲中点击第一个表格视图单元格,并从库中将一个标签对象拖入其中。在属性检查器中,在标签下,使用字体菜单将样式设置为
Semibold并将对齐方式设置为右对齐。 -
点击添加新约束按钮,并将标签的顶部、左侧和右侧约束设置为
0。确保勾选了约束到边距,然后点击添加 3 个约束按钮。标签现在将看起来像以下截图:

图 13.47:应用了约束的标签
-
在文档大纲中点击第二个表格视图单元格,并从库中将一个水平堆叠视图对象拖入其中。在属性检查器中,在堆叠视图下,将间距设置为
8。在视图下,将背景设置为系统青色颜色。 -
点击大小检查器。在视图下,将宽度设置为
252和高度设置为44。 -
点击添加新约束按钮,并设置宽度和高度约束(它们的值应该已经存在)。点击添加 2 个约束按钮。
-
点击对齐按钮,勾选容器内水平对齐和容器内垂直对齐。点击添加 2 个约束按钮。

图 13.48:对齐按钮和对话框
- 堆叠视图现在将看起来像以下截图:

图 13.49:应用了约束的堆叠视图
-
点击第三个表格视图单元格,并从库中将一个标签对象拖入其中。在属性检查器中,在标签下,将样式设置为
Semibold并将对齐方式设置为左对齐。 -
点击添加新约束按钮,并将标签的顶部、左侧和右侧约束设置为
0。确保勾选了约束到边距,然后点击添加 3 个约束按钮。标签现在将看起来像以下截图:

图 13.50:应用了约束的标签
-
点击第四个表格视图单元格,从库中将一个文本视图对象拖入其中。在属性检查器中,在文本视图下取消选中可编辑和可选择。如果您愿意,也可以更改默认文本。
-
点击“添加新约束”按钮,并将顶部、左侧、右侧和底部约束设置为
0。确保勾选约束到边距,并点击“添加 4 个约束”按钮。文本视图现在将看起来像以下截图:

图 13.51:应用约束的文本视图
-
点击第五个表格视图单元格,从库中将一个图像视图对象拖入其中。在属性检查器中,在图像视图下将图像设置为
face.smiling。 -
在大小检查器中,在视图下将宽度和高度设置为
300。 -
点击“添加新约束”按钮,并设置宽度和高度约束(它们的值应该已经存在)。点击“添加 2 个约束”按钮。
-
点击对齐按钮,勾选在容器中水平对齐和在容器中垂直对齐。点击“添加 2 个约束”按钮。图像视图现在将看起来像以下截图:

图 13.52:应用约束的图像视图对象
-
点击第六个表格视图单元格,从库中将一个图像视图对象拖入其中。在属性检查器中,在图像视图下将图像设置为
map。 -
对此图像视图重复步骤 13到15。现在它将看起来像以下截图:

图 13.53:应用约束的图像视图对象
- 所有必需的用户界面元素都已添加到条目详情场景中。构建并运行您的应用,并在“日记列表”屏幕上点击一行以导航到“日记条目详情”屏幕:

图 13.54:模拟器显示“日记条目详情”屏幕
- 滚动查看“日记条目详情”屏幕的剩余部分。

图 13.55:模拟器显示“日记条目详情”屏幕的剩余部分
太棒了!您已经修改了应用的所有屏幕,并准备好在本书的下一部分中添加它们的功能。
摘要
在本章中,您修改了“日记列表”、“添加新日记条目”和“日记条目详情”屏幕,以匹配应用导览中显示的设计。对于“日记列表”屏幕,您通过添加图像视图和两个标签来修改journalCell表格视图单元格。您通过添加自定义视图、开关、文本字段、文本视图和图像视图来修改“添加新日记条目”屏幕,并配置图像视图以显示默认图像。对于“日记条目详情”屏幕,您添加了文本视图、标签和图像视图,并配置图像视图以显示默认图像。
您现在在如何使用 Interface Builder 添加和配置多个用户界面元素、使用大小检查器设置它们的大小和位置以及使用添加新约束和对齐按钮应用必要的约束方面有了更多的经验。这将有助于确保与不同屏幕尺寸和方向的兼容性。您还应该能够轻松地原型化您应用的界面和流程。
您现在已经完成了故事板和设计设置。您可以浏览应用中的每一个屏幕,看看它们的样子,尽管这些屏幕中没有任何实际数据。如果这个应用就像一座正在建造的房子,那么您就像是已经建好了所有的墙壁和地板,房子现在可以开始内部装修了。干得好!
这本书的第二部分到此结束。在下一部分,你将开始输入使你的应用工作的所有必需代码。在下一章,你将开始学习更多关于模型-视图-控制器设计模式的知识。你还将了解表格视图是如何工作的,这对于理解“期刊列表”屏幕的工作方式至关重要。
留下您的评价!
感谢您从 Packt Publishing 购买这本书——我们希望您喜欢它!您的反馈对我们来说是无价的,它帮助我们改进和成长。一旦您阅读完毕,请花一点时间在亚马逊上留下评价;这只需要一分钟,但对于像您这样的读者来说意义重大。扫描下面的二维码或访问链接,以获得您选择的免费电子书。
第三部分
代码
欢迎来到本书的第三部分。随着您的用户界面完成,您现在将添加代码以实现应用程序的功能。要显示列表中的数据,您将使用表格视图,您将学习如何使用数组作为数据源。接下来,您将学习如何从视图控制器传递数据到用作数据源的数组,以及从一个视图控制器传递数据到另一个视图控制器。您还将了解如何确定设备位置以及如何显示包含注释的地图。之后,您将学习如何使用 JSON 文件持久化应用程序数据。然后,您将学习如何创建自定义视图,使用设备相机和照片库,以及向表格视图添加搜索功能。最后,您将用集合视图替换表格视图,使您的应用程序适合更大的屏幕,如 Mac 或 iPad。
本部分包括以下章节:
-
第十四章,开始使用 MVC 和表格视图
-
第十五章,将数据放入表格视图
-
第十六章,在视图控制器之间传递数据
-
第十七章,开始使用 Core Location 和 MapKit
-
第十八章,开始使用 JSON 文件
-
第十九章,开始使用自定义视图
-
第二十章,开始使用相机和照片库
-
第二十一章,开始使用搜索
-
第二十二章,开始使用集合视图
到本部分的结尾,您将完成 JRNL 应用程序。您将拥有从头开始构建完整应用程序的经验,这对于您构建自己的应用程序将非常有用。让我们开始吧!
第十四章:开始使用 MVC 和表格视图
在上一章中,你修改了“期刊列表”屏幕、“添加新期刊条目”屏幕和“期刊条目详情”屏幕,以匹配第十章中显示的应用程序导游,“设置用户界面”。你已经完成了JRNL应用的初始 UI,这标志着本书第二部分的结束。
本章开始本书的第三部分,在这一部分中,你将专注于使你的应用工作的代码。在本章中,你将学习模型-视图-控制器(MVC)设计模式以及应用的不同部分如何相互交互。然后,你将使用沙盒程序以编程方式实现一个表格视图(这意味着使用代码而不是故事板来实现),以了解表格视图是如何工作的。最后,你将回顾在“期刊列表”屏幕上实现的表格视图,以便你可以看到在故事板中实现它和在编程中实现它的区别。
到本章结束时,你将理解模型-视图-控制器(MVC)设计模式,你将学会如何以编程方式创建表格视图控制器,并且将知道如何使用表格视图代理和数据源协议。
本章将涵盖以下主题:
-
理解模型-视图-控制器(MVC)设计模式
-
理解表格视图
-
回顾“期刊列表”屏幕
技术要求
本章的完成版 Xcode 沙盒程序和项目位于本书代码包的Chapter14文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际效果:
创建一个新的沙盒程序,并将其命名为TableViewBasics。你可以使用这个沙盒程序在阅读本章时输入和运行所有代码。在你这样做之前,让我们看看模型-视图-控制器(MVC)设计模式,这是一种在编写 iOS 应用时常用的方法。
理解模型-视图-控制器(MVC)设计模式
模型-视图-控制器(MVC)设计模式是构建 iOS 应用时常用的一个常见方法。MVC 将应用分为三个不同的部分:
-
模型:这处理数据存储和表示以及数据处理任务。
-
视图:这包括屏幕上用户可以与之交互的所有内容。
-
控制器:这管理模型和视图之间信息流的流程。
MVC 的一个显著特点是视图和模型不会相互交互;相反,所有通信都由控制器管理。
例如,想象您在一家餐厅。您查看菜单并选择您想要的东西。然后,服务员过来,接收您的订单,并将其发送给厨师。厨师准备您的订单,当它完成时,服务员取走订单并将其带给您。在这个场景中,菜单是视图,服务员是控制器,厨师是模型。此外,请注意,您与厨房之间的所有交互都只通过服务员进行;您与厨师之间没有交互。
要了解更多关于 MVC 的信息,请访问 developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html。
要了解 MVC 在 iOS 应用程序中的工作原理,让我们先更多了解一下视图控制器。您将看到实现一个视图控制器所需的内容,这个控制器需要管理用于 Journal List 屏幕的表格视图。
探索视图控制器
到目前为止,您已经实现了 JournalListViewController,这是一个管理 Journal List 屏幕上表格视图的视图控制器。然而,您还没有学习到您添加到其中的代码是如何工作的,所以现在让我们来看看。
您可能希望重新阅读 第十一章,构建您的用户界面,其中您实现了 JournalListViewController 类。
当 iOS 应用程序启动时,将加载要显示的第一个屏幕的视图控制器。视图控制器有一个 view 属性,并自动加载分配给其 view 属性的视图实例。该视图可能有子视图,这些子视图也会被加载。
如果其中一个子视图是表格视图,它将具有 dataSource 和 delegate 属性。dataSource 属性被分配给一个为表格视图提供数据的对象。delegate 属性被分配给一个处理与表格视图用户交互的对象。通常,表格视图的视图控制器将被分配给表格视图的 dataSource 和 delegate 属性。
表格视图将向其视图控制器发送的方法调用在 UITableViewDataSource 和 UITableViewDelegate 协议中声明。请记住,协议只提供方法声明;这些方法调用的实现是在视图控制器中。然后,视图控制器将从模型对象中获取数据,并将其提供给表格视图。视图控制器还处理用户输入,并根据需要修改模型对象。
在下一节中,我们将更详细地了解表格视图和表格视图协议。
理解表格视图
JRNL 应用在 Journal List 屏幕上使用表格视图。表格视图通过单列排列的行来展示表格视图单元格。
要了解更多关于表格视图的信息,请访问 developer.apple.com/documentation/uikit/uitableview。
表视图显示的数据通常由视图控制器提供。为表视图提供数据的视图控制器必须遵守UITableViewDataSource协议。该协议声明了一系列方法,告诉表视图显示多少个单元格以及每个单元格中显示什么内容。
要了解更多关于UITableViewDataSource协议的信息,请访问developer.apple.com/documentation/uikit/uitableviewdatasource。
为了启用用户交互,表视图的视图控制器还必须遵守UITableViewDelegate协议,该协议声明了一系列在用户与表视图交互时触发的方法。
要了解更多关于UITableViewDelegate协议的信息,请访问developer.apple.com/documentation/uikit/uitableviewdelegate。
要了解表视图是如何工作的,你将在TableViewBasics游乐场中实现一个控制表视图的视图控制器子类。由于游乐场中没有故事板,你不能像前几章那样使用库添加 UI 元素。相反,你将一切通过编程实现。
你将首先创建TableViewExampleController类,这是一个管理表视图的视图控制器的实现。之后,你将创建一个TableViewExampleController实例,并在游乐场的实时视图中显示一个表视图。按照以下步骤操作:
-
打开你创建的
TableViewBasics游乐场,删除var语句,并添加import PlaygroundSupport语句。现在你的游乐场应该包含以下内容:import UIKit **import** **PlaygroundSupport**
第一个import语句导入了创建 iOS 应用的 API。第二个语句使游乐场能够显示实时视图,你将使用它来显示表视图。
-
在
import语句之后添加以下代码以声明TableViewExampleController类:class TableViewExampleController: UIViewController { }
这个类是UIViewController的子类,UIViewController是苹果提供的一个用于管理屏幕上视图的类。
-
在
TableViewExampleController类中,在大括号内添加以下代码以声明一个表视图属性和一个数组属性:class TableViewExampleController: UIViewController { **var****tableView****: UITableView?** **var****journalEntries****: [[****String****]]** **=** **[** **[****"sun.max"****,****"12 Sept 2024"****,****"Nice weather today"****],** **[****"cloud.rain"****,****"13 Sept 2024"****,****"Heavy rain today"****],** **[****"cloud.sun"****,****"14 Sept 2024"****,****"It's cloudy out"****]** **]** }
tableView属性是一个可选属性,它将被分配一个UITableView实例。journalEntries数组是用于向表视图提供数据的模型对象。
你刚刚声明并定义了TableViewExampleController类的初始实现。太棒了!在下一节中,你将学习如何设置表视图显示的单元格数量,如何设置每个单元格的内容,以及如何从表视图中删除一行。
遵守UITableViewDataSource协议
表视图使用单列排列的行来呈现表视图单元格。然而,在它能够这样做之前,它需要知道要显示多少个单元格以及每个单元格中要放置什么内容。为了向表视图提供这些信息,你将使TableViewExampleController类遵守UITableViewDataSource协议。
此协议有两个必需的方法:
-
tableview(_:numberOfRowsInSection:)由表视图调用,以确定要显示多少个表视图单元格。 -
tableView(_:cellForRowAt:)由表视图调用,以确定在每个表视图单元格中显示什么。
UITableViewDataSource协议还有一个可选方法,tableView(_:commit:forRowAt:),当用户在行上向左滑动时,表视图会调用此方法。你将使用此方法来处理用户想要删除行时发生的情况。
让我们添加一些代码来使TableViewExampleController类遵守UITableViewDataSource协议。按照以下步骤操作:
-
要使
TableViewExampleController类采用UITableViewDataSource协议,在超类名称后输入一个逗号,然后输入UITableViewDataSource。你的代码应该看起来像这样:class TableViewExampleController: UIViewController**, UITableViewDataSource** { -
由于你没有实现两个必需的方法,将出现错误。点击错误图标:

图 14.1:显示错误图标的编辑区域
错误信息表明TableViewExampleController类没有遵守UITableViewDataSource协议。
- 点击修复按钮以添加使类遵守协议所需的存根:

图 14.2:错误解释和修复按钮
-
确认你的代码看起来像这样:
class TableViewExampleController: UIViewController, UITableViewDataSource { **func****tableView****(****_****tableView****: UITableView,** **numberOfRowsInSection** **section****:** **Int****) ->** **Int** **{** **code** **}** **func****tableView****(****_****tableView****: UITableView,** **cellForRowAt** **indexPath****: IndexPath) -> UITableViewCell {** **code** **}** var tableView: UITableView? var journalEntries: [[String]] = [ ["sun.max","12 Sept 2024","Nice weather today"], ["cloud.rain","13 Sept 2024","Heavy rain today"], ["cloud.sun","14 Sept 2024","It's cloudy out"] ] } -
在类定义中,惯例规定属性应在任何方法声明之前在顶部声明。重新排列代码,使属性声明在顶部,如下所示:
class TableViewExampleController: UIViewController, UITableViewDataSource { **var****tableView****: UITableView?** **var****journalEntries****: [[****String****]]** **=** **[** **[****"sun.max"****,****"12 Sept 2024"****,****"Nice weather today"****],** **[****"cloud.rain"****,****"13 Sept 2024"****,****"Heavy rain today"****],** **[****"cloud.sun"****,****"****14 Sept 2024"****,****"It's cloudy out"****]** **]** func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { -
要使表视图为
journalEntries数组中的每个元素显示一行,请在tableView(_:numberOfRowsInSection:)方法定义内的代码占位符中单击,并输入journalEntries.count。完成的方法应如下所示:func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { **journalEntries****.count** }
journalEntries.count返回journalEntries数组中的元素数量。由于其中有三个元素,这将使表视图显示三行。
-
要使表视图在每个单元格中显示日记条目详情,请在
tableView(_:cellForRowAt:)方法定义内的代码占位符中单击,并输入以下内容:func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { **let** **cell** **=** **tableView.dequeueReusableCell(withIdentifier:** **"cell"****, for: indexPath)** **let** **journalEntry** **=****journalEntries****[indexPath.row]** **var** **content** **=** **cell.defaultContentConfiguration()** **content.image** **=** **UIImage(systemName: journalEntry[****0****])** **content.text** **=** **journalEntry[****1****]** **content.secondaryText** **=** **journalEntry[****2****]** **cell.contentConfiguration** **=** **content** **return** **cell** }
让我们分解一下:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
此语句创建一个新的表格视图单元格或重用现有的表格视图单元格,并将其分配给cell。想象一下,你需要在表格视图中显示 1,000 个项目。你不需要包含 1,000 个表格视图单元格的 1,000 行——你只需要足够多的来填满屏幕。滚动出屏幕顶部的表格视图单元格可以重用来显示屏幕底部的项目,反之亦然。由于表格视图可以显示多种类型的单元格,你将单元格重用标识符设置为"cell"以识别这种特定的表格视图单元格类型。此标识符稍后将注册到表格视图中。
let journalEntry = journalEntries[indexPath.row]
indexPath值用于定位表格视图中的行。第一行有一个包含分区0和行0的indexPath。indexPath.row对第一行返回0,因此此语句将journalEntries数组中的第一个元素分配给journalEntry。
var content = cell.defaultContentConfiguration()
默认情况下,UITableViewCell实例可以存储一个图像、一个文本字符串和一个次要文本字符串。你通过使用表格视图单元格的内容配置属性来设置这些属性。此语句检索表格视图单元格样式的默认内容配置并将其分配给一个变量content。
content.image = UIImage(systemName: journalEntry[0])
content.text = journalEntry[1]
content.secondaryText = journalEntry[2]
cell.contentConfiguration = content
这些语句使用journalEntry的详细信息更新content,journalEntry是一个包含三个元素的数组。第一个元素用于指定分配给image属性的图像。第二个元素分配给text属性。第三个元素分配给secondaryText属性。最后一行将content分配给表格视图单元格的contentConfiguration属性。
return cell
此语句返回表格视图单元格,然后将其显示在屏幕上。
tableView(_:cellForRowAt:)方法为表格视图中的每一行执行。
-
要处理表格视图单元格删除,在
tableView(_:cellForRowAt:)的实现之后输入以下代码:func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { journalEntries.remove(at: indexPath.row) tableView.reloadData() } }
这将移除与用户左滑的表格视图单元格对应的journalEntries元素,并重新加载表格视图。
TableViewExampleController类现在遵循UITableViewDataSource协议。在下一节中,你将使其遵循UITableViewDelegate协议。
遵循UITableViewDelegate协议
用户可以点击表格视图单元格来选择它。为了处理用户交互,你将使TableViewExampleController类遵循UITableViewDelegate协议。你将实现此协议的一个可选方法,即tableView(_:didSelectRowAt:),当用户点击行时,表格视图会调用此方法。按照以下步骤操作:
-
要使
TableViewExampleController类采用UITableViewDelegate协议,在类声明中UITableViewDataSource之后输入一个逗号,然后输入UITableViewDelegate。你的代码应该看起来像这样:class TableViewExampleController: UIViewController, UITableViewDataSource**, UITableViewDelegate** { -
在
tableView(_:commit:forRowAt:)的实现之后输入以下代码:func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedJournalEntry = journalEntries[indexPath.row] print(selectedJournalEntry) }
此方法将获取被点击行的journalEntries数组元素并将其打印到调试区域。
-
确认您的
TableViewExampleController类看起来如下:class TableViewExampleController: UIViewController, UITableViewDataSource, UITableViewDelegate { var tableView: UITableView? var journalEntries: [[String]] = [ ["sun.max","12 Sept 2024","Nice weather today"], ["cloud.rain","13 Sept 2024","Heavy rain today"], ["cloud.sun","14 Sept 2024","It's cloudy out"] ] func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { journalEntries.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "cell", for: indexPath) let journalEntry = journalEntries[indexPath.row] var content = cell.defaultContentConfiguration() content.image = UIImage(systemName: journalEntry[0]) content.text = journalEntry[1] content.secondaryText = journalEntry[2] cell.contentConfiguration = content return cell } func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { journalEntries.remove(at: indexPath.row) tableView.reloadData() } } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedjournalEntry = journalEntries[indexPath.row] print(selectedjournalEntry) } }
TableViewExampleController类现在符合UITableViewDelegate协议。
您已完成了TableViewExampleController类的实现。在下一节中,您将学习如何创建此类的实例。
创建TableViewExampleController实例
现在您已经声明并定义了TableViewExampleController类,您将编写一个方法来创建其实例。按照以下步骤操作:
-
在
journalEntries变量声明之后输入以下代码以声明一个新的方法:func createTableView() { }
这声明了一个新的方法createTableView(),您将使用它来创建表格视图的实例并将其分配给tableView属性。
-
在大括号开头之后输入以下代码:
tableView = UITableView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
这将创建一个新的表格视图实例并将其分配给tableView。
-
在下一行,输入以下代码以将表格视图的
dataSource和delegate属性设置为TableViewExampleController实例:tableView?.dataSource = self tableView?.delegate = self
表格视图的dataSource和delegate属性指定包含UITableViewDataSource和UITableViewDelegate方法实现的对象。
-
在下一行,输入以下代码以设置表格视图的背景颜色:
tableView?.backgroundColor = .white -
在下一行,输入以下代码以设置表格视图单元格的标识符为
"cell":tableView?.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
此标识符将在tableView(_:cellForRowAt:)方法中使用,以识别要使用的表格视图单元格的类型。
-
在下一行,输入以下代码以将表格视图添加为
TableViewExampleController实例视图的子视图:view.addSubview(tableView!) -
确认完成的方法如下:
func createTableView() { tableView = UITableView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)) tableView?.dataSource = self tableView?.delegate = self tableView?.backgroundColor = .white tableView?.register(UITableViewCell.self, forCellReuseIdentifier: "cell") view.addSubview(tableView!) }
现在您必须确定何时调用此方法。视图控制器有一个view属性。分配给view属性的视图将在视图控制器加载时自动加载。在视图成功加载后,视图控制器的viewDidLoad()方法将被调用。您将在TableViewControllerExample类中重写viewDidLoad()方法以调用createTableView()。在createTableView()方法之前输入以下代码:
override func viewDidLoad() {
super.viewDidLoad()
view.bounds = CGRect(x: 0, y: 0, width: 375,
height: 667)
createTableView()
}
这设置了实时视图的大小,创建了一个表格视图实例,将其分配给tableView,并将其添加为TableViewExampleController实例视图的子视图。然后表格视图调用数据源方法以确定要显示多少表格视图单元格以及每个单元格中显示的内容。tableView(_:numberOfRowsInSection:)返回journalEntries中的元素数量,因此将显示三个表格视图单元格。tableView(_:cellForRowAt:)创建单元格,创建一个新的单元格配置,设置单元格配置的属性,并将更新的配置分配给单元格。
确认你的完成后的游乐场看起来像这样:
import UIKit
import PlaygroundSupport
class TableViewExampleController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var tableView: UITableView?
var journalEntries: [[String]] = [
["sun.max","12 Sept 2024","Nice weather today"],
["cloud.rain","13 Sept 2024","Heavy rain today"],
["cloud.sun","14 Sept 2024","It's cloudy out"]
]
override func viewDidLoad() {
super.viewDidLoad()
view.bounds = CGRect(x: 0, y: 0, width: 375, height: 667)
createTableView()
}
func createTableView() {
tableView = UITableView(frame: CGRect(x: 0, y: 0,
width: view.frame.width,
height: view.frame.height))
tableView?.dataSource = self
tableView?.delegate = self
tableView?.backgroundColor = .white
tableView?.register(UITableViewCell.self,
forCellReuseIdentifier: "cell")
view.addSubview(tableView!)
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
journalEntries.count
}
func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:
"cell", for: indexPath)
let journalEntry = journalEntries[indexPath.row]
var content = cell.defaultContentConfiguration()
content.image = UIImage(systemName: journalEntry[0])
content.text = journalEntry[1]
content.secondaryText = journalEntry[2]
cell.contentConfiguration = content
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle:
UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
journalEntries.remove(at: indexPath.row)
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt
indexPath: IndexPath) {
let selectedjournalEntry = journalEntries[indexPath.row]
print(selectedjournalEntry)
}
}
现在是时候看到它的实际效果了。按照以下步骤操作:
-
在游乐场中所有其他代码之后输入以下内容:
PlaygroundPage.current.liveView = TableViewExampleController()
此命令创建TableViewExampleController的实例并在游乐场的实时视图中显示其视图。createTableView()方法将创建一个表格视图并将其添加为TableViewExampleController实例视图的子视图,它将显示在屏幕上。
- 要在屏幕上看到表格视图的表示,游乐场的实时视图必须启用。点击调整编辑选项按钮并确认实时视图被选中:

图 14.3:调整编辑选项菜单,选择“实时视图”
- 运行你的代码并验证表格视图是否在实时视图中显示三个表格视图单元格:

图 14.4:游乐场实时视图显示包含三个表格视图单元格的表格视图
- 点击一行。该行的期刊条目详情将在调试区域打印出来:

图 14.5:调试区域显示期刊条目详情
- 在一行上向左滑动。将出现一个删除按钮:

图 14.6:显示删除按钮的表格视图行
- 点击删除按钮从表格视图中移除行:

图 14.7:移除一行后的表格视图
你刚刚创建了一个表格视图的视图控制器,创建了一个实例,并在游乐场的实时视图中显示了一个表格视图。做得好!
在下一节中,你将回顾如何在第十一章,构建用户界面中实现的期刊列表屏幕上使用视图控制器。使用本节学到的内容作为参考,你应该能更好地理解它是如何工作的。
回顾期刊列表屏幕
记住第十一章,构建用户界面中的JournalListViewController类?这是一个管理表格视图的视图控制器示例。请注意,这个类的代码与你的游乐场中的代码非常相似。差异如下:
-
你通过编程在
TableViewExampleController中创建并分配了表格视图到tableView,而不是使用辅助工具。 -
你通过
UITableView(frame:)以编程方式设置表格视图的尺寸,而不是使用大小检查器和约束。 -
你通过编程将数据源和代理出口连接到视图控制器,而不是使用连接检查器。
-
你通过编程设置重用标识符和 UI 元素颜色,而不是使用属性检查器。
-
你通过编程将表格视图作为
TableViewExampleController视图的子视图添加,而不是从库中拖入一个表格视图对象。
你可能希望再次打开 JRNL 项目并回顾 第十一章,构建你的用户界面,以比较使用故事板实现的表格视图实现和你在本章中按编程方式实现的表格视图实现。
摘要
在本章中,你详细学习了 MVC 设计模式和表格视图控制器。然后你回顾了在“期刊列表”屏幕上使用的表格视图,并学习了表格视图控制器是如何工作的。
你现在应该已经理解了 MVC 设计模式,如何创建一个表格视图控制器,以及如何使用表格视图数据源和代理协议。这将使你能够为你的应用程序实现表格视图控制器。
到目前为止,你已经为“期刊列表”屏幕设置了视图和视图控制器,但它只显示一列单元格。在下一章中,你将实现“期刊列表”屏幕的模型对象,以便它可以显示期刊条目列表。为此,你将创建用于存储数据并提供给JournalListViewController实例的结构,以便它可以在“期刊列表”屏幕上的表格视图中显示。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第十五章:将数据放入表格视图中
在上一章中,你学习了模型-视图-控制器(MVC)设计模式和表格视图。你还回顾了期刊列表屏幕中的表格视图。此时,期刊列表屏幕显示的单元格不包含任何数据。如图 10 章“设置用户界面”中的应用游览所示,它应该显示期刊条目的列表。
在本章中,你将实现期刊列表屏幕的模型对象,使其显示期刊条目的列表。你将从了解你将使用的模型对象开始。然后,你将创建一个 Swift 类,可以存储期刊条目实例。之后,你将创建一个静态方法,可以返回样本期刊条目实例。然后,这个数组将用作期刊列表屏幕上表格视图的数据源。
到本章结束时,你将学会如何创建模型对象,如何创建样本数据,以及如何配置视图控制器以填充表格视图。
本章将涵盖以下主题:
-
理解模型对象
-
创建一个表示期刊条目的类
-
创建样本数据
-
在集合视图中显示数据
技术要求
你将继续在第十三章“修改应用屏幕”中修改的JRNL项目上工作。本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter15文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,以查看代码的实际运行情况:
让我们从下一节中需要存储期刊条目数据的模型对象开始分析。
理解模型对象
如你在第十四章“开始使用 MVC 和表格视图”中所学,iOS 应用的一个常见设计模式是模型-视图-控制器(Model-View-Controller),或简称 MVC。为了回顾,MVC 将应用分为三个不同的部分:
-
模型:这处理数据存储、表示和数据处理任务。
-
视图:这是用户可以与之交互的屏幕上的任何内容。
-
控制器:这管理模型和视图之间信息流的流动。
让我们回顾一下你在应用游览中看到的期刊列表屏幕的设计,它看起来像这样:

图 15.1:模拟器显示应用游览中的期刊列表屏幕
构建并运行你的应用,期刊列表屏幕将看起来像这样:

图 15.2:模拟器显示你的应用中的期刊列表屏幕
如您所见,所有单元格当前都显示占位符。根据 MVC 设计模式,您已部分完成所需视图(表格视图)和控制器(JournalListViewController类)的实现。您需要创建一个自定义的UITableViewCell实例来管理表格视图单元格将显示的内容,并且您需要添加模型对象,这些对象将提供日记条目数据。
每个日记条目将存储以下内容:
-
条目创建的日期
-
一个评分值
-
标题文本
-
正文文本
-
一个可选的照片
-
一个可选的地理位置
在第十四章,使用 MVC 和表格视图入门中,您使用了一个String数组的数组来表示日记条目。然而,String数组只能存储字符串,并且您必须能够存储除String之外的数据类型。为了解决这个问题,您将创建一个名为JournalEntry的类来存储日记条目所需的所有数据。接下来,您将创建一个静态方法,该方法返回存储在JournalEntry实例中的示例数据。之后,您将创建一个自定义的UITableView类来管理表格视图单元格显示的数据。最后,您将修改JournalListViewController类,使其能够为表格视图提供显示数据。
创建一个表示日记条目的类
要创建一个可以在您的应用中表示日记条目的模型对象,您需要在项目中添加一个新文件,JournalEntry.swift,并声明一个具有日记条目所需属性的JournalEntry类。在这样做之前,您需要配置项目以使用 Swift 6 并将主项目文件夹更改为组。请按照以下步骤操作:
- 在项目导航器中,单击JRNL图标。单击JRNL目标并单击构建设置。向下滚动到Swift 编译器 - 语言并将Swift 语言版本设置为Swift 6:

图 15.3:编辑区域显示已设置 Swift 6 的构建设置
- 在JRNL图标下的蓝色JRNL项目文件夹上右键单击并选择转换为组:

图 15.4:已选择“转换为组”的弹出菜单
文件夹颜色将从蓝色变为深灰色。将文件夹转换为组将允许您重新排列其中的文件顺序。
要了解更多关于 Xcode 中文件夹和组之间差异的信息,请访问:developer.apple.com/documentation/xcode/managing-files-and-folders-in-your-xcode-project。
- 在组中重新排序文件,直到它们看起来像下面的截图:

图 15.5:项目导航器显示重新排序的文件
- 在项目导航器中右键单击JournalListViewController文件,然后选择从选择新建组:

图 15.6:已选择“从选择新建组”的弹出菜单
这将创建一个包含JournalListViewController文件的新组。
- 将组名的占位文本替换为
Journal List Screen并按回车:

图 15.7:项目导航器显示日记列表屏幕组
- 现在将为用于日记列表屏幕的模型和视图对象创建组。右键单击日记列表屏幕组并选择新建组:

图 15.8:弹出菜单,选中“新建组”
- 将占位文本替换为
Model并按回车:

图 15.9:项目导航器显示模型组
- 通过重复步骤 3并替换占位文本为
View来创建另一个文件夹。项目导航器应该看起来像以下截图:

图 15.10:项目导航器显示视图和模型组
- 您现在将创建一个包含
JournalEntry类实现的文件。右键单击模型文件夹并选择从模板新建文件...:

图 15.11:弹出菜单,选中“从模板新建文件…”
- iOS应该已经选中。选择Swift 文件并点击下一步:

图 15.12:选择新文件的模板:选中 Swift 文件
- 将文件命名为
JournalEntry然后点击创建。文件将出现在项目导航器中,其内容将出现在编辑器区域:

图 15.13:编辑区域显示 JournalEntry 文件的内容
此文件中只有一行是import语句。
import语句允许您将其他代码库导入到项目中,从而让您能够使用它们中的类、属性和方法。Foundation 是苹果的核心框架之一,您可以在这里了解更多信息:developer.apple.com/documentation/foundation。
-
修改
import语句以导入UIKit:import **UIKit**UIKit提供了 iOS 应用所需的基础设施。您可以在这里了解更多信息:developer.apple.com/documentation/uikit。 -
在
import语句后添加以下代码以声明一个名为JournalEntry的类:class JournalEntry { } -
在
JournalEntry类的开头花括号后添加以下代码以添加此类的所需属性:class JournalEntry { **// MARK: - Properties** **let****date****:** **Date** **let****rating****:** **Int** **let****title****:** **String** **let****body****:** **String** **let****photo****:** **UIImage****?** **let****latitude****:** **Double****?** **let****longitude****:** **Double****?** }
让我们分解一下:
-
date属性的类型是Date,将存储日记条目创建的日期。 -
rating属性的类型是Int,将存储日记条目的星级数量。 -
title属性的类型是String,将存储日记条目的标题文本。 -
body属性的类型是String,将存储日记条目的正文文本。 -
photo属性的类型是UIImage?,将存储照片。这是一个可选属性,因为并非所有日记条目都需要照片。 -
lat和long属性的类型为Double?,并将存储日记条目创建的位置。这些是可选属性,因为并非所有日记条目都需要位置信息。
将出现错误,因为你的类没有初始化器。
-
在
longitude属性之后添加以下代码以实现初始化器:// MARK: - Initialization init?(rating: Int, title: String, body: String, photo: UIImage? = nil, latitude: Double? = nil, longitude: Double? = nil) { if title.isEmpty || body.isEmpty || rating < 0 || rating > 5 { return nil } self.date = Date() self.rating = rating self.title = title self.body = body self.photo = photo self.latitude = latitude self.longitude = longitude }类在 第七章,类、结构和枚举 中有所介绍。
让我们分解一下:
init?(rating: Int, title: String, body: String, photo: UIImage? = nil, latitude: Double? = nil, longitude: Double? = nil) {
JournalEntry 类的初始化器有 Int 类型的值、两个 String 类型的值、一个可选的 UIImage 类型的值和两个可选的 Double 类型的值作为参数。所有可选值的默认值都是 nil。init 关键字后面的问号表示这是一个 可失败初始化器;如果某些条件不满足,它将不会创建 JournalEntry 实例。
if title.isEmpty || body.isEmpty || rating < 0 || rating > 5 {
return nil
}
如果以下任一条件返回 true,初始化器将无法创建 JournalEntry 实例:title 为空,body 为空,rating 小于 0,以及 rating 大于 5。
self.date = Date()
当创建 JournalEntry 实例时,将当前日期分配给 date 属性。
self.rating = rating
self.Title = entryTitle
self.Body = entryBody
self.photo = photo
self.latitude = latitude
self.longitude = longitude
这将把参数值赋给 JournalEntry 实例的相应属性。注意使用 self 来区分具有相同名称的属性和参数。
MARK: -语句使代码导航变得容易。点击工具栏下可见的路径的最后一部分,你将看到 属性 和 初始化 两个部分在菜单中显示。这使得你可以轻松地进入这些部分:

图 15.14:显示属性和初始化部分的菜单
到目前为止,你有一个名为 JournalEntry 的类,它可以存储单个日记条目的所有详细信息。在下一节中,你将创建一个返回示例 JournalEntry 实例的静态方法。
创建示例数据
如你在 第十四章,MVC 和表格视图入门 中所见,你可以使用数组作为表格视图的数据源。现在,你将创建一个包含静态方法的扩展,该方法将返回包含三个示例日记条目的数组。
在项目导航器中点击 JournalEntry 文件,并在文件中所有其他代码之后输入以下内容:
// MARK: - Sample data
extension JournalEntry {
static func createSampleJournalEntryData() -> [JournalEntry] {
let photo1 = UIImage(systemName: "sun.max")
let photo2 = UIImage(systemName: "cloud")
let photo3 = UIImage(systemName: "cloud.sun")
guard let journalEntry1 = JournalEntry(rating: 5, title: "Good", body: "Today is a good day", photo: photo1) else {
fatalError("Unable to instantiate journalEntry1")
}
guard let journalEntry2 = JournalEntry(rating: 0, title: "Bad", body: "Today is a bad day",
photo: photo2) else {
fatalError("Unable to instantiate journalEntry2")
}
guard let journalEntry3 = JournalEntry(rating: 3, title: "Ok", body: "Today is an Ok day", photo: photo3) else {
fatalError("Unable to instantiate journalEntry3")
}
return [journalEntry1, journalEntry2, journalEntry3]
}
}
此扩展包含一个 createSampleJournalEntryData() 方法,该方法使用来自 Apple 的 SFSymbols 库的符号创建三个 UIImage 实例,创建三个 JournalEntry 实例,将它们添加到数组中,并返回该数组。static 关键字表示这是一个 JournalEntry 类的方法,而不是 JournalEntry 实例方法。
要了解更多关于类型和实例方法的信息,请参阅此链接:docs.swift.org/swift-book/documentation/the-swift-programming-language/methods/
你现在已经完成了JournalEntry类的实现。你还添加了一个静态方法,该方法将生成三个示例日记条目。在下一节中,你将修改JournalListViewController类,以便使用此方法返回的数组来填充表格视图。
在表格视图中显示数据
在第十四章,开始使用 MVC 和表格视图中,你使用了表格视图单元格配置来设置表格视图单元格要显示的数据。在这里你将无法做到这一点,因为你正在使用你在第十三章,修改应用屏幕中实现的自定义表格视图单元格。
到目前为止,在本章中,你已经实现了一个返回包含三个JournalEntry实例的数组的静态方法。现在,你将修改JournalListViewController类,使其使用该数组作为“日记列表”屏幕上表格视图的数据源。为此,你需要执行以下操作:
-
创建一个自定义
UITableViewCell实例,并将其分配为journalCell表格视图单元格的标识。 -
修改
JournalListViewController类,从createSampleJourneyEntryData静态方法获取示例数据,并将其分配给journalEntries数组。 -
修改
JournalListViewController类中的数据源方法,使用journalEntries数组中的数据来填充表格视图单元格。
你将在下一节中开始创建一个自定义UITableViewCell实例。
创建自定义UITableViewCell子类
目前,在“日记列表”屏幕上的表格视图显示了 10 个不包含任何数据的表格视图单元格。你需要一种方法来设置表格视图单元格中图像视图和标签的值,因此你将创建一个新的UITableViewCell子类JournalEntryTableViewCell来达到这个目的。你将把这个类作为“日记列表”屏幕上表格视图单元格的标识。按照以下步骤操作:
-
在项目导航器中,右键单击视图文件夹,然后选择从模板新建文件...。
-
iOS应该已经选中。选择Cocoa Touch 类,然后点击下一步:

图 15.15:为你的新文件选择一个模板
使用Cocoa Touch 类模板将允许你设置你将要创建的类的超类,并自动插入为你创建的类插入样板代码。
- 选择你的新文件选项屏幕将出现:

图 15.16:选择你的新文件选项屏幕
按照以下方式配置类:
-
类:
JournalListTableViewCell -
子类:
UITableViewCell -
也创建 XIB:未勾选
-
语言:
Swift
完成后点击下一步。
-
点击创建,一个新的文件
JournalListTableViewCell将被添加到项目中的视图组。在里面你会看到以下代码:import UIKit class JournalListTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() // Initialization code } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) // Configure the view for the selected state } } -
从如下所示的
JournalListTableViewCell类声明中删除所有代码:class JournalListTableViewCell: UITableViewCell { } -
要创建与
journalCell表格视图单元格的子视图对应的三个属性,请在JournalEntry类声明的大括号之间输入以下代码:// MARK: - Properties @IBOutlet var photoImageView: UIImageView! @IBOutlet var dateLabel: UILabel! @IBOutlet var titleLabel: UILabel! -
JournalListTableViewCell类的实现已完成。现在您将此类分配为journalCell表格视图单元格的标识。在项目导航器中单击Main故事板文件,然后在文档大纲中单击Journal Scene下的journalCell:

图 15.17:文档大纲显示已选择的 journalCell
- 单击身份检查器按钮。在自定义类部分,将类设置为
JournalListTableViewCell。这会将JournalListTableViewCell实例设置为journalCell的自定义表格视图子类。完成此操作后按Return键:

图 15.18:身份检查器显示 journalCell 的类设置
您刚刚声明并定义了JournalListTableViewCell类,并将其分配为journalCell表格视图单元格的自定义表格视图子类。在下一节中,您将连接此类到journalCell表格视图单元格中的图像视图和标签,以便您可以控制它们显示的内容。
连接 journalCell 中的出口
要管理在“期刊列表”屏幕中表格视图单元格显示的内容,您将使用连接检查器将journalCell表格视图单元格中的图像视图和标签连接到JournalListTableViewCell类的出口。按照以下步骤操作:
- 在文档大纲中选择journalCell后,单击连接检查器以显示其出口。

图 15.19:连接检查器显示 journalCell 的出口
- 从photoImageView出口拖动到表格视图单元格中的图像视图:

图 15.20:连接检查器显示 photoImageView 出口
- 从dateLabel出口拖动到表格视图单元格的顶部标签。

图 15.21:连接检查器显示 dateLabel 出口
- 从titleLabel出口拖动到底部标签在表格视图单元格:

图 15.22:连接检查器显示 titleLabel 出口
记住,如果您出错,可以单击x断开连接,然后再次从出口拖动到 UI 元素。
Main故事板文件中的journalCell表格视图单元格现在已设置了一个自定义表格视图子类,JournalListTableViewCell。表格视图单元格的图像视图和标签的出口也已创建并分配。现在您将能够设置photoImageView、dateLabel和titleLabel出口,以便在应用运行时在每个单元格中显示照片、日期和标题。
在下一节中,你将更新JournalListViewController类中的表格视图数据源方法,以提供表格视图中要显示的表格视图单元格数量,以及为每个单元格提供日记条目的照片、日期和标题。
更新JournalListViewController中的数据源方法
JournalListViewController类中的数据源方法目前设置为显示 10 个表格视图单元格,每个单元格包含一个显示笑脸的图像视图和两个标签。你将更新它们以从SampleJournalEntryData实例中获取要显示的单元格数量以及每个单元格中的数据。按照以下步骤操作:
-
在项目导航器中点击JournalListViewController文件。
-
将
JournalListViewController类中的代码重新排列,以便tableView出口和viewDidLoad()方法位于表格视图委托方法之前:class JournalListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { **@IBOutlet****var****tableView****:** **UITableView****!** **override****func****viewDidLoad****() {** **super****.****viewDidLoad****()** **}** func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { -
在属性声明之前添加一个
MARK语句,如下所示:**// MARK: - Properties** @IBOutlet var tableView: UITableView! -
在
viewDidLoad()方法之前添加一个MARK语句,如下所示:**// MARK: - View controller lifecycle** @override func viewDidLoad() { -
在表格视图数据源方法之前添加一个
MARK语句,如下所示:**// MARK: - UITableViewDataSource** func tableView(_ tableView: UITableView, numberOfRowsInSection section: -
在
UnwindNewEntryCancel(segue:)之前添加一个MARK语句,如下所示:**// MARK: - Methods** @IBAction func unwindNewEntryCancel(segue: UIStoryboardSegue) { -
确认
JournalListViewController中的代码如下所示:class JournalListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { // MARK: - Properties @IBOutlet var tableView: UITableView! // MARK: - View controller lifecycle override func viewDidLoad() { super.viewDidLoad() } // MARK: - UITableViewDataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { tableView.dequeueReusableCell(withIdentifier: "journalCell", for: indexPath) } // MARK: - Methods @IBAction func unwindNewEntryCancel(segue: UIStoryboardSegue) { } } -
在
tableView出口声明之后输入以下代码以创建一个journalEntries属性,该属性将包含JournalEntry实例的数组:@IBOutlet var tableView: UITableView! **private****var****journalEntries****: [****JournalEntry****]** **=** **[]**
private关键字限制了journalEntries数组在JournalListViewController类中的使用。
你可以在以下链接中了解更多关于 Swift 中的访问控制的信息:docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol/。
-
按照以下示例修改
viewDidLoad()方法,以便在应用启动时填充journalEntries数组:override func viewDidLoad() { super.viewDidLoad() **journalEntries =****JournalEntry****.****createSampleJournalEntryData****()** }
createSampleJournalEntryData()方法将创建三个JournalEntry实例并将它们分配给journalEntries数组。
-
按照以下示例更新
tableView(_:numberOfRowsInSection:)方法。这将使表格视图为journalEntries数组中的每个元素显示一个journalCell:func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { **journalEntries.****count** } -
按照以下示例更新
tableView(_:cellForRowAt:)方法,使用来自journalEntries数组中相应元素的数据来设置每个单元格的图像视图和标签:func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let journalCell = tableView.dequeueReusableCell(withIdentifier: "journalCell", for: indexPath) as! JournalListTableViewCell let journalEntry = journalEntries[indexPath.row] journalCell.photoImageView.image = journalEntry.photo journalCell.dateLabel.text = journalEntry.date.formatted( .dateTime.month().day().year() ) journalCell.titleLabel.text = journalEntry.title return journalCell }
让我们分解一下:
let journalCell = tableView.dequeueReusableCell(withIdentifier: "journalCell", for: indexPath) as! JournalListTableViewCell
此语句指定从队列中检索的单元格被转换为JournalListTableViewCell实例。
你可以在以下链接中了解更多关于as!运算符的信息:developer.apple.com/swift/blog/?id=23。
let journalEntry = journalEntries[indexPath.row]
此语句获取与表格视图中当前单元格对应的JournalEntry实例。换句话说,表格视图中的第一个单元格对应于journalEntries数组中的第一个JournalEntry实例,第二个表格视图单元格对应于第二个JournalEntry实例,依此类推。
journalCell.photoImageView.image = journalEntry.photo
此语句从JournalEntry实例获取照片,并将其分配给journalCell实例的photoImageView属性。
journalCell.dateLabel.text = journalEntry.date.formatted(
.dateTime.month().day().year()
)
此语句从JournalEntry实例获取日期,将其格式化为字符串,并将其分配给journalCell实例的dateLabel属性。
journalCell.titleLabel.text = journalEntry.title
此语句从JournalEntry实例中获取存储在title中的字符串,并将其分配给journalCell实例的titleLabel属性的文本。
return journalCell
此语句返回用于在表格视图中显示的已填充的journalCell实例。
- 构建并运行应用程序。您将在“期刊列表”屏幕的表格视图中看到为
journalEntries数组中的每个JournalEntry实例显示的文本和图像:

图 15.23:模拟器显示“期刊列表”屏幕
点击行会显示“期刊条目详情”屏幕,但此屏幕尚未显示所选期刊条目的任何数据。您将在下一章中解决这个问题。
恭喜!到目前为止,“期刊列表”屏幕已显示journalEntries数组中的文本和图像。但您还不能从journalEntries数组中添加或删除期刊条目。您将在下一章中学习如何做到这一点。
摘要
在本章中,您实现了用于“期刊列表”屏幕的模型对象,以便显示期刊条目列表。您了解了将要使用的模型对象,创建了一个可以用于存储期刊条目实例的 Swift 类,并创建了一个返回示例期刊条目的静态方法。然后,您为表格视图创建了一个自定义的UITableViewCell实例,并使用返回示例期刊条目的方法填充了一个数组。然后,此数组被用作“期刊列表”屏幕表格视图的数据源。
您现在知道如何创建模型对象,如何创建示例数据,以及如何配置视图控制器以使用该示例数据填充表格视图。如果您希望创建使用表格视图的自己的应用程序,这将很有用。
在下一章中,您将学习如何在“期刊列表”屏幕中添加和删除期刊条目。您还将学习如何在视图控制器之间传递数据。
加入我们的 Discord!
与其他用户、专家以及作者本人一起阅读此书。提出问题,为其他读者提供解决方案,通过“问我任何问题”会议与作者聊天,等等。扫描二维码或访问链接加入社区。
(https://packt.link/ios-Swift
)
第十六章:传递视图控制器之间的数据
在上一章中,您已配置了JournalListViewController类,即“日记列表”屏幕的视图控制器,以在表格视图中显示包含示例数据的结构中的日记条目。
在本章中,您将学习如何从一个视图控制器传递数据到另一个视图控制器。您将从实现“添加新日记条目”屏幕的视图控制器开始,然后添加代码以从“添加新日记条目”屏幕传递数据到“日记列表”屏幕。接下来,您将学习如何在“日记列表”屏幕上删除日记条目。之后,您将了解文本字段和文本视图代理方法,最后,您将从“日记列表”屏幕传递数据到“日记条目详情”屏幕。
到本章结束时,您将学习如何在视图控制器之间传递数据以及如何使用文本字段和文本视图代理方法。这将使您能够轻松地在自己的应用程序中在视图控制器之间传递数据。
本章将涵盖以下主题:
-
从“添加新日记条目”屏幕传递数据到“日记列表”屏幕
-
从表格视图中删除行
-
探索文本字段和文本视图代理方法
-
从“日记列表”屏幕传递数据到“日记条目详情”屏幕
技术要求
您将继续在上一章中修改的 JRNL 项目上进行工作。
本章的游乐场和完成的 Xcode 项目位于本书代码包的 Chapter16 文件夹中,您可以通过以下链接下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际效果:
让我们从学习如何在下一节中在“添加新日记条目”屏幕和“日记列表”屏幕之间传递数据开始。
从“添加新日记条目”屏幕传递数据到“日记列表”屏幕
如第十章“设置用户界面”的应用程序导游中所示,“添加新日记条目”屏幕允许用户输入数据以创建新的日记条目。为此,用户将点击“日记列表”屏幕右上角的+按钮以显示“添加新日记条目”屏幕。然后,用户将输入新日记条目的详细信息。点击保存按钮将关闭“添加新日记条目”屏幕,并在“日记列表”屏幕上的表格视图中添加一个包含表格视图单元格的新行。该表格视图单元格将显示新添加的日记条目的照片、日期和标题。
为了实现这一功能,你需要为管理“添加新日志条目”屏幕的视图控制器实现prepare(for:sender:)方法。此方法在你从一个视图控制器切换到另一个视图控制器时被触发。使用此方法,你将使用用户输入的信息创建一个新的日志条目,并将其分配给一个变量。你将在JournalListViewController类中实现一个 unwind 方法,这样你就可以在“日志列表”屏幕上访问这个变量。然后,你将从这个变量中获取的新日志条目添加到journalEntries数组中,然后重新绘制表格视图。
要了解更多关于prepare(for:sender:)方法的信息,请参阅:developer.apple.com/documentation/uikit/uiviewcontroller/1621490-prepare。
在下一节中,你将创建一个新的视图控制器实例来管理“添加新日志条目”屏幕。
创建 AddJournalEntryViewController 类
目前,“添加新日志条目”屏幕还没有视图控制器。你将在项目中添加一个新文件,并实现AddJournalEntryViewController类,将其分配给新建条目场景,并连接出口。按照以下步骤操作:
-
从上一章打开你的
JRNL项目。在项目导航器中,通过右键单击JRNL组并选择新建组来创建一个新组。 -
将此组命名为“新建日志条目屏幕”并将其移动到日志列表屏幕组下方。
-
右键单击新建日志条目屏幕组,并选择从模板新建文件...。
-
iOS应该已经选中。选择Cocoa Touch Class并点击下一步。
-
使用以下详细信息配置类:
-
类:
AddJournalEntryViewController -
子类为:
UIViewController -
也创建 XIB:未勾选
-
语言:Swift
-
完成后点击下一步。
- 点击创建,
AddJournalEntryViewController文件将出现在项目导航器中。
AddJournalEntryViewController文件现在已经创建,其中包含AddJournalEntryViewController类的声明。你将设置此类为在点击日志列表屏幕上的+按钮时呈现的视图控制器场景的自定义类。按照以下步骤操作:
- 在项目导航器中点击主故事板文件,然后在文档大纲中点击新建条目场景:

图 16.1:显示新建条目场景的编辑区域
- 点击身份检查器按钮,在自定义类下,将类设置为
AddJournalEntryViewController:

图 16.2:新建条目场景的身份检查器设置
太好了!在下一节中,让我们将新条目场景中的用户界面元素连接到AddJournalEntryViewController类中的出口。通过这样做,AddJournalEntryViewController实例将能够访问用户在添加新日志条目屏幕上输入的数据。
将 UI 元素连接到 AddJournalEntryViewController 类
目前,添加新日志条目屏幕的AddJournalEntryViewController实例无法与其中的 UI 元素通信。你将在AddJournalEntryViewController类中添加出口,并将新条目场景中的相应 UI 元素分配给每个出口。按照以下步骤操作:
-
在项目导航器中,点击AddJournalEntryViewController文件,在
AddJournalEntryViewController类的大括号开头上添加以下属性:// MARK: - Properties @IBOutlet var titleTextField: UITextField! @IBOutlet var bodyTextView: UITextView! @IBOutlet var photoImageView: UIImageView! -
点击Main故事板文件,然后在文档大纲中点击新条目场景:
-
点击连接检查器以显示新条目场景的所有出口。从titleTextField出口拖动到新条目场景中的文本框:
![img/B31371_16_03.png]
图 16.3:连接检查器显示 titleTextField 出口
- 从bodyTextView出口拖动到新条目场景中的文本视图:
![img/B31371_16_04.png]
图 16.4:连接检查器显示 bodyTextView 出口
- 从photoImageView出口拖动到新条目场景中的图像视图:
![img/B31371_16_05.png]
图 16.5:连接检查器显示 photoImageView 出口
记住,如果你犯了错误,你可以点击x来断开连接,然后再次从出口拖动到 UI 元素。
到此为止,你已经将新条目场景中的 UI 元素连接到了AddJournalEntryViewController类中的出口。在下一节中,你将实现代码,当用户点击保存按钮时创建一个JournalEntry实例。
从用户输入创建 JournalEntry 实例
你已经实现了AddJournalEntryViewController类,并将此类中的出口连接到了新条目场景中的文本框、文本视图和图像视图。当用户在文本框和文本视图中输入数据时,你可以使用这些信息来创建一个新的日志条目。
当视图控制器即将过渡到另一个视图控制器时,视图控制器的prepare(for:sender:)方法会被调用。你将实现这个方法来创建一个新的JournalEntry实例,然后可以将它传递给 Journal List 屏幕的视图控制器。按照以下步骤操作:
-
在项目导航器中,点击AddJournalEntryViewController文件,在出口声明后添加一个
newJournalEntry属性到AddJournalEntryViewController类中://MARK: - Properties @IBOutlet var titleTextField: UITextField! @IBOutlet var bodyTextView: UITextView! @IBOutlet var photoImageView: UIImageView! **var****newJournalEntry****:** **JournalEntry****?**
使用用户输入的数据创建的JournalEntry实例将被分配给这个属性。
- 在此类中取消注释
prepare(for:sender:)方法。它应该看起来像以下这样:

图 16.6:显示prepare(for:sender:)方法的编辑区域
-
在此方法的花括号之间添加以下代码:
let title = titleTextField.text ?? "" let body = bodyTextView.text ?? "" let photo = photoImageView.image let rating = 3 newJournalEntry = JournalEntry(rating: rating, title: title, body: body, photo: photo)
这将分别将文本字段和文本视图中的字符串以及图像视图中的图像分配给title、body和photo。由于在应用演示中显示的自定义评分控件尚未实现,因此将占位符值分配给rating。然后使用这些常量创建一个新的JournalEntry实例,并将其分配给newJournalEntry属性。
你现在已添加了代码,在添加新期刊条目屏幕过渡到期刊列表屏幕之前创建JournalEntry实例。在下一节中,你将修改JournalListViewController类以获取新的JournalEntry实例并将其添加到journalEntries数组中。
更新表格视图以显示新的期刊条目
在“期刊列表”屏幕上,期刊条目以表格视图的形式显示。表格视图从包含在sampleJournalEntryData结构中的journalEntries数组获取数据。你将在JournalListViewController类中添加代码以获取分配给newJournalEntry属性的JournalEntry实例。之后,你将此实例插入到journalEntries数组中。按照以下步骤操作:
-
在项目导航器中,点击JournalListViewController文件,并在闭合花括号之前添加以下代码:
@IBAction func unwindNewEntrySave(segue: UIStoryboardSegue) { if let sourceViewController = segue.source as? AddJournalEntryViewController, let newJournalEntry = sourceViewController.newJournalEntry { journalEntries.insert(newJournalEntry, at: 0) tableView.reloadData() } }
此方法检查源视图控制器是否是AddJournalEntryViewController类的实例,如果是,则从newJournalEntry属性获取JournalEntry实例。然后,将此实例作为journalEntries数组中的第一个条目插入。之后,tableView.reloadData()语句将重新绘制表格视图。
- 点击Main故事板文件,并在文档大纲中展开新条目场景。Ctrl + 拖动文档大纲中的保存按钮到场景退出,并从弹出菜单中选择unwindNewEntrySaveWithSegue:。

图 16.7:弹出菜单显示已选择 unwindNewEntrySaveWithSegue:
当你运行你的项目时,点击保存按钮将从添加新条目屏幕过渡到期刊列表屏幕,并执行unwindNewEntrySave(segue:)方法。
- 构建并运行你的项目,然后点击+按钮。在文本字段和文本视图中输入一些示例文本。点击保存。

图 16.8:突出显示保存按钮的模拟器
- 当“期刊列表”屏幕再次出现时,新的期刊条目将出现在表格视图中。

图 16.9:突出显示新表格视图单元格的模拟器
太棒了!你已经成功实现了“添加新日记条目”屏幕的视图控制器,现在可以添加新的日记条目,它们将出现在日记列表屏幕上。在下一节中,你将实现代码,这将允许你从日记列表屏幕上的表格视图中删除日记条目。
从表格视图中删除行
如你在第十四章中学习的,MVC 和表格视图入门,表格视图行删除由tableView(_:commit:forRowAt:)方法处理,这是在UITableViewDataSource协议中声明的方法之一。
你将在JournalListViewController类中实现此方法。按照以下步骤操作:
-
在项目导航器中,点击JournalListViewController文件,并在
JournalListViewController类中现有表格视图数据源方法之后添加以下代码:func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { journalEntries.remove(at: indexPath.row) tableView.reloadData() } }
这将允许你向左滑动以显示删除按钮,当你点击删除按钮时,相应的JournalEntry实例将从journalEntries数组中移除,并且表格视图将被重新绘制。
- 构建并运行你的项目。在任意一行上向左滑动以显示删除按钮:

图 16.10:模拟器显示在日记列表屏幕上的删除按钮
- 点击删除按钮,行将从表格视图中移除:

图 16.11:模拟器显示重新绘制的表格视图
有了这个,你已经成功实现了一种从表格视图中删除行的方法!太棒了!在下一节中,你将学习更多关于文本字段和文本视图代理方法,这在你在“添加新日记条目”屏幕中输入数据时将非常有用。
探索文本字段和文本视图代理方法
目前,在“添加新日记条目”屏幕上存在一些问题。第一个问题是,一旦软件键盘出现在屏幕上,就无法将其关闭。第二个问题是,即使文本字段和文本视图为空,也可以点击保存按钮。
为了使与文本字段一起工作更容易,Apple 实现了UITextFieldDelegate,这是一个声明了一组可选方法来管理文本字段对象中文本的编辑和验证的协议。Apple 还实现了UITextViewDelegate,这是一个声明了用于接收文本视图对象编辑相关消息的方法的协议。
你可以在此链接中了解更多关于UITextFieldDelegate协议的信息:developer.apple.com/documentation/uikit/uitextfielddelegate。
你可以在此链接中了解更多关于UITextViewDelegate协议的信息:developer.apple.com/documentation/uikit/uitextviewdelegate。
你将实现UITextFieldDelegate和UITextViewDelegate协议中的方法到AddJournalEntryViewController类,以便用户在数据输入完成后可以关闭软件键盘。按照以下步骤操作:
-
在项目导航器中,点击AddJournalEntryViewController文件。在
AddJournalEntryViewController类声明的闭合花括号之后添加一个扩展,使其符合UITextFieldDelegate和UITextViewDelegate协议:extension AddJournalEntryViewController: UITextFieldDelegate, UITextViewDelegate { } -
按照以下方式修改
viewDidLoad()方法,将AddJournalEntryViewController实例设置为文本字段和文本视图的代理:override func viewDidLoad() { super.viewDidLoad() **titleTextField****.****delegate****=****self** **bodyTextView****.****delegate****=****self** }
这意味着文本字段和文本视图代理方法的实现位于AddJournalEntryViewController类中。
-
在扩展的开启花括号之后添加以下代码,以便在你在文本字段中完成文本输入后点击回车键时关闭软件键盘:
extension AddJournalEntryViewController: UITextFieldDelegate, UITextViewDelegate { **// MARK: - UITextFieldDelegate** **func****textFieldShouldReturn****(****_****textField****:** **UITextField****) ->****Bool** **{** **textField.****resignFirstResponder****()** **return****true** **}** } -
在
textFieldShouldReturn(_:)方法之后添加以下代码,以便在你在文本视图中完成文本输入后点击回车键时关闭软件键盘://MARK: - UITextViewDelegate func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { if (text == "\n") { textView.resignFirstResponder() } return true }
当你在屏幕上点击文本字段或文本视图时,它会获得第一响应者状态,并且软件键盘从屏幕底部弹出。你在键盘上输入的任何内容都会发送到具有第一响应者状态的对象。在实现上述方法后,在文本字段或文本视图中点击软件键盘上的回车键将告诉它放弃第一响应者状态,这将自动使键盘消失。
- 构建并运行你的应用,然后点击文本字段。如果软件键盘没有出现,从模拟器的I/O菜单中选择键盘 | 切换软件键盘:

图 16.12:模拟器 I/O 菜单,已选择键盘 | 切换软件键盘
- 使用软件键盘在文本字段或文本视图中输入一些文本:

图 16.13:模拟器显示软件键盘
- 在文本字段或文本视图中输入一些文本后,在软件键盘上点击回车键,它应该会自动消失。
第一个问题已经解决,用户现在可以关闭软件键盘了。太好了!现在你将修改你的应用,以便用户只有在文本字段和文本视图中输入了文本后才能点击保存。按照以下步骤操作:
-
要启用或禁用保存按钮,你需要能够设置其状态。在输出声明之后输入以下内容以创建保存按钮的输出:
@IBOutlet var titleTextField: UITextField! @IBOutlet var bodyTextView: UITextView! @IBOutlet var photoImageView: UIImageView! **@IBOutlet****var****saveButton****:** **UIBarButtonItem****!** var newJournalEntry: JournalEntry? -
在项目导航器中,点击Main故事板文件,然后在文档大纲中点击New Entry Scene:
-
点击连接检查器按钮,从saveButton输出拖动到New Entry场景中的保存按钮:

图 16.14:连接检查器显示保存按钮输出
-
在项目导航器中,点击 AddJournalEntryViewController 文件。在
textFieldShouldReturn(_:)方法之后添加显示的UITextFieldDelegate方法:func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } **func****textFieldDidBeginEditing****(****_****textField****:** **UITextField****) {** **saveButton****.****isEnabled****=****false** **}**
此方法在用户开始在文本框中编辑文本时禁用 保存 按钮。
-
在
textView(_:shouldChangeTextIn range:replacementText:)方法之后添加显示的UITextViewDelegate方法:func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { if(text == "\n") { textView.resignFirstResponder() } return true } **func****textViewDidBeginEditing****(****_****textView****:** **UITextView****) {** **saveButton****.****isEnabled****=****false** **}**
此方法在用户开始在文本视图中编辑文本时禁用 保存 按钮。
-
在文本框或文本视图中存在文本之前,添加一个方法来启用 保存 按钮:
// MARK: - Private methods private func updateSaveButtonState() { let textFieldText = titleTextField.text ?? "" let textViewText = bodyTextView.text ?? "" saveButton.isEnabled = !textFieldText.isEmpty && !textViewText.isEmpty }
private 关键字表示此方法仅在此类内部可访问。
-
在
textFieldDidBeginEditing(_:)方法之后添加显示的UITextFieldDelegate方法:func textFieldDidBeginEditing(_ textField: UITextField) { saveButton.isEnabled = false } **func****textFieldDidEndEditing****(****_****textField****:** **UITextField****) {** **updateSaveButtonState****()** **}**
此方法在文本框放弃第一响应者状态后调用 updateSaveButtonState()。
-
在
textViewDidBeginEditing(_:)方法之后添加显示的UITextViewDelegate方法:func textViewDidBeginEditing(_ textView: UITextView) { saveButton.isEnabled = false } **func****textViewDidEndEditing****(****_****textView****:** **UITextView****) {** **updateSaveButtonState****()** **}** **func** **textViewDidChange****(****_** **textView:** **UITextView****) {** **updateSaveButtonState****()** **}**
这些方法在文本视图放弃第一响应者状态以及文本视图内容发生变化后调用 updateSaveButtonState()。
-
在
viewDidLoad()方法中,当添加新日记条目屏幕首次出现时,调用updateSaveButtonState()方法来禁用 保存 按钮:override func viewDidLoad() { super.viewDidLoad() titleTextField.delegate = self bodyTextView.delegate = self **updateSaveButtonState****()** } -
构建并运行您的项目,然后点击 + 按钮进入添加条目屏幕。保存 按钮将被禁用:
![img/B31371_16_15.png]
图 16.15:显示禁用保存按钮的模拟器
- 在文本框中输入一些文本并按 回车 键。由于文本视图中已经有占位文本,保存 按钮将被启用。
添加新日记条目屏幕的两个问题都已解决。太棒了!在下一节中,您将学习如何在点击表格视图行时如何从日记列表屏幕传递数据到日记条目详情屏幕。
从日记列表屏幕传递数据到日记条目详情屏幕
如 第十章 中应用程序导游中所示,设置用户界面,日记条目详情屏幕允许用户在点击日记列表屏幕上的表格视图单元格时查看日记条目的详细信息。为此,您将创建一个视图控制器子类来管理日记条目详情屏幕。接下来,您将为 JournalListViewController 类实现 prepare(for:sender:) 方法以获取被点击行的 JournalEntry 实例。然后,您将此实例传递给管理日记条目详情屏幕的视图控制器实例。
在下一节中,您将首先创建一个新的视图控制器实例来管理日记条目详情屏幕。
创建 JournalEntryDetailViewController 类
目前,日记条目详情屏幕没有视图控制器。您将向项目中添加一个新文件并实现 JournalEntryDetailViewController 类,将其分配为 条目详情 场景 的身份,并连接出口。按照以下步骤操作:
-
在项目导航器中,通过右键点击JRNL组并选择新建组来创建一个新组。将此组命名为Journal Entry Detail Screen,并将其移动到Add New Journal Entry Screen组下方。
-
右键点击Journal Entry Detail Screen组,并选择从模板新建文件...。
-
iOS应该已经选中。选择Cocoa Touch Class并点击下一步。
-
使用以下详细信息配置类:
-
类:
JournalEntryDetailViewController -
子类:
UITableViewController -
也创建 XIB:未勾选
-
语言:
Swift
-
点击下一步。
- 点击创建,
JournalEntryDetailViewController文件将出现在项目导航器中。
有了这些,JournalEntryDetailViewController文件已经创建,其中包含JournalEntryDetailViewController类的声明。现在你将设置在 Journal List 屏幕上点击表格视图单元格时呈现的视图控制器场景的标识。按照以下步骤操作:
-
在项目导航器中点击Main故事板文件,并在文档大纲中选择Entry Detail Scene。
-
点击标识检查器按钮,在自定义类下将类设置为
JournalEntryDetailViewController:

图 16.16:Entry Detail Scene 的标识检查器设置
太棒了!在下一节中,你将连接Entry Detail Scene中的用户界面元素到JournalEntryDetailViewController类的出口。通过这样做,JournalEntryDetailViewController实例将能够显示期刊条目的详细信息。
将 UI 元素连接到 JournalEntryDetailViewController 类
目前,Journal Entry Detail 屏幕的JournalEntryDetailViewController实例无法与其中的 UI 元素进行通信。你将在JournalEntryDetailViewController类中创建出口,并将Entry Detail Scene中的相应 UI 元素分配给每个出口。按照以下步骤操作:
-
在项目导航器中,点击JournalEntryDetailViewController文件,并删除大括号之间的所有代码,除了
viewDidLoad()方法。 -
在
JournalEntryDetailViewController类中开括号之后添加以下属性:// MARK: - Properties @IBOutlet var dateLabel: UILabel! @IBOutlet var titleLabel: UILabel! @IBOutlet var bodyTextView: UITextView! @IBOutlet var photoImageView: UIImageView! -
点击Main故事板文件,并在文档大纲中选择Entry Detail Scene。
-
点击连接检查器按钮,从dateLabel出口拖动到Entry Detail Scene中的第一个标签:

图 16.17:连接检查器显示 dateLabel 出口
- 从titleLabel出口拖动到Entry Detail Scene中的第二个标签:

图 16.18:连接检查器显示 titleLabel 出口
- 从bodyTextView出口拖动到Entry Detail Scene中的文本视图:

图 16.19:连接检查器显示 bodyTextView 出口
- 从photoImageView出口拖动到“条目详情场景”中的图像视图:

图 16.20:连接检查器显示 photoImageView 出口
记住,如果您犯了一个错误,您可以点击x来断开连接,然后再次从出口拖动到 UI 元素。
您现在已成功将“条目详情”场景中的 UI 元素连接到JournalEntryDetailViewController类中的出口。在下一节中,您将实现代码,当用户在日记列表屏幕上点击表格视图单元格时,显示JournalEntry实例的详细信息。
显示日记条目的详细信息
到目前为止,您已实现了JournalEntryDetailViewController类,并将此类中的出口连接到“条目详情”场景中的标签、文本视图和图像视图。当用户在日记列表屏幕上点击表格视图单元格时,您将从数据源获取相应的JournalEntry实例,并将其传递给JournalEntryDetailViewController实例以在日记条目详情屏幕上显示。为此,您将在JournalListViewController类中实现prepare(for:sender:)方法。按照以下步骤操作:
- 在项目导航器中,点击Main故事板文件,然后点击连接“日记场景”和“条目详情场景”的过渡。点击属性检查器按钮,在Storyboard Segue下设置Identifier为
entryDetail:

图 16.21:属性检查器显示 Identifier 设置为 entryDetail
您将稍后使用此标识符来识别从“日记”场景到“条目详情”场景的过渡。
-
在项目导航器中点击JournalEntryDetailViewController文件,并在出口声明之后添加以下属性到
JournalEntryDetailViewController类中://MARK: - Properties @IBOutlet var dateLabel: UILabel! @IBOutlet var titleLabel: UILabel! @IBOutlet var bodyTextView: UITextView! @IBOutlet var photoImageView: UIImageView! **var****selectedJournalEntry****:** **JournalEntry****?**
您传递给JournalEntryDetailViewController实例的JournalEntry实例将被分配给selectedJournalEntry属性。
-
在项目导航器中点击JournalListViewController文件,并在表格视图委托方法之后实现
JournalListViewController类中的prepare(for:sender:)方法,如下所示:// MARK: - Navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) guard segue.identifier == "entryDetail" else { return } guard let journalEntryDetailViewController = segue.destination as? JournalEntryDetailViewController, let selectedJournalEntryCell = sender as? JournalListTableViewCell, let indexPath = tableView.indexPath(for: selectedJournalEntryCell) else { fatalError("Could not get indexPath") } let selectedJournalEntry = journalEntries[indexPath.row] journalEntryDetailViewController.selectedJournalEntry = selectedJournalEntry }
让我们分解一下:
guard segue.identifier == "entryDetail" else {
return
}
此代码检查是否使用了正确的过渡,如果没有,则方法退出。
guard let journalEntryDetailViewController = segue.destination as? JournalEntryDetailViewController,
let selectedJournalEntryCell = sender as? JournalListTableViewCell,
let indexPath = tableView.indexPath(for: selectedJournalEntryCell) else {
fatalError("Could not get indexpath")
}
此代码检查目标视图控制器是否为JournalEntryDetailViewController的实例,获取用户点击的表格视图单元格,并获取该单元格的索引路径。
let selectedJournalEntry = journalEntries[indexPath.row]
此语句从journalEntries数组中获取相应的JournalEntry实例。
journalEntryDetailViewController.selectedJournalEntry = selectedJournalEntry
此语句将JournalEntry实例分配给目标视图控制器的selectedJournalEntry属性。
你现在已经添加了代码,当从日记列表屏幕切换到日记条目详情屏幕时,会将用户点击的表格视图单元格对应的日记条目传递给JournalEntryDetailViewController实例。在下一节中,你将修改JournalEntryDetailViewController类以显示日记条目的详细信息。
显示选定的日记条目的详细信息
当切换到日记条目详情屏幕时,用户点击的表格视图单元格对应的JournalEntry实例将被分配给JournalEntryDetailViewController实例的selectedJournalEntry属性。你需要在JournalEntryDetailViewController类中添加代码以访问此属性并显示日记条目的详细信息。请按照以下步骤操作:
-
在项目导航器中,点击JournalEntryDetailViewController文件,并修改
JournalEntryDetailViewController类的viewDidLoad()方法,如图所示以显示日记条目的详细信息:override func viewDidLoad() { super.viewDidLoad() **dateLabel****.****text****=****selectedJournalEntry****?****.****date****.****formatted****(** **.****dateTime****.****day****().****month****(.****wide****).****year****()** **)** **titleLabel****.****text****=****selectedJournalEntry****?****.****title** **bodyTextView****.****text****=****selectedJournalEntry****?****.****body** **photoImageView****.****image****=****selectedJournalEntry****?****.****photo** }
如你所见,之前传递给此视图控制器的JournalEntry实例的属性被用来填充用户界面元素。请注意,date属性需要在分配给dateLabel text属性之前格式化为字符串。
- 构建并运行你的项目,并点击一个表格视图单元格。与该表格视图单元格对应的日记条目的详细信息将在日记条目详情屏幕上显示:

图 16.22:模拟器显示日记条目详情屏幕
恭喜!你已经成功实现了日记条目详情屏幕的视图控制器。现在,当用户在日记列表屏幕上点击表格视图单元格时,你将能够在其中显示日记条目的详细信息。
摘要
在本章中,你学习了如何从一个视图控制器传递数据到另一个视图控制器。你实现了添加新日记条目屏幕的视图控制器,然后你添加了代码从添加新日记条目屏幕传递数据到日记列表屏幕。接下来,你学习了如何在日记列表屏幕上删除日记条目。之后,你学习了文本字段和文本视图代理方法,最后你学习了如何从日记列表屏幕传递数据到日记条目详情屏幕。
你现在知道如何在不同视图控制器之间传递数据,以及如何使用文本字段和文本视图代理方法。这将使你能够轻松地在自己的应用程序中在不同视图控制器之间传递数据。酷!
在下一章中,你将在地图屏幕中添加一个视图控制器,并配置它使用地图标记显示日记条目位置。你还将配置地图标记,以便在标记呼出菜单中的按钮被点击时显示日记条目详情屏幕。
加入我们的 Discord!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,以及更多。扫描二维码或访问链接加入社区。
第十七章:开始使用 Core Location 和 MapKit
在上一章中,你学习了如何从“添加新日志条目”屏幕传递数据到日志列表屏幕,以及从日志列表屏幕传递数据到日志条目详情屏幕。你还学习了UITextFieldDelegate和UITextViewDelegate方法。
在本章中,你将学习如何使用苹果的Core Location框架获取设备位置,以及如何使用苹果的MapKit框架设置地图区域、显示地图标注和创建地图快照。如果你计划构建使用地图的应用程序,例如Apple Maps或Waze,这将非常有用。
首先,你将修改“添加新日志条目”屏幕,以便用户可以将他们的当前位置添加到新的日志条目中。接下来,你将创建一个MapViewController类(地图屏幕的视图控制器)并配置它以显示以你的位置为中心的地图区域。然后,你将更新JournalEntry类以符合MKAnnotation协议,这允许你将日志条目作为地图标注添加到地图中。之后,你将修改MapViewController类以在之前设置的地图区域内显示每个日志条目的标记。你将配置标记以显示呼出视图,并在呼出视图中配置按钮,以便在点击时显示日志条目详情屏幕。最后,你将修改JournalEntryViewController类,在日志条目详情屏幕中显示显示日志条目制作位置的地图快照。
到本章结束时,你将学会如何使用 Core Location 获取设备位置,以及如何使用 MapKit 指定地图区域、将地图标注视图添加到地图中,以及创建地图快照。
本章将涵盖以下主题:
-
使用 Core Location 框架获取设备位置
-
将
JournalEntry类更新为符合MKAnnotation协议 -
在地图屏幕上显示标注视图
-
在日志条目详情屏幕上显示地图快照
技术要求
你将继续在上一章中修改的JRNL项目上工作。
本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter17文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际效果:
在下一节中,你将学习如何使用 Core Location 框架获取设备位置。
使用 Core Location 框架获取设备位置
每个 iPhone 都有多种确定其位置的方式,包括 Wi-Fi、GPS、蓝牙、磁力计、气压计和蜂窝硬件。Apple 创建了 Core Location 框架,以使用 iOS 设备上所有可用的组件来收集位置数据。
要了解更多关于 Core Location 的信息,请参阅developer.apple.com/documentation/corelocation。
要配置您的应用程序使用 Core Location,您需要创建一个CLLocationManager实例,该实例用于配置、启动和停止位置服务。接下来,您将创建一个CLLocationUpdate实例,这是一个包含由 Core Location 框架提供的位置信息的结构。调用CLLocationUpdate类型的liveUpdates方法告诉 Core Location 开始提供包含用户位置、授权状态和位置可用性的位置更新。
要了解更多关于CLLocationManager类的信息,请参阅developer.apple.com/documentation/corelocation/configuring_your_app_to_use_location_services。
您可以在此处观看 Apple 的 WWDC 2023 视频,了解简化的位置更新:developer.apple.com/videos/play/wwdc2023/10180/。
由于位置信息被视为敏感用户数据,您还需要获得授权才能使用位置服务。您还可以检查由CLLocationUpdate类型的liveUpdates方法提供的位置更新授权状态。
要了解更多关于请求使用位置服务的授权信息,请参阅developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services。
您可以在此处观看 Apple 的 WWDC 2024 视频,了解位置授权的新功能:developer.apple.com/videos/play/wwdc2024/10212/。
在下一节中,您将修改AddJournalEntryViewController类,以便用户在创建新的日记条目时可以分配位置。
修改AddJournalEntryViewController类
目前,添加新日记条目屏幕有一个获取位置开关,但目前它还没有任何作用。您将为这个开关在AddJournalEntryViewController类中添加一个输出端口,并在开关打开时将其修改为将位置添加到JournalEntry实例。请按照以下步骤操作:
-
在项目导航器中,点击
AddJournalEntryViewController文件。在此文件中,在import UIKit语句之后添加一个import语句以导入 Core Location 框架:import UIKit **import** **CoreLocation** -
在文件中的所有其他代码之后添加一个
AddJournalEntryViewController类的扩展:extension AddJournalEntryViewController { // MARK: - CoreLocation }
你将在稍后在这个扩展中实现请求使用用户私人数据并确定用户位置的代码。
-
在
AddJournalEntryViewController类中,在其他所有输出端口之后,添加用于获取位置开关及其旁边的标签的输出端口:@IBOutlet var photoImageView: UIImageView! @IBOutlet var saveButton: UIBarButtonItem! **@IBOutlet****var****getLocationSwitch****:** **UISwitch****!** **@IBOutlet****var****getLocationSwitchLabel****:** **UILabel****!** var newJournalEntry: JournalEntry? -
在所有其他属性声明之后,添加用于存储
CLLocationManager类实例、一个将管理位置更新的异步任务以及当前设备位置的属性:var newJournalEntry: JournalEntry? **private let****locationManager****=****CLLocationManager****()** **private var** **locationTask****=****Task****<****Void****,** **Error****>?** **private var****currentLocation****:** **CLLocation****?**
所有这些属性都是private的,因为它们将仅在此类中使用。
-
在
AddJournalEntryViewController扩展中,在闭合花括号之前实现一个用于确定用户位置的方法:private func fetchUserLocation() { locationManager.requestWhenInUseAuthorization() self.locationTask = Task { for try await update in CLLocationUpdate.liveUpdates() { if let location = update.location { updateCurrentLocation(location) } else if update.authorizationDenied { failedToGetLocation(message: "Check Location Services settings for JRNL in Settings > Privacy & Security.") } else if update.locationUnavailable { failedToGetLocation(message: "Location Unavailable") } } } }
让我们分解一下:
locationManager.requestWhenInUseAuthorization()
此语句请求用户允许使用他们的位置信息。
self.locationTask = Task {
此语句将一个将不断获取位置更新的异步任务分配给locationTask属性。
for try await update in
CLLocationUpdate.liveUpdates() {
此语句获取CLLocation.liveUpdates()提供的更新,并将每个更新分配给一个update实例。
if let location = update.location {
updateCurrentLocation(location)
}
此语句从update实例获取用户位置并调用updateCurrentLocation方法。
else if update.authorizationDenied {
failedToGetLocation(message: "Check Location
Services settings for JRNL in Settings > Privacy & Security."))}
如果用户没有授权使用他们的私人数据,此语句将调用failedToGetLocation(message:)方法。
else if update.locationUnavailable {
failedToGetLocation(message: "Location
Unavailable")
}
如果用户的位置信息不可用,此语句将调用failedToGetLocation(message:)方法。
你将看到错误消息,因为fetchUserLocation()调用的方法尚未实现。
-
在
AddJournalEntryViewController扩展的闭合花括号之前,实现由fetchUserLocation()类调用的updateCurrentLocation(_:)方法:private func updateCurrentLocation(_ location: CLLocation) { let interval = location.timestamp.timeIntervalSinceNow if abs(interval) < 30 { self.locationTask?.cancel() getLocationSwitchLabel.text = "Done" let lat = location.coordinate.latitude let long = location.coordinate.longitude currentLocation = CLLocation(latitude: lat, longitude: long) } }
此方法首先获取location的时间戳。此位置可能是一个旧的、缓存的地址,而不是实际的位置,因此时间戳与当前日期和时间进行比较。如果持续时间小于 30 秒,这表明用户的位置是当前的。在这种情况下,取消分配给locationTask的异步任务,将获取位置开关标签的文本设置为完成,并将currentLocation属性设置为这个位置。
-
在
AddJournalEntryViewController扩展的闭合花括号之前,实现由fetchUserLocation()类调用的failedToGetLocation(message:)方法:private func failedToGetLocation(message: String) { self.locationTask?.cancel() getLocationSwitch.setOn(false, animated: true) getLocationSwitchLabel.text = "Get location" let alertController = UIAlertController(title: "Failed to get location", message: message, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) alertController.addAction(okAction) present(alertController, animated: true) }
此方法将取消分配给locationTask的异步任务,将获取位置开关和标签的值重置为其初始值,并显示一个配置了适当错误消息的警告。
-
在
AddJournalEntryViewController类中,在闭合花括号之前实现一个动作,当获取位置开关的值改变时执行:// MARK: - Actions @IBAction func locationSwitchValueChanged(_ sender: UISwitch) { if getLocationSwitch.isOn { getLocationSwitchLabel.text = "Getting location..." fetchUserLocation() } else { currentLocation = nil getLocationSwitchLabel.text = "Get location" self.locationTask?.cancel() } }
如果获取位置开关处于开启状态,开关标签文本设置为获取位置...,并调用fetchUserLocation()方法。如果开关处于关闭状态,currentLocation将被设置为nil,开关标签文本重置为获取位置,并且分配给locationTask的异步任务被取消。
-
修改
prepare(for:sender:)方法以向JournalEntry实例添加位置信息:override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let title = titleTextField.text ?? "" let body = bodyTextView.text ?? "" let photo = photoImageView.image let rating = 3 **let** **lat** **=****currentLocation****?****.****coordinate****.****latitude** **let** **long** **=****currentLocation****?****.****coordinate****.****longitude** newJournalEntry = JournalEntry(rating: rating, title: title, body: body, photo: photo**,** **latitude****: lat,** **longitude****: long**) } -
修改
updateSaveButtonState()方法,以便在获取位置开关开启后仅当找到位置时启用保存按钮:private func updateSaveButtonState() { let textFieldText = titleTextField.text ?? "" let textViewText = bodyTextView.text ?? "" **let** **textIsValid** **=****!****textFieldText.****isEmpty****&&** **!****textViewText.****isEmpty** **if****getLocationSwitch****.****isOn** **{** **saveButton****.****isEnabled****=** **textIsValid** **&&****currentLocation****!=****nil** **}** **else** **{** **saveButton****.****isEnabled****=** **textIsValid** **}** } -
在
updateCurrentLocation(_:)中调用updateSaveButtonState(),以便在找到位置后更新保存按钮的状态:currentLocation = CLLocation(latitude: lat, Longitude: long **updateSaveButtonState****()** } } -
在项目导航器中点击
Main故事板文件。在文档大纲中点击新条目场景。 -
大多数日记条目可能不需要位置,因此您将获取位置开关的默认值设置为关闭。点击获取位置开关并点击属性检查器按钮。在开关下,将状态设置为关闭:

图 17.1:属性检查器显示将获取位置开关状态设置为关闭
- 在文档大纲中点击新条目场景,然后点击连接检查器按钮。将getLocationSwitch出口连接到新条目场景中的获取位置开关:

图 17.2:连接检查器显示 getLocatioSwitch 出口
- 将getLocationSwitchLabel出口连接到新条目场景中获取位置开关旁边的标签:

图 17.3:连接检查器显示 getLocatioSwitchLabel 出口
- 将locationSwitchValueChanged动作连接到获取位置开关,并从弹出菜单中选择值已更改:

图 17.4:属性检查器显示 getLocatioSwitchValueChanged 出口
您已完成对AddJournalEntryViewController类的修改。在下一节中,您将学习如何配置应用程序以访问用户数据。
修改 Info.plist 文件
由于您的应用程序使用用户数据,您需要请求用户允许使用它。为此,您需要在应用程序的Info.plist文件中添加一个新的设置。按照以下步骤操作:
- 在项目导航器中点击
Info.plist文件。如果将指针移至信息属性列表行,您将看到一个小的+按钮。点击它以创建一个新行:

图 17.5:编辑区域显示 Info.plist 的内容
- 在新行中,将键设置为Privacy - Location When In Use Usage Description,并将值设置为
此应用程序使用您的位置来记录日记条目。完成操作后,您的Info.plist文件应如下所示:

图 17.6:添加新行后的 Info.plist
- 启动模拟器并从模拟器的功能菜单中选择位置 | 苹果来模拟一个位置:

图 17.7:从模拟器的功能菜单中选择位置 | 苹果
- 构建并运行你的应用,点击+按钮以显示新建日记条目屏幕。当提示时,点击允许在应用使用时按钮:

图 17.8:显示允许在应用使用时高亮的警告
注意,这个警告将在本章第一次启动你的应用时出现,你选择的设置将在后续启动中默认使用。
- 输入日记条目的标题和正文,并将获取位置开关设置为开启:

图 17.9:显示获取位置开关的新建日记条目屏幕
一旦确定了位置,获取位置开关旁边的标签将显示完成,并且保存按钮将变为活动状态。
- 点击保存。你将被返回到日记列表屏幕。
到目前为止,获取位置开关及其旁边的标签已经连接到AddJournalEntryViewController类中的输出,获取设备位置的函数已经分配给获取位置开关,并且已经添加了将你的位置添加到新的JournalEntry实例所需的所有代码。太棒了!
在下一节中,你将创建一个地图屏幕的视图控制器,并配置它以显示当前设备的位置。
创建 MapViewController 类
在第十二章,完成用户界面中,你已经在地图屏幕中添加了一个地图视图。地图视图是MKMapView类的一个实例。你可以在苹果地图应用中看到它的样子。
要了解更多关于MKMapView的信息,请参阅developer.apple.com/documentation/mapkit/mkmapview。
当你构建并运行你的应用时,你将在屏幕上看到一个地图。你可以通过设置地图视图的region属性来指定屏幕上可见的地图部分。
要了解更多关于区域及其创建方法的信息,请参阅developer.apple.com/documentation/mapkit/mkmapview/1452709-region。
你将创建一个新的类,MapViewController,作为地图屏幕的视图控制器,并使用 Core Location 来确定将要显示的地图区域的中心点。按照以下步骤操作:
-
通过在JRNL组上右键单击并选择新建组,在你的项目中创建一个新的组。将此组命名为
Map Screen,并将其移动到日记条目详情屏幕组下方。 -
右键单击地图屏幕组并选择从模板新建文件...。
-
iOS应该已经选中。选择Cocoa Touch 类并点击下一步。
-
使用以下详细信息配置该类:
-
类:
MapViewController -
子类:
UIViewController -
也创建 XIB:未勾选
-
语言:Swift
-
完成后点击下一步。
-
点击创建。
MapViewController文件出现在项目导航器中,其内容出现在编辑器区域: -
在现有的
import语句之后添加代码以导入Core Location和MapKit框架:import UIKit **import** **CoreLocation** **import** **MapKit** -
在文件中的所有其他代码之后为
MapViewController类添加一个新的扩展:extension MapViewController { // MARK: - CoreLocation }
你将在本扩展中实现代码以请求使用用户的私人数据并确定用户的位置:
-
在
MapViewController类中,在开括号之后添加一个用于地图视图的输出:// MARK: - Properties @IBOutlet var mapView: MKMapView! -
在
mapView属性之后添加属性以保存CLLocationManager实例和异步任务:@IBOutlet var mapView: MKMapView! **private let****locationManager****=****CLLocationManager****()** **private var** **locationTask****=****Task****<****Void****,** **Error****>?** -
在
MapViewController扩展中闭括号之前实现一个确定用户位置的方法:private func fetchUserLocation() { locationManager.requestWhenInUseAuthorization() self.navigationItem.title = "Getting location..." self.locationTask = Task { for try await update in CLLocationUpdate.liveUpdates() { if let location = update.location { updateMapWithLocation(location) } else if update.authorizationDenied { failedToGetLocation(message: "Check Location Services settings for JRNL in Settings > Privacy & Security.") } else if update.locationUnavailable { failedToGetLocation(message: "Location Unavailable") } } } }
此方法与你在AddJournalEntryViewController扩展中实现的fetchUserLocation()方法类似。首先,它将请求使用私人用户数据的权限并将地图屏幕的标题设置为Getting location...。接下来,将一个异步任务分配给locationTask,以连续确定用户的位置。如果找到用户的位置,地图将更新以显示用户的位置。否则,将显示一个带有适当错误消息的警报:
注意,由于fetchUserLocation()调用的方法尚未实现,你将看到错误消息:
-
在闭括号之前在
MapViewController扩展中实现缺失的方法:private func updateMapWithLocation(_ location: CLLocation) { let interval = location.timestamp.timeIntervalSinceNow if abs(interval) < 30 { self.locationTask?.cancel() let lat = location.coordinate.latitude let long = location.coordinate.longitude navigationItem.title = "Map" mapView.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: long), span: MKCoordinateSpan( latitudeDelta: 0.01, longitudeDelta: 0.01)) } } private func failedToGetLocation(message: String) { self.locationTask?.cancel() navigationItem.title = "Location not found" let alertController = UIAlertController(title: "Failed to get location", message: message, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) alertController.addAction(okAction) present(alertController, animated: true) }
这些方法与你在AddJournalEntryViewController扩展中之前实现的updateCurrentLocation(location:)和failedToGetLocation(message:)方法类似:
updateMapWithLocation(location:)方法将设置地图屏幕的标题为Map,创建一个以用户位置为中心的地图区域,并将其分配给mapView属性:
如果拒绝使用用户数据或位置不可用,failedToGetLocation(message:)方法将设置地图屏幕的标题为Location not found并显示适当的错误消息:
-
修改
MapViewController类的viewDidLoad()方法,如所示调用fetchUserLocation():override func viewDidLoad() { super.viewDidLoad() **fetchUserLocation****()** } -
在项目导航器中点击
Main故事板文件,然后点击文档大纲中的第一个Map Scene。点击身份检查器按钮,在自定义类下将类设置为MapViewController:

图 17.10:地图场景的身份检查器设置
- 点击连接检查器以显示Map Scene的所有输出。从mapView输出拖动到Map Scene中的地图视图:

图 17.11:连接检查器显示 mapView 输出
记住,如果你犯了错误,你可以点击x来断开连接,然后再次从输出拖动到 UI 元素:
- 构建并运行你的应用,并确保从模拟器的功能 | 位置菜单中选择了Apple。点击地图标签按钮以显示一个以你选择的地点为中心的地图区域,在这个例子中,是苹果公司园区:

图 17.12:模拟器显示以你的位置为中心的地图
你可以通过选择功能 | 位置 | 自定义位置并在其中输入所需位置的经纬度来在模拟器中模拟任何位置。
由于viewDidLoad()在MapViewController实例加载视图时只调用一次,因此如果用户的位置在最初设置后发生变化,地图将不会更新。此外,如果你在实际的 iOS 设备上运行应用,你会发现确定位置需要很长时间。你将在下一章中解决这两个问题。
你已经成功为地图屏幕创建了一个新的视图控制器,并配置它显示以你的设备位置为中心的地图区域。太棒了!在下一节中,你将了解MKAnnotation协议,以及如何使一个类符合它。
将 JournalEntry 类更新为符合 MKAnnotation 协议
当你在 iPhone 上的地图应用中操作时,你可以点击并按住地图来放置一个标记:

图 17.13:地图应用显示放置的标记
要在你的应用中为地图视图添加标记,你需要一个符合MKAnnotation协议的类。此协议允许你将此类的一个实例与特定位置关联。
要了解更多关于MKAnnotation协议的信息,请参阅developer.apple.com/documentation/mapkit/mkannotation。
任何类都可以通过实现包含位置的coordinate属性来采用MKAnnotation协议。可选的MKAnnotation协议属性包括title,它包含注释的标题,以及subtitle,它包含注释的副标题。
当符合MKAnnotation协议的类的实例位于屏幕上可见的地图区域时,地图视图会要求其代理(通常是视图控制器)提供一个相应的MKAnnotationView类的实例。此实例在地图上显示为一个标记。
要了解更多关于MKAnnotationView的信息,请参阅developer.apple.com/documentation/mapkit/mkannotationview。
如果用户滚动地图并且MKAnnotationView实例离开屏幕,它将被放入重用队列并在稍后回收,就像表格视图单元格和集合视图单元格被回收一样。
要在地图屏幕上表示日志条目位置,你需要修改 JournalEntry 类以使其符合 MKAnnotation 协议。这个类将有一个 coordinate 属性来存储日志条目的位置,一个 title 属性来存储日志条目日期,以及一个 subtitle 属性来存储日志条目标题。你将使用 JournalEntry 实例的 latitude 和 longitude 属性来计算分配给 coordinate 属性的值。按照以下步骤操作:
-
在项目导航器中,点击
JournalEntry文件(位于 Journal List Screen | Model 组内)。在import UIKit语句之后输入以下内容以导入MapKit框架:import UIKit **import** **MapKit**
这允许你在代码中使用 MKAnnotation 协议。
-
MKAnnotation协议有一个可选的title属性,你将在稍后使用。你将把JournalEntry类中的title属性名称改为entryTitle,这样你就不需要两个具有相同名称的属性。右键单击title属性,从弹出菜单中选择 重构 | 重命名。 -
将新名称设置为
entryTitle,如图所示,然后点击 重命名:

图 17.14:将标题属性重命名为 entryTitle
-
将
JournalEntry类声明修改如下,使其成为NSObject类的子类并采用MKAnnotation协议:class JournalEntry**:** **NSObject****,** **MKAnnotation**{ -
你会看到一个错误,因为你还没有实现
coordinate属性,这是符合MKAnnotation协议所必需的。在初始化器之后输入以下内容:// MARK: - MKAnnotation var coordinate: CLLocationCoordinate2D { guard let lat = latitude, let long = longitude else { return CLLocationCoordinate2D() } return CLLocationCoordinate2D(latitude: lat, longitude: long) }
coordinate 属性是 CLLocationCoordinate2D 类型,它包含一个地理位置。coordinate 属性的值不是直接分配的;guard 语句从 latitude 和 longitude 属性获取纬度和经度值,然后使用这些值来创建 coordinate 属性的值。这样的属性被称为 计算属性。
-
要实现可选的
title属性,在coordinate属性之后输入以下内容:var title: String? { date.formatted( .dateTime.day().month().year() ) }
这是一个计算属性,返回格式化为字符串的日志条目日期。
-
要实现可选的
subtitle属性,在title属性之后输入以下内容:var subtitle: String? { entryTitle }
这是一个计算属性,返回日志条目标题。
到目前为止,你已经修改了 JournalEntry 类以符合 MKAnnotation 协议。在下一节中,你将修改 MapViewController 类,向地图视图中添加一个 JournalEntry 实例数组,并且地图视图显示区域内的任何实例都将出现在地图屏幕上的一个标记上。
在地图屏幕上显示注释视图
当前地图屏幕显示以您的设备位置为中心的地图区域。现在地图区域已设置,您可以根据其 coordinate 属性确定哪些 JournalEntry 实例位于此区域。请记住,JournalEntry 类符合 MKAnnotation。作为地图视图的视图控制器,MapViewController 类负责为该区域内的任何 MKAnnotation 实例提供 MKAnnotationView 实例。您现在将修改 MapViewController 类,从 SampleJournalEntryData 结构中获取 JournalEntry 实例的数组并将其添加到地图视图中。按照以下步骤操作:
-
在项目导航器中,点击 JournalEntry 文件。在
createSampleJournalEntryData()方法中,修改创建journalEntry2实例的语句,如下所示:guard let journalEntry2 = JournalEntry(rating: 0, title: "Bad", body: "Today is a bad day", photo: photo2**,** **latitude****:** **37.3318****,** **longitude****:** **-****122.0312**) else { fatalError("Unable to instantiate journalEntry2") }
此实例现在具有 latitude 和 longitude 属性的值,这些值将用于设置其 coordinate 属性。使用的值是靠近苹果公司校园的位置,您将在运行应用时在模拟器中设置。
您可以使用任何您想要的地点,但您需要确保该地点靠近地图的中心点,否则,针将不会显示。
-
在项目导航器中,点击 MapViewController 文件。在文件中的所有其他代码之后添加一个扩展,使
MapViewController类符合MKMapViewDelegate协议:extension MapViewController: MKMapViewDelegate { } -
在
locationTask属性声明之后,添加以下代码以创建一个私有属性annotations,该属性将包含JournalItem实例的数组:private var locationTask: Task<Void, Error>? **private var****annotations****:****[****JournalEntry****] = []**目前日记列表和地图屏幕之间没有连接。这意味着您使用添加新日记条目屏幕添加的任何日记条目都不会出现在地图屏幕上。您将在下一章创建一个共享实例,该实例将由两个视图控制器使用。
-
在
viewDidLoad()方法中,在闭合花括号之前,将地图视图的delegate属性设置为MapViewController实例:fetchUserLocation() **mapView****.****delegate****=****self** } -
在下一行,通过调用
sampleJournalEntryData结构的createSampleJournalEntryData()方法来填充journalEntries数组:mapView.delegate = self **annotations** **=** **JournalEntry****.****createSampleJournalEntryData****()** } -
在下一行,添加以下语句以将所有示例日记条目(符合
MKAnnotation协议)添加到地图视图中:annotations = JournalEntry.**createSampleJournalEntryData**() **mapView****.****addAnnotations****(****annotations****)** }
地图视图的代理(在本例中为 MapViewController 类)现在将自动为地图屏幕上显示的地图区域内的每个 JournalItem 实例提供一个 MKAnnotationView 实例。
- 构建并运行您的应用,并使用模拟器的 Features | Location 菜单验证位置是否已设置为 Apple。您应该在地图屏幕上看到一个单独的针(
MKAnnotationView实例):

图 17.15:iOS 模拟器显示标准 MKAnnotationView 实例
地图屏幕现在可以显示图钉,但点击图钉只会使其变大。你将在下一节中添加代码,使图钉显示带有按钮的呼出窗口。
由于viewDidLoad()方法仅在MapViewController实例加载其视图时调用一次,因此在此之后添加到journalEntries数组中的任何带有位置的日记条目都不会作为注释添加到地图上。你将在下一章中解决这个问题。
配置图钉以显示呼出窗口
目前,地图屏幕显示标准的MKAnnotationView实例,看起来像图钉。点击图钉只会使其变大。MKAnnotationView实例可以被配置为在点击时显示呼出气泡。为了实现这一点,你需要实现mapView(_:viewFor:)方法,这是一个可选的MKMapViewDelegate协议方法。按照以下步骤操作:
-
在项目导航器中点击MapViewController文件。在
MKMapViewDelegate扩展中,在开括号之后添加以下方法:// MARK: - MKMapViewDelegate func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? { let identifier = "mapAnnotation" guard annotation is JournalEntry else { return nil } if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { annotationView.annotation = annotation return annotationView } else { let annotationView = MKMarkerAnnotationView(annotation:annotation, reuseIdentifier:identifier) annotationView.canShowCallout = true let calloutButton = UIButton(type: .detailDisclosure) annotationView.rightCalloutAccessoryView = calloutButton return annotationView } }
让我们分解一下:
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView?
这是MKMapViewDelegate协议中指定的方法之一。当MKAnnotation实例位于地图区域内时,它会触发,并返回一个MKAnnotationView实例,用户将在屏幕上看到它。
let identifier = "mapAnnotation"
将一个常量identifier赋值为"MapAnnotation"字符串。这将作为MKAnnotationView实例的重用标识符。
guard annotation is JournalEntry else {
return nil
}
这个guard语句检查注解是否是JournalEntry实例,如果不是,则返回nil。
if let annotationView =
mapView.dequeueReusableAnnotationView(withIdentifier:
identifier) {
annotationView.annotation = annotation
return annotationView
这个if语句检查是否存在一个最初可见但现在不在屏幕上的现有MKAnnotationView实例。如果存在,它可以被重用,并分配给annotationView常量。然后,JournalItem实例被分配给annotationView的annotation属性,并返回annotationView。
} else {
let annotationView =
MKMarkerAnnotationView(annotation:annotation,
reuseIdentifier:identifier)
如果没有可以重用的现有MKAnnotationView实例,将执行else子句。使用之前指定的重用标识符(MapAnnotation)创建一个新的MKAnnotationView实例。
annotationView.canShowCallout = true
let calloutButton = UIButton(type: .detailDisclosure)
annotationView.rightCalloutAccessoryView = calloutButton
MKAnnotationView实例被配置了呼出窗口。当你点击地图上的图钉时,会出现一个呼出气泡,显示标题(日记条目日期)、副标题(日记条目标题)和一个按钮。你将在稍后编程按钮以显示日记条目详情屏幕。
return annotationView
返回自定义的MKAnnotationView实例。
要了解更多关于mapView(_:viewFor:)方法的信息,请参阅developer.apple.com/documentation/mapkit/mkmapviewdelegate/1452045-mapview。
- 构建并运行你的应用,并在模拟器的功能 | 位置菜单中将位置设置为Apple。你应该在地图屏幕上看到一个单独的图钉。点击图钉以显示一个呼出窗口:

图 17.16:iOS 模拟器显示在点击图钉时出现的呼出窗口
您已成功创建了一个自定义的 MKAnnotationView,当点击时会显示一个呼出窗口,但点击呼出气泡中的按钮目前还没有任何反应。您将在下一节中配置按钮以显示日记条目详情屏幕。
从地图屏幕转到日记条目详情屏幕
到目前为止,地图屏幕现在显示了一个 MKAnnotationView 实例,点击它会显示一个显示日记条目详情的呼出气泡。尽管呼出气泡中的按钮目前还不能工作。
要从呼出按钮显示日记条目详情屏幕,您将在地图屏幕和日记条目详情屏幕之间添加一个转换,并实现可选的 MKMapViewDelegate 协议方法 mapView(_:annotationView:calloutAccessoryControlTapped:),以便在点击呼出按钮时执行该转换。按照以下步骤操作:
- 在项目导航器中点击 Main 故事板文件。在文档大纲中找到 Map Scene 下的 Map 图标。Ctrl + 拖动 从 Map 图标到 Entry Detail Scene 并从弹出菜单中选择 Show 以在 Map Scene 和 Entry Detail Scene 之间添加一个转换:

图 17.17:转换弹出菜单
- 您将为这个转换设置一个标识符,以便
mapView(_:annotationView:calloutAccessoryControlTapped:)方法知道要执行哪个转换。选择连接到 Map Scene 和 Entry Detail Scene 的转换:

图 17.18:地图场景和条目详情场景之间的转换
- 在属性检查器中,在 Storyboard Segue 下,将 Identifier 设置为
showMapDetail:

图 17.19:showDetail 转换的属性检查器设置
-
在项目导航器中点击 MapViewController 文件。在
MapViewController类中,在所有其他属性声明之后添加一个属性以存储日记条目:private var annotations: [JournalEntry] = [] **private var****selectedAnnotation****:** **JournalEntry****?**
此属性将存储被点击的 MKAnnotationView 实例的 JournalEntry 实例。
-
在
mapView(_:viewFor:)方法之后添加mapView(_:annotationView:calloutAccessoryControlTapped:)方法:func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { guard let annotation = mapView.selectedAnnotations.first else { return } selectedAnnotation = annotation as? JournalEntry self.performSegue(withIdentifier: "showMapDetail", sender: self) }
当用户点击呼出气泡按钮时,会触发此方法。将分配给呼出视图的注释将赋值给 selectedAnnotation 属性,并且带有 showMapDetail 标识符的转换将被执行,这将显示日记条目详情屏幕。
要了解更多关于 mapView(_:annotationView:calloutAccessoryControlTapped:) 方法的知识,请参阅 developer.apple.com/documentation/mapkit/mkmapviewdelegate/1616211-mapview。
-
构建并运行您的应用程序,并使用模拟器的 Features | Location 菜单将位置设置为 Apple。您应该在地图屏幕上看到一个单独的图钉。点击图钉以显示呼出窗口,并点击呼出窗口内的按钮。
-
日志条目详情屏幕出现,但它不包含任何关于日志条目的详细信息:

图 17.20:iOS 模拟器显示一个空白的日志条目详情屏幕
-
为了使日志条目详情屏幕显示日志条目的详细信息,你将使用
prepare(for:sender:)方法将选定的日志条目传递到日志条目详情屏幕的视图控制器。在MapViewController类中,取消注释并修改prepare(for:sender:)方法,如下所示:// MARK: - Navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) guard segue.identifier == "showMapDetail" else { fatalError("Unexpected segue identifier") } guard let entryDetailViewController = segue.destination as? JournalEntryDetailViewController else { fatalError("Unexpected view controller") } entryDetailViewController.selectedJournalEntry = selectedAnnotation }
如你之前所学的,prepare(for:sender:)方法是在视图控制器切换到另一个视图控制器之前由视图控制器执行的。在这种情况下,此方法是在地图屏幕切换到日志条目详情屏幕之前被调用的。如果转场标识符是showMapDetail,并且转场目标是JournalEntryDetailViewController实例,则selectedAnnotation将被分配给JournalEntryDetailViewController实例的selectedJournalEntry属性。
- 构建并运行你的应用,并使用模拟器的功能 | 位置菜单验证位置是否已设置为Apple。你应该在地图屏幕上看到一个单个图钉。点击图钉以显示呼叫框,并在呼叫框内点击按钮。日志条目详情屏幕出现,并显示你在地图屏幕上点击的日志条目的详细信息。

图 17.21:模拟器显示日志条目详情屏幕
你已经将日志条目详情屏幕连接到地图屏幕,并且已成功从地图屏幕上选定的日志条目传递数据到日志条目详情屏幕。太棒了!在下一节中,你将配置日志条目详情屏幕以显示日志条目位置的地图快照。
在日志条目详情屏幕上显示地图快照
当前地图屏幕显示一个代表日志条目的单个图钉。当你点击地图屏幕上的图钉并点击呼叫按钮时,日志条目的详细信息将在日志条目详情屏幕上显示,但当前日志条目详情屏幕上的第二个图像视图显示的是一个占位符地图图像。你可以使用MKMapSnapshotter类捕获地图区域并将其转换为图像。
有关MKMapSnapshotter类的更多信息,请参阅developer.apple.com/documentation/mapkit/mkmapsnapshotter。
为了配置快照中捕获的地图的区域和外观,使用MKMapSnapshotter.Options对象。
有关MKMapSnapshotter.Options对象的更多信息,请参阅developer.apple.com/documentation/mapkit/mkmapsnapshotter/options。
您将在条目详情场景中的第二个图像视图中连接到JournalEntryDetailViewController类中的一个出口,并用显示日记条目位置的地图快照替换占位符图像。按照以下步骤操作:
-
在项目导航器中,点击
JournalEntryDetailViewController文件。在import UIKit语句之后输入以下内容以导入MapKit框架:import UIKit **import** **MapKit** -
在
JournalEntryDetailViewController类中的所有其他出口之后添加以下出口:@IBOutlet var bodyTextView: UITextView! @IBOutlet var photoImageView: UIImageView! **@IBOutlet****var****mapImageView****:** **UIImageView****!** var selectedJournalEntry: JournalEntry? -
在闭合花括号之前添加一个生成地图快照的方法:
// MARK: - Private methods private func getMapSnapshot() { guard let lat = selectedJournalEntry?.latitude, let long = selectedJournalEntry?.longitude else { self.mapImageView.image = nil return } let options = MKMapSnapshotter.Options() options.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: long), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) options.size = CGSize(width: 300, height: 300) options.preferredConfiguration = MKStandardMapConfiguration() let snapshotter = MKMapSnapshotter(options: options) snapshotter.start { snapshot, error in if let snapshot { self.mapImageView.image = snapshot.image } else if let error { print("snapshot error: \(error.localizedDescription)") } } }
此方法检查journalEntry的latitude和longitude属性是否有值。如果有,则创建、配置并分配一个MKMapSnapShotter.Options对象给一个MKMapSnapshotter对象。然后使用MKMapSnapshotter对象生成地图快照,该快照将被分配给mapImageView属性的image属性。
-
在
viewDidLoad()方法中调用getMapSnapshot()方法,在闭合花括号之前:photoImageView.image = journalEntry?.photo **getMapSnapshot****()** } -
在项目导航器中,点击主故事板文件,并在文档大纲中点击条目详情 场景。点击连接检查器按钮,将mapImageView出口连接到条目详情 场景中的第二个图像视图:

图 17.22:连接检查器显示地图 ImageView 出口
在文档大纲中的图像视图中拖动可能更容易。
- 构建并运行您的应用程序,并使用模拟器的功能 | 位置菜单验证位置是否已设置为苹果。您应该在地图屏幕上看到一个单独的图钉。点击图钉以显示呼出窗口,并点击呼出窗口内的按钮。日记条目详情屏幕出现,并显示您在地图屏幕上点击的日记条目的详细信息。向下滚动,您将在第二个图像视图中看到地图快照:

图 17.23:模拟器显示带有地图快照的日记条目详情屏幕
现在日记条目详情屏幕可以显示显示日记条目位置的地图快照。酷!
摘要
在本章中,您修改了添加新日记条目屏幕,以便用户可以将他们的当前位置添加到新的日记条目中。然后,您创建了一个MapViewController类,并配置它显示以您的位置为中心的自定义地图区域。然后,您更新了JournalEntry类以符合MKAnnotation协议。之后,您修改了MapViewController类以在地图区域内显示每个日记条目的图钉。您配置了图钉以显示呼出窗口,并在呼出窗口中配置了按钮,以便在点击时显示日记条目详情屏幕。最后,您修改了JournalEntryViewController类以在日记条目详情屏幕上显示日记条目的地图快照。
你现在知道了如何使用苹果的 Core Location 框架获取设备位置,如何使用苹果的 MapKit 框架创建自定义地图区域并显示地图标注,以及如何创建地图快照,这对于你计划构建使用地图的应用程序,如苹果地图或Waze,将非常有用。
在下一章中,你将学习如何创建共享数据实例,以及如何从 JSON 文件中加载数据和保存数据。
加入我们的 Discord!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything(问我任何问题)环节与作者聊天,等等。扫描二维码或访问链接加入社区。
第十八章:开始使用 JSON 文件
在上一章中,你修改了添加日志条目屏幕,以便用户可以将他们的当前位置添加到新的日志条目中,并配置地图屏幕以显示以你的当前位置为中心的区域以及代表日志条目位置的标记。然而,由于 MapViewController 实例无法访问 JournalListViewcontroller 实例中的 journalEntries 数组,新添加的日志条目不会在地图屏幕上作为标记出现。此外,当你退出应用程序时,所有新添加的日志条目都会丢失。
在本章中,你将创建一个 单例,SharedData,它将为日志列表和地图屏幕提供日志条目数据。此类还将用于在应用程序启动时从你的设备上的文件中加载日志条目数据,并在你添加或删除日志条目时将日志条目数据保存到你的设备上的文件中。
你将从创建 SharedData 类并配置你的应用程序使用它开始。接下来,你将修改 JournalEntry 类以兼容 JSON 格式,这样你就可以将日志条目保存到 JSON 文件中,并从 JSON 文件中加载日志条目。之后,你将添加方法在添加或删除日志条目时保存日志条目数据,以及在应用程序启动时加载日志条目数据。
到本章结束时,你将了解如何创建一个类来存储、加载和从 JSON 文件中保存数据,以便在你的应用程序中使用。
本章将涵盖以下主题:
-
创建一个单例
-
修改
JournalEntry类以兼容 JSON -
加载和保存 JSON 数据
技术要求
你将继续在上一章中修改的 JRNL 项目上工作。
本书代码包中的 Chapter18 文件夹包含本章的资源文件和完成的 Xcode 项目,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,了解代码的实际应用:
让我们从创建一个新的单例来存储你的应用程序使用的数据开始。
创建一个单例
目前,当你向你的应用程序中添加新的日志条目时,它们将出现在日志列表屏幕上,但当你切换到地图屏幕时,新添加的日志条目并不存在。这是因为 MapViewController 实例无法访问 JournalListViewcontroller 实例中的 journalEntries 数组。为了解决这个问题,你将创建一个新的单例来存储你的应用程序数据。单例只创建一次,然后在你的应用程序中引用。这意味着 JournalListViewController 类和 MapViewController 类将从单一来源获取数据。
关于单例的更多信息,请参阅developer.apple.com/documentation/swift/managing-a-shared-resource-using-a-singleton。
你将创建一个名为 SharedData 的单例,并配置 JournalListViewController 和 MapViewController 类以使用它。按照以下步骤操作:
- 在项目导航器中,将位于 Journal List Screen 组内的 Model 组移动到 SceneDelegate 文件下方的新位置:

图 18.1:模型组移动到新位置
这反映了模型对象不再仅由 Journal List 屏幕使用,而是被整个应用程序使用。
-
右键点击 Model 组,并选择 从模板新建文件...。
-
iOS 应已选中。选择 Swift 文件 并点击 下一步。
-
将文件命名为
SharedData,然后点击 创建。它将出现在项目导航器中,其内容将出现在编辑器区域。 -
将此文件的内容替换为以下代码以声明和定义
SharedData类:import **UIKit** class SharedData { // MARK: - Properties @MainActor static let shared = SharedData() private var journalEntries: [JournalEntry] = [] // MARK: - Initializers private init() { } // MARK: - Access methods func numberOfJournalEntries() -> Int { journalEntries.count } func journalEntry(at index: Int) -> JournalEntry { journalEntries[index] } func allJournalEntries() -> [JournalEntry] { journalEntries } func addJournalEntry(_ newJournalEntry: JournalEntry) { journalEntries.insert(newJournalEntry, at: 0) } func removeJournalEntry(at index: Int) { journalEntries.remove(at: index) } }
让我们分解一下:
@MainActor static let shared = SharedData()
此语句创建此类的单个实例,这意味着你的应用程序中 SharedData 的唯一实例存储在 shared 属性中。此属性标记为 @MainActor 以确保它只能从主队列访问。
更多信息,请观看 Apple 的 WWDC 2022 视频标题为 使用 Swift Concurrency 消除数据竞争,在此处:developer.apple.com/videos/play/wwdc2022/110351/。
private var journalEntries: [JournalEntry] = []
此语句创建一个名为 journalEntries 的空数组,该数组将用于存储 JournalEntry 实例。私有关键字意味着 journalEntries 数组只能由 SharedData 类中的方法修改。这是为了确保应用程序的任何其他部分都不能更改 journalEntries 数组。
private init() {
}
init() 方法体为空。这防止了意外创建 SharedData() 实例。
func numberOfJournalEntries() -> Int {
journalEntries.count
}
此方法返回 journalEntries 数组中的项目数量。
func journalEntry(at index: Int) -> JournalEntry {
journalEntries[index]
}
此方法返回位于 journalEntries 数组指定索引处的 JournalEntry 实例。
func allJournalEntries() -> [JournalEntry] {
journalEntries
}
此方法返回 JournalEntries 数组的副本。
func addJournalEntry(_ newJournalEntry: JournalEntry) {
journalEntries.insert(newJournalEntry, at: 0)
}
此方法将传入的 JournalEntry 实例插入到 JournalEntries 数组的索引 0 处。
func removeJournalEntry(at index: Int) {
journalEntries.remove(at: index)
}
此方法从 JournalEntries 数组中移除指定索引处的 JournalEntry 实例。
现在您已创建了 SharedData 类,您将修改应用程序以使用它。按照以下步骤操作:
-
在项目导航器中,点击 JournalListViewController 文件。从
JournalListViewController类中移除journalEntries属性://MARK: - Properties @IBOutlet var tableView: UITableView! **private var****journalEntries****: [****JournalEntry****] = []** **// remove** -
在
viewDidLoad()方法中,移除创建样本数据并将其追加到journalEntries数组的语句:override func viewDidLoad() { super.viewDidLoad() **journalEntries** **=****JournalEntry****.****createSampleJournalEntryData****()** **// remove** } -
修改
tableView(_:numberOfRowsInSection:)方法以从SharedData获取表格视图的行数:func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { **SharedData****.****shared****.****numberOfJournalEntries****()** } -
修改
tableView(_:cellForRowAt:)方法以从SharedData获取所需的JournalEntry实例:let journalCell = tableView.dequeueReusableCell(withIdentifier: "journalCell", for: indexPath) as! JournalListTableViewCell **let** **journalEntry** **=****SharedData****.****shared****.****journalEntry****(****at****: indexPath.****row****)** journalCell.photoImageView.image = journalEntry.photo -
修改
tableView(_:commit:forRowAt:)方法以从SharedData中移除选中的JournalEntry实例:if editingStyle == .delete { **SharedData****.****shared****.****journalEntry****(****at****: indexPath.****row****)** tableView.reloadData() } -
修改
prepare(for:sender:)方法以使用SharedData获取选中的JournalEntry实例:let selectedJournalEntry = **SharedData****.****shared****.****journalEntry****(****at****: indexPath.****row****)** journalEntryDetailViewController.selectedJournalEntry = selectedJournalEntry -
修改
unwindNewEntrySave(segue:)方法以向SharedData添加新的JournalEntry实例:if let sourceViewController = segue.source as? AddJournalEntryViewController, let newJournalEntry = sourceViewController.newJournalEntry { **SharedData****.****shared****.****addjournalEntry****(newJournalEntry)** tableView.reloadData() }
你已经对 JournalListViewController 类做了所有必要的修改。现在你将修改 MapViewController 类以使用 SharedData。正如前一章所述,当在实际设备上运行你的应用时,确定设备位置需要很长时间,如果用户的位置发生变化,地图屏幕上的地图将不会更新。你将解决这两个问题。按照以下步骤操作:
-
在项目导航器中,点击 MapViewController 文件。从
MapViewController类中移除annotations属性://MARK: - Properties @IBOutlet var mapView: MKMapView! let locationManager = CLLocationManager() private var locationTask: Task<Void, Error>? **private var****annotations****:** **[****JournalEntry****]****= []** **// remove** var selectedJournalEntry: JournalEntry? -
通过删除高亮语句来修改
viewDidLoad()方法:override func viewDidLoad() { super.viewDidLoad() **fetchUserLocation****()** **// remove** mapView.delegate = self **annotations** **=** **JournalEntry****.** **createSampleJournalEntryData****()** **// remove** **mapView****.****addAnnotations****(****annotations****)** **// remove** } -
为了减少确定用户位置所需的时间,向
fetchUserLocation()中添加一个语句,如图所示,将位置管理器实例的精度设置为kCLLocationAccuracyKilometer:locationManager.requestWhenInUseAuthorization() **locationManager****.****desiredAccuracy****=****kCLLocationAccuracyKilometer** self.navigationItem.title = "Getting location..."
此属性的默认值为 kCLLocationAccuracyBest,它需要相对较长的时间来确定。这种权衡是可以接受的,因为 JRNL 应用在显示地图上的注释时不需要最高级别的精度。
-
为了在地图屏幕出现时更新用户的位置,首先在
viewDidLoad()方法之后实现以下方法:override func viewIsAppearing(_ animated: Bool) { super.viewIsAppearing(animated) fetchUserLocation() }
viewIsAppearing() 视图控制器生命周期方法是在 WWDC 2023 上引入的。你可以通过此链接了解更多关于此方法的信息:developer.apple.com/documentation/uikit/uiviewcontroller/4195485-viewisappearing。
-
在
updateMapWithLocation(_:)方法中添加以下语句,如图所示,以便在确定用户位置并设置地图区域后,地图视图从SharedData获取所有注释:mapView.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: long), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) **mapView****.****addAnnotations****(****SharedData****.****shared** **.****allJournalEntries****())** } }
通过这个修改,如果你在期刊列表屏幕上,点击 Map 标签栏按钮将更新用户的位置,在地图屏幕上重新绘制地图,并重新加载地图注释。
你已经对 MapViewController 类做了所有必要的修改。现在让我们测试你的应用。按照以下步骤操作:
- 启动模拟器,并在模拟器的 Features 菜单中选择 Location | Apple 来模拟位置。构建并运行你的应用。
点击 + 按钮添加一个新的期刊条目。确保 Get Location 开关处于开启状态:

图 18.2:模拟器显示添加新期刊条目屏幕
- 点击 Map 标签按钮进入地图屏幕:

图 18.3:模拟器显示地图标签按钮
- 注意您之前添加的日记条目在地图屏幕上作为图钉可见。点击图钉,然后点击呼叫按钮:

图 18.4:模拟器显示图钉呼叫按钮
日记条目详情显示在日记条目详情屏幕上:

图 18.5:模拟器显示日记条目详情屏幕
您已成功创建了一个单例并配置了您的应用使用它,但一旦应用退出,数据就会丢失。稍后,您将编写代码将日记条目保存到您的设备存储。但在您能够这样做之前,您将修改 JournalEntry 类,以便其中的数据可以以 JSON 格式存储。您将在下一节中这样做。
将 JournalEntry 类修改为与 JSON 兼容
目前,当您退出应用时,所有应用数据都会丢失。您需要实现一种保存应用数据的方法。iOS 提供了许多存储应用数据的方式。其中之一是将数据转换为 JavaScript 对象表示法(JSON)格式,然后将其作为文件写入您的设备存储。JSON 是一种在文件中结构化数据的方式,可以很容易地被人和计算机读取。
为了帮助您理解 JSON 格式,请查看下面的示例:
[
{
"dateString": "May 17, 2023"
"rating": 5
"entryTitle": "Good"
"entryBody": "Today is a good day"
"photoData": "<photo data for the sun.max image>"
"latitude":
"longitude":
},
{
"dateString": "May 17, 2023"
"rating": 0
"entryTitle": "Bad"
"entryBody": "Today is a bad day"
"photoData": "<photo data for the cloud image>"
"latitude": 37.331354
"longitude": -122.031791
},
{
"dateString": "May 17, 2023"
"rating": 3
"entryTitle": "Good"
"entryBody": "Today is a good day"
"photoData": "<photo data for the cloud.sun>"
"latitude":
"longitude":
}
]
此示例是 JSON 格式中 journalEntries 数组的表示。如您所见,它以一个开方括号开始,每个内部项目都由包含日记条目信息的键值对组成,这些键值对被花括号包围并用逗号分隔。
在文件的最后,您可以看到一个闭方括号。方括号表示数组,花括号表示字典。字典中的键对应于 JournalEntry 实例中的属性,而值对应于分配给这些属性的值。
要了解更多关于在 Swift 类型中使用 JSON 的信息,请参阅 developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types。
要了解更多关于解析 JSON 文件的信息,请观看这里可用的视频:devstreaming-cdn.apple.com/videos/wwdc/2017/212vz78e2gzl2/212/212_hd_whats_new_in_foundation.mp4。
在将自定义 Swift 类型转换为 JSON 或从 JSON 转换之前,它需要遵守 Codable 协议。
要了解更多关于 Codable 的信息,请参阅 developer.apple.com/documentation/swift/codable。
JSON 支持日期、字符串、数字、布尔值和 null 值,但不支持图像。为了遵循 Codable 协议,你需要修改 JournalEntry 类以使用 JSON 支持的类型,并修改应用的其他部分以与更新的 JournalEntry 实例一起工作。按照以下步骤操作:
-
在项目导航器中,点击 JournalEntry 文件。按照以下方式修改
JournalEntry类声明以采用Codable协议:class JournalEntry: NSObject, MKAnnotation**,** **Codable** { -
将会显示一个错误,因为
UIImage类型不遵循Codable协议。按照以下方式修改photo属性,使JournalEntry遵循Codable协议:let date: Date let rating: Int let entryTitle: String let body: String let **photoData****:** **Data****?** let latitude: Double? let longitude: Double?
错误将会消失,但在初始化器中会出现另一个错误。
-
按照以下方式修改初始化器:
self.date = Date() self.rating = rating self.entryTitle = title self.body = body self.photoData = **photo?.jpegData(compressionQuality:** **1.0****)**
这将 photo 参数中的值转换为 Data 实例,并将其分配给 photoData。
要了解更多关于 Data 类型的信息,请参阅 developer.apple.com/documentation/foundation/data。
初始化器中的所有错误都已消失,但如果你现在构建你的应用,你会看到其他错误出现。让我们现在修复它们。按照以下步骤操作:
-
在项目导航器中,点击 JournalListViewController 文件。按照以下方式修改
JournalListViewController类中的tableView(_:cellForRowAt:)方法:let journalEntry = SharedData.shared.getJournalEntry(index: indexPath.row) **if****let** **photoData** **=** **journalEntry.****photoData** **{** **journalCell.****photoImageView****.****image****=****UIImage****(****data****: photoData)** **}** journalCell.dateLabel.text = journalEntry.date.formatted( .dateTime.month().day().year() ) journalCell.titleLabel.text = journalEntry.entryTitle return journalCell
更新后的代码将存储在 photoData 中的数据转换回 UIImage,并将其分配给 journalCell 中的图像视图。
-
在项目导航器中,点击 JournalEntryDetailViewController 文件。按照以下方式修改
viewDidLoad()方法:override func viewDidLoad() { super.viewDidLoad() dateLabel.text = selectedJournalEntry?.date.formatted( .dateTime.day().month(.wide).year() ) titleLabel.text = selectedJournalEntry?.entryTitle bodyTextView.text = selectedJournalEntry?.entryBody **if****let** **photoData** **=****selectedJournalEntry****?****.****photoData** **{** **photoImageView****.****image****=****UIImage****(****data****: photoData)** **}** getMapSnapshot() }
更新后的代码将存储在 photoData 中的数据转换为 UIImage 实例,并将其分配给 photoImageView 的 image 属性。在这个阶段,你的应用中应该不再有错误。
- 构建并运行你的应用。验证模拟位置是否已设置,并添加一个新的日志条目。你的应用应该像之前一样工作。
注意,图像现在是黑色而不是蓝色。这是由于图像转换过程造成的,当你使用相机或照片库中的图像时,这不会引起注意。你将在 第二十章,开始使用相机和照片库 中学习如何做到这一点。
你已经成功修改了 JournalEntry 类以遵循 Codable 协议,并且已经解决了应用中的所有错误。在下一节中,你将实现保存和加载数据,这样在退出应用时数据就不会丢失。
加载和保存 JSON 数据
现在你已经修改了 JournalEntry 类以遵循 Codable 协议,你就可以开始实现从 JSON 文件加载数据和保存数据了。
为了让你更容易地处理 JSON 文件,Apple 提供了 JSONDecoder 和 JSONEncoder 类。
JSONDecoder 实例从 JSON 对象解码数据类型实例,你将在从设备存储加载文件时使用它。
要了解更多关于 JSONDecoder 的信息,请参阅 developer.apple.com/documentation/foundation/jsondecoder。
JSONEncoder 实例将数据类型实例编码为 JSON 对象,您将在将文件保存到设备存储时使用它。
要了解更多关于 JSONEncoder 的信息,请参阅 developer.apple.com/documentation/foundation/jsonencoder。
现在,您将在 SharedData 类中实现从文件加载数据和将数据保存到文件的方法。按照以下步骤操作:
-
在项目导航器中,点击 SharedData 文件。在
SharedData类中,在闭合花括号之前实现一个获取您可以在设备存储上加载或保存文件的位置的方法:// MARK: - Persistence func documentDirectory() -> URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! }
这与在您的 Mac 上获取家目录中 Documents 目录的路径类似。
要了解更多关于访问 iOS 文件系统的信息,请参阅 developer.apple.com/documentation/foundation/filemanager。
-
在
documentDirectory()方法之后实现一个从设备存储上的文件加载日记条目的方法:func loadJournalEntriesData() { let pathDirectory = documentDirectory() let fileURL = pathDirectory. appendingPathComponent("journalEntriesData.json") do { let data = try Data(contentsOf: fileURL) let entries = try JSONDecoder().decode( [JournalEntry].self, from: data) journalEntries = entries } catch { print("Failed to read JSON data: \(error.localizedDescription)") } }
此方法使用 documentDirectory() 方法获取可以从中加载文件的位置。然后它指定一个文件名,journalEntriesData.json,数据将被保存并附加到路径上。然后它尝试加载该文件。如果成功,它尝试将数据解码为 JournalEntry 实例的数组并将其分配给 journalEntries 数组。
-
在
loadJournalEntriesData()方法之后实现一个将日记条目保存到设备存储上的文件的方法:func saveJournalEntriesData() { let pathDirectory = documentDirectory() do { try? FileManager().createDirectory(at: pathDirectory, withIntermediateDirectories: true) let filePath = pathDirectory.appendingPathComponent( "journalEntriesData.json") let json = try JSONEncoder().encode(journalEntries) try json.write(to: filePath) } catch { print("Failed to write JSON data: \(error.localizedDescription)") } }
此方法使用 documentDirectory() 方法获取可以保存文件的位置。然后它指定一个要保存数据的文件名并将其附加到路径上。然后它尝试使用 JSONEncoder 实例将 journalEntries 数组编码为 JSON 格式,并将其写入之前指定的文件。
您已经在您的应用中实现了加载数据和保存日记条目的方法。现在,您将修改 JournalListViewController 类,在适当的时候调用这些方法。按照以下步骤操作:
-
在项目导航器中,点击 JournalListViewController 文件。在
JournalListViewController类中,修改viewDidLoad()以调用loadJournalEntriesData()方法:override func viewDidLoad() { super.viewDidLoad() **SharedData****.****shared****.****loadJournalEntriesData****()** }
这将在应用启动时加载任何已保存的日记条目。
-
修改
tableView(_:commit:forRowAt:)方法,在从表格视图中删除一行后调用saveJournalEntriesData()方法:if editingStyle == .delete { SharedData.shared.removeJournalEntry(at: indexPath.row) **SharedData****.****shared****.****saveJournalEntriesData****()** tableView.reloadData() } -
修改
unwindNewEntrySave(segue:)方法,在添加新的日记条目后调用saveJournalEntriesData()方法:if let sourceViewController = segue.source as? AddJournalEntryViewController, let newJournalEntry = sourceViewController.newJournalEntry { SharedData.shared.addJournalEntry(newJournalEntry) **SharedData****.****shared****.****saveJournalEntriesData****()** tableView.reloadData() } -
构建并运行您的应用。验证已设置模拟位置,并添加一个新的日记条目。您的应用应该像之前一样工作。
-
停止你的应用程序并重新运行它。你之前添加的日记条目应该仍然存在:

图 18.6:模拟器显示应用程序中的持久化应用程序数据
如果你正在模拟器中运行你的应用程序,你可以在saveJournalEntriesData()方法中使用print(filePath)语句将文件路径打印到调试区域。这将告诉你journalEntriesData.json文件在您的 Mac 上保存的位置。
你已经成功实现了使用 JSON 文件为你的应用程序保存和加载!做得好!
摘要
在本章中,你创建了一个单例SharedData,并配置了你的应用程序使用它。接下来,你修改了JournalEntry类以兼容 JSON 格式,这样你就可以将日记条目保存到 JSON 文件中,并从 JSON 文件中加载日记条目。之后,你添加了在添加或删除日记条目时保存日记条目数据的方法,以及在应用程序启动时加载日记条目数据的方法。
你现在知道如何创建一个类来存储、加载和保存数据,以便在您的应用程序中使用 JSON 文件。
在下一章中,你将实现一个自定义用户界面元素,允许你为日记条目设置星级评分。
加入我们的 Discord!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,等等。扫描二维码或访问链接加入社区。
第十九章:开始使用自定义视图
到目前为止,您的 JRNL 应用已经可以正常工作。所有屏幕都正常工作,但由于缺少评分用户界面元素,您无法设置如应用演示中所示的星星评分。您也无法设置自定义图片,但这将在 第二十章,开始使用相机和照片库 中解决。
到目前为止,您一直在使用苹果的标准 UI 元素。在本章中,您将创建一个 UIStackView 类的 自定义视图 子类,该子类以星星的形式显示日记条目的评分,并且您将修改这个子类,以便用户可以通过点击来设置日记条目的评分。之后,您将在“添加新日记条目”屏幕上实现它。最后,您将在“日记条目详情”屏幕上实现它。
到本章结束时,您将学会如何为您的应用创建自定义视图。
本章将涵盖以下主题:
-
创建自定义
UIStackView子类 -
将您的自定义视图添加到“添加新日记条目”屏幕
-
将您的自定义视图添加到“日记条目详情”屏幕
技术要求
您将继续在上一章中修改的 JRNL 项目上工作。
本章完成的 Xcode 项目位于本书代码包的 Chapter19 文件夹中,可以通过以下链接下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
观看以下视频以查看代码的实际效果:
让我们先学习如何创建一个自定义 UIStackView 子类,该子类将在屏幕上显示星星评分。
创建自定义 UIStackView 子类
到目前为止,您只使用了苹果预定义的 UI 元素,例如标签和按钮。您所需要做的只是点击库按钮,搜索您想要的元素,并将其拖入故事板。然而,在某些情况下,苹果提供的对象可能不适合或不存在。在这种情况下,您将需要自己构建。让我们回顾一下您在应用演示中看到的“添加新日记条目”屏幕:

图 19.1:显示星星评分的“添加新日记条目”屏幕
您可以看到一组五个星星位于 获取位置 开关的上方。目前,Main 故事板文件中的 新条目场景 和 条目详情场景 有占位符视图对象,星星应该在那里。您将创建 RatingView 类,这是一个 UIStackView 类的定制子类,您将在两个场景中使用它。这个类的实例将显示评分作为星星。
在本章的剩余部分,RatingView 类的实例将被称为评分视图(与 UIButton 类的实例被称为按钮的方式相同)。
让我们从创建 UIStackView 类的子类开始。按照以下步骤操作:
- 在项目导航器中,右键单击JRNL文件夹,从弹出菜单中选择新建组。将此组命名为视图。将此组移动到模型组下方的新位置:

图 19.2:项目导航器显示视图组位于模型组下方
-
右键单击视图文件夹,从弹出菜单中选择从模板新建文件...。
-
iOS应该已经选中。选择Cocoa Touch 类然后点击下一步。
-
按照以下方式配置文件:
-
类:
RatingView -
子类:
UIStackView -
语言:
Swift
-
点击下一步。
-
点击创建。
RatingView文件将出现在项目导航器中。 -
删除此文件中的所有注释代码,并在
RatingView类声明之后输入以下内容以声明类的属性:// MARK: - Properties private var ratingButtons = [UIButton()] var rating = 0 private let buttonSize = CGSize(width: 44.0, height: 44.0) private let buttonCount = 5
ratingButtons属性是一个数组,将包含此类中所有的按钮。
rating属性用于存储日记条目的评分。它决定了将要绘制多少个星形以及星形的类型。例如,如果rating包含3,评分视图将显示三个填充星形和两个空星形。
buttonSize属性确定要在屏幕上绘制的按钮的高度和宽度。
buttonCount属性确定要在屏幕上绘制的按钮总数。
-
在属性声明之后实现此类的初始化器:
// MARK: - Initialization required init(coder: NSCoder) { super.init(coder: coder) } -
在初始化器之后实现一个在屏幕上绘制星形的方法:
// MARK: - Private methods private func setupButtons() { for button in ratingButtons { removeArrangedSubview(button) button.removeFromSuperView() } ratingButtons.removeAll() let filledStar = UIImage(systemName:"star.fill" ) let emptyStar = UIImage(systemName: "star") let highlightedStar = UIImage(systemName: "star.fill")?.withTintColor(.red, renderingMode: .alwaysOriginal) for _ in 0..<buttonCount { let button = UIButton() button.setImage(emptyStar, for: .normal) button.setImage(filledStar, for: .selected) button.setImage(highlightedStar, for: .highlighted) button.setImage(highlightedStar, for: [.highlighted, .selected]) button.translatesAutoresizingMaskIntoConstraints = false button.heightAnchor.constraint(equalToConstant: buttonSize.height).isActive = true button.widthAnchor.constraint(equalToConstant: buttonSize.width).isActive = true addArrangedSubview(button) ratingButtons.append(button) } }
让我们分解一下:
for button in ratingButtons {
removeArrangedSubview(button)
button.removeFromSuperView()
}
ratingButtons.removeAll()
这些语句从堆叠视图中移除任何现有的按钮和ratingButtons数组。
let filledStar = UIImage(systemName:"star.fill" )
let emptyStar = UIImage(systemName: "star")
let highlightedStar =
UIImage(systemName: "star.fill")?.withTintColor(.red,
renderingMode: .alwaysOriginal)
这些语句从苹果的SFSymbols库中的符号创建三个UIImage实例。filledStar将存储填充星形的图像,emptyStar将存储星形轮廓的图像,而highlightedStar将存储被着色为红色的填充星形的图像。
更多关于苹果的SFSymbols库的信息,请参阅developer.apple.com/design/human-interface-guidelines/sf-symbols。
for _ in 0..<buttonCount {
由于buttonCount设置为5,这个for循环将重复五次。
let button = UIButton()
此语句将UIButton的一个实例分配给button。
更多关于UIButton的信息,请参阅developer.apple.com/documentation/uikit/uibutton。
button.setImage(emptyStar, for: .normal)
button.setImage(filledStar, for: .selected)
button.setImage(highlightedStar, for: .highlighted)
button.setImage(highlightedStar, for: [.highlighted, .selected])
这些语句设置了UIButton实例的不同状态下的图像。.normal状态显示星形轮廓。在.selected状态下,显示填充星形。如果你点击UIButton实例,它将处于.highlighted状态或.highlighted和.selected状态,这取决于点击之前它是在.normal状态还是.selected状态。然后显示带有红色色调的填充星形。
button.translatesAutoresizingMaskIntoConstraints =
false
button.heightAnchor.constraint(equalToConstant: buttonSize.height).isActive = true
button.widthAnchor.constraint(equalToConstant: buttonSize.width).isActive = true
这些语句设置了按钮的大小。第一条语句将UIButton实例的translatesAutoresizingMaskIntoConstraints属性设置为false;否则,系统将创建一组约束,这些约束会重复视图自动调整大小掩码中指定的行为,你将无法设置自己的约束。接下来的两个语句通过使用存储在buttonSize中的值以编程方式设置实例的高度和宽度约束。
addArrangedSubview(button)
这条语句将UIButton实例作为子视图以编程方式添加到堆叠视图中。
ratingButtons.append(button)
这条语句将UIButton实例添加到ratingButtons数组中。
-
在初始化器中调用
setupButtons()方法:required init(coder: NSCoder) { super .init(coder: coder) **setupButtons****()** }
当评分视图初始化时,这将在屏幕上绘制评分视图。
你已经创建了一个名为RatingView的自定义UIStackView子类,并且已经添加了代码使其在屏幕上绘制五个星星。现在让我们添加代码,使用户能够在评分视图中的星星被点击时更改评分。按照以下步骤操作:
-
实现一个方法,在
setupButtons()方法之后,当在ratingButtons数组中的按钮被点击时,更改评分视图的rating属性:@objc private func ratingButtonTapped(_ button: UIButton) { guard let index = ratingButtons.firstIndex(of: button) else { fatalError("The button, \(button), is not in the ratingButtons array: \(ratingButtons)") } let selectedRating = index + 1 if selectedRating == rating { rating = 0 } else { rating = selectedRating } }
当ratingButtons数组中的按钮被点击时,guard语句将按钮的索引分配给index。然后selectedRating被设置为index + 1 存储的值。如果rating属性的值与selectedRating相同,则将其设置为0;否则,将其设置为与selectedRating相同的值。
例如,假设你在评分视图中的第三个星星上点击。由于第三个星星是ratingButtons数组中的第三个元素,index将被设置为2,selectedRating将被设置为 2 + 1 = 3。假设rating属性的初始值是0,selectedRating == rating将返回false,因此rating属性的值将被设置为3。
-
在设置约束的语句之后,在
setupButtons方法的for循环中将此方法分配为按钮动作:button.widthAnchor.constraint(equalToConstant: starSize.width).isActive = true **button.****addTarget****(****self****, action:** **#selector****(****RatingView****.****ratingButtonTapped****(****_****:)),** **for****: .****touchUpInside****)** addArrangedSubview(button) -
添加一个方法来根据在闭合花括号之前设置的评分来改变按钮的状态:
private func updateButtonSelectionStates() { for (index, button) in ratingButtons.enumerated() { button.isSelected = index < rating } }
为了了解这是如何工作的,让我们假设rating属性被设置为3。每个按钮的默认状态是.normal。
第一个按钮位于索引0,因此button.isSelected是 0 < 3,返回true。由于.selected状态下的图像是一个填充的星星,因此这个按钮的图像被设置为填充的星星。对于接下来的两个按钮也是如此。
第四个按钮位于索引3,因此button.isSelected是 3 < 3,返回false。这意味着按钮的状态保持为.normal。.normal状态下的图像是一个星星轮廓,因此这个按钮的图像被设置为星星轮廓。对于第五个按钮也是如此。
简而言之,当rating属性设置为3时,评分视图显示前三个带有填充星星的按钮,其余两个按钮带有星星轮廓。
-
每当
rating属性的值发生变化时,都需要调用updateButtonSelectionStates()方法。为此,按如下方式修改rating属性:var rating = 0 **{** **didSet** **{** **updateButtonSelectionStates****()** **}** **}**
这被称为属性观察者,每当rating属性的值发生变化时,updateButtonSelectionStates()方法都会被调用。
您已完成了评分视图的实现。在下一节中,您将将其添加到添加新日志条目屏幕。
将您的自定义视图添加到添加新日志条目屏幕
到目前为止,您已在项目中创建了一个新的RatingView类,并配置它在其中的星形按钮被点击时设置其rating属性。在本节中,您将设置新条目场景中位于获取位置开关上方的堆栈视图对象的身份为RatingView类,在AddJournalEntryViewController类中为其配置一个出口,并在创建新日志条目时添加使用rating属性值的代码。按照以下步骤操作:
-
在项目导航器中,点击AddJournalEntryViewController文件。在所有其他属性声明之后,在
AddJournalEntryViewController类中为评分视图添加一个新的出口:@IBOutlet var getLocationSwitch: UISwitch! @IBOutlet var getLocationSwitchLabel: UILabel! **@IBOutlet****var****ratingView****:** **RatingView****!** -
修改
prepare(for:sender:)方法,在创建新日志条目时获取评分视图的rating属性值:let photo = photoImageView.image let rating = **ratingView****.****rating** let lat = currentLocation?.coordinate.latitude -
点击主故事板文件,并在文档大纲中选择新条目场景。如图所示,点击位于获取位置开关上方的
UIStackView对象:

图 19.3:编辑区域显示位于获取位置开关上方的 UIStackView 对象
- 点击身份检查器按钮。在自定义类下,将类设置为
RatingView:

图 19.4:身份检查器,类设置为 RatingView
- 点击属性检查器按钮。验证堆栈视图下的设置,并在视图下将背景设置为
Default:

图 19.5:背景设置为默认的属性检查器
- 在文档大纲中点击新条目场景,然后点击连接检查器按钮。将
ratingView出口连接到新条目场景中的评分视图:

图 19.6:连接检查器显示 ratingView 出口
- 构建并运行您的应用。点击+按钮进入添加新日志条目屏幕,您将看到评分视图显示在获取位置开关上方。添加日志条目标题、正文和评分,然后点击保存:

图 19.7:模拟器显示在添加新日志条目屏幕上的评分视图
带有评分的新日志条目现在在日志列表屏幕上可见。在下一节中,您将修改日志条目详情屏幕以显示此日志条目的评分。
将您的自定义视图添加到日志条目详情屏幕
到目前为止,你可以在使用“添加新日记条目”屏幕创建新日记条目时设置评分,但你设置的评分在“日记条目详情”屏幕上不可见。你将为评分视图添加一个输出,并修改JournalEntryDetailViewController类中的代码,你将在条目详情场景中添加一个评分视图。
按照以下步骤操作:
-
在项目导航器中,点击JournalEntryDetailViewController文件。在其他属性声明之后添加一个评分视图的输出:
@IBOutlet var photoImageView: UIImageView! @IBOutlet var mapImageView: UIImageView! **@IBOutlet****var****ratingView****:** **RatingView****!** -
修改
viewDidLoad()方法中的代码以设置评分视图的rating属性:super.viewDidLoad() dateLabel.text = selectedJournalEntry?.date.formatted( .dateTime.day().month(.wide).year() ) **ratingView****.****rating****=****selectedJournalEntry****?****.****rating****??****0** titleLabel.text = selectedJournalEntry?.entryTitle -
点击Main故事板文件,然后在文档大纲中点击Entry Detail Scene。选择第二个表格视图单元格中的堆叠视图。点击身份检查器按钮,并在自定义类下将类设置为
RatingView:

图 19.8:身份检查器,类设置为 RatingView
- 点击属性检查器按钮,并验证堆叠视图下的设置。在视图下,取消选中用户交互启用复选框(因为用户不应该能够在“日记条目详情”屏幕上更改评分),并将背景设置为
默认:

图 19.9:背景设置为默认的属性检查器
- 在文档大纲中点击Entry Detail Scene,然后点击连接检查器按钮。将ratingView输出连接到Entry Detail场景中的评分视图:

图 19.10:连接检查器显示 ratingView 输出
- 构建并运行你的应用程序。点击上一节中添加的日记条目,你将在“日记条目详情”屏幕上的评分视图中看到评分:

图 19.11:模拟器显示“日记条目详情”屏幕上的评分视图
你已经在“日记条目详情”屏幕上成功添加并配置了评分视图!做得好!
摘要
在本章中,你创建了一个自定义的UIStackView类子类,以星形的形式显示日记条目的评分,并且修改了这个子类,使用户可以通过点击来设置日记条目的评分。之后,你将其添加到“添加新日记条目”屏幕中。最后,你在“日记条目详情”屏幕上实现了它。
现在,你知道如何为你的应用程序创建自定义视图。
在下一章中,你将学习如何处理来自相机或照片库的照片。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读这本书。提问、为其他读者提供解决方案、通过“问我任何问题”环节与作者聊天,等等。扫描二维码或访问链接加入社区。
第二十章:开始使用相机和相册
在上一章中,你创建了RatingView类并将其添加到了“添加新日记条目”和“日记条目详情”屏幕中。
在本章中,你将通过添加用户从相机或相册获取照片的方式,来完成“添加新日记条目”屏幕的实现。你将首先在“新条目场景”中的图像视图上添加一个轻触手势识别器,并将其配置为显示一个图像选择器控制器实例。然后,你将实现UIImagePickerControllerDelegate协议中的方法,这允许你从相机或相册获取照片,并在将其保存到日记条目实例之前将其缩小。你还将修改Info.plist文件,以便你可以访问相机或相册。
到本章结束时,你将学会如何在你的应用中访问相机或相册。
本章将涵盖以下主题:
-
创建一个新的
UIImagePickerController实例 -
实现
UIImagePickerControllerDelegate方法 -
获取使用相机或相册的权限
技术要求
你将继续在上一章中修改的JRNL项目中工作。
本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter20文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
观看以下视频,以查看代码的实际效果:
让我们先修改“添加新日记条目”屏幕,以显示一个图像选择器控制器,这允许你使用设备相机或从用户的相册中选择照片。
创建一个新的 UIImagePickerController 实例
为了让用户更容易使用相机或相册,Apple 实现了UIImagePickerController类。这个类管理了系统接口,用于拍照和从用户的相册中选择项目。这个类的实例被称为图像选择器控制器,它可以在屏幕上显示图像选择器。
如果你曾经向社交媒体帖子中添加过照片,你将看到图像选择器的样子。它通常显示来自你的相机的视图或来自你的相册的照片网格,然后你可以选择一张照片添加到你的帖子中:

图 20.1:模拟器显示图像选择器
要了解更多关于UIImagePickerController类的信息,请参阅developer.apple.com/documentation/uikit/uiimagepickercontroller。
在“添加新日志条目”屏幕上显示图片选择器,你需要在“新条目场景”中的图片视图上添加一个轻点手势识别器实例,并且当图片视图被轻点时,你需要添加一个创建并显示图片选择器控制器的方法。按照以下步骤操作:
-
你应该在助理编辑器中看到
AddJournalEntryViewController文件的全部内容。Ctrl + 拖动文档大纲中的轻点手势识别器到locationSwitchValueChanged(_:)方法和闭合花括号之间的空间: -
![图片]()
这条语句将图片选择器控制器的delegate属性设置为AddJournalEntryViewController实例。

- 如果需要更多工作空间,请点击导航器和检查器按钮。点击调整编辑器选项按钮,并从弹出菜单中选择助理:
图 20.4:调整编辑器选项菜单,选择助理

- 在项目导航器中,点击主故事板文件。在文档大纲中点击新条目场景。


- 这条语句创建了一个
UIImagePickerController类的实例,并将其分配给imagePickerController。
点击新条目场景中的图片视图。点击属性检查器按钮,在视图下勾选用户交互启用复选框:
让我们分解这个过程:
- 在弹出对话框中,将名称设置为
getPhoto,将类型设置为UITapGestureRecognizer。点击连接:

你已成功将轻点手势识别器添加到新条目场景中的图片视图,并将其链接到AddJournalEntryViewController类中的getPhoto()方法。现在你将修改getPhoto()方法以创建并显示一个UIImagePickerController实例。
- 图 20.3:选择轻点手势识别器对象的库
在项目导航器中,点击AddJournalEntryViewController文件。在文件中所有其他代码之后添加一个新扩展,使AddJournalEntryViewController类声明符合UIImagePickerControllerDelegate和UINavigationControllerDelegate协议:
图 20.7:助理编辑器显示 getPhoto(_:)方法

图 20.2:属性检查器显示用户交互启用复选框
-
按照以下步骤操作:
extension AddJournalEntryViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { } -
按照以下方式修改
getPhoto()方法:@IBAction func getPhoto(_ sender: UITapGestureRecognizer) { **let****imagePickerController****=****UIImagePickerController****()** **imagePickerController.****delegate****=****self** **#if****targetEnvironment(simulator)** **imagePickerController.****sourceType****=****.****photoLibrary** **#else** **imagePickerController.****sourceType** **= .****camera** **imagePickerController.****showsCameraControls** **=** **true** **#endif** **present****(imagePickerController,** **animated****:** **true****)** }点击库按钮以显示库。在过滤器字段中输入
tap。一个轻点手势识别器对象将作为结果之一出现。将其拖到图片视图:let imagePickerController = UIImagePickerController()验证
getPhoto(_:)方法是否已在AddJournalEntryViewController类中创建。点击x关闭助理编辑器窗口:imagePickerController.delegate = self![图片]()
#if targetEnvironment(simulator) imagePickerController.sourceType = .photoLibrary #else imagePickerController.sourceType = .camera imagePickerController.showsCameraControls = true #endif这段代码块被称为条件编译块。它从
#if编译指令开始,以#endif编译指令结束。如果你在模拟器中运行,只有设置图像选择控制器sourceType属性为相册库的语句会被编译。如果你在一个实际设备上运行,设置图像选择控制器
sourceType属性为相机并显示相机控制的语句会被编译。这意味着当在模拟器中运行时,图像选择控制器将使用相册库;当在实际设备上运行时,将使用相机。你可以在此链接中了解更多关于条件编译块的信息:https://docs.swift.org/swift-book/documentation/the-swift-programming-language/statements/#Conditional-Compilation-Block。
present(imagePickerController, animated: true)这条语句在屏幕上显示图像选择控制器。
你已经实现了在图像视图被点击时显示图像选择控制器的所有必需代码。在下一节中,你将实现当用户选择图像或取消时会被调用的UIImagePickerControllerDelegate方法。
实现 UIImagePickerControllerDelegate 方法
UIImagePickerControllerDelegate协议有一组方法,你必须在你委托对象中实现这些方法以与图像选择控制器界面交互。
要了解更多关于UIImagePickerControllerDelegate协议的信息,请参阅developer.apple.com/documentation/uikit/uiimagepickercontrollerdelegate。
当图像选择控制器出现在屏幕上时,用户可以选择选择照片或取消。如果用户取消,将触发imagePickerControllerDidCancel(_:)方法;如果用户选择照片,将触发imagePickerController(_:didFinishPickingMediaWithInfo:)方法。
现在,你将在AddJournalEntryViewController类中实现这些方法。在项目导航器中,点击AddJournalEntryViewController文件。在UIImagePickerControllerDelegate扩展中输入以下代码:
// MARK: - UIImagePickerControllerDelegate
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage =
info[UIImagePickerController.InfoKey.originalImage]
as? UIImage else {
fatalError("Expected a dictionary containing an image,
but was provided the following: \(info)")
}
let smallerImage = selectedImage.preparingThumbnail(
of: CGSize(width: 300, height: 300))
photoImageView.image = smallerImage
dismiss(animated: true)
}
当用户取消时,将触发imagePickerControllerDidCancel(_:)方法。图像选择控制器将被关闭,用户将返回到添加新日志条目屏幕。
当用户选择照片时,将触发imagePickerController(_:didFinishPickingMediaWithInfo:)方法。然后,这张照片将被分配给selectedImage。接下来,将使用selectedImage实例的preparingThumbnail(of:)方法创建一个宽度为 300 点、高度为 300 点的较小图像,其大小与日志条目详情屏幕上的图像视图大小相同。然后,这个图像将被分配给photoImageView属性,图像选择控制器将被关闭。
您可以在此链接中了解更多关于preparingThumbnail(of:)方法的信息:developer.apple.com/documentation/uikit/uiimage/3750835-preparingthumbnail。
所有必需的UIImagePickerController代理方法都已实现。在下一节中,您将修改Info.plist文件,以便您的应用请求使用相机或照片库的权限。
获取使用相机或照片库的权限
苹果规定,如果您的应用希望访问相机或照片库,必须通知用户。如果您不这样做,您的应用将被拒绝,并且不允许在 App Store 上发布。
您将修改项目中的Info.plist文件,以便在应用尝试访问相机或照片库时显示消息。请按照以下步骤操作:
-
在项目导航器中点击Info文件。将指针移至信息属性列表行,并点击+按钮以创建新行。
-
在新行中,将键设置为隐私 - 照片库使用描述,并将值设置为
此应用在创建日志条目时使用您的照片库中的照片。 -
使用+按钮添加第二行。这次,将键设置为隐私 - 相机使用描述,并将值设置为
此应用在创建日志条目时使用您的相机。完成操作后,您的Info.plist文件应如下所示:

图 20.8:添加额外键的 Info.plist
- 构建并运行您的应用。转到添加新日志条目屏幕,并点击图像视图。将出现图像选择器:

图 20.9:模拟器显示图像选择器
如果您在实际的 iOS 设备上运行应用,将出现一个对话框请求使用相机的权限。点击确定以继续。
- 选择一张照片,它将出现在添加新日志条目屏幕上的图像视图中。为日志条目输入示例详细信息,然后点击保存:

图 20.10:模拟器显示在添加新日志条目屏幕上的照片
- 您将返回到日志列表屏幕。点击新添加的日志条目。您将在日志条目详情屏幕上看到照片:

图 20.11:模拟器显示在日志条目详情屏幕上的照片
您现在可以在添加新日志条目屏幕上添加照片,并在日志条目详情屏幕上显示它们。太棒了!
摘要
在本章中,你通过添加用户从相机或相册获取照片的方法,完成了添加新日记条目屏幕的实现。首先,你向新条目场景中的图像视图添加了一个轻点手势识别器,并将其配置为显示图像选择器控制器。然后,你实现了UIImagePickerDelegate协议,这允许你从相机或相册获取照片,并在将其保存到日记条目实例之前将其照片缩小。你还修改了Info.plist文件,以便你可以访问相机和相册。
现在,你能够编写自己的应用程序,从你的相机或相册导入照片。
在下一章中,你将实现用户在日记列表屏幕上搜索日记条目的方法。
加入我们的 Discord 社区!
与其他用户、专家以及作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,以及更多。扫描二维码或访问链接加入社区。
第二十一章:开始使用搜索
在上一章中,你为用户添加了一种从相机或照片库获取照片的方法,可以将照片添加到新的期刊条目中。
在本章中,你将为期刊列表屏幕实现一个 搜索栏。你将从修改 JournalListViewController 类,使其符合 UISearchResultsUpdating 协议并在期刊列表屏幕上显示搜索栏开始。接下来,你将修改数据源方法,以便在用户输入搜索词时显示正确的期刊条目。然后,你将修改 prepare(for:sender:) 方法,以确保在期刊条目详情屏幕上显示正确的期刊条目详情。最后,你将修改用于删除期刊条目的方法。
到本章结束时,你将学会如何为你的应用实现搜索栏。以一个例子来说,如果你正在创建一个联系人应用,你可以使用搜索栏来搜索特定的联系人。
本章将涵盖以下主题:
-
在期刊列表屏幕上实现搜索栏
-
修改表格视图数据源方法
-
修改
prepare(for:sender:)方法 -
修改删除期刊条目的方法
技术要求
你将继续在上一章中修改的 JRNL 项目上工作。
本章的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter21 文件夹中,可以通过以下链接下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
观看以下视频,查看代码的实际效果:
让我们从修改 JournalListViewController 类开始,使其符合 UISearchResultsUpdating 协议,并在期刊列表屏幕上显示搜索栏。
实现期刊列表屏幕的搜索栏
目前,你在期刊列表屏幕上只有几个条目。但随着你使用该应用的时长增加,条目会越来越多,找到特定的条目将会变得困难。为了使查找期刊条目更加方便,你将在期刊列表屏幕的导航栏中实现一个搜索栏。你将使用 Apple 的 UISearchController 类来完成这项工作。这个类包含一个 UISearchBar 类,你可以将其安装到你的用户界面中。为了执行搜索,你需要采用 UISearchResultsUpdating 协议并实现该协议所需的 updateSearchResults(for:) 方法。
要了解更多关于 UISearchController 类的信息,请参阅 developer.apple.com/documentation/uikit/uisearchcontroller。
现在,你将在JournalListViewController类中添加UISearchController类的实例,采用UISearchResultsUpdating协议,并实现updateSearchResults(for:)方法。按照以下步骤操作:
-
在项目导航器中,单击JournalListViewController文件。在此文件的所有其他代码之后添加一个新扩展,使
JournalListViewController类符合UISearchResultsUpdating协议:extension JournalListViewController: UISearchResultsUpdating { } -
你会看到一个错误,因为尚未实现符合
UISearchResultsUpdating协议所需的方法。将以下代码添加到新添加的扩展中来实现它:// MARK: - Search func updateSearchResults(for searchController: UISearchController) { guard let searchBarText = searchController.searchBar.text else { return } print(searchBarText) }
你输入到搜索栏中的任何文本都将打印到调试区域。
-
在
JournalListViewController类中tableView属性之后声明以下属性:@IBOutlet var tableView: UITableView! **private let****search****=****UISearchController****(****searchResultsController****:** **nil****)** **private var****filteredTableData****: [****JournalEntry****]** **=** **[]**
search属性将存储UISearchController类的实例。
filteredTableData属性将存储一个与用户输入的搜索文本匹配的JournalEntry实例数组。
-
修改
JournalListViewController类中的viewDidLoad()方法,如下所示:override func viewDidLoad() { super.viewDidLoad() SharedData.shared.loadJournalEntriesData() **search****.****searchResultsUpdater****=****self** **search****.****obscuresBackgroundDuringPresentation****=****false** **search****.****searchBar****.****placeholder****=****"Search titles"** **navigationItem****.****searchController****=****search** }
让我们分解一下:
search.searchResultsUpdater = self
此语句将JournalListViewController实例设置为负责更新搜索结果的对象。
search.obscuresBackgroundDuringPresentation = false
当用户与搜索栏交互时,此语句会遮挡包含搜索内容的视图控制器。由于你在期刊列表屏幕上使用表格视图来显示搜索结果,因此此值设置为false,否则你将遮挡搜索结果。
search.searchBar.placeholder = "Search titles"
此语句设置了搜索栏的占位文本。
navigationItem.searchController = search
此语句将搜索栏添加到屏幕上的导航栏。
- 构建并运行你的应用,你将在期刊列表屏幕上看到一个搜索栏。在搜索栏中输入一些文本:

图 21.1:模拟器显示期刊列表屏幕上的搜索栏
- 注意,你输入到搜索栏中的文本将显示在调试区域:

图 21.2:调试区域显示搜索文本
你已经在期刊列表屏幕上添加了一个搜索栏。太好了!在下一节中,你将修改JournalListViewController文件,以显示与搜索栏中输入的搜索文本匹配的期刊条目。
修改表格视图数据源方法
如你在第十四章中学习的,MVC 和表格视图入门,你可以使用UITableViewDataSource方法来确定要显示多少表格视图行,以及每行要放置什么内容。
在上一节中,你添加了一个新的属性filteredTableData,用于存储与搜索文本匹配的JournalEntry实例数组。你将修改updateSearchResults(for:)方法,用与搜索文本匹配的JournalEntry实例填充filteredTableData,并修改UITableViewDataSource方法,在搜索栏活动时在期刊列表屏幕上显示filteredTableData的内容。按照以下步骤操作:
-
在项目导航器中,点击JournalListViewController文件。修改
UISearchResultsUpdating扩展中的updateSearchResults(for:)方法,如图所示://MARK: - Search func updateSearchResults(for searchController: UISearchController) { guard let searchBarText = searchController.searchBar.text else { return } **filteredTableData =** **SharedData****.****shared****.****allJournalEntries****()** **.****filter** **{ entry in** **entry.****entryTitle****.****lowercased****().****contains****(searchBarText** **.****lowercased****())** **}** **tableView****.****reloadData****()** }
此方法获取journalEntries数组的副本,然后将匹配搜索文本的JournalEntry实例添加到filteredTableData数组中。完成后,表格视图将被重新加载。
-
修改
tableView(_:numberOfRowsInSection:)方法,当搜索栏处于使用状态时,从filteredTableData数组中获取JournalEntry实例的数量://MARK: - UITableViewDataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { **if****search****.****isActive** **{** **return** **filteredTableData.****count** **}** **else** **{** **return****SharedData****.****shared****.****numberOfJournalEntries****()** **}** } -
修改
tableView(_:cellForRowAt:)方法,当搜索栏处于使用状态时,从filteredTableData数组中获取指定行的JournalEntry实例:func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let journalCell = tableView.dequeueReusableCell(withIdentifier: "journalCell", for: indexPath) as! JournalListTableViewCell **let** **journalEntry:** **JournalEntry** **if****search****.****isActive** **{** **journalEntry** **=** **filteredTableData[indexPath.****row****]** **}** **else** **{** **journalEntry** **=****SharedData****.****shared****.****journalEntry****(****at****: indexPath.****row****)** **}** if let photoData = journalEntry.photoData { journalCell.photoImageView.image = UIImage(data: photoData) } journalCell.dateLabel.text = journalEntry.date.formatted( .datetime.month().day().year() ) journalCell.titleLabel.text = journalEntry.entryTitle return journalCell } -
构建并运行你的应用,并在搜索栏中输入与你的日志条目标题匹配的文本。将显示与搜索文本匹配的日志条目:

图 21.3:模拟器显示与搜索文本匹配的日志条目
你现在可以显示与搜索文本匹配的日志条目,但当你点击它们时,日志条目详情屏幕可能或可能不会显示被点击日志条目的详细信息。你将在下一节中修复此问题。
修改prepare(for:sender:)方法
当你在搜索栏中输入文本时,与搜索文本匹配的日志条目将出现在日志列表屏幕的表格视图中。但如果你点击其中一个,日志条目详情屏幕可能或可能不会显示被点击日志条目的详细信息。这是因为prepare(for:sender:)方法将引用SharedData.shared实例中的journalEntries数组,而不是filteredTableData数组。要修复此问题,请按照以下步骤操作:
-
修改
JournalListViewController类中的prepare(for:sender:)方法,如图所示,当搜索栏处于活动状态时,将filteredTableData数组中适当的JournalEntry实例分配给目标视图控制器的journalEntry属性://MARK: - Navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) guard segue.identifier == "entryDetail" else { return } guard let journalEntryDetailViewController = segue.destination as? JournalEntryDetailViewController, let selectedJournalEntryCell = sender as? JournalListTableViewCell, let indexPath = tableView.indexPath(for: selectedJournalEntryCell) else { fatalError("Could not get indexpath") } **let** **selectedJournalEntry:** **JournalEntry** **if****search****.****isActive** **{** **selectedJournalEntry** **=** **filteredTableData[indexPath.****row****]** **}** **else** **{** **selectedJournalEntry** **=** **SharedData****.****shared****.****journalEntry****(****at****: indexPath.****row****)** **}** journalEntryDetailViewController.selectedJournalEntry = selectedJournalEntry } -
构建并运行你的应用,并在搜索栏中输入与你的日志条目标题匹配的文本。将显示与搜索文本匹配的日志条目:

图 21.4:模拟器显示与搜索文本匹配的日志条目
- 点击其中一个日志条目,现在在日志条目详情屏幕上显示的详细信息与被点击的日志条目相匹配:

图 21.5:模拟器显示在日志条目详情屏幕上被点击日志条目的详细信息
你的应用现在可以在日志条目详情屏幕中正确显示被点击日志条目的详细信息。酷!在下一节中,你将修改JournalListViewController类中用于删除日志条目的方法。
修改删除日志条目的方法
到目前为止,用于从SharedData实例中的journalEntries数组中删除JournalEntry实例的方法使用表格视图行来识别要删除的JournalEntry实例的索引。然而,当搜索栏处于活动状态时,表格视图行可能不会匹配要删除的JournalEntry实例的索引。你将为JournalEntry类添加一个属性以存储一个将识别JournalEntry实例的值,并修改SharedData和JournalListViewController类中的方法以使用此属性来确定要删除的JournalEntry实例。按照以下步骤操作:
-
在项目导航器中,点击JournalEntry文件。向
JournalEntry类中添加一个新的属性以存储所谓的UUID字符串:class JournalEntry: NSObject, MKAnnotation, Codable { // MARK: - Properties **var****key****=****UUID****().****uuidString** let date: Date let rating: Int
当创建一个新的JournalEntry实例时,key属性被分配一个由UUID类生成的字符串,该字符串保证是唯一的。
要了解更多关于UUID类的信息,请参阅developer.apple.com/documentation/foundation/uuid。
-
在项目导航器中点击SharedData文件。在
removeJournalEntry(at:)方法之后添加一个方法到SharedData类中,用于删除与传入的JournalEntry实例的UUID字符串匹配的JournalEntry实例:func removeSelectedJournalEntry(_ selectedJournalEntry: JournalEntry) { journalEntries.removeAll { $0.key == selectedJournalEntry.key } } -
在项目导航器中点击JournalListViewController文件。按照如下所示修改
JournalListViewController类中的tableView(_:commit:forRowAt:)方法://MARK: - TableViewDelegate func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { **if****search****.****isActive** **{** **let** **selectedJournalEntry** **=****filteredTableData****[** **indexPath.****row****]** **filteredTableData****.****remove****(****at****: indexPath.****row****)** **SharedData****.****shared****.****removeSelectedJournalEntry****(** **selectedJournalEntry)** **}** **else** **{** SharedData.shared.removeJournalEntry(at: indexPath.row) **}** SharedData.shared.saveJournalEntriesData() tableView.reloadData() } }
此方法现在会检查搜索栏是否处于活动状态。如果是,则将对应于被点击行的filteredTableData数组中的JournalEntry实例分配给selectedJournalEntry。然后,从这个filteredTableData数组中删除此实例,并将其作为参数传递给removeSelectedJournalEntry(_:)方法。具有与传递给removeSelectedJournalEntry(_:)方法相同的UUID字符串的journalEntry实例将从SharedData实例中的journalEntries数组中删除。
- 构建并运行你的应用。由于你对
JournalEntry类进行了修改,之前保存在 JSON 文件中的已保存日记条目将无法加载,因此你需要创建新的示例日记条目:

图 21.6:模拟器显示日记列表屏幕上的日记条目
- 在搜索栏中输入一些与日记条目标题匹配的文本。与搜索文本匹配的日记条目将被显示。在行上向左滑动并点击取消以退出搜索:

图 21.7:模拟器显示即将被删除的行
当搜索栏处于活动状态时,在表格视图行上向左滑动将删除它从表格视图中,并从SharedData实例中的journalEntries数组中删除相应的日记条目。
- 验证已删除的期刊条目不再出现在期刊列表屏幕上:

图 21.8:模拟器显示期刊列表屏幕上剩余的期刊条目
你已成功修改了删除期刊条目的方法,现在在期刊列表屏幕上实现搜索栏的工作已经完成。太棒了!
摘要
在本章中,你为期刊列表屏幕实现了搜索栏。首先,你修改了JournalListViewController类以符合UISearchResultsUpdating协议并在期刊列表屏幕上显示搜索栏。接下来,你修改了数据源方法,以便在用户输入搜索词时显示正确的期刊条目。然后,你修改了prepare(for:sender:)方法以确保在期刊条目详情屏幕上显示正确的期刊条目详情。最后,你修改了用于删除期刊条目的方法。
你现在已经学会了如何在你的应用中实现搜索栏,并且你也完成了JRNL应用。做得太棒了!
在下一章中,你将学习如何让你的应用为 iPad 和 Mac 做准备。
加入我们的 Discord 频道!
与其他用户、专家以及作者本人一起阅读这本书。提问、为其他读者提供解决方案、通过“问我任何问题”的环节与作者聊天,以及更多。扫描二维码或访问链接加入社区。
第二十二章:开始使用集合视图
在上一章中,你为 Journal List 屏幕实现了搜索栏,现在你的应用已经完成。然而,你的应用是为 iPhone 的屏幕设计的,如果你要在 iPad 或 Mac 上运行它,你会看到它没有充分利用更大的屏幕尺寸。
在本章中,你将用 集合视图 替换 Journal List 屏幕上的表格视图,这将更好地利用在 iPad 或 Mac 上运行应用时额外的屏幕空间。你还将使用大小类在设备旋转时动态修改列数和集合视图单元格大小。
首先,在 Main 故事板文件中,你将用集合视图替换 Journal List 屏幕上的表格视图,并配置集合视图单元格以显示表格视图单元格曾经显示的相同信息。接下来,你将重构 JournalListViewController 和 JournalListTableViewCell 类以与添加的集合视图和集合视图单元格一起工作。然后,你将添加代码以动态更改集合视图单元格大小以适应应用正在运行的显示。最后,你将在不同设备上测试你的应用。
到本章结束时,你将了解集合视图、如何使用集合视图代理和数据源协议,以及如何根据大小类动态修改应用界面。
本章将涵盖以下主题:
-
理解集合视图
-
将 Journal List 屏幕修改为使用集合视图
-
使用大小类动态修改集合视图单元格大小
-
在不同设备上测试你的应用
技术要求
你将继续在上一章中修改的 JRNL 项目上工作。
本章的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter22 文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际运行情况:
让我们从下一节学习集合视图开始。
理解集合视图
集合视图是 UICollectionView 类的一个实例。它管理一个有序的元素集合,并使用可定制的布局来展示这些元素。
要了解更多关于集合视图的信息,请访问 developer.apple.com/documentation/uikit/uicollectionview。
集合视图显示的数据通常由视图控制器提供。为集合视图提供数据的视图控制器必须采用UICollectionViewDataSource协议。此协议声明了一系列方法,告诉集合视图显示多少个单元格以及每个单元格显示什么内容。
要了解更多关于UICollectionViewDataSource协议的信息,请访问developer.apple.com/documentation/uikit/uicollectionviewdatasource。
为了提供用户交互,集合视图的视图控制器还必须采用UICollectionViewDelegate协议,该协议声明了一系列在用户与集合视图交互时触发的方法。
要了解更多关于UICollectionViewDelegate协议的信息,请访问developer.apple.com/documentation/uikit/uicollectionviewdelegate。
集合视图的布局方式由一个UICollectionViewLayout对象指定。这决定了集合视图边界内的单元格位置、辅助视图和装饰视图。
你将使用UICollectionViewFlowLayout类,它是UICollectionViewLayout类的子类,为你的应用提供支持。集合视图中的单元格从一行或一列流向下一行或一列,每行包含尽可能多的单元格。
要了解更多关于UICollectionViewFlowLayout类的信息,请访问developer.apple.com/documentation/uikit/uicollectionviewflowlayout。
流布局通过与集合视图的代理对象协同工作来确定每个部分和网格中项目、头部和脚部的大小。该代理对象必须遵守UICollectionViewDelegateFlowLayout协议。这允许你动态调整布局信息。
要了解更多关于UICollectionViewFlowLayoutDelegate协议的信息,请访问developer.apple.com/documentation/uikit/uicollectionviewdelegateflowlayout。
现在你已经对集合视图有了基本的了解,你将在下一节中通过将表格视图替换为集合视图来修改“期刊列表”屏幕。
修改“期刊列表”屏幕以使用集合视图
目前,“JRNL”应用中的“期刊列表”屏幕使用的是表格视图。表格视图通过单列排列的行来展示表格视图单元格。这在 iPhone 上效果很好,但如果你在 iPad 上运行该应用,你会看到“期刊列表”屏幕上有大量的空白屏幕空间,如下面的图所示:

图 22.1:模拟器显示的“期刊列表”屏幕,其中包含一个 iPad 上的表格视图
为了解决这个问题,您将用集合视图替换表格视图,这将允许您更有效地使用可用的屏幕空间,如图所示:

图 22.2:模拟器显示包含 iPad 上集合视图的期刊列表屏幕
要在期刊列表屏幕上实现集合视图,您需要执行以下操作:
-
在
Main故事板文件中,将期刊场景中的表格视图替换为集合视图。 -
向集合视图单元格添加 UI 元素。
-
修改
JournalListTableViewCell类以管理集合视图单元格的内容。 -
修改
JournalListViewController类以管理集合视图显示的内容。 -
添加方法以根据设备屏幕尺寸和方向动态更改集合视图单元格的大小。
在下一节中,您将开始修改Main故事板文件中的期刊场景,使用集合视图而不是表格视图。
替换表格视图为集合视图
目前,Main故事板文件中的期刊场景包含一个表格视图。您将用集合视图替换它。按照以下步骤操作:
- 打开上一章中修改的
JRNL项目,并从目标菜单中选择iPad(第 10 代):

图 22.3:显示已选择 iPad(第 10 代)的目标菜单
- 构建并运行您的应用,并注意它在 iPad 屏幕上的显示:

图 22.4:模拟器显示 iPad 屏幕
虽然应用按预期工作,但请注意,在期刊列表屏幕的右侧浪费了很多空间。
- 点击停止按钮。在项目导航器中点击主故事板文件。在文档大纲中,点击期刊场景下的表格视图。按删除键将其删除:

图 22.5:编辑区域显示文档大纲中选中的表格视图
- 点击库按钮以显示库。在过滤器字段中输入
collec。一个集合视图对象将作为结果之一出现。将其拖到期刊场景视图的中间:

图 22.6:库中选中的集合视图对象
- 确保集合视图被选中,然后点击自动布局添加新约束按钮:

图 22.7:已选择集合视图的期刊场景
- 在顶部、左侧、右侧和底部边缘约束字段中输入
0,并点击所有浅红色支柱。确保所有支柱都已变为亮红色,并且约束到边距未选中。然后,点击添加 4 个约束按钮:

图 22.8:自动布局 – 添加新约束弹出对话框
这将设置集合视图边缘与包围视图边缘之间的空间为 0,将集合视图的边缘绑定到包围视图的边缘。
- 验证集合视图的四个边现在都已绑定到屏幕的边缘,如图所示:

图 22.9:填充整个屏幕的集合视图的日志场景
- 在集合视图仍然被选中时,点击大小检查器按钮。在集合视图下,将估算大小设置为无。

图 22.10:大小检查器,高亮显示估算大小
你将在稍后添加代码以动态确定集合视图的大小。
- 你需要重新建立日志列表屏幕和日志条目详情屏幕之间的转换。Ctrl + 拖动文档大纲中的集合视图单元格到条目详情场景,并从弹出菜单中选择显示。

图 22.11:显示拖动目的地的编辑区域
- 点击新添加的故事板转换,并点击属性检查器按钮。在故事板转换下,将标识符设置为
entryDetail。

图 22.12:属性检查器,标识符设置为 entryDetail
你已经向日志场景添加了一个集合视图,并使用自动布局约束使其填充整个屏幕,但当前原型集合视图单元格是空的。你将在下一节向集合视图单元格添加 UI 元素。
向集合视图单元格添加 UI 元素
你已经将日志场景内的表格视图替换为集合视图,但集合视图内的原型集合视图单元格是空的。你需要向原型集合视图单元格添加一个图像视图和两个标签,并设置它们的约束,使其与之前使用的表格视图单元格相匹配。按照以下步骤操作:
- 在文档大纲中选择日志场景的集合视图单元格。将集合视图单元格的右边缘向右拖动,直到它达到屏幕的右侧:

图 22.13:显示集合视图单元格的编辑区域
-
点击大小检查器按钮,在集合视图单元格下,将高度设置为
90。 -
要向表格视图单元格添加图像视图,点击库按钮。在过滤器字段中输入
imag。一个图像视图对象将出现在结果中。将其拖动到原型单元格中:

图 22.14:添加了图像视图的原型单元格
-
在选择图像视图后,点击添加新约束按钮并输入以下值以设置新添加的图像视图的约束:
-
顶部:
0 -
左侧:
0 -
底部:
0 -
宽度:
90
-
约束到边距不应被勾选。完成设置后,点击添加 4 个约束按钮:

图 22.15:图像视图的约束
- 点击属性检查器按钮。在图像视图下,将图像设置为
face.smiling:

图 22.16:图像视图,图像设置为 face.smiling
- 接下来,你将为显示日记条目日期添加一个标签。点击图书馆按钮。在过滤器字段中输入
label。一个标签对象将在结果中显示。将其拖动到刚刚添加的图像视图和单元格右侧的空间之间:

图 22.17:添加标签的原型单元格
- 在属性检查器中,在标签下,使用字体菜单将字体设置为标题 1:

图 22.18:标签的属性检查器
-
点击添加新约束按钮并输入以下值以设置标签的约束:
-
顶部:
0 -
左侧:
8 -
右侧:
0
-
约束到边距应该被勾选,这设置了标准的 8 点边距。完成时,点击添加 3 个约束按钮。

图 22.19:标签的约束条件
- 最后,你将为显示日记条目标题添加一个标签。点击图书馆按钮。在过滤器字段中输入
label。一个标签对象将在结果中显示。将其拖动到刚刚添加的标签和单元格底部之间的空间:

图 22.20:添加第二个标签的原型单元格
- 在属性检查器中,在标签下,使用字体菜单将字体设置为正文,并将行数设置为
2:

图 22.21:标签的属性检查器
-
点击添加新约束按钮并输入以下值以设置标签的约束:
-
顶部:
0 -
左侧:
8 -
右侧:
0
-
约束到边距应该被勾选,这设置了标准的 8 点边距。完成时,点击添加 3 个约束按钮。

图 22.22:第二个标签的约束条件
现在原型集合视图单元格已包含一个图像视图和两个标签,并且已添加所有必要的约束。太棒了!在下一节中,你将修改JournalListTableViewCell类以管理集合视图单元格的内容。
修改JournalListTableViewCell类
最初,JournalListTableViewCell类用于在“日记列表”屏幕中管理表格视图实例的表格视图单元格。由于你已经用集合视图替换了表格视图,因此需要重新建立JournalListTableViewCell类与集合视图单元格中添加的 UI 元素之间的所有连接。按照以下步骤操作:
- 首先,你将更改
JournalListTableViewCell类的名称,以更准确地描述其新角色。在项目导航器中点击JournalListTableViewCell文件。在文件中的类名上右键单击,并从弹出菜单中选择重构 | 重命名…:

图 22.23:弹出菜单,选择 Refactor | Rename…
- 将名称更改为
JournalListCollectionViewCell并点击重命名:

图 22.24:显示新名称的编辑区域
-
你将修改类声明,因为这个类现在用于管理集合视图单元格。将超类更改为
UICollectionViewCell:class JournalListCollectionViewCell: **UICollectionViewCell** { -
现在,将这个类分配为集合视图单元格的标识符。在项目导航器中点击Main故事板文件,然后在文档大纲中点击Journal Scene下的Collection View Cell:

图 22.25:编辑区域显示在期刊场景中的集合视图单元格
- 点击标识符检查器按钮。在自定义类部分下,将类设置为
JournalListCollectionViewCell。这设置了一个JournalListCollectionViewCell实例作为集合视图单元格的自定义集合视图子类。完成后按Return键:

图 22.26:标识符检查器显示类设置为 JournalListCollectionViewCell
- 点击属性检查器按钮。在集合可重用视图下,将标识符设置为
journalCell:

图 22.27:属性检查器显示标识符设置为 journalCell
注意,文档大纲中集合视图单元格的名称已更改为journalCell。
-
在文档大纲中选中journalCell,点击连接检查器按钮以显示journalCell的出口。
-
从photoImageView出口拖动到表格视图单元格中的图像视图。
-
从dateLabel出口拖动到表格视图单元格顶部的标签。
-
从titleLabel出口拖动到底部标签在表格视图单元格中。
-
完成后,验证连接看起来像以下截图:

图 22.28:连接检查器显示 journalCell 的连接
记住,如果你犯了错误,你可以点击x来断开连接,然后再次从出口拖动到 UI 元素。
Main故事板文件中的journalCell集合视图单元格已使用JournalCollectionTableViewCell类设置。集合视图单元格的图像视图和标签的出口也已分配。在下一节中,你将更新JournalListViewController类以使用集合视图而不是表格视图。
修改 JournalListViewController 类
目前,JournalListViewController类有一个UITableView对象的出口,并实现了数据源和代理方法来管理表格视图。你将修改这个类以使用集合视图。按照以下步骤操作:
-
首先,你将修改类声明以使用
UICollectionView实例。在项目导航器中点击JournalListViewController文件。按照以下方式修改类声明:class JournalListViewController: UIViewController, **UICollectionViewDataSource****,** **UICollectionViewDelegate****,** **UICollectionViewDelegateFlowLayout** {
在这里,您已将数据源和代理协议更改为集合视图的等效协议,并添加了对新协议 UICollectionViewDelegateFlowLayout 的遵守。此协议用于确定集合视图中集合视图单元格的布局。您将看到一个错误,因为集合视图数据源方法尚未实现。不要担心这个错误,因为您将在本节稍后的步骤中修复它。
- 要更改
tableview输出的名称,右键单击它,并从弹出菜单中选择 重构 | 重命名…:

图 22.29:选择“重构 | 重命名…”的弹出菜单
- 将名称更改为
collectionView并点击 重命名:

图 22.30:编辑区域显示新名称
-
由于视图控制器将管理一个集合视图,将输出类型从
UITableView更改为UICollectionView:@IBOutlet var collectionView: **UICollectionView**! -
要建立 UI 元素与您的代码之间的连接,请在项目导航器中点击 Main 故事板文件,并在文档大纲中点击第一个 Journal 场景。
-
点击连接检查器按钮,从 collectionView 输出拖动到文档大纲中的 集合视图:

图 22.31:连接检查器显示 JournalListViewController 的连接
- 在文档大纲中点击 Collection View。从 dataSource 和 delegate 输出拖动到文档大纲中的视图控制器(显示为 Journal)。

图 22.32:连接检查器显示 collectionView 的连接
-
现在您将修复
JournalListViewController类中的错误。在项目导航器中点击 JournalListViewController 文件,并将您的代码中的表格视图数据源方法替换为以下集合视图数据源方法:// MARK: - UICollectionViewDataSource func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { if search.isActive { return filteredTableData.count } else { return SharedData.shared.numberOfJournalEntries() } } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let journalCell = collectionView.dequeueReusableCell( withReuseIdentifier: "journalCell", for: indexPath) as! JournalListCollectionViewCell let journalEntry: JournalEntry if search.isActive { journalEntry = filteredTableData[indexPath.row] } else { journalEntry = SharedData.shared.journalEntry(at: indexPath.row) } if let photoData = journalEntry.photoData { journalCell.photoImageView.image = UIImage(data: photoData) } journalCell.dateLabel.text = journalEntry.date.formatted( .dateTime.month().day().year() ) journalCell.titleLabel.text = journalEntry.entryTitle return journalCell }
如您所见,它们与您之前使用的表格视图数据源方法非常相似。
-
由于您现在正在使用集合视图,
tableView(_:commit:forRowAt:)方法不能再用于删除单元格。将tableView(_:commit:forRowAt:)方法替换为以下方法:// MARK: - UICollectionView delete method func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? { guard let indexPath = indexPaths.first else { return nil } let config = UIContextMenuConfiguration( previewProvider: nil) { (elements) -> UIMenu? in let delete = UIAction(title: "Delete") { (action) in if self.search.isActive { let selectedJournalEntry = self.filteredTableData[ indexPath.item] self.filteredTableData.remove(at: indexPath.item) SharedData.shared.removeSelectedJournalEntry( selectedJournalEntry) } else { SharedData.shared.removeJournalEntry(at: indexPath.item) } SharedData.shared.saveJournalEntriesData() collectionView.reloadData() } return UIMenu(children: [delete]) } return config }
代替向左滑动以删除,此方法实现了一个上下文菜单,其中包含一个选项 删除,当您在集合视图单元格上轻按并保持时出现。
-
您还会在
prepare(for:sender:)方法中看到一个错误。按照以下方式修改prepare(for:sender:)方法中的guard语句:guard let journalEntryDetailViewController = segue.destination as? JournalEntryDetailViewController, let selectedJournalEntryCell = sender as? JournalListCollectionViewCell, let indexPath = **collectionView**.indexPath(for: selectedJournalEntryCell) else { fatalError("Could not get indexpath") }
JournalListViewController 中的所有错误都已解决。太棒了!在下一节中,您将添加代码以根据设备屏幕尺寸和方向更改集合视图单元格的大小。
使用大小类动态修改集合视图单元格大小
正如你之前看到的,在“期刊列表”屏幕上的表格视图使用单列排列的行来呈现表格视图单元格。这在 iPhone 上工作得很好,但正如你所看到的,如果在 iPad 上运行应用,这会导致大量的空间浪费。尽管你可以为 iPhone 和 iPad 使用相同的 UI,但如果你能根据每个设备进行定制会更好。
为了做到这一点,你将添加一些代码,使你的应用能够识别其正在运行的屏幕大小,并且你将动态修改集合视图中的集合视图单元格的大小以适应。你可以使用大小类别来识别当前屏幕大小;你将在下一节中了解它们。
理解大小类别
为了确定你的应用正在运行的屏幕大小,你必须考虑设备方向对你的 UI 的影响。由于屏幕尺寸在纵向和横向都有很大的差异,这可能会很有挑战性。为了使这更容易,你将使用大小类别而不是设备的物理分辨率。
关于大小类别的更多信息,请参阅此链接:developer.apple.com/design/human-interface-guidelines/layout。
大小类别是操作系统自动分配给视图的特性。定义了两个类别,描述了视图的高度和宽度:常规(扩展空间)和紧凑(约束空间)。让我们看看不同设备上全屏视图的大小类别:

图 22.33:不同 iOS 设备的大小类别
对于JRNL应用,你将在“期刊列表”屏幕中配置集合视图,如果大小类别是紧凑的,则使用单列的集合视图单元格,如果大小类别是常规的,则使用两列。
你将为你的应用添加代码以确定当前的大小类别。一旦你知道了大小类别,你就能设置要使用的列数和集合视图中的集合视图单元格的大小。你将在下一节中学习如何做到这一点。
修改 JournalListViewController 类
你已经使JournalListViewController类采用了UICollectionViewDelegateFlowLayout协议。现在你将创建并设置集合视图的布局,使用UICollectionViewFlowLayout实例,并实现动态设置集合视图单元格大小的方法。
按照以下步骤操作:
-
在项目导航器中点击JournalListViewController文件。在
JournalListViewController类中,在闭合花括号之前添加以下方法到类定义中:func setupCollectionView() { let flowLayout = UICollectionViewFlowLayout() flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) flowLayout.minimumInteritemSpacing = 0 flowLayout.minimumLineSpacing = 10 collectionView.collectionViewLayout = flowLayout }
此方法创建了一个UICollectionViewFlowLayout类的实例,将集合视图的所有边缘内边距设置为 10 点,将最小项间距设置为 0 点,将最小行间距设置为 10 点,并将其分配给集合视图。分区内边距反映了分区外边缘的间距。最小项间距是在同一行中的项之间使用的最小间距。最小行间距是在网格中项的行之间使用的最小间距。
-
在
setupCollectionView()方法之后添加以下UICollectionViewDelegateFlowLayout方法:// MARK: - UICollectionViewDelegateFlowLayout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let numberOfColumns: CGFloat if (traitCollection.horizontalSizeClass == .compact) { numberOfColumns = 1 } else { numberOfColumns = 2 } let viewWidth = collectionView.frame.width let inset = 10.0 let contentWidth = viewWidth - inset * ( numberOfColumns + 1) let cellWidth = contentWidth / numberOfColumns let cellHeight = 90.0 return CGSize(width: cellWidth, height: cellHeight) }
此方法确定要显示的列数,并设置集合视图单元格的高度和宽度。
让我们分解一下:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
此方法返回一个CGSize实例,应将其设置为集合视图单元格的大小。
let numberOfColumns: CGFloat
if (traitCollection.horizontalSizeClass == .compact) {
numberOfColumns = 1
} else {
numberOfColumns = 2
}
此代码设置要显示的列数。
let viewWidth = collectionView.frame.width
此语句获取屏幕宽度并将其分配给viewWidth。
let inset = 10.0
let contentWidth = viewWidth - inset * (
numberOfColumns + 1)
此代码减去边缘内边距所占用的空间,以便确定单元格大小。
let cellWidth = contentWidth / numberOfColumns
此语句通过将contentWidth除以列数来计算单元格宽度,并将其分配给cellWidth。
let cellHeight = 90.0
此语句将90分配给cellHeight,这将用于设置单元格高度。
return CGSize(width: cellWidth, height: cellHeight)
}
此语句返回包含单元格大小的CGSize实例。
假设您正在以竖屏模式在 iPhone 16 Pro Max 上运行。水平尺寸类将是.compact,因此numberOfColumns设置为1。viewWidth将被分配给 iPhone 屏幕的宽度,即414点。contentWidth设置为414 - (10 x 2) = 394。cellWidth设置为contentWidth / numberOfColumns = 394,cellHeight设置为90,因此返回的CGSize实例将是(394, 90),使得一行可以容纳一个单元格。
当将相同的 iPhone 旋转到横屏模式时,水平尺寸类将是.regular,因此numberOfColumns设置为2。viewWidth将被分配给 iPhone 屏幕的高度,即896点。contentWidth设置为896 - (10 x 3) = 866。cellWidth设置为contentWidth / numberOfColumns = 433,cellHeight设置为90,因此返回的CGSize实例将是(433, 90),使得两行可以容纳两个单元格。
-
修改
viewDidLoad()方法以调用setupCollectionView()方法:override func viewDidLoad() { super.viewDidLoad() SharedData.shared.loadJournalEntriesData() **setupCollectionView****()** search.searchResultsUpdater = self search.obscuresBackgroundDuringPresentation = false search.searchBar.placeholder = "Search titles" navigationItem.searchController = search } -
在
viewDidLoad()方法之后添加以下方法,以便在设备旋转时重新计算列数和集合视图单元格的大小:override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() collectionView.collectionViewLayout.invalidateLayout() }
您已经实现了所有根据尺寸类更改集合视图单元格大小的代码。太棒了!在下一节中,您将在不同的模拟设备和您的 Mac 上测试您的应用。
在不同设备上测试您的应用
现在您已经实现了所有动态设置集合视图单元格大小的代码,您将在不同的模拟设备和您的 Mac 上测试您的应用。按照以下步骤操作:
- 模拟器应仍然设置为 iPad。构建并运行您的应用程序。它将显示两列,如下所示:

图 22.34:模拟器显示双列的 iPad 屏幕
- 从设备菜单中选择向左旋转,您仍然会看到两列,但单元格已扩展以填充屏幕:

图 22.35:模拟器显示左旋转的 iPad 屏幕,带有两列
- 停止您的应用程序,并在目标菜单中选择iPhone SE (第 3 代)。再次在模拟器上运行您的应用程序,它将显示单列,如下所示:

图 22.36:模拟器显示单列的 iPhone 屏幕
模拟器在启动 iPhone 实例时不会自动关闭 iPad 实例。为了获得更好的性能,请手动关闭 iPad 实例。
- 从设备菜单中选择向左旋转,您仍然会看到单列,但单元格大小已扩展以填充屏幕:

图 22.37:模拟器显示旋转后的 iPhone 屏幕,带有单列
- 停止您的应用程序,并在目标菜单中选择iPhone 16 Pro Max。再次在模拟器上运行您的应用程序,它将显示单列,如下所示:

图 22.38:模拟器显示单列的 iPhone 屏幕
- 从设备菜单中选择向左旋转,您将看到两列:

图 22.39:模拟器显示旋转后的 iPhone 屏幕,带有两列
- 停止您的应用程序,并在目标菜单中选择MyMac (专为 iPad 设计)。运行您的应用程序,它应该显示两列:

图 22.40:具有两列的 Mac 应用程序
您需要一个免费或付费的 Apple 开发者账户才能在您的 Mac 上运行您的应用程序。
在撰写本文时,当应用程序在 Mac 上运行时点击相机按钮会导致应用程序崩溃。为了解决这个问题,从菜单栏中选择产品 | 方案 | 编辑方案...,从侧边栏中选择运行,点击诊断选项卡,并取消选中Metal API 验证复选框。
您已将您的应用程序修改为使用集合视图代替表格视图,并且已启用它在不同设备上运行时动态修改集合视图单元格大小。做得好!
摘要
在本章中,您将 Journal List 屏幕上的表格视图替换为集合视图,这使得在您在 iPad 或 Mac 上运行应用程序时更好地利用额外的屏幕空间。您还使用大小类使您的应用程序在设备旋转时动态修改列数和集合视图单元格大小。
首先,在Main故事板文件中,您将期刊列表屏幕上的表格视图替换为集合视图,并配置集合视图单元格以显示表格视图单元格曾经显示的相同信息。接下来,您修改了JournalListTableViewController和JournalListTableViewCell类,以便与集合视图和集合视图单元格一起工作。然后,您添加了代码以动态更改集合视图单元格的大小,以适应您的应用程序正在运行的显示。最后,您在模拟器和您的 Mac 上创建并测试了您的应用程序。
您现在应该能够在您的应用程序中使用集合视图,并了解如何根据尺寸类别动态修改您应用程序的界面。
这本书的第三部分到此结束。在下一部分,您将了解苹果在 WWDC 2024 期间推出的新酷炫功能,从 SwiftUI 开始。
留下评论!
感谢您从 Packt Publishing 购买此书——我们希望您喜欢它!您的反馈对我们来说无价,它帮助我们改进和成长。一旦您阅读完毕,请花一点时间在亚马逊上留下评论;这只需一分钟,但对像您这样的读者来说意义重大。扫描下面的二维码或访问链接,以获取您选择的免费电子书。
第四部分
功能
欢迎来到本书的第四部分。在本部分中,你将实现最新的 iOS 18 功能。首先,你将学习如何使用苹果公司的新 SwiftData 框架持久化你的应用数据。接下来,你将学习如何开发 SwiftUI 应用,这是一种为所有苹果平台开发应用的新方法。之后,你将学习如何使用 Swift Testing 测试你的代码,以及如何在你的应用中实现 Apple Intelligence 功能。最后,你将了解如何使用内部和外部测试人员测试你的应用并将其上传到 App Store。
本部分包括以下章节:
-
第二十三章,SwiftData 入门
-
第二十四章,SwiftUI 入门
-
第二十五章,Swift Testing 入门
-
第二十六章,Apple Intelligence 入门
-
第二十七章,测试并提交你的应用到 App Store
到本部分结束时,你将能够在你自己的应用中实现酷炫的 iOS 18 功能。你还将能够测试并发布你自己的应用到 App Store。让我们开始吧!
第二十三章:SwiftData 入门
在 2023 年苹果公司的全球开发者大会(WWDC)上,他们介绍了全新的框架SwiftData,用于保存应用程序数据。之前,开发者必须使用编辑器来创建数据模型,但 SwiftData 允许开发者使用常规 Swift 代码描述数据模型并操作模型实例。自动提供的关系管理、撤销/重做支持、iCloud 同步等功能。在 2024 年,苹果公司添加了新的 API,使开发者能够构建自定义数据存储、处理事务历史记录、模型索引和复合唯一约束等。
在本章中,你将修改在第十六章,“在视图控制器之间传递数据”中完成的JRNL应用程序,以使用 SwiftData 保存日记条目。这意味着当你向应用程序添加新的日记条目时,它们将在应用程序下次启动时再次出现。
首先,你将了解 SwiftData 及其组件。接下来,你将修改JournalEntry类以使其与 SwiftData 兼容,并修改JournalListViewController类以与修改后的JournalEntry类协同工作。之后,你将通过添加代码来实现 SwiftData,这些代码将允许你读取、写入和删除日记条目;最后,你将修改JournalViewController类以读取、保存和删除存储的日记条目。
到本章结束时,你将学会如何使用 SwiftData 保存应用程序数据,并将能够在自己的应用程序中实现它。
本章将涵盖以下主题:
-
介绍 SwiftData
-
修改
JournalEntry类 -
实现 SwiftData 组件
-
修改
JournalListViewController类
技术要求
你将继续在第十六章,“在视图控制器之间传递数据”中修改的JRNL项目上工作。
本书代码包中的资源文件和完成后的 Xcode 项目位于Chapter23文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,了解代码的实际应用:
让我们从了解 SwiftData 开始。
介绍 SwiftData
SwiftData 是苹果公司全新的框架,用于将应用程序数据保存到您的设备上。它自动提供关系管理、撤销/重做支持、iCloud 同步等功能。您可以使用常规 Swift 类型来建模数据,然后 SwiftData 将根据您指定的模型构建自定义模式,并将字段映射到设备存储。您可以使用由编译器进行类型检查的表达式查询和筛选数据,从而减少错误或打字错误。
您可以在 developer.apple.com/documentation/swiftdata 上了解更多关于 SwiftData 的信息。
在 WWDC 2024 上,Apple 为 SwiftData 添加了新的 API。这些 API 允许您使用宏来建模索引和复合唯一约束,使用您自己的文档格式构建自定义数据存储,跟踪事务历史记录,以及更多。
您可以在 developer.apple.com/videos/play/wwdc2024/10137/ 上了解更多关于 SwiftData 中的新功能。
实现应用程序的 SwiftData 需要几个步骤。首先,使用 @Model 宏将现有类转换为模型。支持基本类型,如 Bool、Int 和 String,以及复杂值类型,如结构和枚举。接下来,根据需要使用注释如 @Attribute(.unique) 来确保属性的值是唯一的,以及 @Attribute(.externalStorage) 来将属性的值作为二进制数据存储在模型存储旁边。然后,指定要持久化的模型并创建一个 ModelContainer 实例,该实例管理应用程序的模式和模型存储配置。
之后,使用 ModelContext 实例在应用程序运行时检索、插入和删除模型实例,并将任何更改保存到设备存储。最后,为了从设备存储中检索特定实例,使用包含搜索谓词和排序顺序的 FetchDescriptor 实例。
您可以在 developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches 上了解更多关于在应用程序启动之间保留应用程序模型数据的信息。
在您为 JRNL 应用程序实现 SwiftData 之前,这里有一个示例来帮助您可视化您需要执行的操作以保存日记条目。
想象一下您正在使用 Microsoft Word 保存日记条目。您首先创建一个新的 Word 文档模板,其中包含日记条目的相关字段。然后,您根据模板创建新的 Word 文档并填写数据。您进行必要的更改,比如更改日记条目的文本或更改照片。当您对文档满意时,您将其保存到计算机的硬盘上。下次您想查看日记条目时,您在硬盘上搜索相关文档,双击它以在 Word 中打开,以便再次查看。
现在您已经了解了需要执行的操作,让我们回顾一下实现应用程序的 SwiftData 所需的步骤。
首先,您将现有的 JournalEntry 类转换为模型,这就像是一个 Microsoft Word 模板。您通过使用 @Model 宏注释 JournalEntry 类来实现这一点。JournalEntry 类的属性就像 Microsoft Word 模板中的字段,如果需要,您将使用 @Attribute 宏自定义属性。
接下来,您将创建一个ModelContainer实例,它将用于在您的设备存储上存储JournalEntry模型实例,并创建一个ModelContext实例,它将用于在内存中存储JournalEntry模型实例。这就像从 Microsoft Word 模板创建的 Microsoft Word 文件可以存储在您的计算机硬盘上或在编辑时保留在内存中一样。
之后,您将添加代码,以便在创建新的期刊条目时,创建一个JournalEntry模型实例并将其添加到ModelContext实例中,然后它将与ModelContainer实例协调以将其保存到设备存储。这就像在完成 Word 文档后将其保存到硬盘上一样。
您可以通过观看以下视频了解更多关于如何使用 SwiftData 构建应用程序的信息:developer.apple.com/videos/play/wwdc2023/10154/.
现在您已经基本了解了 SwiftData 的工作原理,您将在下一节中使用JournalEntry类创建JournalEntry模型。
修改JournalEntry类
目前,当您使用添加新期刊条目屏幕创建新的期刊条目并点击保存按钮时,条目将出现在期刊列表屏幕上:

图 23.1:模拟器显示添加到期刊列表屏幕的新条目
如果您退出并重新启动您的应用程序,新添加的条目将消失:

图 23.2:模拟器显示在应用程序重新启动后新条目消失
这是因为journalEntries数组的内容仅在内存中保留,当应用程序关闭时并未保存到设备存储。为了解决这个问题,您将为您的应用程序实现 SwiftData。实现 SwiftData 的第一步是从现有的JournalEntry类创建模型对象,根据需要修改此和其他类。
按照以下步骤操作:
-
在项目导航器中,点击JournalEntry文件(位于期刊列表场景 | 模型组)。导入
SwiftData框架,并使用@Model宏注释JournalEntry类,如下所示:import UIKit **import** **SwiftData** **@Model** class JournalEntry { //MARK: - Properties let date: Date -
从 Xcode 的产品菜单中选择构建。您将在导航器区域看到错误消息出现。点击前三个错误消息中的任何一个以展开宏并显示错误:

图 23.3:编辑区域显示错误
这些错误出现是因为 SwiftData 目前不支持UIImage类。为了修复这个问题,您需要修改JournalEntry类,使用Data实例代替UIImage实例。
-
将
JournalEntry类中的photo属性替换为photoData属性,类型为Data?,如下所示:let body: String **@Attribute****(.****externalStorage****)** **let****photoData****:** **Data****?** let latitude: Double?
@Attribute(.externalStorage)注释将照片数据存储在模型数据相邻的二进制文件中,这使得它更高效。
- 接下来,您将解决其他错误消息,这些错误消息说Cannot expand accessors on variable declared with ‘let’:

图 23.4:显示错误的导航器区域
-
为了解决这个问题,将
JournalEntry类属性的let关键字全部替换为var,如下所示:// MARK: - Properties **var** date: Date **var** rating: Int **var** title: String **var** body: String @Attribute(.externalStorage) **var** photoData: Data? **var** latitude: Double? **var** longitude: Double? -
JournalEntry类的初始化器中会有一个错误。为了修复它,按照以下方式修改初始化器中的代码:self.body = body **self****.****photoData****=** **ph********oto****?****.jpegData(compressionQuality:** **1.0****)** self.latitude = latitude****
****此语句将使用 JPEG 编码将UIImage实例转换为Data实例,这可以存储在photoData属性中。
-
如果现在构建项目,将出现更多错误。在项目导航器中单击JournalEntryDetailViewController文件。按照以下方式修改
viewDidLoad()方法:titleLabel.text = selectedJournalEntry?.title bodyTextView.text = selectedJournalEntry?.body **if****let** **photoData** **=****selectedJournalEntry****?****.****photoData** **{** **photoImageView****.****image****=****UIImage****(****data****:** **photoData****)** **}**
此代码检查JournalEntry实例的photoData属性是否有值。如果有,它将转换为UIImage实例并分配给photoImageView实例的image属性。
-
在项目导航器中单击JournalListViewController文件。按照以下方式修改
tableView(_:cellForRowAt:)方法:let journalEntry = journalEntries[indexPath.row] **if****let** **photoData** **=** **journalEntry.****photoData** **{** **Task** **{** **journalCell.****photoImageView.****image****=****UIImage****(****data****: **** photoData)** **}** **}** journalCell.dateLabel.text = journalEntry.date.formatted( .dateTime.month().day().year() )
此代码检查JournalEntry实例的photoData属性是否有值。如果有,它将转换为UIImage实例并分配给journalCell.photoImageView.image。由于此过程在表格视图的每一行都会重复,并且将 JPEG 数据解码为UIImage实例可能很慢,因此使用Task块使此过程异步。
所有错误现在应该都已解决。您可能需要退出并重新打开项目,才能使所有错误消失。
如果现在构建并运行您的项目,您将得到一个错误,因为 SwiftData 的ModelContainer实例尚未创建。您将在下一节中学习如何创建它。
实现 SwiftData 组件
现在您已经使用JournalEntry类创建了一个JournalEntry模型,您将创建一个包含ModelContainer对象和ModelContext实例的单例类。然后您将为存储在ModelContext中的JournalEntry模型实例添加方法。
单词singleton意味着在您的应用程序中只有一个此类实例。
按照以下步骤操作:
-
在项目导航器中右键单击
JRNL文件夹,并从弹出菜单中选择New File from Template...。 -
iOS应该已经选中。选择Swift File然后点击Next。
-
将此文件命名为
SharedData。点击Create。SharedData文件出现在项目导航器中。将文件移动到SceneDelegate文件下。 -
在
import语句之后添加以下代码:import SwiftData
这允许您使用 SwiftData 框架。
-
在
import语句之后添加以下代码以声明和定义SharedData类:class SharedData { // MARK: - Properties @MainActor static let shared = SharedData() let container: ModelContainer let context: ModelContext // MARK: - Initialization private init() { do { container = try ModelContainer(for: JournalEntry.self) context = ModelContext(container) } catch { fatalError("Could not create SwiftData model container or context") } } }
此类创建了一个单例实例,该实例将在你的整个应用程序中可用,并将其分配给 shared 静态变量。它还创建并初始化 ModelContainer 和 ModelContext 实例,并将它们分别分配给 container 和 context 属性。
接下来,你将添加用于加载、添加和删除 JournalEntry 模型实例的方法。按照以下步骤操作:
-
在初始化器之后添加以下代码以实现
loadJournalEntries()方法:func loadJournalEntries() -> [JournalEntry] { let descriptor = FetchDescriptor<JournalEntry>(sortBy: [SortDescriptor<JournalEntry>(\.date, order: .reverse)]) do { let journalEntries = try context.fetch(descriptor) return journalEntries } catch { return [] } }
让我们分解一下:
func loadJournalEntries() -> [JournalEntry] {
此方法返回一个 JournalEntry 实例数组。
let descriptor = FetchDescriptor<JournalEntry>(sortBy:
[SortDescriptor<JournalEntry>(\.date, order: .reverse)])
该语句创建了一个 FetchDescriptor 实例,指定从 ModelContext 实例中检索并按日期排序(从新到旧)存储的所有 JournalEntry 模型实例。
do {
let journalEntries = try context.fetch(descriptor)
return journalEntries
} catch {
return []
}
}
这段代码从 ModelContext 实例中获取由 FetchDescriptor 实例指定的所有 JournalEntry 模型实例,并将它们分配给 journalEntries,这是一个类型为 [JournalEntry] 的常量,然后返回。如果操作失败,将返回一个空数组。
-
通过在最后的括号之前添加以下代码来实现
saveJournalEntry(_:)方法:func saveJournalEntry(_ journalEntry: JournalEntry) { context.insert(journalEntry) try? context.save() }
此方法将传递给它的 journalEntry 实例作为 JournalEntry 模型实例插入到 ModelContext 实例中。
-
通过在最后的括号之前添加以下代码来实现
deleteJournalEntry(_:)方法:func deleteJournalEntry(_ journalEntry: JournalEntry) { context.delete(journalEntry) try? context.save() }
此方法将从 ModelContext 实例中删除相应的 JournalEntry 模型实例。
你已经创建了一个 SharedData 类,该类创建了一个 ModelContainer 实例,并实现了从 ModelContext 对象中检索、添加和删除 JournalEntry 模型实例的方法。你可以构建你的应用程序以测试错误,但你还不能运行它。
到目前为止,你已经在你的应用程序中实现了所有必需的 SwiftData 组件。在下一节中,你将配置 JournalListViewController 类,以便在应用程序运行时从 ModelContainer 实例中检索所有存储的日记条目,在你添加新的日记条目时将新的 JournalEntry 模型实例添加到 ModelContext 实例中,以及在你删除日记条目时从 ModelContext 实例中删除 JournalEntry 模型实例。
修改 JournalListViewController 类
在之前,当你从你的应用程序中添加或删除日记条目时,当你停止并再次运行应用程序时所做的更改将会消失,因为 JournalListViewController 类中的代码没有将应用程序数据保存到你的设备上的方法。在本节中,你将向 JournalListViewController 类中添加代码以使用 SwiftData 保存应用程序数据。
当你运行应用程序时,你将更新 viewDidLoad() 方法以从设备存储中检索所有日记条目,更新 unwindNewEntrySave(segue:) 方法以将新的日记条目添加到 ModelContext 实例中,并更新 tableView(_:commit:forRowAt:) 方法以从 ModelContext 实例中删除指定的日记条目。按照以下步骤操作:
-
在项目导航器中点击
JournalListViewController文件,并在闭合花括号之前添加一个方法以从设备存储中检索所有日记条目:func fetchJournalEntries() { journalEntries = SharedData.shared.loadJournalEntries() tableView.reloadData() }
此方法调用SharedData单例中的loadJournalEntries()方法,该方法返回一个JournalEntry实例数组。然后此数组被分配给journalEntries数组,表格视图被重新加载。
-
按照以下方式修改
viewDidLoad()方法以调用fetchJournalEntries()方法:override func viewDidLoad() { super.viewDidLoad() **fetchJournalEntries****()** }
由于您添加到应用中的日记条目现在将持久化,因此您不再需要调用用于创建应用示例数据的方法。
-
修改
unwindNewEntrySave(segue:)方法,以便将新的日记条目添加到设备存储并更新表格视图:@IBAction func unwindNewEntrySave(segue: UIStoryboardSegue) { if let sourceViewController = segue.source as? AddJournalEntryViewController, let newJournalEntry = sourceViewController.newJournalEntry { **SharedData****.****shared****.****saveJournalEntry****(newJournalEntry)** **fetchJournalEntries****()** } }
新的JournalEntry实例被传递到SharedData单例中的saveJournalEntry方法,在那里它作为JournalEntry模型实例插入到ModelContext实例中。然后fetchJournalEntries()方法更新journalEntries数组并重新加载表格视图。
-
修改
tableView(_:commit:forRowAt:)方法,以便从设备存储中删除指定的日记条目并更新表格视图:func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { **SharedData****.****shared****.****deleteJournalEntry****(****journalEntries****[indexPath.****row****])** **fetchJournalEntries****()** } }
要删除的JournalEntry实例被传递到SharedData单例中的deleteJournalEntry方法,在那里相应的JournalEntry模型实例从ModelContext实例中删除。然后fetchJournalEntries()方法更新journalEntries数组并重新加载表格视图。
- 您已为
JournalListViewController类完成所有必要的更改。构建并运行您的应用,您应该在日记列表屏幕上看到一个空白的表格视图。使用“添加新日记条目”屏幕添加几个日记条目,它们将出现在日记列表屏幕上。停止并再次运行您的应用。您添加的日记条目仍然会保留:

图 23.5:模拟器显示在您的应用重新启动后,新条目仍然存在
- 滑动一行以删除日记条目。停止并再次运行您的应用。您删除的日记条目仍然会消失。

图 23.6:模拟器显示在您的应用重新启动后,已删除的条目不会重新出现
您已成功在您的应用中实现了 SwiftData,现在当您的应用重新启动时,添加到您的应用中的日记条目仍然会保留。恭喜您!
摘要
在本章中,你修改了你在 第十六章,在视图控制器之间传递数据 中完成的 JRNL 应用程序,使其使用 SwiftData 将日记条目保存到设备存储中,这样你下次启动应用程序时所做的任何更改都会保留。首先,你了解了 SwiftData 及其不同的组件。接下来,你修改了 JournalEntry 类以使其与 SwiftData 一起工作,并修改了你的 JournalListViewController 类以与修改后的 JournalEntry 类一起工作。之后,你添加了代码,允许你从 SwiftData 模型容器中检索、添加和删除日记条目,最后,你修改了 JournalViewController 类,使其能够读取、保存和删除存储的日记条目。
你现在对 SwiftData 的工作原理有了基本的了解,你将能够编写自己的应用程序,使用 SwiftData 来保存应用程序数据。做得好!
在下一章中,你将了解 SwiftUI 的最新发展。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第二十四章:SwiftUI 入门
在前面的章节中,你使用故事板创建了 JRNL 应用的 用户界面(UI)。这个过程涉及将代表视图的对象拖动到故事板中,在视图控制器文件中创建输出,并将两者连接起来。
本章将重点介绍 SwiftUI,这是一种简单且创新的方法,可以在所有 Apple 平台上创建应用。SwiftUI 使用声明式 Swift 语法来指定用户界面,并与新的 Xcode 设计工具协同工作,以保持代码和设计的同步。动态类型、暗黑模式、本地化和无障碍功能都自动支持。
尽管本书侧重于 UIKit,但了解 SwiftUI 对你来说是有益的,因为一些 iOS 功能,如小部件,只能使用 SwiftUI 实现。此外,SwiftUI 似乎将成为所有 Apple 平台应用开发的未来之路,但到目前为止,它还没有与 UIKit 具有相同的功能性。
在本章中,你将使用 SwiftUI 构建一个简化版的 JRNL 应用。此应用将仅包含期刊列表和期刊条目详情屏幕。由于使用 SwiftUI 编写应用与您之前所做的方式非常不同,你将不会修改迄今为止一直在工作的 JRNL 项目。相反,你将创建一个新的 SwiftUI Xcode 项目。
你将首先添加并配置 SwiftUI 视图以创建期刊列表屏幕。接下来,你将在你的应用中添加模型对象,并配置期刊列表和期刊条目详情屏幕之间的导航。之后,你将学习如何使用 MapKit 为期刊条目详情屏幕构建地图视图。最后,你将创建期刊条目详情屏幕。
到本章结束时,你将学会如何构建一个 SwiftUI 应用,该应用可以读取模型对象,以列表形式展示它们,并允许导航到包含地图视图的第二屏幕。然后你可以在自己的项目中实现此功能。
将涵盖以下主题:
-
创建 SwiftUI Xcode 项目
-
创建期刊列表屏幕
-
添加模型对象和配置导航
-
在 SwiftUI 中使用 MapKit
-
创建期刊条目详情屏幕
技术要求
你将为本章创建一个新的 SwiftUI Xcode 项目。
本章的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter24 文件夹中,可以在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
观看以下视频以查看代码的实际运行情况:
让我们从下一节开始创建一个新的 SwiftUI Xcode 项目,用于你的 SwiftUI 应用。
创建 SwiftUI Xcode 项目
创建一个 SwiftUI Xcode 项目的方式与创建一个常规 Xcode 项目的方式相同,但您需要配置它以使用 SwiftUI 而不是故事板来创建用户界面。正如您将看到的,用户界面完全由代码生成,您在修改代码时将能够立即看到用户界面的变化。
您可以在 WWDC20 期间观看苹果 SwiftUI 演示的视频,网址为developer.apple.com/videos/play/wwdc2020/10119。
您可以在 WWDC24 期间观看 SwiftUI 新功能的视频,网址为developer.apple.com/videos/play/wwdc2024/10144/。
您可以在网上找到苹果官方的 SwiftUI 文档,网址为developer.apple.com/xcode/swiftui/。
让我们首先创建一个新的 SwiftUI Xcode 项目。按照以下步骤操作:
-
启动 Xcode 并创建一个新的 Xcode 项目。
-
点击iOS。选择App模板,然后点击下一步。
-
选择您新项目选项屏幕出现:

图 24.1:项目选项屏幕
按照以下方式配置此屏幕:
-
产品名称:
JRNLSwiftUI -
界面:
SwiftUI
其他设置应该已经设置好了。完成后点击下一步。
-
选择保存
JRNLSwiftUI项目的位置,然后点击创建。 -
您的项目将显示在屏幕上,
ContentView文件在项目导航器中被选中。您将在编辑器区域的左侧看到此文件的内容,以及包含预览的画布在右侧:

图 24.2:Xcode 显示 JRNLSwiftUI 项目
- 如果您在画布上看到一个预览暂停框,请点击圆形箭头以显示预览:

图 24.3:带有圆形箭头的预览暂停框
ContentView文件包含声明和定义应用初始视图的代码。如果您需要更多的工作空间,点击导航器按钮隐藏导航器,并将编辑器区域中的边框拖动以调整画布大小:

图 24.4:Xcode 界面显示导航器按钮和编辑器区域中的边框
您已成功创建您的第一个 SwiftUI Xcode 项目!太棒了!现在您将看到如何更改编辑器区域中的代码将更新画布上的预览。
让我们来看看ContentView文件。这个文件包含一个ContentView结构和#Preview宏。ContentView结构描述了视图的内容和布局,并遵循View协议。#Preview宏生成声明ContentView结构预览的源代码,该预览在画布上显示。
要查看宏生成的代码,右键单击宏,并在弹出菜单中选择展开宏。
要查看实际操作,将Hello, World!文本更改为如图所示的JRNL:
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("**JRNL**")
}
.padding()
}
}
画布中的预览将更新以反映您的更改:

图 24.5:显示带有更新后的文本视图的应用预览的画布
在下一节中,您将创建期刊列表屏幕,从显示特定期刊条目数据的视图开始。
创建期刊列表屏幕
当使用故事板时,您使用属性检查器修改视图的属性。在 SwiftUI 中,您可以修改代码或画布中的预览。如您所见,更改ContentView文件中的代码将立即更新预览,而修改预览将更新代码。
让我们自定义ContentView结构以显示特定餐厅的数据。按照以下步骤操作:
- 点击图书馆按钮。在过滤器字段中输入
tex,然后将文本视图拖到编辑区域,并将其放置在包含“JRNL”字符串的文本视图下方:

图 24.6:带有可拖动文本对象的图书馆
-
Xcode 已自动将代码添加到
ContentView文件中,用于此文本视图。请确认您的代码看起来像这样:struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("JRNL") **Text****(****"Placeholder"****)** } .padding() } }
如您所见,在包含"JRNL"字符串的文本视图之后添加了一个第二个文本视图,并且两个文本视图和一个图像视图都被包含在一个VStack视图中。VStack视图包含垂直排列的子视图,它类似于故事板中的垂直堆叠视图。请注意,图像视图有一个systemName属性。此属性可以设置为苹果公司SF Symbols库中的其中一个图像。
您可以在此处了解更多关于 SF Symbols 库的信息:developer.apple.com/sf-symbols/。
- 右键单击
VStack视图,从弹出菜单中选择嵌入到 HStack。

图 24.7:弹出菜单显示嵌入到 HStack 中
-
请确认您的代码看起来像这样:
struct ContentView: View { var body: some View { **HStack** **{** VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("JRNL") Text("Placeholder") } .padding() **}** } }
如您所见,VStack视图现在被包含在一个HStack视图中。HStack视图包含水平排列的子视图,它类似于故事板中的水平堆叠视图。
-
修改如下代码以显示示例期刊条目并重新定位图像视图到两个文本视图的左侧:
struct ContentView: View { var body: some View { HStack { **Image****(****systemName****:** **"face.smiling"****)** **.****imageScale****(.****large****)** **.****foregroundStyle****(.****tint****)** VStack { Text(**"18 Aug 2024"**) Text(**"****Today is a good day"**) } .padding() } } } -
验证更改是否反映在预览中:

图 24.8:显示更新后的文本视图的应用预览
-
要更改用户界面元素的外观,您使用修饰符而不是属性检查器。这些是改变对象外观或行为的方法。请注意,图像视图已经具有修饰符。按照如下所示更新您的代码以设置文本视图的样式和颜色,并设置图像视图的大小:
struct ContentView: View { var body: some View { HStack { Image(systemName: "face.smiling") ** .****resizable****()** **.****frame****(****width****:** **90****,** **height****:** **90****)** VStack { Text("18 Aug 2023") **.****font****(.****title****)** **.****fontWeight****(.****bold****)** **.****frame****(****maxWidth****: .****infinity****,** **alignment****: .****leading****)** Text("Today is a good day") **.****font****(.****title2****)** **.****foregroundStyle****(.****secondary****)** **.****frame****(****maxWidth****: .****infinity****,** **alignment****: .****leading****)** }**.****padding****()** }**.****padding****()** } } -
验证更改是否反映在预览中:

图 24.9:显示示例期刊条目的应用预览
您的视图现在已完成。您将在下一节中将此视图用作期刊列表屏幕上的单元格。
添加模型对象和配置导航
现在,您有一个可以用来在期刊列表屏幕上表示期刊条目的视图。您将使用此视图作为 SwiftUI 列表中的一个单元格,这是一个以单列形式展示数据的容器。您还将配置模型对象以填充此列表。按照以下步骤操作:
-
右键点击
HStack视图,从弹出菜单中选择嵌入到 VStack 中。这样,当您将视图嵌入到列表中时,所有视图都会保持在一起。 -
右键点击外部的
VStack视图,并选择嵌入到列表中以在画布中显示包含五个单元格的列表。同时移除填充修饰符。 -
确认您的代码现在看起来像这样:
struct ContentView: View { var body: some View { **List****(****0****..<****5****) { item** **in** **VStack** **{** HStack { Image(systemName: "face.smiling") .resizable() .frame(width: 90, height: 90) VStack { Text("18 Aug 2024") .font(.title) .fontWeight(.bold) .frame(maxWidth: .infinity, alignment: .leading) Text("Today is a good day") .font(.title2) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } } ** }** **}** } }
如您在画布中所见,您在上一节中创建的视图现在被配置为显示五个项目的列表所包围。请注意,在列表中显示数据不需要任何代表者和数据源。
-
打开您从
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition下载的代码包中的Chapter24文件夹内的resources文件夹。将JournalEntry文件拖到项目导航器中,并在提示时点击完成将其添加到您的项目中。 -
在项目导航器中点击
JournalEntry文件,您应该在其中看到以下代码:import UIKit struct JournalEntry: Identifiable, Hashable { // MARK: - Properties let id = UUID() let date = Date() let rating: Int let entryTitle: String let entryBody: String let photo: UIImage? let latitude: Double? let longitude: Double? } //MARK: - Sample data let testData = [ JournalEntry(rating: 5, entryTitle: "Today is a good day", entryBody: "I got top marks in my exam today! Great!", photo: UIImage(systemName: "sun.max"), latitude: 37.3346, longitude: -122.0090), JournalEntry(rating: 0, entryTitle: "Today is a bad day", entryBody: "I wasn't feeling very well today.", photo: UIImage(systemName: "cloud"), latitude: nil, longitude: nil), JournalEntry(rating: 3, entryTitle: "Today is an OK day", entryBody: "Just having a nice lazy day at home", photo: UIImage(systemName: "cloud.sun"), latitude: nil, longitude: nil) ]
JournalItem文件包含一个结构JournalItem和一个数组testData。
JournalItem结构类似于您在JRNL项目中使用的JournalItem类。要在列表中使用此结构,您必须使其符合Identifiable协议。此协议指定列表项必须有一个可以识别特定项的id属性。在创建每个JournalEntry实例时,都会分配一个UUID实例,以确保每个id属性中存储的值是唯一的。
注意,此结构也符合Hashable协议。这将在您点击单元格时用于确定要显示的数据。
testData是一个包含三个JournalItem实例的数组,您将使用它来填充期刊列表屏幕。
您可以在以下链接中了解更多关于Identifiable协议的信息:developer.apple.com/documentation/swift/identifiable。
您可以在以下链接中了解更多关于Hashable协议的信息:developer.apple.com/documentation/swift/hashable。
-
在项目导航器中点击
ContentView文件。在ContentView结构的开括号之后添加一个journalEntries属性,并将其分配给testData数组:struct ContentView: View { **var****journalEntries****=****testData** var body: some View { -
按照以下所示修改您的代码,以在每个视图中显示期刊条目的照片、日期和标题:
struct ContentView: View { var journalEntries = testData var body: some View { List(**journalEntries****journalEntry**) { in VStack { HStack { Image(**uiImage****: journalEntry.photo** **??****UIImage****(** **systemName****:** **"face.smiling"****)****!**) .resizable() .frame(width: 90, height: 90) VStack { Text(**journalEntry.date.****formatted****(** **.****dateTime****.****day****().****month****().****year****())** ) .font(.title) .fontWeight(.bold) .frame(maxWidth: .infinity, alignment: .leading) Text(**journalEntry.****entryTitle**) .font(.title2) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } } } } } }
让我们看看这段代码是如何工作的。
ContentView结构在journalEntries属性中存储了一个JournalEntry实例数组。这个数组被传递到列表中。对于journalEntries数组中的每个项目,都会创建一个视图并将其分配给项目的属性数据。
每个日记条目的图像都是从存储在journalEntry.photo中的UIImage实例转换而来,如果photo属性为nil,则提供默认值。日期使用formatted()方法转换为文本字符串。
由于数组中有三个项目,画布中会出现三个VStack视图。
当你对代码进行重大更改时,画布的自动更新会暂停。如果需要继续,请点击预览暂停框中的圆形箭头。
接下来,您将实现导航,以便当单元格被点击时,将显示一个第二屏幕,该屏幕将显示特定日记条目的详细信息。按照以下步骤操作:
-
右键点击
List视图,从弹出菜单中选择嵌入...,并用NavigationStack替换占位文本。 -
确认您的代码现在看起来像这样:
struct ContentView: View { var journalEntries = testData var body: some View { **NavigationStack** **{** List(journalEntries) { journalEntry in
在前面的代码中,导航堆栈工作类似于之前在您的应用中使用的UINavigationController类实例。
-
在此处所示的位置添加一个
navigationTitle()修饰符,将列表视图的title属性设置为在屏幕顶部显示Journal List:alignment: .leading } } } }**.****navigationTitle****(****"Journal List"****)** } } } -
如此嵌入单元格到导航链接视图中,并在
.navigationTitle()修饰符之后添加.navigationDestination(for:destination:)修饰符,以便在点击VStack视图时在新屏幕中显示日记条目的标题:List(journalEntries) { journalEntry in **NavigationLink****(****value****: journalEntry) {** VStack { HStack { Image(uiImage: journalEntry.photo ?? UIImage( systemName: "face.smiling")!) .resizable() .frame(width: 90, height: 90) VStack { Text(journalEntry.date.formatted( .dateTime.day().month().year())) .font(.title) .fontWeight(.bold) .frame(maxWidth: .infinity, alignment: .leading) Text(journalEntry.entryTitle) .font(.title2) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } } } }.navigationTitle("Journal List") **.****navigationDestination****(****for****:** **JournalEntry****.****self****) {** **journalEntry** **in** **Text****(journalEntry.****entryTitle****)** **}** } -
注意,画布中的列表已自动显示展开箭头:

图 24.10:显示展开箭头的应用预览
要确保在应用中按预期工作,请确保画布中的实时预览按钮被选中:

图 24.11:显示实时预览按钮的画布
点击预览中的任何单元格,将显示包含被点击日记条目标题的文本:

图 24.12:显示被点击日记条目标题的应用预览
这是一种确保您的列表按预期工作的好方法。
-
视图代码开始显得杂乱,因此您将
VStack视图提取到其自己的单独视图中。右键点击NavigationLink视图,从弹出菜单中选择提取子视图。 -
确认所有
VStack视图的视图代码都已移动到名为ExtractedView的单独视图中。您的代码现在将看起来像这样:struct ContentView: View { var journalEntries = testData var body: some View { NavigationStack { List(journalEntries) { journalEntry in **ExtractedView****()** }.navigationTitle("Journal List") .navigationDestination(for: JournalEntry.self) { journalEntry in Text(journalEntry.entryTitle) } } } } #Preview { ContentView() } **struct****ExtractedView****:** **View** **{** **var** **body:** **some****View** **{** **NavigationLink****(****value****: journalEntry) {** -
右键点击
ExtractedView视图,从弹出菜单中选择重构 | 重命名。 -
将提取视图的名称更改为
JournalCell,完成后点击重命名。

图 24.13:从 ExtractedView 重命名为 JournalCell
-
确认您的代码现在看起来像这样:
struct ContentView: View { var journalEntries = testData var body: some View { NavigationStack { List(journalEntries) { journalEntry in **JournalCell****(**) }.navigationTitle("Journal List") .navigationDestination(for: JournalEntry.self) { journalEntry in Text(journalEntry.entryTitle) } } } } #Preview { ContentView() } struct **JournalCell**: View { var body: some View { NavigationLink(value: journalEntry) {
不要担心错误;您将在接下来的两个步骤中修复它。
-
为
JournalCell视图添加一个属性以保存JournalEntry实例:struct JournalCell: View { **var****journalEntry****:** **JournalEntry** -
按照以下方式在
ContentView结构中添加代码,将JournalEntry实例传递到JournalCell视图中:struct ContentView: View { var journalEntries: = testData var body: some View { NavigationStack { List(journalEntries) { journalEntry in JournalCell(**journalEntry****: journalEntry**) }.navigationTitle("Journal List") .navigationDestination(for: JournalEntry.self) { journalEntry in Text(journalEntry.entryTitle) } } } } -
验证预览是否仍然按预期工作。
到此为止,你已经完成了 Journal List 屏幕的实现。酷!在下一节中,你将了解如何使用 MapKit for SwiftUI 创建一个将在 Journal Entry Detail 屏幕中使用的地图视图。
使用 MapKit for SwiftUI
在 WWDC23 期间,Apple 引入了针对 SwiftUI 的 MapKit 扩展支持,这使得将地图集成到你的应用程序中比以往任何时候都更容易。使用 SwiftUI,你可以轻松地向地图添加注释和覆盖层,控制相机等等。
要观看 WWDC23 中 Apple 的 Meet MapKit for SwiftUI 视频,请参阅此链接:developer.apple.com/videos/play/wwdc2023/10043/。
到目前为止,你已经创建了 Journal List 屏幕并且点击该屏幕上的每个单元格都会在第二个屏幕上显示日记条目的标题。你将修改你的应用程序以在点击 Journal List 屏幕上的单元格时显示 Journal Entry Detail 屏幕但在此之前,你将创建一个 SwiftUI 视图来显示地图。
按照以下步骤操作:
-
选择 文件 | 新建 | 从模板新建... 以打开模板选择器。
-
iOS 应已选中。在 用户界面 部分中,点击 SwiftUI 视图 并点击 下一步。
-
将新文件命名为
MapView并点击 创建。MapView文件将出现在项目导航器中。 -
在
MapView文件中,导入MapKit并将Text视图替换为Map视图:import SwiftUI **import** **MapKit** struct MapView: View { var body: some View { **Map****()** } } -
验证画布中是否显示地图:

图 24.14:显示地图的画布
-
在
MapView结构中添加一个类型为JournalEntry的journalEntry属性:struct MapView: View { **var****journalEntry****:** **JournalEntry** var body: some View { Map() } } -
按照以下方式修改
#Preview宏,将testData数组中的一个日记条目分配给journalEntry属性:#Preview { MapView(**journalEntry****:** **testData****[****0****]**) } -
按照以下方式将
Marker实例添加到Map视图中:Map() { **Marker****(****journalEntry****.****entryTitle****,** **coordinate****:****CLLocationCoordinate2D****(****latitude****:** **journalEntry****.****latitude****??****0.0****,** **longitude****:****journalEntry****.****longitude****??****0.0****))** }
Marker 实例的 title 和 coordinate 属性的值是从 journalEntry 实例的 entryTitle、latitude 和 longitude 属性中获得的,并且 Marker 实例的 coordinate 属性将确定要显示的地图区域的中心点。
-
当前地图已完全缩进。要设置缩放级别,请将以下代码添加到 Map 视图中:
Map(**bounds****:** **MapCameraBounds****(****minimumDistance****:** **4500****)**) { Marker(journalEntry.entryTitle, coordinate: CLLocationCoordinate2D(latitude: journalEntry.latitude ?? 0.0, longitude: journalEntry.longitude ?? 0.0)) } -
验证地图当前是否显示 Apple Park 的地图:

图 24.15:显示 Apple Park 地图的画布
你已经创建了一个显示日记条目位置的 SwiftUI 地图视图。现在,让我们看看如何在下一节中制作完整的 Journal Entry Detail 屏幕。
完成 Journal Entry Detail 屏幕的实现
现在,你有一个显示地图的 SwiftUI 地图视图。现在,你将创建一个新的 SwiftUI 视图来表示 Journal Entry Detail 屏幕并将地图视图添加到其中。按照以下步骤操作:
-
选择文件 | 新建 | 从模板新建文件...以打开模板选择器。
-
iOS应该已经选中。在用户界面部分,点击SwiftUI 视图并点击下一步。
-
将新文件命名为
JournalEntryDetail并点击创建。JournalEntryDetail文件将出现在项目导航器中。 -
在此文件中声明并定义
JournalEntryDetail结构,如下所示:import SwiftUI struct JournalEntryDetail: View { var selectedJournalEntry: JournalEntry var body: some View { ScrollView { VStack(spacing: 30) { Text(selectedJournalEntry.date.formatted( .dateTime.day().month().year())) .font(.title) .fontWeight(.bold) .frame(maxWidth: .infinity, alignment: .trailing) Text(selectedJournalEntry.entryTitle) .font(.title) .fontWeight(.bold) .frame(maxWidth: .infinity, alignment: .leading) Text(selectedJournalEntry.entryBody) .font(.title2) .frame(maxWidth: .infinity, alignment: .leading) Image(uiImage: selectedJournalEntry.photo ?? UIImage(systemName: "face.smiling")!) .resizable() .frame(width: 300, height: 300) if (selectedJournalEntry.longitude != nil && selectedJournalEntry.latitude != nil) { MapView(journalEntry: selectedJournalEntry) .frame(width: 300, height: 300) } }.padding() .navigationTitle("Entry Detail") } } } #Preview { NavigationView { JournalEntryDetail(selectedJournalEntry: testData[0]) } }
JournalEntryDetail结构包含一个类型为JournalEntry的selectedJournalEntry属性和一个包含Vstack视图的ScrollView视图。VStack视图包含显示所选日记条目的日期、标题和正文的Text视图,显示所选日记条目照片的Image视图,以及显示所选日记条目位置的地图的MapView视图,前提是所选日记条目的longitude和latitude属性不是nil。
要在画布中创建预览,将testData数组中的第一个JournalEntry实例分配给selectedJournalEntry属性。请注意,JournalEntryDetail实例被包含在一个NavigationView实例中,以便在预览中显示导航栏。
- 验证画布现在是否显示了一个可滚动的日记条目详情屏幕,并渲染了地图:

图 24.16:应用预览显示日记条目详情屏幕
现在你已经使用 SwiftUI 完成了日记条目详情屏幕的实现,你将修改日记列表屏幕上的列表,以便在单元格被点击时显示日记条目详情屏幕。
按照以下步骤操作:
-
在项目导航器中点击
ContentView文件,并修改navigationDestination(for:destination:)修饰符,以便在单元格被点击时使用JournalEntryDetail结构作为目标:.navigationDestination(for: JournalEntry.self) { journalEntry in **JournalEntryDetail****(selectedJournalEntry: journalEntry)** } -
画布中的实时预览按钮应该已经选中。在日记列表屏幕上点击一个单元格。你将看到该餐厅的日记条目详情屏幕:

图 24.17:应用预览显示日记条目详情屏幕
- 构建并运行你的应用以在模拟器中测试它:

图 24.18:模拟器显示日记列表屏幕
你已经使用 SwiftUI 构建了一个简单的JRNL应用版本!太棒了!
摘要
在本简要介绍 SwiftUI 中,你看到了如何使用 SwiftUI 构建JRNL应用的简化版本。
你首先添加并配置 SwiftUI 视图以创建日记列表屏幕。然后,你将模型对象添加到你的应用中,并配置了日记列表和日记条目详情屏幕之间的导航。之后,你使用 MapKit 为日记条目详情屏幕构建了一个地图视图。最后,你创建了日记条目详情屏幕,并将之前创建的地图视图添加到其中。
你现在知道了如何使用 SwiftUI 创建一个读取模型对象、在列表中展示它们并允许导航到包含地图视图的第二屏幕的应用程序。你现在可以在自己的项目中实现这一点。
如果你想了解更多关于 SwiftUI 的信息,可以参考苹果的 Develop in Swift 教程:
developer.apple.com/tutorials/develop-in-swift
Packt Publishing 也有一本关于 SwiftUI 的书。你可以在这里了解更多信息:
www.amazon.com/SwiftUI-Cookbook-building-beautiful-interactive/dp/1805121731
在下一章中,你将学习关于 Swift 测试 的内容,它让你能够轻松地测试你的 Swift 代码。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会话与作者聊天,等等。扫描二维码或访问链接加入社区。
第二十五章:Swift 测试入门
苹果在 WWDC24 上推出了 Swift 测试。这是一个新的框架,它使您能够使用表达性和直观的 API 简单地测试您的 Swift 代码。
在本章中,您将为 JournalEntry 类创建和运行测试,以确保它按预期工作。
您将首先为您的应用添加一个新的 单元测试 目标。接下来,您将为 JournalEntry 类编写一些测试,最后,您将在 JournalEntry 类上运行这些测试,以确保它按预期工作。
到本章结束时,您将学会如何为您的应用中的类编写测试,以确保它们按预期工作。这对于涉及许多人员的大型项目非常有用,在这些项目中,您无法查看项目中所有类的源代码。
以下内容将涵盖:
-
介绍 Swift 测试
-
将单元测试目标添加到您的应用中
-
为
JournalEntry类编写测试 -
测试
JournalEntry类
技术要求
您将继续在 Chapter 23 中修改的 JRNL 项目上工作,SwiftData 入门。
本章完成的 Xcode 项目位于本书代码包的 Chapter25 文件夹中,您可以通过以下链接下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频以查看代码的实际运行情况:
让我们先了解 Swift 测试及其工作原理。
介绍 Swift 测试
如果您是涉及许多开发者的大型项目的首席开发者,您详细审查每个人的源代码可能并不实用,在某些情况下,您甚至无法查看源代码。相反,您将发布关于一个类应该做什么的规范,然后开发者的工作是为您编写这个类。
例如,让我们看看 JRNL 应用中 JournalEntry 类的初始化代码:
init?(rating: Int, title: String, body: String, photo: UIImage? = nil, latitude: Double? = nil, longitude: Double? = nil) {
if title.isEmpty || body.isEmpty || rating < 0 || rating > 5 {
return nil
}
self.date = Date()
self.rating = rating
self.title = title
self.body = body
self.photoData = photo?.jpegData(compressionQuality: 1.0)
self.latitude = latitude
self.longitude = longitude
}
如您所见,只有当 entryTitle 和 entryBody 不为空且 rating 在 0 到 5(包括 0 和 5)之间时,您才能创建有效的 JournalEntry 实例。如果这些要求不满足,初始化器将返回 nil。
假设您无法查看 JournalEntry 类的源代码。您如何知道这个类按预期工作?这就是 Swift 测试发挥作用的地方。
Swift 测试使用宏构建了一个清晰且表达性强的 API,这使得编写测试以确定您的代码是否正常工作变得容易。它适用于 Swift 支持的所有主要平台,并且是开源的。
您可以在此处观看有关 Swift 测试的更多信息:developer.apple.com/videos/play/wwdc2024/10179/.
在下一节中,您将了解如何将单元测试目标添加到您的项目中。
将单元测试目标添加到您的应用中
为了能够测试 JournalEntry 类,您将向您的应用添加一个单元测试目标。您将能够在这里编写应用中所有类的所有测试。按照以下步骤操作:
-
在 Xcode 中,选择 文件 | 新建 | 目标 以打开模板选择器。
-
iOS 应已选中。在 测试 部分,单击 单元测试包 并单击 下一步:

图 25.1:模板选择器窗口
- 在 选择新目标选项 窗口中,保留所有选项的默认值并单击 完成:

图 25.2:选项窗口
- 确认在项目导航器中可见 JRNLTests 文件夹:

图 25.3:项目导航器显示 JRNLTests 文件夹
您已成功将单元测试目标添加到您的应用中!在下一节中,您将编写一些测试以确保 JournalEntry 类按预期工作。
为 JournalEntry 类编写测试
正如您所看到的,JournalEntry 类的初始化器只有在 title 和 body 不为空且 rating 在 0 到 5(包含)之间时才会返回 JournalEntry 实例。如果这些条件不满足,初始化器将返回 nil。为了测试这一点,您将编写测试以确认在满足上述条件时创建了有效的 JournalEntry 实例,并且在不满足条件时返回 nil。按照以下步骤操作:
-
在项目导航器中单击 JRNLTests 文件夹内的 JRNLTests 文件。
-
按照以下方式修改此文件的内容:
import Testing **@testable****import** **JRNL** struct JRNLTests { **@Test****func****JournalEntryInitializationSucceeds****()** { let zeroRatingJournalEntry = JournalEntry(rating: 0, title: "Zero", body: "Zero rating entry") #expect(zeroRatingJournalEntry != nil) let positiveRatingJournalEntry = JournalEntry( rating: 5, title: "Highest", body: "Highest rating entry") #expect(positiveRatingJournalEntry != nil) } @Test func JournalEntryInitializationFails() { let entryTitleEmptyJournalEntry = JournalEntry( rating: 3, title: "", body: "No title") #expect(entryTitleEmptyJournalEntry == nil) let entryBodyEmptyJournalEntry = JournalEntry( rating: 3, title: "No body", body: "") #expect(entryBodyEmptyJournalEntry == nil) let negativeRatingJournalEntry = JournalEntry( rating: -1, title: "Negative", body: "Negative rating entry") #expect(negativeRatingJournalEntry == nil) let invalidRatingJournalEntry = JournalEntry( rating: 6, title: "Invalid", body: "Invalid rating entry") #expect(invalidRatingJournalEntry == nil) } }
让我们分解一下:
import Testing
这导入了 Swift 测试框架。
@testable import JRNL
这使得 JRNL 项目中的所有代码都可用于测试。
@Test func JournalEntryInitializationSucceeds() {
let zeroRatingJournalEntry = JournalEntry(rating: 0, title: "Zero", body: "Zero rating entry")
#expect(zeroRatingJournalEntry != nil)
let positiveRatingJournalEntry = JournalEntry( rating: 5, title: "Highest", body: "Highest rating entry")
#expect(positiveRatingJournalEntry != nil)
}
此函数用于确认当 entryTitle 有值、entryBody 有值且 rating 在 0 到 5(包含)之间时,创建了有效的 JournalEntry 类实例。#expect 宏检查 zeroRatingJournalEntry 和 positiveRatingJournalEntry 都不是 nil,因此确认已创建了有效的 JournalEntry 实例。
@Test func JournalEntryInitializationFails() {
let entryTitleEmptyJournalEntry = JournalEntry( rating: 3, title: "", body: "No title")
#expect(entryTitleEmptyJournalEntry == nil)
let entryBodyEmptyJournalEntry = JournalEntry( rating: 3, title: "No body", body: "")
#expect(entryBodyEmptyJournalEntry == nil)
let negativeRatingJournalEntry = JournalEntry( rating: -1, title: "Negative", body: "Negative rating entry")
#expect(negativeRatingJournalEntry == nil)
let invalidRatingJournalEntry = JournalEntry(
rating: 6, title: "Invalid", body:
"Invalid rating entry")
#expect(invalidRatingJournalEntry == nil)
}
此函数用于确认当 entryTitle 为空、entryBody 为空且 rating 不在 0 到 5(包含)之间时,将返回 nil。#expect 宏将确定 entryTitleEmptyJournalEntry、entryBodyEmptyJournalEntry、negativeRatingJournalEntry 和 invalidRatingJournalEntry 是否都是 nil,因此确认没有创建 JournalEntry 实例。
由于这是 Swift 测试的简要介绍,单个函数中会检查多个条件,但为了清晰起见,专业程序员通常使用一个函数执行一个测试。
您已完成了 JournalEntry 类所有测试的编写。在下一节中,您将运行测试以确认 JournalEntry 类按预期工作。
测试 JournalEntry 类
由于你在上一节中已经完成了 JournalEntry 类的所有测试编写,你现在将运行测试以查看 JournalEntry 类是否按预期工作。请按照以下步骤操作:
-
在 Xcode 中,从产品菜单中选择测试。
-
Xcode 将自动运行所有测试。完成后,你将看到以下结果:

图 25.4:显示小部件预览的画布
带勾号的绿色方块表明所有测试都已成功完成,JournalEntry 类按预期工作。做得好!
摘要
在本章中,你测试了 JournalEntry 类,以确定它是否按预期工作。
首先,你为你的应用添加了一个新的单元测试目标。接下来,你为 JournalEntry 类编写了一些测试,最后,你运行了 JournalEntry 类的测试,以确保它按预期工作。
你现在已经学会了如何为你的应用中的类编写测试,以确保它们按预期工作。这对于涉及许多人的大型项目非常有用,在这些项目中,你无法查看项目中所有类的源代码,并且还能确保在应用中任何地方所做的更改都不会破坏现有功能。
在下一章中,你将了解Apple Intelligence,这是苹果在 WWDC24 期间推出的 AI 技术的实现。
加入我们的 Discord!
与其他用户、专家和作者本人一起阅读这本书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything(问我任何问题)会议与作者聊天,等等。扫描二维码或访问链接加入社区。
[(https://packt.link/ios-Swift)]
[(https://packt.link/ios-Swift
)]
第二十六章:开始使用苹果智能
在 2024 年苹果全球开发者大会上,苹果推出了苹果智能,这是一个将强大的生成模型置于苹果设备中的个人智能系统,使您的应用能够实现新的 AI 驱动功能。这些功能包括写作工具、图像游乐场、Genmoji和带有应用意图的 Siri。然而,在撰写本文时,只有写作工具可用。
在本章中,您将使用您在第二十二章,开始使用集合视图中完成的JRNL应用来探索苹果智能功能。
首先,您将了解苹果智能及其功能。接下来,您将看到预测代码补全如何帮助您编写应用。最后,您将了解写作工具,并了解它在您的应用中的工作方式。
到本章结束时,您将学会如何在 Xcode 和您的应用中使用苹果智能功能。
本章将涵盖以下主题:
-
介绍苹果智能
-
在 Xcode 中使用预测代码补全
-
在您的应用中实现写作工具
技术要求
您将继续在第二十二章,开始使用集合视图中修改的JRNL项目中工作。
本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter26文件夹中,可在此处下载:
github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition
查看以下视频,了解代码的实际应用:
让我们从下一节开始学习苹果智能。
介绍苹果智能
苹果智能是一个由设备端和服务器端处理组成的人工智能平台,它将启用令人难以置信的新功能,帮助用户沟通、工作和表达自己。这些功能包括:
-
预测代码补全,帮助您使用 Xcode 编写代码。
-
写作工具,帮助用户校对、重写和总结文本。
-
图像游乐场,允许用户创建有趣和富有创意的图像。
-
Genmoji,允许用户创建适合任何场合的 emoji。
-
带有应用意图的 Siri,允许开发者赋予 Siri 在您的应用中执行操作的能力。
苹果智能将免费提供给所有苹果用户,并将于 2024 年秋季推出。在撰写本文时,只有预测代码补全和写作工具可用。
要了解苹果智能功能可以做什么,请观看这个视频:www.youtube.com/watch?v=Q_EYoV1kZWk.
您可以通过此链接查看有关苹果智能的苹果开发者文档:developer.apple.com/apple-intelligence/.
在下一节中,你将学习苹果智能如何帮助你为你的应用程序编写代码。
在 Xcode 中使用预测代码补全
正如你所知,当你输入代码时,Xcode 将通过在弹出菜单中显示建议来帮助你。代码补全将此提升到另一个层次,通过设备上的 AI 编码模型提供更全面的代码建议。此模型专门针对 Swift 和 Apple SDK 进行训练,并将能够根据周围的代码上下文(如函数名称和注释)推断出你试图做什么。
要看到这个动作,你将在项目中添加一个新文件并创建一些示例结构和函数。按照以下步骤操作:
-
打开代码包中
Chapter22文件夹中的完成 Xcode 项目,该代码包可以从github.com/PacktPublishing/iOS-18-Programming-for-Beginners-Ninth-Edition下载。 -
在Xcode菜单中点击设置,然后点击文本编辑选项卡。点击编辑选项卡并勾选预测代码完成复选框:

图 26.1:显示预测代码完成复选框的设置窗口
- 将会出现一个下载预测代码完成模型?的警告。点击下载按钮下载并安装语言模型,并等待其完成:

图 26.2:下载预测代码完成模型?的警告
- 在设置窗口中点击组件选项卡,并验证预测代码完成模型是否存在:

图 26.3:显示组件选项卡的设置窗口
-
在项目导航器中右键单击JRNL文件夹,然后选择从模板新建文件...。从模板选择器中选择Swift 文件并命名文件为
Employee。它将出现在项目导航器中。 -
在
import语句之后输入以下注释和代码:// This file contains the definition of the Employee structure, a method that will generate sample data, an EmployeeDatabase structure containing an array of Employee instances and methods to add, delete and find employees in an array. struct Employee -
Xcode 将显示预测代码建议。按Tab键接受它:

图 26.4:显示预测代码建议的编辑区域
- Xcode 将显示
Employee结构可能属性的列表。按Tab键接受它:

图 26.5:显示预测代码建议的编辑区域
注意,Xcode 已自动为你创建了Employee结构。真酷!
-
在
Employee结构定义之后输入ex并按Tab键接受预测代码建议。 -
确认 Xcode 已自动创建一个生成示例数据的方法:

图 26.6:显示语法错误的编辑区域
注意,可能存在一些需要修复的语法错误。随着苹果公司随着时间的推移更新语言模型,这可能会得到改善。
修复语法错误后,你的代码应该看起来类似于以下这样:
struct Employee: Codable, Identifiable {
let id: Int
let name: String
let age: Int
let salary: Double
}
extension Employee {
static func generateSampleData() -> [Employee] {
[
Employee(id: 1, name: "John Doe", age: 25,
salary: 100000),
Employee(id: 2, name: "Jane Smith", age: 28,
salary: 120000),
Employee(id: 3, name: "Jimmy Johnson", age: 30,
salary: 150000),
]
}
}
-
在扩展后输入
struct Emp,并继续按Tab键接受建议,直到没有更多建议出现。生成的代码将类似于以下内容:struct EmployeeDatabase: Codable { var employees: [Employee] } -
在
employees属性声明后输入func,并继续按Tab键接受建议,直到没有更多建议出现。生成的代码将类似于以下内容:mutating func add(_ employee: Employee) { employees.append(employee) } -
在
add(_:)方法的定义后输入两次Return键,等待代码建议出现。继续按Tab键接受建议,直到没有更多建议出现。生成的代码将类似于以下内容:func find(by id: Int) -> Employee? { employees.first(where: { $0.id == id }) } -
重复步骤 14,并继续按Tab键接受建议,直到没有更多建议出现。生成的代码将类似于以下内容:
mutating func delete(by id: Int) { employees.removeAll(where: { $0.id == id }) } -
确认生成的代码与这里显示的代码类似:
// This file contains the definition of the Employee structure, a method that will generate sample data, an EmployeeDatabase structure containing an array of Employee instances and methods to add, delete and find employees in an array. struct Employee: Codable, Identifiable { let id: Int let name: String let age: Int let salary: Double } extension Employee { static func generateSampleData() -> [Employee] { [ Employee(id: 1, name: "John Doe", age: 25, salary: 100000), Employee(id: 2, name: "Jane Smith", age: 28, salary: 120000), Employee(id: 3, name: "Jimmy Johnson", age: 30, salary: 150000), ] } } struct EmployeeDatabase: Codable { var employees: [Employee] mutating func add(_ employee: Employee) { employees.append(employee) } func find(by id: Int) -> Employee? { employees.first(where: { $0.id == id }) } mutating func delete(by id: Int) { employees.removeAll(where: { $0.id == id }) } }
在预测代码补全的帮助下,您已成功创建了注释中描述的类和方法,所需输入的代码非常少!太棒了!
然而,请注意,生成的代码并不完美,您可能需要根据需要修复错误和其他问题。
在下一节中,您将学习如何将写作工具实现到您的应用中,以校对、重写和总结文本。
在您的应用中实现写作工具
写作工具是 Apple 智能的一项系统级功能,可以帮助您校对、重写和总结文本。只要您的应用在支持的环境中运行,并且您在应用中使用UITextView、NSTextView或WKWebView,写作工具就会自动出现。苹果公司还引入了文本视图代理方法和属性,以便在写作工具使用时,您的应用可以采取适当的操作。
要查看 Apple 的 WWDC24 关于写作工具的视频,请参阅:developer.apple.com/videos/play/wwdc2024/10168/。
要查看写作工具的实际应用,您需要在 Mac 上运行JRNL应用,并使用写作工具修改“添加新日志条目”屏幕中的文本。您还将探索苹果公司引入的新文本视图代理方法和属性。按照以下步骤操作:
- 在您的 Mac 上打开系统设置,在侧边栏中选择Apple 智能与 Siri,并开启Apple 智能:

图 26.7:开启 Apple 智能的系统设置窗口
- 在 Xcode 中打开
JRNL项目,并在工具栏中的目标菜单中选择我的 Mac(适用于 iPad):

图 26.8:选择“我的 Mac”(适用于 iPad)的目标菜单
- 点击项目导航器顶部的JRNL图标,点击JRNL目标,然后点击签名与功能选项卡。将团队设置为免费或付费的 Apple 开发者账户,并根据需要修改捆绑标识符,直到配置文件中没有更多错误:

图 26.9:签名和功能屏幕
在设备上运行您的应用程序的详细信息请参阅第一章,探索 Xcode。
- 在您的 Mac 上构建并运行应用程序,然后点击+按钮以显示添加新日志条目屏幕:

图 26.10:带有高亮显示+按钮的日志屏幕
- 在文本视图中输入几段文本。选择所有文本,右键单击,并选择写作工具 | 校对:

图 26.11:显示已选择校对的弹出菜单的文本视图
- 点击左右箭头按钮以浏览更改,并在您完成审查后点击完成:

图 26.12:在文本视图中审查写作工具更改
- 尝试其他写作工具功能,并观察它们的作用:

图 26.13:显示可用功能的写作工具子菜单
-
您可能希望在写作工具激活时禁用文本视图的编辑。在项目导航器中点击AddJournalViewController文件。在文件中的所有其他代码之后添加以下扩展:
extension AddJournalEntryViewController { // MARK: - Writing Tools Delegate methods func textViewWritingToolsWillBegin(_ textView: UITextView) { textView.isEditable = false } func textviewWritingToolsDidEnd(_ textView: UITextView) { textView.isEditable = true } }
此代码在写作工具激活时禁用了文本视图的编辑。
-
在某些情况下,您可能希望完全禁用写作工具。在
AddJournalEntryViewController类的viewDidLoad()方法中,在所有其他代码之后添加此行:bodyTextView.writingToolsBehavior = .none
此代码禁用了写作工具,并且写作工具菜单项将不再显示。
您已成功探索了如何在您的应用程序中使用写作工具。太棒了!
摘要
在本章中,您修改了JRNL应用程序,该应用程序是在第二十二章,开始使用集合视图中完成的,以与 Apple Intelligence 一起工作。
首先,您了解了 Apple Intelligence 及其功能。接下来,您在预测代码补全的帮助下创建了一个新的结构和相关函数。最后,您学习了写作工具及其在您的应用程序中的工作方式。
您现在已经学会了如何在 Xcode 和您的应用程序中使用 Apple Intelligence 功能。太棒了!
在下一章中,您将学习如何测试并将您的应用程序提交到 App Store。
加入我们的 Discord 社区!
与其他用户、专家和作者本人一起阅读此书。提出问题,为其他读者提供解决方案,通过 Ask Me Anything 会议与作者聊天,等等。扫描二维码或访问链接以加入社区。
第二十七章:测试并将你的应用提交至 App Store
恭喜你,你已经到达了这本书的最后一章!
在本书的整个过程中,你已经学习了 Swift 编程语言以及如何使用 Xcode 构建整个应用。然而,到目前为止,你只在自己的设备上使用免费苹果开发者账号在模拟器中运行过你的应用。
在本章中,你将首先学习如何获得一个付费的苹果开发者账号。接下来,你将了解证书、标识符、测试设备注册和配置文件。之后,你将学习如何创建 App Store 列表并提交你的应用至 App Store。最后,你将学习如何使用内部和外部测试员对你的应用进行测试。
在本章结束时,你将了解如何测试以及如何提交自己的应用至 App Store。
以下内容将涵盖:
-
获取苹果开发者账号
-
探索你的苹果开发者账号
-
将你的应用提交至 App Store
-
进行内部和外部测试
技术要求
完成本章你需要一个苹果账户和一个付费的苹果开发者账号。
本章没有项目文件,因为它应该作为提交应用的参考,而不是针对任何特定应用。
要查看 App Store 的最新更新,请访问developer.apple.com/app-store/whats-new/。
要查看 App Store Connect 的新功能,请观看此视频:developer.apple.com/videos/play/wwdc2024/10063/。
让我们从学习如何获取付费的苹果开发者账号开始,这是提交至 App Store 所必需的,将在下一节中介绍。
获取苹果开发者账号
正如你在前面的章节中看到的,你只需要一个免费的苹果账户就可以在设备上测试你的应用。然而,这些应用只能使用几天,你将无法添加高级功能,如使用苹果登录或上传你的应用到 App Store。为此,你需要一个付费的苹果开发者账号。按照以下步骤购买个人/个体经营苹果开发者账号:
-
访问
developer.apple.com/programs/并点击注册按钮。 -
点击开始注册。
-
当提示时,输入你的苹果账户和密码。
-
在信任此浏览器?屏幕上,只有当你是这个浏览器唯一的用户时才点击信任;否则,点击现在不。这是为了保护你的账户信息。
-
点击在网页上继续注册。
-
在确认你的个人信息屏幕上,输入你的个人信息并点击继续。
-
在选择你的实体类型屏幕上,选择个人/个体经营。点击继续。
-
在审查和接受屏幕上,检查页面底部的复选框并点击继续。
-
在完成购买屏幕上,点击购买。
-
按照屏幕上的说明完成购买。一旦你购买了你的账户,前往
developer.apple.com/account/并使用你购买开发者账户时使用的相同 Apple 账户登录。你应该会看到以下内容:

图 27.1:登录了付费 Apple 开发者账户的 Apple 开发者网站
现在你有了付费的 Apple 开发者账户,让我们在下一节中学习如何配置为你的应用程序所需的各项设置。
探索你的 Apple 开发者账户
你的 Apple 开发者账户拥有你开发应用程序和提交应用程序所需的一切。你可以查看你的会员状态,添加和组织你的开发团队成员,访问开发者文档,下载测试版软件等等。尽管所有这些功能都不在本书的范围之内,但本节将仅涵盖你需要做的以将你的应用程序上传到 App Store。
首先,你将获得你将在 Mac 上安装的 Apple 开发者证书。这些证书将用于对你的应用程序进行数字签名。接下来,你需要注册你的应用程序的 Apple 账户以及你将测试应用程序的设备。之后,你将能够生成配置文件,允许你的应用程序在测试设备上运行,并允许你提交应用程序到 App Store。
当你将你的 Apple 开发者账户的 Apple 账户和密码添加到Xcode | 设置 | 账户时,Xcode 可以自动为你处理这个过程。
让我们从学习证书签名请求(CSR)开始,这是在 Mac 上安装 Apple 开发者证书所必需的,这些证书将用于在 Mac 上安装,在下一节中。
生成证书签名请求
在你编写将要提交到 App Store 的应用程序之前,你需要在运行 Xcode 的 Mac 上安装一个开发者证书。证书标识应用程序的作者。要获取此证书,你需要创建一个CSR。以下是创建 CSR 的方法:
-
使用 Mac 上的 Spotlight 来查找钥匙串访问并启动它。
-
从钥匙串访问菜单中选择证书助手 | 从证书颁发机构请求证书...。

图 27.2:钥匙串访问应用程序
- 在用户电子邮件地址字段中,输入你用于注册 Apple 开发者账户的 Apple 账户的电子邮件地址。在常用名称字段中,输入你的名字。在请求是下选择保存到磁盘,然后点击继续:

图 27.3:证书助手屏幕
-
将 CSR 保存到你的硬盘上。
-
点击完成。
现在你已经有了 CSR,让我们看看你将如何使用它来获取开发证书(用于在您的设备上进行测试)和分发证书(用于 App Store 提交)在下一节中。
创建开发和分发证书
一旦你有了 CSR,你可以使用它来创建开发和分发证书。开发证书用于你想在测试设备上测试你的应用时,分发证书用于你想将应用上传到 App Store 时。
以下是创建开发和分发证书的方法:
- 登录到你的 Apple 开发者账户并点击证书:

图 27.4:登录了付费 Apple 开发者账户的 Apple 开发者网站
- 你将看到证书屏幕。点击+按钮:

图 27.5:显示+按钮的证书屏幕
- 点击Apple 开发单选按钮,并点击继续:

图 27.6:创建新证书屏幕显示 Apple Development 单选按钮
- 点击选择文件:

图 27.7:上传 CSR 屏幕
-
通过选择你之前保存在硬盘上的 CSR 文件,并点击选择来上传你的 CSR。
-
点击继续:

图 27.8:上传 CSR 屏幕,证书已上传
- 你的证书将自动生成。点击下载将生成的证书下载到你的 Mac 上:

图 27.9:下载证书屏幕
-
双击下载的证书以在 Mac 上安装它。
-
再次重复步骤 3-8,但这次在步骤 3中选择Apple 分发单选按钮:

图 27.10:创建新证书屏幕显示 Apple 分发单选按钮
太好了!你现在拥有了开发和分发证书。下一步是为你的应用注册App ID以在 App Store 中识别它。你将在下一节中学习如何操作。
注册 App ID
当你在第一章,探索 Xcode中创建项目时,你为它创建了一个包标识符(也称为 App ID)。App ID 用于在 App Store 中识别你的应用。在将应用上传到 App Store 之前,你需要在你的开发者账户中注册此 App ID。以下是注册 App ID 的方法:
-
登录到你的 Apple 开发者账户并点击标识符。
-
点击+按钮:

图 27.11:标识符屏幕
- 点击App IDs单选按钮并点击继续:

图 27.12:注册新标识符屏幕
- 点击App并点击继续:

图 27.13:标识符类型屏幕
- 为此 App ID 输入描述,例如
JRNL Packt Publishing App ID。勾选显式按钮,并在字段中输入你的应用包标识符。确保此值与你在创建项目时使用的包标识符相同。完成后,点击继续按钮:

图 27.14:描述和包标识符屏幕
一旦您的应用发布,您将无法再更改应用的包标识符。
- 点击注册:

图 27.15:注册界面
您的 App ID 现已注册。太酷了!在下一节中,您将注册您将在其上测试应用的所有设备。
注册您的设备
要在您的个人设备上测试您的应用,您需要在您的开发者账户中注册它们。以下是注册您的设备的步骤:
-
登录您的 Apple 开发者账户并点击设备。
-
点击+按钮:

图 27.16:设备注册界面
- 注册新设备界面出现:

图 27.17:注册新设备界面
您需要提供设备名称和设备 ID来注册您的设备。
- 将您的设备连接到您的 Mac。启动 Xcode 并从窗口菜单中选择设备和模拟器。在左侧窗格中选择设备并复制标识符值:

图 27.18:设备和模拟器窗口
- 在设备名称字段中为设备输入一个名称,并将标识符值粘贴到设备 ID (UDID)字段中。点击继续:

图 27.19:注册设备界面
您已成功注册测试设备。太好了!下一步是创建配置文件。需要一个iOS 应用开发配置文件,以便您的应用可以在测试设备上运行,并且需要一个iOS 应用商店分发配置文件,以便将应用上传到应用商店。您将在下一节中创建开发和分发配置文件。
创建配置文件
您需要创建两个配置文件。需要一个 iOS 应用开发配置文件,以便应用可以在测试设备上运行。需要一个 iOS 应用商店分发配置文件,用于提交您的应用到应用商店。以下是创建开发配置文件的步骤:
-
登录您的 Apple 开发者账户并点击配置文件。
-
点击+按钮:

图 27.20:配置文件界面
- 点击iOS 应用开发单选按钮并点击继续:

图 27.21:注册新配置文件界面
- 选择您要测试的应用的App ID并点击继续:

图 27.22:选择 App ID 界面
- 选择开发证书复选框并点击继续:

图 27.23:选择开发证书
- 选择您将在其上测试此应用的全部设备并点击继续:

图 27.24:选择设备
- 为配置文件输入一个名称并点击生成:

图 27.25:生成配置文件界面
-
点击下载按钮下载配置文件。
-
双击配置文件进行安装。
接下来,您将创建一个分发配置文件:
- 点击所有配置文件链接返回上一页:

图 27.26:所有配置文件链接
- 点击+按钮:

图 27.27:配置文件屏幕
- 点击App Store单选按钮并点击继续:

图 27.28:注册新的配置文件
- 选择您想要发布到 App Store 的应用的App ID,然后点击继续:

图 27.29:选择 App ID 屏幕
- 选择分发证书复选框并点击继续:

图 27.30:选择分发证书
- 为配置文件输入一个名称并点击生成:

图 27.31:生成分发配置文件
-
点击下载按钮下载配置文件。
-
双击配置文件以安装它。
您已经完成了提交应用到 App Store 之前所需的所有步骤。让我们在下一节中了解更多关于提交过程的细节,以ShareOrder应用为例。
将您的应用到 App Store 提交
您现在可以提交您的应用到 App Store 了!在本节中,我们将使用ShareOrder应用作为示例。让我们回顾一下到目前为止您所做的工作。您已经创建了开发和分发证书,注册了您的 App ID 和测试设备,并生成了开发和分发配置文件。
要在您的测试设备上测试您的应用,您将使用开发证书、App ID、注册的测试设备和开发配置文件。要提交您的应用到 App Store,您将使用分发证书、App ID 和分发配置文件。您将配置 Xcode 以自动为您管理这些操作。
在您提交应用之前,您必须创建您应用的图标并获取您应用的截图。然后,您可以创建 App Store 列表,生成要上传的存档构建,并完成 App Store Connect 信息。苹果公司将对您的应用进行审查,如果一切顺利,它将出现在 App Store 上。
要了解更多关于如何提交您的应用的信息,请访问developer.apple.com/app-store/submitting/。
在下一节中,我们将看看如何为您的应用创建图标,这些图标将在应用安装到设备屏幕上时显示。
为您的应用创建图标
在您上传您的应用到 App Store 之前,您必须为它创建一个图标集。以下是创建应用图标集的步骤:
-
创建一个 1,024 x 1,024 像素的应用图标。
-
在项目导航器中点击资产文件,并将您创建的图标拖到以下截图所示的空白区域:

图 27.32:资产文件显示应用图标的空白区域
当您在模拟器或设备上运行您的应用并退出应用时,您应该能够在主屏幕上看到应用的图标。真不错!
让我们看看如何创建截图。你需要它们来提交 App Store,这样顾客就能看到你的应用外观。你将在下一节中完成这项操作。
创建你应用的截图
你需要你应用的截图,这些截图将用于你的 App Store 列表。要创建它们,请在模拟器中运行你的应用并点击截图按钮。它将被保存在桌面:

图 27.33:显示截图按钮的模拟器
苹果要求你提交在 6.5 英寸显示屏的 iPhone(例如 iPhone 16 Pro)和 5.5 英寸显示屏的 iPhone(例如 iPhone 8 Plus)上运行你应用的截图。可选地,你也可以提交在 6.7 英寸显示屏的 iPhone(例如 iPhone 16 Pro Max)、12.9 英寸显示屏的 iPad Pro(第 6 代)和 12.9 英寸显示屏的 iPad Pro(第 2 代)上运行你应用的截图。如果你没有实际设备,可以使用模拟器来模拟所有这些设备,并且你的截图应该显示你应用的功能以及你的应用在不同屏幕尺寸下的外观。
在下一节中,我们将更详细地讨论如何提交应用截图。App Store 列表包含了将在 App Store 中显示的所有关于你应用的信息,这样顾客就可以在下载或购买你的应用之前做出明智的决定。
创建 App Store 列表
现在你已经有了你应用的图标和截图,接下来你需要创建 App Store 的列表。这可以让顾客在下载之前看到你应用的信息。请按照以下步骤操作:
- 前往
appstoreconnect.apple.com并选择我的应用:

图 27.34:App Store Connect 网站
- 点击屏幕左上角的+按钮并选择新建应用:

图 27.35:新建应用按钮和菜单
- 将会显示一个包含字段列表的新建应用屏幕:

图 27.36:应用详情屏幕
-
输入你的应用详细信息:
-
平台:你应用支持的所有平台(iOS、macOS 和/或 tvOS)。
-
名称:你应用的名称。
-
主要语言:你应用使用的语言。
-
包标识符:你之前创建的包标识符。
-
SKU:任何你用来引用你应用的参考数字或字符串。例如,你可以使用类似
231020-1的东西,这将是你应用版本和完成日期的参考。它可以是你认为有意义的任何数字。 -
用户访问:这管理着你的开发者账户团队中谁可以在 App Store Connect 中看到这个应用。如果你是团队中唯一的一个人,只需将其设置为完全访问。
-
-
完成后,请点击创建。
应用现在将列在你的账户中,但你仍然需要上传应用及其所有相关信息。要上传应用,你需要创建一个存档构建,你将在下一节中学习如何做。
创建存档构建
您将创建一个存档构建,该构建将被提交给 Apple,以便在 App Store 上发布。这还将用于您的内部和外部测试。以下是创建存档构建的步骤:
- 打开 Xcode,在项目导航器中选择项目名称,并选择通用选项卡。在标识部分,您可以按需更改版本和构建号。例如,如果这是您应用的第一个版本,并且是您第一次构建它,可以将版本设置为
1.0,将构建设置为1:

图 27.37:显示通用选项卡的编辑区域
- 选择签名与能力选项卡。确保已勾选自动管理签名。这将允许 Xcode 创建证书、App ID 和配置文件,以及注册连接到您的 Mac 的设备。从团队下拉菜单中选择您的付费开发者账户:

图 27.38:显示签名与能力选项卡的编辑区域
- 选择任何 iOS 设备作为构建目标:

图 27.39:选择 Any iOS Device 的方案菜单
- 如果您的应用未使用加密,请通过添加
ITSAppUsesNonExemptEncryption来更新Info.plist文件,将其类型设置为Boolean,并将值设置为NO。它应该随后出现在项目导航器中:

图 27.40:选择 Info.plist 的项目导航器
更多详细信息,请使用此链接:developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption。
- 从产品菜单中选择存档:

图 27.41:产品菜单中选择存档
- 组织者窗口出现,并选中存档选项卡。您的应用将显示在此屏幕上。选择它并点击分发应用按钮:

图 27.42:组织者窗口中选择分发应用按钮
- 选择App Store Connect并点击分发:

图 27.43:选择分发方法
-
等待上传完成。如果您被提示输入密码,请输入您的 Mac 账户密码并点击始终允许。
-
当您的上传完成时,点击完成:

图 27.44:应用上传完成屏幕
到此为止,将通过 App Store 分发的应用构建已上传。在下一节中,您将了解如何上传截图并完成将在 App Store 上与应用一起显示的应用信息。
在 App Store Connect 中完成信息
您的应用已上传,但您仍需要在 App Store Connect 中完成有关您应用的详细信息。以下是步骤:
-
前往
appstoreconnect.apple.com并选择我的应用。 -
选择您刚刚创建的应用:

图 27.45:选择您的应用的应用屏幕
- 在屏幕左侧选择应用信息,并确保所有信息都是正确的:

图 27.46:应用信息屏幕
- 对定价和可用性以及应用隐私部分也进行相同的操作:

图 27.47:应用隐私屏幕
- 在屏幕左侧选择准备提交。在应用预览和截图部分,拖入您之前拍摄的截图:

图 27.48:准备提交屏幕显示应用预览和截图部分
- 滚动并填写推广文本、描述、关键词、支持 URL(包含您应用的支持信息)和营销 URL(包含您应用的营销信息)字段:

图 27.49:版本信息部分
- 滚动到通用应用信息部分并填写所有必需的详细信息:

图 27.50:通用应用信息部分
- 滚动到构建部分,您将看到您之前上传的存档构建。如果您看不到它,点击+或添加构建按钮,选择一个构建,然后点击完成:

图 27.51:构建选择屏幕
苹果处理您的提交可能需要长达 30 分钟。
- 验证您的构建是否在构建部分:

图 27.52:构建部分
- 滚动到应用审核信息部分。如果您想向应用审核员提供任何额外信息,请将其放在此处:

图 27.53:应用审核信息部分
- 滚动到版本发布部分并保持默认设置,这样您的应用在苹果批准后就会自动发布:

图 27.54:版本发布部分
- 滚动到屏幕顶部并点击添加审核按钮:

图 27.55:添加审核按钮
- 验证应用状态是否已更改为等待审核:

图 27.56:显示“等待审核”的应用状态
您需要等待苹果审核应用,您将收到一封电子邮件,告知您的应用是否被批准或拒绝。如果您的应用被拒绝,将有一个链接带您到苹果的解决方案中心页面,该页面描述了您的应用为何被拒绝。修复问题后,您可以更新存档并重新提交。
您现在知道如何将应用提交到 App Store!太棒了!
在下一节中,您将学习如何对您的应用进行内部和外部测试,这对于确保应用高质量且无错误至关重要。
测试您的应用
苹果公司有一个名为TestFlight的功能,允许您在将应用发布到 App Store 之前将其分发给测试者。您需要下载 TestFlight 应用,该应用可在developer.apple.com/testflight/获取,以测试您的应用。您的测试者可以是您内部团队(内部测试者)或公众(外部测试者)。首先,让我们看看如何在下一节中允许内部团队成员测试您的应用。
内部测试您的应用
内部测试应在应用开发的早期阶段进行。它仅涉及您的内部团队成员;苹果不会为内部测试者审查应用。您可以为内部测试发送最多 100 个测试者的构建。为此,请按照以下步骤操作:
-
前往
appstoreconnect.apple.com并选择我的应用。 -
选择您想要测试的应用。
-
点击TestFlight标签页:

图 27.57:TestFlight 标签页
- 点击INTERNAL TESTING旁边的+按钮以创建一个新的内部测试组:

图 27.58:显示+按钮的 TestFlight 屏幕
- 创建新内部组对话框将出现。为您的内部测试组命名并点击创建:

图 27.59:创建新内部组对话框
- 在您的测试组创建完成后,点击+按钮将用户添加到您的组中:

图 27.60:显示+按钮的测试组屏幕
- 选择您想要发送测试构建的所有用户并点击添加。他们将受邀测试所有可用的构建:

图 27.61:添加测试者屏幕
- 验证测试者是否已添加:

图 27.62:显示测试者部分的 TestFlight 屏幕
请记住,内部测试仅涉及您的团队成员。如果您想要与超过 100 个测试者进行测试,您将需要进行外部测试,这将在下一节中描述。
外部测试您的应用
外部测试应在应用开发的最后阶段进行。您可以选择任何人作为外部测试者,并且您可以发送最多 10,000 个测试者的构建。苹果可能会为外部测试者审查应用。以下是步骤:
-
前往
appstoreconnect.apple.com并选择我的应用。 -
选择您想要测试的应用。
-
点击TestFlight标签页。
-
点击EXTERNAL TESTING旁边的+按钮:

图 27.63:显示+按钮的 TestFlight 屏幕
- 输入测试组的名称并点击创建:

图 27.64:创建新组屏幕
- 点击添加构建链接以选择您想要测试人员测试的构建:

图 27.65:外部测试组显示添加构建链接
- 选择您的一个构建并点击下一步:

图 27.66:选择要测试的构建屏幕
- 如果测试人员有问题,他们需要知道联系谁。输入您的联系信息并点击下一步:

图 27.67:测试信息屏幕
- 在提供的框中输入您希望测试人员测试的内容并点击提交审查:

图 27.68:要测试的内容屏幕
苹果可能会在将测试构建提供给测试人员之前对其进行审查。如果您的应用程序被拒绝,您需要修复问题并重新提交。
- 现在,您将向您的组添加外部测试人员。点击测试人员标签:

图 27.69:我的外部测试组屏幕显示测试人员链接
- 点击测试人员旁边的+按钮:

图 27.70:外部测试组显示添加新测试人员的+按钮
- 在邀请测试人员屏幕上,选择电子邮件并点击下一步:

图 27.71:邀请测试人员屏幕
- 输入您的测试人员的姓名和电子邮件地址。请注意,当构建准备好测试时,苹果会自动通知他们:

图 27.72:将新测试人员添加到组的屏幕
太好了!您现在知道如何内部和外部测试您的应用程序,并且您已经完成了这本书的最后一章!
摘要
您现在已经完成了构建应用程序并将其提交到 App Store 的整个过程。恭喜!
您首先学习了如何获取苹果开发者账户。接下来,您学习了如何生成 CSR 以创建证书,这些证书允许您在自己的设备上测试应用程序并在 App Store 上发布它们。您学习了如何创建一个捆绑标识符以在 App Store 上唯一标识您的应用程序并注册您的测试设备。之后,您学习了如何创建开发和生产配置文件,以便应用程序可以在您的测试设备上运行并上传到 App Store。接下来,您学习了如何创建 App Store 列表并将您的发布构建提交到 App Store。最后,您学习了如何使用内部和外部测试人员对您的应用程序进行测试。
您现在知道如何构建自己的应用程序,对其内部和外部进行测试,并将它们提交到 App Store。
一旦应用程序提交审查,您所能做的就是等待苹果审查您的应用程序。如果应用程序被拒绝,请不要担心——这发生在所有开发者身上。与苹果合作通过解决中心解决问题,并做研究以了解什么可以接受什么不可以接受苹果。
您的应用程序在 App Store 上之后,请随时通过 Twitter 联系我(@shah_apple),让我知道——我很乐意看看您构建了什么。
留下评论!
感谢您从 Packt Publishing 购买此书——我们希望您喜欢它!您的反馈对我们来说是无价的,它帮助我们改进和成长。阅读完毕后,请花一点时间在亚马逊上留下评论;这只需一分钟,但对像您这样的读者来说意义重大。
![B31371_QR.png]
[(https://packt.link/r/1836204892)]
扫描下面的二维码或访问链接,免费获得您选择的电子书。


[(http://packt.com)]
订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及领先的行业工具,帮助您规划个人发展并推进职业生涯。更多信息,请访问我们的网站。
为什么订阅?
-
使用来自 4,000 多名行业专业人士的实用电子书和视频,节省学习时间,增加编码时间
-
通过为您量身定制的技能计划提高您的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
在www.packt.com网站上,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
您可能还会喜欢的其他书籍
如果您喜欢这本书,您可能还会对 Packt 出版的以下其他书籍感兴趣:
[(https://www.packtpub.com/en-in/product/designing-and-prototyping-interfaces-with-figma-9781835465042)]
使用 Figma 设计和原型化界面
Fabio Staiano
ISBN: 9781835464601
-
创建满足用户需求的高质量设计,提供卓越的体验
-
掌握移动优先设计和响应式设计概念
-
将 AI 功能集成到您的设计工作流程中,以提高生产力和探索设计创新
-
使用条件原型和变量制作沉浸式原型
-
有效沟通技术和非技术受众
-
为复杂的设计挑战开发创意解决方案
-
通过交互式原型收集和应用用户反馈
[(https://www.packtpub.com/en-in/product/swiftui-cookbook-9781805129844)]
SwiftUI 食谱
Juan C. Catalan
ISBN: 9781805121732
-
使用 SwiftUI 5 为 iOS 17、macOS 14 和 watchOS 10 创建令人惊叹、用户友好的应用
-
使用 Xcode 15 的高级预览功能
-
使用 async/await 编写并发和响应式代码
-
使用 Swift Charts 创建强大的数据可视化
-
使用现代动画和过渡增强用户参与度
-
使用 Firebase 和 Sign in with Apple 实现用户身份验证
-
了解高级主题,如自定义修饰符、动画和状态管理
-
使用 SwiftUI 构建多平台应用
(https://www.packtpub.com/en-in/product/101-ux-principles-2nd-edition-9781803230511)
《101 UX 原则 - 第二版》
威尔·格兰特
ISBN: 9781803234885
-
与用户期望合作,而不是对抗
-
使交互元素明显且易于发现
-
优化你的界面以适应移动设备
-
简化创建和输入密码的过程
-
在用户界面中谨慎使用动画
-
如何处理具有破坏性的用户操作
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球技术社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。



浙公网安备 33010602011771号