IOS15-编程初学者指南-全-

IOS15 编程初学者指南(全)

原文:zh.annas-archive.org/md5/2ce6c0f21fd98e6b54812c545d80cdfa

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎来到 iOS 15 编程入门。本书是 iOS 编程入门系列的第六版,已全面更新以支持 iOS 15、macOS 12.0 Monterey 和 Xcode 13。

在本书中,您将构建一个名为Let's Eat的餐厅预订应用程序。您将从探索 Xcode 开始,这是苹果的编程环境,也称为其集成开发环境IDE)。接下来,您将开始学习 Swift 的基础知识,这是 iOS 应用程序中使用的编程语言,并了解它是如何用于完成常见编程任务的。

在您掌握了 Swift 的坚实基础之后,您将开始创建Let's Eat应用程序的用户界面。在这个过程中,您将使用故事板,并通过 segues 连接您的应用程序场景。

在完成用户界面后,您将添加代码以实现应用程序的功能。为了在网格中显示您的数据,您将使用集合视图,而在列表中显示数据时,您将使用表格视图。您还将了解如何在地图上添加基本和自定义注释。您将看到如何使用 JSON 文件将实际餐厅数据导入到您的集合视图、表格视图和地图中。您将允许用户为特定餐厅添加评分、评论和照片,您将使用 Core Data 保存这些信息。

您现在有一个完整的应用程序,但如何添加最新的 iOS 15 功能呢?您将从修改您的应用程序以在 iPhone 和 iPad 上运行开始,并使其在 Mac 上也能运行。接下来,您将学习如何使用 SwiftUI 开发应用程序,这是一种为所有 Apple 平台开发应用程序的全新方法。之后,您将使用 Swift Concurrency 实现异步和并行编程,并使用 SharePlay 为您的应用程序实现共享用户体验。

最后,您将学习如何使用内部和外部测试人员测试您的应用程序,并将其提交到 App Store。

本书面向对象

如果您是一位对 iOS 和 Swift 编程语言完全陌生的经验丰富的开发者,那么这本书是为您准备的。然而,如果您是一位希望探索最新 iOS 15 功能的 iOS 开发者,您也会发现这本书很有用。

本书涵盖内容

第一章**,熟悉 Xcode,带您游览 Xcode,并讨论您在整个书中将使用的所有不同面板。

第二章**,简单值和类型,讨论 Swift 语言如何实现值和类型。

第三章**,条件语句和可选类型,展示了如何实现ifswitch语句,以及如何实现可能或可能没有值的变量。

第四章**,范围运算符和循环,展示了如何在 Swift 中处理范围以及循环的不同实现方式。

第五章**,集合类型,涵盖了常见的集合类型,包括数组、字典和集合。

第六章**,函数和闭包,介绍了如何使用函数和闭包将指令组合在一起。

第七章**,类、结构和枚举,讨论了在 Swift 中如何表示包含状态和行为的复杂对象。

第八章**,协议、扩展和错误处理,讨论了创建复杂数据类型可以采用的协议,扩展现有类型的功能,以及如何在代码中处理错误。

第九章**,设置用户界面,涉及创建 Let's Eat 应用,添加图形资源,并设置用户将看到的初始屏幕。

第十章**,构建用户界面,介绍了为 Let's Eat 应用设置主屏幕。

第十一章**,完成用户界面,涵盖了为 Let's Eat 应用设置剩余屏幕。

第十二章**,修改和配置单元格,是关于在故事板中自定义表格和集合视图单元格。

第十三章**,开始使用 MVC 和集合视图,关注于使用集合视图以及如何使用它们来显示项目网格。

第十四章**,将数据引入集合视图,关注于将数据合并到集合视图中。

第十五章**,开始使用表格视图,教你如何使用表格视图,并深入探讨动态表格视图。

第十六章**,开始使用 MapKit,涉及使用 MapKit 向地图添加标注,你还将为你的地图创建自定义标注。

第十七章**,开始使用 JSON 文件,涉及学习如何使用数据管理器读取 JSON 文件并在你的应用中使用其中的数据。

第十八章**,在静态表格视图中显示数据,教你如何使用 segues 从一个视图控制器传递数据到另一个视图控制器以填充静态表格视图。

第十九章**,开始使用自定义 UIControls,探讨了如何创建你自己的自定义视图。

第二十章**,开始使用相机和照片库,讲述了如何使用设备的相机和照片库。

第二十一章**,理解 Core Data,涵盖了使用 Core Data 的基础知识以及如何保存评论和餐厅照片。

第二十二章**,开始使用 Mac Catalyst,处理修改您的应用以在 iPad 的大屏幕上运行良好,并使其在 Mac 上运行。

第二十三章**,开始使用 SwiftUI,介绍了如何使用苹果公司的新 SwiftUI 技术来构建应用。

第二十四章**,开始使用 Swift Concurrency,介绍了并行和异步编程的概念,并展示了您如何在您的应用中实现它。

第二十五章**,开始使用 SharePlay,展示了您如何通过使用 SharePlay 和 Group Activities 框架来实现用户的共享体验。

第二十六章**,测试并将您的应用提交到 App Store,关注了如何测试并将您的应用提交到 App Store。

要充分利用这本书

本书已完全修订以适应 iOS 15、macOS 12.0 Monterey、Xcode 13 和 Swift 5.5。本书的第四部分还涵盖了苹果在 2021 年 WWDC 上推出的最新技术,包括 Mac Catalyst、SwiftUI、Swift 并发和 SharePlay。

要完成本书中的所有练习,您将需要:

  • 运行 macOS 11 Big Sur 或 macOS 12.0 Monterey 的 Mac 电脑

  • Xcode 13.0 或更高版本

要检查您的 Mac 是否支持 macOS 12.0 Big Sur,请参阅此链接:www.apple.com/macos/monterey/。如果您的 Mac 支持,您可以通过系统偏好设置中的软件更新来更新 macOS。

要获取最新版本的 Xcode,您可以从 Mac App Store 下载。大多数练习可以在没有 Apple 开发者账户的情况下完成,并使用 iOS 模拟器。如果您希望在实际的 iOS 设备上测试您正在开发的应用,您将需要一个免费或付费的 Apple 开发者账户,以下章节需要付费的 Apple 开发者账户:

第二十五章**,开始使用 SharePlay

第二十六章**,测试并将您的应用提交到 App Store

第二十六章**,测试并将您的应用提交到 App Store中,有关于如何测试并将您的应用提交到 App Store 的说明。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

代码实战

访问以下链接查看代码运行的视频:

bit.ly/3kdYBGc

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:bit.ly/3kdYBGc

static.packt-cdn.com/downloads/9781801811248_ColorImages.pdf.

使用的约定

本书使用了多种文本约定。

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 项目:”

小贴士或重要提示

看起来是这样的。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 mailto:customercare@packtpub.com 与我们联系。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/support/errata,选择您的书,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 mailto:copyright@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《iOS 15 编程入门 第六版》,我们非常期待听到您的想法!请点击此处直接访问亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都非常重要,并将帮助我们确保我们提供高质量的内容。

Discord 研讨会布局修订

第一部分:Swift

欢迎来到本书的第一部分。在本部分中,你将首先探索 Xcode,苹果的编程环境,也被称为集成开发环境IDE)。之后,你将开始学习 Swift 5 的基础知识,这是 iOS 应用程序中使用的编程语言,并了解它是如何用于完成常见编程任务的。

本部分包括以下章节:

  • 第一章**,熟悉 Xcode

  • 第二章**,简单值和类型

  • 第三章**,条件和可选

  • 第四章**,范围运算符和循环

  • 第五章**,集合类型

  • 第六章**,函数和闭包

  • 第七章**,类、结构和枚举

  • 第八章**,协议、扩展和错误处理

在本部分结束时,你将了解创建应用程序并在模拟器或设备上运行应用程序的过程,并且你将具备使用 Swift 编程语言完成常见编程任务的工作知识。这将为你学习下一部分做好准备,并使你能够创建自己的 Swift 程序。让我们开始吧!

第一章:第一章:熟悉 Xcode

欢迎来到iOS 15 编程入门。希望您会发现这是一本关于在 App Store 上创建和发布 iOS 15 应用的实用介绍。

在本章中,您将学习如何在您的 Mac 上下载和安装Xcode。您将熟悉 Xcode 用户界面的不同部分,并创建您的第一个iOS 应用,然后在iOS 模拟器中运行它。然后,您将学习如何通过 USB 将iOS 设备连接到 Xcode,以便在设备上运行应用,如何将Apple ID添加到 Xcode,以便在您的设备上创建和安装必要的数字证书,以及如何信任设备上的证书。最后,您将学习如何通过 Wi-Fi 连接到您的设备,这样您就不再需要在每次运行应用时插拔设备。

到本章结束时,您将知道如何在 iOS 模拟器或 iOS 设备上创建和运行应用,这是您在构建自己的应用时需要做的。

以下内容将涵盖:

  • 从 App Store 下载和安装 Xcode

  • 理解 Xcode 用户界面

  • 在 iOS 模拟器中运行应用

  • 使用 iOS 设备进行开发

技术要求

要完成本章的练习,您需要以下内容:

  • 运行 macOS 11 Big Sur 或 macOS 12 Monterey 的 Apple Mac 计算机

  • Apple ID(如果您没有,您将在本章中创建一个)

  • 可选,运行 iOS 15 的 iOS 设备

本章的 Xcode 项目位于本书代码包的Chapter01文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,了解代码的实际效果:

bit.ly/3wqcqpG

您将从下一节开始下载 Xcode,这是 Apple 的集成开发环境(IDE),用于从 App Store 开发 iOS 应用。

小贴士

下载的大小非常大(在撰写本文时为 11.7 GB),因此可能需要一段时间才能下载。在下载之前,请确保您有足够的磁盘空间。

从 App Store 下载和安装 Xcode

在您开始编写 iOS 应用之前,您需要从 App Store 下载和安装 Xcode。为此,请按照以下步骤操作:

  1. Apple菜单中选择App Store

  2. 在右上角的搜索框中,输入Xcode并按回车键。

  3. 您应该在搜索结果中看到Xcode。点击获取并点击安装

  4. 如果您有 Apple ID,请在Apple ID文本框中输入它。如果您没有,请点击创建 Apple ID按钮,并按照逐步说明创建一个:![图 1.1:Apple ID 创建对话框 图 1.01:示例图片

    图 1.1:Apple ID 创建对话框

    重要信息

    您可以通过此链接了解如何创建 Apple ID 的更多信息:support.apple.com/en-us/HT204316#appstore

  5. Xcode 安装完成后,启动它。您应该会看到以下欢迎使用 Xcode屏幕。在左侧面板中点击创建新的 Xcode 项目图 1.2:欢迎使用 Xcode 屏幕

    图 1.2:欢迎使用 Xcode 屏幕

  6. 您将看到以下新项目屏幕。在选择您新项目的模板部分,选择iOS。然后选择App并点击下一步图 1.3:选择新项目模板屏幕

    图 1.3:选择新项目模板屏幕

  7. 您将在文本字段中看到ExploringXcode

  8. com.yourname。这被称为反向域名命名格式,通常在这里使用。

  9. 界面: 创建您应用用户界面的方法。将其设置为Storyboard

  10. 语言: 要使用的编程语言。将其设置为Swift

    将其他设置保留为默认值,并确保所有复选框都没有勾选。完成后点击下一步

  11. 您将看到一个保存对话框。选择一个位置来保存您的项目,例如桌面文档文件夹,然后点击创建图 1.5:保存对话框

    图 1.5:保存对话框

  12. 如果您看到一个对话框说版本控制系统未提供作者信息,请点击修复

    重要信息

    您看到此对话框的原因是源代码控制复选框已被勾选。Apple 建议启用源代码控制。源代码控制超出了本书的范围,但如果您想了解更多关于版本控制和 Git 的信息,请参阅此链接:git-scm.com/video/what-is-version-control

  13. 您将看到以下源代码控制首选项屏幕:

图 1.6:源代码控制首选项屏幕

图 1.6:源代码控制首选项屏幕

输入以下信息:

  • 作者姓名:– 您的名字

  • 作者电子邮件:– 您的电子邮件地址

完成后,通过点击左上角的红色关闭按钮关闭源代码控制首选项屏幕。Xcode 主窗口将出现。

太棒了!您现在已成功下载并安装了 Xcode,并创建了您的第一个项目。在下一节中,您将了解 Xcode 用户界面。

理解 Xcode 用户界面

您已经创建了您的第一个 Xcode 项目!如您所见,Xcode 用户界面被分为几个不同的部分,如下所示:

图 1.7:Xcode 用户界面

图 1.7:Xcode 用户界面

让我们更详细地看看每一部分。以下描述对应于前面截图中的数字:

  • 工具栏(1) – 用于构建和运行你的应用,并查看运行任务的进度。

  • 导航区域(2) – 提供快速访问项目各个部分的途径。默认情况下显示项目导航器

  • 编辑区域(3) – 允许你编辑源代码、用户界面和其他资源。

  • 检查器区域(4) – 允许你查看和编辑在导航区域或编辑区域中选定的项目的信息。

  • 调试区域(5) – 包含调试栏变量视图控制台。通过按Shift + Command + Y来切换调试区域。

接下来,让我们更仔细地检查工具栏。以下是工具栏左侧的显示:

图 1.8:Xcode 工具栏(左侧)

图 1.8:Xcode 工具栏(左侧)

让我们更详细地看看每一部分。以下描述对应于前面截图中的数字:

  • 导航器按钮(1) – 切换导航区域的开启和关闭。

  • 停止按钮(2) – 只有在应用正在运行时才会出现在播放按钮旁边。停止当前运行的应用。

  • 播放按钮(3) – 用于构建和运行你的应用。

  • 方案菜单(4) – 显示构建项目的特定方案(探索 Xcode)和运行应用的设备目标(iPhone SE (第 2 代)。

    方案和目标不同。方案指定了构建和运行项目的设置。目标指定了应用的安装位置,存在于物理设备和模拟器中。

  • 活动视图(5) - 显示运行任务的进度。

以下是工具栏右侧的显示:

图 1.9:Xcode 工具栏(右侧)

图 1.9:Xcode 工具栏(右侧)

让我们更详细地看看每一部分。以下描述对应于前面截图中的数字:

  • 库按钮(1) – 显示用户界面元素、代码片段和其他资源。

  • 检查器按钮(2) – 切换检查器区域的开启和关闭。

不要被所有不同的部分吓倒,因为你在后面的章节中会详细了解它们。现在,你已经熟悉了 Xcode 界面,你将在 iOS 模拟器中运行你刚刚创建的应用,它将显示你的 iOS 设备的表示。

在 iOS 模拟器中运行应用

当你安装 Xcode 时,iOS 模拟器会被安装。它提供了一个模拟的 iOS 设备,这样你就可以看到你的应用看起来如何以及它的行为,而无需物理 iOS 设备。它可以模拟 iPad 和 iPhone 的所有屏幕尺寸和分辨率,这样你可以轻松地在多个设备上测试你的应用。

要在模拟器中运行你的应用,请按照以下步骤操作:

  1. 点击工具栏中的方案菜单,你会看到一个模拟器的列表。从该菜单中选择iPhone SE (第 2 代)图 1.10:选择 iPhone SE (第 2 代) 的 Xcode 方案菜单

    图 1.10:Xcode 方案菜单,选择 iPhone SE(第 2 代)

  2. 点击播放按钮来安装并运行当前所选模拟器上的应用程序。你也可以使用Command + R键盘快捷键。

  3. 如果你看到 Mac 管理员账户的用户名密码,并点击继续

  4. 模拟器将启动并显示 iPhone SE(第 2 代)的表示。你的应用程序显示一个白色屏幕,因为你还没有向你的项目添加任何内容:![图 1.11:iOS 模拟器]

    ](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_1.11_B17469.jpg)

    图 1.11:iOS 模拟器

  5. 切换回 Xcode 并点击停止按钮(或按Command + .)来停止当前运行的项目。

你刚刚在模拟器中创建并运行了你的第一个 iOS 应用程序!做得好!

如果你查看方案菜单,可能会好奇无设备构建部分是用于什么的。让我们在下一节中看看它们。

重要信息

如果你使用的是 M1 Mac,你将在方案菜单中看到我的 Mac(专为 iPad 设计)而不是无设备

理解无设备和构建部分

在上一节中,你学习了如何在方案菜单中选择模拟器来运行你的应用程序。除了模拟器列表之外,方案菜单还有无设备构建部分。这些允许你在真实 iOS 设备上运行应用程序,并为提交到 App Store 准备应用程序。

点击工具栏中的方案菜单,可以看到菜单顶部的无设备构建部分:

![图 1.12:Xcode 方案菜单,选择任何 iOS 设备(arm64)]

](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_1.12_B17469.jpg)

图 1.12:Xcode 方案菜单,选择任何 iOS 设备(arm64)

目前,无设备部分显示的文本是无设备连接到“我的 Mac”...,因为你目前没有任何 iOS 设备连接到你的电脑。如果你插入一个 iOS 设备,它将出现在这个部分,你将能够运行你为其开发的测试应用程序。在真实设备上运行你的应用程序是推荐的,因为模拟器不会准确反映真实 iOS 设备的性能特征,并且没有一些真实设备拥有的硬件功能和软件 API。

构建部分只有一个菜单项,任何 iOS 设备(arm64)。当你需要提交到 App Store 之前存档你的应用程序时使用它。你将在第二十六章**,测试并将你的应用程序提交到 App Store中学习如何这样做。

现在让我们看看如何在真实 iOS 设备上构建和运行你的应用程序。尽管这本书中的大多数说明不需要你拥有 iOS 设备,所以如果你没有,请跳过下一节,直接进入第二章**,简单值和类型

使用 iOS 设备进行开发

虽然您将能够使用此书中的大多数练习,但建议在实际的 iOS 设备上构建和测试您的应用程序,因为模拟器无法模拟某些硬件组件和软件 API。

重要信息

要全面了解模拟器和实际设备之间的所有差异,请参阅此链接:help.apple.com/simulator/mac/current/#/devb0244142d

除了您的设备外,您还需要一个 Apple ID 或付费的 Apple 开发者账户来在您的设备上构建和运行您的应用程序。目前,您将使用用于从 App Store 下载 Xcode 的相同的 Apple ID。请按照以下步骤操作:

  1. 使用随您的 iOS 设备一起提供的电缆将您的设备连接到您的 Mac,并确保 iOS 设备已解锁。

    小贴士

    您可以通过在 Xcode 菜单栏中选择 窗口 | 设备和模拟器 来查看已连接的设备。

  2. 方案 菜单中,选择您的设备(在本例中为 iPhone)作为运行目标:![图 1.13:已选择实际 iOS 设备的 Xcode 方案菜单 图片

    图 1.13:已选择实际 iOS 设备的 Xcode 方案菜单

  3. 等待 Xcode 完成索引和处理,这可能需要一段时间。一旦完成,就绪将在状态窗口中显示。

  4. 通过单击 播放 按钮运行项目(或使用 Command + R)。您将得到以下错误:“Exploring Xcode”的签名需要开发团队:![图 1.14:Xcode 签名与能力窗格 图片

    图 1.14:Xcode 签名与能力窗格

    这是因为在 iOS 设备上运行应用程序需要数字证书,您需要将 Apple ID 或付费的 Apple 开发者账户添加到 Xcode 中,以便生成数字证书。

    重要信息

    使用 Apple ID,您可以在 iOS 设备上测试您的应用程序,但您需要一个付费的 Apple 开发者账户来在 App Store 上分发应用程序。您将在第二十六章**,测试并将您的应用程序提交到 App Store中了解更多信息。

    重要信息

    证书确保只有您授权的应用程序才能在您的设备上运行。这有助于防止恶意软件。您也可以通过此链接了解更多信息:help.apple.com/xcode/mac/current/#/dev60b6fbbc7

  5. 单击 添加账户... 按钮:![图 1.15:Xcode 签名与能力窗格,已选择“添加账户...”按钮 图片

    图 1.15:Xcode 签名与能力窗格,已选择“添加账户...”按钮

  6. Xcode 首选项 窗口出现,并已选择 账户 窗格。输入您的 Apple ID 并单击 下一步:![图 1.16:Apple ID 创建对话框 图片

    图 1.16:Apple ID 登录对话框

    小贴士

    您也可以通过在 Xcode 菜单中选择 首选项 来访问 Xcode 首选项。

  7. 当提示时,输入您的密码。几分钟后,账户 面板将显示您的账户设置:![图 1.17:Xcode 首选项中的账户面板

    ![图 1.17_B17469.jpg]

    图 1.17:Xcode 预设中的账户面板

  8. 完成后,通过点击左上角的红色关闭按钮关闭 首选项 窗口。

  9. 在 Xcode 的编辑器区域中,点击 签名与能力。确保 自动管理签名 已勾选,并且从 团队 下拉菜单中选择 个人团队:![图 1.18:设置账户的 Xcode 签名与能力面板

    ![图 1.18_B17469.jpg]

    图 1.18:设置 Xcode 预设中的账户面板

  10. 如果您在此屏幕上仍然看到错误,请尝试更改您的 com.myname4352.ExploringXcode

  11. 当您构建和运行时,一切应该都能正常工作,并且您的应用将安装到您的 iOS 设备上。然而,它将无法启动,您将看到以下消息:

![图 1.19:无法启动 "ExploringXcode" 对话框

![图 1.19_B17469.jpg]

图 1.19:无法启动 "ExploringXcode" 对话框

这意味着您需要信任已安装到您设备上的证书。您将在下一节中学习如何进行此操作。

在您的 iOS 设备上信任开发者应用证书

开发者应用证书 是一个特殊文件,它将与您的应用一起安装到您的 iOS 设备上。在您的应用可以运行之前,您需要信任它。请按照以下步骤操作:

  1. 在您的 iOS 设备上,点击 设置 | 通用 | VPN 与设备管理:![图 1.20:iOS 设置中的设备管理设置

    ![图 1.20_B17469.jpg]

    图 1.20:iOS 设置中的设备管理设置

  2. 点击 Apple 开发者:![图 1.21:设备管理设置中的 Apple 开发者部分

    ![图 1.21_B17469.jpg]

    图 1.21:设备管理设置中的 Apple 开发者部分

  3. 点击 信任 "Apple Development: ":![图 1.22:信任按钮

    ![图 1.22_B17469.jpg]

    图 1.22:信任按钮

  4. 点击 信任:![图 1.23:信任对话框

    ![图 1.23_B17469.jpg]

    图 1.23:信任对话框

  5. 您应该看到以下文本,这表明应用现在已被信任:![图 1.24:带有信任证书的 Apple 开发者部分

    ![图 1.24_B17469.jpg]

    图 1.24:带有信任证书的 Apple 开发者部分

  6. 在 Xcode 中点击 播放 按钮以重新构建和运行。您将看到您的应用在 iOS 设备上启动并运行。

恭喜!请注意,您必须使用电缆将您的 iOS 设备连接到 Mac 以构建和运行您的应用。您将在下一节中学习如何通过 Wi-Fi 连接到您的设备。

无线连接 iOS 设备

随着时间的推移,将您的 iOS 设备从 Mac 上拔出并重新连接会变得相当麻烦,因此您现在将配置 Xcode 通过 Wi-Fi 连接到您的 iOS 设备。请按照以下步骤操作:

  1. 确保您的 iOS 设备已连接到您的 Mac,并且 Mac 和 iOS 设备处于同一无线网络中。

  2. 从 Xcode 菜单栏中选择窗口 | 设备和模拟器图 1.25:选择设备和模拟器的 Xcode 窗口菜单

    图 1.25:选择设备和模拟器的 Xcode 窗口菜单

  3. 点击标记为通过网络连接的复选框:

图 1.26:Xcode 设备和模拟器窗口,已勾选通过网络连接

图 1.26:Xcode 设备和模拟器窗口,已勾选通过网络连接

太棒了!你的 iOS 设备现在已通过无线方式连接到 Xcode,你不再需要将其连接到 USB 线缆。

摘要

在本章中,你学习了如何在你的 Mac 上下载和安装 Xcode。你熟悉了 Xcode 用户界面的不同部分。你创建了你的第一个 iOS 应用,选择了一个模拟器,并构建和运行了该应用。你学习了无设备通用 iOS 设备菜单项的用途。这使得你能够在没有 iOS 设备的情况下在你的 Mac 上创建和运行 iOS 应用。

你学习了如何通过 USB 将 iOS 设备连接到 Xcode,以便你可以在其上运行应用。你向 Xcode 添加了 Apple ID,以便可以在你的设备上创建和安装必要的数字证书,并在你的设备上信任该证书。这让你能够在实际设备上运行你的应用,从而更准确地确定它们的性能,并利用在 iOS 模拟器中不可用的功能。

最后,你学习了如何通过 Wi-Fi 连接到你的设备,因此你不再需要在运行应用时每次都插拔设备。这使得在 iOS 设备上构建和测试你的应用变得更加方便,因为任何新的构建都可以立即通过空中传输。

在下一章中,你将开始使用 Swift Playgrounds 探索 Swift 语言,并学习简单的值和类型在 Swift 中的实现方式。

第二章:第二章:简单值和类型

现在,您已经对 Xcode 进行了简要的浏览,让我们来看看 Swift 编程语言。

首先,您将探索 Swift 游乐场,这是一个交互式环境,您可以在此输入 Swift 代码并立即显示结果。接下来,您将学习 Swift 如何表示和存储各种类型的数据。然后,您将了解一些酷炫的 Swift 功能,如 类型推断类型安全,这些功能可以帮助您编写更简洁的代码并避免常见错误。最后,您将学习如何对数据进行常见操作,以及如何将消息打印到调试区域以帮助您解决问题。

到本章结束时,您应该能够编写能够存储和处理字母和数字的简单程序。

以下内容将涵盖:

  • 理解 Swift 游乐场

  • 探索数据类型

  • 探索常量和变量

  • 理解类型推断和类型安全

  • 探索运算符

  • 使用 print() 语句

    重要信息

    更多关于 Swift 语言最新版本的信息,请访问 docs.swift.org/swift-book/

技术要求

要完成本章的练习,您需要以下内容:

  • 运行 macOS 11 Big Sur 或 macOS 12 Monterey 的 Apple Mac 计算机

  • 已安装 Xcode 13(有关安装 Xcode 的说明,请参阅 第一章熟悉 Xcode

本章的 Xcode 游乐场位于本书代码包的 Chapter02 文件夹中,可在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,了解代码的实际应用:

bit.ly/3bTuizM

在下一节中,您将创建一个新的游乐场,您可以在其中输入本章中展示的代码。

理解 Swift 游乐场

游乐场是交互式编码环境。您在左侧窗格中输入代码,结果会立即在右侧窗格中显示。这是一种很好的实验代码和探索系统 API 的方法。

重要信息

API 是应用程序编程接口的缩写。要了解更多信息,请访问此链接:en.wikipedia.org/wiki/API

让我们从创建一个新的游乐场并检查其用户界面开始。按照以下步骤进行:

  1. 要创建游乐场,启动 Xcode 并从 Xcode 菜单栏选择 File | New | Playground...:![图 2.1:Xcode 菜单栏,选中 File | New | Playground...

    ![img/Figure_2.1_B17469.jpg]

    图 2.1:Xcode 菜单栏,选中 File | New | Playground... ![图 2.1:Xcode 菜单栏,选中 File | New | Playground...

  2. 模板屏幕出现。iOS 应已选中。选择 空白 并点击 下一步:![图 2.2:为您的新的游乐场选择一个模板:屏幕

    ![Figure_2.2_B17469.jpg]

    图 2.2:为您的新的 playground 选择一个模板:屏幕

  3. 将 playground 命名为SimpleValues并保存到您喜欢的任何位置。完成后点击创建:![Figure_2.3: Save dialog box]

    ![Figure_2.3_B17469.jpg]

    图 2.3:保存对话框

  4. 您应该在屏幕上看到 playground:

![Figure_2.4: Xcode playground 用户界面]

![Figure_2.4_B17469.jpg]

图 2.4:Xcode playground 用户界面

如您所见,它比 Xcode 项目简单得多。让我们更详细地看看界面:

  • 导航器按钮(1) - 显示或隐藏导航器区域。

  • 活动视图(2) - 显示当前操作或状态。

  • 库按钮(3) - 显示代码片段和其他资源。

  • 检查器按钮(4) - 显示或隐藏检查器区域。

  • 导航器区域(5) - 提供快速访问项目各个部分的途径。默认情况下显示项目导航器。

  • 编辑器区域(6) - 您在这里编写代码。

  • 结果区域(7) - 为您编写的代码提供即时反馈。

  • 播放按钮(8) - 从所选行执行代码。

  • 边框(9) - 这个边框将编辑器结果区域分开。如果您发现结果区域显示的结果被截断,将边框向左拖动以增加其大小。

  • 播放/停止按钮(10) - 执行或停止 playground 中所有代码的执行。

  • print()命令。

  • 调试按钮(12) - 显示或隐藏调试区域。

您可能会发现 playground 中的代码太小,难以阅读。让我们看看如何在下一节中使其变大。

定制字体和颜色

Xcode 提供了广泛的定制选项。您可以在首选项...菜单中访问它们。如果您发现文本太小,难以看清,请按照以下步骤操作:

  1. Xcode菜单中选择首选项...以显示首选项窗口。

  2. 在首选项窗口中,点击主题并选择演示(浅色)以使代码更大,更容易阅读:![Figure_2.5: Xcode Preferences window with the Themes pane selected]

    ![Figure_2.5_B17469.jpg]

    图 2.5:选择主题面板的 Xcode 首选项窗口

  3. 关闭首选项窗口返回到 playground。注意,playground 中的文本比之前大。如果您愿意,也可以尝试其他主题。

现在您已经将字体和颜色定制到您喜欢的样子,让我们看看如何在下一节中在 playground 中运行代码。

运行 playground 代码

您的 playground 中已经有一个指令。要执行指令,请按照以下步骤操作:

  1. 点击 playground 左下角的播放/停止按钮。您可能会看到一个以下对话框:![Figure_2.6: Developer Tools Access dialog box]

    ![Figure_2.6_B17469.jpg]

    图 2.6:开发者工具访问对话框

  2. 结果区域显示的"Hello, playground"中输入:

![Figure_2.7: Playground showing "Hello, playground" in the Results area]

![Figure_2.7_B17469.jpg]

图 2.7:游乐场中结果区域显示 "Hello, playground"

小贴士

你可以使用键盘快捷键 Command + Shift + Return 来在游乐场中运行代码。

为了准备在本章剩余部分使用游乐场,请从游乐场中删除 var greeting = "Hello, playground" 指令。在编写代码的过程中,将本章中显示的代码输入到游乐场中,并在必要时点击 播放/停止 按钮来运行它。

让我们进入下一节,了解 Swift 中使用的简单数据类型。

探索数据类型

所有编程语言都可以存储数字、逻辑状态和单词,Swift 也不例外。即使你是一位经验丰富的程序员,你也可能会发现 Swift 以与其他你可能熟悉的语言不同的方式表示这些对象。

重要信息

更多有关数据类型的信息,请访问:docs.swift.org/swift-book/LanguageGuide/TheBasics.html

让我们按顺序介绍 Swift 中的 整数浮点数布尔值字符串,在下一节中。

整数的表示

假设你想存储以下内容:

  • 城市中的餐馆数量

  • 飞机上的乘客

  • 酒店房间

你会使用整数,这些是没有分数部分的数字(包括负数)。

Swift 中的整数由 Int 类型表示。

浮点数的表示

假设你想存储以下内容:

  • 圆周率(3.14159...)

  • 绝对零度(-273.15 °C)

你会使用浮点数,这些是带有分数部分的数字。

Swift 中浮点数的默认类型是 Double,它使用 64 位,包括负数。您还可以使用 Float,它使用 32 位,但 Double 更受欢迎。

表示布尔值

假设你想存储对简单是/否问题的答案,例如以下内容:

  • 在下雨吗?

  • 餐厅里还有空位吗?

为了这个,你使用布尔值。

Swift 提供了一个 Bool 类型,可以是 truefalse

表示字符串

假设你想存储以下内容:

  • 餐馆的名称,例如 "Bombay Palace"

  • 职位描述,例如 "会计师" 或 "程序员"

  • 一种水果,例如 "香蕉"

你会使用 Swift 的 String 类型,它表示字符序列,并且完全符合 Unicode。这使得表示不同的字体和语言变得容易。

重要信息

要了解更多关于 Unicode 的信息,请访问此链接:home.unicode.org/basic-info/faq/

现在你已经了解了 Swift 如何表示这些常见数据类型,让我们在之前创建的游乐场中尝试它们,在下一节中。

在游乐场中使用常见数据类型

您在游乐场中输入的任何内容都将被执行,结果将出现在结果区域。让我们看看当您在游乐场中输入数字、布尔值和字符串并执行时会发生什么。按照以下步骤操作:

  1. 在它前面的//中输入以下代码是一个注释。注释是创建笔记或提醒给自己的一种好方法,并且将被 Xcode 忽略。

  2. 点击播放/停止按钮来运行您的代码。

  3. 等待几秒钟。Xcode 将评估您的输入并在结果区域显示结果,如下所示:

    42
    -23
    
    3.14159
    0.1
    -273.15
    
    true
    false
    
    "hello, world" 
    "albatross"
    

    注意,注释不会出现在结果区域。

太棒了!您刚刚创建并运行了您的第一个游乐场。让我们看看如何在下一节中存储不同的数据类型。

探索常量和变量

现在您已经了解了 Swift 支持的基本数据类型,让我们看看如何存储它们,这样您以后就可以对它们进行操作。

您可以使用常量变量来存储数据。两者都是具有名称的容器,但常量的值只能设置一次,一旦设置后就不能更改,而变量的值可以在任何时候更改。

在使用之前,您必须声明常量和变量。常量使用let关键字声明,而变量使用var关键字声明。

让我们通过在您的游乐场中实现它们来探索常量和变量是如何工作的。按照以下步骤操作:

  1. 将以下代码添加到您的游乐场中,以声明三个常量:

    let theAnswerToTheUltimateQuestion = 42 
    let pi = 3.14159
    let myName = "Ahmad Sahar"
    
  2. 点击"Ahmad Sahar",用于为myName分配值。这些被称为字符串字面量

  3. 在常量声明之后添加以下代码以声明三个变量:

    var currentTemperatureInCelsius = 27 
    var myAge = 50
    var myLocation = "home"
    

    与常量类似,每个情况下都会创建一个容器并命名,然后存储分配的值。

    小贴士

    存储的值将在结果区域显示。

  4. 常量的值一旦设置就无法更改。为了测试这一点,在变量声明之后添加以下代码:

    let isRaining = true
    isRaining = false
    

    当您输入第二行代码时,将出现一个带有建议的弹出菜单:

    ![图 2.8:自动完成弹出菜单 图片 2.8

    图 2.8:自动完成弹出菜单

    使用上下箭头键选择isRaining常量,然后按Tab键选择它。这个功能被称为自动完成,可以帮助您在输入代码时防止输入错误。

  5. 输入完成后,等待几秒钟。在第二行,您应该看到一个带有白色点的红色圆圈:![图 2.9:错误通知 图片 2.9

    图 2.9:错误通知

    这意味着您的程序中存在错误,Xcode 认为它可以修复。错误出现是因为您在常量的初始值设置后尝试为其分配新值。

  6. 点击红色圆圈以展开错误消息。您应该看到一个带有修复按钮的以下框:![图 2.10:展开的错误通知 图片 2.10

    图 2.10:扩展的错误通知

    Xcode 会告诉您问题所在(无法赋值:'isRaining' 是一个 'let' 常量)并建议一个修正(将 'let' 更改为 'var' 以使其可变)。

  7. 点击 修复 按钮。

  8. 您应该看到 isRaining 常量声明已被更改为变量声明:

图 2.11:应用修复后的代码

图 2.11:应用修复后的代码

由于新值可以在创建变量后分配,错误得到了解决。但是,请注意,建议的修正可能不是最佳解决方案。

如果您查看输入的代码,可能会想知道 Xcode 如何知道变量或常量中存储的数据类型。您将在下一节中了解这一点。

理解类型推断和类型安全

在上一节中,您声明了常量和变量,并给它们赋值。Swift 会根据您提供的值自动确定常量或变量的类型,这被称为 类型推断。您可以通过按住 Option 键并单击其名称来查看常量或变量的类型。要查看实际操作,请按照以下步骤进行:

  1. 将以下代码添加到您的游乐场中,以声明一个字符串:

    let cuisine = "American"
    
  2. 点击 播放/停止 按钮来运行它。

  3. 按住 Option 键并单击 cuisine 以显示常量类型。您应该看到以下内容:

图 2.12:类型声明弹出窗口

](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_2.12_B17469.jpg)

图 2.12:类型声明弹出窗口

如您所见,cuisine 的类型是 String

如果您想为变量或常量设置特定的类型,您将在下一节中了解如何操作。

使用类型注解来指定类型

您已经看到 Xcode 会尝试根据提供的值自动确定变量或常量的数据类型。然而,有时您可能希望指定一个类型而不是让 Xcode 为您做这件事。为此,在常量或变量名称后输入一个冒号,后跟所需类型。这被称为 类型注解

将以下代码添加到您的游乐场中,以声明一个具有特定类型的变量,并点击 播放/停止 按钮来运行它:

var restaurantRating: Double = 3

在这里,您指定了 restaurantRating 具有特定的类型,Double。即使您分配了一个整数给 restaurantRating,它也会被存储为浮点数。

在下一节中,您将了解 Xcode 如何通过强制执行 类型安全 来帮助您减少程序中的错误数量。

使用类型安全来检查值

Swift 是一种类型安全的语言。它会检查您是否正在将正确类型的值分配给变量,并将不匹配的类型标记为错误。让我们通过以下步骤来了解它是如何工作的:

  1. 将以下代码添加到您的游乐场中,以将字符串赋值给 restaurantRating

    restaurantRating = "Good"
    
  2. 点击 播放/停止 按钮来运行代码。

  3. 你应该看到一个带有x的红色圆圈。感叹号表示 Xcode 无法为此提供修复建议。点击红色圆圈。

  4. 由于你试图将一个字符串赋值给类型为Double的变量,因此显示以下错误信息:![Figure 2.13: Expanded error notification with no fix]

    ![img/Figure_2.13_B17469.jpg]

    图 2.13:无修复建议的扩展错误通知

  5. 在该行前输入//来注释掉它,如下所示:

    // restaurantRating = "Good"
    

    红色圆圈消失了,因为你的程序中没有错误了。

    小贴士

    选择代码行并输入Command + / 来注释掉它们。

现在你已经知道了如何在常量和变量中存储数据,让我们看看如何在下一节中对这些数据进行操作。

探索操作符

你可以在 Swift 中执行算术、比较和逻辑操作。truefalse

重要信息

关于操作符的更多信息,请访问docs.swift.org/swift-book/LanguageGuide/BasicOperators.html

让我们更详细地看看每种操作符类型。你将在下一节开始学习算术操作符(加法、减法、乘法和除法)。

使用算术操作符

你可以使用这里显示的标准算术操作符对整数和浮点数执行数学运算:

![Figure 2.14: Arithmetic operators]

![img/Figure_2.14_B17469.jpg]

图 2.14:算术操作符

让我们看看这些操作符是如何使用的。按照以下步骤操作:

  1. 将以下代码添加到你的 playground 中,以添加算术运算:

    let sum = 23 + 20
    let result = 32 - sum
    let total = result * 5
    let divide = total / 10
    
  2. 点击43-11-55-5,分别。请注意,55 除以 10 返回 5 而不是 5.5,因为这两个数都是整数。

  3. 操作符只能与相同类型的操作数一起工作。输入以下代码并运行它,看看如果操作数是不同类型会发生什么:

    let a = 12
    let b = 12.0 
    let c = a + b
    

    你会得到一个错误信息,ab是不同类型。请注意,Xcode 无法自动修复这个问题,因此不会显示任何修复建议。

  4. 为了修复错误,按照以下方式修改程序:

    let c = Double(a) + b
    

    Double(a)a中获取存储的值并从中创建一个浮点数。现在两个操作数都是相同类型,现在你可以将b中的值加到它上面。存储在c中的值是24.024将在结果区域显示。

现在你已经知道了如何使用算术操作符,你将在下一节中查看复合赋值操作符(+=-=*=/=)。

使用复合赋值操作符

你可以使用这里显示的复合赋值操作符对一个值执行操作并将结果赋给一个变量:

![Figure 2.15: Compound assignment operators]

![img/Figure_2.15_B17469.jpg]

图 2.15:复合赋值操作符

让我们看看这些操作符是如何使用的。将以下代码添加到你的 playground 中,并点击播放/停止按钮来运行它:

var aa = 1
aa += 2
aa -= 1

a += 2 表达式是 a = a + 2 的简写,所以 a 中的值现在是 1 + 2,并将 3 赋值给 a。同样,a -= 1a = a - 1 的简写,所以 a 中的值现在是 3 - 1,并将 2 赋值给 a

现在你已经熟悉了复合赋值运算符,让我们在下一节看看比较运算符(==/=><>=<=)。

使用比较运算符

你可以使用比较运算符将一个值与另一个值进行比较,结果将是 truefalse。你可以使用以下比较运算符:

![Figure 2.16: Comparison operators]

![Figure_2.16_B17469.jpg]

图 2.16:比较运算符

让我们看看这些运算符是如何使用的。将以下代码添加到你的游乐场中,并点击 Play/Stop 按钮来运行它:

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。

返回的布尔值将在结果区域显示。

如果你想要检查多个条件,那逻辑运算符(ANDORNOT)就派上用场了。你将在下一节学习这些内容。

使用逻辑运算符

当你处理两个或更多条件时,逻辑运算符很有用。例如,如果你在便利店,如果你有现金或信用卡,你可以为商品付款。在这种情况下,OR 是逻辑运算符。

你可以使用以下逻辑运算符:

![Figure 2.17: Logical operators]

![Figure_2.17_B17469.jpg]

图 2.17:逻辑运算符

要查看这些运算符的使用方法,请将以下代码添加到你的游乐场中,并点击 Play/Stop 按钮来运行它:

(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==1true,所以 NOT true 返回 false

返回的布尔值将在结果区域显示。

到目前为止,你只处理过数字。在下一节中,你将看到如何使用 Swift 的 String 类型对单词和句子进行操作,它们作为字符串存储。

执行字符串操作

如你之前所见,字符串是一系列字符。它们由 String 类型表示,并且完全符合 Unicode 标准。

重要信息

更多有关字符串的信息,请访问:docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html

让我们了解一些常见的字符串操作。按照以下步骤操作:

  1. 您可以使用 + 运算符将两个字符串连接起来。将以下代码添加到您的游乐场中,然后点击 "Good""Morning" 将被连接,并在结果区域显示 "Good Morning"

  2. 您可以通过 rating 常量(包含 3.5,类型为 Double)与其他类型的常量和变量结合使用。将 rating 放在 String() 的括号中,获取存储在 rating 中的值,并基于它创建一个新的字符串 "3.5",然后将其与 ratingResult 变量中的字符串结合,返回字符串 "The restaurant rating is 3.5"

  3. 有一种更简单的方法来组合字符串,称为字符串中的 \(" 和 ")" 。输入以下代码并运行它:

    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.18:调试区域显示 print() 语句的结果图 2.18:调试区域显示  语句的结果

图 2.18:调试区域显示 print() 语句的结果

当你刚开始学习时,可以随意使用尽可能多的 print() 语句。这是一种非常好的理解程序中发生情况的方法。

摘要

在本课中,您学习了如何创建和使用游乐场文件,这允许您探索和实验 Swift。

您看到了 Swift 如何表示不同类型的数据,以及如何使用常量和变量。这使得您可以在程序中存储数字、布尔值和字符串。

您还了解了类型推断、类型注解和类型安全,这些有助于您编写简洁且错误更少的代码。

你已经了解了如何对数字和字符串进行操作,这让你能够执行简单的数据处理任务。

你学习了如何修复错误,以及如何将输出打印到调试区域,这在尝试查找和修复你编写的程序中的错误时非常有用。

在下一章中,你将学习条件语句可选参数。条件语句用于在程序中做出逻辑选择,而可选参数则处理变量可能或可能没有值的情况。

第三章:第三章: 条件语句和可选类型

在上一章中,你学习了数据类型、常量、变量和操作。到目前为止,你能够编写简单的程序来处理字母和数字。然而,程序并不总是按顺序执行。很多时候,你需要根据条件执行不同的指令。Swift 允许你通过使用 条件语句 来做到这一点,你也将在这章中学习如何使用它们。

另一件你可能注意到的事情是,在上一章中,每个变量或常量都被立即赋予了值。如果你需要一个可能最初没有值的变量,你将需要一个方法来创建一个可能或可能没有值的变量。Swift 允许你通过使用 可选类型 来做到这一点,你也将在这章中了解它们。

到本章结束时,你应该能够编写根据不同条件执行不同操作的程序,并处理可能或可能没有值的变量。

以下内容将涵盖:

  • 介绍条件语句

  • 介绍可选类型和可选绑定

    小贴士

    请花些时间了解可选类型。对于新手程序员来说,它们可能会有些令人畏惧。

技术要求

本章的 Xcode 演示文稿位于本书代码包的 Chapter03 文件夹中,您可以通过以下链接下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,看看代码的实际效果:

bit.ly/3woRRKq

创建一个新的演示文稿,并将其命名为 ConditionalsAndOptionals。你可以一边阅读一边在这个章节中输入和运行所有代码。你将从学习条件语句开始。

介绍条件语句

有时,你可能需要根据特定的条件执行不同的代码块,例如以下场景:

  • 在酒店选择不同的房间类型。大房间的价格会更高。

  • 在在线商店之间切换不同的支付方式。不同的支付方式会有不同的程序。

  • 在快餐店决定要订购什么。每种食品的准备程序都会不同。

要做到这一点,你需要使用条件语句。在 Swift 中,这是通过使用 if 语句(用于单个条件)和 switch 语句(用于多个条件)来实现的。

重要信息

想了解更多关于条件语句的信息,请访问 docs.swift.org/swift-book/LanguageGuide/ControlFlow.html

让我们看看 if 语句是如何根据条件值执行不同任务的,在下一节中。

使用 if 语句

一个 if 语句会在条件为 true 时执行一段代码,如果条件为 false,则可选地执行另一段代码。if 语句看起来是这样的:

if condition {
   code1
} else {
   code2
}

现在我们来实现一个if语句来观察其效果。想象一下你正在为一家餐厅编写一个应用。这个应用将允许你检查餐厅是否营业,搜索餐厅,以及检查顾客是否超过饮酒年龄限制。

按照以下步骤操作:

  1. 要检查餐厅是否营业,将以下代码添加到你的沙盒中,以创建一个常量,并在常量的值是true时执行一个语句。点击isRestaurantOpen,并将其赋值为true。接下来,你有一个检查存储在isRestaurantOpen中的值的if语句。由于值是trueprint()语句被执行,并在 Debug 区域打印Restaurant is open

  2. 尝试将isRestaurantOpen的值改为false并再次运行你的代码。由于当前条件是false,Debug 区域将不会打印任何内容。

  3. 如果一个值是false,你也可以执行语句。比如说,如果顾客搜索的餐厅不在应用数据库中,应用应该显示一条消息说明餐厅未找到。输入以下代码创建一个常量,并在常量的值是false时执行一个语句:

    let isRestaurantFound = false 
    if isRestaurantFound == false {
       print("Restaurant was not found")
    }
    

    常量isRestaurantFound被设置为false。接下来,检查if语句。isRestaurantFound == false条件返回true,并在 Debug 区域打印Restaurant was not found

  4. 尝试将isRestaurantFound的值改为true。由于当前条件是false,Debug 区域将不会打印任何内容。

  5. 要在条件为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的值改为19customerAge < drinkingAgeLimit将返回true,因此将在 Debug 区域打印Under age limit

到目前为止,你只处理了单一条件。如果有多个条件怎么办?这就是switch语句的用武之地,你将在下一节中学习它们。

使用switch语句

要理解switch语句,我们先从实现一个带有多个条件的if语句开始。想象一下你正在编写一个交通灯程序。交通灯有三种可能的状态——红色、黄色或绿色,并且你希望根据灯光的颜色执行不同的操作。为此,你可以将多个if语句链接起来。按照以下步骤操作:

  1. 将以下代码添加到您的游乐场中,以使用多个if语句实现交通灯,并点击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 case 中的代码。

  2. 这是如何将前面显示的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 "Red":匹配,case "Yellow":case "Green":default:将不会执行。

  • switch语句必须覆盖所有可能的 case。在前面显示的例子中,任何除了"Red""Yellow""Green"之外的trafficLightColor值都会匹配到default:,并在调试区域打印出Invalid color

这部分关于ifswitch语句的内容到此结束。

在下一节中,你将学习关于可选值的内容,它允许你创建没有初始值的变量,以及可选绑定,它允许在可选值有值时执行指令。

介绍可选值和可选绑定

到目前为止,每次你声明一个变量或常量时,你都会立即给它赋值。但如果你想先声明一个变量,然后稍后赋值怎么办?在这种情况下,你会使用可选值。

重要信息

更多关于可选值的信息,请访问docs.swift.org/swift-book/LanguageGuide/TheBasics.html

让我们学习如何创建和使用可选值,并看看它们如何在程序中使用。想象你正在编写一个程序,其中用户需要输入他们配偶的名字。当然,如果用户没有结婚,那么这个值就不存在。在这种情况下,你可以使用可选值来表示配偶的名字。

可选可能有两种状态之一。它可以包含一个值,或者不包含值。如果可选包含一个值,你可以访问其中的值。访问可选值的这个过程称为解包可选。让我们看看这是如何工作的。按照以下步骤:

  1. 将以下代码添加到你的沙盒中,以创建一个变量并打印其内容:

    var spouseName: String 
    print(spouseName)
    
  2. 点击播放/停止按钮来运行它。由于 Swift 是类型安全的,它将显示一个错误,在初始化之前使用变量'spouseName'

  3. 要解决这个问题,你可以将空字符串赋给spouseName。按照以下方式修改你的代码:

    var spouseName: String spouseName should not have a value.
    
  4. 由于spouseName最初不应该有值,让我们将其设置为可选。为此,在类型注解后输入一个问号并移除空字符串赋值:

    var spouseName: StringspouseName is now an optional string variable instead of a regular string variable, and the print() statement is expecting a regular string variable: ![Figure 3.1: Warning notification    ](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_3.1_B17469.jpg)Figure 3.1: Warning notificationClick the Play/Stop button. Even though there is a warning, the program will execute. Ignore the warning for now. The value of `spouseName` is shown as `"nil\n"` in the Results area, and `nil` is printed in the Debug area. `nil` is a special keyword that means the optional variable `spouseName` has no value.
    
  5. 警告出现是因为print语句将spouseName视为Any类型而不是String?类型。点击黄色三角形以显示可能的修复方案,并选择第一个修复方案:图 3.2:选择第一个修复方案的展开警告通知

    图 3.2:选择第一个修复方案的展开警告通知

    语句将变为print(spouseName ?? default value)。注意??操作符的使用。如果spouseName没有值,它将分配default value

  6. default value占位符替换为"spouseName 中没有值",如下所示。警告将消失。再次运行你的程序,"spouseName 中没有值"将出现在结果区域:图 3.3:显示默认值的区域

    图 3.3:显示默认值的区域

  7. 让我们给spouseName赋一个值。按照以下方式修改代码:

    var spouseName: String?
    Nia appears in the Debug area.
    
  8. 添加一行代码以将spouseName与另一个字符串连接,如下所示:

    print(spouseName ?? "No value in spouseName")
    String variable to an optional using the + operator. To use the string inside the optional, you'll have to unwrap it first.
    
  9. 点击红色圆圈以显示可能的修复方案,你会看到以下内容:图 3.4:展开的错误通知

    图 3.4:展开的错误通知

    第二个修复方案建议spouseName有值,但如果spouseNamenil,你的程序将崩溃。

  10. 点击第二个修复方案,你会在代码的最后一行看到spouseName后面出现一个感叹号,这表示可选被强制解包:

    let greeting = "Hello, " + spouseName!
    
  11. 当你的程序运行时,Hello, Nia被分配给greeting,如结果区域所示。这意味着spouseName已被成功解包。

  12. 要看到强制解包包含nil的变量的效果,将spouseName设置为nil

    spouseName = spouseName is now nil, the program crashed while attempting to force-unwrap spouseName.A better way of handling this is to use optional binding. In optional binding, you attempt to assign the value in an optional to a temporary variable (you can name it whatever you like). If the assignment is successful, a block of code is executed.
    
  13. 要看到可选绑定的效果,按照以下方式修改你的代码:

    spouseName = Hello, Nia will appear in the Debug area. Here's how it works. If spouseName has a value, it will be unwrapped and assigned to a temporary variable, spouseTempVar, and the if statement will return true. The statements between the curly braces will be executed and the constant greeting will then be assigned the value Hello, Nia. Then, Hello, Nia will be printed in the Debug area. Note that the temporary variable spouseTempVar is not an optional.If `spouseName` does not have a value, no value can be assigned to `spouseTempVar` and the `if` statement will return `false`. In this case, the statements in the curly braces will not be executed at all.
    
  14. 要看到可选包含nil时的可选绑定效果,再次将nil赋给spouseName

    spouseName = spouseName is nil.
    

这就完成了关于可选和可选绑定的部分,你现在可以创建和使用可选变量了。太棒了!

摘要

你做得很好!你已经学会了如何使用 ifswitch 语句,这意味着你现在能够编写自己的程序,根据不同的条件执行不同的操作。

你还学习了可选值和可选绑定。这意味着你现在可以表示可能存在也可能不存在的变量,并且只有当变量的值存在时才执行指令。

在下一章中,你将学习如何使用一系列值而不是单个值,以及如何使用循环重复程序语句。

第四章:第四章:范围操作符和循环

在上一章中,您学习了条件语句,它允许您根据不同的条件执行不同的操作,以及可选类型,它允许您创建可能或可能没有值的变量。

在本章中,您将了解范围操作符循环。范围操作符允许您通过指定范围的开头和结尾值来表示一系列值,您还将了解不同类型的范围操作符。循环允许您重复执行指令或一系列指令。您可以重复执行固定次数的序列,或者重复执行序列直到满足某个条件。您将了解用于完成此操作的不同类型的循环。

到本章结束时,您将学会如何使用范围,以及如何创建和使用不同类型的循环(for-inwhilerepeat-while)。

以下内容将涵盖:

  • 探索范围操作符

  • 探索循环

技术要求

本章的 Xcode 游乐场位于本书代码包的Chapter04文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际效果:

bit.ly/309pdRJ

如果您想从头开始,创建一个新的游乐场并将其命名为RangeOperatorsAndLoops

您可以在阅读过程中输入并运行本章中的所有代码。让我们从使用范围操作符指定数字范围开始。

探索范围操作符

范围操作符允许您表示一系列值。假设您想表示从firstNumber开始到lastNumber结束的数字序列。您不需要指定每个值;您只需以这种方式指定范围:

firstNumber...lastNumber

假设您需要编写一个为百货公司编写程序,该程序可以自动向 18 至 30 岁的顾客发送折扣券。如果需要为每个年龄设置ifswitch语句,将会非常繁琐。在这种情况下使用范围操作符会方便得多。

重要信息

想要了解更多关于范围操作符的信息,请访问:docs.swift.org/swift-book/LanguageGuide/BasicOperators.html.

让我们在游乐场中尝试一下。按照以下步骤操作:

  1. 将以下代码添加到您的游乐场中,并点击10和以20结束的数字,包括这两个数字,到myRange常量中。这被称为闭区间操作符

  2. 结果区域显示的结果可能被截断。点击结果右侧的方形图标。它将在编辑器区域中显示:![Figure 4.1: 编辑器区域显示内联结果 img/Figure_4.1_B17469.jpg

    Figure 4.1: 编辑器区域显示内联结果

    你现在可以在代码行下的框中看到完整的输出结果。如果你愿意,可以拖动右边缘使框变大。

    小贴士

    记住你可以拖动结果和编辑器区域之间的边界来增加结果区域的大小。

  3. 如果你不想在范围内包含序列的最后一个数字,请使用 ..< 代替 ...。输入并运行以下代码:

    let myRange2 = 10..<20
    

    这将在 myRange2 常量中存储从 10 开始到 19 结束的序列,并被称为 半开范围运算符

还有一种类型的范围运算符,即 单侧范围运算符,你将在下一章中了解它。

现在你已经了解了如何创建和使用范围,你将在下一节中学习循环、不同的循环类型以及如何使用它们。

探索循环

在编程中,你经常需要一遍又一遍地做同样的事情。例如,每个月,公司都需要为每位员工生成工资条。如果公司有 10,000 名员工,编写 10,000 条指令来创建工资条将是不高效的。重复一条指令 10,000 次会更好,循环就是为此而用的。

有三种类型的循环;for-in 循环、while 循环和 repeat-while 循环。for-in 循环将重复已知次数,而 whilerepeat-while 循环将重复直到循环条件为真。

重要信息

更多关于循环的信息,请访问:docs.swift.org/swift-book/LanguageGuide/ControlFlow.html

让我们逐一查看每种类型,首先是 for-in 循环,它在你知道循环应该重复多少次时使用。

使用 for-in 循环

for-in 循环遍历序列中的每个值,并在每次迭代时执行大括号内的语句集,即循环体。每个值依次分配给一个临时变量,并且可以在循环体内使用该临时变量。以下是它的样子:

for item in sequence {   
   code
}

循环重复的次数由序列中的项目数量决定。让我们首先创建一个 for-in 循环来显示 myRange 中的所有数字。按照以下步骤操作:

  1. 将以下代码添加到您的游乐场中,并点击 myRange 包含范围中的最后一个数字。

  2. 让我们尝试相同的程序,但这次使用 myRange2。按照以下方式修改代码并运行:

    for number in 19.
    
  3. 你甚至可以直接在 in 关键字后使用范围运算符。输入并运行以下代码:

    for number in 0...5 {
       print(number)
    }
    

    05 的每个数字都在调试区域中显示。

  4. 如果你想要反转序列,请使用 reversed() 函数。按照以下方式修改代码并运行:

    for number in 5 to 0 is displayed in the Debug area.
    

干得好!让我们在下一节中检查 while 循环,它在循环序列应该重复直到条件为 true 时使用。

使用 while 循环

while循环包含一个条件和一组用大括号括起来的语句,称为循环体。首先检查条件;如果为true,则执行循环体,并且循环会重复,直到条件为false。它看起来是这样的:

while condition == true {
   code
}

将以下代码添加以创建一个变量,将其增加5,并且只要变量的值小于50就持续这样做。点击播放/停止按钮来运行它:

var y = 0
while y < 50 {
   y += 5
   print("y is \(y)")
}

让我们逐步分析代码。最初,y被设置为0。检查y < 50条件并返回true,因此执行循环体。y的值增加5,并在调试区域打印出y is 5。循环重复,并再次检查y < 50。由于y现在是55 < 50仍然返回true,因此再次执行循环体。这会一直重复,直到y的值为50,此时y < 50返回false,循环停止。

如果while循环的初始条件为false,则循环体将永远不会执行。尝试将y的值更改为100来查看这一点。

在下一节中,你将学习repeat-while循环。这些循环将首先执行循环体内的语句,然后再检查循环条件。

重复-直到循环

while循环类似,repeat-while循环也包含一个条件和一组循环体,但循环体在检查条件之前先执行。如果条件为true,则循环会重复,直到条件返回false。它看起来是这样的:

repeat {
   code
} while condition == true

将以下代码添加以创建一个变量,将其增加5,并且只要变量的值小于50就持续这样做。点击播放/停止按钮来运行它:

var x = 0
repeat {
   x += 5
   print("x is \(x)")
} while x < 50

让我们逐步分析代码。最初,x被设置为0。执行循环体。x的值增加5,因此现在x包含5,并在调试区域打印出x is 5。检查x < 50条件,由于它返回true,循环会重复。x的值增加5,因此现在x包含10,并在调试区域打印出x is 10。循环会重复,直到x包含50,此时x < 50返回false,循环停止。

即使初始条件为false,循环体也会至少执行一次。尝试将x的值更改为100来查看这一点。

你现在知道了如何创建和使用不同的循环类型。太棒了!

摘要

在本章中,你了解了闭包和半开区间运算符,这些运算符允许你指定一个数字范围,而不是逐个指定每个数字。

你还学习了三种不同的循环类型:for-in循环、while循环和repeat-while循环。for-in循环允许你重复一组语句固定次数,而whilerepeat-while循环允许你在条件为true的情况下重复一组语句。做得好!

在下一章中,你将学习集合类型,这些类型允许你通过索引存储数据集合、通过键值对存储数据集合以及存储无结构的数据集合。

第五章:第五章:集合类型

到目前为止,你已经学到了很多!你现在可以创建一个程序,将数据存储在常量或变量中,并对它们进行操作,并且你可以使用条件语句和循环来控制流程。但到目前为止,你主要存储的是单个值。

在本章中,你将学习存储值集合的方法。Swift 有三种集合类型:数组,它存储一个有序的值列表;字典,它存储一个无序的键值对列表;以及集合,它存储一个无序的值列表。

到本章结束时,你将学会如何创建数组、字典和集合,以及如何对它们进行操作。

以下内容将涵盖:

  • 理解数组

  • 理解字典

  • 理解集合

技术要求

本章的 Xcode 游乐场位于本书代码包的Chapter05文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频,看看代码的实际效果:

bit.ly/3H5blc2

如果你希望从头开始,创建一个新的游乐场,并将其命名为CollectionTypes。你可以一边阅读一边输入并运行本章中的所有代码。

重要信息

要了解更多关于数组、字典和集合的信息,请访问docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html

你将要学习的第一种集合类型是数组,它允许你按顺序列表存储信息。

理解数组

假设你想要存储以下内容:

  • 便利店购物清单

  • 每月必须做的家务

数组非常适合这个。数组按顺序列表存储值。它看起来是这样的:

图 5.1:数组

图 5.1:数组

值必须是同一类型。你可以通过使用数组索引来访问数组中的任何值,索引从0开始。

如果你使用let关键字创建一个数组,那么一旦创建,其内容就不能更改。如果你想创建后更改数组的内容,请使用var关键字。

让我们看看如何使用数组。你将在下一节中通过给它赋值来创建一个数组。

创建一个数组

在前面的章节中,你通过声明并给它赋一个初始值来创建了一个常量或变量。你可以用同样的方式创建一个数组。

想象一下你的配偶让你去便利店买一些东西。让我们使用数组来实现一个购物清单。将以下代码添加到你的游乐场中,然后点击播放/停止按钮来运行它:

var shoppingList = ["Eggs", "Milk"]

这条指令创建了一个名为shoppingList的数组变量。分配的值["Eggs", "Milk"]是一个包含"Eggs"在索引0处的String数组。

在这里使用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"。这可以在结果显示区域看到。

小贴士

你也可以使用+运算符向数组添加新元素,以下代码:shoppingList = shoppingList + ["Cooking Oil"]

你可以使用insert(_:at:)在指定索引处添加新项目。输入并运行以下代码:

shoppingList.insert("Chicken", at: 1)

这将在索引1处插入"Chicken",因此现在shoppingList数组包含"Eggs""Chicken""Milk""Cooking Oil"。注意,"Chicken"是数组的第二个元素,因为第一个元素在索引0。这可以在结果显示区域看到。

想象一下,你已经买到了购物清单上的第一项,现在你需要知道列表中的下一项是什么。在下一节中,你将看到如何使用数组索引访问特定数组元素。

访问数组元素

你可以指定数组索引来访问特定元素。输入并运行以下代码:

shoppingList[2]

这返回存储在索引2处的数组元素,结果显示区域显示"Milk"

想象一下,你的配偶打电话让你买豆浆而不是牛奶。由于这个数组是使用var关键字声明的,你可以修改其中存储的值。你将在下一节中学习如何做到这一点。

分配新值给特定索引

你可以通过指定索引并分配新值来替换现有的数组元素。输入并运行以下代码:

shoppingList[2] = "Soy Milk"
shoppingList

这将替换存储在索引2"Milk"处的值,用"Soy Milk"替换。现在shoppingList数组包含"Eggs""Chicken""Soy Milk""Cooking Oil",如结果区域所示。

注意使用的索引必须是有效的。例如,你不能使用索引4,因为这里唯一的有效索引是0123。这样做会导致程序崩溃。

假设你的配偶给你打电话,告诉你冰箱里有鸡肉,所以你不再需要购买它。在下一节中,你将看到两种从数组中移除元素的方法。

从数组中移除元素

你可以使用remove(at:)从数组中移除一个元素。输入并运行以下代码:

shoppingList.remove(at: 1)
shoppingList

这将从shoppingList数组中移除索引为1的元素"Chicken",因此现在它包含"Eggs""Soy Milk""Cooking Oil"。你可以在结果区域中看到这一点。

如果你需要从数组中移除最后一个元素,可以使用removeLast()

假设你已经获取了列表中的每一项,并且你想再次遍历列表以确保无误。你需要依次访问数组中的每个元素并对每个元素执行操作。你将在下一节中看到如何做到这一点。

遍历数组

记得你在上一章中学到的for-in循环吗?你可以使用它来遍历数组中的每个元素。输入并运行以下代码:

for shoppingListItem in shoppingList {
   print(shoppingListItem)
}

这将在调试区域中打印出数组中的每个元素。

你也可以使用1...。输入并运行以下代码:

for shoppingListItem in shoppingList[1...] {
   print(shoppingListItem)
}

这将在调试区域中打印出从索引1开始的数组元素。

现在,你已经知道如何使用数组创建一个有序列表,例如购物清单,以及如何执行数组操作。在下一节中,我们将探讨如何使用字典存储无序列表中的键值对。

理解字典

假设你正在编写一个地址簿应用。你需要存储一个包含姓名及其对应联系号码的列表。字典对于这个用途再合适不过了。

字典在无序列表中存储键值对。它看起来是这样的:

图 5.2:字典

图 5.2:字典

所有键必须是同一类型,并且必须是唯一的。所有值也必须是同一类型,但不一定是唯一的。键和值不必是同一类型的。你使用键来获取相应的值。

如果你使用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["Jane"] = "+0229876543"
contactList

这将在 contactList 字典中添加一个新的键值对,键为 "Jane",值为 "+0229876543"。现在它包括 "Shah": "+60126789345""Aamir": "+0223456789""Jane": "+0229876543"。你可以在结果区域看到这一点。

假设你想拨打其中一个联系人的电话,并需要该联系人的电话号码。在下一节中,你将看到如何通过指定键来访问所需值的字典元素。

访问字典元素

你可以指定一个字典键来访问其对应值。输入并运行以下代码:

contactList["Shah"]

这将返回键 "Shah" 的值,并且 +60123456789 将在结果区域显示。

假设你的某个联系人换了一个新电话,因此你需要更新该联系人的电话号码。你可以修改存储在字典中的键值对。你将在下一节中学习如何操作。

为现有键分配新值

你可以给现有的键分配一个新的值。输入并运行以下代码:

contactList["Shah"] = "+60126789345"
contactList

这将为键"Shah"分配一个新的值。现在contactList字典包含"Shah": "+60126789345""Aamir": "+0223456789""Jane": "+0229876543"。你可以在结果区域中看到这一点。

假设你需要在你的应用中删除一个联系人。让我们在下一节中看看如何从字典中删除元素。

从字典中删除元素

要从字典中删除一个元素,将nil赋给现有的键。输入并运行以下代码:

contactList["Jane"] = nil
contactList

这将从contactList字典中删除键为"Jane"的元素,现在它包含"Shah": "+60126789345""Aamir": "+0223456789"。你可以在结果区域中看到这一点。

如果你想要保留你正在删除的值,请使用removeValue(for:Key)代替。输入并运行以下代码:

var oldDictValue = contactList.removeValue(forKey: "Aamir")
oldDictValue 
contactList

这将从contactList字典中删除键为"Aamir"的元素,并将它的值赋给oldDictValue。现在oldDictValue包含"+0223456789",而contactList字典包含"Shah": "+60126789345"。你可以在结果区域中看到这一点。

假设你想要给每个联系人打电话,祝他们新年快乐。你必须依次访问字典中的每个元素并对每个元素执行操作。你将在下一节中看到如何做到这一点。

遍历字典

就像数组一样,你可以使用for-in循环来遍历字典中的每个元素。输入并运行以下代码:

for (name, contactNumber) in contactList {
   print("\(name) : \(contactNumber)")
}

这会将字典中的每个元素打印到调试区域。由于字典是无序的,当你再次运行此代码时,你可能会得到不同的结果顺序。

你现在知道如何使用字典来创建一个无序的键值对列表,例如联系人列表,以及如何执行字典操作。在下一节中,我们将看看如何在一个集合中存储无序的值列表。

理解集合

假设你正在编写一个电影应用,并且想要存储电影类型列表。你可以使用集合来实现这一点。

一个集合以无序列表的形式存储值。这里是其外观:

![Figure 5.3: Set]

![Figure 5.03_B17469.jpg]

![Figure 5.3: Set]

所有值都是同一类型。

如果你使用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

"Action"movieGenres 集合中移除并分配给 oldSetValuemovieGenres 集合现在包含 "Horror""Romantic Comedy""War"。你将在结果区域中看到这一点。

要从集合中移除所有元素,请使用 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)

在这里,您正在对两个集合movieGenresmovieGenres2执行集合运算。让我们看看每个集合运算的结果:

  • union(_:) 返回一个新集合,包含两个集合中的所有值,因此{"Horror", "Romantic Comedy", "War", "Science Fiction", "Fantasy"}将在结果区域显示。

  • intersection(_:) 返回一个新集合,仅包含两个集合共有的值,因此{"War"}将在结果区域显示。

  • subtracting(_:) 返回一个新集合,其中不包含指定集合中的值,因此{"Horror", "Romantic Comedy"}将在结果区域显示。

  • symmetricDifference(_:) 返回一个新集合,其中不包含两个集合共有的值,因此{"Horror", "Romantic Comedy", "Science Fiction", "Fantasy"}将在结果区域显示。

假设您希望您的应用程序比较您喜欢的类型与另一个人喜欢的类型。在下一节中,您将了解如何检查一个集合是否等于另一个集合,是否是另一个集合的一部分,或者与另一个集合没有任何共同点。

探索集合成员和相等性

检查一个集合是否等于、是另一个集合的子集超集不相交的另一个集合非常简单。输入并运行以下代码:

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)

让我们看看这段代码是如何工作的:

  • isEqual运算符(==)检查一个集合的所有成员是否与另一个集合的成员相同。由于movieGenres集合的并非所有成员都与movieGenres2集合中的成员相同,结果区域将显示false

  • isSubset(of:) 检查一个集合是否是另一个集合的子集。由于movieGenresSubset集合的所有成员都在movieGenres集合中,结果区域将显示true

  • isSuperset(of:) 检查一个集合是否是另一个集合的超集。由于movieGenres集合的所有成员都在movieGenresSuperset集合中,结果区域将显示true

  • isDisjoint(with:) 检查一个集合是否与另一个集合没有共同值。由于movieGenresDisjoint集合与movieGenres集合没有共同成员,结果区域将显示true

现在,你知道了如何使用集合来创建一个无序列表,例如电影类型列表,以及如何执行集合操作。这标志着集合类型章节的结束。做得好!

概述

在本章中,你学习了 Swift 中的集合类型。首先,你了解了数组。这让你可以使用有序列表来表示像购物清单这样的项目,并对它进行操作。

接下来,你学习了字典。这让你可以使用无序列表来表示像联系人列表这样的项目,并对它进行操作。

最后,你学习了集合。这让你可以使用无序列表来表示像电影类型列表这样的项目,并对它进行操作。

在下一章中,你将学习如何使用函数将一组指令组合在一起。当你想在程序中多次执行一组指令时,这非常有用。

第六章:第六章:函数和闭包

到目前为止,你可以编写合理复杂的程序,这些程序可以做出决策并重复指令序列。你还可以使用集合类型存储程序数据。随着你编写的程序在大小和复杂性上增长,理解它们所做的工作将变得更加困难。

为了使大型程序更容易理解,Swift 允许你创建函数,这让你可以将多个指令组合在一起,并通过调用单个名称来执行它们。你还可以创建闭包,这让你可以将多个指令组合在一起,而不需要名称,并将其分配给常量或变量。

到本章结束时,你将了解函数、嵌套函数、作为返回类型的函数、作为参数的函数以及guard语句。你还将了解如何创建和使用闭包。

以下将涵盖以下主题:

  • 理解函数

  • 理解闭包

技术要求

本章的 Xcode 游乐场位于本书代码包的Chapter06文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频,看看代码是如何运行的:

bit.ly/3o2MYTs

如果你希望从头开始,创建一个新的游乐场,并将其命名为FunctionsAndClosures

你可以在进行过程中输入并运行本章中的所有代码。让我们先学习函数。

理解函数

函数对于封装执行特定任务的一组指令非常有用,例如:

  • 计算餐厅餐费的 10%服务费。

  • 计算你希望购买的汽车的月供。

下面是一个函数的样子:

func functionName(parameter1: ParameterType, ...) -> ReturnType {
   code
}

每个函数都有一个描述性的名称。你可以定义一个或多个作为输入的函数值,这些值被称为参数。你还可以定义函数完成后的输出类型,这被称为其返回类型。参数和返回类型都是可选的。

你“调用”函数的名称来执行它。这就是函数调用的样子:

functionName(parameter1: argument1, …)

你提供与函数参数类型匹配的输入值(称为参数)。

重要信息

要了解更多关于函数的信息,请访问docs.swift.org/swift-book/LanguageGuide/Functions.html

在下一节中,我们将看看如何创建一个计算服务费的函数。

创建一个函数

在其最简单的形式中,函数只是执行一些指令,没有参数或返回类型。你将通过编写一个计算餐费服务费的函数来了解这是如何工作的。服务费应该是餐费的 10%。

将以下代码添加到您的游乐场中,以创建并调用此函数,然后点击 播放/停止 按钮运行它:

func serviceCharge() {
   let mealCost = 50
   let serviceCharge = mealCost / 10
   print("Service charge is \(serviceCharge)")
}
serviceCharge()

您刚刚创建了一个名为 serviceCharge() 的非常简单的函数。它所做的只是计算价值为 $50 的餐费的 10% 服务费,即 50 / 10,返回 5。然后您使用其名称调用此函数。您将在调试区域看到 Service charge is 5

这个函数不太有用,因为每次调用此函数时 mealCost 总是 50,结果 5 只在调试区域打印,并且不能在您的程序的其他地方使用。让我们添加一些参数和一个返回类型来使这个函数更有用。

按照以下所示修改您的代码:

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 {
   return mealCost / 10
}
let serviceChargeAmount = serviceCharge(forMealPrice: 50)
print(serviceChargeAmount)

函数的工作方式与之前完全相同,但为了调用它,您使用 serviceCharge(forMealPrice:)。这听起来更像英语,并使您更容易了解函数的功能。

在下一节中,您将学习如何在其他函数体内使用几个较小的函数,这些函数被称为嵌套函数

使用嵌套函数

在另一个函数体内有一个函数是可能的,这些函数被称为嵌套函数。嵌套函数可以使用封装函数的变量。让我们通过编写一个计算贷款月供的函数来了解嵌套函数是如何工作的。

输入并运行以下代码:

func calculateMonthlyPayments(carPrice: Double, downPayment: Double, interestRate: Double, paymentTerm: Double) -> Double {
   func loanAmount() -> Double {
      return carPrice - downPayment
   }
   func totalInterest() -> Double {
      return interestRate * paymentTerm
   }
   func numberOfMonths() -> Double {
      return 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,这是您为了购买这辆车在 7 年内每月需要支付的金额。

如你所见,Swift 中的函数与其他语言中的函数类似,但有一个酷炫的功能。函数在 Swift 中是 一等类型,因此它们可以用作参数和返回类型。让我们在下一节中看看这是如何实现的。

使用函数作为返回类型

一个函数可以作为其返回类型返回另一个函数。输入并运行以下代码以创建一个生成 Pi 值的函数:

func makePi() -> (() -> Double) {
   func generatePi() -> Double {
      return 22.0 / 7.0
   }
   return generatePi
}
let pi = makePi()
print(pi())

makePi() 函数的返回类型是一个没有参数且返回类型为 Double 的函数。generatePi() 是一个没有参数且返回类型为 Double 的函数,它将是返回的函数。因此,pi 将被分配给 generatePi(),并在调用时返回 22.0/7.03.142857142857143 将在调试区域打印。

让我们看看一个函数如何作为另一个函数的参数在下一节中使用。

使用函数作为参数

一个函数可以将另一个函数作为参数。输入并运行以下代码以创建一个函数,该函数用于确定一个满足特定条件的数字是否存在于数字列表中:

func isThereAMatch(listOfNumbers: [Int], condition: (Int) -> Bool) -> Bool {
   for item in listOfNumbers {
      if condition(item) {
         return true
      }
   }
   return false
}
func oddNumber(number: Int) -> Bool {
   return (number % 2) > 0
}
var numbersList = [2, 4, 6, 7]
isThereAMatch(listOfNumbers: numbersList, condition: oddNumber)

isThereAMatch(listOfNumbers:condition:) 有两个参数;一个整数数组和函数。提供的函数作为参数必须接受一个整数值并返回一个布尔值。oddNumber(number:) 接受一个整数并返回 true 如果该数字是奇数,这意味着它可以作为第二个参数的参数。包含奇数的 numbersList 数组用作第一个参数的参数。由于 numbersList 包含奇数,当调用 isThereAMatch(listOfNumbers:condition:) 时,它将返回 true

在下一节中,你将看到如果使用的参数不适合,如何在函数上执行早期退出。

使用 guard 语句提前退出函数

如果输入数据有问题,能够提前退出函数是有用的。比如说,你需要一个函数用于在线购买终端。这个函数将在你购买东西时计算借记卡或信用卡的剩余余额。你想要购买的商品价格输入在一个文本字段中。文本字段中的值被转换为整数,以便你可以计算剩余的卡余额。

输入并运行以下代码:

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 关键字将参数和返回类型与闭包体分开。

闭包可以被分配给一个常量或变量,所以当你需要在程序内部传递它们时,它们非常有用。例如,假设你有一个应用程序从互联网上下载文件,并且你需要在文件下载完成后对文件进行一些操作。你可以在闭包中放置一个处理文件的指令列表,并在文件下载完成后让程序执行它。你将在 第十六章 使用 MapKit 入门 中看到闭包是如何使用的。

重要信息

要了解更多关于闭包的信息,请访问 docs.swift.org/swift-book/LanguageGuide/Closures.html

现在,你将编写一个闭包,它将对数字数组中的每个元素执行计算。将以下代码添加到你的游乐场中,然后点击播放/停止按钮来运行它:

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 }

因此,闭包现在确实非常简洁,但对于新开发者来说可能有些难以理解。请随意以你舒适的方式编写闭包。

现在,你已经知道了如何创建和使用闭包,以及如何更简洁地编写它们。太棒了!

摘要

在本章中,你学习了如何将语句组合成函数。你了解了如何使用自定义参数标签、函数内的函数、函数作为返回类型以及函数作为参数。这在以后你需要在不同程序点完成相同任务时将非常有用。

你还学习了如何创建闭包。当你需要在程序中传递代码块时,这将非常有用。

在下一章中,你将学习类、结构和枚举。类和结构允许创建可以存储状态和行为的复杂对象,而枚举可以用来限制可以分配给变量或常量的值,从而减少出错的可能性。

第七章:第七章: 类、结构和枚举

在上一章中,你已经学习了如何使用函数和闭包将指令序列分组在一起。

是时候考虑如何在代码中表示复杂对象了。例如,考虑一辆车。你可以使用一个String常量来存储车名,以及一个Double变量来存储车价,但它们之间没有关联。你已经看到你可以将指令分组在一起来创建函数和闭包。在本章中,你将学习如何使用结构将常量和变量组合成一个单一实体,以及如何操作它们。你还将学习如何使用枚举将一组相关值组合在一起。

到本章结束时,你将学会如何创建和初始化一个类,从现有类创建子类,创建和初始化结构体,区分类和结构体,以及创建枚举。

本章将涵盖以下主题:

  • 理解类

  • 理解结构

  • 理解枚举

技术要求

本章的 Xcode 游乐场位于本书代码包的Chapter07文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际操作:

bit.ly/3HbRJTA

如果你希望从头开始,创建一个新的游乐场,并将其命名为Classes,StructuresAndEnumerations。你可以一边输入一边运行本章中的所有代码。让我们从学习什么是类以及如何声明和定义它开始。

理解类

类对于表示复杂对象非常有用,例如:

  • 公司的个别员工信息

  • 在电子商务网站上出售的商品

  • 为了保险目的而拥有的家庭物品

下面是一个类声明和定义的例子:

class ClassName {
   property1
   property2 
   property3 
   method1() { 
      code
   }
   method2() {
      code
   }
} 

每个类都有一个描述性的名称,它包含用于表示对象的变量或常量。与类关联的变量或常量称为属性

一个类也可以包含执行特定任务的函数。与类关联的函数称为方法

一旦你声明并定义了一个类,你就可以创建Animal类,你可以使用该类的实例来表示动物园中不同类型的动物。这些实例的属性值将各不相同。

重要信息

要了解更多关于类的信息,请访问:docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

让我们看看如何处理类。您将学习如何声明和定义类,根据类声明创建实例,并操作这些实例。您将从下一节中创建一个表示动物的类声明开始。

创建类声明

让我们声明并定义一个可以存储有关动物详情的类。将以下代码添加到您的游乐场中:

class Animal {
   var name: String = "" 
   var sound: String = ""
   var numberOfLegs: Int = 0
   var breathesOxygen: Bool = true
   func makeSound() {
      print(self.sound)
   }
}

您刚刚声明了一个非常简单的名为Animal的类。惯例规定,类名应以大写字母开头。此类具有存储动物名称、它发出的声音、它拥有的腿数以及它是否呼吸氧气的属性。此类还有一个makeSound()方法,该方法将打印的噪音输出到调试区域。

现在您已经有了Animal类,让我们在下一节中使用它来创建一个动物的实例。

创建类的实例

一旦声明并定义了一个类,您就可以创建该类的实例。现在,您将创建一个代表猫的Animal类的实例。请按照以下步骤操作:

  1. 要创建Animal类的实例,列出其所有属性并调用其makeSound()方法,请在类声明之后输入以下内容并运行:

    let cat = Animal()
    print(cat.name)
    print(cat.sound) 
    print(cat.numberOfLegs) 
    print(cat.breathesOxygen)
    cat.makeSound()
    

    您可以通过在实例名称后输入一个点,然后跟您想要的属性或方法来访问实例属性和方法。您将在调试区域看到实例属性和方法的值。由于这些值是在创建类时分配的默认值,因此namesound包含空字符串,numberOfLegs包含0breathesOxygen包含true,而makeSound()方法打印一个空字符串。

  2. 让我们为此实例的属性分配一些值。按照以下所示修改您的代码:

    let cat = Animal()
    makeSound() method are printed to the Debug area. Note that here you create the instance first, and then assign values to that instance. It is also possible to assign the values when the instance is being created, and you do this by implementing an initializer in your class declaration. 
    
  3. 初始化器负责确保在创建类时所有实例属性都有有效的值。让我们为Animal类添加一个初始化器。按照以下所示修改您的类定义:

    class Animal {
       var name: String 
       var sound: String
       var numberOfLegs: Int
       var breathesOxygen: Bool
    init keyword and has a list of parameters that will be used to set the property values. Note that the self keyword distinguishes the property names from the parameters. For example, self.name refers to the property and name refers to the parameter. At the end of the initialization process, every property in the class should have a valid value.
    
  4. 在这一点上,您将看到一些代码错误。您需要更新您的函数调用以解决这个问题。按照以下所示修改您的代码并运行它:

       func makeSound() {
          print(self.sound)
       }
    }
    let cat = Animal(name: "Cat", sound: "Mew", 
    numberOfLegs: 4, breathesOxygen: true)
    print(cat.name)
    

结果与步骤 2中的相同,但您在一个指令中创建了实例并设置了其属性。太棒了!

现在有不同类型的动物,如哺乳动物、鸟类、爬行动物和鱼类。您可以创建每个类型的类,也可以基于现有类创建一个子类。让我们在下一节中看看如何做到这一点。

创建子类

一个类的子类继承了现有类的所有方法和属性。如果您愿意,还可以向其中添加额外的属性和方法。现在,您将创建Mammal类,它是Animal类的子类。请按照以下步骤操作:

  1. 要声明Mammal类,请在Animal类声明之后输入以下代码:

    class Mammal: Animal {
       let hasFurOrHair: Bool = true
    }
    

    在类名后键入: Animal使Mammal类成为Animal类的子类。它具有在Animal类中声明的所有属性和方法,以及一个额外的属性hasFurOrHair。由于Animal类是Mammal类的父类,您可以将其称为Mammal类。

  2. 按照以下所示修改创建类实例的代码,并运行它:

    let cat = cat is now an instance of the Mammal class instead of the Animal class. As you can see, the results displayed in the Debug area are the same as before, and there are no errors. The value for hasFurOrHair has not been displayed though. Let's fix that.
    
  3. 在您的 playground 中所有其他代码之后输入以下代码以显示hasFurOrHair属性的值并运行它:

    print(cat.hasFurOrHair)
    

    由于Animal类的初始化器没有参数来分配hasFurOrHair的值,因此将使用默认值,并在调试区域显示true

    您已经看到子类可以具有额外的属性。子类还可以具有额外的属性,子类中的方法实现可以与超类实现不同。让我们在下一节中看看如何做到这一点。

覆盖超类方法

到目前为止,您一直使用多个print()语句来显示类实例的值。您将实现一个description()方法来在调试区域显示所有实例属性,因此不再需要多个print()语句。请按照以下步骤操作:

  1. 按照以下所示修改您的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(self.sound)
       }
       func description() -> String {
    return "name: \(self.name) 
          sound: \(self.sound)
          numberOfLegs: \(self.numberOfLegs)
          breathesOxygen: \(self.breathesOxygen)"
       }
    }
    
  2. 按照以下所示修改代码,以使用description()方法代替多个print()语句,并运行程序:

    let cat = Mammal(name: "Cat", sound: "Mew", 
    numberOfLegs: 4, breathesOxygen: true)
    description() method is not implemented in the Mammal class, it is implemented in the Animal class. This means it will be inherited by the Mammal class, and the instance properties will be printed to the Debug area. Note that the value for the hasFurOrHair property is missing, and you can't put it in the description() method because the hasFurOrHair property does not exist for the Animal class.
    
  3. 您可以将Mammal类中description()方法的实现修改为显示hasFurOrHair属性值。将以下代码添加到您的Mammal类定义中并运行它:

    Mammal: Animal {
       let hasFurOrHair: Bool = true
    override keyword is used here to specify that the description() method implemented here is to be used in place of the superclass implementation. The super keyword is used to call the superclass implementation of description(). The value in hasFurOrHair is then added to the string returned by super.description(). You will see the following in the Debug area:
    
    

    name: 猫 声音: 喵 拥有腿数: 4 呼吸氧气: 是 有毛或毛发: 是

    
    

hasFurOrHair属性的值在调试区域显示,表明您正在使用Mammal子类的description()方法实现。

你已经创建了类和子类声明,并创建了这两个类的实例。你还在两个类中添加了初始化器和方法。太棒了!让我们看看如何在下一节中声明和使用结构体。

理解结构体

与类一样,结构体也组合了用于表示对象和执行特定任务的属性和方法。还记得您创建的Animal类吗?您也可以使用结构体来完成相同的事情。不过,类和结构体之间有一些区别,您将在稍后了解更多。

下面是一个结构体声明和定义的例子:

struct StructName { 
   property1 
   property2 
   property3
   method1() {
      code
   }
   method2(){
      code
   }
}

如您所见,结构体与类非常相似。它也有一个描述性的名称,可以包含属性和方法,并且您可以创建实例。

重要信息

要了解更多关于结构体的信息,请访问:docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

让我们看看如何处理结构。你将学习如何声明和定义结构,根据结构创建实例,并操作它们。你将从下一节创建一个表示爬行动物的结构开始。

创建结构声明

继续动物主题,让我们声明并定义一个可以存储爬行动物详细信息的结构。在你的游乐场中所有其他代码之后添加以下代码:

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 {
      return "Structure: Reptile name: \(self.name) 
      sound: \(self.sound) 
      numberOfLegs: \(self.numberOfLegs)
      breathesOxygen: \(self.breathesOxygen) 
      hasFurOrHair: \(self.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

尽管结构声明与类声明非常相似,但类和结构之间有两个区别:

  • 结构不能从另一个结构继承。

  • 类是引用类型,而结构是值类型

让我们在下一节中看看值类型和引用类型之间的区别。

比较值类型和引用类型

类是引用类型。这意味着当你将一个类实例赋值给一个变量时,你实际上是在变量中存储原始实例的内存位置,而不是实例本身。

结构是值类型。这意味着当你将结构实例赋值给一个变量时,该实例将被复制,你对原始实例所做的任何更改都不会影响副本。

现在,你将创建一个类的实例和一个结构的实例,并观察它们之间的差异。按照以下步骤操作:

  1. 你将首先创建一个包含结构实例的变量,并将其赋值给第二个变量,然后更改第二个变量中属性的值。输入以下代码并运行:

    struct SampleValueType {
       var sampleProperty = 10
    }
    var a = SampleValueType()
    var b = a 
    b.sampleProperty = 20 
    print(a.sampleProperty) 
    print(b.sampleProperty)
    

    在这个例子中,你声明了一个结构体,SampleValueType,它包含一个属性,sampleProperty。然后,你创建了该结构体的一个实例并将其赋值给变量a。之后,你将a赋值给一个新的变量b。接下来,你将bsampleProperty值更改为20。当你打印出asampleProperty值时,在调试区域打印出10,这表明对bsampleProperty值的任何更改都不会影响asampleProperty值。这是因为当你将a赋值给b时,a的一个副本被赋值给b,因此它们是完全独立的实例,不会相互影响。

  2. 接下来,你将创建一个包含类实例的变量,并将其赋值给第二个变量,然后更改第二个变量的属性值。输入以下代码并运行:

    class SampleReferenceType {
       var sampleProperty = 10
    }
    var c = SampleReferenceType()
    var d = c 
    c.sampleProperty = 20 
    print(c.sampleProperty) 
    print(d.sampleProperty)
    

在这个例子中,你声明了一个类,SampleReferenceType,它包含一个属性,sampleProperty。然后,你创建了该类的实例并将其赋值给变量c。之后,你将c赋值给一个新的变量d。接下来,你将dsampleProperty值更改为20。当你打印出csampleProperty值时,在调试区域打印出20,这表明对cd的任何更改都会影响相同的SampleReferenceType实例。

现在,问题是,你应该使用类还是结构体?让我们在下一节中探讨这个问题。

在类和结构体之间做出选择

你已经看到你可以使用类或结构体来表示一个复杂对象。那么,你应该使用哪一个?

建议使用结构体,除非你需要类才能实现的功能,例如子类。这实际上有助于防止由于类是引用类型而可能发生的某些微妙错误。

太棒了!现在你已经了解了类和结构体,让我们看看下一节中的枚举,它允许你将相关的值分组在一起。

理解枚举

枚举允许你将相关的值分组在一起,例如:

  • 指南针方向(东、西、北、南)

  • 交通灯颜色

  • 彩虹的颜色

为了理解枚举为何适合这个目的,让我们考虑以下示例。

想象你正在编写交通灯的代码。你可以使用一个整型变量来表示不同的交通灯颜色,其中0代表红色,1代表黄色,2代表绿色,如下所示:

var trafficLightColor = 2

虽然这是一种表示交通灯的可能方式,但当将3赋值给trafficLightColor时会发生什么?这将导致问题,因为3不代表有效的交通灯颜色。因此,如果能够将trafficLightColor的可能值限制为它可以显示的颜色,那就更好了。

下面是一个枚举声明的样子:

enum EnumName {
   case value1 
   case value2 
   case value3
}

每个枚举都有一个描述性的名称,其主体包含该枚举的关联值。

重要信息

要了解更多关于枚举的信息,请访问 docs.swift.org/swift-book/LanguageGuide/Enumerations.html

让我们看看如何与枚举一起工作。你将学习如何创建和操作它们。你将在下一节中创建一个表示交通灯颜色的枚举。

创建一个枚举

让我们创建一个枚举来表示交通灯。按照以下步骤进行:

  1. 将以下代码添加到你的游乐场中并运行它:

    enum TrafficLightColor {
       case red 
       case yellow 
       case green
    }
    var trafficLightColor = TrafficLightColor.red
    

    这创建了一个名为 TrafficLightColor 的枚举,它将红色、黄色和绿色值分组在一起。正如你所看到的,trafficLightColor 变量的值被限制为红色、黄色和绿色;设置任何其他值将生成错误。

  2. 就像类和结构体一样,枚举也可以包含方法。让我们给 TrafficLightColor 添加一个方法。按照以下所示修改你的代码,使 TrafficLightColor 返回一个表示交通灯颜色的字符串,并运行它:

    enum TrafficLightColor {
       case red 
       case yellow 
       case green
       func description() -> String {
          switch self {
          case .red:
    return "red" 
          case .yellow:
    return "yellow" 
          default:
             return "green"
          }
       }
    }
    var trafficLightColor = TrafficLightColor.red
    print(trafficLightColor.description())
    

description() 方法返回一个字符串,取决于 trafficLightColor 的值。由于 trafficLightColor 的值是 TrafficLightColor.red,因此 red 将会在调试区域中显示。

你已经学会了如何创建和使用枚举来存储分组值,以及如何向它们添加方法。这章的内容到此结束。做得好!

摘要

在本章中,你学习了如何使用类声明复杂对象,创建类的实例,创建子类,以及重写类方法。你还学会了如何声明结构体,创建结构体的实例,并理解引用类型和值类型之间的区别。最后,你学习了如何使用枚举来表示一组特定的值。

你现在知道了如何使用类和结构体来表示复杂对象,以及如何在你的程序中使用枚举来将相关值分组在一起。

在下一章中,你将学习如何使用协议在类和结构体中指定共同特性,使用扩展扩展内置类的功能,以及处理程序中的错误。

第八章:第八章:协议、扩展和错误处理

在上一章中,你已经学习了如何使用类或结构体来表示复杂对象,以及如何使用枚举将相关值分组在一起。

在结束 Swift 的章节时,你将学习协议扩展错误处理。协议定义了一个蓝图,其中包含了可以被类、结构体或枚举采用的函数、属性和其他要求。扩展允许你为现有的类、结构体或枚举提供新的功能。错误处理涵盖了如何在程序中响应和恢复错误。

到本章结束时,你将能够编写自己的协议以满足你应用程序的需求,使用扩展为现有类型添加新功能,并在你的应用程序中处理错误条件而不会崩溃。

本章将涵盖以下主题:

  • 理解协议

  • 理解扩展

  • 探索错误处理

技术要求

本章的 Xcode 游乐场位于本书代码包的Chapter08文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,看看代码的实际效果:

bit.ly/3H1XWkQ

如果你希望从头开始,创建一个新的游乐场,并将其命名为Protocols, ExtensionsAndErrorHandling。你可以一边阅读一边在这个章节中输入和运行所有代码。让我们从协议开始,协议是一种指定类、结构体或枚举应该具有的属性和方法的方式。

理解协议

协议就像蓝图,决定了对象应该具有哪些属性或方法。在你声明了一个协议之后,类、结构体和枚举可以采用这个协议,并为所需的属性和方法提供自己的实现。

下面是一个协议声明的样子:

protocol ProtocolName {
   var readWriteProperty1 {get set}
   var readOnlyProperty2 {get}
   methodName1()
   methodName2()
}

就像类和结构体一样,协议名称以大写字母开头。属性需要使用var关键字声明。如果你想创建一个可读写的属性,使用{get set},如果你想创建一个只读属性,使用{get}。请注意,你只需指定属性和方法名称。实现是在采用该协议的类、结构体或枚举内部完成的。

重要信息

更多关于协议的信息,请访问:docs.swift.org/swift-book/LanguageGuide/Protocols.html

为了帮助你理解协议,想象一个快餐店使用的应用程序。管理层已经决定显示所提供餐点的卡路里含量。该应用程序目前有以下类、结构体和枚举,它们都没有实现卡路里含量:

  • 一个Burger

  • 一个Fries结构体

  • 一个Sauce枚举

将以下代码添加到您的 playground 中,以声明Burger类、Fries结构和Sauce枚举:

class Burger {
}
struct Fries {
}
enum Sauce { 
   case chili 
   case tomato
}

这些代表应用中现有的类、结构和枚举。不用担心空定义,因为它们对于本课程不是必需的。如您所见,它们目前都没有卡路里计数。让我们看看如何使用协议来指定实现卡路里计数所需的属性和方法。您将在下一节中声明一个指定所需属性和方法的协议。

创建协议声明

让我们创建一个指定所需属性calories和所需方法description()的协议。在类、结构和枚举声明上方输入以下内容到您的 playground 中:

protocol CalorieCount {
   var calories: Int { get }
   func description() -> String
}

此协议命名为CalorieCount。它指定了采用它的任何对象都必须有一个属性calories,用于存储卡路里计数,以及一个返回字符串的方法description(){ get }表示您只需要能够读取存储在calories中的值,而不需要写入它。请注意,description()方法的定义没有指定,因为这将在类、结构或枚举中完成。您要采用协议,只需在类名后跟一个冒号,然后跟协议名,并实现所需的属性和方法。

要使Burger类符合此协议,按照以下方式修改您的代码:

class Burger: CalorieCount {
   let calories = 800
   func description() -> String {
      return "This burger has \(calories) calories"
   }
}

如您所见,calories属性和description()方法已被添加到Burger类中。尽管协议指定了一个变量,但在这里您可以使用常量,因为协议只要求您能够获取calories的值,而不需要设置它。

让我们将Fries结构也采用这个协议。按照以下方式修改您的Fries结构代码:

struct Fries: CalorieCount {
   let calories = 500
   func description() -> String {
      return "These fries have \(calories) calories"
   }
}

用于Burger类的相同过程也用于Fries结构,现在它也符合CalorieCount协议。

您也可以以同样的方式修改Sauce枚举,但让我们使用扩展来完成它。扩展扩展了一个现有类的功能。您将在下一节中使用扩展将CalorieCount协议添加到Sauce枚举中。

理解扩展

扩展允许您在不修改原始对象定义的情况下为对象提供额外的功能。您可以在苹果提供的对象上使用它们(您没有访问对象定义的权限)或者当您希望将代码分离以提高可读性和易于维护时使用它们。以下是一个扩展的示例:

class ExistingType {
   property1
   method1()
}
extension ExistingType : ProtocolName {
   property2
   method2()
}

在这里,扩展被用来为一个现有的类提供额外的属性和方法。

重要信息

想要了解更多关于扩展的信息,请访问docs.swift.org/swift-book/LanguageGuide/Extensions.html

让我们看看如何使用扩展。你将首先通过在下一节中使用扩展使Sauce枚举遵循CalorieCount协议。

通过扩展采用协议

目前,Sauce枚举不遵循CalorieCount协议。你将使用扩展来添加使其遵循所需的属性和方法。在Sauce枚举声明之后输入以下代码:

enum Sauce {
   case chili 
   case tomato
}
extension Sauce: CalorieCount {
   var calories: Int {
      switch self {
      case .chili:
return 20 
case .tomato: 
         return 15
      }
   }
   func description() -> String {
      return "This sauce has \(calories) calories"
   }
} 

如您所见,没有对Sauce枚举的原始定义进行任何更改。如果你想要扩展现有的 Swift 标准类型,如StringInt,这同样非常有用。

枚举不能有存储属性,因此使用switch语句根据枚举的值返回卡路里数,使用self关键字。description()方法与Burger类和Fries结构中的方法相同。

到目前为止,所有三个对象都有一个calories属性和一个description()方法。太棒了!

让我们看看如何将它们放入一个数组中,并执行一个操作来获取一顿饭的总卡路里数。

创建不同类型对象的数组

通常,数组的元素必须是同一类型。然而,由于Burger类、Fries结构和Sauce枚举都遵循CalorieCount协议,你可以创建一个包含遵循此协议的元素的数组。按照以下步骤操作:

  1. 要将Burger类的实例、Fries结构和Sauce枚举添加到数组中,在所有协议和对象声明之后,输入以下代码:

    let burger = Burger()
    let fries = Fries()
    let sauce = Sauce.tomato
    let foodArray: [CalorieCount] = [burger, fries, sauce]
    
  2. 要获取总卡路里数,在创建foodArray常量的行之后添加以下代码:

    var totalCalories = 0
    for food in foodArray {
       totalCalories += food.calories
    }
    print(totalCalories)
    

    for循环遍历foodArray数组中的每个元素。对于每次迭代,每个食物项目的calories属性中的值将添加到totalCalories中,总卡路里数1315将在调试区域中显示。

你已经学会了如何创建一个协议,并使类、结构或枚举遵循它,无论是在类定义中还是在扩展中。接下来,让我们看看错误处理,它探讨了如何响应或从程序中的错误中恢复。

探索错误处理

当你编写应用程序时,请记住可能会发生错误条件,错误处理是应用程序如何响应和从这些条件中恢复的方式。

首先,你创建一个符合 Swift 的Error协议的类型,这使得该类型可用于错误处理。枚举通常用于此目的,因为你可以为不同类型的错误指定关联值。当发生意外情况时,你可以通过抛出错误来停止程序执行。你使用throw语句来做这件事,并提供一个符合Error协议的类型的实例,并带有适当的值。这允许你看到出了什么问题。

当然,如果您能够在不停止程序的情况下响应错误,那就更好了。为此,您可以使用 do-catch 块,其外观如下:

do {
   try expression1
   statement1
} catch {
   statement2
}

在这里,您尝试使用 try 关键字在 do 块中执行代码。如果抛出错误,则执行 catch 块中的语句。您可以有多个 catch 块来处理不同的错误类型。

重要信息:

有关错误处理的更多信息,请访问 docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html

例如,假设您有一个需要访问网页的应用程序。然而,如果该网页所在的服务器宕机,编写处理错误(例如尝试备用网页服务器或通知用户服务器已宕机)的代码就取决于您了。

让我们创建一个符合 Error 协议的枚举,当发生错误时使用 throw 语句停止程序执行,并使用 do-catch 块来处理错误。按照以下步骤操作:

  1. 将以下代码输入到您的游乐场中:

    enum WebsiteError: Error {
       case noInternetConnection
       case siteDown
       case wrongURL
    }
    

    这声明了一个采用 Error 协议的枚举,WebsiteError。它涵盖了三种可能的错误条件:没有互联网连接、网站不可用或 URL 无法解析。

  2. WebpageError 声明后输入以下代码以声明一个函数,该函数检查网站是否可用:

    func checkWebsite(siteUp: Bool) throws -> String {
       if siteUp == false {
         throw WebsiteError.siteDown
       }
       return "Site is up"
    }
    

    如果 siteUptrue,则返回 "Site is up"。如果 siteUpfalse,程序将停止执行并抛出错误。

  3. 在函数声明后输入以下代码以调用您的函数,并运行您的程序:

    let siteStatus = true
    try checkWebsite(siteUp: siteStatus)
    

    由于 siteStatustrueSite is up 将出现在结果区域。

  4. siteStatus 的值更改为 false 并运行您的程序。您的程序崩溃,并在调试区域显示以下错误消息:

    Playground execution terminated: An error was thrown and was not caught:
    __lldb_expr_5.WebsiteError.siteDown
    
  5. 当然,如果您能够在不使程序崩溃的情况下处理错误,那就更好了。您可以通过使用 do-catch 块来实现这一点。按照以下方式修改您的代码并运行它:

    let siteStatus = false
    do block tries to execute the checkWebsite(siteUp:) function and prints the status if successful. If there is an error, instead of crashing, the statements in the catch block are executed, and the error message siteDown appears in the Debug area.TipYou can make your program handle different error conditions by implementing multiple `catch` blocks. See this link for details: [`docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html`](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html).
    

您已经学会了如何在您的应用程序中处理错误而不会使其崩溃。给自己鼓掌吧;您已经完成了这本书的第一部分!

摘要

在本章中,您学习了如何编写协议以及如何使类、结构和枚举符合这些协议。您还学习了如何通过使用扩展来扩展类的功能。最后,您学习了如何使用 do-catch 块来处理错误。

现在可能看起来相当抽象且难以理解,但正如您将在本书的 第三部分 中看到的,您将使用协议来实现程序不同部分中的常见功能,而不是一遍又一遍地编写相同的程序。您将看到扩展在组织代码方面的有用性,这使得维护变得容易。最后但同样重要的是,您将看到良好的错误处理如何使您能够轻松地定位在编写应用程序时犯下的错误。

在下一章中,你将通过使用故事板来创建应用程序的界面,开始编写你的第一个 iOS 应用程序,这允许你快速原型化一个应用程序,而无需编写大量代码。

第二部分:设计

欢迎来到本书的第二部分。到目前为止,你已经熟悉了 Xcode 的用户界面,并且对使用 Swift 有了坚实的基础。在本部分,你将开始创建一个名为Let's Eat的餐厅预订应用的用户界面。你将使用 Interface Builder 来构建应用将使用的屏幕,向其中添加按钮、标签和字段等元素,并通过 segues 将它们连接起来。正如你将看到的,你可以用最少的编码来完成这些工作。

本部分包括以下章节:

  • 第九章**,设置用户界面

  • 第十章**,构建你的用户界面

  • 第十一章**,完成你的用户界面

  • 第十二章**,修改和配置单元格

到本部分的结束时,你将能够在 iOS 模拟器中导航你应用的各个屏幕,并且将知道如何原型化你自己的应用的用户界面。让我们开始吧!

第九章:第九章:设置用户界面

在本书的第一部分中,你学习了 Swift 语言及其工作原理。现在你对这门语言有了很好的了解,你可以学习如何开发 iOS 应用。在本部分,你将构建一个餐厅预订应用Let's Eat的用户界面。你将使用 Xcode 的Interface Builder,并将编码保持到最小。

你将从这个章节开始学习 iOS 应用开发中使用的有用术语,这些术语在本书中被广泛使用。接下来,你将浏览Let's Eat应用中使用的屏幕,并学习用户如何使用该应用。最后,你将开始使用 Interface Builder 重新创建应用的 UI,从标签栏开始,允许用户在探索地图屏幕之间进行选择。你将在两个屏幕的顶部添加导航栏。你还将学习如何配置应用启动时显示的启动屏幕,以及如何为启动屏幕和标签栏按钮使用自定义图标。

到本章结束时,你将学习到 iOS 应用开发中常用的术语,了解你的应用流程将如何,以及如何向应用添加资源,以及如何使用 Interface Builder 添加、配置和定位 UI 元素。

以下内容将涵盖:

  • 学习 iOS 开发中的有用术语

  • 浏览Let's Eat应用

  • 创建新的 Xcode 项目

  • 设置标签栏控制器场景

  • 设置启动屏幕

技术要求

在本章中,你将创建一个新的 Xcode 项目,名为LetsEat

本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter09文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,看看代码的实际效果:

bit.ly/3qcB2kO

在创建项目之前,你将学习一些在 iOS 开发中常用的术语。

学习 iOS 开发中的有用术语

当你开始 iOS 应用开发的旅程时,你将遇到特殊的术语和定义。以下是一些最常用的术语和定义。现在先阅读一下。即使你现在可能并不完全理解,但随着你的学习,它们会变得更加清晰:

  • UIView类或其子类。你屏幕上看到的一切(按钮、文本字段、标签等)都是一个视图。你将使用视图来构建你的 UI。

  • UIStackView类,它是UIView的子类。它用于将视图组合成水平或垂直堆叠,这使得它们更容易使用自动布局进行定位,这在本节后面将进行描述。

  • UIViewController 类。它决定了视图向用户显示的内容,以及用户与视图交互时会发生什么。每个视图控制器都有一个 view 属性,其中包含对视图的引用。

  • UITableViewController 类,它是 UIViewController 类的子类。它的 view 属性指向一个 UITableView 实例(UITableViewCell 实例,表格视图单元)。

    设置应用以表格视图的形式显示您的设备设置:

图 9.1:设置应用

图 9.1:设置应用

正如您所看到的,所有不同的设置(通用辅助功能隐私等)都在表格视图的表格单元中显示。

  • UICollectionViewController 类,它是 UIViewController 类的子类。它的 view 属性指向一个 UICollectionView 实例(UICollectionViewCell 实例,集合视图单元)。

    照片应用以集合视图的形式显示照片:

图 9.2:照片应用

图 9.2:照片应用

正如您所看到的,缩略图在集合视图的集合视图单元中显示。

  • UINavigationController 类,它是 UIViewController 类的子类。它有一个 viewControllers 属性,包含一个视图控制器数组。数组中最后一个视图控制器的视图显示在屏幕上,同时在屏幕顶部还有一个导航栏。

    设置应用中的表格视图控制器嵌入在导航控制器中,您可以看到表格视图上方的导航栏:

图 9.3:设置应用中的导航栏

图 9.3:设置应用中的导航栏

当您点击设置时,该设置的视图控制器将被添加到分配给 viewControllers 属性的视图控制器数组中。用户会看到该视图控制器从右侧滑入。注意屏幕顶部的导航栏,它可以包含标题和按钮。一个 < 设置 按钮出现在导航栏的左上角。点击此按钮将返回到上一个屏幕。

  • UITabBarController 类,它是 UIViewController 类的子类。它有一个 viewControllers 属性,包含一个视图控制器数组。数组中第一个视图控制器的视图显示在屏幕上,同时还有一个底部带有按钮的标签栏。最左边的按钮对应于数组中的第一个视图控制器,并且已经被选中。当您点击另一个按钮时,相应的视图控制器将被加载,其视图将显示在屏幕上。

    照片应用使用标签栏控制器在屏幕底部显示一行按钮:

图 9.4:照片应用中的标签栏

图 9.4:照片应用中的标签栏

点击标签栏中的每个按钮将显示不同的屏幕。

  • 模型-视图-控制器 (MVC):这是一个在 iOS 应用开发中非常常见的模式。用户与屏幕上的视图进行交互。应用数据存储在数据模型对象中。控制器管理视图和数据模型对象之间的信息流。它将在第十三章中详细讨论,开始使用 MVC 和集合视图

  • 第一章中创建的 Exploring Xcode 项目中,点击 Main 故事板文件:

图 9.5:探索 Xcode 项目,显示主故事板文件

图 9.5:探索 Xcode 项目,显示主故事板文件

你将看到一个场景,当你你在 iOS 模拟器中运行你的应用时,这个场景的内容将显示在屏幕上。你可以在故事板文件中拥有多个场景。

  • ExploringXcode 项目在其故事板中只有一个场景,因此没有任何转换,但你将在本章的后续部分看到它们。

  • 自动布局:作为一名开发者,你必须确保你的应用在不同屏幕尺寸的设备上看起来都很好。自动布局帮助你根据你指定的约束来布局你的用户界面。例如,你可以设置一个约束来确保按钮无论屏幕大小如何都居中显示,或者当设备从纵向旋转到横向时,使文本字段扩展。

现在你已经熟悉了 iOS 应用开发中使用的术语,让我们浏览一下你将要构建的应用。

浏览 Let's Eat 应用

让我们快速浏览一下你将要构建的应用。Let's Eat 应用是一个餐厅应用,允许用户探索按菜系分类的餐厅列表或查看显示特定区域内所有餐厅的地图。你将在下一节中看到应用中使用的所有屏幕及其整体流程。

小贴士

你可以通过这个链接看到这个应用浏览的视频版本:bit.ly/3G0Pv7U

使用探索屏幕

当应用启动时,你会看到探索屏幕:

图 9.6:探索屏幕

图 9.6:探索屏幕

让我们研究这个屏幕的不同部分。

屏幕底部的 UITabBar 实例(标签栏)显示包含位于屏幕顶部的位置按钮的 UICollectionReusableView 实例(部分标题)。

在你选择菜系之前,你必须通过点击位置按钮来选择一个位置。

使用位置屏幕

当你点击位置按钮时,你会看到位置屏幕:

图 9.7:位置屏幕

图 9.7:位置屏幕

让我们研究这个屏幕的不同部分。

屏幕顶部有一个包含取消完成按钮的导航栏。一个表格视图显示表格单元格中的位置列表。

您必须点击一行来选择位置,并点击完成按钮来确认。一旦点击完成,您将返回到探索屏幕,然后可以选择一种菜系。您也可以点击取消返回到探索屏幕而不选择位置。

使用餐厅列表屏幕

一旦设置了位置(本例中为ASPEN, CO),您可以点击一个菜系。这将显示餐厅列表屏幕:

图 9.8:餐厅列表屏幕

图 9.8:餐厅列表屏幕

让我们研究一下这个屏幕的不同部分。

屏幕顶部的导航栏包含一个返回按钮。一个集合视图在该位置显示提供所选菜系的餐厅列表,在集合视图中显示。

您必须点击一个餐厅来查看其详情。您也可以点击返回按钮返回到探索屏幕而不选择餐厅。

使用餐厅详情屏幕

点击餐厅列表屏幕上的餐厅会显示该餐厅在餐厅详情屏幕上的详细信息:

图 9.9:餐厅详情屏幕

图 9.9:餐厅详情屏幕

让我们研究一下这个屏幕的不同部分。

屏幕顶部的导航栏包含一个显示位置(本例中为ASPEN, CO)的按钮。一个表格视图在表格视图中显示餐厅的位置、评分、顾客评论、照片评论和位置地图。

您可以点击ASPEN, CO按钮返回到餐厅列表屏幕,或点击添加评论添加照片按钮来显示评论表单照片滤镜屏幕。

使用查看表单屏幕

点击添加评论按钮会显示评论表单屏幕:

图 9.10:评论表单屏幕

图 9.10:评论表单屏幕

让我们研究一下这个屏幕的不同部分。

屏幕顶部的导航栏包含取消保存按钮。一个表格视图在表格视图中显示评分和文本字段。

您可以在该屏幕上为餐厅设置评分并撰写评论。然后,您可以点击保存按钮来保存您的评分和评论,或点击取消按钮不保存直接返回餐厅详情屏幕。

使用照片滤镜屏幕

点击添加照片按钮会显示照片滤镜屏幕:

图 9.11:照片滤镜屏幕

图 9.11:照片滤镜屏幕

让我们研究一下这个屏幕的不同部分。

屏幕顶部的导航栏包含取消相机保存按钮。一个图像视图显示一张图片,一个集合视图在集合视图中显示照片滤镜。

您可以在该屏幕上选择一张图片并对其应用滤镜。然后,您可以点击保存按钮来保存您的图片,或点击取消按钮不保存直接返回餐厅详情屏幕。

使用地图屏幕

点击标签栏中的 地图 按钮将显示 地图 屏幕:

图 9.12:地图屏幕

图 9.12:地图屏幕

让我们研究这个屏幕的不同部分。

屏幕底部的一个标签栏显示 MKMapView 实例(地图视图),在屏幕上显示地图,并用图钉指示餐厅位置。

点击一个图钉将显示一个注释,点击注释中的按钮将显示该餐厅的 餐厅详情 屏幕。

这完成了应用的浏览。现在,是时候开始构建您应用的 UI 了!

创建一个新的 Xcode 项目

既然您已经知道了应用屏幕的样式,您就可以开始构建您的应用了。让我们先创建一个新的项目。这与您在 第一章熟悉 Xcode 中创建 ExploringXcode 项目所使用的相同过程。按照以下步骤操作:

  1. 启动 Xcode 并点击 创建一个新的 Xcode 项目

  2. iOS 应已选中。选择 App 并点击 下一步

  3. LetsEat

    com. 后跟您的名字

    Storyboard

    将其余设置保留为默认值。点击 下一步

  4. 选择一个位置来保存您的项目并点击 创建

  5. 您将使用 iPhone SE (第 2 代) iOS 模拟器作为测试设备。在 方案 菜单中,选择 iPhone SE (第 2 代) 模拟器。

构建并运行您的应用。您将看到一个空白白色屏幕。如果您在项目导航器中点击 Main 故事板文件,您将看到它包含一个包含空白视图的单个场景。这就是为什么您运行应用时只看到一个空白白色屏幕的原因。

要配置 UI,您将使用 Interface Builder 修改 Main 故事板文件。Interface Builder 允许您添加和配置场景。每个场景代表用户将看到的屏幕。您可以将 UI 对象,如视图和按钮,添加到场景中,并使用 属性检查器 配置它们。

重要信息

有关如何使用 Interface Builder 的更多信息,请访问此链接:help.apple.com/xcode/mac/current/#/dev31645f17f

现在您已经创建了项目,您将向其中添加一个标签栏控制器场景。这个场景在屏幕底部显示一个标签栏,其中包含两个标签。点击一个标签将显示与之关联的屏幕。这些屏幕对应于应用浏览中显示的 探索地图 屏幕。让我们在下一节中看看如何做到这一点。

设置标签栏控制器场景

如您在应用浏览中看到的,Let's Eat 应用在屏幕底部有一个标签栏,其中包含两个按钮,用于显示 ViewController Swift 文件,并将带有两个按钮的标签栏控制器场景添加到项目中。按照以下步骤操作:

  1. 在项目导航器中点击 Main 故事板文件:![图 9.14:已选择 Main 故事板文件的项目导航器 图片

    图 9.14:项目导航器,已选择主故事板文件

  2. Main故事板文件的内容出现在编辑区域。如果存在,点击文档大纲按钮以折叠文档大纲。这为你提供了更多的工作空间:![图 9.15:显示文档大纲按钮的编辑区域 图片

    图 9.15:显示文档大纲按钮的编辑区域

  3. 点击+按钮打开:![图 9.16:显示库按钮的工具栏 图片

    图 9.16:显示库按钮的工具栏

    库允许你选择要添加到场景中的 UI 对象。

  4. 在过滤器字段中输入tabbar。一个Tab Bar Controller对象将出现在结果列表中:![图 9.17:选中标签栏控制器对象的库 图片

    图 9.17:选中标签栏控制器对象的库

  5. Tab Bar Controller对象拖动到故事板中,以添加一个新的标签栏控制器场景。如果它覆盖了现有的视图控制器场景,那也是可以的。你可以看到它由一个场景组成,其中包含两个箭头,代表通向两个更多场景的转换:![图 9.18:添加了标签栏控制器场景的主故事板文件 图片

    图 9.18:添加了标签栏控制器场景的主故事板文件

  6. 点击-按钮以缩小视图,并重新排列故事板中的场景,以便标签栏控制器场景和视图控制器场景都可见:![图 9.19:显示缩小按钮的编辑区域 图片

    图 9.19:显示缩小按钮的编辑区域

  7. 按照所示选择指向视图控制器场景的箭头。这个箭头决定了你的应用的初始视图控制器场景,使得应用启动时视图出现:![图 9.20:编辑区域,箭头显示初始视图控制器场景 图片

    图 9.20:编辑区域,箭头显示初始视图控制器场景

  8. 按照所示,从视图控制器场景拖动箭头到标签栏控制器场景。这使得标签栏控制器场景成为初始场景,当你启动应用时会出现标签栏:![图 9.21:编辑区域,标签栏控制器场景作为初始视图控制器 图片

    图 9.21:编辑区域,标签栏控制器场景作为初始视图控制器

    小贴士

    你也可以通过选择它,点击属性检查器按钮,并勾选is Initial View Controller复选框,将标签栏控制器场景设置为初始视图控制器场景。你将在下一节中了解更多关于属性检查器的内容。

  9. 在故事板中选择现有的视图控制器场景,然后在键盘上按Delete键将其删除,因为你不会在这个项目中使用它。

  10. 在项目导航器中选择ViewController文件,然后在键盘上按Delete键将其删除,因为您不会在这个项目中使用它:图 9.22:项目导航器显示要删除的文件

    图 9.22:项目导航器显示要删除的文件

  11. 点击弹出的对话框中的移动到废纸篓

图 9.23:移动到废纸篓对话框

图 9.23:移动到废纸篓对话框

在 iOS 模拟器中构建并运行您的应用,您将在屏幕底部看到带有两个按钮的标签栏:

图 9.24:iOS 模拟器显示带有两个按钮的标签栏

图 9.24:iOS 模拟器显示带有两个按钮的标签栏

您已成功将标签栏添加到项目中,但如您所见,按钮标题目前为项目 1项目 2。您将在下一节中将它们更改为探索地图

设置标签栏按钮的标题

您的应用现在在屏幕底部显示了一个标签栏,但按钮标题与应用导览中显示的不匹配。为了使它们匹配,您将在属性检查器中将按钮标题配置为探索地图。按照以下步骤操作:

  1. 在项目导航器中点击Main故事板文件。点击文档大纲按钮以显示文档大纲。在文档大纲中点击项目 1 场景图 9.25:文档大纲显示选中的项目 1 场景

    图 9.25:文档大纲显示选中的项目 1 场景

  2. 点击项目 1 场景下的项目 1按钮。点击属性检查器按钮:图 9.26:属性检查器被选中

    图 9.26:属性检查器被选中

  3. 探索下:图 9.27:属性检查器标题设置为探索

    图 9.27:属性检查器标题设置为探索

  4. 点击地图

图 9.28:属性检查器标题设置为地图

图 9.28:属性检查器标题设置为地图

在模拟器中构建并运行您的应用。您会看到按钮的标题已分别更改为探索地图。太棒了!

点击探索地图按钮将显示探索地图屏幕的场景。如应用导览所示,如果您在探索屏幕上点击位置按钮,您将在位置屏幕的顶部看到一个包含取消完成按钮的导航栏。您还会在地图屏幕的顶部看到一个空白的导航栏。如您在应用导览中所见,一些屏幕在导航栏中有标题和按钮。在下一节中,您将学习如何向您的屏幕添加导航栏,这样您就可以根据需要稍后添加按钮和标题。

在导航控制器中嵌入视图控制器

正如您在应用游览中看到的,探索地图 屏幕在屏幕顶部都有一个导航栏。要为两个屏幕添加导航栏,您需要将 探索地图 场景的视图控制器嵌入到导航控制器中。这将使得当 探索地图 屏幕显示时,导航栏会出现在屏幕顶部。按照以下步骤操作:

  1. 在文档大纲中点击 探索场景:![图 9.29:已选择探索场景的文档大纲 图片

    图 9.29:已选择探索场景的文档大纲

  2. 选择 编辑器 | 嵌入 | 导航控制器:![图 9.30:已选择嵌入 | 导航控制器的编辑器菜单 图片

    图 9.30:已选择嵌入 | 导航控制器的编辑器菜单

  3. 一个导航控制器场景出现在 标签栏控制器场景探索场景 之间:

![图 9.31:显示已添加导航控制器场景的编辑区域图片

图 9.31:显示已添加导航控制器场景的编辑区域

构建并运行您的应用。现在 探索 屏幕上有一个导航栏,但由于它与背景颜色相同,所以在屏幕上不明显。

在导航控制器中嵌入视图控制器会将该视图控制器添加到导航控制器的 viewControllers 数组中。然后导航控制器在屏幕上显示视图控制器的视图。导航控制器还会在屏幕顶部显示导航栏。

地图 屏幕目前还没有导航栏。现在让我们添加一个。按照以下步骤操作:

  1. 在文档大纲中点击 地图场景:![图 9.32:已选择地图场景的文档大纲 图片

    图 9.32:已选择地图场景的文档大纲

  2. 选择 编辑器 | 嵌入 | 导航控制器

  3. 一个导航控制器场景出现在 标签栏控制器场景地图场景 之间:

![图 9.33:显示已添加导航控制器场景的编辑区域图片

图 9.33:显示已添加导航控制器场景的编辑区域

构建并运行您的应用。现在探索和地图屏幕都有导航栏,尽管您现在看不到它们。

标签栏按钮可以在 探索地图 屏幕之间切换,并且每个屏幕现在都有一个导航栏。按钮标题是正确的,但按钮本身没有图标。要获取按钮图标,您将在下一节中添加包含项目所需所有图形资源的文件。

添加 Assets.xcassets 文件

Assets.xcassets 文件包含项目资源,例如应用图标和自定义图片。由于您刚刚创建了此项目,它目前是空的。如果您还没有这样做,您需要从以下链接下载 Assets.xcassets 文件(它包含 Let's Eat 应用程序的所有资源):

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

下载文件后,您可以按照以下步骤将其添加到项目中:

  1. 您必须从项目中删除现有的 Assets.xcassets 文件(在项目导航器中显示为Assets),选择它并在项目导航器中按 Delete 键删除:图 9.34:项目导航器显示要删除的文件

    图 9.34:项目导航器显示要删除的文件

  2. 在弹出的对话框中点击移动到废纸篓

  3. 打开您下载的代码包文件中的 Chapter09 文件夹。您将看到其中的 Assets.xcassets图 9.35:下载的代码包中的 Assets.xcassets 文件

    图 9.35:下载的代码包中的 Assets.xcassets 文件

  4. 将新的 Assets.xcassets 文件拖动到项目导航器区域。将出现选择添加这些文件的选项对话框。勾选如果需要则复制项目复选框。勾选创建组单选按钮。将其他设置保留为默认值。点击完成图 9.36:选择添加这些文件的选项对话框

    图 9.36:选择添加这些文件的选项对话框

  5. Assets.xcassets 文件已添加到您的项目中。请注意,它在项目导航器中显示为 Assets。点击它以查看其内容:

图 9.37:已选择 Assets.xcassets 的项目导航器

图 9.37:已选择 Assets.xcassets 的项目导航器

包含在图形资源中的图标包括标签栏按钮的图标。您将在下一节中添加探索地图按钮的图标。

添加探索和地图按钮的图标

Assets.xcassets 文件夹。按照以下步骤将它们添加到按钮中:

  1. 点击 Main 故事板文件。在文档大纲中的探索场景下点击探索按钮。点击属性检查器按钮:图 9.38:已选择的探索按钮的属性检查器

    图 9.38:已选择的探索按钮的属性检查器

  2. icon-explore-on 下:图 9.39:属性检查器中图像设置为 icon-explore-on

    图 9.39:属性检查器中图像设置为 icon-explore-on

  3. 点击 icon-map-on

图 9.40:属性检查器中图像设置为 icon-map-on

图 9.40:属性检查器中图像设置为 icon-map-on

构建并运行您的应用。您现在可以看到探索和地图按钮现在有了图标:

图 9.41:iOS 模拟器显示带有图标的探索和地图按钮

图 9.41:iOS 模拟器显示带有图标的探索和地图按钮

恭喜!您刚刚已为您应用配置了标签栏!

当你的应用启动时,你可能会在看到标签栏之前短暂地看到一个白色屏幕。这个屏幕被称为启动屏幕,当你的应用启动时会短暂显示。你将在下一节学习如何配置这个屏幕以显示自定义颜色和应用程序徽标。

设置启动屏幕

你项目中的LaunchScreen故事板文件。当你创建 Xcode 项目时,此文件会自动创建。

你将为这个屏幕创建一个新的自定义颜色,从Assets.xcassets文件夹中添加一个图标,并使用自动布局约束设置图标的定位。

重要信息

有关自动布局及其使用方法的更多信息,请参阅此链接:developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/.

你将在下一节创建一个新的自定义颜色。

配置启动屏幕的背景颜色

属性检查器可以用来修改屏幕上 UI 元素的颜色。你可以通过使用自定义的红色、绿色和蓝色值来指定你想要的精确颜色。你将通过以下步骤为启动屏幕设置自定义颜色:

  1. 在你的项目导航器中点击Launchscreen故事板文件:图 9.42:选择 LaunchScreen 故事板的项目导航器

    图 9.42:选择 LaunchScreen 故事板的项目导航器

  2. 在文档大纲中选择视图。点击属性检查器按钮。在视图下,点击背景弹出菜单:图 9.43:选择背景属性的属性检查器

    图 9.43:选择背景属性的属性检查器

  3. 从弹出菜单中选择自定义...图 9.44:选择自定义...的背景弹出菜单

    图 9.44:选择自定义...的背景弹出菜单

  4. 在颜色选择器中,选择第二个标签(带有三个滑块的标签):图 9.45:选择第二个标签的颜色选择器

    图 9.45:选择第二个标签的颜色选择器

  5. 十六进制颜色 # 框中选中4A4A4A

图 9.46:显示十六进制颜色#设置为 4A4A4A 的颜色选择器

图 9.46:显示十六进制颜色#设置为 4A4A4A 的颜色选择器

颜色由红色、绿色和蓝色组成。每种颜色的值范围从0255。这个十六进制值将红色、绿色和蓝色值设置为74,从而得到一种令人愉悦的深灰色。

构建并运行你的应用。你应该在标签栏出现之前短暂地看到一个深灰色屏幕。酷!

在下一节中,你将为这个屏幕添加一个徽标,并使用自动布局约束将其定位在屏幕的精确中心,无论设备类型和方向如何。添加徽标会告知用户应用正在启动,这是让你的应用看起来不错的一种方式。

将 logo 和约束添加到启动屏幕

你之前添加到项目中的Assets.xcassets文件。你还将使用自动布局约束将 logo 放置在屏幕中央。按照以下步骤操作:

  1. 在项目导航器中应仍然选择LaunchScreen故事板文件。点击+按钮以显示库:图 9.47:显示库按钮的工具栏

    图 9.47:显示库按钮的工具栏

  2. 点击媒体按钮以显示项目中所有的图形文件:图 9.48:选择媒体按钮的库

    图 9.48:选择媒体按钮的库

  3. 在过滤器字段中输入detail。你将在结果中看到detail-logo图 9.49:选择 detail-logo 的库

    图 9.49:选择 detail-logo 的库

  4. detail-logo拖动到你的视图控制器场景的视图中,并垂直和水平居中。你会看到蓝色辅助线来帮助你。完成操作后,点击自动布局对齐按钮:图 9.50:添加了 logo 的 Launchscreen 故事板视图控制器场景

    图 9.50:添加了 logo 的 Launchscreen 故事板视图控制器场景

    小贴士

    如果你没有看到自动布局对齐按钮,请点击工具栏中的检查器按钮以隐藏检查器区域。

  5. 选择在容器中水平对齐在容器中垂直对齐。点击添加 2 个约束图 9.51:自动布局对齐弹出对话框

    图 9.51:自动布局对齐弹出对话框

  6. 约束已添加到detail-logo,并在文档大纲中可见:

图 9.52:显示 logo 约束的文档大纲

图 9.52:显示 logo 约束的文档大纲

约束的作用是指定 logo 相对于视图控制器视图的位置。在这个例子中,视图控制器视图是容器。在容器中水平对齐计算 logo 相对于容器的左右边的水平位置,而在容器中垂直对齐计算 logo 相对于容器的顶部和底部的垂直位置。

构建并运行你的应用。你将在屏幕中间看到 logo。即使你在 iOS 模拟器中尝试以不同的屏幕尺寸运行应用,logo 仍然会位于屏幕的精确中心。

恭喜!你已经成功配置了你的应用启动屏幕!

你可能已经注意到,在 Interface Builder 中表示的屏幕与你在 iOS 模拟器中选择的 iPhone 型号不匹配,你可能会发现最小图显示会妨碍你在应用中排列屏幕。让我们对 Interface Builder 进行一些额外的配置以修复这个问题。

配置 Interface Builder

即使您已将 iPhone SE(第二代)配置为您的应用程序的 iOS 模拟器,您可能会发现 Interface Builder 中显示的场景是为不同的 iPhone 型号。您还可能希望隐藏最小地图显示。让我们配置 Interface Builder 以使用 iPhone SE(第二代)的屏幕并隐藏最小地图显示。按照以下步骤操作:

  1. 应仍然选择Launchscreen故事板文件。要配置 Interface Builder,请单击设备配置按钮:图 9.53:显示设备配置按钮的编辑区域

    图 9.53:显示设备配置按钮的编辑区域

  2. 将出现显示不同设备屏幕的弹出窗口:图 9.54:带有设备弹出窗口的编辑区域

    图 9.54:带有设备弹出窗口的编辑区域

    注意已选择iPhone 11

  3. 从此弹出窗口中选择iPhone SE (第二代)图 9.55:显示为 iPhone SE(第二代)选择的设备弹出窗口

    图 9.55:显示为 iPhone SE(第二代)选择的设备弹出窗口

  4. 在设备弹出窗口中设置 iPhone SE(第二代)后,请注意场景已更改以反映 iPhone SE(第二代)的屏幕。标志仍然位于启动屏幕的精确中心图 9.56:显示为 iPhone SE(第二代)选择的设备配置按钮

    图 9.56:显示为 iPhone SE(第二代)选择的设备配置按钮

  5. 在项目导航器中单击Main故事板文件。在此处配置故事板以使用 iPhone SE(第二代):图 9.57:显示为故事板文件选择 iPhone SE(第二代)的设备配置按钮

    图 9.57:显示为Main故事板文件选择 iPhone SE(第二代)的设备配置按钮

  6. 如果您想隐藏最小地图,请从 Xcode 菜单栏中选择编辑器 | 最小地图以取消选中它:图 9.58:显示 Canvas | 最小地图已选中的编辑器菜单

    图 9.58:显示 Canvas | 最小地图已选中的编辑器菜单

  7. 确认您在Main故事板文件中有以下场景:

图 9.59:显示完成的主故事板文件的编辑区域

图 9.59:显示完成的主故事板文件的编辑区域

构建并运行您的应用程序。它应该像之前一样工作。

您已经为您的应用程序创建了探索地图屏幕!做得好!

摘要

在本章中,您学习了 iOS 应用程序开发中使用的某些有用术语。这将使您更容易理解本书的其余部分,以及有关该主题的其他书籍或在线资源。

接下来,你还学习了让我们吃饭应用中使用的不同屏幕以及用户如何使用该应用。当你从头开始重新创建应用的用户界面时,你将能够将你所做的工作与实际应用的外观进行比较。

最后,你学习了如何使用界面构建器和故事板将标签栏控制器场景添加到你的应用中,配置按钮标题,并为包含项目中所需所有图形文件的Assets.xcassets文件配置导航栏。你还配置了自定义标签栏按钮图标,并使用自定义颜色和图标配置了应用的启动屏幕。这将使你熟悉为你的应用添加 UI 元素、配置它们以及设置它们的约束。

在下一章中,你将继续设置你应用的用户界面,并熟悉更多的 UI 元素。你将配置探索屏幕以显示一个展示集合视图和集合视图单元格的集合视图,以及一个包含按钮的集合视图部分标题,当按钮被点击时,会显示另一个视图。

第十章:第十章:构建您的用户界面

在上一章中,您创建了一个新的 Xcode 项目,向您的应用添加了一个标签栏,允许用户在包含您的应用资源的Assets.xcassets文件和修改应用的自定义颜色和图标之间进行选择。当您的应用启动时,您应该会看到启动屏幕短暂显示。之后,探索屏幕将显示,但目前它是空的。

如您在第九章的“设置用户界面”应用浏览中看到的那样,探索屏幕应显示一个集合视图,显示一系列在集合视图单元格中的菜系,以及包含一个位置按钮的集合视图部分标题。点击位置按钮应显示一个包含位置列表的位置屏幕。

在本章中,您将使探索屏幕显示一个包含 20 个空集合视图单元格集合视图,以及一个包含一个按钮的集合视图部分标题,当点击该按钮时,将显示表示位置屏幕的视图。您还将配置一个取消按钮来关闭此视图并返回到探索屏幕。

您将在您的应用中添加少量代码,但不必过于担心这一点——您将在本书的下一部分了解更多关于它的内容。

到本章结束时,您将学会如何将视图控制器添加到故事板场景中,将视图控制器中的出口链接到场景,设置集合视图单元格和集合视图部分标题,以及以模态方式呈现视图控制器。

在本章中,您将涵盖以下主题:

  • 探索屏幕中添加集合视图

  • 将故事板元素连接到视图控制器中的出口

  • 配置集合视图的数据源方法

  • 向集合视图中添加集合视图部分标题

  • 配置故事板元素大小

  • 以模态方式呈现视图

技术要求

您将继续在上一章中创建的LetsEat项目中工作。

本章完成的 Xcode 项目位于本书代码包的Chapter10文件夹中,可在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际效果:

bit.ly/3kjIKFQ

让我们从向探索场景添加集合视图开始,这将最终显示菜系列表和位置按钮。

在探索屏幕中添加集合视图

收集视图是UICollectionView类的实例。类似于电子表格程序,它显示一个单元格网格。收集视图中的每个单元格都是一个收集视图单元格,它是UICollectionViewCell类的实例。您将首先将收集视图添加到Main故事板文件的视图控制器场景中,然后添加自动布局约束使其填满屏幕。

重要信息

有关自动布局及其使用方法的更多信息,请访问developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/

打开上一章中创建的LetsEat项目并运行应用程序,以确保一切仍然按预期工作,然后按照以下步骤操作:

  1. 在项目导航器中点击Main故事板文件,然后点击“库”按钮:![图 10.1:显示库按钮的工具栏 图片 10.01_B17469.jpg

    图 10.1:显示库按钮的工具栏

  2. 程序库将出现。确保已选择“对象”按钮,然后在过滤器字段中输入collec。一个收集视图对象将作为结果之一出现。将其拖动到探索屏幕的视图控制器场景视图的中间:![图 10.2:已选择收集视图对象的程序库 图片 10.02_B17469.jpg

    图 10.2:已选择收集视图对象的程序库

    已添加收集视图(包含一个原型单元格),但它只占据了屏幕的一小部分。如前一章的应用程序游览中所示,它应该填满屏幕。

  3. 您将使用“自动布局添加新约束”按钮将收集视图的边缘绑定到其包含视图的边缘。确保已选择收集视图。点击“自动布局添加新约束”按钮:![图 10.3:已选择收集视图的视图控制器场景 图片 10.03_B17469.jpg

    图 10.3:已选择收集视图的视图控制器场景

  4. 在顶部、左侧、右侧和底部边缘约束字段中输入0,然后点击所有浅红色支柱。确保所有支柱都已变为亮红色。点击0,将收集视图的边缘绑定到包含视图的边缘。现在,收集视图将填满屏幕,无论设备类型和方向如何。

  5. 确认收集视图的四面现在都已绑定到屏幕的边缘,如图所示:

![图 10.5:已填满屏幕的收集视图的视图控制器场景图片 10.05_B17469.jpg

图 10.5:已填满屏幕的收集视图的视图控制器场景

您已将收集视图添加到探索屏幕的视图控制器场景视图中,并使用自动布局约束使其填满屏幕,但运行应用程序时探索屏幕仍然为空。

在下一节中,你将添加一个ExploreViewController类,并将在这个类中连接出口到ExploreViewController类中的 UI 元素,以控制Explore屏幕显示的内容。

将故事板元素连接到视图控制器中的出口

你已经在该文件中的UIViewController子类中添加了一个集合视图,并将 UI 元素连接到UIViewController子类。

重要信息

第十三章“开始使用 MVC 和集合视图”中,将更详细地解释模型-视图-控制器设计模式和集合视图控制器。

让我们先向项目中添加一个 Cocoa Touch 类文件,这样你就可以在下一节中声明和定义一个UIViewController子类。

向项目中添加 Cocoa Touch 类文件

Cocoa Touch是用于构建 iOS、iPadOS、watchOS 和 tvOS 应用的开发环境。Cocoa Touch 类文件使你能够轻松实现任何 Cocoa Touch 类或子类。它包含基于你在创建时指定的超类的样板代码。你将在本节中将 Cocoa Touch 类文件添加到你的项目中。

首先,你将在项目中创建一个新的Explore组以保持事物有序。接下来,你将创建并添加一个名为ExploreViewController的 Cocoa Touch 类文件到这个组。你将在该文件中声明和定义一个名为ExploreViewControllerUIViewController类子类,并将这个类的实例设置为Explore屏幕的视图控制器。你将向这个类添加属性和方法来管理你在上一节中添加的集合视图。按照以下步骤操作:

  1. 在项目导航器中右键点击LetsEat组,然后选择新建组

  2. 组的名称将被突出显示。完成更改后,按键盘上的Return键:![Figure 10.6: 已选择 Explore 组的项目导航器

    ![img/Figure_10.06_B17469.jpg]

    图 10.6:已选择 Explore 组的项目导航器

    如果你犯了错误,请再按一次Return键。这将使字段可编辑,以便你可以更改名称。

  3. 在项目导航器中右键点击Explore组,然后选择新建文件...

  4. iOS应该已经选中。选择Cocoa Touch Class并点击下一步:![Figure 10.7: 选择新文件的模板屏幕

    ![img/Figure_10.07_B17469.jpg]

    图 10.7:选择新文件的模板屏幕

  5. 选择新文件选项屏幕将出现:![Figure 10.8: 选择新文件选项屏幕

    ![img/Figure_10.08_B17469.jpg]

    图 10.8:选择新文件选项屏幕

  6. ExploreViewController中输入以下内容

    UIViewController

    点击下一步

  7. 点击ExploreViewController文件已添加到项目中的Explore文件夹内的项目导航器中。在编辑区域中查看代码。注意,ExploreViewControllerUIViewController的子类,这意味着它从UIViewController类继承属性和方法。在类定义中有一个方法,即viewDidLoad(),但现在不会使用它。

  8. ExploreViewController文件中的viewDidLoad()类之后删除注释代码,使其看起来像这样:

图 10.9:显示 ExploreViewController.swift 内容的编辑区域

图 10.9:显示 ExploreViewController.swift 内容的编辑区域

你刚刚将包含ExploreViewController类声明和定义的ExploreViewController文件添加到你的应用程序中。

下一步是将ExploreViewController类分配为探索屏幕的视图控制器身份,并为之前添加到视图控制器场景中的集合视图分配一个出口。你将在下一节中看到如何完成这个操作。

将故事板元素连接到视图控制器

让我们回顾一下你现在所在的位置。在Main故事板文件中,你有一个ExploreViewController文件的视图控制器场景,你有一个声明和定义ExploreViewController类的代码。

当你运行应用程序并启用你管理集合视图显示时,你需要将ExploreViewController类分配为Explore屏幕视图控制器的身份。按照以下步骤操作:

  1. 在项目导航器中点击Main故事板文件。确保已选择探索屏幕的视图控制器场景。在文档大纲中点击视图控制器图标,然后点击标识符检查器按钮:图 10.10:已选择标识符检查器

    图 10.10:已选择标识符检查器

  2. ExploreViewController下:图 10.11:将类设置为 ExploreViewController 的标识符检查器

    图 10.11:将类设置为 ExploreViewController 的标识符检查器

    当你运行应用程序时,这会创建一个ExploreViewController实例作为该场景的视图控制器。请注意,场景名称已从视图控制器场景更改为探索视图控制器场景

    现在让我们为集合视图创建出口。

  3. 点击导航器和检查器按钮以隐藏导航器和检查器区域,以便你有更多空间工作:图 10.12:显示导航器和检查器按钮的工具栏

    图 10.12:显示导航器和检查器按钮的工具栏

  4. 点击调整编辑器选项按钮:图 10.13:调整编辑器选项按钮

    图 10.13:调整编辑器选项按钮

  5. 从弹出菜单中选择助手:![图 10.14:选择“助手”调整编辑选项菜单 图片

    图 10.14:选择“助手”调整编辑选项菜单

    这将在辅助编辑器中显示与此场景关联的任何 Swift 文件。

  6. 如您所见,Main故事板文件的内容显示在编辑区域左侧,而ExploreViewController类定义显示在右侧。查看代码上方的小条形图。验证ExploreViewController.swift是否已选择:![图 10.15:显示已选择ExploreViewController.swift的条形图 图片

    图 10.15:显示已选择ExploreViewController.swift的条形图

  7. 如果您没有看到已选择ExploreViewController.swift,请单击条形图,并从弹出菜单中选择ExploreViewController.swift

  8. 要连接ExploreViewController类中的集合视图,Ctrl + 拖动从集合视图到类名声明下方ExploreViewController文件:![图 10.16:编辑区域 图片

    图 10.16:编辑区域

  9. 将会弹出一个小的弹出对话框。在名称文本框中输入出口名称collectionView,将存储设置为,然后单击连接:![图 10.17:创建出口的弹出对话框 图片

    图 10.17:创建出口的弹出对话框

  10. 验证创建collectionView出口的代码是否已自动添加到ExploreViewController文件中。注意IBOutlet关键字,它表示collectionView是一个出口。完成此操作后,单击x关闭助手编辑器窗口:

![图 10.18:显示 collectionView 出口的编辑区域图片

图 10.18:显示 collectionView 出口的编辑区域

现在,ExploreViewController类有一个出口,collectionView,用于在ExploreViewController实例中管理集合视图显示的内容。

在使用Ctrl + 拖动从故事板场景中的元素拖动到 Cocoa Touch 类文件时,容易出现错误。如果在这样做时出错,这可能会导致应用程序启动时崩溃。要检查集合视图和ExploreViewController类之间的连接是否存在错误,请按照以下步骤操作:

  1. 点击导航器和检查器按钮以显示导航器和检查器区域。

  2. 出口部分,将collectionView出口连接到集合视图。

  3. 如果您看到一个微小的错误图标,请单击x断开连接:![图 10.20:显示 collectionView 出口的连接检查器 图片

    图 10.20:显示 collectionView 出口的连接检查器

  4. 出口下,将collectionView出口从collectionView拖动回来以重新建立连接:

图 10.21:显示要连接的收集视图的编辑区域

图 10.21:显示要连接的收集视图的编辑区域

到目前为止,你已经将 ExploreViewController 类分配为收集视图的 ExploreViewController 类的身份。

为了在屏幕上显示收集视图单元格,你需要通过在 ExploreViewController 类中添加一些代码来实现收集视图的数据源方法。你将在下一节中这样做。

配置用于收集视图的数据源方法

当你的应用运行时,ExploreViewController 类的一个实例充当 UICollectionViewDataSource 的视图控制器,为此目的。你所需要做的就是将收集视图的 dataSource 输出口连接到 ExploreViewController 类,并实现该协议的所需方法。

收集视图还需要知道如果用户点击收集视图单元格时应该做什么。同样,收集视图的视图控制器负责此事,苹果为此创建了 UICollectionViewDelegate 协议。你将连接收集视图的 delegate 输出口到 ExploreViewController 类,但你现在不会实现该协议的任何方法。

提示

协议在 第八章**,协议、扩展和错误处理 中有所介绍。

你将需要在本章中输入一小段代码。不用担心它的含义;你将在 第十三章**,MVC 和收集视图入门 中了解更多关于收集视图控制器及其相关协议的内容。

在下一节中,你将使用连接检查器将收集视图的 dataSourcedelegate 输出口分配给 ExploreViewController 类。

设置收集视图的委托和数据源属性

ExploreViewController 类的一个实例将提供收集视图将显示的数据,以及用户与收集视图交互时将执行的方法。你需要将收集视图的 dataSourcedelegate 属性连接到 ExploreViewController 类的输出口,以便实现这一点。按照以下步骤操作:

  1. 如果你还没有这样做,点击导航器和检查器按钮以再次显示导航器和检查器区域。

  2. 应该仍然选择 Main 故事板文件。点击 dataSourcedelegate 输出口。从每个空圆圈拖动到文档大纲中的 ExploreViewController 图标:图 10.22:连接检查器已选择

    图 10.22:连接检查器已选择

  3. 验证收集视图的 dataSourcedelegate 属性是否已连接到 ExploreViewController 类的输出口:

![Figure 10.23: 连接检查器已设置数据源和代理输出

![Figure_10.23_B17469.jpg]

Figure 10.23: 连接检查器已设置数据源和代理输出

在下一节中,您将添加一些代码使 ExploreViewController 类遵守 UICollectionViewDataSource 协议,并在运行您的应用程序时配置集合视图以显示 20 个集合视图单元格。

采用 UICollectionViewDataSourceUICollectionViewDelegate 协议

到目前为止,您已使 ExploreViewController 类成为集合视图的数据源和代理。下一步是使其采用 UICollectionViewDataSourceUICollectionViewDelegate 协议并实现任何必需的方法。您还将更改集合视图单元格的颜色,以便在屏幕上可见。按照以下步骤操作:

  1. 在文档大纲中点击 Collection View Cell。这代表集合视图将显示的集合视图单元格。确保属性检查器已选中:![Figure 10.24: 属性检查器已选中

    ![Figure_10.24_B17469.jpg]

    Figure 10.24: 属性检查器已选中

  2. exploreCell 下按 Return 键。将名称设置为 Light Gray Color 以便在运行应用程序时可以看到它们:![Figure 10.25: 属性检查器已设置身份和背景颜色

    ![Figure_10.25_B17469.jpg]

    Figure 10.25: 属性检查器已设置身份和背景颜色

  3. 在项目导航器中点击 ExploreViewController 文件。在类声明之后输入以下代码,使 ExploreViewController 类采用 UICollectionViewDataSourceUICollectionViewDelegate 协议:

    class ExploreViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
    

    几秒钟后,将出现一个错误。点击它以显示错误信息。

  4. 错误信息表示需要使 ExploreViewController 类遵守 UICollectionViewDataSource 协议。点击 ExploreViewController 类。

  5. 验证 UICollectionViewDataSource 协议的两个必需方法的存根是否已自动插入到 ExploreViewController 文件中,如图所示:![Figure 10.26: 显示 UICollectionViewDataSource 方法存根的编辑区域

    ![Figure_10.26_B17469.jpg]

    Figure 10.26: 显示 UICollectionViewDataSource 方法存根的编辑区域

    第一种方法告诉集合视图显示多少个单元格,而第二种方法告诉集合视图在每个集合视图单元格中显示什么。

  6. 将第一个方法中的 code 文本替换为 20(如果只是一行代码,则 return 关键字是可选的)。这告诉集合视图显示 20 个单元格:![Figure 10.27: 显示 20 个单元格的代码编辑区域

    ![Figure_10.27_B17469.jpg]

    Figure 10.27: 显示 20 个单元格的代码编辑区域

  7. 将第二个方法中的 code 文本替换为以下代码:

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "exploreCell", for: indexPath)
    return cell
    

    不要担心这对你意味着什么,因为你将在 第十三章 “开始使用 MVC 和集合视图” 中学习更多关于集合视图的内容。

构建并运行你的应用。你应该看到模拟器显示一个由 20 个浅灰色集合视图单元格组成的网格,如下所示:

图 10.28:iOS 模拟器显示 20 个集合视图单元格

图 10.28:iOS 模拟器显示 20 个集合视图单元格

如你在 第九章 “设置用户界面” 的应用游览中看到的,屏幕右上角应该有一个位置按钮。你将在下一节中启用集合视图的部分标题以容纳此按钮。

向集合视图中添加部分标题

集合视图可以配置部分标题和部分页脚。它们都是 UICollectionReusableView 类的实例。你将在 探索 屏幕中启用集合视图的部分标题,因此你将有一个放置 位置 按钮的地方。按照以下步骤操作:

  1. 在项目导航器中点击 Main 故事板文件,然后在文档大纲中点击 集合视图。点击属性检查器按钮。在 集合视图 下,勾选 部分标题 复选框:图 10.29:属性检查器中已勾选部分标题复选框

    图 10.29:属性检查器中已勾选部分标题复选框

    这将启用集合视图的部分标题。

  2. 注意在完成时按 header 并按 Return图 10.30:属性检查器中标识符设置为 header

    图 10.30:属性检查器中标识符设置为 header

  3. 在项目导航器中点击 ExploreViewController 文件。在数据源方法之前,输入以下代码:

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
       let headerView =
       collectionView.dequeueReusableSupplementaryView(
       ofKind: kind, withReuseIdentifier: "header", 
       for: indexPath)
       return headerView
    }
    

    此方法返回具有 header 标识符的 UICollectionReusableView 实例,你刚刚已配置,它将在屏幕上显示。

构建并运行你的应用。你应该看到集合视图部分标题作为集合视图单元格和导航栏之间的空白区域:

图 10.31:iOS 模拟器显示集合视图部分标题

图 10.31:iOS 模拟器显示集合视图部分标题

在你添加 位置 按钮之前,你需要增加集合视图部分标题的高度和集合视图单元格的大小,以使它们与在应用游览中显示的 探索 屏幕相匹配(参考 第九章 “设置用户界面”)。你将在下一节中使用 大小检查器 设置单元格大小和标题高度。

配置故事板元素大小

大小检查器用于更改故事板元素的大小。您将使用它来更改集合视图单元格和集合视图分区标题的大小,以使它们与在第九章中应用导游中显示的探索屏幕相匹配,设置用户界面。请按照以下步骤操作:

  1. 在项目导航器中单击Main故事板文件,然后在文档大纲中单击集合视图。单击大小检查器按钮:图 10.32:大小检查器被选中

    图 10.32:大小检查器被选中

  2. 集合视图大小设置将在大小检查器中显示,如图所示:图 10.33:大小检查器显示集合视图的大小设置

    图 10.33:大小检查器显示集合视图的大小设置

  3. 配置集合视图大小设置,如下所示:

    177177

    0100

    07

    7

在更改每个值后,请记住按Return键。

大小检查器中使用的单位是点。每个点可能代表设备屏幕上的一个或多个像素。对于 iPhone SE(第二代),屏幕宽度为 375 点,高度为 667 点,尽管实际屏幕分辨率为 750 x 1,334 像素。

单元格大小确定集合视图单元格的大小。标题大小确定集合视图分区标题的大小。最小间距确定单元格之间的空间。分区内边距确定包含单元格的分区与包围视图的侧边之间的空间。这些设置特定于 iPhone SE(第二代)。在第二十二章中,使用 Mac Catalyst 入门,您将根据设备屏幕的尺寸计算最佳单元格大小。

构建并运行您的应用,您应该看到探索屏幕显示 20 个集合视图单元格和一个集合视图分区标题:

图 10.34:iOS 模拟器显示调整大小的集合视图单元格和分区标题

图 10.34:iOS 模拟器显示调整大小的集合视图单元格和分区标题

注意,尽管单元格中没有数据,标题中没有按钮,但它看起来与应用导游中在第九章**设置用户界面中显示的探索屏幕相似。您将在本书的下一部分配置单元格以显示数据。现在,让我们向集合视图分区标题添加一个按钮,该按钮将用于稍后显示位置屏幕。

以模态方式呈现视图

在本节中,您将向集合视图部分标题添加一个按钮。当点击时,此按钮将显示一个视图,显示位置屏幕。此视图将来自一个嵌入在导航控制器中的新视图控制器场景,您将将其添加到项目中。视图将以模态方式显示,这意味着在它被关闭之前,您将无法执行任何其他操作。要关闭它,您将在视图的导航栏中添加一个取消按钮。您还将添加一个完成按钮,但您将在第十七章“开始使用 JSON 文件”中实现其功能。

让我们从向集合视图标题添加库中的按钮开始。

向集合视图标题添加按钮

第九章“设置用户界面”中的应用程序导游所示,屏幕右上角有一个位置按钮。您将在集合视图部分标题中添加一个按钮来表示位置按钮。按照以下步骤操作:

  1. 在项目导航器中单击Main故事板文件。确保已选择探索视图控制器场景。单击库按钮以显示库:![图 10.35:显示库按钮的工具栏

    ![img/Figure_10.35_B17469.jpg]

    图 10.35:显示库按钮的工具栏

  2. 在过滤器字段中输入button。一个按钮对象将在结果中显示。将按钮拖动到集合视图部分标题:![图 10.36:选择按钮对象的库

    ![img/Figure_10.36_B17469.jpg]

    图 10.36:选择按钮对象的库

  3. 将按钮放置在集合视图部分标题的右侧:

![图 10.37:添加了按钮的集合视图部分标题

![img/Figure_10.37_B17469.jpg]

图 10.37:添加了按钮的集合视图部分标题

目前它的确切位置并不重要,因为您将在第十二章“修改和配置单元格”中自定义按钮的位置。

您现在在您的集合视图部分标题中有一个按钮。接下来,您将添加一个视图控制器场景来表示当按钮被点击时出现的位置屏幕。

添加新的视图控制器场景

第九章“设置用户界面”中的应用程序导游所示,当您点击位置按钮时,位置屏幕将显示一系列位置。您将向项目中添加一个新的视图控制器场景来表示此屏幕。按照以下步骤操作:

  1. 单击库按钮以显示库,并在过滤器字段中输入view con视图控制器对象将在搜索结果中。将视图控制器对象拖放到故事板中:![图 10.38:选择视图控制器对象的库

    ![img/Figure_10.38_B17469.jpg]

    图 10.38:选择视图控制器对象的库

  2. 将其放置在探索视图控制器场景的右侧:![Figure 10.39:显示视图控制器场景与探索视图控制器场景并排的编辑区域

    ![img/Figure_10.39_B17469.jpg]

    图 10.39:显示视图控制器场景与探索视图控制器场景并排的编辑区域

  3. 新增的视图控制器场景应该已经选中。在文档大纲中,点击此场景的视图控制器图标:![Figure 10.40:选择视图控制器的文档大纲

    ![img/Figure_10.40_B17469.jpg]

    图 10.40:选择视图控制器的文档大纲

  4. 您需要为取消完成按钮腾出空间,因此您将在此视图控制器场景中嵌入导航控制器,以提供一个可以放置按钮的导航栏。从编辑器菜单中选择嵌入 | 导航控制器

  5. 导航控制器场景将出现在视图控制器场景的左侧:![Figure 10.41:显示嵌入在导航控制器中的视图控制器场景的编辑区域

    ![img/Figure_10.41_B17469.jpg]

    图 10.41:显示嵌入在导航控制器中的视图控制器场景的编辑区域

  6. 使用Ctrl + 拖动从按钮到导航控制器场景:![Figure 10.42:显示探索视图控制器场景中的按钮

    ![img/Figure_10.42_B17469.jpg]

    图 10.42:显示探索视图控制器场景中的按钮

  7. 将会出现切换弹出菜单。选择以模态方式呈现:![Figure 10.43:选择“以模态方式呈现”的切换弹出菜单

    ![img/Figure_10.43_B17469.jpg]

    图 10.43:选择“以模态方式呈现”的切换弹出菜单

    这样,当按钮被点击时,视图控制器视图会从屏幕底部向上滑动。在此视图关闭之前,您将无法与其他视图进行交互。

  8. 验证是否有一个切换将探索视图控制器场景和导航控制器场景连接在一起:

![Figure 10.44:显示探索视图控制器场景之间切换的编辑区域

和导航控制器场景

![img/Figure_10.44_B17469.jpg]

图 10.44:显示探索视图控制器场景和导航控制器场景之间切换的编辑区域

构建并运行您的应用程序。如果您点击按钮,新的视图控制器视图应该从屏幕底部向上滑动:

![Figure 10.45:iOS 模拟器显示探索和位置屏幕

![img/Figure_10.45_B17469.jpg]

图 10.45:iOS 模拟器显示探索和位置屏幕

目前,您无法关闭此视图。在下一节中,您将在导航栏中添加一个取消按钮,并编程使其关闭视图。您还会添加一个完成按钮,但暂时不会对其进行编程。

向导航栏添加取消和完成按钮

将视图控制器嵌入导航控制器的一个好处是屏幕顶部的导航栏。你可以在其左右两侧放置按钮。按照以下步骤将取消完成按钮添加到导航栏:

  1. 在文档大纲中点击视图控制器场景导航项。点击库按钮:![Figure 10.46: 工具栏显示库按钮

    ![Figure_10.46_B17469.jpg]

    Figure 10.46: 显示库按钮的工具栏

  2. 在过滤器字段中输入bar b,并将两个栏按钮项对象拖到导航栏的两侧:![Figure_10.47: 库中选择了栏按钮项对象

    ![Figure_10.47_B17469.jpg]

    Figure 10.47: 库中选择了栏按钮项对象

  3. 点击右侧的项目按钮:![Figure_10.48: 选择右侧按钮的视图控制器场景

    ![Figure_10.48_B17469.jpg]

    Figure 10.48: 选择右侧按钮的视图控制器场景

  4. 点击属性检查器按钮。在栏按钮项下,从系统项菜单中选择完成:![Figure_10.49: 属性检查器,将系统项设置为完成

    ![Figure_10.49_B17469.jpg]

    Figure 10.49: 属性检查器,将系统项设置为完成

  5. 点击viewControllers,它包含一个视图控制器数组。当你点击viewControllers数组中的按钮,其视图从屏幕底部出现,覆盖探索屏幕。

  6. 要关闭视图,你需要链接ExploreViewController类,当ExploreViewController文件执行时,在文件底部,紧接最后一个花括号之前添加以下方法:

    @IBAction func unwindLocationCancel(segue: UIStoryboardSegue) {
    }
    
  7. 在项目导航器中点击Main故事板文件。Ctrl + 拖动取消按钮到场景退出图标(第三个图标),并从弹出菜单中选择unwindLocationCancelWithSegue:

![Figure_10.51: 显示取消按钮动作设置的视图控制器场景

![Figure_10.51_B17469.jpg]

Figure 10.51: 显示取消按钮动作设置的视图控制器场景

当你的应用运行时,点击viewControllers数组,使模态显示的视图消失,并执行unwindLocationCancel(segue:)方法。注意,此方法不执行任何操作。

构建并运行你的应用,点击探索屏幕部分标题中的按钮。新视图将出现在屏幕上。当你点击取消按钮时,新视图消失:

![Figure_10.52: iOS 模拟器显示探索和位置屏幕

![Figure_10.52_B17469.jpg]

Figure 10.52: iOS 模拟器显示探索和位置屏幕

恭喜!你已经完成了探索屏幕的基本结构。

摘要

在本章中,你向Main故事板文件中添加了一个集合视图,并添加了一个新文件ExploreViewController,其中包含了ExploreViewController类的实现。你将ExploreViewController类设置为包含集合视图的场景的视图控制器。然后,你修改了ExploreViewController类,使其在故事板中具有集合视图的输出,并使其成为集合视图的数据源和代理。你向集合视图中添加了一个集合视图部分标题,并设置了集合视图单元格和集合视图部分标题的大小。最后,你添加了一个按钮以显示第二个视图,并配置了一个取消按钮来关闭它。

到目前为止,你应该已经相当熟练地使用 Interface Builder 向故事板场景中添加视图和视图控制器,将视图控制器输出链接到故事板中的 UI 元素,设置集合视图单元格和部分标题,以及以模态方式显示视图。当你为自己的应用程序设计用户界面时,这将非常有用。

在下一章中,你将配置新的视图控制器以显示表格视图,实现应用程序的剩余屏幕,并为地图屏幕实现一个地图视图。

第十一章:第十一章: 完成用户界面

在上一章中,你配置了探索屏幕以在集合视图中显示 20 个空集合视图单元格,向集合视图部分标题中添加了一个按钮以模态方式呈现代表位置屏幕的视图,并添加了一个取消按钮以关闭它。

在本章中,你将实现应用浏览中展示的剩余屏幕,即第九章“设置用户界面”中展示的屏幕。首先,你将在位置屏幕中添加一个空白表格视图。接下来,你将添加餐厅列表屏幕,当在探索屏幕中点击单元格时将显示此屏幕。你将配置此屏幕以显示包含单个集合视图单元格的集合视图。之后,你将添加餐厅详情屏幕,当在餐厅列表屏幕中点击单元格时将显示此屏幕。你将配置此屏幕以显示静态表格视图单元格的表格视图。你还将向其中一个单元格添加一个按钮,当点击时将显示代表评论表单屏幕的视图。最后,你将使地图屏幕显示地图。

到本章结束时,你将学会如何向 Storyboard 场景中添加和配置表格视图,如何在场景之间添加 segues,以及如何向场景中添加地图视图。你的应用程序的基本用户界面将完成,你将能够遍历模拟器中的所有屏幕。所有屏幕都不会显示数据,但你将在本书的第三部分中完成它们的实现。

本章将涵盖以下主题:

  • 在位置屏幕中添加表格视图

  • 实现餐厅列表屏幕

  • 实现餐厅详情屏幕

  • 地图屏幕中添加地图视图

技术要求

你将继续在上一章创建的LetsEat项目中工作。

本章完成的 Xcode 项目位于本书代码包的Chapter11文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,了解代码的实际应用:

bit.ly/3EYWb6i

首先,你将在位置屏幕中添加一个表格视图,它最终将用于显示餐厅位置列表。

在位置屏幕中添加表格视图

当你在探索屏幕的集合视图部分标题中点击按钮时,将模态地呈现另一个代表位置屏幕的视图,但当前它是空的。让我们向此视图添加一个表格视图。按照以下步骤操作:

  1. 构建并运行LetsEat应用以确保一切仍然按预期工作。在项目导航器中点击Main故事板文件。在文档大纲中,选择由探索视图控制器场景中的按钮模态显示的视图控制器图标。点击库按钮:![图 11.1:显示库按钮的工具栏 图片

    图 11.1:显示库按钮的工具栏

  2. 库将出现。在过滤器字段中输入table。结果中将出现表格视图对象。

  3. 表格视图对象拖动到视图控制器场景中的视图中:![图 11.2:选择表格视图对象的库 图片

    图 11.2:选择表格视图对象的库

  4. 你将添加约束使表格视图填满整个屏幕。选择表格视图后,点击“添加新约束”按钮:![图 11.3:选择表格视图的视图控制器场景 图片

    图 11.3:选择表格视图的视图控制器场景

  5. 在所有最近邻居间距字段中输入0,并确保所有浅红色支撑结构都被选中(它们将变为鲜红色)。点击添加 4 个约束按钮:![图 11.4:自动布局添加新约束弹出对话框 图片

    图 11.4:自动布局添加新约束弹出对话框

  6. 验证表格视图的边缘现在与视图控制器场景中的视图边缘对齐:

![图 11.5:表格视图填满屏幕的视图控制器场景图片

图 11.5:表格视图填满屏幕的视图控制器场景

构建并运行你的应用,点击章节标题中的按钮。你将在位置屏幕中看到一个空白的表格视图:

![图 11.6:iOS 模拟器显示探索和位置屏幕图片

图 11.6:iOS 模拟器显示探索和位置屏幕

你将在第十五章“使用表格视图入门”中实现位置屏幕的视图控制器。最终,这个表格视图将显示餐厅位置列表,如应用浏览中所示。正如你所见,这个过程与在上一章中添加到探索屏幕中的集合视图类似。

在下一节中,你将在故事板中添加一个视图控制器场景来表示餐厅列表屏幕。

实现餐厅列表屏幕

第九章“应用浏览”中所示,在探索屏幕中设置位置并点击一种菜系后,将出现餐厅列表屏幕,显示餐厅列表。

要实现RestaurantListViewController类,将其设置为视图控制器场景视图的视图控制器,并将集合视图的出口连接到这个类。步骤与上一章中为ExploreViewController类执行的步骤非常相似。

让我们先添加新的视图控制器场景。按照以下步骤操作:

  1. Main故事板文件中,将上一章中添加的导航控制器场景和视图控制器场景向上移动,为即将添加的新视图控制器场景腾出空间:![Figure 11.7: 显示 Main 故事板文件内容的编辑区域

    ![img/Figure_11.07_B17469.jpg]

    Figure 11.7: 显示 Main 故事板文件内容的编辑区域

  2. 点击库按钮,在过滤器字段中输入view con。结果中将有一个视图控制器对象。将视图控制器对象拖动到故事板中以表示餐厅列表屏幕:![Figure 11.8: 已选择视图控制器对象的库

    ![img/Figure_11.08_B17469.jpg]

    Figure 11.8: 已选择视图控制器对象的库

  3. 点击库按钮,在过滤器字段中输入collec。结果中将有一个集合视图对象。将集合视图对象拖动到视图控制器场景中的视图中:![Figure 11.9: 已选择集合视图对象的库

    ![img/Figure_11.09_B17469.jpg]

    Figure 11.9: 已选择集合视图对象的库

  4. 你将添加约束使集合视图填满整个屏幕。在集合视图被选中时,点击添加新约束按钮:![Figure 11.10: 已选择集合视图的视图控制器场景

    ![img/Figure_11.10_B17469.jpg]

    Figure 11.10: 已选择集合视图的视图控制器场景

  5. 在所有最近邻间距字段中输入0,并确保所有浅红色支撑结构都被选中(它们将变为亮红色)。点击添加 4 个约束按钮。验证集合视图的边缘现在是否与视图控制器场景中视图的边缘对齐:

![Figure 11.11: 集合视图填满屏幕的视图控制器场景

![img/Figure_11.11_B17469.jpg]

Figure 11.11: 集合视图填满屏幕的视图控制器场景

餐厅列表屏幕的视图控制器场景已添加,但尚未包含视图控制器。你需要一个视图控制器来使其显示集合视图单元格。在下一节中,你将在应用中添加一个新的 Cocoa Touch 类文件,以便声明和定义此屏幕的新视图控制器类。

声明 RestaurantListViewController 类

如前一章所述,你将在项目中添加一个新的 Cocoa Touch 类文件,但这次,你将实现RestaurantListViewController类。你将使用这个类的实例作为餐厅列表屏幕的视图控制器。按照以下步骤操作:

  1. 点击导航器和检查器按钮以打开导航器和检查器区域。

  2. 右键点击 LetsEat 组,从弹出菜单中选择新建组

  3. 将这个新组命名为 Restaurants。如果你出错,点击名称并按键盘上的 Return 键使其再次可编辑。

  4. 右键点击 Restaurants 组,选择新建文件...

  5. iOS 应已选中。选择Cocoa Touch 类并点击下一步

  6. RestaurantListViewController

    UIViewController

    完成后点击下一步

  7. 在下一屏,点击创建

  8. RestaurantListViewController 文件已添加到项目中,你将在其中看到 RestaurantListViewController 类的样板代码。RestaurantListViewController 类是 UIViewController 类的子类,包含一个方法,viewDidLoad()。如同你在上一章中所做的,从 RestaurantListViewController 类中的 viewDidLoad() 类之后删除注释代码,直到只剩下以下截图所示的代码:

![图 11.12:显示 RestaurantListViewController 文件内容的编辑区域图片

图 11.12:显示 RestaurantListViewController 文件内容的编辑区域

如同之前对 RestaurantListViewController 类所做的,为视图场景中的视图控制器采用集合视图数据源和代理协议。你还需要在类定义中手动添加集合视图的出口,并使用连接检查器将出口连接到故事板中的集合视图。你将在下一节中这样做。

采用代理和数据源协议

你将修改 RestaurantListViewController 类,使其符合 UICollectionViewDataSourceUICollectionViewDelegate 协议,并添加任何必需的协议方法。你还将添加集合视图的出口,并将 RestaurantListViewController 类的实例设置为视图的控制器。按照以下步骤操作:

  1. 按照以下所示修改 RestaurantListViewController 类声明,使其采用 UICollectionViewDataSourceUICollectionViewDelegate 协议:

    class RestaurantListViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
    
  2. 当错误图标出现时,点击它。

  3. 你会看到这个错误,因为符合你添加的协议所需的方法没有在类定义中存在。点击修复按钮,将所需方法的占位符添加到你的类定义中。

  4. 确认方法占位符已添加到文件中。重新排列一切,使占位符位于 viewDidLoad() 方法之后:![图 11.13:显示 UICollectionViewDataSource 方法占位符的编辑区域 图片

    图 11.13:显示 UICollectionViewDataSource 方法占位符的编辑区域

  5. 按照以下所示修改方法占位符,以便在应用运行时在屏幕上显示单个集合视图单元格:

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
       1
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
    {
       collectionView.dequeueReusableCell(
       withReuseIdentifier: "restaurantCell",
       for: indexPath)
    }
    
  6. 确认你的代码看起来像这样:![图 11.14:显示显示单个单元格的代码的编辑区域 图 11.14:B17469.jpg

    图 11.14:显示单个单元格代码的编辑区域

  7. 在类声明之后添加一个输出,collectionView

    @IBOutlet var collectionView: UICollectionView!
    

    你将在稍后链接到故事板中的集合视图。

  8. 确保你的代码看起来像这样:![图 11.15:显示 collectionView 输出的编辑区域 图 11.15:B17469.jpg

    图 11.15:显示 collectionView 输出的编辑区域

    你不会使用辅助编辑器将输出链接到集合视图,就像你在上一章中所做的那样。这是一个个人偏好的问题——你可以自由选择最适合你的方法。

  9. 点击 Main 故事板文件,点击文档大纲中新增的 视图控制器场景视图控制器 图标。点击身份检查器按钮:![图 11.16:已选择身份检查器 图 11.16:B17469.jpg

    图 11.16:已选择身份检查器

  10. 要使 RestaurantListViewController 类的实例成为此场景的视图控制器,在 字段中选择 RestaurantListViewController:![图 11.17:身份检查器,类设置为 RestaurantListViewController 图 11.17:B17469.jpg

    图 11.17:身份检查器,类设置为 RestaurantListViewController

    注意,视图控制器场景的名称已更改为餐厅列表视图控制器场景

  11. 点击连接检查器按钮。要将 RestaurantListViewController 类定义中的集合视图分配,从 collectionView 输出的旁边的小圆圈拖动到 餐厅列表视图控制器场景 中的集合视图:![图 11.18:显示要连接的集合视图的编辑区域 图 11.18:B17469.jpg

    图 11.18:显示要连接的集合视图的编辑区域

  12. 确认 RestaurantListViewController 类定义中的 collectionView 输出的集合视图现在已连接:![图 11.19:显示 collectionView 输出的连接检查器 图 11.19:B17469.jpg

    图 11.19:显示 collectionView 输出的连接检查器

  13. 点击 RestaurantListViewController 类,为集合视图选择数据源和委托对象,从 dataSourcedelegate 输出的旁边的小圆圈拖动到文档大纲中的 餐厅列表视图控制器 图标:![图 11.20:已选择连接检查器 图 11.20:B17469.jpg

    图 11.20:已选择连接检查器

  14. 确认 dataSourcedelegate 输出现在已连接:![图 11.21:已设置 dataSource 和 delegate 输出的连接检查器 图 11.21:B17469.jpg

    图 11.21:已设置 dataSource 和 delegate 输出的连接检查器

  15. 在文档大纲中点击集合视图单元格。点击属性检查器按钮以设置集合视图单元格的标识符和颜色(你将在第十三章开始使用 MVC 和集合视图)中了解更多关于标识符的信息):图 11.22:属性检查器已选中

    图 11.22:属性检查器已选中

  16. 设置restaurantCell并设置浅灰色颜色

图 11.23:属性检查器已设置标识符和背景颜色

图 11.23:属性检查器已设置标识符和背景颜色

餐厅列表视图控制器场景的设置现在已完成。现在,你需要在一个单元格在探索屏幕中被点击时显示这个屏幕。为了做到这一点,你将在下一节中在探索屏幕和餐厅列表屏幕之间添加一个 segue。

展示餐厅列表屏幕

在上一章中,你添加了一个 segue,当探索屏幕中的按钮被点击时,会显示位置屏幕。为了在点击探索屏幕中的单元格时显示餐厅列表屏幕,你也会使用一个 segue。按照以下步骤操作:

  1. 在文档大纲中点击exploreCell餐厅列表视图控制器场景以在它们之间添加一个 segue:图 11.24:文档大纲显示 exploreCell

    图 11.24:文档大纲显示 exploreCell

  2. segue菜单将出现。从菜单中选择显示

图 11.25:带有“显示”选项的 segue 弹出菜单

图 11.25:带有“显示”选项的 segue 弹出菜单

这使得当点击探索屏幕中的单元格时,餐厅列表屏幕从右侧滑入。在导航栏中会出现一个<返回按钮。

构建并运行你的应用。在探索屏幕中,点击一个单元格。你应该会看到包含一个单元格的集合视图的餐厅列表屏幕出现。在导航栏中点击<返回按钮将关闭餐厅列表屏幕:

图 11.26:iOS 模拟器显示探索和餐厅列表屏幕

图 11.26:iOS 模拟器显示探索和餐厅列表屏幕

餐厅列表屏幕的实现现在已完成,你可以从探索屏幕导航到餐厅列表屏幕并返回。最终,这个屏幕中的集合视图将显示特定位置的餐厅列表,如应用演示所示。太棒了!接下来,你将添加一个视图控制器场景来表示餐厅详情屏幕。当点击餐厅列表屏幕中的单元格时,将显示此屏幕。你将在下一节中这样做。

实现餐厅详情屏幕

第九章 “设置用户界面” 中的应用程序导游所示,当你点击 餐厅列表 屏幕中的餐厅时,将出现包含该餐厅详细信息的 餐厅详情 屏幕。点击 添加评论 按钮将显示可以添加评论的 评论表单 屏幕,点击 添加照片 按钮将显示可以添加照片并应用滤镜的 照片滤镜 屏幕。

在本节中,你将在你的故事板中添加一个新的表格视图控制器场景来表示 餐厅详情 屏幕,并添加第二个视图控制器场景来表示 评论表单 屏幕。你将在表格视图中的一个单元格中放置一个按钮来显示 评论表单 屏幕。

让我们从添加新的视图控制器场景开始。按照以下步骤操作:

  1. 点击库按钮,在过滤器字段中输入 table,然后将 表格视图控制器 对象拖动到故事板中,位于 餐厅列表视图控制器场景 旁边:![图 11.27:选择表格视图控制器对象的库 图片

    图 11.27:选择表格视图控制器对象的库

    这将代表 餐厅详情 屏幕。

  2. 验证是否已添加 表格视图控制器场景:![图 11.28:显示表格视图控制器场景和 Restaurant List 视图控制器场景的编辑区域 图片

    图 11.28:显示表格视图控制器场景和 Restaurant List 视图控制器场景的编辑区域

    注意,它已经包含了一个表格视图,因此你不需要在场景中添加表格视图,就像你之前章节中所做的那样。

  3. 要将 restaurantCell(在文档大纲中位于 餐厅列表视图控制器场景 之下)显示到表格视图中,以在它们之间添加过渡:![图 11.29:选择 restaurantCell 的文档大纲 图片

    图 11.29:选择 restaurantCell 的文档大纲

  4. 过渡 菜单中选择 显示。这使得当点击 餐厅列表 屏幕中的单元格时,餐厅详情 屏幕从右侧滑入。在导航栏中会出现一个 <返回 按钮。

  5. 验证两个场景之间是否出现了过渡:![图 11.30:显示 Restaurant List 视图控制器场景和表格视图控制器场景之间的过渡 图片

    图 11.30:显示 Restaurant List 视图控制器场景和表格视图控制器场景之间的过渡

  6. 餐厅详情 屏幕始终显示固定数量的单元格。在文档大纲中,点击 表格视图控制器场景 下的 表格视图,然后点击属性检查器按钮:![图 11.31:属性检查器已选择 图片

    图 11.31:属性检查器已选择

  7. 内容设置为静态单元格以使餐厅详情屏幕显示固定数量的单元格:

![图 11.32:内容设置为静态单元格的属性检查器图片

图 11.32:内容设置为静态单元格的属性检查器

你这样做是因为餐厅详情屏幕总是使用相同数量的单元格来显示餐厅详情。

构建并运行你的应用。在探索屏幕上点击一个单元格以显示餐厅列表屏幕。然后,在餐厅列表屏幕上点击一个单元格以显示餐厅详情屏幕:

![图 11.33:显示餐厅详情屏幕的 iOS 模拟器图片

图 11.33:显示餐厅详情屏幕的 iOS 模拟器

点击后退按钮返回。

在下一节中,你将在表格视图中的一个单元格内实现一个按钮以显示表示评论表单屏幕的屏幕。

实现评论表单屏幕

在本节中,你将实现一个新的视图控制器场景来表示评论表单屏幕,并在餐厅详情屏幕中配置一个按钮以显示它。按照以下步骤操作:

  1. 你需要在过滤器字段中的button里添加一个按钮。一个按钮对象将作为结果之一出现。将其拖动到表示餐厅详情屏幕的表格视图控制器场景中的顶部静态单元格:![图 11.34:选择按钮对象的库 图片

    图 11.34:选择按钮对象的库

  2. 将其放置在单元格的右侧:![图 11.35:显示带有按钮的表格视图控制器场景的编辑区域 图片

    图 11.35:显示带有按钮的表格视图控制器场景的编辑区域

  3. 点击库按钮并在过滤器字段中输入view con。一个视图控制器对象将作为结果之一出现。将其拖动到表格视图控制器场景旁边以表示评论表单屏幕:![图 11.36:选择视图控制器对象的库 图片

    图 11.36:选择视图控制器对象的库

  4. 验证是否已添加新的视图控制器场景:![图 11.37:显示视图控制器场景和表格视图控制器场景的编辑区域 图片

    图 11.37:显示视图控制器场景和表格视图控制器场景的编辑区域

  5. 点击库按钮并在过滤器字段中输入label。一个标签对象将作为结果之一出现。将其拖动到新视图控制器场景的中心以表示一条评论:![图 11.38:选择标签对象的库 图片

    图 11.38:选择标签对象的库

  6. 将标签文本更改为评论。点击对齐按钮以向其添加水平和垂直约束:![图 11.39:显示带有标签的视图控制器场景的编辑区域 图片

    图 11.39:显示带有标签的视图控制器场景的编辑区域

  7. 打勾容器中水平容器中垂直复选框。点击添加 2 个约束按钮。验证是否已添加约束:![图 11.40:显示已设置标签约束的编辑区域

    ![img/Figure_11.40_B17469.jpg]

    图 11.40:显示已设置标签约束的编辑区域

    这些约束确保当应用运行时,评论标签始终位于屏幕中间,无论方向或屏幕大小如何。

  8. Ctrl + 拖动从表格视图单元格中的按钮到新添加的视图控制器场景,并从弹出菜单中选择显示。这样,当按钮被点击时,会出现审查表单屏幕:

![img/Figure_11.41: Segue pop-up menu with Show selected

![img/Figure_11.41_B17469.jpg]

图 11.41:带有“显示选中”的切换弹出菜单

构建并运行你的应用。点击探索屏幕中的一个单元格,然后点击餐厅列表屏幕中的一个单元格。在餐厅详情屏幕中点击按钮以显示审查表单屏幕:

![img/Figure_11.42: iOS simulator showing Review Form screen

![img/Figure_11.42_B17469.jpg]

图 11.42:显示审查表单屏幕的 iOS 模拟器

太棒了!除了照片滤镜屏幕外,所有可以从探索标签访问的屏幕现在都已实现,几乎不需要编写任何代码!如果你愿意,可以重复本节中的步骤来添加照片滤镜屏幕。

最后要做的就是使地图屏幕显示地图。你将在下一节中这样做。

实现地图屏幕

当你启动应用时,会显示探索屏幕。在标签栏中点击地图按钮会使地图屏幕出现,但它为空白。要使地图屏幕显示地图,你需要在地图屏幕的视图控制器场景中添加一个地图视图。按照以下步骤操作:

  1. 选择地图屏幕的视图控制器场景:![图 11.43:显示视图控制器场景的编辑区域

    ![img/Figure_11.43_B17469.jpg]

    图 11.43:显示视图控制器场景的编辑区域

  2. 要使此场景显示地图,点击库按钮并在过滤器字段中输入map。一个地图视图对象作为结果之一出现。将其拖动到视图控制器场景中的视图上:![图 11.44:选中地图视图对象的库

    ![img/Figure_11.44_B17469.jpg]

    图 11.44:选中地图视图对象的库

  3. 地图应填满整个屏幕。在地图视图选中状态下,点击“添加新约束”按钮:![图 11.45:选中地图视图的视图控制器场景

    ![img/Figure_11.45_B17469.jpg]

    图 11.45:选中地图视图的视图控制器场景

  4. 在所有最近邻间距字段中输入0,并确保选择了浅红色支撑(它们会变成鲜红色)。点击添加 4 个约束按钮。验证地图视图是否填满了整个屏幕:

图 11.46:视图控制器场景,地图视图填充整个屏幕

图 11.46:视图控制器场景,地图视图填充整个屏幕

构建并运行您的应用。点击地图按钮。您应该看到与以下所示类似的地图:

图 11.47:iOS 模拟器显示地图屏幕

图 11.47:iOS 模拟器显示地图屏幕

如果您此时查看Main故事板文件,您应该看到如下内容:

图 11.48:编辑区域显示主故事板文件中的所有场景

图 11.48:编辑区域显示主故事板文件中的所有场景

确认您拥有前述截图中的所有场景,您也可以在模拟器中运行您的应用以检查所有屏幕是否正常工作。

太棒了!您现在已经完成了应用的基本用户界面!

摘要

在本章中,您完成了应用的基本结构。首先,您在位置屏幕中添加了一个空白表格视图。您还在故事板中添加了一个新的视图控制器场景来表示餐厅列表屏幕,为该屏幕添加并配置了一个集合视图,并实现了当在探索屏幕中点击单元格时显示该切换。您添加了一个新的表格视图控制器场景来表示餐厅详情屏幕,为该屏幕配置了一个具有静态单元格的表格视图,并实现了当在餐厅列表屏幕中点击单元格时显示该屏幕的切换。您还在餐厅详情屏幕的一行中添加了一个按钮,添加了一个表格视图控制器场景来表示评论表单屏幕,并配置了您添加的按钮以显示它。最后,您在表示地图屏幕的视图控制器场景中添加了一个地图视图,当点击地图按钮时,它现在会显示地图。

您已成功实现了应用所需的所有屏幕,当您在模拟器中运行它时,您将能够测试应用流程。您也应该更加熟练地使用界面构建器。现在您知道如何向故事板场景添加和配置表格视图,如何添加场景之间的切换,以及如何向场景添加地图视图。这将有助于您实现包含表格视图、使用切换在不同屏幕间导航以及显示地图的应用。太棒了!

在下一章中,您将修改探索屏幕、餐厅列表屏幕和位置屏幕内的单元格,以便它们与在应用游览中显示的设计相匹配。

第十二章:第十二章:修改和配置单元格

在上一章中,你实现了应用程序所需的所有屏幕,但 Explore餐厅列表位置 屏幕中的单元格仍需要工作。例如,Explore 屏幕的收藏视图部分标题和收藏视图单元格与 第九章**,设置用户界面 中应用程序浏览中显示的设计不匹配。

在本章中,你将通过添加图像视图和标签来修改和配置 exploreCell 收藏视图单元格。对于 restaurantCell 收藏视图单元格,通过添加标签、按钮和图像视图来修改。你还将配置图像视图以显示默认图像。对于 locationCell,对于表格视图单元格。

到本章结束时,你将熟练于添加和定位用户界面元素,并知道如何使用约束来确定它们之间的相对位置。

将介绍以下主题:

  • 修改 Explore 屏幕的收藏视图部分标题

  • 修改 exploreCell 收藏视图单元格

  • 修改 restaurantCell 收藏视图单元格

  • 配置 locationCell 收藏视图单元格

技术要求

你将继续在上一章修改的 LetsEat 项目上工作。

本章完成的 Xcode 项目位于本书代码包的 Chapter12 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际效果:

bit.ly/3kjQVSD

让我们从向 Explore 屏幕的收藏视图部分标题添加 UI 元素开始,使其与应用程序浏览中显示的相匹配。

修改 Explore 屏幕部分标题

让我们看看应用程序浏览中 Explore 屏幕的收藏视图部分标题看起来是什么样子:

![Figure 12.1: 完成的 Let's Eat 应用程序的收藏视图部分标题img/Figure_12.01_B17469.jpg

Figure 12.1: 完成的 Let's Eat 应用程序的收藏视图部分标题

在此收藏视图部分标题中有四个元素:两个标签(标题和副标题)、一个按钮和一个视图(标题和按钮下方的灰色线条)。

你已经在 第十章**,构建用户界面 中向 Explore 屏幕的收藏视图部分标题添加了一个按钮。现在,你将添加标签和视图,然后修改所有元素以匹配应用程序浏览中显示的收藏视图部分标题。按照以下步骤操作:

  1. 首先,在编辑器区域中打开边界矩形。这将用蓝色突出显示用户界面元素的边界,并使它们更容易看到。从编辑器菜单中选择画布 | 边界矩形以打开它们:图 12.2:Xcode 菜单栏显示编辑器 | 画布 | 边界矩形

    图 12.2:Xcode 菜单栏显示编辑器 | 画布 | 边界矩形

  2. 请验证集合视图部分标题的大小已正确设置。在Main故事板文件中找到0

    100

    图 12.3:集合视图部分标题的大小检查器设置

    图 12.3:集合视图部分标题的大小检查器设置

    请记住,使用的单位是点。将集合视图部分标题的宽度设置为0将自动使其与屏幕宽度相同。

  3. 您将在集合视图部分标题中添加一个视图,作为您将添加的所有其他用户界面元素的容器。单击库按钮。在过滤器字段中输入uiview。一个视图对象将出现在结果中。将其拖动到集合视图部分标题中:图 12.4:选中视图对象的库

    图 12.4:选中视图对象的库

  4. 在文档大纲中,将视图拖动到顶部,使其成为集合可重用视图子视图列表中的第一个项目。列表中的第一个子视图将首先绘制到屏幕上,这确保它不会覆盖按钮:图 12.5:显示视图位置的文档大纲

    图 12.5:显示视图位置的文档大纲

  5. 您将使视图与集合视图部分标题具有相同的大小。使用0

    0

    375

    100

    图 12.6:视图的大小检查器设置

    图 12.6:视图的大小检查器设置

    XY值确定视图相对于集合视图部分标题的水平和垂直偏移量,而宽度高度值确定视图的宽度和高度。这使得此视图的左上角位置与集合视图部分标题的左上角位置相同,将视图的宽度设置为与屏幕相同的宽度(375 点),并将视图的高度设置为 100 点。这将使您稍后添加约束更容易,因为您将添加的视图相对于此容器视图进行定位。

  6. 您需要标签来在过滤器字段中显示标签。一个标签对象将出现在结果中。将两个标签对象拖动到您之前拖入的视图中:图 12.7:选中标签对象的库

    图 12.7:选中标签对象的库

  7. 两个标签都必须是容器视图的子视图,因为您将相对于容器视图中的位置对它们应用约束。在文档大纲中,验证两个标签对象都是视图的子视图,并且视图集合可重用视图的子视图:![图 12.8:包含两个标签的视图 图片 12.08

    图 12.8:包含两个标签的视图

  8. 按钮也必须是容器视图的子视图,因为它也将相对于容器视图应用约束。在文档大纲中选择按钮,并将其拖动到视图上,使其成为视图的子视图。完成时,它应该看起来像这样:![图 12.9:显示 Collection Reusable View 子视图的文档大纲 图片 12.09

    图 12.9:显示 Collection Reusable View 子视图的文档大纲

  9. 在集合视图部分标题中的标签之一应设置为自定义灰色颜色。您将在您的资产目录中创建一个文件夹,并向其中添加新的自定义颜色。单击Assets.xcassets文件。在文档大纲的空白区域右键单击,如图所示:![图 12.10:Assets.xcassets 文件夹显示文档大纲 图片 12.10

    图 12.10:Assets.xcassets 文件夹显示文档大纲

  10. 从弹出菜单中选择新建文件夹以创建新文件夹:![图 12.11:已选择新建文件夹的弹出菜单 图片 12.11

    图 12.11:已选择新建文件夹的弹出菜单

  11. 将文件夹的名称更改为colors:![图 12.12:Assets.xcassets 文件夹显示 colors 文件夹 图片 12.12

    图 12.12:Assets.xcassets 文件夹显示 colors 文件夹

    您将把所有自定义颜色放在这个文件夹中。

  12. 现在,您将在项目中添加一个新的自定义颜色。右键单击colors文件夹,并选择新建颜色集:![图 12.13:已选择新建颜色集的弹出菜单 图片 12.13

    图 12.13:已选择新建颜色集的弹出菜单

  13. 单击新创建的颜色集。确保LetsEat Light Gray,设置8 位十六进制并将#AFAFB2输入到十六进制字段中,完成后按Return键:![图 12.14:LetsEat Light Gray 属性检查器设置 图片 12.14

    图 12.14:LetsEat Light Gray 属性检查器设置

    注意,在任何外观颜色框旁边,有一个暗外观颜色框。如果用户开启暗模式,则使用暗外观颜色框中的颜色。保留默认值。

    重要信息

    要了解更多关于暗模式的信息,请访问此链接:support.apple.com/en-us/HT210332

  14. 您将创建第二种颜色,当您修改LetsEat Dark Gray并设置#AAAAAA时使用:![图 12.15:LetsEat Dark Gray 属性检查器设置 图片 12.15

    图 12.15:LetsEat 深灰色属性检查器设置

  15. 对于LetsEat 深灰色,您将把暗外观颜色改为更深的灰色。单击暗外观颜色框并单击显示颜色面板按钮:![图 12.16:选择显示颜色面板按钮的属性检查器 图 12.16_B17469.jpg

    图 12.16:选择显示颜色面板按钮的属性检查器

  16. 要设置颜色,选择滑块面板,从弹出菜单中选择灰度滑块并单击从左数第二个灰色阴影,如图所示:![图 12.17:LetsEat 深灰色暗外观的颜色面板设置 图 12.17_B17469.jpg

    图 12.17:LetsEat 深灰色暗外观的颜色面板设置

  17. 确认colors文件夹的内容如下:![图 12.18:Assets.xcassets 显示 colors 文件夹的内容 图 12.18_B17469.jpg

    图 12.18:Assets.xcassets 显示 colors 文件夹的内容

  18. 要将标签配置为副标题,点击Main故事板文件并选择一个标签。点击属性检查器按钮并更新以下值:

    普通然后在下面的空文本字段中添加请选择一个位置

    LetsEat 亮灰色

    系统半粗体 13.0

    ![图 12.19:副标题标签的属性检查器设置 图 12.19_B17469.jpg

    图 12.19:副标题标签的属性检查器设置

  19. 在选择标签的情况下,单击大小检查器按钮。在8部分更新以下值

    24

    359

    21

    ![图 12.20:副标题标签的大小检查器设置 图 12.20_B17469.jpg

    图 12.20:副标题标签的大小检查器设置

    此标签是您之前添加到集合视图部分标题中的容器视图的子视图。这意味着标签的位置将相对于此视图。标签的左上角将水平偏移 8 点,垂直偏移 24 点,出现在集合视图部分标题的左上角下方和右侧。标签的宽度将是 359 点,高度是 21 点。

  20. 要将另一个标签设置为标题,选择它并单击属性检查器按钮。更新以下值:

    普通然后在下面的空文本字段中添加探索

    系统粗体 40.0

    ![图 12.21:标题标签的属性检查器设置 图 12.21_B17469.jpg

    图 12.21:标题标签的属性检查器设置

  21. 在选择标签的情况下,单击大小检查器按钮。在视图部分更新以下值:

    8

    45

    255

    37

    ![图 12.22:标题标签的大小检查器设置 图 12.22_B17469.jpg

    图 12.22:标题标签的大小检查器设置

    这些设置将此标签向右偏移8点,并在容器视图的左上角下方偏移45点,将其定位在第一个标签下方。请注意,它不会延伸到整个右侧,为稍后要添加的按钮留出一些空间。

  22. 要为按钮配置自定义图片,请选择它,然后在属性检查器中,更新以下值在自定义中,并从btn-location下的字段中删除文本图 12.23:位置按钮的属性检查器设置

    图 12.23:位置按钮的属性检查器设置

    此图片包含在您在第九章**,设置用户界面中添加到您的应用的Assets.xcassets文件中。

  23. 仍然选择按钮,点击大小检查器按钮。更新以下值在271

    50

    96

    25

    图 12.24:位置按钮的大小检查器设置

    图 12.24:位置按钮的大小检查器设置

  24. 您最后要添加的是集合视图部分标题底部的细灰色线条。点击库按钮。在过滤器字段中输入uiview。结果中会出现一个视图对象。将视图对象拖放到容器视图中:图 12.25:选择视图对象的库

    图 12.25:选择视图对象的库

  25. 要将新添加的视图定位到正确位置,请点击大小检查器按钮,并更新以下值在8

    89

    359

    1

    图 12.26:灰色线条视图的大小检查器设置

    图 12.26:灰色线条视图的大小检查器设置

    这样会将视图放置在所有其他元素下方,但它是不透明的。

  26. 要为视图设置颜色,请点击属性检查器按钮并设置LetsEat Light Gray

图 12.27:灰色线条视图的属性检查器设置

图 12.27:灰色线条视图的属性检查器设置

所有必需的用户界面元素都已添加。构建并运行您的应用。您的集合视图部分标题现在应该看起来像这样:

图 12.28:显示完成集合视图部分标题的 iOS 模拟器

图 12.28:显示完成集合视图部分标题的 iOS 模拟器

如您所见,集合视图部分标题现在与应用导览中显示的设计相匹配。它在 iPhone SE(第二代)模拟器上运行得很好,但要确保它在其他屏幕尺寸上也能工作,您将添加自动布局约束。您将在下一节中这样做。

将自动布局添加到探索屏幕的标题部分

如果你现在在 iPhone SE(第二代)模拟器中构建并运行你的应用,集合视图部分标题看起来会很棒,但如果你切换到更大屏幕的模拟器,你会看到一些图形元素的位置不正确。正如你在前面的章节中看到的,自动布局确保 UI 适应设备的屏幕尺寸和方向。例如,启动屏幕上的“Let's Eat”应用标志无论在哪种设备上都保持在屏幕的精确中心,而在“位置”屏幕中的表格视图即使在设备旋转时也占据所有可用屏幕空间。

到目前为止,你只使用了与单个用户界面元素相关的自动布局约束。在本节中,你将把它们添加到集合视图部分标题内的多个用户界面元素中。你将首先向容器视图添加约束,然后继续向其中所有其他项目添加约束。按照以下步骤操作:

  1. 选择文档大纲中包含其他视图的视图:![Figure 12.29: 文档大纲显示容器视图 图片

    Figure 12.29: 文档大纲显示容器视图

  2. 点击“添加新约束”按钮并输入以下值以设置此视图的约束:

    0

    0

    0

    90

    完成后,点击添加 4 个约束按钮。这将绑定容器视图的顶部、左侧和右侧边缘到集合视图部分标题的边缘。视图的高度设置为 90 点,这决定了底部边缘的位置。

  3. 在文档大纲中选择请选择位置标签:![Figure 12.30: 文档大纲显示副标题标签 图片

    Figure 12.30: 文档大纲显示副标题标签

  4. 点击“添加新约束”按钮并输入以下值以设置此标签的约束:

    24

    8

    8

    21

    完成后,分别点击24个点、8个点和8个点。注意,标签的宽度未设置,允许它在运行在屏幕较小或较大的模拟器上时进行变化。与之前一样,设置高度约束决定了标签底部边缘的位置。

  5. 在文档大纲中选择位置按钮:![Figure 12.31: 文档大纲显示位置按钮 图片

    Figure 12.31: 文档大纲显示位置按钮

  6. 点击“添加新约束”按钮并输入以下值以设置此按钮的约束:

    5

    8

    96

    25

    完成后,点击添加 4 个约束按钮。由于位置按钮位于请选择位置标签下方,因此顶部约束决定了位置按钮顶部边缘与标签底部边缘之间的空间,而不是容器视图的顶部边缘。位置按钮的右侧边缘与容器视图的右侧边缘之间的空间设置为 8 点,宽度和高度约束决定了位置按钮的左侧和底部边缘的位置。

  7. 在文档大纲中选择灰色线视图:![图 12.32:显示灰色线视图的文档大纲 图片

    图 12.32:显示灰色线视图的文档大纲

  8. 点击添加新约束按钮,并输入以下值以设置此视图的约束:

    8

    8

    0

    1

    完成后,点击添加 4 个约束按钮。视图的左右边缘与容器视图的左右边缘之间的空间设置为 8 点,视图的底部边缘绑定到容器视图的底部边缘。高度约束决定了视图顶部边缘的位置。

  9. 在文档大纲中选择探索标签:![图 12.33:显示标题标签的文档大纲 图片

    图 12.33:显示标题标签的文档大纲

  10. 点击添加新约束按钮,并输入以下值以设置此标签的约束:

    0

    8

    8

    37

    完成后,点击添加 4 个约束按钮。标签的顶部边缘绑定到请选择位置标签的底部边缘。标签的左侧边缘与容器视图的左侧边缘之间的空间为 8 点。标签的右侧边缘与位置按钮的左侧边缘之间的空间为 8 点。高度约束决定了标签底部边缘的位置。

您已经完成了在探索屏幕的集合视图部分标题中添加所有视图的自动布局约束。尝试使用不同的模拟器运行您的应用,以查看用户界面如何适应不同的屏幕尺寸。太酷了!

您可能想知道为什么在添加约束之前需要使用大小检查器设置用户界面元素的位置。实际上,您不必这样做,但通过这样做,可以使添加约束变得更加容易,因为当您点击添加新约束按钮时,您看到的约束值是从当前用户界面元素之间的空间中派生出来的。

对于新手开发者来说,使用自动布局可能会有些挑战。请慢慢来。如果它不能正常工作,请清除所有约束并重新开始。为此,请点击屏幕底部的解决自动布局问题按钮并选择清除约束

![图 12.34:选择清除约束的解决自动布局问题菜单图片

图 12.34:选择清除约束的自动布局问题菜单

你已经将所有必需的用户界面元素和约束添加到了下一节中exploreCell集合视图单元格的集合视图部分标题中。你将添加一些用户界面元素,使其与应用导览中显示的单元格相匹配。

修改 exploreCell 集合视图单元格

让我们看看在应用导览中exploreCell集合视图单元格看起来是什么样子:

图 12.35:完成后的 Let's Eat 应用的 exploreCell 集合视图单元格

图 12.35:完成后的 Let's Eat 应用的 exploreCell 集合视图单元格

在上一章中,你为exploreCell集合视图单元格设置了背景颜色,并配置了集合视图以显示 20 个单元格的网格。现在,你将移除背景颜色,并将用户界面元素添加到exploreCell集合视图单元格中,以匹配应用导览中的设计。按照以下步骤操作:

  1. 在开始之前,检查exploreCell集合视图单元格的初始设置。在exploreCell的文档大纲中选择exploreCell。设置默认图 12.36:exploreCell 集合视图单元格的属性检查器设置

    图 12.36:exploreCell 集合视图单元格的属性检查器设置

  2. 你将在exploreCell集合视图单元格中添加一个容器视图。点击库按钮。在过滤器字段中输入uiview。一个视图对象将出现在结果中。将其拖入原型单元格:图 12.37:选择视图对象的库

    图 12.37:选择视图对象的库

  3. 为了确保新添加视图的约束可以正确设置,请验证exploreCell内容视图是否选中:图 12.38:选择视图的文档大纲

    图 12.38:选择视图的文档大纲

  4. 点击“添加新约束”按钮,并输入以下值以设置新添加视图的约束:

    0

    0

    0

    40

    完成后,点击exploreCell集合视图单元格。底部边缘的位置由底部约束决定,该约束设置了视图底部边缘与exploreCell集合视图单元格底部边缘之间的距离。你将在稍后在这个空间中添加一个标签。

  5. 你将添加一个图像视图来显示一道菜的照片。点击库按钮。在过滤器字段中输入image。一个图像视图对象将出现在结果中。将其拖到之前添加的视图上方:图 12.39:选择图像视图对象的库

    图 12.39:选择图像视图对象的库

  6. 为了确保图像视图的约束可以正确设置,请验证图像视图是否是你之前添加的视图的子视图,并且已选中:![图 12.40:选择图像视图的文档大纲 图片 12.40

    图 12.40:选中图像视图的文档大纲

  7. 点击添加新约束按钮,并输入以下值以设置图像视图的约束:

    0

    0

    0

    0

    完成后,点击 添加 4 个约束 按钮。这将绑定图像视图的边缘到您之前添加的视图的边缘。

  8. 您将添加一个标签来显示菜系类型。点击库按钮。在过滤器字段中输入 label。一个 标签 对象将出现在结果中。将其拖动到您刚刚添加的图像视图和单元格底部之间的空间:![图片 12.41:库中选中标签对象 图片 12.41

    图 12.41:选中标签对象的书库

  9. 为了确保标签的约束可以正确设置,请验证 exploreCell 集合视图单元格的 内容视图,而不是您之前添加的 视图 的子视图:![图片 12.42:选中标签的文档大纲 图片 12.42

    图 12.42:选中标签的文档大纲

  10. 点击添加新约束按钮,并输入以下值以设置标签的约束:

    9

    8

    8

    21

    完成后,点击 9 个点。标签的左右边缘与 exploreCell 内容视图的相应边缘之间的空间都设置为 8 个点。高度约束通过设置标签上下边缘之间的空间来确定标签底部边缘的位置。

    所有必要的约束都已添加。构建并运行您的应用:

![图片 12.43:iOS 模拟器显示完成的 exploreCell 集合视图单元格图片 12.43

图 12.43:iOS 模拟器显示完成的 exploreCell 集合视图单元格

如您所见,探索屏幕现在更接近应用浏览中显示的设计。每个单元格现在都有一个图像视图和位于其下的标签,并且已添加所有必要的约束。太棒了!

注意,与上一节不同,在添加约束之前,您没有使用大小检查器设置用户界面元素的位置。您可以在添加少量元素时这样做,并且每个元素相对于其他元素的相对位置是不言自明的。

您已将所有必需的用户界面元素和约束添加到 exploreCell 集合视图单元格中。现在,您已经完成了对 exploreCell 集合视图单元格的修改,接下来在下一节中,我们将通过向其中添加一些用户界面元素来修改 restaurantCell 集合视图单元格。

修改 restaurantCell 集合视图单元格

让我们看看在应用浏览中 restaurantCell 集合视图单元格看起来是什么样子:

![图片 12.44:完成的 Let's Eat 应用程序的 restaurantCell 集合视图单元格图片 12.44

图 12.44:完成的 Let's Eat 应用程序的 restaurantCell 集合视图单元格

如您所见,restaurantCell 集合视图单元格有许多元素。您现在将修改它以匹配应用程序浏览中显示的设计。所需更改的摘要如下:

  • restaurantCell 集合视图单元格的大小调整为更大,并将背景颜色更改为默认颜色。

  • 添加一个视图,然后添加一个标签和一个包含三个按钮的堆叠视图来显示可预订的时间。

  • 添加一个视图,然后添加一个图像视图来显示餐厅的照片。

  • 在左上角添加一个标签来显示餐厅的名称。

  • 在名称标签下方添加一个标签来显示餐厅提供的菜肴。

您将使用大小检查器来定位所有元素,这将使添加必要的自动布局约束更容易。请慢慢操作,以减少出错的机会。按照以下步骤操作:

  1. 您将首先设置 restaurantCell 集合视图单元格的大小。在 Main 故事板文件中,点击 335312。设置为 None:图 12.45:餐厅单元格集合视图单元格的大小检查器设置

    图 12.45:餐厅单元格集合视图单元格的大小检查器设置

  2. 要检查 restaurantCell 集合视图单元格的标识符和背景颜色,请点击文档大纲中的 restaurantCell。点击属性检查器按钮。确认 restaurantCell。设置 默认:图 12.46:餐厅单元格集合视图单元格的属性检查器设置

    图 12.46:餐厅单元格集合视图单元格的属性检查器设置

  3. 你将在筛选字段中为 uiview 添加一个容器视图。结果中会出现一个 View 对象。将其拖入原型单元格:图 12.47:选择 View 对象的库

    图 12.47:选择 View 对象的库

  4. 选择新添加的视图后,点击大小检查器按钮。为 55 更新以下值

    245

    224

    56

    图 12.48:容器视图的大小检查器设置

    图 12.48:容器视图的大小检查器设置

  5. 你将在筛选字段中添加一个包含文本 label 的标签。结果中会出现一个 Label 对象。将其拖入你刚刚添加的视图中:图 12.49:选择 Label 对象的库

    图 12.49:选择 Label 对象的库

  6. 选择标签后,点击大小检查器按钮。在 0 中更新以下值

    2

    224

    21

    图 12.50:可用时间标签的大小检查器设置

    图 12.50:可用时间标签的大小检查器设置

  7. 在下面的空文本字段中设置和配置 Plain,然后添加 Available Times

    居中

    系统粗体 17.0

    图 12.51:可用时间标签的属性检查器设置

    Figure 12.51: 可用时间标签的属性检查器设置

  8. 你将添加带有预约时间的按钮到视图中。点击图书馆按钮。在过滤器字段中输入button。一个按钮对象将出现在结果中。将其拖动到与可用时间标签相同的视图中:![Figure 12.52: Library with Button object selected 图片

    Figure 12.52: Library with Button object selected

  9. 要设置按钮的文本和背景,选择它并点击属性检查器按钮。更新以下值:

    System

    Default

    在下面的空白文本字段中输入Plain,然后添加7:30pm

    System Bold 15.0

    White Color

    time-bg

    ![Figure 12.53: 预约按钮的属性检查器设置 图片

    Figure 12.53: 预约按钮的属性检查器设置

  10. 要验证按钮的宽度和高度,点击大小检查器按钮。在68中更新以下值

    27

    ![Figure 12.54: 预约按钮的大小检查器设置 图片

    Figure 12.54: 预约按钮的大小检查器设置

  11. 如应用浏览中所见,有三个预约按钮。选择按钮并按Command + C复制。按Command + V两次粘贴。你现在应该有三个按钮。按照以下方式排列它们:![Figure 12.55: View showing button arrangement 图片

    Figure 12.55: 显示按钮排列的视图

  12. 在堆叠视图中嵌入用户界面元素使它们更容易管理。你将把所有按钮嵌入到堆叠视图中。点击一个按钮,然后按Shift并点击其他两个按钮。现在所有三个按钮都应该被选中:![Figure 12.56: 显示所有按钮被选中的视图 图片

    Figure 12.56: 显示所有按钮被选中的视图

  13. 编辑器菜单中选择嵌入 | 堆叠视图。这将把所有三个按钮放入一个有 1 行 3 列单元格的堆叠视图中。

  14. 通过检查文档大纲中的堆叠视图的子视图来验证所有按钮现在都是堆叠视图的子视图:![Figure 12.57: View showing all buttons embedded in a stack view 图片

    Figure 12.57: 显示所有按钮嵌入在堆叠视图中的视图

    重要信息

    你可以通过这个链接了解更多关于堆叠视图的信息:developer.apple.com/documentation/uikit/uistackview.

  15. 选择Horizontal

    Fill

    Equal Spacing

    10

    ![Figure 12.58: 堆叠视图的属性检查器设置 图片

    Figure 12.58: 堆叠视图的属性检查器设置

  16. 要在包含视图中定位堆叠视图,点击大小检查器按钮。在0中更新以下值

    29

    ![Figure 12.59: 堆叠视图的大小检查器设置 图片

    Figure 12.59: 堆叠视图的大小检查器设置

  17. 你将为restaurantCell集合视图单元格添加一个容器视图。这个视图将包含一个显示餐厅照片的图像视图。点击库按钮。在过滤器字段中输入uiview。结果中会出现一个视图对象。将其拖入原型单元格:图 12.60:选择视图对象的库

    图 12.60:选择视图对象的库

  18. 选择视图后,点击大小检查器按钮。在11中更新以下值

    42

    316

    200

    图 12.61:容器视图的大小检查器设置

    图 12.61:容器视图的大小检查器设置

  19. 你将为之前添加的容器视图添加一个图像视图。点击库按钮。在过滤器字段中输入image。结果中会出现一个图像视图对象。将其拖入你刚刚添加的视图中:图 12.62:选择图像视图对象的库

    图 12.62:选择图像视图对象的库

  20. 为了为图像视图设置一个临时的占位图像,选择它并点击属性检查器按钮。设置american图 12.63:图像视图的属性检查器设置

    图 12.63:图像视图的属性检查器设置

    你将在第十四章中使用代码加载图像,将数据加载到集合视图中

  21. 为了在容器视图中定位图像视图,选择它并点击大小检查器按钮。在0中更新以下值

    0

    316

    200

    图 12.64:图像视图的大小检查器设置

    图 12.64:图像视图的大小检查器设置

  22. 你将添加标签,用于显示餐厅的名称和它提供的菜系类型。点击库按钮。在过滤器字段中输入label。结果中会出现一个标签对象。将两个标签对象拖入原型单元格:图 12.65:选择标签对象的库

    图 12.65:选择标签对象的库

  23. 其中一个标签将用于餐厅名称。选择一个标签并点击属性检查器按钮。将字体样式设置为System Bold 17.0以配置此标签。图 12.66:名称标签的属性检查器设置

    图 12.66:名称标签的属性检查器设置

  24. 为了在restaurantCell集合视图单元格中定位此标签,选择标签并点击大小检查器按钮。在10中更新以下值

    3

    315

    19

    图 12.67:名称标签的大小检查器设置

    图 12.67:名称标签的大小检查器设置

  25. 另一个标签将用于显示餐厅提供的菜系。选择另一个标签并点击属性检查器按钮。更新以下值以配置其字体和颜色:

    LetsEat Dark Gray

    System 14.0

    图 12.68:餐饮标签的属性检查器设置

    图 12.68:餐饮标签的属性检查器设置

  26. 此标签应位于显示餐厅名称的标签下方。选中标签后,点击大小检查器按钮。更新restaurantCell集合视图单元格中的以下值:

    10

    22

    315

    16

图 12.69:餐饮标签的大小检查器设置

图 12.69:餐饮标签的大小检查器设置

您已为restaurantCell添加了所有元素,并使用大小检查器设置了它们的定位。现在,您需要为它们添加自动布局约束以确保用户界面适应设备屏幕大小和方向。您将在下一节中这样做。

向餐厅单元格集合视图单元格添加自动布局约束

正如您之前为exploreCell集合视图单元格所做的那样,您现在将为restaurantCell中的所有元素添加自动布局约束。由于您已经使用大小检查器定位了元素,它们之间的相对位置和约束值应该已经正确设置,这将使您添加约束变得容易。由于restaurantCell中有许多元素,请在本节中留出时间。按照以下步骤操作:

  1. 要设置包含餐厅名称的标签的约束,请选择顶部的restaurantCell集合视图单元格。验证是否已选择正确的标签。它应该是黑色而不是浅灰色。

  2. 点击“添加新约束”按钮并输入以下值:

    3(包围视图顶部边缘和顶部边缘之间的空间)

    10(包围视图左侧边缘和左侧边缘之间的空间)

    10(包围视图右侧边缘和右侧边缘之间的空间)

    19(顶部和底部边缘之间的空间)

    完成后,点击添加 4 个约束按钮。

  3. 要设置包含餐厅菜系的标签的约束,请选择restaurantCell集合视图单元格。验证是否已选择正确的标签。它应该是浅灰色而不是黑色。

  4. 点击“添加新约束”按钮并输入以下值:

    0(将上一个标签的顶部边缘绑定到底部边缘)

    10(包围视图左侧边缘和左侧边缘之间的空间)

    10(包围视图右侧边缘和右侧边缘之间的空间)

    16(顶部和底部边缘之间的空间)

    完成后,点击添加 4 个约束按钮。

  5. 要设置包含餐厅照片的图像视图的约束,请在文档大纲中选择包含图像视图的视图:![图 12.72:选择容器视图的文档大纲 图 12.72_B17469.jpg

    图 12.72:选择容器视图的文档大纲

  6. 点击“添加新约束”按钮并输入以下值:

    4(标签的顶部边缘和底部边缘之间的空间)

    316(左右边缘之间的空间)

    200(顶部和底部边缘之间的空间)

    点击“完成时添加 3 个约束”按钮。请注意,此视图的水平位置尚未设置。

  7. 点击“对齐”按钮,勾选0。完成时点击“添加 1 个约束”按钮。这会将视图的水平位置设置为包围视图的中心。由于已设置此视图的宽度,可以自动确定左右边缘的位置。

  8. 要设置包含餐厅照片的图像视图的约束,请在文档大纲中选择american:![图 12.73:选择图像视图的文档大纲 图 12.73_B17469.jpg

    图 12.73:选择图像视图的文档大纲

  9. 点击“添加新约束”按钮并输入以下值:

    0

    0

    0

    0

    点击“完成时添加 4 个约束”按钮。这将绑定图像视图的边缘到包围视图。

  10. 要设置包含标签和按钮的视图的约束,请在文档大纲中选择包含可用时间标签和Stack View视图:![图 12.74:选择容器视图的文档大纲 图 12.74_B17469.jpg

    图 12.74:选择容器视图的文档大纲

  11. 点击“添加新约束”按钮并输入以下值:

    3(包含图像视图的视图的顶部边缘和底部边缘之间的空间)

    224(左右边缘之间的空间)

    56(顶部和底部边缘之间的空间)

    点击“完成时添加 3 个约束”按钮。请注意,此视图的水平位置尚未设置。

  12. 点击“对齐”按钮。勾选0。完成时点击“添加 1 个约束”按钮。这会将视图的水平位置设置为内容视图的中心。由于已设置此视图的宽度,可以自动确定左右边缘的位置。

  13. 要设置包含按钮的堆叠视图的约束,请在文档大纲中选择Stack View:![图 12.75:选择 Stack View 的文档大纲 图 12.75_B17469.jpg

    图 12.75:选择 Stack View 的文档大纲

  14. 点击“添加新约束”按钮并输入以下值:

    60(包围视图的左边缘和左边缘之间的空间)的顶部边缘和底部边缘之间的空间)

    0(包围视图的右边缘和右边缘之间的空间)

    27(顶部和底部边缘之间的空间)

    完成后,点击添加 4 个约束按钮。

  15. 要设置可用时间标签的约束,在文档大纲中选择可用时间标签:图 12.76:选中可用时间标签的文档大纲

    图 12.76:选中可用时间标签的文档大纲

  16. 点击添加新约束按钮并输入以下值:

    2(顶部边缘和封装视图顶部边缘之间的空间)

    0(将左侧边缘绑定到封装视图的左侧边缘)

    0(将右侧边缘绑定到封装视图的右侧边缘)

    21(顶部和底部边缘之间的空间)

    完成后,点击添加 4 个约束按钮。

restaurantCell集合视图单元格的所有自动布局约束都已设置。构建并运行你的应用,转到餐厅列表屏幕。你应该看到以下内容:

图 12.77:iOS 模拟器显示完成的集合视图单元格

图 12.77:iOS 模拟器显示完成的restaurantCell集合视图单元格

如你所见,restaurantCell集合视图单元格现在看起来就像应用导览设计,并且已经添加了所有必要的约束。太棒了!

你已经将所有必需的用户界面元素和约束添加到了restaurantCell中。现在你已经完成了对restaurantCell的修改,接下来让我们在下一节中修改位置屏幕中的表格视图单元格。

配置locationCell表格视图单元格

本章最后要做的就是配置locationCell内部的表格视图单元格。按照以下步骤操作:

  1. 1中找到由按钮触发的视图控制器场景:图 12.78:表格视图的属性检查器设置

    图 12.78:表格视图的属性检查器设置

    表格视图中将出现一个原型表格视图单元格。

  2. 要设置单元格的样式和标识符,点击基本

    locationCell

    图 12.79:表格视图单元格的属性检查器设置

    图 12.79:locationCell表格视图单元格的属性检查器设置

    注意,原型表格视图单元格的名称将更改为locationCell。当你将样式从自定义更改为基本时,单元格中应出现单词标题。这只是一个占位符。你将在稍后代码中更改此值。

  3. 要设置单元格的字体大小,点击locationCell20

图 12.80:表格视图单元格中文本的属性检查器设置

图 12.80:locationCell表格视图单元格中文本的属性检查器设置

如果你现在构建并运行你的应用,然后转到位置屏幕,它仍然会显示为空白,因为你还没有添加任何代码来在单元格中显示数据。你将在第十五章“使用表格视图入门”中这样做。

摘要

在本章中,你通过向exploreCell集合视图单元格添加一个图像视图和一个标签,以及必要的约束来修改了它内部的单元格。对于restaurantCell集合视图单元格,你添加了标签、按钮和一个图像视图,配置它显示默认图像,并添加了必要的约束。对于locationCell

你现在知道如何使用 Interface Builder 添加和配置多个用户界面元素,使用大小检查器设置它们的大小和位置,并使用添加新约束和对齐按钮应用必要的约束,以确保与不同屏幕尺寸和方向兼容。这在你设计自己的用户界面时将非常有用。你也应该能够轻松地原型化你自己的应用的外观和流程。

到目前为止,你已经完成了故事板和设计设置。你可以浏览应用应该拥有的每一个屏幕,看看它们的样子,尽管这些屏幕中没有任何实际数据。如果这个应用就像一座正在建造的房子,那么你现在已经建好了所有的墙壁和地板,房子现在可以开始内部装修了。干得好!

这本书的第二部分到此结束。在下一部分,你将开始输入应用运行所需的所有代码。在下一章,你将开始学习更多关于模型-视图-控制器设计模式的知识。你还将了解集合视图是如何工作的,这对于理解探索餐厅列表屏幕的工作方式至关重要。

第三部分:代码

欢迎来到本书的第三部分。在完成用户界面后,您将添加代码以实现应用程序的功能。要显示网格中的数据,您将使用集合视图;要显示列表中的数据,您将使用表格视图。您还将了解如何向地图添加基本和自定义注释。之后,您将学习关于 JSON 文件的知识,以及如何使用它们将实际餐厅数据导入到您的集合视图、表格视图和地图中。接下来,您将添加允许用户添加餐厅评论和照片以及评分餐厅的代码。最后,您将使用 Core Data 使餐厅评论和照片持久化。

本部分包括以下章节:

  • 第十三章**,开始使用 MVC 和集合视图

  • 第十四章**,将数据导入集合视图

  • 第十五章**,开始使用表格视图

  • 第十六章**,开始使用 MapKit

  • 第十七章**,开始使用 JSON 文件

  • 第十八章**,在静态表格视图中显示数据

  • 第十九章**,开始使用自定义 UI 控件

  • 第二十章**,开始使用相机和照片库

  • 第二十一章**,理解 Core Data

到本部分的结尾,您将完成“Let's Eat”应用程序。您将拥有从头开始构建完整应用程序的经验,这对于您构建自己的应用程序将非常有用。让我们开始吧!

第十三章:第十三章:MVC 和集合视图入门

在上一章中,你修改了 探索 屏幕内、餐厅列表 屏幕和 位置 屏幕中的单元格,以匹配 第九章 中的应用导览,“设置用户界面”。你已经完成了 Let's Eat 应用的初始 UI,这标志着本书第二部分的结束。

本章开始本书的第三部分,你将专注于使你的应用工作的代码。在本章中,你将学习 模型-视图-控制器 (MVC) 设计模式以及应用的各个部分如何相互交互。然后,你将使用 playground 以编程方式实现集合视图(这意味着使用代码而不是 storyboards 来实现),以了解集合视图的工作原理。最后,你将回顾在 探索餐厅列表 屏幕中实现的集合视图,以便你可以看到在 storyboards 中实现它们和在编程方式中实现它们的区别。

到本章结束时,你将理解 MVC 设计模式,学习如何以编程方式创建集合视图控制器,以及如何使用集合视图代理和数据源协议。

将涵盖以下主题:

  • 理解模型-视图-控制器设计模式

  • 探索控制器和类

技术要求

本章的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter13 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频,看看代码的实际效果:

bit.ly/3wsOeCZ

创建一个新的 playground 并将其命名为 CollectionViewBasics。你可以使用这个 playground 在阅读本章时输入和运行所有代码。在这样做之前,让我们看一下模型-视图-控制器设计模式,这是一种常用的 iOS 应用编写方法。

理解模型-视图-控制器设计模式

模型-视图-控制器MVC)设计模式是构建 iOS 应用的一种常用方法。MVC 将应用分为三个不同的部分:

  • 模型:这处理数据存储和表示,以及数据处理任务。

  • 视图:这包括用户可以与之交互的所有屏幕上的内容。

  • 控制器:这管理模型和视图之间信息流的流动。

MVC 的一个显著特点是视图和模型不会相互交互;相反,所有通信都由控制器管理。

例如,想象你正在一家餐厅。你查看菜单并选择你想要的东西。然后,服务员过来,接收你的订单,并将其发送给厨师。厨师准备你的订单,当它完成时,服务员取走订单并将其带给你。在这个场景中,菜单是视图,服务员是控制器,厨师是模型。此外,请注意,你与厨房之间的所有互动都只通过服务员进行;你与厨师之间没有互动。

重要信息

要了解更多关于 MVC 的信息,请访问en.wikipedia.org/wiki/Model–view–controller

要了解 MVC 是如何工作的,让我们更多地了解控制器和类。你将看到实现一个必须管理集合视图的视图控制器需要什么,这个视图控制器用于探索屏幕和餐厅列表屏幕。

探索控制器和类

到目前为止,你已经在主故事板文件中使用 Interface Builder 实现了视图控制器场景。你将ExploreViewController,一个管理RestaurantListViewController中集合视图的视图控制器,以及RestaurantListViewController,一个管理餐厅列表屏幕中集合视图的视图控制器,添加到了你的项目中。然而,你还没有学习如何你的代码在各个视图控制器中工作,所以现在让我们看看这一点。

小贴士

你可能希望重新阅读第十章构建你的用户界面,其中你创建了ExploreViewController类,以及第十一章完成你的用户界面,其中你创建了RestaurantListViewController类。

当一个典型的 iOS 应用启动时,将加载要显示的第一个屏幕的视图控制器。视图控制器有一个view属性,并自动加载分配给其view属性的视图实例。该视图可能有子视图,这些子视图也会被加载。如果一个子视图是集合视图,它将具有dataSourcedelegate属性。dataSource属性被分配给一个为集合视图提供数据的对象。delegate属性被分配给一个处理与集合视图用户交互的对象。通常,集合视图的视图控制器也会被分配到集合视图的dataSourcedelegate属性。集合视图将向其视图控制器发送的方法调用在UICollectionViewDataSourceUICollectionViewDelegate协议中声明。请记住,协议只提供方法声明;这些方法调用的实现是在视图控制器中。然后,视图控制器将为集合视图提供所需的数据或处理用户交互。

在下一节中,让我们更仔细地看看集合视图和集合视图协议。

理解集合视图

收集视图通过可定制的布局显示有序的收集视图单元格集合。

重要信息

要了解更多关于收集视图的信息,您可以参考 developer.apple.com/documentation/uikit/uicollectionview

收集视图的布局由 UICollectionViewFlowLayout 决定。它决定了收集视图中元素的方向和大小。

重要信息

要了解更多关于 UICollectionViewFlowLayout 的信息,您可以参考 developer.apple.com/documentation/uikit/uicollectionviewflowlayout

收集视图显示的数据通常由视图控制器提供。为收集视图提供数据的视图控制器必须遵守 UICollectionViewDataSource 协议。该协议声明了一系列方法,告诉收集视图显示多少个单元格以及每个单元格中显示什么。它还涵盖了辅助视图(如收集视图分区标题)的创建和配置。

重要信息

要了解更多关于 UICollectionViewDataSource 协议的信息,您可以参考 developer.apple.com/documentation/uikit/uicollectionviewdatasource

为了提供用户交互,收集视图的视图控制器还必须遵守 UICollectionViewDelegate 协议,该协议声明了一系列在用户与收集视图交互时被触发的方法。

重要信息

要了解更多关于 UICollectionViewDelegate 协议的信息,您可以参考 developer.apple.com/documentation/uikit/uicollectionviewdelegate

要理解收集视图的工作原理,您将在 CollectionViewBasics 游乐场中实现一个控制收集视图的视图控制器。然后,您将比较下一节中 探索餐厅列表 屏幕中的视图控制器实现。由于游乐场中没有故事板,您不能像之前章节中那样添加 UI 元素。相反,您将按编程方式添加它们。

重要信息

在下一章(第十四章,“将数据放入收集视图”)中,将介绍如何将模型对象添加到收集视图中。

您将首先创建 CollectionViewExampleController 类,这是一个视图控制器,用于管理收集视图。之后,您将创建一个 CollectionViewExampleController 实例,并在游乐场的实时视图中显示包含单个收集视图单元格的收集视图。按照以下步骤操作:

  1. 打开您在本章开头创建的 CollectionViewBasics 游乐场。在游乐场的最顶部,删除 var 语句并添加 import PlaygroundSupport 语句。您的游乐场现在应包含以下内容:

    import UIKit
    import statement imports the API for creating iOS apps. The second statement enables the playground to display a live view, which you will use to display the collection view.
    
  2. import 语句之后添加以下代码以声明 CollectionViewExampleController 类:

    class CollectionViewExampleController: UIViewController {
    }
    

    此类是 UIViewController 的子类,这是一个苹果公司提供的用于管理屏幕上视图的类。

  3. 在大括号内添加以下代码以向 CollectionViewExampleController 类添加一个可选属性,collectionView

    var collectionView: UICollectionView?
    

    将为该属性分配一个集合视图的实例。

  4. 确保您的代码看起来如下:

    class CollectionViewExampleController: UIViewController {
       var collectionView: UICollectionView?
    }
    

在下一节中,您将学习如何设置集合视图显示的单元格数量以及如何设置每个单元格的内容。

遵循 UICollectionViewDataSource 协议

集合视图在屏幕上显示一个集合视图单元格的网格。然而,在它能够这样做之前,它需要知道要显示多少个单元格以及每个单元格中要放置什么内容。为了向集合视图提供这些信息,您将使 CollectionViewExampleController 类遵循 UICollectionViewDataSource 协议。

此协议有两个必需的方法:

  • collectionView(_:numberOfItemsInSection:) 由集合视图调用,以确定应显示多少个集合视图单元格。

  • collectionView(_:cellForItemAt:) 由集合视图调用,以确定在每个集合视图单元格中显示什么。

让我们添加一些代码使 CollectionViewExampleController 遵循 UICollectionViewDataSource 协议。按照以下步骤操作:

  1. 要使 CollectionViewExampleController 采用 UICollectionViewDataSource 协议,在超类声明后输入一个逗号,然后输入 UICollectionViewDataSource。完成时,您的代码应如下所示:

    class CollectionViewExampleController: UIViewController, UICollectionViewDataSource {
       var collectionView:UICollectionView?
    }
    
  2. 由于您尚未实现两个必需的方法,将出现错误。点击错误图标:图 13.1:显示错误图标的编辑区域

    图 13.1:显示错误图标的编辑区域

  3. 错误消息表明缺少 UICollectionViewDataSource 协议的必需方法。点击 修复 按钮以添加必需的方法:图 13.2:错误解释和修复按钮

    图 13.2:错误解释和修复按钮

  4. 确保您的代码看起来如下:

    class CollectionViewExampleController: UIViewController, UICollectionViewDataSource {
       func collectionView(_ collectionView:
    UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          code
       }
    func collectionView(_ collectionView: 
    UICollectionView, cellForItemAt indexPath: 
       IndexPath) -> UICollectionViewCell{
          code
       }
       var collectionView:UICollectionView?
    }
    
  5. 在类定义中,惯例规定属性应在类顶部声明,在所有方法声明之前。重新排列代码,使 collectionView 属性声明位于顶部,如下所示:

    class CollectionViewExampleController: UIViewController, UICollectionViewDataSource {
       var collectionView:UICollectionView?
       func collectionView(_ collectionView:
       UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          code
       }
    
  6. collectionView(_:numberOfItemsInSection:) 中,点击单词 code 并输入 1。完成的方法应如下所示:

    func collectionView(_ collectionView: 
    UICollectionView, numberOfItemsInSection 
    section: Int) -> Int {
       collectionView instance to display a single collection view cell. Typically, the number of cells to be displayed will be provided by a model object. You will learn more about them in *Chapter 14*, *Getting Data into Collection Views*.
    
  7. collectionView(_:cellForItemAt:) 中,点击单词 code 并按如下方式修改方法:

    func collectionView(_ collectionView: 
    UICollectionView, cellForItemAt indexPath: 
    IndexPath) -> UICollectionViewCell{
       let cell = collectionView.dequeueReusableCell
       (withReuseIdentifier: "BoxCell", for: indexPath)
    cell.backgroundColor = .red 
       return cell
    }
    

    下面是这个方法的工作原理。想象一下,你需要在集合视图中显示 1,000 个项目。你不需要 1,000 个集合视图单元格;你只需要足够多的单元格来填满屏幕。滚动出屏幕顶部的集合视图单元格可以重用来显示屏幕底部的项目。为了确保你使用的是正确的单元格类型,你使用重用标识符来识别单元格类型。重用标识符需要与集合视图注册,你将在稍后进行注册。下一行代码将单元格的背景颜色设置为红色,下一行代码返回单元格,然后单元格在屏幕上显示。这个过程会重复进行,直到第一行方法中指定的单元格数量,在这种情况下是 1。

  8. 确认你的CollectionViewExampleController类看起来如下:

    class CollectionViewExampleController: 
    UIViewController, UICollectionViewDataSource {
       var collectionView: UICollectionView?
       func collectionView(_ collectionView:
       UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          1
       }
       func collectionView(_ collectionView: 
       UICollectionView, cellForItemAt indexPath: 
       IndexPath) -> UICollectionViewCell{
          let cell = collectionView.dequeueReusableCell
          (withReuseIdentifier: "BoxCell", for: indexPath)
          cell.backgroundColor = .red 
          return cell
       }
    }
    

你已经完成了CollectionViewExampleController类的实现。在下一节中,你将学习如何创建此类的实例。

创建CollectionViewExampleController实例

现在你已经声明并定义了CollectionViewExampleController类,你需要编写一个方法来创建它的实例。按照以下步骤操作:

  1. collectionView变量声明之后输入以下代码以声明一个新的方法:

    func createCollectionView() {
    }
    

    这声明了一个新的方法createCollectionView(),你将使用它来创建集合视图的实例并将其分配给collectionView属性。

  2. 在大括号开头的代码之后输入以下代码以定义此方法的主体:

    self.collectionView = UICollectionView(
    frame: CGRect(x: 0, y: 0, width: 
    self.view.frame.width, height: 
    self.view.frame.height),
    collectionViewLayout: UICollectionViewFlowLayout())
    

    这将创建一个新的集合视图实例并将其分配给collectionView。这个集合视图的尺寸与其包含视图完全相同,并使用默认的流布局。流布局决定了集合视图单元格的显示顺序,即从左到右。

  3. 进入下一行,然后输入以下代码以将集合视图的dataSource属性设置为CollectionViewExampleController的一个实例:

    self.collectionView?.dataSource = self
    

    集合视图的dataSource属性将指定哪个对象包含所需的UIViewControllerDataSource方法的实现。

  4. 进入下一行,然后输入以下代码以将集合视图的背景颜色设置为白色:

    self.collectionView?.backgroundColor = .white
    
  5. 进入下一行,然后输入以下代码以将集合视图中单元格的标识符设置为BoxCell

    self.collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier:"BoxCell")
    

    此标识符将在collectionView(_:cellForItemAt:)方法中使用,以识别要重用的集合视图单元格的类型。

  6. 进入下一行,然后输入以下代码将集合视图添加为CollectionViewExampleController实例视图的子视图:

    self.view.addSubview(self.collectionView!)
    

    当一个视图控制器实例被加载到内存中时,其视图也会被加载,以及任何子视图。在这种情况下,CollectionViewExampleController实例将自动加载其视图,由于集合视图是其视图的子视图,因此集合视图也将被加载。

  7. 确认完成的方法看起来如下:

    func createCollectionView() {
       self.collectionView = UICollectionView(frame:
       CGRect(x: 0, y: 0, width: self.view.frame.width,
       height: self.view.frame.height),
       collectionViewLayout: 
       UICollectionViewFlowLayout()) 
       self.collectionView?.dataSource = self
       self.collectionView?.backgroundColor = .white
       self.collectionView?.register(
       UICollectionViewCell.self, 
       forCellWithReuseIdentifier: "BoxCell")   
       self.view.addSubview(self.collectionView!)
    }
    

现在你需要一个合适的地方来调用此方法。视图控制器有一个view属性。分配给view属性的视图将在视图控制器加载时自动加载。在视图成功加载后,将调用视图控制器的viewDidLoad()方法。你将在CollectionViewControllerExample类中重写viewDidLoad()方法以调用createCollectionView()。按照以下步骤操作:

  1. createCollectionView()方法之前输入以下代码:

    override func viewDidLoad(){ 
       super.viewDidLoad() 
       self.view.bounds = CGRect(x: 0, y: 0, width: 375,
       height: 667)
       createCollectionView()
    }
    

    这设置了实时预览的大小,创建了一个集合视图实例,将其分配给collectionView,并将其作为子视图添加到CollectionViewExampleController实例的视图中。然后,集合视图会调用数据源方法以确定要显示多少个集合视图单元格以及每个单元格中显示什么内容。

    collectionView(_:numberOfItemsInSection:)返回1,因此将显示单个集合视图单元格。

    collectionView(_:cellForItemAt:)创建单元格,将单元格的背景颜色设置为红色,并将其返回以供显示。

  2. 确认你的完成后的 playground 看起来如下:

    import UIKit
    import PlaygroundSupport
    class CollectionViewExampleController:
    UIViewController, UICollectionViewDataSource{
       var collectionView: UICollectionView?
       override func viewDidLoad(){ 
          super.viewDidLoad()
          self.view.bounds = CGRect(x: 0, y: 0, 
          width: 375, height: 667)
          createCollectionView()
       }
       func createCollectionView(){
          self.collectionView = UICollectionView(frame:
          CGRect(x: 0, y: 0, width: self.view.frame.width,
          height: self.view.frame.height),
          collectionViewLayout: 
          UICollectionViewFlowLayout()) 
          self.collectionView?.dataSource = self
          self.collectionView?.backgroundColor = .white
          self.collectionView?.register(
          UICollectionViewCell.self,
          forCellWithReuseIdentifier: "BoxCell")
          self.view.addSubview(self.collectionView!)
       }
       func collectionView(_ collectionView: 
       UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          1
       }
       func collectionView(_ collectionView:
       UICollectionView, cellForItemAt indexPath: 
       IndexPath) -> UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(
          withReuseIdentifier: "BoxCell", for: indexPath)
          cell.backgroundColor = .red 
          return cell
       }
    }
    
  3. 现在是时候看到它的实际效果了。在 playground 中的所有其他代码之后输入以下内容:

    PlaygroundPage.current.liveView = CollectionViewExampleController()
    

    此命令创建CollectionViewExampleController的实例,并在 playground 的实时预览中显示其视图。createCollectionView()方法将创建一个集合视图并将其作为子视图添加到CollectionViewExampleController实例的视图中,它将出现在屏幕上。

  4. 运行 playground。如果你在屏幕上没有看到集合视图的表示,你需要打开 playground 的实时预览。点击调整编辑器选项按钮:图 13.3:调整编辑器选项按钮

    图 13.3:调整编辑器选项按钮

  5. 确保已选择实时预览图 13.4:选择实时预览的编辑器选项菜单

    图 13.4:选择实时预览的编辑器选项菜单

  6. 你将在实时预览中看到集合视图显示一个红色集合视图单元格:

图 13.5:显示包含一个集合视图单元格的集合视图的 Playground 实时预览

图 13.5:显示包含一个集合视图单元格的集合视图的 Playground 实时预览

你刚刚创建了一个集合视图的视图控制器,创建了一个其实例,并在 playground 的实时预览中显示了一个集合视图。做得好!

在下一节中,您将重新审视在 探索餐厅列表 屏幕中使用集合视图控制器的方式,这些屏幕是在 第十章**,构建用户界面第十一章**,完成用户界面 中实现的。使用本节学到的知识作为参考,您应该能够理解它们是如何工作的。

重新审视探索和餐厅列表屏幕

记得您在 第十章**,构建用户界面 中添加的 ExploreViewController 类,以及您在第十一章**,完成用户界面 中添加的 RestaurantListViewController 类?这两个都是管理集合视图的视图控制器示例。请注意,这两个中的代码与您在游乐场中的代码非常相似。区别如下:

  • 您在 collectionView(_:cellForItemAt:) 中通过编程方式设置单元格的背景颜色,而不是在属性检查器中设置。

  • 您通过编程方式在 CollectionViewExampleController 中创建并分配集合视图到 collectionView 属性。

  • 您在 UICollectionView(frame:collectionViewLayout:) 中通过编程方式设置集合视图的尺寸,而不是使用尺寸检查器。

  • 您通过编程方式将数据源出口连接到视图控制器,而不是使用连接检查器。

  • 您可以通过编程方式设置集合视图的背景颜色,而不是使用属性检查器。

  • 您可以通过编程方式设置集合视图单元格的重用标识符,而不是使用属性检查器。

  • 您通过编程方式将集合视图作为 CollectionViewExampleController 视图的子视图添加,而不是从库中拖入 集合视图 对象。

打开 LetsEat 项目。再次回顾 第十章**,构建用户界面第十一章**,完成用户界面,以便比较和对照使用故事板实现的集合视图,以及像本章中那样通过编程实现。

摘要

在本章中,您详细学习了 MVC 设计模式和集合视图控制器。然后,您重新审视了在 探索餐厅列表 屏幕中使用的集合视图,并学习了它们是如何工作的。

您现在应该理解了 MVC 设计模式、如何创建集合视图控制器以及如何使用集合视图数据源协议。这将使您能够为您的应用程序实现集合视图控制器。

到目前为止,您已经为ExploreViewController实例设置了视图和视图控制器,以便它可以通过探索屏幕中的集合视图进行显示。

第十四章:第十四章:将数据放入集合视图

在上一章中,你学习了模型-视图-控制器(MVC)设计模式和集合视图。你还回顾了探索餐厅列表屏幕,并看到了这两个屏幕中的集合视图是如何工作的。然而,到目前为止,这两个屏幕只是显示不包含任何数据的单元格。如第九章“设置用户界面”中的应用程序导游所示,探索屏幕应显示一系列菜系,而餐厅列表屏幕应显示一系列餐厅。

在本章中,你将实现用于探索屏幕的模型对象,使其显示一系列菜系。你将从学习你将使用的模型对象开始。接下来,你将学习属性列表,并了解它们如何用于存储菜系数据,你还将创建一个 Swift 结构,可以存储菜系实例。之后,你将创建一个数据管理类,从属性列表中读取数据并填充结构数组。这个结构数组然后将作为探索屏幕中集合视图的数据源。

到本章结束时,你将学习如何使用属性列表存储数据,如何创建模型对象,如何创建一个可以从属性列表中加载数据到模型对象数组的数据库管理类,如何配置视图控制器以向集合视图提供模型对象,以及如何配置集合视图以在屏幕上显示数据。

以下内容将涵盖:

  • 理解模型对象

  • 理解.plist文件

  • 创建一个表示菜系的结构

  • 实现一个数据管理类以从.plist文件中读取数据

  • 在集合视图中显示数据

技术要求

你将继续在第十二章“修改和配置单元格”中修改的LetsEat项目上工作。

本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter14文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际效果:

bit.ly/3mWQ8sJ

让我们从查看所需的不同模型对象开始,这些模型对象用于存储初始数据,将数据加载到应用程序中,并在应用程序内存储数据。

理解模型对象

如你在第十三章“开始使用 MVC 和集合视图”中学到的,iOS 应用程序的一个常见设计模式是模型-视图-控制器,或 MVC。为了回顾,MVC 将应用程序分为三个不同的部分:

  • 模型:这处理数据存储、表示和数据处理任务。

  • 视图:这是用户可以与之交互的屏幕上的任何内容。

  • 控制器:这管理着模型和视图之间信息流的流程。

让我们回顾一下在应用导览中看到的 探索 屏幕的设计,它看起来像这样:

图 14.1:iOS 模拟器显示应用导览中的探索屏幕

图 14.1:iOS 模拟器显示应用导览中的探索屏幕

构建并运行您的应用,探索屏幕将看起来像这样:

图 14.2:iOS 模拟器显示您的应用中的探索屏幕

图 14.2:iOS 模拟器显示您的应用中的探索屏幕

如您所见,所有的单元格目前都是空的。根据 MVC 设计模式,您已经完成了视图(集合视图部分标题和集合视图)和控制器(ExploreViewController 类)的实现。现在,您将添加模型对象,这些对象将提供要显示的数据。

首先,您将在项目中添加一个属性列表,ExploreData.plist,它包含每个菜谱的名称和图像文件名。菜谱图像本身已经存在于您的 Assets.xcassets 文件夹中。接下来,您将创建一个模型对象,ExploreItem,它将是一个具有两个属性的构造型。一个属性将用于存储菜谱名称,另一个将用于存储图像文件名。之后,您将创建一个数据管理类,ExploreDataManager,它将从 ExploreData.plist 加载数据,将其放入 ExploreItem 实例的数组中,并将数组提供给 ExploreViewController 实例。最后,您将修改 ExploreViewController 类,使其能够为集合视图提供数据以显示。

要了解更多关于属性列表文件的信息,现在让我们将 ExploreData.plist 添加到您的项目中,看看它是如何存储菜谱数据的。

理解 .plist 文件

苹果开发了属性列表来存储数据结构或对象状态,以便稍后重新构成和传输。它们通常用于存储应用程序的首选项。属性列表文件使用 .plist 文件名扩展名,因此通常被称为 .plist 文件。您将在项目中使用包含菜谱数据的 .plist 文件,ExploreData.plist

您需要下载此书的代码包以获取 ExploreData.plist 文件。之后,您可以使用 Xcode 来查看其内容。请按照以下步骤操作:

  1. 如果您还没有这样做,请从以下链接下载资源文件和此 Xcode 项目:github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

  2. 打开 Chapter14 文件夹,然后查看 resources 文件夹以找到 ExploreData.plist。此文件存储菜谱名称和图像文件名。

  3. 打开 LetsEat 项目,在项目导航器中右键单击 Explore 文件夹,然后选择 新建组

  4. 将您刚刚添加的新组重命名为 Model

  5. ExploreData.plist 拖动到该组中。

  6. 确保已勾选 如果需要则复制项目,然后点击 完成

当您在项目导航器中单击 ExploreData.plist 时,您将看到一个包含字典的数组,如下所示:

图 14.3:编辑区域显示 ExploreData.plist 的内容

图 14.3:编辑区域显示 ExploreData.plist 的内容

每个字典都有两个元素。第一个元素有一个键 name,描述了一种菜系类型。第二个元素有一个键 image,包含一个菜系图像的文件名。所有菜系图像都存储在项目中的 Assets.xcassets 文件中。

要使用 ExploreData.plist 中包含的数据,您需要在应用程序中创建一个结构来存储它,以便以后可以通过 ExploreViewController 实例访问它。您将在下一节中创建它。

创建一个结构来表示一种菜系

要创建一个可以在您的应用程序中表示菜系的模型对象,您需要在项目中添加一个新文件 ExploreItem,并声明一个具有菜系名称和图像属性的 ExploreItem 结构。按照以下步骤操作:

  1. 右键单击 Model 文件夹,然后选择 新建文件

  2. iOS 应已选中。选择 Swift 文件,然后点击 下一步

  3. 将文件命名为 ExploreItem,然后点击 import 语句。

    重要信息

    import 语句允许您将其他代码库导入到项目中,从而让您能够使用它们中的类、属性和方法。Foundation 是 Apple 的核心框架之一,您可以在以下位置了解更多信息:developer.apple.com/documentation/foundation

  4. 将以下代码添加到文件中,以声明一个名为 ExploreItem 的结构:

    struct ExploreItem {
    }
    
  5. 在最后一个大括号之前添加以下代码,以向 ExploreItem 结构添加两个可选的 String 属性:

    Struct ExploreItem {
    name property will store the cuisine name, and the image property will store the filename of an image from the Assets.xcassets file.Important InformationStructures are covered in *Chapter 7*, *Classes, Structures, and Enumerations.*
    

结构会自动获得默认初始化器。您可以通过以下语句创建 ExploreItem 的实例:

let myExploreItem = ExploreItem(name:"name", 
image:"image")

然而,ExploreData.plist 中的名称和图像文件名存储为字典元素,如下例所示:

["name": "All", "image": "all.png"]

重要信息

字典在 第五章集合类型 中介绍。

您将创建一个自定义初始化器,该初始化器接受一个字典作为参数,并将从字典元素中获取的值分配给 ExploreItem 实例中的属性。您将使用扩展来将此自定义初始化器添加到 ExploreItem 结构中。

重要信息

扩展在 第八章协议、扩展和错误处理 中介绍。

按照以下步骤操作:

  1. ExploreItem 结构声明之后输入以下内容以添加一个扩展:

    extension ExploreItem {
    
    }
    

    你将使用这个扩展来向 ExploreItem 结构添加初始化器方法。

  2. 在扩展的大括号之间添加以下内容以声明一个自定义初始化器方法:

    init(dict: [String: String]) {
    
    }
    

    忽略出现的错误,因为你将很快修复它。这个初始化器接受一个字典作为参数。注意键和值都是字符串。

  3. 在初始化器的大括号之间添加以下内容:

    self.name = dict["name"]
    self.image = dict["image"]
    

    这将字典中键为 "name" 的项的值分配给 name 属性,将键为 "image" 的项的值分配给 image 属性。

    完成的扩展应如下所示:

    extension ExploreItem {
       init(dict: [String: String]) {
          self.name = dict["name"]
          self.image = dict["image"]
       }
    }
    

    到目前为止,你有一个结构,ExploreItem,它有两个 String 属性,nameimage。当你创建这个结构的实例时,你将传递一个包含键和值的字典,键和值都是 String 类型。name 键的值将被分配给 name 属性,image 键的值将被分配给 image 属性。

在下一节中,你将实现一个数据管理类。这将读取 ExploreData.plist 文件中的字典数组并将字典元素的值分配给 ExploreItem 实例。

实现一个数据管理类以从 .plist 文件读取数据

你已经向项目中添加了一个 .plist 文件,ExploreData.plist,其中包含餐饮数据,并且你已经创建了一个结构,ExploreItem,可以存储每个餐饮的详细信息。现在,你需要创建一个新的类,ExploreDataManager,它可以读取 .plist 文件中的数据并将其存储在 ExploreItem 实例的数组中。你将把这个类称为数据管理类。按照以下步骤操作:

  1. 右键点击 Model 文件夹并选择 New File

  2. iOS 应该已经选中。选择 Swift File,然后点击 Next

  3. 将文件命名为 ExploreDataManager 并点击 Create 以在编辑器区域显示其内容。

  4. 在文件中输入以下代码以声明 ExploreDataManager 类:

    class ExploreDataManager {
    
    }
    
  5. 在大括号之间输入以下代码以实现一个方法,loadData(),它将读取 ExploreData.plist 文件的全部内容并返回一个字典数组:

    private func loadData() -> [[String: String]] {
       let decoder = PropertyListDecoder()
       if let path = Bundle.main.path(forResource:
       "ExploreData", ofType: "plist"), 
       let exploreData = FileManager.default.contents(
       atPath: path), 
       let exploreItems = try? decoder.decode([[String: 
       String]].self, from: exploreData) {
          return exploreItems
       }
       return [[:]]
    }
    

    让我们分解这个方法:

    private
    

    private 关键字表示该方法只能在这个类内部使用。

    func loadData() -> [[String: String]]
    

    loadData() 方法声明没有参数,并返回一个字典数组,每个字典包含键和值都是 String 类型的元素。

    let decoder = PropertyListDecoder()
    

    这个语句创建了一个属性列表解码器实例,它将用于解码 ExploreData.plist 文件中的数据。

    if let path = Bundle.main.path(forResource:
    "ExploreData", ofType: "plist"),
    

    当你构建你的应用时,结果是一个包含所有应用资源的文件夹,称为应用程序包。ExploreData.plist 就在这个包内。这个语句试图获取 ExploreData.plist 文件的路径并将其分配给一个常量,path

    let exploreData = FileManager.default.contents(
    atPath: path),
    

    此语句尝试从 path 获取存储的 ExploreData.plist 文件并将其分配给一个常量,exploreData

    let exploreItems = try? decoder.decode([[String: 
    String]].self, from: exploreData) {
    

    如果您在项目中点击 ExploreData.plist,请注意,根级别的对象是一个数组,并且数组中的每个项目都是一个字典,如下所示:

图 14.4:显示 ExploreData.plist 中数组和字典的编辑区域

图 14.4:显示 ExploreData.plist 中数组和字典的编辑区域

图 14.4:显示 ExploreData.plist 中数组和字典的编辑区域

此语句尝试从 ExploreData.plist 文件的内容创建一个数组并将其分配给一个常量,exploreItems

   return exploreItems
}

如果可选绑定成功,此语句将返回 exploreItems 作为字典数组,并且每个字典都是 [String: String] 类型:

   return [[:]]
}

如果可选绑定失败,将返回一个空的字典数组。

到目前为止,您有一个数据管理器类,ExploreDataManager,它包含一个方法,可以从 ExploreData.plist 文件加载数据并将其分配给一个数组,exploreItems。在下一节中,您将了解如何使用该数组中的字典来初始化 ExploreItem 实例,这些实例最终将被传递给管理 Explore 屏幕的 ExploreViewController 实例。

使用数据管理器初始化 ExploreItem 实例

目前,您有一个数据管理器类,ExploreDataManager。此类包含一个方法,loadData(),它从 ExploreData.plist 读取数据并返回一个字典数组。您将添加一个方法,使用该数组中的字典创建和初始化 ExploreItem 实例。按照以下步骤操作:

  1. ExploreDataManager 类定义中,在 loadData() 方法之前添加以下代码以实现一个 fetch() 方法:

    func fetch() {
       for data in loadData() {
          print(data)
       }
    }
    

    此方法将调用 loadData(),它返回一个字典数组。然后使用 for 循环将数组中每个字典的内容打印到调试区域,以确保已成功读取 ExploreData.plist 文件。

  2. 在项目导航器中点击 ExploreViewController 文件。将以下代码添加到 viewDidLoad() 方法中。这将在 ExploreViewController 实例加载其视图时创建一个 ExploreDataManager 实例并调用其 fetch() 方法:

    override func viewDidLoad() {
       super.viewDidLoad()
    ExploreData.plist file are read and printed in the Debug area, as shown:![Figure 14.5: Debug area displaying contents of ExploreData.plist    ](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_14.05_B17469.jpg)Figure 14.5: Debug area displaying contents of ExploreData.plistNow, you'll assign the `name` and `image` strings from each dictionary in the array to an `ExploreItem` instance.
    
  3. 点击 ExploreDataManager 文件。在 fetch() 方法之前添加一个属性声明来存储 ExploreItem 实例数组:

    private var exploreItems: [ExploreItem] = []
    

    这向 ExploreDataManager 类添加一个属性 exploreItems 并将其分配给一个空数组。

  4. fetch() 方法内部,将 print() 语句替换为以下语句,该语句初始化 ExploreItem 实例并将它们追加到 exploreItems 数组中:

    exploreItems.append(ExploreItem(dict: data))
    

    ExploreItem 类中的自定义初始化器将 ExploreData.plist 中读取的每个字典中的 nameimage 字符串分配给 ExploreData 实例的 nameimage 属性。

  5. 验证 ExploreDataManager 文件的内容看起来如下:

    import Foundation
    class ExploreDataManager {
       private var exploreItems: [ExploreItem] = []
       func fetch() {
          for data in loadData() {
             exploreItems.append(ExploreItem(dict: 
             data))
          }
       }
       private func loadData() -> [[String: String]] {
          let decoder = PropertyListDecoder()
          if let path = Bundle.main.path(forResource:
          "ExploreData", ofType: "plist"), 
          let exploreData = 
          FileManager.default.contents(atPath: path), 
          let exploreItems = try? 
          decoder.decode([[String: String]].self, 
          from: exploreData) {
             return exploreItems
          }
          return [[:]]
       }
    }
    

到目前为止,您已经有一个ExploreDataManager类,它从ExploreData.plist文件中读取数据并将其存储在exploreItems数组中,这是一个ExploreItem实例的数组。这个数组将是ExploreViewController实例管理的集合视图的数据源。它包含的菜谱信息最终将在探索屏幕中显示。

在集合视图中显示数据

您已经实现了一个数据管理类ExploreDataManager,它从ExploreData.plist文件中读取菜谱数据并将其存储在ExploreItem实例的数组中。现在,您将修改ExploreViewController类,使其使用该数组作为探索屏幕中集合视图的数据源。

目前,在ExploreCell中的集合视图用于此目的。然后,您可以配置集合视图的视图控制器ExploreViewController,从ExploreDataManager实例获取菜谱详情并将其提供给集合视图进行显示。要创建ExploreCell,请按照以下步骤操作:

  1. 在项目导航器中右键点击Explore文件夹并选择新建组

  2. 将新组View重命名。

  3. 右键点击View文件夹并选择新建文件

  4. iOS应该已经选中。选择Cocoa Touch 类,然后点击下一步

  5. 按照以下配置类:

    ExploreCell

    UICollectionViewCell

    未选中

    Swift

    点击下一步

  6. 点击创建

  7. 将在您的项目中添加一个名为ExploreCell的新文件。在其中您将看到以下内容:

    import UIKit
    class ExploreCell: UICollectionViewCell {
    }
    

    重要信息

    UIKit为 iOS 应用提供了所需的基础设施。您可以在这里了解更多信息:developer.apple.com/documentation/uikit

  8. 现在,您将ExploreCell类分配为exploreCell集合视图单元格的标识。在项目导航器中点击Main故事板文件,然后在文档大纲中的探索视图控制器场景内点击exploreCell。点击标识检查器按钮:![Figure 14.6:选择身份检查器]

    ![img/Figure_14.06_B17469.jpg]

    图 14.6:选择身份检查器

  9. ExploreCell下。这设置了一个ExploreCell实例作为exploreCell的管理器。完成设置后按回车键:

![Figure 14.7:显示 exploreCell 类设置的标识检查器]

![img/Figure_14.07_B17469.jpg]

图 14.7:显示 exploreCell 类设置的标识检查器

您刚刚声明并定义了ExploreCell类,并将其分配为exploreCell集合视图单元格的管理器。现在,您将在该类中创建输出,这些输出将连接到exploreCell集合视图单元格中的图像视图和标签,以便您可以控制它们显示的内容。

连接 exploreCell 中的输出

为了管理exploreCell集合视图中单元格显示的内容,将其连接到ExploreCell类中的输出。您将使用辅助编辑器来完成此操作。请按照以下步骤操作:

  1. 点击导航器和检查器按钮以隐藏导航器和检查器区域。这将为您提供更多的工作空间:![图 14.8:显示导航器和检查器按钮的工具栏 图片 14.08_B17469.jpg

    图 14.8:显示导航器和检查器按钮的工具栏

  2. 点击调整编辑器选项按钮以显示菜单:![图 14.9:调整编辑器选项按钮 图片 14.09_B17469.jpg

    图 14.9:调整编辑器选项按钮

  3. 从菜单中选择辅助以显示辅助编辑器:![图 14.10:调整编辑器选项菜单,已选择辅助 图片 14.10_B17469.jpg

    图 14.10:调整编辑器选项菜单,已选择辅助

  4. 要为 ExploreCell 类创建输出端口,辅助编辑器路径应设置为 自动 | ExploreCell.swift。如果您在路径中看不到 ExploreCell.swift,请从辅助编辑器的路径下拉菜单中选择 exploreCell 集合视图单元格的 ExploreCell.swift:![图 14.11:显示 ExploreCell.swift 的辅助编辑器栏 图片 14.11_B17469.jpg

    图 14.11:显示 ExploreCell.swift 的辅助编辑器栏

  5. Ctrl + 拖动 从集合视图单元格中的标签元素到括号之间的空间,如图所示。这将为它创建一个输出端口:![图 14.12:显示 ExploreCell.swift 的编辑区域 图片 14.12_B17469.jpg

    图 14.12:显示 ExploreCell.swift 的编辑区域

  6. 在弹出对话框中,在名称字段中输入 exploreNameLabel 以设置输出端口的名称:![图 14.13:创建 exploreNameLabel 输出端口的弹出对话框 图片 14.13_B17469.jpg

    图 14.13:创建 exploreNameLabel 输出端口时的弹出对话框

  7. Ctrl + 拖动exploreNameLabel 属性。这将为它创建一个输出端口:![图 14.14:显示 ExploreCell.swift 的编辑区域 图片 14.14_B17469.jpg

    图 14.14:显示 ExploreCell.swift 的编辑区域

  8. 在弹出对话框中,在名称字段中输入 exploreImageView 以设置输出端口的名称:![图 14.15:创建 exploreImageView 输出端口的弹出对话框 图片 14.15_B17469.jpg

    图 14.15:创建 exploreImageView 输出端口的弹出对话框

  9. exploreNameLabelexploreImageView 输出端口已添加到 ExploreCell 类,并连接到 exploreCell 集合视图单元格的图像视图和标签元素,如图所示:![图 14.16:显示 exploreNameLabel 和 exploreImageView 输出端口的编辑区域 图片 14.16_B17469.jpg

    图 14.16:显示 exploreNameLabel 和 exploreImageView 输出端口的编辑区域

  10. 点击x按钮关闭辅助编辑器:

![图 14.17:辅助编辑器关闭按钮图片 14.17_B17469.jpg

图 14.17:辅助编辑器关闭按钮

Main 故事板文件中,exploreCell 集合视图单元格已经设置了一个类,ExploreCell。集合视图单元格的图片视图和标签的出口也已创建并分配。现在,你可以设置 ExploreCell 实例中的 exploreNameLabelexploreImageView 出口,以便在应用运行时在每个单元格中显示菜名和图片。

提示

你可以在连接检查器中检查是否正确连接了出口。

在下一节中,你将在 ExploreDataManager 中添加代码,以提供 collectionView 要显示的单元格数量,并提供一个 ExploreItem 实例,其属性将用于确定单元格将显示的图片和标签。

实现额外的数据管理器方法

如你在 第十三章 “MVC 和集合视图入门” 中所学,集合视图需要知道要显示多少个单元格以及每个单元格中要放入什么内容。你将在 ExploreDataManager 类中添加两个方法,这两个方法将提供 exploreItems 数组中 ExploreItem 实例的数量,并在指定的数组索引处返回一个 ExploreItem 实例。在项目导航器中点击 ExploreDataManager 文件,并在 loadData() 方法之后添加这两个方法:

func numberOfExploreItems() -> Int {
   exploreItems.count
}
func exploreItem(at index: Int) -> ExploreItem {
   exploreItems[index]
}

第一个方法 numberOfExploreItems() 将确定集合视图要显示的单元格数量。

第二个方法 exploreItem(at:) 将返回一个与集合视图中单元格位置相对应的 ExploreItem 实例。

在下一节中,你将更新 ExploreViewController 类中的集合视图数据源方法,以提供集合视图中要显示的单元格数量,并为每个单元格提供菜名和图片。

更新 ExploreViewController 中的数据源方法

ExploreViewController 类中的数据源方法目前设置为显示 20 个单元格,每个单元格包含一个空的图片视图和标签。你将更新它们以从 ExploreDataManager 实例获取要显示的单元格数量以及每个单元格中要放入的数据。按照以下步骤操作:

  1. 点击 ExploreViewController 文件,找到 viewDidLoad() 方法。它应该看起来像这样:

    override func viewDidLoad() {
       super.viewDidLoad()
       let manager = ExploreDataManager()
       manager.fetch()
    }
    

    这意味着 ExploreDataManager 实例仅在 viewDidLoad() 方法中可用。你需要使其对整个类可用。

  2. let manager = ExploreDataManager() 这一行移动到 collectionView 属性声明之后,以便在 ExploreViewController 类的所有方法中都可以使用 ExploreDataManager 实例,如下所示:

    @IBOutlet var collectionView: UICollectionView!
    let manager = ExploreDataManager()
    
  3. 按照以下所示更新 collectionView(_:numberOfItemsInSection:)。这将使集合视图为 items 数组中的每个元素显示一个单元格:

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
       manager.numberOfExploreItems()
    }
    
  4. 按照以下所示更新 collectionView(_:cellForItemAt:),使用来自相应数组元素的 items 数组中的数据来设置每个单元格的图片视图和标签:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
       let cell = collectionView.dequeueReusableCell(
       withReuseIdentifier: "exploreCell", for: 
       indexPath) ExploreCell.
    
    

    let exploreItem = manager.exploreItem(at:

    indexPath.row)

    
    Gets the `ExploreItem` instance that corresponds to the current cell in the collection view. In other words, the first cell in the collection view corresponds to the first `ExploreItem` instance in the `exploreItems` array, the second cell corresponds to the second `ExploreItem` instance, and so on.
    
    

    cell.exploreNameLabel.text = exploreItem.name

    
    Sets the `text` property of the cell's `nameLabel` to the name of the `ExploreItem` instance.
    
    

    cell.exploreImageView.image = UIImage(named:

    exploreItem.image!)

    
    Gets the `image` string from the `ExploreItem` instance, gets the corresponding image from the `Assets.xcassets` file, and assigns it to the `image` of the cell's `imgExplore` property.
    

构建并运行应用。你将在探索屏幕上看到不同菜系的图片和文本的集合视图。点击集合视图中的一个单元格将显示餐厅列表屏幕:

图 14.18:iOS 模拟器显示探索和餐厅列表屏幕

图 14.18:iOS 模拟器显示探索和餐厅列表屏幕

到目前为止,ExploreData.plist文件。在第十七章,“使用 JSON 文件入门”中,你将修改RestaurantListViewController类,使餐厅列表屏幕显示提供所选菜系的餐厅列表。但在你能够做到这一点之前,你需要在位置屏幕中设置一个位置,这将提供一个在该位置所有可用餐厅的列表。这将在下一章中介绍。

摘要

在本章中,你向项目中添加了一个属性列表文件,ExploreData.plist。你实现了ExploreItem结构,它是ExploreDataManager的模型对象,用于从ExploreData.plist读取数据,将数据放入ExploreItem实例的数组中,并将其提供给ExploreViewController。你为exploreCell集合视图单元格创建了一个类。最后,你在ExploreViewController类中配置了数据源方法,使用ExploreItem实例的数组中的数据来填充集合视图,现在探索屏幕显示了一个菜系列表。做得好!

现在,你已经知道如何使用.plist文件向应用提供数据,创建模型对象,创建将.plist文件加载到模型对象中的数据管理类,配置集合视图以显示已加载的数据,以及配置集合视图的视图控制器。如果你希望创建使用集合视图的自己的应用,这将非常有用。

在下一章中,你将了解表格视图,它在某些方面与集合视图相似,并配置位置屏幕,在点击探索屏幕中的位置按钮时,在表格视图中显示位置列表。

第十五章:第十五章:开始使用表格视图

在上一章中,您配置了 ExploreViewController 类,这是 ExploreData.plist 在集合视图中的视图控制器。

在本章中,您将从学习表格视图和表格视图控制器开始。您将使用 playground 以编程方式实现表格视图(这意味着使用代码而不是 storyboards 实现),以了解表格视图的工作原理。接下来,您将从头开始创建一个用于 .plist 文件的表格视图控制器,以保存位置列表,创建一个数据管理类以从 .plist 文件中读取数据,并配置表格视图控制器以从数据管理器获取数据并将其提供给表格视图。然后,位置 屏幕将显示餐厅位置列表。

到本章结束时,您将学会如何创建 .plist 文件,以及如何实现表格视图控制器。这将使您能够在自己的应用中实现使用 .plist 文件作为数据源的 .plist 文件和表格视图。

本章将涵盖以下主题:

  • 理解表格视图

  • 创建 LocationViewController

  • 为表格视图添加位置数据

  • 创建 LocationDataManager

技术要求

您将首先在 playground 中工作,然后继续在上一章中修改的 LetsEat 项目上工作。

本章的 playground 和完成的 Xcode 项目位于本书代码包的 Chapter15 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频以查看代码的实际效果:

bit.ly/3obiApY

让我们从通过在 playground 中实现一个管理表格视图的视图控制器来了解表格视图的工作原理开始。创建一个新的 playground 并将其命名为 TableViewBasics。您可以在接下来的部分中输入并运行所有显示的代码。

理解表格视图

Let's Eat 应用在 位置 屏幕中使用表格视图来显示餐厅位置列表。表格视图使用单列排列的行来呈现表格视图单元格。

重要信息

要了解更多关于表格视图的信息,请访问 developer.apple.com/documentation/uikit/uitableview

表格视图显示的数据通常由视图控制器提供。为表格视图提供数据的视图控制器必须遵守 UITableViewDataSource 协议。此协议声明了一个方法列表,告诉表格视图显示多少个单元格以及每个单元格中显示什么内容。

重要信息

要了解更多关于 UITableViewDataSource 协议的信息,请访问 developer.apple.com/documentation/uikit/uitableviewdatasource

为了提供用户交互,表格视图的视图控制器还必须遵守 UITableViewDelegate 协议,该协议声明了一系列在用户与表格视图交互时被触发的方法。

重要信息

要了解更多关于 UITableViewDelegate 协议的信息,请访问 developer.apple.com/documentation/uikit/uitableviewdelegate

要了解表格视图是如何工作的,你将实现一个视图控制器子类来控制你的 TableViewBasics 游乐场中的表格视图。由于游乐场中没有故事板,你不能像前几章那样使用库来添加 UI 元素。相反,你将完全通过编程方式完成所有操作。按照以下步骤操作:

  1. 打开你在本章开头创建的 TableViewBasics 游乐场。在游乐场的最顶部,删除 var 语句并添加 import PlaygroundSupport 语句。

    你的游乐场现在应该包含以下内容:

    import UIKit
    import statement imports the API for creating iOS apps. The second allows the playground to display a live view, which you will use to display the table view.
    
  2. import 语句之后添加以下代码以声明 TableViewExampleController 类:

    class TableViewExampleController: UIViewController {
    }
    

    此类是 UIViewController 的子类,这是一个苹果公司提供的用于管理屏幕上视图的类。

  3. 在大括号内添加以下代码以声明 TableViewExampleController 类的表格视图属性和数组属性:

    class TableViewExampleController: UIViewController {
    tableView property is an optional property that will be assigned a UITableView instance. The names array is the model object that will be used to provide data to the table view. A table view displays a single column of rows on the screen, and each row contains a table view cell instance. Similar to collection views, table views need to know how many rows to display and what to put in each row. To provide this information to the table view, you will make the `TableViewExampleController` class conform to the `UITableViewDataSourceProtocol`.This protocol has two required methods:*   `tableview(_:numberOfRowsInSection:)` is called by the table view to determine how many table view cells to display. *   `tableView(_:cellForRowAt:)` is called by the table view to determine what to display in each table view cell.
    

让我们添加一些代码使 TableViewExampleController 类遵守 UITableViewDataSource 协议。按照以下步骤操作:

  1. 要使 TableViewExampleController 类采用 UITableViewDataSource 协议,在超类名称后输入一个逗号,然后输入 UITableViewDataSource。你的代码应如下所示:

    class TableViewExampleController: UIViewController, UITableViewDataSource {
    
  2. 将会出现一个错误,因为你还没有实现两个必需的方法。点击错误图标:图 15.1:显示错误图标的编辑区域

    图 15.1:显示错误图标的编辑区域

  3. 出现的错误信息表明缺少 UITableViewDataSource 协议所需的两个方法。点击 修复 按钮将必需的方法存根添加到类中:图 15.2:错误解释和修复按钮

    图 15.2:错误解释和修复按钮

  4. 确认你的代码看起来像这样:

    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 names: [String] = ["Divij","Aamir","Shubham"]
    }
    
  5. 在类定义中,惯例规定属性应在任何方法声明之前在顶部声明。重新排列代码,使属性声明在顶部,如下所示:

    class TableViewExampleController: UIViewController,
    UITableViewDataSource {
       var tableView: UITableView?
       var names: [String] = ["Divij","Aamir","Shubham"]
       func tableView(_ tableView: UITableView, 
       numberOfRowsInSection section: Int) -> Int {
    
  6. 要使表格视图为 names 数组中的每个元素显示一行,请在 tableView(_:numberOfRowsInSection:) 方法定义中的 code 单词处点击,并输入 names.count。完成的方法应如下所示:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       names.count returns the number of elements inside the names array. Since there are three names in it, this will make the table view display three rows.
    
  7. 要使表格视图在每个单元格中显示名称,请在tableView(_:cellForRowAt:)方法定义中的code单词处单击并输入以下内容:

    func tableView(_ tableView: UITableView, cellForRowAt 
    indexPath: IndexPath) -> UITableViewCell {
    cell. Imagine you have 1,000 items to display in a table view. You don't need 1,000 rows containing 1,000 table view cells—you only need just enough to fill the screen. Table view cells that scroll off the top of the screen can be reused to display items that appear at the bottom of the screen. To make sure you are using the right type of cell, you set the reuse identifier to Cell. This reuse identifier will be registered with the table view later.
    
    

    let name = names[indexPath.row]

    
    The `indexPath` value locates the row in the table view. The first row has an `indexPath` containing section `0` and row `0`. `indexPath.row` returns `0` for the first row, so `name` is assigned the first element in the `names` array.
    
    

    cell.textLabel?.text = name

    
    This assigns `name` to the `text` property of the table view cell's `textLabel`.
    
    

    return cell

    
    This returns the table view cell, which is then displayed on the screen.This method is executed for each row in the table view.
    
  8. 确认你的TableViewExampleController类看起来像这样:

    class TableViewExampleController: UIViewController, 
    UITableViewDataSource {
       var tableView: UITableView?
       var names: [String] = ["Divij","Aamir","Shubham"]
       func tableView(_ tableView: UITableView, 
       numberOfRowsInSection section: Int) -> Int {
          names.count
       }
       func tableView(_ tableView: UITableView,
       cellForRowAt indexPath: IndexPath) ->
       UITableViewCell {
          let cell = tableView.dequeueReusableCell(
          withIdentifier: "Cell", for:indexPath) 
          let name = names[indexPath.row]
          cell.textLabel?.text = name 
          return cell
       }
    }
    

你已经完成了TableViewExampleController类的实现。现在你将编写一个名为createTableView()的方法,以创建其实例。在属性声明之后输入以下代码以声明和定义createTableView()方法:

func createTableView() {
   self.tableView = UITableView(frame: CGRect(x: 0, y: 0,
   width: self.view.frame.width, 
   height: self.view.frame.height)) 
   self.tableView?.dataSource = self 
   self.tableView?.backgroundColor = .white 
   self.tableView?.register(UITableViewCell.self,
   forCellReuseIdentifier: "Cell")
   self.view.addSubview(self.tableView!)
}

让我们分解一下:

self.tableView = UITableView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))

这将创建一个与包含视图大小完全相同的UITableView新实例,并将其分配给tableView

self.tableView?.dataSource = self

这告诉表格视图其数据源是TableViewExampleController的一个实例。

self.tableView?.backgroundColor = .white

这将表格视图的背景颜色设置为白色。

self.tableView?.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

这将表格视图单元格的重用标识符设置为"Cell"。此重用标识符将在tableView(_:cellForRowAt:)方法中用于识别可重用的单元格。

self.view.addSubview(self.tableView!)

这将表格视图作为子视图添加到TableViewExampleController实例的视图中。

现在你必须调用此方法。UIViewController类有一个方法viewDidLoad(),当其视图被加载时会被调用。此方法由TableViewExampleController类继承,你需要重写它以调用createTableView()。按照以下步骤操作:

  1. createTableView()方法之前输入以下代码:

    override func viewDidLoad() { 
       super.viewDidLoad() 
       self.view.bounds = CGRect(x: 0, y: 0, 
       width: 375, height: 667)
       createTableView()
    }
    

    这设置了实时视图的大小,创建了一个表格视图并将其作为子视图添加到TableViewExampleController实例的视图中。然后使用数据源方法来确定要显示多少个表格视图单元格以及每个表格视图单元格中要放置的内容。tableView(_:numberOfRowsInSection:)返回3,因此显示三行。tableView(_:cellForRowAt:)将每个单元格的文本设置为names数组中的对应名称。

  2. 确认你的完成代码看起来像这样:

    import UIKit
    import PlaygroundSupport
    class TableViewExampleController: UIViewController, 
    UITableViewDataSource {
       var tableView: UITableView?
       var names: [String] = ["Divij","Aamir","Shubham"]
       override func viewDidLoad() { 
          super.viewDidLoad() 
          self.view.bounds = CGRect(x: 0, y: 0, 
          width: 375, height: 667)
          createTableView()
       }
       func createTableView() {
          self.tableView = UITableView(frame: CGRect(x: 0,
          y: 0, width: self.view.frame.width, height: 
          self.view.frame.height)) 
          self.tableView?.dataSource = self 
          self.tableView?.backgroundColor = .white
          self.tableView?.register(UITableViewCell.self, 
          forCellReuseIdentifier: "Cell")
          self.view.addSubview(self.tableView!)
       }
       func tableView(_ tableView: UITableView, 
       numberOfRowsInSection section: Int) -> Int {
          names.count
       }
       func tableView(_ tableView: UITableView,
       cellForRowAt indexPath: IndexPath) ->
       UITableViewCell {
          let cell = tableView.dequeueReusableCell(
          withIdentifier: "Cell", for:indexPath)
          let name = names[indexPath.row]
          cell.textLabel?.text = name 
          return cell
       }
    }
    
  3. 在 playground 中的所有其他代码之后输入以下内容:

    PlaygroundPage.current.liveView = TableViewExampleController()
    

    此命令创建了一个TableViewExampleController实例,并在 playground 的实时视图中显示其视图。

  4. 运行 playground。如果你看不到表格视图,请点击调整编辑器选项按钮:![图 15.3:调整编辑器选项按钮]

    图片

    ![图 15.3:调整编辑器选项按钮]

  5. 确保从弹出菜单中选择实时视图:![图 15.4:调整带有实时视图选择的编辑器选项菜单]

    图片

    ![图 15.4:调整带有实时视图选择的编辑器选项菜单]

  6. 你将看到表格视图显示一个包含三个名称行的表格,如下所示:

![图 15.5:显示带有名称的表格视图的 playground 实时视图]

图片

![图 15.5:显示带有名称的表格视图的 playground 实时视图]

太好了!现在你已经知道了表格视图的工作原理,让我们完成位置屏幕的实现。你将在下一节中创建这个屏幕的视图控制器,以便它可以管理表格视图将显示的内容。

创建LocationViewController

第九章中应用程序导览所示,设置用户界面locationCell。根据模型-视图-控制器MVC)设计模式,你已经完成了所需的视图,但你还没有完成控制器或模型。

目前,当你点击探索屏幕中的位置按钮时,会显示一个空白的表格视图:

图 15.6:iOS 模拟器显示应用中的位置屏幕

图 15.6:iOS 模拟器显示应用中的位置屏幕

你将创建LocationViewController类作为位置屏幕的视图控制器,向其中添加一个表格视图的输出端口,并将其配置为表格视图的数据源和代理。按照以下步骤操作:

  1. 从上一章打开你的LetsEat项目。通过右键点击LetsEat文件夹并选择视图模型,在项目内创建一个新的文件夹Location。完成后,你将看到以下文件夹结构:图 15.7:项目导航器显示位置文件夹和子文件夹

    图 15.7:项目导航器显示位置文件夹和子文件夹

  2. 右键点击Location文件夹并选择新建文件

  3. iOS应该已经选中。选择Cocoa Touch Class并点击下一步

  4. 使用以下详细信息配置类:

    LocationViewController

    UIViewController

    未选中

    Swift

    点击下一步

  5. 点击LocationViewController文件出现在项目导航器中。

LocationViewController文件已经创建,其中包含LocationViewController类的声明。现在你将设置当你轻触位置按钮时显示的视图控制器场景的标识为此类。按照以下步骤操作:

  1. 在项目导航器中打开Main故事板文件。

  2. 选择当你点击LocationViewController时显示的视图控制器场景。注意场景的名称将更改为Location View Controller Scene

图 15.8:位置视图控制器场景的标识检查器设置

图 15.8:位置视图控制器场景的标识检查器设置

太棒了!在下一节中,让我们将表格视图连接到LocationViewController类中的一个输出端口。通过这样做,位置屏幕的LocationViewController实例将能够管理表格视图。

将表格视图连接到LocationViewController

目前,将LocationViewController类的实例分配给LocationViewController并分配表格视图给它。按照以下步骤操作:

  1. 如果需要,点击导航器和检查器按钮以隐藏导航器和检查器区域。

  2. 点击调整编辑器选项按钮并从菜单中选择辅助

  3. Main故事板文件中,点击文档大纲中的表格视图。辅助编辑器应设置为自动 > LocationViewController.swift,如图所示:![图 15.9:显示LocationViewController.swift的辅助编辑器栏

    ![img/Figure_15.09_B17469.jpg]

    图 15.9:显示LocationViewController.swift的辅助编辑器栏

  4. Ctrl + 拖动viewDidLoad():![图 15.10:显示LocationViewController文件内容的编辑区域

    ![img/Figure_15.10_B17469.jpg]

    图 15.10:显示LocationViewController文件内容的编辑区域

  5. 在弹出菜单中,在名称字段中输入tableView并点击连接:![图 15.11:创建tableView出口的弹出对话框

    ![img/Figure_15.11_B17469.jpg]

    图 15.11:创建tableView出口的弹出对话框

  6. 验证tableView出口是否已添加到LocationViewController类并连接到故事板中的表格视图:![图 15.12:显示LocationViewController文件内容的编辑区域

    ![img/Figure_15.12_B17469.jpg]

    图 15.12:显示LocationViewController文件内容的编辑区域

  7. 点击x按钮关闭辅助编辑器:

![图 15.13:辅助编辑器关闭按钮

![img/Figure_15.13_B17469.jpg]

图 15.13:辅助编辑器关闭按钮

你已经将表格视图连接到了LocationViewController类中的出口。为了使表格视图能够显示数据和响应用户交互,LocationViewController类必须遵守UITableViewDataSourceUITableViewDelegate协议并实现所需的方法。你将在下一节中完成这项工作。

添加数据源和委托方法

表格视图的视图控制器必须采用UITableViewDataSourceUITableViewDelegate协议,并实现所需的方法以允许数据显示和用户交互。在本节中,你将连接LocationViewController到表格视图的dataSourcedelegate出口并实现所需的数据源方法。你将在第十七章**,开始使用 JSON 文件中实现委托方法。按照以下步骤操作:

  1. 如果需要,点击导航器和检查器按钮以显示导航器和检查器区域。

  2. Main故事板文件中,确保你在文档大纲中选择表格视图。点击连接检查器按钮。从dataSourcedelegate出口拖动到文档大纲中的LocationViewController图标:![图 15.14:显示LocationViewController类出口的连接检查器

    ![img/Figure_15.14_B17469.jpg]

    图 15.14:连接检查器显示LocationViewController类的输出端

    这将表格视图连接到LocationViewController类的输出端。

  3. 验证表格视图的dataSourcedelegate属性是否已连接到LocationViewController类的输出端:

图 15.15:连接检查器已设置数据源和代理输出端

图 15.15:显示数据源和代理输出端的连接检查器

图 15.15:连接检查器已设置数据源和代理输出端

接下来,你将使LocationViewController遵守UITableViewDataSource协议并实现该协议所需的这些方法。按照以下步骤操作:

  1. 在项目导航器中点击LocationViewController文件,并从LocationViewController类定义中删除所有注释代码,只留下以下内容:

    class LocationViewController: UIViewController {
       @IBOutlet weak var tableView: UITableView!
       override func viewDidLoad(){
          super.viewDidLoad()
       }
    }
    
  2. 要使LocationViewController采用UITableViewDataSource协议,在超类名称UIViewController后输入逗号,并输入UITableViewDataSource。完成操作后,你的代码应如下所示:

    class LocationViewController: UIViewController, 
    UITableViewDataSource {
    
  3. 由于你尚未实现两个必需的方法,将出现错误。点击错误图标以查看错误信息:![图 15.16:显示错误图标的编辑区域 图 15.16:显示错误图标的编辑区域

    图 15.16:显示错误图标的编辑区域

  4. 点击修复按钮将所需的方法存根添加到类中:![图 15.17:错误解释和修复按钮 图 15.17:显示错误解释和修复按钮

    图 15.17:错误解释和修复按钮

  5. 重新排列代码,使属性声明和viewDidLoad()方法位于顶部。这遵循了 iOS 开发的一般编码约定,并使你的代码更容易维护。完成操作后,验证你的代码如下所示:

    class LocationViewController: UIViewController, 
    UITableViewDataSource {
       @IBOutlet var tableView: UITableView!
       override func viewDidLoad() {
          super.viewDidLoad()
       }
       func tableView(_ tableView: UITableView,
       numberOfRowsInSection section: Int) -> Int {
          code
       }
       func tableView(_ tableView: UITableView,
       cellForRowAt indexPath: IndexPath) ->
       UITableViewCell {
          code
       }
    }
    
  6. 在第一个必需的方法内部,点击单词code并输入10。这将使表格视图显示 10 行。完整的方法应如下所示:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       10
    }
    
  7. 在第二个必需的方法内部,点击单词code并输入以下内容,使表格视图在每一行显示字符串"A Cell"

    func tableView(_ tableView: UITableView, cellForRowAt 
    indexPath: IndexPath) -> UITableViewCell {
    locationCell identifier and assigns it to cell. You set this identifier in the Main storyboard file in *Chapter 12*, *Modifying and Configuring Cells*.
    
    

    cell.textLabel?.text = "A Cell"

    
    This assigns a string, `"A Cell"` to the `text` property of the table view cell's `textLabel`.
    
    

    return cell

    
    This returns the cell, which is then displayed on the screen. This process is repeated for the number of cells that are given in the first method, which, in this case, is 10.
    

构建并运行你的项目。点击如图所示的A Cell

图 15.18:iOS 模拟器显示表格视图单元格

图 15.18:iOS 模拟器显示表格视图单元格

图 15.18:iOS 模拟器显示表格视图单元格

你已经完成了LocationsViewController类的实现,并且你的表格视图现在正在显示表格视图单元格。太棒了!现在,既然你的表格视图的视图控制器已经设置好了,让我们在下一节创建一些模型对象,以便为它提供数据。

为表格视图添加位置数据

在此阶段,你已经创建并配置了LocationViewController类。这个类的实例将作为包含位置数据的.plist文件中的表格视图的数据源,但你将从头创建一个,并向其中添加位置数据。按照以下步骤操作:

  1. 右键点击Location文件夹中的Model文件夹,并选择新建文件

  2. 在过滤器字段中输入proper属性列表将在窗口中显示。选择属性列表并点击下一步

  3. 将文件命名为Locations,然后点击创建

Locations.plist文件已添加到项目中。在上一章中,你已经看到了ExploreData.plist如何将数据存储为数组字典。你将配置Locations.plist,使其以相同的格式存储位置屏幕的数据,然后将所有餐厅位置添加到其中。按照以下步骤操作:

  1. 点击Locations.plist并将其更改为数组。注意左侧的展开三角形应指向下方。点击+按钮:图 15.19:显示位置.plist 内容的编辑区域

    图 15.19:显示位置.plist 内容的编辑区域

  2. 将一个新的条目项目 0添加到数组中。将类型更改为字典。点击展开三角形使其指向下方。点击+按钮:图 15.20:添加了项目 0 的位置.plist

    图 15.20:添加了项目 0 的位置.plist

  3. 将一个新的条目新条目添加到项目 0字典中。点击+按钮:图 15.21:添加新条目后的位置.plist

    图 15.21:添加了新条目的位置.plist

  4. 将第二个条目添加到city,并将值设置为Aspen。对于第二个条目,将键更改为state,并将值更改为CO图 15.22:在项目 0 中添加了城市和州的位置.plist

    图 15.22:在项目 0 中添加了城市和州的位置.plist

  5. 点击项目 0字典旁边的展开三角形以折叠它:图 15.23:折叠了项目 0 的位置.plist

    图 15.23:折叠了项目 0 的位置.plist

  6. 选择项目 0,然后在键盘上按Command + C复制它,并按Command + V粘贴。你会看到一个新条目,项目 1图 15.24:复制并粘贴了项目 0 的位置.plist

    图 15.24:复制并粘贴了项目 0 的位置.plist

  7. 点击Boston旁边的展开三角形以及将州设置为MA图 15.25:配置了项目 1 的位置.plist

    图 15.25:配置了项目 1 的位置.plist

  8. 通过添加以下城市和州继续相同的过程:

完成的.plist文件应如下所示:

图 15.26:完成的位置.plist

图 15.26:完成的位置.plist

Locations.plist文件已完成。在下一节中,你将创建一个数据管理类,类似于上一章中创建的类,该类将读取Locations.plist文件并将其提供给位置屏幕的LocationViewController实例。

创建 LocationDataManager 类

如前一章所述,你将创建一个数据管理类,从 Locations.plist 加载位置数据,并将其提供给 Locations 屏幕的 LocationsViewController 实例。然后,这些数据将用于在 Locations 屏幕中填充表格视图。按照以下步骤操作:

  1. 右键点击 Location 文件夹中的 Model 文件夹,并选择 New File

  2. iOS 应该已经选中。选择 Swift File 并点击 Next

  3. 将此文件命名为 LocationDataManager 并点击 Create

  4. 在项目导航器中点击 LocationDataManager 文件,在 import 语句之后,输入以下内容以声明 LocationDataManager 类:

    class LocationDataManager {
    }
    
  5. 在大括号内,添加一个数组属性 locations,用于存储位置列表:

    private var locations: [String] = []
    

    private 关键字表示 locations 属性只能由本类中的方法访问。

  6. 在属性声明之后添加以下方法:

    private func loadData() -> [[String: String]] {
       let decoder = PropertyListDecoder()
       if let path = Bundle.main.path(forResource:
       "Locations", ofTypes: "plist"),
       let locationsData = Filemanager.default.contents(
       atPath: path),
       let locations = try? decoder.decode([[String:
       String]].self, from: locationsData) {
          return locations
       }
       return [[:]]
    }
    func fetch() {
       for location in loadData() {
          if let city = location["city"], let
          state = location["state"] {
             locations.append("\(city), \(state)")
          }
       }
    }
    func numberOfLocationItems() -> Int {
       locations.count
    }
    func locationItem(at index: Int) -> String {
       locations[index]
    }
    

    这些方法与 ExploreDataManager 中的方法类似。让我们分解一下:

    loadData() 
    

    加载 Locations.plist 的内容,并返回一个字典数组。每个字典存储一个位置的城市和州。

    fetch() 
    

    接收 loadData() 提供的数组,将每个元素的 citystate 连接起来,并将生成的字符串追加到 locations 数组中。

    numberOfLocationItems() 
    

    返回 locations 数组中的元素数量。

    locationItem(at:) 
    

    返回存储在 locations 数组给定数组索引处的字符串。

现在 LocationDataManager 类已经完成,让我们配置 LocationViewController 类,使其能够从 LocationDataManager 实例获取数据并将其提供给表格视图。你将在下一节中完成此操作。

在表格视图中显示数据

当前是 "A Cell"。你将更新 LocationViewController 类以使用 LocationDataManager 实例作为数据源。按照以下步骤操作:

  1. 在项目导航器中点击 LocationViewController 文件。在 LocationViewController 类定义中 viewDidLoad() 方法之前,创建一个 LocationDataManager 实例,并将其分配给一个属性 manager,输入以下内容:

    let manager = LocationDataManager()
    
  2. viewDidLoad() 方法内部,通过调用 manager.fetch() 获取表格视图的数据:

    override func viewDidLoad() { 
       super.viewDidLoad() 
       manager.fetch()
    }
    
  3. 修改 tableView(_:numberOfRowsInSection:) 以从 manager 获取表格视图中要显示的行数:

    func tableView(_ tableView: UITableView,
    numberOfRowsInSection section: Int) -> Int {
       manager.numberOfLocationItems()
    }
    
  4. 修改 tableView(_:cellForRowAt:) 如下所示,以便表格视图在每个表格视图中显示包含城市和州的字符串:

    func tableView(_ tableView: UITableView, cellForRowAt
    indexPath: IndexPath) -> UITableViewCell {
       let cell = tableView.dequeueReusableCell(
       withIdentifier: "locationCell", for: indexPath)
       cell.textLabel?.text =
       text property of the table view cell's textLabel to the corresponding element in the locations array. The indexPath returns the section and row number of a particular row in a table view. For example, the first row has an indexPath containing section 0 and row 0. indexPath.row returns 0 for the first row, so manager returns the string stored at index 0 in the locations array. This string is then assigned to the text property of the first table view cell's textLabel.
    

构建并运行你的应用。你应该在表格视图中看到来自 ExploreData.plist 的位置信息:

图 15.27:iOS 模拟器显示完成的 **Locations** 屏幕

图 15.27:iOS 模拟器显示完成的 Locations 屏幕

你已经完成了 Locations 屏幕的实现。做得好!

摘要

在本章中,你学习了表格视图和表格视图控制器,并在游乐场中实现了一个表格视图的视图控制器。接下来,你从头开始实现了LocationViewController类,这是一个用于.plist文件的表格视图控制器,名为Locations.plist,用于存储位置列表。你创建了一个数据管理类,LocationDataManager,用于从.plist文件中读取数据。最后,你配置了LocationViewController类,使其从LocationDataManager实例获取数据,并将其提供给表格视图,以便位置屏幕显示餐厅位置列表。

这将使你能够从头开始创建.plist文件以存储数据,并实现使用.plist文件作为数据源的自定义表格视图。太棒了!

在下一章中,你将为地图屏幕添加一个地图视图,并配置它以显示餐厅位置。你还将为地图屏幕设置自定义标注,并设置餐厅详情屏幕,该屏幕将在标注呼出中的按钮被点击时显示。

第十六章:第十六章:开始使用 MapKit

在上一章中,你学习了表格视图和表格视图控制器,并完成了位置屏幕的实现。现在它显示餐厅位置的列表。

在本章中,你将在MKAnnotation协议上显示餐厅位置,这允许你将你创建的类与特定的地图位置关联起来。你将创建一个新的类RestaurantItem,使其符合此协议。接下来,你将创建MapDataManager,一个数据管理类,它从.plist文件中加载餐厅数据并将其放入RestaurantItem实例的数组中。你将创建一个新的DataManager协议来读取.plist文件,并将MapDataManagerExploreDataManager类更新以避免冗余代码(重构)。之后,你将创建一个MapViewController类,它是RestaurantDetailViewController类的视图控制器,也是MapViewController实例的视图控制器。最后,你将使用扩展来清理和组织你的代码,使其更容易阅读和维护。

到本章结束时,你将学会如何创建自定义地图注释视图并将其添加到地图中,如何使用故事板引用将故事板链接在一起,以及如何使用扩展来组织你的代码,使其更容易阅读。

以下内容将涵盖:

  • 理解和创建注释

  • 向地图视图添加注释

  • 从地图视图转到餐厅详情屏幕

  • 组织你的代码

技术要求

你将继续在上一章中修改的LetsEat项目上工作。

本章的资源文件和完成的 Xcode 项目位于本书代码包的Chapter16文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际效果:

bit.ly/3kEKEB7

现在我们来了解地图注释,它们用于在地图屏幕上标记餐厅位置。

理解和创建注释

第十一章**,完成用户界面中,你向MKMapView类添加了一个地图视图。你可以在苹果地图应用中看到它的样子。

重要信息

要了解更多关于MKMapView的信息,请参阅developer.apple.com/documentation/mapkit/mkmapview

当你构建并运行你的应用时,你将在屏幕上看到一个地图。屏幕上可见的地图部分可以通过设置地图的region属性来指定。

重要信息

要了解更多关于区域及其创建方法的信息,请参阅 developer.apple.com/documentation/mapkit/mkmapview/1452709-region

MKAnnotationView 类上的图钉。要向地图视图添加图钉,您需要一个遵循 MKAnnotation 协议的对象。此协议允许您将一个对象与特定的地图位置关联起来。

重要信息

要了解更多关于 MKAnnotation 协议的信息,请参阅 developer.apple.com/documentation/mapkit/mkannotation

任何对象都可以通过实现一个包含地图位置的 coordinate 属性来遵循 MKAnnotation 协议。可选的 MKAnnotation 协议属性包括 title,一个包含注释标题的字符串,以及 subtitle,一个包含注释副标题的字符串。

当遵循 MKAnnotation 协议的对象位于屏幕上可见的地图区域时,地图视图会要求其代理(通常是视图控制器)提供一个相应的 MKAnnotationView 类实例。这个实例在地图上显示为一个图钉。

重要信息

要了解更多关于 MKAnnotationView 的信息,请参阅 developer.apple.com/documentation/mapkit/mkannotationview

如果用户滚动地图并且 MKAnnotationView 实例离开屏幕,它将被放入重用队列并在稍后回收,类似于表格视图单元格和集合视图单元格的回收方式。MKAnnotationView 实例可以自定义以显示自定义图标,并且在被点击时可以显示呼出气泡。呼出气泡可以包含执行动作的按钮,例如显示屏幕。

对于您的应用,您将创建一个新的类,RestaurantItem,该类遵循 MKAnnotation 协议。让我们看看如何在下一节中创建这个类。

创建 RestaurantItem 类

为了在 RestaurantItem 上表示餐厅位置,该类遵循 MKAnnotation 协议。这个类将有一个 coordinate 属性来存储餐厅的位置,一个 title 属性来存储餐厅名称,以及一个 subtitle 属性来存储它提供的菜系。

您需要设置餐厅位置以设置 RestaurantItem 实例的 coordinate 属性。餐厅数据(包括其位置)将以 .plist 文件的形式提供。在您创建 RestaurantItem 类之前,您需要将此 .plist 文件导入到您的应用中。按照以下步骤操作:

  1. 打开 LetsEat 项目。在项目导航器中,右键单击 LetsEat 文件夹并创建一个名为 Map 的新组。

  2. 右键单击 Map 文件夹并创建一个名为 Model 的新组。

  3. 如果你还没有这样做,请从github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition下载完成的项目和项目资源,并在Chapter16文件夹中的resources文件夹内找到Maplocations.plist文件。

  4. Maplocations.plist文件拖动到项目中的Model文件夹,并点击它以查看其内容。你会看到它是一个字典数组,每个字典包含一家餐厅的详细信息(包括其位置)。你将在RestaurantItem类中创建属性以存储你将使用的数据,这些数据最终将在餐厅详情屏幕上显示:

图 16.1:编辑区域显示 MapLocations.plist 的内容

图 16.1:编辑区域显示 MapLocations.plist 的内容

按照以下步骤创建RestaurantItem类:

  1. 右键点击Model文件夹并选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch Class然后点击下一步

  3. 按照以下方式配置文件:

    RestaurantItem

    NSObject

    Swift

    点击下一步

  4. 点击RestaurantItem文件出现在项目导航器中。

  5. RestaurantItem文件中,在import UIKit语句之后输入以下内容以导入MapKit框架:

    import MapKit
    

    这使你可以访问MKAnnotationMKMapViewDelegate等协议。

  6. 修改类声明如下以采用MKAnnotation协议:

    class RestaurantItem: NSObjectcoordinate property, which is required to conform to MKAnnotation. You will do so shortly.
    
  7. 在大括号之间输入以下内容:

    let name: String?
    let cuisines: [String]
    let lat: Double?
    let long: Double?
    let address: String?
    let postalCode: String?
    let state: String?
    let imageURL: String?
    let restaurantID: Int?
    

    这些属性将保存从Maplocations.plist文件获取的数据。让我们看看它们是用来做什么的:

    name存储餐厅的名称。

    cuisines存储餐厅提供的菜系。

    latlong存储餐厅位置的纬度和经度。

    address存储餐厅的地址。

    postalCode存储餐厅的邮政编码。

    state存储餐厅所在的州。

    imageURL存储指向餐厅照片的链接。

    restaurantID存储用作餐厅标识符的唯一数字。

    注意,你还没有为存储Maplocations.plist文件中包含的每个餐厅的详细信息创建属性,这是可以的。你只需要为将在餐厅详情屏幕上显示的详细信息创建属性。

  8. 你将使用自定义初始化器使用.plist文件中的数据初始化RestaurantItem实例。在最后一个属性声明之后输入以下内容:

    init(dict: [String: AnyObject]) {
       self.lat = dict["lat"] as? Double
       self.long = dict["long"] as? Double 
       self.name = dict["name"] as? String  
       self.cuisines = dict["cuisines"] as? [String] ?? []
       self.address = dict["address"] as? String
       self.postalCode = dict["postalCode"] as? String
       self.state = dict["state"] as? String 
       self.imageURL = dict["image_url"] as? String
       self.restaurantID = dict["id"] as? Int
    }
    

    尽管这个初始化器看起来很复杂,但实际上非常简单。每一行都查找特定的字典项键并将其值分配给相应的属性。例如,第一行查找包含lat键的字典项并将其关联的值分配给lat属性。

  9. 你将使用 latlong 属性来创建 coordinate 属性的值,这是符合 MKAnnotation 所必需的。在 init(dict:) 方法之后输入以下内容以实现它:

    var coordinate: CLLocationCoordinate2D {
       guard let lat = lat, let long = long else { 
          return CLLocationCoordinate2D() 
       }
       return CLLocationCoordinate2D(latitude: lat,
       longitude: long)
    }
    

    coordinate 属性的类型是 CLLocationCoordinate2D,它包含一个地理位置。coordinate 属性的值不是直接分配的;guard 语句从 latlong 属性中获取纬度和经度值,然后用于创建 coordinate 属性的值。这样的属性被称为 计算属性

  10. coordinate 属性之后添加以下代码以实现 title 属性:

    var title: String? {
       name
    }
    

    title 是一个计算属性,它返回 name 属性的内容。

  11. 最后,在 title 属性之后添加以下代码以实现 subtitle 属性:

    var subtitle: String? {
       if cuisines.isEmpty { 
          return "" 
       } else if cuisines.count == 1 { 
          return cuisines.first 
       } else { 
          return cuisines.joined(separator: ", ") 
       }
    }
    

    subtitle 也是一个计算属性。第一行检查 cuisines 属性是否为空,如果是,则返回一个空字符串。如果 cuisines 属性包含一个项目,则返回该项目。如果 cuisines 属性包含多个项目,则每个项目都会添加到一个字符串中,项目之间用逗号分隔。例如,如果 cuisines 包含 ["American", "Bistro", "Burgers"] 数组,生成的字符串将是 "American, Bistro, Burgers"

    你的 RestaurantItem 类现在已完成且没有错误,应该看起来像这样:

    import UIKit 
    import MapKit
    class RestaurantItem: NSObject, MKAnnotation {
       let name: String?
       let cuisines: [String]
       let lat: Double?
       let long: Double?
       let address: String?
       let postalCode: String?
       let state: String?
       let imageURL: String?
       let restaurantID: Int?
       init(dict: [String: AnyObject]) {
          self.lat = dict["lat"] as? Double
          self.long = dict["long"] as? Double 
          self.name = dict["name"] as? String  
          self.cuisines = dict["cuisines"] as? [String] 
          ?? []
          self.address = dict["address"] as? String
          self.postalCode = dict["postalCode"] as? String
          self.state = dict["state"] as? String 
          self.imageURL = dict["image_url"] as? String
          self.restaurantID = dict["id"] as? Int
       }
       var coordinate: CLLocationCoordinate2D {
          guard let lat = lat, let long = long else {
             return CLLocationCoordinate2D() 
          }
          return CLLocationCoordinate2D(latitude: lat,
          longitude: long)
       }
       var title: String? {
          name
       }
       var subtitle: String? {
          if cuisines.isEmpty { 
             return "" 
          } else if cuisines.count == 1 { 
             return cuisines.first 
          } else { 
             return cuisines.joined(separator: ", ") 
          }
       }
    }
    

到目前为止,你已经将 Maplocations.plist 文件添加到你的应用中,并且已经创建了 RestaurantItem 类。接下来,让我们创建一个数据管理类,它从 Maplocations.plist 文件中读取餐厅数据,并将其放入一个 RestaurantItem 实例数组中,以便你的应用使用。

创建 MapDataManager 类

如同你在前面的章节中所做的那样,你将创建一个数据管理类,名为 MapDataManager,它将从 Maplocations.plist 文件中加载餐厅数据,并将数据放入一个 RestaurantItem 实例数组中。按照以下步骤操作:

  1. 右键点击 Map 文件夹内的 Model 文件夹,并选择 新建文件

  2. iOS 应该已经选中。选择 Swift 文件 然后点击 下一步

  3. 将此文件命名为 MapDataManager。点击项目导航器中出现的 MapDataManager 文件。

  4. MapDataManager 文件中,在 import 语句之后添加以下内容以声明 MapDataManager 类:

    class MapDataManager {
    
    }
    
  5. 在大括号之间添加以下属性以存储从 .plist 文件中读取的 RestaurantItem 实例:

    private var items: [RestaurantItem] = []
    var annotations: [RestaurantItem] {
       items 
    }
    

    items 数组将包含 RestaurantItem 实例。private 使得 items 数组只能在 MapDataManager 类内部访问,而 annotations 是一个计算属性,当访问时返回 items 数组的副本。这允许其他对象访问 items 数组的内容,但不能修改它。

  6. 在属性声明之后添加以下方法以加载.plist文件,读取其内部数据,并将其存储在RestaurantItem实例数组中:

    private func loadData() -> [[String: AnyObject]] {
       guard let path = Bundle.main.path(forResource:
       "MapLocations", ofType: "plist"), 
       let itemsData = FileManager.default.contents(
       atPath: path),
       let items = try! PropertyListSerialization
       .propertyList(from: itemsData, format: nil) as? 
       [[String: AnyObject]] else {
          return [[:]]
       }
       return items
    } 
    func fetch(completion: (_ annotations: 
    [RestaurantItem]) -> ()){
       if !items.isEmpty { 
          items.removeAll() 
       }
       for data in loadData() {
          items.append(RestaurantItem(dict: data))
       }
       completion(items)
    }
    

    loadData()fetch(completion:)方法执行与ExploreDataManager类中的loadData()fetch()方法相同的任务。

    小贴士

    你可能希望重新阅读第十四章**,将数据添加到集合视图中,以刷新你对ExploreDataManager类的记忆。

然而,这里使用的loadData()方法能够返回一个包含字典的数组,其中值是AnyObject类型。这是必要的,因为与ExploreData.plist文件不同,MapLocations.plist文件不仅包含[String: String]类型的字典。此外,这里使用的fetch(completion:)方法有一个作为参数的完成闭包,它可以接受任何接受RestaurantItems数组作为参数的函数或闭包:

(_ annotations:[RestaurantItem]) -> ())

有时候,你不知道一个操作何时会完成。例如,你需要在从互联网下载文件后执行一个动作,但你不知道下载需要多长时间。你可以指定一个完成闭包,一旦操作完成就应用它。在这种情况下,完成闭包将处理items数组,一旦.plist文件中的所有数据都被读取。

现在再次考虑MapLocations.plist文件:

![Figure 16.2: Editor area showing array and dictionaries in MapLocations.plist]

![img/Figure_16.02_B17469.jpg]

图 16.2:显示 MapLocations.plist 中的数组和字典的编辑区域

此文件的结构与ExploreData.plist相同。Root项是一个包含字典的数组。由于ExploreData.plistMapLocations.plist都有一个字典数组,因此如果你能创建一个单一的方法来加载.plist文件并在需要的地方使用它,将会更高效。你将在下一节中这样做。

创建 DataManager 协议

你将不会在每个类中创建一个方法来加载.plist文件,而是创建一个新的协议DataManager来处理.plist文件加载。此协议将实现一个使用扩展加载.plist文件的方法。

小贴士

你可能希望重新阅读第八章**,协议、扩展和错误处理,该章节涵盖了协议和扩展。

在你创建了DataManager协议之后,任何需要加载.plist文件的自定义类都可以采用它。你需要修改ExploreDataManagerMapDataManager类以采用此协议。按照以下步骤操作:

  1. 右键点击LetsEat文件夹,创建一个名为Misc的新组。

  2. 右键点击Misc文件夹,选择新建文件

  3. iOS应该已经选中。选择Swift 文件然后点击下一步

  4. 将此文件命名为DataManager。点击创建

  5. 在项目导航器中点击 DataManager 文件,并声明 DataManager 协议如下:

    import Foundation
    loadPlist(file:) that takes a string as a parameter and returns an array of dictionaries. The string will hold the name of the .plist file to be loaded.
    
  6. 在协议声明之后添加一个扩展,包含 loadPlist(file:) 方法的实现:

    import Foundation
    protocol DataManager {
       func loadPlist(file name: String) -> 
       [[String: AnyObject]]
    }
    loadPlist(file:) method. This method looks for a .plist file specified in the name parameter inside the application bundle. If the file is not found, an empty array of dictionaries is returned. Otherwise, the contents of the .plist file are loaded into an array of dictionaries of type [String: AnyObject] and returned. 
    

现在您有了这个协议,您将修改 MapDataManagerExploreDataManager 类以采用它。当您将现有代码修改为更有效地完成相同的事情时,这个过程被称为 重构

在下一节中,您将开始重构 MapDataManager 类以符合 DataManager 协议。

重构 MapDataManager 类

MapDataManager 类已经有一个 loadData() 方法,它被硬编码为读取 Maplocations.plist。现在您已经创建了 DataManager 协议,您将修改 MapDataManager 类以使用它。按照以下步骤操作:

  1. 在项目导航器中选择 MapDataManager 文件,找到并删除 loadData() 方法。您会看到一个错误,因为 fetch() 方法调用了您刚刚删除的 loadData() 方法。您将在稍后修复这个问题。

  2. DataManager 协议添加到类声明中,如下所示:

    class MapDataManagerloadPlist(file:) method available to the MapDataManager class.
    
  3. fetch() 方法中的 for data in loadData() 行按照以下方式修改以修复错误:

    for data in MapDataManager class should look like this:
    
    

    import Foundation

    class MapDataManager: DataManager {

    private var items: [RestaurantItem] = []

    var annotations: [RestaurantItem] {

    items

    }

    func fetch(completion: (_ annotations:

    [RestaurantItem]) -> ()){

    if !items.isEmpty {

    items.removeAll()

    }

    for data in loadPlist(file: "MapLocations") {

    items.append(RestaurantItem(dict: data))

    }

    completion(items)

    }

    }

    
    

错误应该已经消失了。在下一节中,您将同样重构 ExploreDataManager 类以使其符合 DataManager 协议。

重构 ExploreDataManager 类

MapDataManager 类类似,ExploreDataManager 类也有一个 loadData() 方法,它被硬编码为读取 ExploreData.plist

提示

您可能希望重新阅读 第十四章,“将数据放入集合视图”,以刷新您对 ExploreDataManager 类的记忆。

您需要将 ExploreDataManager 类中与 MapDataManager 类相同的更改进行修改。按照以下步骤操作:

  1. 在项目导航器中选择 ExploreDataManager 文件,找到并删除 loadData() 方法。忽略错误,因为它将在稍后修复。

  2. DataManager 协议添加到类声明中,如下所示:

    class ExploreDataManagerloadPlist(file:) method available to the ExploreDataManager class.
    
  3. 按照以下方式修改 fetch() 方法以修复错误:

       func fetch() {
          for data in data is cast as [String: String] so that it can be used to initialize instances of the ExploreItem class.You can now make any class that needs to load a `.plist` file containing an array of dictionaries adopt the `DataManager` protocol, as you did here with the `MapDataManager` and `ExploreDataManager` classes. It's not always clear when you should refactor, but the more experience you have, the easier it becomes. One indication that you need to refactor is when you are writing the same code in more than one class.
    

你已经完成了 MapDataManager 类的实现,创建了 DataManager 协议,并将 MapDataManagerExploreDataManager 类重构为符合此协议。使用 MapDataManager 类,你可以从 MapLocations.plist 文件加载数据,并返回一个 RestaurantItem 实例数组。现在,让我们看看如何使用这个数组向地图视图添加标记,这些标记将在 Map 屏幕上显示为图钉。

向地图视图添加标记

第十一章**,完成用户界面,你向项目中添加了地图视图到 MapLocations.plist 文件,并创建了 RestaurantItemMapDataManager 类。还记得 MVC 设计模式吗?在这个阶段,你已经创建了 Map 屏幕的视图和模型,所以你现在只需要视图控制器。

视图控制器将负责以下任务:

  • 向地图视图添加符合 MKAnnotation 协议的 RestaurantItem 实例。

  • 对于地图视图中显示的区域内的 RestaurantItem 实例,提供地图视图请求的 MKAnnotationView 实例。

  • 提供自定义的 MKAnnotationView 实例,当点击时显示包含按钮的呼出气泡,并在按钮点击时显示 Restaurant Detail 屏幕。

你将首先创建 MapViewController 类,作为下一节中 Map 屏幕的视图控制器。

创建 MapViewController 类

你已经为 MapViewController 创建了视图和模型对象,使其成为 Map 屏幕的视图控制器。按照以下步骤操作:

  1. 右键点击 Map 文件夹,选择 New File

  2. iOS 应已选中。选择 Cocoa Touch Class 然后点击 Next

  3. 按照以下方式配置文件:

    MapViewController

    UIViewController

    Swift

    点击 Next

  4. 在项目导航器中点击 MapViewController 文件。

  5. MapViewController 文件中,在 import UIKit 之后添加以下行以导入 MapKit 框架:

    import MapKit
    
  6. 修改类声明如下,使 MapViewController 类采用 MKMapViewDelegate 协议:

    class MapViewController: UIViewController, 
    MKMapViewDelegate {
    

你已经声明了 MapViewController 类。在下一节中,你将把这个类指定为 Map 屏幕的视图控制器,并为地图视图创建一个出口。

连接地图视图的出口

MapViewController 类创建视图控制器场景,使其成为 Map 屏幕的视图控制器,并为地图视图添加一个出口。按照以下步骤操作:

  1. 点击 Main 故事板文件。点击 MapViewController:![Figure 16.3: Identity inspector showing Class setting for MapViewController

    ![img/Figure_16.03_B17469.jpg]

    ![Figure 16.3: Identity inspector showing Class setting for MapViewController]

  2. 在文档大纲中选择 Map View:![Figure 16.4: Document outline with Map View selected]

    ![img/Figure_16.04_B17469.jpg]

    图 16.4:选择地图视图的文档大纲

  3. 点击调整编辑器选项按钮。

  4. 在弹出菜单中选择辅助选项。

  5. 辅助编辑器出现,显示了MapViewController文件的内容。从地图视图Ctrl + 拖动到类声明下方空隙:![图 16.5:显示 MapViewController 文件内容的编辑器区域 图片

    图 16.5:显示 MapViewController 文件内容的编辑器区域

  6. 名称字段中输入mapView并点击连接:![图 16.6:创建 mapView 输出端口时的弹出对话框 图片

    图 16.6:创建 mapView 输出端口时的弹出对话框

  7. 地图视图已连接到MapViewController类中的mapView输出端口。点击x按钮关闭辅助编辑器:

![图 16.7:辅助编辑器关闭按钮图片

图 16.7:辅助编辑器关闭按钮

MapViewController类现在有一个输出端口mapView,通过添加一个基于餐厅位置生成新区域的方法将其链接到MapDataManager类中的地图视图,因此它可以提供地图区域以供地图视图显示。

设置要显示的地图视图区域

在地图视图中,屏幕上可见的地图部分称为区域。要指定一个区域,你需要该区域中心点的坐标以及表示要显示的地图尺寸的水平和垂直跨度。

MapDataManager类中的fetch(completion:)方法返回一个RestaurantItem实例数组。你将实现一个方法initialRegion(latDelta:longDelta:),从该数组中获取第一个RestaurantItem实例,获取餐厅的坐标,并使用它们来创建一个区域。按照以下步骤操作:

  1. 在项目导航器中点击MapDataManager文件。在import Foundation语句之后,添加import MapKit

  2. 在关闭花括号之前,实现initialRegion(latDelta:longDelta:)方法如下:

    func initialRegion(latDelta: CLLocationDegrees, 
    longDelta: CLLocationDegrees) -> MKCoordinateRegion {
       guard let item = items.first else {
          return MKCoordinateRegion()
       }
       let span = MKCoordinateSpan(latitudeDelta: 
       latDelta, longitudeDelta: longDelta)
       return MKCoordinateRegion(center: item.coordinate,
       span: span)
    }
    

    让我们分解一下:

    func initialRegion(latDelta: CLLocationDegrees, longDelta: CLLocationDegrees) -> MKCoordinateRegion
    

    此方法接受两个参数并返回一个MKCoordinateRegion实例。latDelta指定要显示的地图区域的北到南距离(以度为单位)。一度大约是 69 英里。longDelta指定要显示的地图区域的东到西距离(以度为单位)。返回的MKCoordinateRegion实例确定将在屏幕上显示的区域。

    guard let item = items.first else { return MKCoordinateRegion() }
    

    guard语句从RestaurantItem实例数组中获取第一个项目并将其分配给item。如果数组为空,则返回一个空的MKCoordinateRegion实例。

    let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: longDelta)
    

    latDeltalongDelta用于创建一个MKCoordinateSpan实例,这是要创建的区域水平和垂直跨度。

    return MKCoordinateRegion(center: item.coordinate, span: span)
    

    使用item的坐标属性和MKCoordinateSpan实例创建并返回一个MKCoordinateRegion实例。

现在,地图区域已经确定,你可以根据它们的 coordinate 属性确定哪些 RestaurantItem 实例位于此区域。记住,RestaurantItem 类符合 MKAnnotation。作为地图视图的视图控制器,MapViewController 类负责为该区域内的任何 RestaurantItem 实例提供 MKAnnotationView 实例。

在下一节中,你将修改 MapViewController 类以提供地图视图显示区域内的 RestaurantItem 实例的 MKAnnotationViews

在地图视图中显示 MKAnnotationView 实例

到目前为止,你已经有 MapViewController 类来管理 MapDataManager 类中的 initialRegion(latDelta:longDelta:) 方法上的地图视图,以设置地图区域。现在,你将修改 MapViewController 类以从 MapDataManager 类获取 RestaurantItem 实例的数组并将其添加到地图视图中。按照以下步骤操作:

  1. 在项目导航器中点击 MapViewController 文件并删除注释代码。

  2. mapView 属性声明之后,添加以下代码以创建 MapDataManager 类的实例并将其分配给 manager

    private let manager = MapDataManager() 
    
  3. viewDidLoad() 之后添加以下方法。此方法将 RestaurantItem 实例(符合 MKAnnotation 协议)添加到地图视图中:

    func setupMap(_ annotations: [RestaurantItem]) {
       mapView.setRegion(manager.initialRegion(
       latDelta: 0.5, longDelta: 0.5), animated: true)
       mapView.addAnnotations(manager.annotations)
    }
    

    setupMap(_:) 方法接受一个参数 annotations,它是一个 RestaurantItem 实例的数组。它使用 MapDataManager 类的 initialRegion(latDelta:longDelta:) 方法设置地图视图要显示的区域,然后将 annotations 数组中的每个 RestaurantItem 实例添加到地图视图中。然后,地图视图的代理(在本例中为 MapViewController 类)自动为该区域内的每个 RestaurantItem 实例提供 MKAnnotationView 实例。

  4. setupMap(_:) 方法之前添加以下方法。此方法调用 MapDataManager 实例的 fetch(completion:) 方法,并将 setupMap(_:) 方法作为完成闭包传入:

    func initialize() {
       manager.fetch {(annotations) in
       setupMap(annotations)}
    }
    

    fetch(completion:) 方法加载 MapLocations.plist 文件,并创建并将 RestaurantItem 实例的数组分配给 items 数组。annotations 属性返回 items 数组的副本。然后,该数组由传入作为完成闭包的 setupMap(_:) 方法处理。

  5. viewDidLoad() 中调用 initialize() 方法,以便在地图视图加载时调用:

    override func viewDidLoad() { 
       super.viewDidLoad() 
       initialize()
    }
    

构建并运行应用程序。你应该在 地图 屏幕上看到图钉(MKAnnotationView 实例):

图 16.8:iOS 模拟器显示标准 MKAnnotationView 实例

图 16.8:iOS 模拟器显示标准 MKAnnotationView 实例

在地图区域中为每个 RestaurantItem 实例添加了一个 MKAnnotationView 实例。每个 MKAnnotationView 实例由一个图钉表示。你现在地图上有显示餐厅位置的图钉,但需要添加代码来显示应用导览中所示的定制图钉。你将在下一节中完成这项工作。

创建自定义 MKAnnotationView 实例

目前,MKAnnotationView 实例看起来像图钉。你可以用自定义图像替换标准图钉图像。Assets.xcassets 文件中有一个自定义图像,你将配置 MapViewController 类使用它。这将使屏幕上的图钉与应用导览中的图钉相匹配。你还将配置每个图钉,以便在点击时显示呼出气泡。按照以下步骤操作:

  1. 在项目导航器中点击 MapViewController 文件。

  2. initialize() 方法中,在开括号之后添加以下代码。这使得 MapViewController 类成为地图视图的代理:

    func initialize() {
       mapView.delegate = self
    
  3. setupMap(_:) 方法之后添加以下方法。此方法为地图视图显示区域内的每个 MKAnnotation 实例返回一个自定义的 MKAnnotationView 实例:

    func mapView(_ mapView: MKMapView, viewFor annotation:
    MKAnnotation) -> MKAnnotationView? {
       let identifier = "custompin"
       guard !annotation.isKind(of: MKUserLocation.self)
       else { 
          return nil 
       }
       let annotationView: MKAnnotationView
       if let customAnnotationView = 
       mapView.dequeueReusableAnnotationView(
       withIdentifier: identifier) {
          annotationView = customAnnotationView 
          annotationView.annotation = annotation
       } else {
          let av = MKAnnotationView(annotation: 
          annotation, reuseIdentifier: identifier)
          av.rightCalloutAccessoryView = 
          UIButton(type: .detailDisclosure)
          annotationView = av
       }
       annotationView.canShowCallout = true 
       if let image = UIImage(named: 
            "custom-annotation") {
                annotationView.image = image 
                annotationView.centerOffset = CGPoint(
                x: -image.size.width / 2, 
                y: -image.size.height / 2)
       }
       return annotationView
    }
    

    让我们分解一下:

    func mapView(_ mapView: MKMapView, viewFor 
    annotation: MKAnnotation) -> MKAnnotationView?
    

    这是 MKMapViewDelegate 协议中指定的委托方法之一。当 MKAnnotation 实例位于地图区域内时,它会触发,并返回一个 MKAnnotationView 实例,用户将在屏幕上看到它。你将使用此方法用自定义图钉替换默认图钉。

    let identifier = "custompin"
    

    常量 identifier 被分配了 "custompin" 字符串。这将作为重用标识符。

    guard !annotation.isKind(of: MKUserLocation.self) 
    else { 
       return nil 
    }
    

    除了你指定的注释外,MKMapView 实例还会为用户位置添加一个注释。这个 guard 语句检查注释是否是用户位置。如果是,则返回 nil,因为用户位置不是餐厅位置。

    let annotationView: MKAnnotationView
    

    annotationViewMKAnnotationView 类型的常量。你创建这个常量是为了稍后配置和返回它。

    if let customAnnotationView = 
    mapView.dequeueReusableAnnotationView (withIdentifier:
    identifier) { 
       annotationView = customAnnotationView 
       annotationView.annotation = annotation
    }
    

    if 语句检查是否有任何现有的注释最初是可见的,但现在不再在屏幕上。如果有,那个注释的 MKAnnotationView 实例可以被重用,并分配给 annotationView 变量。annotation 参数被分配给 annotationViewannotation 属性。

    else {
       let av = MKAnnotationView(annotation: annotation, 
       reuseIdentifier: identifier) 
       av.rightCalloutAccessoryView = 
       UIButton(type: .detailDisclosure)
       annotationView = av
    }
    

    如果没有可重用的现有MKAnnotationView实例,将执行else子句。使用之前指定的重用标识符(custompin)创建一个新的MKAnnotationView实例。通过调用配置MKAnnotationView实例。当你点击地图上的标记时,会出现一个调用气泡,显示标题(餐厅名称)、副标题(菜系)和一个按钮。你将在稍后编程按钮以呈现你刚刚创建的MKAnnotationView实例,在调用气泡中显示额外信息,并将自定义图像设置为存储在Assets.xcassets中的custom-annotation图像。当添加自定义图像时,注释使用图像的中心作为标记点,因此使用centerOffset属性设置标记点的正确位置,位于标记的尖端。

    return annotationView
    

    返回自定义的MKAnnotationView实例。

构建并运行你的应用。你可以在地图上看到自定义的标记:

图 16.9:iOS 模拟器显示自定义 MKAnnotationView 实例

图 16.9:iOS 模拟器显示自定义 MKAnnotationView 实例

你已经使用从MapDataManager类获取的数据配置了MKAnnotationView实例。点击标记会显示一个调用气泡,显示餐厅名称和它提供的菜系。调用气泡中的按钮目前不起作用。你将在下一节中配置按钮以呈现餐厅详情屏幕。

从地图屏幕切换到餐厅详情屏幕

MKAnnotationView实例,点击一个会显示一个调用气泡,显示餐厅详情。调用气泡中的按钮目前不起作用。

在你之前下载的resources文件夹中,你会找到名为RestaurantDetail.StoryboardPhotoFilter.StoryboardReviewForm.Storyboard的完成的故事板,你需要将这些故事板添加到你的项目中。这些故事板包含了餐厅详情屏幕、照片滤镜屏幕和评论表单屏幕的场景。

RestaurantDetail故事板文件呈现出来。你将在下一节中这样做。

创建和配置故事板引用

Main故事板文件中有许多场景。随着你的项目增长,你会发现跟踪应用中的所有场景更具挑战性。管理这些场景的一种方法是为它们创建额外的故事板文件,并使用故事板引用来链接它们。你将添加RestaurantDetailPhotoFilterReviewForm故事板文件到你的项目中,并且你将使用故事板引用将Main故事板文件链接到RestaurantDetail故事板文件。按照以下步骤将故事板引用添加到你的项目中:

  1. 打开Main故事板文件,并点击库按钮。

  2. 在过滤器字段中输入story。一个Storyboard Reference对象将出现在结果中。

  3. Main故事板文件拖动到地图视图控制器场景旁边:图 16.10:选择 Storyboard Reference 对象的库

    图 16.10:选择 Storyboard Reference 对象的库

  4. 打开您之前下载的resources文件夹,并找到您将添加到项目中的三个故事板文件(RestaurantDetail.storyboardPhotoFilter.storyboardReviewForm.storyboard):图 16.11:资源文件夹的内容

    图 16.11:资源文件夹的内容

  5. 在项目导航器中,在您的LetsEat文件夹内创建一个名为RestaurantDetail的新文件夹,并将RestaurantDetail故事板文件复制到其中:图 16.12:项目导航器显示 RestaurantDetail 文件夹及其内容

    图 16.12:项目导航器显示 RestaurantDetail 文件夹及其内容

  6. 在您的LetsEat文件夹内创建一个名为ReviewForm的新文件夹,并将ReviewForm故事板文件复制到其中,然后在您的LetsEat文件夹内创建一个名为PhotoFilter的新文件夹,并将PhotoFilter故事板文件复制到其中:图 16.13:项目导航器显示 PhotoFilter 和 ReviewForm 文件夹及其内容

    图 16.13:项目导航器显示 PhotoFilter 和 ReviewForm 文件夹及其内容

  7. 现在您将为之前添加到项目中的 Storyboard Reference 分配RestaurantDetail故事板文件。点击Main故事板文件,选择您之前添加的故事板引用,并点击属性检查器按钮。在RestaurantDetail下:图 16.14:RestaurantDetail 故事板引用的属性检查器设置

    图 16.14:RestaurantDetail 故事板引用的属性检查器设置

  8. 地图屏幕中MKAnnotationView实例的呼出气泡按钮处Ctrl + 拖动

  9. 您将为这个转场设置一个标识符。稍后,当呼出气泡按钮被点击时,您将添加一个使用此标识符执行转场的方法。选择连接地图视图控制器场景到故事板引用的转场:图 16.16:地图视图控制器场景和 RestaurantDetail 故事板引用之间的转场

    图 16.16:地图视图控制器场景和 RestaurantDetail 故事板引用之间的转场

  10. 在属性检查器中,在showDetail下:

图 16.17:showDetail 转场的属性检查器设置

图 16.17:showDetail 转场的属性检查器设置

现在,您已经使用转场将地图屏幕的视图控制器场景与餐厅详情屏幕的视图控制器场景链接起来。在下一节中,您将实现一个方法,当呼出气泡按钮被点击时,显示餐厅详情屏幕。

执行 showDetail 转场

你已经链接了showDetail的视图控制器场景。现在你需要一个方法来执行这个转换,但在实现它之前,你将创建一个包含此项目所有转换标识符的枚举。这通过在你稍后代码中输入转换标识符时启用自动完成来减少潜在的错误。按照以下步骤操作:

  1. LetsEat文件夹内的Misc文件夹上右键点击并选择新建文件

  2. iOS应该已经选中。选择Swift 文件然后点击下一步

  3. 将此文件命名为Segue。点击后,Segue文件将出现在项目导航器中。

  4. import语句之后添加以下内容以声明和定义Segue枚举:

    enum Segue: String { 
       case showDetail 
       case showRating 
       case showReview
       case showAllReviews 
       case restaurantList 
       case locationList 
       case showPhotoReview 
       case showPhotoFilter
    }
    

    注意,Segue枚举的类型是String,因此每个情况的原始值都是字符串。例如,showDetail情况的原始值是"showDetail"

现在你可以添加一个方法,当点击呼出气泡按钮时执行showDetail转换。在项目导航器中点击MapViewController文件,并在setupMap(_:)方法之后添加以下方法:

func mapView(_ mapView: MKMapView, annotationView view: 
MKAnnotationView, calloutAccessoryControlTapped control: 
UIControl) {
   self.performSegue(withIdentifier: 
   Segue.showDetail.rawValue, sender: self)
}

mapView(_:annotationView:calloutAccessoryControlTapped:)MKMapViewDelegate协议中指定的一种方法。当用户点击呼出气泡按钮时,它会触发。

self.performSegue(withIdentifier: Segue.showDetail.rawValue, sender: self)使用"showDetail"标识符执行转换,显示餐厅详情屏幕。

构建并运行你的项目。在地图屏幕上,点击一个标记并点击呼出气泡内的按钮:

图 16.18:iOS 模拟器显示呼出气泡按钮

图 16.18:iOS 模拟器显示呼出气泡按钮

新的餐厅详情屏幕出现,但它不包含任何关于餐厅的详细信息:

图 16.19:iOS 模拟器显示餐厅详情屏幕

图 16.19:iOS 模拟器显示餐厅详情屏幕

你将使餐厅详情屏幕显示在第第十八章**,静态表格视图中的数据展示中餐厅的详细信息,但现在,让我们只是将所选餐厅的数据传递到餐厅详情屏幕的视图控制器,并将其打印到调试区域。你将在下一节中这样做。

将数据传递到餐厅详情屏幕

当点击时显示呼出气泡的MKAnnotationView实例。当呼出气泡中的按钮被点击时,将RestaurantItem实例传递到尚未创建的餐厅详情屏幕的视图控制器。按照以下步骤现在创建它:

  1. RestaurantDetail文件夹上右键点击并选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch 类然后点击下一步

  3. 按照以下方式配置文件:

    RestaurantDetailViewController

    UITableViewController

    Swift

    点击下一步

  4. 点击RestaurantDetailViewController文件出现在项目导航器中。

  5. 删除所有注释代码。你的文件应该看起来像这样:

    import UIKit
    class RestaurantDetailViewController: 
    UITableViewController {
       override func viewDidLoad() {
          super.viewDidLoad()
       }
    }
    
  6. viewDidLoad()方法之前声明一个名为selectedRestaurant的属性:

    var selectedRestaurant: RestaurantItem?
    

    这个属性保存了将被从MapViewController实例传递到RestaurantDetailViewController实例的RestaurantItem实例:

  7. viewDidLoad()方法中关闭花括号之前添加以下代码,以将RestaurantItem实例的内容打印到调试区域:

    dump(selectedRestaurant as Any)
    

    这确认了MapViewController实例已成功将RestaurantItem实例传递给RestaurantDetailViewController实例。

  8. 确认你的文件看起来像以下这样:

    import UIKit
    class RestaurantDetailViewController: UITableViewController {
       var selectedRestaurant: RestaurantItem?
       override func viewDidLoad() { 
          super.viewDidLoad() 
          dump(selectedRestaurant as Any)
       }
    }
    
  9. RestaurantDetail文件夹内点击RestaurantDetail故事板文件。选择RestaurantDetailViewController:![Figure 16.20: Identity inspector settings for Restaurant Detail View Controller scene

    ![img/Figure_16.20_B17469.jpg]

    图 16.20:餐厅详情视图控制器场景的标识检查器设置

    注意场景名称将更改为餐厅详情视图控制器场景

  10. 在项目导航器中点击MapViewController文件。

  11. private let manager = MapDataManager()语句之后添加一个属性来保存RestaurantItem实例:

    var selectedRestaurant: RestaurantItem?
    
  12. func mapView(_:annotationView:calloutAccessoryControlTapped:)方法中,在调用self.performSegue(withIdentifier:sender:)方法之前添加以下代码:

    func mapView(_ mapView: MKMapView, annotationView 
    view: MKAnnotationView, calloutAccessoryControlTapped 
    control: UIControl){
    RestaurantItem instance associated with MKAnnotationView instance that was tapped and assigns it to selectedRestaurant.
    
  13. 要从MapViewController实例将RestaurantItem实例传递到RestaurantDetailViewController实例,你需要重写名为prepare(for:sender:)UIViewController方法。在viewDidLoad()之后输入以下代码:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?){
       switch segue.identifier! {
          case Segue.showDetail.rawValue:
             showRestaurantDetail(segue: segue)
          default:
             print("Segue not added")
       }
    }
    

    在切换到另一个视图控制器之前,视图控制器会执行prepare(for:sender:)方法。在这种情况下,这个方法在调用showDetail之前被调用,在这种情况下,showRestaurantDetail(segue:)方法被调用。这个方法将为RestaurantDetailViewController实例设置selectedRestaurant属性。你会看到一个错误,因为showRestaurantDetail(segue:)还没有被创建。

  14. setupMap(_:)方法之后添加以下代码以实现showRestaurantDetail(segue:)

    func showRestaurantDetail(segue: UIStoryboardSegue) {
       if let viewController = segue.destination as? 
       RestaurantDetailViewController, let restaurant = 
       selectedRestaurant {
          viewController.selectedRestaurant = restaurant
       }
    }
    

    这确保了过渡目标是RestaurantDetailViewController实例。如果是,临时常量restaurant被分配了MapViewController实例中的selectedRestaurant属性。然后restaurant被分配给RestaurantDetailViewController实例中的selectedRestaurant属性。

    换句话说,从RestaurantItem实例获取的餐厅详情被传递到RestaurantDetailViewController实例。

构建并运行你的应用。在 地图 屏幕上,点击一个标记并然后点击呼叫按钮。餐厅详情 屏幕将出现。点击报告导航器并点击如图所示的第一个条目。你应该在编辑器区域看到餐厅的详细信息:

![Figure 16.21:报告导航器显示第一个条目的内容

![img/Figure_16.21_B17469.jpg]

图 16.21:报告导航器显示第一个条目的内容

你已经为 RestaurantDetailViewController 实例添加了故事板,现在它有了在 地图 屏幕上选择的 RestaurantItem 实例的数据。太好了!你将在下一章配置 餐厅详情 屏幕以显示这些数据。

你在本章中做了很多工作,所以在你进入下一章之前,让我们组织你编写的代码,使其更容易理解。你将在下一节中使用扩展来完成此操作。

组织你的代码

随着你的程序变得更加复杂,你将使用扩展(在第 *第八章**,协议、扩展和错误处理)来组织你的代码。扩展可以帮助你使代码更易于阅读并避免杂乱。

你将组织四个类:ExploreViewControllerRestaurantListViewControllerLocationViewControllerMapViewController。你将使用扩展将相关代码块分离。让我们从下一节中的 ExploreViewController 类开始。

重构 ExploreViewController

你将使用扩展将 ExploreViewController 文件中的代码划分为不同的部分。按照以下步骤操作:

  1. 在项目导航器中点击 ExploreViewController 文件。在最后的括号闭合之后,添加以下内容:

    // MARK: Private Extension
    private extension ExploreViewController {
       // code goes here
    }
    // MARK: UICollectionViewDataSource
    extension ExploreViewController: 
    UICollectionViewDataSource {
       // code goes here
    }
    

    在这里,你正在创建两个扩展。第一个扩展将是私有的,这意味着这个扩展的内容只能被 ExploreViewController 类访问。第二个扩展将包含所有的 UICollectionViewDataSource 方法。

  2. 你会得到一个错误,因为 UICollectionViewDataSource 出现在两个地方。从文件顶部的类声明中删除 UICollectionViewDataSource。你的类声明应该看起来像这样:

    class ExploreViewController: UIViewController, UICollectionViewDelegate {
    
  3. 将所有 UICollectionViewDataSource 方法移动到第二个扩展中。它应该看起来像这样:

    // MARK: UICollectionViewDataSource
    extension ExploreViewController: 
    UICollectionViewDataSource {
       viewDidLoad() as clean as possible, you will create an initialize() method inside the private extension, and put everything you need to initialize the view controller in there. After that, you will call initialize() in viewDidLoad(). 
    
  4. private 扩展内添加 initialize() 方法:

    func initialize() {
       manager.fetch()
    }
    
  5. unwindLocationCancel(segue:) 方法也移动到 private 扩展内部。

  6. 确认私有扩展看起来如下:

    // MARK: Private Extension
    private extension ExploreViewController {
       func initialize() {
          manager.fetch()
       }
       @IBAction func unwindLocationCancel(segue:
       UIStoryboardSegue) {
       }
    }
    
  7. 最后,按照以下方式修改 viewDidLoad()

    override func viewDidLoad() { 
       super.viewDidLoad() 
       initialize()
    }
    

这种方式分离代码的好处现在可能并不明显,但随着你的类变得更加复杂,你会发现查找特定方法以及维护代码变得更加容易。在你对其他文件做同样操作之前,让我们看看下一节中 // MARK: 语法是如何使用的。

使用 // MARK: 语法

// MARK: 语法用于在代码的不同部分之间轻松导航。让我们看看它做了什么:

  1. 查看位于工具栏下方可见的路径,并点击显示的部分:图 16.22:显示路径的编辑区域

    img/Figure_16.22_B17469.jpg

    图 16.22:显示路径的编辑区域

  2. 将显示一个菜单,你将看到 // MARK: 语法。这使你能够轻松跳转到这些部分:

图 16.23:选择 Private 扩展的路径菜单

img/Figure_16.23_B17469.jpg

图 16.23:选择 Private 扩展的路径菜单

你已经组织了 ExploreViewController 类,接下来让我们通过重构并添加扩展来处理 RestaurantListViewController 类。

重构 RestaurantListViewController

你将为 RestaurantListViewController 类添加两个扩展,类似于你在 ExploreViewController 类中添加的。按照以下步骤操作:

  1. 在项目导航器中点击 RestaurantListViewController 文件。在最后的括号闭合之后,添加以下内容:

    // MARK: Private Extension
    private extension RestaurantListViewController {
       // code goes here
    }
    // MARK: UICollectionViewDataSource 
    extension RestaurantListViewController: 
    UICollectionViewDataSource {
       // code goes here
    }
    

    你将在第一个扩展中放置 RestaurantListViewController 类的私有方法,并在第二个扩展中放置所有的 UICollectionViewDataSource 方法。

  2. 从文件顶部的类声明中删除 UICollectionViewDataSource。你的类声明应该看起来像这样:

    class RestaurantListViewController: UIViewController, UICollectionViewDelegate {
    
  3. 将所有的 UICollectionViewDataSource 方法移动到第二个扩展中。完成后的样子应该是这样的:

    // MARK: UICollectionViewDataSource extension 
    RestaurantListViewController: 
    UICollectionViewDataSource {
    func collectionView(_ collectionView: 
    UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          1
       }
    func collectionView(_ collectionView: 
    UICollectionView, cellForItemAt indexPath: 
       IndexPath) -> UICollectionViewCell {
          collectionView.dequeueReusableCell(
    withReuseIdentifier: "restaurantCell", 
          for: indexPath)
       }
    }
    

你已经完成了 RestaurantListViewController 类的组织,接下来让我们在下一节中清理 LocationViewController 类。

重构 LocationViewController

如同之前一样,你将在 LocationViewController 文件中添加两个扩展。按照以下步骤操作:

  1. 在项目导航器中点击 LocationViewController 文件。在最后的括号闭合之后,添加以下内容:

    // MARK: Private Extension
    private extension LocationViewController {
       // code goes here
    }
    // MARK: UITableViewDataSource
    extension LocationViewController: 
    UITableViewDataSource {
       // code goes here
    }
    

    第一个扩展将包含 LocationViewController 类的私有方法。第二个扩展将包含所有的 UITableViewDataSource 方法。

  2. 从文件顶部的类声明中删除 UITableViewDataSource。你的类声明应该看起来像这样:

    class LocationViewController: UIViewController {
    
  3. 将所有的 UITableViewDataSource 方法移动到第二个扩展中。完成后的样子应该是这样的:

    // MARK: UITableViewDataSource
    extension LocationViewController: UITableViewDataSource 
    {
       func tableView(_ tableView: UITableView, 
       numberOfRowsInSection section: Int) -> Int {
          manager.numberOfLocationItems()
       }
       func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> 
       UITableViewCell {
          let cell = tableView.dequeueReusableCell(
          withIdentifier: "locationCell", for: indexPath)
    cell.textLabel?.text = 
          manager.locationItem(at: indexPath.row)
          return cell
       }
    }
    
  4. 就像你在 ExploreViewController 类中所做的那样,你将在第一个扩展中创建一个 initialize() 方法,并将初始化 LocationViewController 类所需的所有内容放入其中。在第一个扩展中添加以下内容:

    // MARK: Private Extension
    private extension LocationViewController {
       func initialize() {
          manager.fetch()
       }
    }
    
  5. 按照以下方式修改 viewDidLoad() 方法以调用 initialize() 方法:

    override func viewDidLoad() { 
       super.viewDidLoad() 
       initialize()
    }
    

你已经完成了 LocationViewController 类的组织,接下来让我们在下一节中清理 MapViewController 类。

重构 MapViewController

如同之前对其他类所做的,你将为 MapViewController 类添加两个扩展。按照以下步骤操作:

  1. 在项目导航器中点击 MapViewController 文件。在最后的括号闭合之后,添加以下内容:

    // MARK: Private Extension
    private extension MapViewController {
       // code goes here
    }
    // MARK: MKMapViewDelegate
    extension MapViewController: MKMapViewDelegate {
       // code goes here
    }
    

    第一个扩展将包含 MapViewController 类的私有方法。第二个将包含所有的 MKMapViewDelegate 方法。

  2. 从文件顶部的类声明中删除 MKMapViewDelegate。你的类定义应该看起来像这样:

    class MapViewController: UIViewController {
    
  3. 将所有 MKMapViewDelegate 方法移动到第二个扩展中。它应该看起来像这样:

    // MARK: MKMapViewDelegate
    extension MapViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, annotationView 
    view: MKAnnotationView, 
       calloutAccessoryControlTapped control: UIControl){
    guard let annotation = 
    mapView.selectedAnnotations.first else 
    { 
    return 
          }
    selectedRestaurant = annotation as? 
    RestaurantItem 
    self.performSegue(withIdentifier: 
          Segue.showDetail.rawValue, sender: self)
       }
    func mapView(_ mapView: MKMapView, viewFor 
       annotation:MKAnnotation) -> MKAnnotationView? {
          let identifier = "custompin"
    guard !annotation.isKind(of: 
    MKUserLocation.self) else { 
    return nil 
          }
          let annotationView: MKAnnotationView
          if let customAnnotationView = mapView.
    dequeueReusableAnnotationView(withIdentifier: 
    identifier) { 
    annotationView = customAnnotationView 
             annotationView.annotation = annotation
          } else {
    let av = MKAnnotationView(annotation: 
             annotation, reuseIdentifier: identifier)
    av.rightCalloutAccessoryView = 
             UIButton(type: .detailDisclosure)
             annotationView = av
          }
          annotationView.canShowCallout = true
          if let image = UIImage(named: "custom-
    annotation") { 
             annotationView.image = image
    annotationView.centerOffset = 
    CGPoint(x: -image.size.width / 2, 
             y: -image.size height / 2 )
          }
          return annotationView
       }
    }
    
  4. initialize(), setupMap(_:), 和 showRestaurantDetail(segue:) 方法移动到第一个扩展中。它应该看起来像这样:

    // MARK: Private Extension
    private extension MapViewController {
    func initialize() { 
    mapView.delegate = self 
    manager.fetch { (annotations) in 
          setupMap(annotations) }
       }
       func setupMap(_ annotations: [RestaurantItem]) {
          mapView.setRegion(manager.currentRegion(
          latDelta: 0.5, longDelta: 0.5), animated: true)
          mapView.addAnnotations(manager.annotations)
       }
       func showRestaurantDetail(segue:UIStoryboardSegue){
          if let viewController = segue.destination as?
    RestaurantDetailViewController, let restaurant 
          = selectedRestaurant {
    viewController.selectedRestaurant 
             = restaurant
          }
       }
    }
    

你已经使用扩展组织了所有四个视图控制器(ExploreViewController, RestaurantListViewController, LocationViewController, 和 MapViewController)。做得好!

摘要

在本章中,你创建了一个新的类,RestaurantItem,它符合 MKAnnotation 协议。接下来,你创建了 MapDataManager,一个数据管理类,它从 .plist 文件中加载数据并将其放入 RestaurantItem 实例的数组中。你创建了 DataManager 协议,并将 MapDataManagerExploreDataManager 类重构为使用此协议。之后,你创建了 MapViewController 类,它是 RestaurantDetailViewController 类的视图控制器,也是 MapViewController 实例的视图控制器。此时,你知道如何创建符合 MKAnnotation 协议的对象,如何将它们添加到地图视图中,以及如何创建自定义的 MKAnnotationViews,这使你能够将标注地图添加到自己的项目中。

你还向项目中添加了故事板文件,学习了如何使用故事板引用,并使用扩展组织了你的视图控制器类(ExploreViewController, RestaurantListViewController, LocationViewController, 和 MapViewController)。这将帮助你组织大型项目的故事板和代码,使其更容易阅读和维护。

在下一章中,你将学习关于 JSON 文件的知识,以及如何从它们中加载数据,以便 餐厅列表地图 屏幕可以显示特定餐厅的详细信息。

第十七章:第十七章: 开始使用 JSON 文件

在上一章中,您配置了 .plist 文件。您为每个餐厅位置配置了自定义注释,并在其中配置了呼出按钮,以便在点击时显示餐厅详情屏幕。您还使用扩展来组织代码,使其更容易阅读和维护。

在本章中,您将使用存储在 .plist 文件中的数据。接下来,您将配置 LocationViewController 类以在用户在 ExploreViewController 实例中选择位置时存储该位置,当选择一种菜系时,ExploreViewController 类将传递所选位置和菜系到 RestaurantListViewController 实例。最后,RestaurantListViewController 类将被修改以从与所选位置和菜系对应的 JSON 文件中获取餐厅列表并显示在餐厅列表屏幕上。

到本章结束时,您将了解如何从 JSON 文件中加载数据并进行解析,以便在自己的应用中使用。您还将学习关于 UITableViewDelegate 方法以及如何从一个视图控制器传递数据到另一个视图控制器的方法。

以下内容将涵盖:

  • 从 JSON 文件中获取数据

  • 在您的应用中使用来自 JSON 文件的数据

  • 配置 MapDataManager 实例以使用来自 RestaurantDataManager 实例的数据

  • 存储用户选择的地点

  • 将位置和菜系信息传递给 RestaurantListViewController 实例

技术要求

您将继续在上一章中修改的 LetsEat 项目上工作。

本章的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter17 文件夹中,可在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频以查看代码的实际运行情况:

bit.ly/3Hl8Ulz

让我们从学习如何读取和解析 JSON 文件以获取用于应用中的数据开始。

从 JSON 文件中获取数据

第十四章**,将数据加载到集合视图中,您学习了如何加载文件,使用数据管理类从文件中读取数据,并将其放入应用中的对象中。在本章中,您也将做同样的事情,不同的是,您将读取来自 JSON 文件而不是 .plist 文件的数据。这将模拟从在线基于 Web 的服务中读取数据,其中 JSON 是一种常用的格式。让我们先了解更多关于 JSON 文件以及它们是如何工作的信息。

理解 JSON 格式

JavaScript 对象表示法 (JSON) 是一种在文件中结构化数据的方式,既可以被人阅读,也可以被计算机读取。许多 iOS 应用通过与在线基于 Web 的服务合作来访问 JSON 文件,这些文件随后被用来为应用提供数据。

在本章中,您将不会学习如何连接到在线服务。相反,您将使用从 opentable.herokuapp.com 下载的示例 JSON 文件,这些文件已被 Craig Clayton 为本书修改。您将看到,处理 JSON 文件与处理 .plist 文件类似。

为了帮助您理解 JSON 格式,您需要将示例 JSON 文件添加到您的项目中,并查看其中一个的结构。按照以下步骤操作:

  1. 在项目导航器中,在 Misc 文件夹内创建一个新的组,并将其命名为 JSON

  2. 如果您尚未下载本章的项目文件,请从以下链接下载:github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition.

  3. 解压文件夹,打开 Chapter17 文件夹中的 resources 文件夹。您应该在里面看到几个 JSON 文件。

  4. 将所有 JSON 文件拖放到您刚刚创建的 JSON 文件夹中。

  5. 在出现的屏幕上点击 完成

  6. 每个 JSON 文件都包含特定城市的餐厅详细信息。点击 Charleston.json,您应该看到以下内容:

Figure 17.1: Editor area showing contents for Charleston.json

img/Figure_17.01_B17469.jpg

图 17.1:显示 Charleston.json 内容的编辑区域

如您所见,文件以一个开方括号开始,文件内的每个项目都由包含餐厅信息的键值对组成,这些键值对被花括号包围,并用逗号分隔。在文件的最后,您可以看到一个闭方括号。方括号表示数组,花括号表示字典。换句话说,JSON 文件包含一个字典数组,这与您之前使用过的 .plist 文件完全相同。

现在您已经看到了 JSON 文件的样子,让我们在下一节创建一个数据管理类,以便将数据从 JSON 文件加载到您的应用中。

创建 RestaurantDataManager

您已经学会了如何在 第十四章 “将数据放入集合视图” 中创建一个数据管理类来从 .plist 文件加载数据。现在,您将创建 RestaurantDataManager,这是一个数据管理类,用于从您之前添加到项目中的 JSON 文件加载数据。您将看到,从 JSON 文件加载数据将与从 .plist 文件加载数据类似。

重要信息

要了解更多关于解析 JSON 文件的信息,请观看这里可用的视频:developer.apple.com/videos/play/wwdc2017/212/.

在您创建 RestaurantDataManager 类之前,您需要修改 RestaurantItem 类,使其符合 Decodable 协议。采用此协议允许您使用 JSONDecoder 类,通过 JSON 文件中的数据填充 RestaurantItem 实例。

重要信息

要了解更多关于 Decodable 和 JSON 解码器类的信息,请查看以下链接:

developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

developer.apple.com/documentation/foundation/jsondecoder

要修改RestaurantItem类以使其符合Decodable协议,请按照以下步骤操作:

  1. 在项目导航器中,点击Map文件夹中Model文件夹内的RestaurantItem文件。修改RestaurantItem类的声明以采用Decodable协议,如下所示:

    class RestaurantItem: NSObject, MKAnnotation, 
    Decodable {
    
  2. 删除init()方法并添加以下枚举以使RestaurantItem类符合Decodable协议:

    enum CodingKeys: String, CodingKey {
       case name
       case cuisines 
       case lat
       case long 
       case address
       case postalCode = "postal_code" 
       case state
       case imageURL = "image_url"
       case restaurantID = "id"
    }
    

    CodingKeys枚举将RestaurantItem类的属性与 JSON 文件中的键匹配。这允许JSONDecoder实例从 JSON 文件中获取值并将它们分配给RestaurantItem类中的属性。如果键名与属性名不匹配,你可以将键映射到属性,如前一个代码块中所示,对于postalCodeimageURL和 restaurantID。

    修改RestaurantItem类后,你将在MapDataManager文件中的fetch(completion:)方法中看到一个错误。不用担心,你将在下一节中修复它。

现在让我们创建RestaurantDataManager类,它将读取 JSON 文件并将数据放入一个RestaurantItem实例数组中。按照以下步骤操作:

  1. 右键点击Restaurants文件夹,创建一个名为Model的新组。然后右键点击Model文件夹并选择New File

  2. iOS应该已经选中。选择Swift File然后点击Next

  3. 将此文件命名为RestaurantDataManager。点击RestaurantDataManager文件出现在项目导航器中。

  4. import语句之后添加以下内容以声明RestaurantDataManager类:

    class RestaurantDataManager {
    
    }
    
  5. 在大括号之间添加以下属性以保存一个RestaurantItem实例数组:

    private var restaurantItems: [RestaurantItem] = []
    

    restaurantItems数组将存储从 JSON 文件中获取的RestaurantItem实例。private关键字表示它只能从这个类内部访问。

  6. restaurantItems属性之后添加以下方法以读取 JSON 文件并返回一个RestaurantItem实例数组:

    func fetch(location: String, selectedCuisine: 
    String = "All", completionHandler: (_ 
    restaurantItems: [RestaurantItem]) -> Void) { 
       if let file = Bundle.main.url(forResource: 
       location, withExtension: "json") {
          do {
             let data = try Data(contentsOf: file)
             let restaurants = try JSONDecoder().decode(
             [RestaurantItem].self, from: data)
             if selectedCuisine != "All" {
                restaurantItems = restaurants.filter {
                  ($0.cuisines.contains(selectedCuisine))
                }
             } else { 
                restaurantItems = restaurants 
             }
          } catch {
             print("There was an error \(error)")
          }
       }
       completionHandler(restaurantItems)
    }
    

    让我们分解一下:

    func fetch(location: String, selectedCuisine: 
    String = "All", completionHandler: (_ restaurantItems:
    [RestaurantItem]) -> Void)
    

    此方法接受三个参数:location,一个包含餐厅位置的字符串,selectedCuisine,一个包含用户选择的菜系的字符串,以及completionHandler,一个闭包,用于在方法执行完毕后处理此方法的执行结果。如果您不提供selectedCuisine的值,它将默认为"All"

    if let file = Bundle.main.url(forResource: location, 
    withExtension: "json")
    

    这将获取应用包中 JSON 文件的网络地址并将其分配给file

    do代码块:

    第一条语句尝试将file的内容分配给data。下一条语句尝试使用JSONDecoder实例解析data并将其存储为RestaurantItem实例数组,该数组被分配给restaurants。在下一条语句中,如果selectedCuisine不是All,则使用{($0.cuisines.contains(selectedCuisine))}闭包将filter方法应用于restaurants数组。这导致一个RestaurantItem实例数组,其中cuisines属性包含用户选择的菜系,并将此数组分配给restaurantItems。否则,整个restaurants数组被分配给restaurantItems

    catch代码块:

    如果do代码块失败,此操作将在调试区域打印错误信息。

    completionHandler(items)
    

    此语句使用提供的闭包处理restaurantItems数组。

    注意,当你使用 Xcode 调用此方法时,自动完成功能会给你两个可能的选择;一个包含selectedCuisine:参数(该参数包含所选菜系字符串)的选项,另一个不包含(selectedCuisine设置为全部)。

  7. fetch(location:selectedCuisine:completionHandler:)方法之后添加一个方法,返回restaurantItems数组中的项目数量:

    func numberOfRestaurantItems() -> Int {
       restaurantItems.count
    }
    

    你将调用此方法来确定在餐厅列表屏幕中显示的收集视图单元格的数量。

  8. numberOfRestaurantItems()方法之后添加一个方法,从restaurantItems数组中返回索引提供的RestaurantItem实例:

    func restaurantItem(at index: Int) -> 
    RestaurantItem {
       restaurantItems[index]
    }
    

    你将调用此方法来配置餐厅列表屏幕中每个收集视图单元格的内容。

已创建RestaurantDataManager类,它允许你读取存储在JSON文件中的数据并将其放入RestaurantItem实例数组中。在使用它之前,你需要相当大幅度地修改你的项目。让我们看看在下一节中显示地图餐厅列表屏幕所需的条件。

在你的应用中使用来自 JSON 文件的数据

让我们回顾一下应用的工作原理。在地图屏幕中,用户将看到用户位置附近的全部餐厅。点击餐厅将显示一个呼出气泡,点击呼出按钮将显示该餐厅的详细信息在餐厅详情屏幕中。

探索屏幕中,用户将点击位置按钮并在位置屏幕上选择一个位置,例如查尔斯顿,北卡罗来纳州。选择位置后,用户点击完成按钮,将返回到探索屏幕。然后,用户将在探索屏幕中选择一个菜系,该位置提供该菜系的餐厅列表将在餐厅列表屏幕中显示。点击餐厅将显示该餐厅的详细信息在餐厅详情屏幕中。

你需要执行以下操作:

  • 配置MapViewController类,以便从 JSON 文件而不是.plist文件中获取餐厅列表。这将修复MapDataManager类中的fetch(completion:)方法的错误。

  • 配置LocationViewController类以存储用户选定的位置。

  • 将选定的位置传递给ExploreViewController实例。

  • 配置ExploreViewController类,以便将选定的位置和菜系传递给RestaurantListViewController实例。

  • 配置RestaurantListViewController类,以便从与选定位置对应的 JSON 文件中获取餐厅列表。

  • 配置RestaurantListViewController类,以便根据选定的位置和菜系显示餐厅列表。

这可能看起来有些令人畏惧,所以您将逐步进行。首先,您将配置MapDataManager类,使其从 JSON 文件而不是.plist文件中读取数据。

配置 MapDataManager 实例以使用 RestaurantDataManager 实例的数据

目前,MapDataManager文件中存在一个错误。这是因为您在MapDataManager类中的fetch(completion:)方法调用了您从RestaurantItem类中移除的初始化方法。您现在将更新MapDataManager类,使其使用RestaurantDataManager实例作为数据源,从而修复错误。在项目导航器中点击MapDataManager文件(位于Map文件夹中的Model文件夹内),并按以下方式更新fetch(completion:)方法:

func fetch(completion: (_ annotations: [RestaurantItem]) -> ()){
   let manager = RestaurantDataManager()
manager.fetch(location: "Boston", completionHandler: { 
      (restaurantItems) in self.items = restaurantItems
      completion(items)
   })
}

让我们分解一下:

func fetch(completion: (_ annotations: [RestaurantItem]) -> ())

此方法有一个完成方法参数。完成方法将在方法执行完毕后用于处理结果。

let manager = RestaurantDataManager() 

这将创建一个RestaurantDataManager类的实例,并将其分配给manager

manager.fetch(location: "Boston", completionHandler: { 
   (restaurantItems) in self.items = restaurantItems
   completion(items)
})

这将调用manager实例的fetch()方法,从Boston.json获取餐厅列表。目前这是硬编码的,因为 iOS 模拟器没有功能性的 GPS。要查看不同位置的餐厅,请更改用于另一个位置的 JSON 文件名称。此方法返回的RestaurantItem实例数组被分配给MapViewController实例的items数组,并且传入的完成方法用于处理此数组。正如您在上一章中看到的,这将生成将在地图屏幕中添加到地图视图的注释。

重要信息

要了解更多关于如何确定您位置的信息,请访问developer.apple.com/documentation/mapkit/mkmapview/converting_a_user_s_location_to_a_descriptive_placemark

如果您现在运行您的应用程序并选择地图屏幕,您应该会看到波士顿的餐厅标记。

在下一节中,您将配置LocationViewController类,使其能够存储用户选定的位置。

存储用户选定的位置

目前,LocationDataManager 类从 Locations.plist 加载数据并将位置信息存储在字符串数组中。你将创建一个新的结构 LocationItem,并配置 LocationDataManager 类以将位置存储在 LocationItem 实例数组中。之后,你将修改 LocationViewController 类,使其能够存储包含用户选中位置的一个 LocationItem 实例。然后你可以将此实例传递到你的应用中的 RestaurantListViewController 实例。按照以下步骤:

  1. 右键单击 Location 文件夹内的 Model 文件夹,然后选择 New File

  2. iOS 应已选中。选择 Swift File 然后点击 Next

  3. 将此文件命名为 LocationItem。点击后,LocationItem 文件将出现在项目导航器中。

  4. LocationItem 文件中,在 import 语句之后添加以下内容以声明和定义 LocationItem 结构:

    struct LocationItem { 
       let city: String?
       let state: String?
    }
    extension LocationItem {
       init(dict: [String: String]) {
          self.city = dict["city"]
          self.state = dict["state"]
       }
       var cityAndState: String {
          guard let city = self.city, let state = 
          self.state else { 
             return "" 
          }
          return "\(city), \(state)"
       }
    }
    

    LocationItem 结构有两个 String 属性,citystate

    init() 方法接受一个字典 dict 作为参数,并将 citystate 键的值分配给 citystate 属性。

    cityAndState 计算属性返回由 citystate 值组合而成的字符串。

现在,你将更新 LocationDataManager 类,使其能够将城市和州信息存储在 LocationItem 实例数组中而不是字符串。按照以下步骤:

  1. 在项目导航器中点击 LocationDataManager 并修改 locations 数组以存储 LocationItem 实例而不是字符串:

    private var locations:[LocationItem] = []
    
  2. fetch() 方法现在将显示一个错误。修改 fetch() 方法以使用 LocationItem 实例而不是字符串:

    func fetch() {
       for location in loadData() {
          loadData() method is now used to initialize LocationItem instances instead of strings, which are then appended to the locations array. The loadData() method still uses the same Locations.plist file that you created in *Chapter 15*, *Getting Started with Table Views*.
    
  3. locationItem(at:) 方法现在显示了一个错误。修改它,使其返回一个 LocationItem 实例而不是一个字符串:

    func locationItem(at index: Int) -> 
    LocationItem {
       locations[index]
    }
    

到目前为止,你已经完成了 LocationDataManager 类。接下来,你将更新 LocationViewController 类,使用 LocationItem 实例而不是字符串来填充表格视图单元格。按照以下步骤:

  1. 在项目导航器中点击 LocationViewController 文件。

  2. 你会在 tableView(_:cellForRowAtIndexPath:) 方法中看到一个错误。这个错误是因为你不能将一个 LocationItem 实例分配给单元格的 textLabel 属性。按照以下方式修改此方法:

    func tableView(_ tableView: UITableView, cellForRowAt 
    indexPath: IndexPath) -> UITableViewCell {
       let cell = tableView.dequeueReusableCell(
       withIdentifier: "locationCell", for: indexPath)
       cityAndState property of the LocationItem structure returns a string that combines a location's city and state strings, fixing the error.
    
  3. 你需要一个属性来跟踪用户的选中项。在 manager 声明之后添加以下属性声明:

    let manager = LocationDataManager()
    var selectedCity: LocationItem?
    

要处理用户与表格视图的交互,你将使 LocationViewController 类遵守 UITableViewDelegate 协议。

提示

UITableViewDelegate 协议在 第十五章**,开始使用表格视图 中进行了介绍。

UITableViewDelegate 协议指定了当用户与其中的行交互时,表格视图将向其代理发送的消息。按照以下步骤采用它:

  1. UITableViewDataSource扩展之后添加以下扩展:

    // MARK: UITableViewDelegate
    extension LocationViewController: 
    UITableViewDelegate {
    
    }
    

    此扩展有助于保持您的代码整洁,而// MARK:语法使得此扩展在编辑器区域中易于查找。

  2. 当用户在表格视图中点击一行时,会触发UITableViewDelegate方法tableView(_:didSelectRowAt:)。在扩展的大括号之间添加此方法。它应该看起来像以下这样:

    // MARK: UITableViewDelegate
    extension LocationViewController: UITableViewDelegate 
    {
    selectedCity property is assigned the corresponding LocationItem instance in the locations array. For example, if you tap the third row, the LocationItem instance with the values "Charleston" and "NC" is assigned to selectedCity. 
    

LocationViewController类现在可以存储用户选择的位置,但在您在ExploreViewController类中选择位置并将其分配给完成按钮之前,您首先需要在Explore屏幕中创建一个新的视图控制器来管理集合视图部分标题。这将允许您在Explore屏幕中显示用户选择的位置。您将在下一节中这样做。

在 Explore 屏幕中为部分标题添加一个 UICollectionReusableView 子类

集合视图部分标题的UICollectionReusableView子类,并为副标题标签设置一个出口,以便您可以在集合视图部分标题中显示用户选择的位置。按照以下步骤操作:

  1. 右键单击Explore文件夹内的View文件夹,然后选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch 类然后点击下一步

  3. 按照以下方式配置文件:

    ExploreHeaderView

    UICollectionReusableView

    Swift

    点击下一步

  4. 点击ExploreHeaderView文件出现在项目导航器中。验证它是否包含以下内容:

    class ExploreHeaderView: UICollectionReusableView {
    
    }
    
  5. 在项目导航器中点击Main故事板文件。选择ExploreHeaderView

![图 17.2:将类设置为 ExploreHeaderView 的标识检查器]

](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_17.02_B17469.jpg)

图 17.2:将类设置为 ExploreHeaderView 的标识检查器

注意,在文档大纲中可重用视图将更改为Explore 标题视图

现在ExploreHeaderView类正在管理集合视图部分标题。在下一节中,您将把集合视图部分标题中的副标题标签链接到ExploreHeaderView类中的一个出口,并在ExploreViewController类中为ExploreHeaderView类添加一个属性。

将部分标题的标签连接到 ExploreViewController 类

要在集合视图部分标题中显示用户选择的城市,您需要将副标题标签连接到ExploreHeaderView类中的一个出口,然后在ExploreViewController类中为它添加一个属性。按照以下步骤操作:

  1. 在文档大纲中,点击Explore 标题视图的副标题标签(它是带有文本请选择一个位置的标签):![图 17.3:带有请选择一个位置标签选中的文档大纲]

    ](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_17.03_B17469.jpg)

    图 17.3:带有请选择一个位置标签选中的文档大纲

  2. 点击调整编辑器选项按钮,从菜单中选择助手。一个助手编辑器出现在屏幕的右侧。确保它显示的是ExploreHeaderView文件的内容。

  3. Ctrl + 拖动请选择一个位置标签到花括号之间:![Figure 17.4: 编辑器区域显示 ExploreHeaderView 文件内容

    ![img/Figure_17.04_B17469.jpg]

    Figure 17.4: 编辑器区域显示 ExploreHeaderView 文件内容

  4. 在出现的框中,将名称设置为locationLabel,然后点击连接

  5. locationLabel出口已在ExploreHeaderView类中创建。通过点击x按钮关闭辅助编辑器。

  6. 在项目导航器中点击ExploreViewController文件。在manager声明之后添加以下属性声明,以存储由LocationViewController实例传递给ExploreViewController实例的位置:

    let manager = ExploreDataManager()
    var selectedCity: LocationItem?
    
  7. 在声明selectedCity属性之后,声明一个属性,该属性将被分配一个ExploreHeaderView实例,这将允许ExploreViewController实例设置locationLabel的值:

    let manager = ExploreDataManager()
    var selectedCity: LocationItem?
    var headerView: ExploreHeaderView!
    

接下来,让我们配置locationLabel,当点击时将其设置为用户选择的市和州。然后,探索屏幕将出现,所选城市显示在集合视图部分的标题中。你将在下一节中完成这项操作。

向完成按钮添加撤销操作方法

第十章**,构建您的用户界面中,您为ExploreViewController类添加了一个撤销操作方法,该方法将ExploreViewController实例中的selectedCity属性撤销到用户选择的位置。按照以下步骤操作:

  1. unwindLocationCancel(segue:)方法之后添加以下内容,以实现LocationViewController实例的撤销操作,目标视图控制器是一个ExploreViewController实例。此方法首先检查源视图控制器是否是LocationViewController实例。如果是,则将LocationViewController实例的selectedCity属性的值分配给如果存在的ExploreViewController实例的selectedCity属性。如果ExploreViewController实例的selectedCity属性有值,则将其分配给location,并将副标题标签的文本设置为locationcityAndState属性。

  2. 修改collectionView(_:viewForSupplementaryElementOfKind:at:)方法如下:

    func collectionView(_ collectionView: UICollectionView, 
    viewForSupplementaryElementOfKind kind: String, at 
    indexPath: IndexPath) -> UICollectionReusableView {
       let UICollectionViewDataSource protocol. It returns the view that will be used as the collection view section header. Here, the collection view section header in the ExploreHeaderView instance.Important InformationYou can learn more about the `UICollectionViewDataSource` protocol at this link: [ https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618037-collectionview](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618037-collectionview).
    
  3. 点击Main故事板文件。选择位置视图控制器场景。为了设置由完成按钮触发的操作,Ctrl + 拖动完成按钮到场景工具栏中的退出图标:![Figure 17.5: 位置视图控制器场景显示完成按钮操作设置

    ![img/Figure_17.05_B17469.jpg]

    Figure 17.5: 位置视图控制器场景显示完成按钮操作设置

  4. 从弹出菜单中选择 unwindLocationDoneWithSegue:。这将在 ExploreViewController 类中将 unwindLocationDone(segue:) 返回操作链接起来:

图 17.6:显示 unwindLocationDoneWithSegue: 已选的弹出菜单

图 17.6:显示 unwindLocationDoneWithSegue: 已选的弹出菜单

构建并运行你的应用,并点击 地点 按钮。点击一个城市,行中会出现一个勾选标记。点击 完成

图 17.7:iOS 模拟器显示已选择地点的地点屏幕

图 17.7:iOS 模拟器显示已选择地点的地点屏幕

选定的城市名称和州将替换集合视图部分标题内的 请选择一个地点 文本:

图 17.8:iOS 模拟器显示带有副标题标签的探索屏幕

图 17.8:iOS 模拟器显示带有副标题标签的探索屏幕

虽然这可行,但在选择地点时你需要修复两个问题。第一个问题是你可以选择多个地点:

图 17.9:iOS 模拟器显示已选择多个地点的地点屏幕

图 17.9:iOS 模拟器显示已选择多个地点的地点屏幕

你希望用户只能选择一个地点,如果选择了另一个地点,则之前选择的地点应该取消选中。

第二个问题是,如果你在 地点 屏幕中点击 完成 并再次在 探索 屏幕中点击 地点 按钮,用户选择的地点旁边的勾选标记会消失:

图 17.10:iOS 模拟器显示缺少勾选标记的地点屏幕

图 17.10:iOS 模拟器显示缺少勾选标记的地点屏幕

当你再次回到 地点 屏幕时,最后选择的地点应该有一个勾选标记。

让我们在下一节修改 LocationDataManager 类,以确保一次只能选择一个地点。

在地点屏幕中仅选择一个地点

目前,你可以在 地点 屏幕中选择多个地点。你应该只能选择一个地点,如果选择了另一个地点,则之前选择的地点应该取消选中。按照以下步骤操作:

  1. 点击 LocationItem 文件。通过修改 LocationItem 结构使其符合 Equatable 协议:

    struct LocationItem: LocationItem instances are equal.
    
  2. 点击 LocationViewController 文件。在 viewDidLoad() 方法之后,添加一个方法,只为包含所选城市的行设置勾选标记:

    private func setCheckmark(for cell: UITableViewCell, 
    location: LocationItem) {
       if selectedCity == location {
          cell.accessoryType = .checkmark {
       } else {
          cell.accessoryType = .none
       }
    }
    

    setCheckmark(for:location:) 方法接受 cell,一个表格视图单元格,和 location,一个 LocationItem 实例,作为参数。如果 locationselectedCity 相等,则设置该行的勾选标记。否则,不设置勾选标记。

  3. tableView(_:cellForRowAt:)方法中,修改如下代码,在设置单元格的textLabel属性文本的行之后调用setCheckmark(for:location:)

    func tableView(_ tableView: UITableView, cellForRowAt 
    indexPath: IndexPath) -> UITableViewCell {
       let cell = tableView.dequeueReusableCell(
       withIdentifier: "locationCell", for: indexPath)
       let location = manager.locationItem(at 
       indexPath.row)
       cell.textLabel?.text = location.cityAndState 
       setCheckmark(for:at:) will be called when each row in the table view is rendered, and the checkmark will only be set on the row containing the selected location.
    
  4. tableView(_:didSelectRowAt:)方法中,修改if语句中的代码,在选择了位置后重新加载表格视图(从而渲染其中的所有行):

    if let cell = tableView.cellForRow(at: indexPath) {
       cell.accessoryType = .checkmark
       selectedCity = manager.locationItem(at:
       indexPath.row)
       tableView.reloadData()
    }
    

构建并运行您的项目。现在您应该只能设置一个位置,如果您选择另一个位置,之前选择的位置将被取消选中。

在下一节中,您将修复第二个问题,以便一旦选择了一个位置,当您返回到RestaurantListViewController实例时,它将保持选中状态,这样最终可以显示用户选择的菜系在特定位置的餐厅列表。

小贴士

这是一章很长的内容,所以您可能希望在这里休息一下。

将位置和菜系信息传递给 RestaurantListViewController 实例

目前,您可以在RestaurantListViewController实例中设置位置,然后它会显示所选位置的提供所选菜系的餐厅。如果您在Locations屏幕中之前选择了一个位置,您还将使旁边的选择标记重新出现。按照以下步骤操作:

  1. 您将为连接到Main故事板文件的每个切换添加标识符,并点击Explore View Controller Scene。选择Explore View Controller SceneLocation View Controller Scene之间的切换:![Figure 17.11: Editor area showing segue between Explore and Location screens selected

    ![img/Figure_17.11_B17469.jpg]

    图 17.11:编辑区域显示 Explore 和 Location 屏幕之间的切换选择

  2. 点击属性检查器按钮。在locationList下:![Figure 17.12: Attributes inspector with Identifier set to locationList

    ![img/Figure_17.12_B17469.jpg]

    图 17.12:属性检查器中设置标识符为 locationList

  3. 选择restaurantList之间的切换:![Figure 17.13: Attributes inspector identifier setting for the segue between

    Explore 和 Restaurant List 屏幕

    ![img/Figure_17.13_B17469.jpg]

    图 17.13:设置 Explore 和 Restaurant List 屏幕之间切换的属性检查器标识符

    一旦您知道哪个切换正在发生,您可以为每个切换指定要执行的方法。您将创建两个方法,showLocationList(segue:)showRestaurantList(segue:),您将使用prepare(for:sender:)方法根据哪个切换正在发生来执行所需的方法。

  4. 在项目导航器中点击ExploreViewController文件。在private扩展中,在unwindLocationCancel()方法之前声明并定义showLocationList(segue:)方法,以便如果之前设置了用户选择的城市,则将其传递回LocationViewController实例:

    func showLocationList(segue: UIStoryboardSegue) {
       guard let navController = segue.destination as? 
       UINavigationController, let viewController = 
       navController.topViewController as? 
       LocationViewController else {
          return
       }
       viewController.selectedCity = selectedCity
    }
    

    Main故事板文件之前,showLocationList(segue:)方法会被调用,你嵌入的viewControllers属性包含一个视图控制器数组,并且数组中的最后一个视图控制器的视图在屏幕上是可见的。你可以使用导航控制器的topViewController属性来访问数组中的最后一个视图控制器。

    guard语句检查 segue 目标是否是UINavigationController实例,以及topViewController是否是LocationViewController实例。如果是,则检查ExploreViewController实例的selectedCity属性是否包含值。如果包含,则将该值分配给LocationViewController实例的selectedCity属性。记住,LocationViewController实例的setCheckmark(for:at:)方法将为表格视图中的每一行调用,并在包含选定城市的行上设置勾选标记。

这将修复RestaurantListViewController实例的第二个问题。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantListViewController文件。在RestaurantListViewController类中@IBOutlet声明之前添加以下属性:

    selectedRestaurant will be set if you pick a restaurant in the selectedCity stores the city you picked in the selectedCuisine stores the cuisine you picked in the Explore screen.
    
  2. viewDidLoad()方法之后添加以下代码,以便在RestaurantListViewController实例的视图出现在屏幕上时,将选定的城市和菜系打印到调试区域:

    override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       print("selected city \(selectedCity as Any)")
       print("selected cuisine \(selectedCuisine as Any)")
    }
    

    viewDidAppear()方法会在每次视图控制器视图出现在屏幕上时被调用,而viewDidLoad()方法则只会在视图控制器最初加载其视图时被调用一次。在这里使用viewDidAppear()是因为RestaurantListViewController实例每次其视图出现在屏幕上时都会显示不同的餐厅列表,这取决于用户的选择。目前,代码只是将选定的位置和菜系打印到调试区域,因此你可以看到这些值正在正确传递。

    重要信息

    要了解更多关于视图控制器生命周期的信息,请参阅此链接:developer.apple.com/documentation/uikit/uiviewcontroller

  3. 在项目导航器中点击ExploreViewController文件。在showLocationList(segue:)方法之后声明并定义showRestaurantList(segue:)方法,以设置RestaurantListViewController实例的selectedCuisineselectedCity属性:

    func showRestaurantList(segue: UIStoryboardSegue) {
       if let viewController = segue.destination as? 
       RestaurantListViewController, let city = 
       selectedCity, let index = 
       collectionView.indexPathsForSelectedItems?.first?
       .row {
          viewController.selectedCuisine = 
          manager.exploreItem(at: index).name
          viewController.selectedCity = city
       }
    }
    

    if-let 语句检查目标视图控制器是否为 RestaurantListViewController 实例之前,你将调用此方法,如果它是,则将 city 设置为 ExploreViewController 实例的 selectedCity 值,并获取用户点击的集合视图单元格的索引。如果该语句成功,则将 RestaurantListViewController 实例的 selectedCuisine 属性设置为 items 数组中该索引处的 ExploreItem 实例的 name。在下一行,RestaurantListViewController 实例的 selectedCity 属性将被分配 city 中存储的值。

为了使此方法正常工作,必须在过渡到 餐厅详情 屏幕之前先设置 ExploreViewController 实例的 selectedCity 属性。你将在选择菜系之前提醒用户设置城市。按照以下步骤操作:

  1. 在项目导航器中点击 ExploreViewController 文件。在 unwindLocationCancel() 之前添加以下方法以显示警报:

    func showLocationRequiredAlert() {
       let alertController = UIAlertController(title: 
       "Location Needed", message: "Please select a 
       location.", preferredStyle: .alert)
       let okAction = UIAlertAction(title: "OK", style: 
       .default, handler: nil)
       alertController.addAction(okAction)
       present(alertController, animated: true, 
       completion: nil)
    }
    

    showLocationRequiredAlert() 方法创建一个标题设置为 "Location Needed" 和消息的 UIAlertController 实例。然后向 UIAlertController 实例添加一个带有 OK 按钮的 UIAlertAction 实例。最后,将警报呈现给用户,并点击 OK 按钮以关闭它。

  2. viewDidLoad() 之后添加以下代码以显示此警报,如果尚未选择位置:

    override func shouldPerformSegue(withIdentifier 
    identifier: String, sender: Any?) -> Bool {
       if identifier == Segue.restaurantList.rawValue, 
       selectedCity == nil {
          showLocationRequiredAlert()
          return false
       }
       return true
    }
    

    shouldPerformSegue(withIdentifier:sender:) 方法用于检查 restaurantList 是否已设置以及 selectedCity 是否已设置;如果没有,则调用 showLocationRequiredAlert() 方法,并且 shouldPerformSegue(withIdentifier:sender:) 返回 false。否则,shouldPerformSegue(withIdentifier:sender:) 返回 true,并且出现 餐厅列表 屏幕。

现在你已经创建了 showLocationList(segue:)showRestaurantList(segue:) 方法,你将在 viewDidLoad() 之后和 shouldPerformSegue(withIdentifier:) 之前添加代码以实现 prepare(for:sender:) 方法。这个方法根据将要执行的 segue 调用 showLocationList(segue:)showRestaurantList(segue:)

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   switch segue.identifier! {
   case Segue.locationList.rawValue:
     showLocationList(segue: segue)
   case Segue.restaurantList.rawValue:
     showRestaurantList(segue: segue)
   default:
      print("Segue not added")
   }
}

locationList,因此 showLocationList(segue:) 方法在过渡到 restaurantList 之前执行,所以 showRestaurantListing(segue:) 方法在过渡到 RestaurantListViewController 实例中的 selectedTypeselectedCity 属性之前执行,这将打印到调试区域。

构建并运行你的项目。如果你尝试选择一种菜系,你会看到这个警报,指出你需要选择一个位置:

图 17.14:iOS 模拟器显示警报

图 17.14:iOS 模拟器显示警报

如果你选择了一个位置,点击 完成,然后再次点击 位置 按钮,之前选择的位置应该仍然被选中:

![图 17.15:iOS 模拟器显示带有勾选标记的位置屏幕

![img/Figure_17.15_B17469.jpg]

图 17.15:iOS 模拟器显示带有勾选标记的位置屏幕

如果你选择了一个菜系,你会看到餐厅列表屏幕:

![图 17.16:iOS 模拟器显示餐厅列表屏幕

![img/Figure_17.16_B17469.jpg]

图 17.16:iOS 模拟器显示餐厅列表屏幕

你选择的位置和菜系将出现在调试区域:

![图 17.17:显示所选位置和菜系的调试区域

![img/Figure_17.17_B17469.jpg]

图 17.17:显示所选位置和菜系的调试区域

现在,RestaurantListViewController实例有了位置,你可以从RestaurantDataManager实例获取该位置的餐厅数据。在项目导航器中点击RestaurantListViewController文件,并更新viewDidAppear()如下。这将打印出所选位置和菜系提供的餐厅列表:

override func viewDidAppear(_ animated: Bool) {
   super.viewDidAppear(animated)
guard let city = selectedCity?.city, let cuisine = 
selectedCuisine else { 
return 
   }
   let manager = RestaurantDataManager()
   manager.fetch(location: city, selectedCuisine: cuisine) {
      restaurantItems in if !restaurantItems.isEmpty {
         for restaurantItem in restaurantItems {
            if let restaurantName = restaurantItem.name {
               print(restaurantName)
            }
         }
      } else {
         print("No data")
      }
   }
}

guard语句检查citycuisine是否已成功分配了值,如果没有则返回。接下来,创建一个RestaurantDataManager实例并将其分配给managerfetch(location:selectedCuisine:completion:)方法返回一个包含所选citycuisineRestaurantItem实例的数组,for循环将餐厅名称打印到调试区域。如果没有符合标准的餐厅,调试区域将打印No data。构建并运行你的项目,选择一个城市,点击一个菜系,并在调试区域中注意结果。

你也可以在报告导航器中看到结果。点击报告导航器按钮并选择如所示的第一项:

![图 17.18:显示餐厅名称列表的报告导航器

![img/Figure_17.18_B17469.jpg]

图 17.18:显示餐厅名称列表的报告导航器

你将在编辑器区域看到餐厅列表或No data

因此,到目前为止,RestaurantListViewController实例已成功获取显示餐厅列表所需的数据。现在你有了这些数据,你需要配置集合视图以向用户显示它。为此,你需要创建集合视图单元格的视图控制器,并配置RestaurantListViewController实例以填充它们。你将在下一节中这样做。

为餐厅列表屏幕上的单元格创建视图控制器

目前为这个目的使用RestaurantCell类。按照以下步骤操作:

  1. 右键点击Restaurants文件夹并选择查看

  2. 右键点击View文件夹并选择新建文件

  3. iOS应该已经选中。选择Cocoa Touch Class然后点击下一步

  4. 按照以下方式配置文件:

    RestaurantCell

    UICollectionViewCell

    Swift

    点击下一步

  5. 在项目导航器中点击RestaurantCell文件。它包含RestaurantCell类的实现:

    import UIKit
    class RestaurantCell: UICollectionViewCell {
    }
    

现在让我们在RestaurantCell类中创建用于收集视图单元格的出口。你将在下一节中完成这个操作。

连接 RestaurantCell 类的出口

现在你已经创建了RestaurantCell类,你需要在其中创建出口并将它们链接到收集视图单元格内的 UI 元素,以便RestaurantCell实例管理收集视图单元格显示的内容。按照以下步骤操作:

  1. 点击Main故事板文件。在RestaurantCell中点击restaurantCell图 17.19:restaurantCell 的标识检查器类设置

    图 17.19:restaurantCell 的标识检查器类设置

  2. 在点击调整编辑器选项按钮时在辅助编辑器中显示的RestaurantCell.swift文件中:图 17.20:文档大纲中选中 Available Times 标签

    图 17.20:文档大纲中选中 Available Times 标签

  3. 点击调整编辑器选项按钮并从菜单中选择助手

  4. 辅助编辑器出现。顶部的路径栏应显示RestaurantCell文件:图 17.21:辅助编辑器显示 RestaurantCell.swift

    图 17.21:辅助编辑器显示 RestaurantCell.swift

  5. 在出现的框中,在名称字段中输入titleLabel并点击连接图 17.22:显示标签名称设置为 titleLabel 的对话框

    图 17.22:显示标签名称设置为 titleLabel 的对话框

  6. 从你刚刚创建的副标题titleLabel属性处Ctrl + 拖动。在出现的框中,在名称字段中输入cuisineLabel并点击连接图 17.23:显示标签名称设置为 cuisineLabel 的对话框

    图 17.23:显示标签名称设置为 cuisineLabel 的对话框

  7. american图像视图处Ctrl + 拖动到其他你创建的属性之后。在出现的框中,在名称字段中输入restaurantImageView并点击连接图 17.24:显示图像视图名称设置为 restaurantImageView 的对话框

    图 17.24:显示图像视图名称设置为 restaurantImageView 的对话框

  8. 点击x按钮关闭辅助编辑器。

RestaurantCell类的出口现在已连接到收集视图单元格中的 UI 元素。稍后你将配置RestaurantListViewController实例以填充这个收集视图,但在你这样做之前,有一个需要考虑的可能性。用户对位置和菜系的选项可能不会返回任何结果,因此你需要实现一个屏幕来通知用户没有数据可以显示。你将在下一节中完成这个操作。

显示自定义 UIView 以指示没有可用数据

用户在 UIView 子类和相应的 XIB 文件中做出的位置和餐饮选择。XIB 代表 Xcode 接口构建器,在实现故事板之前,XIB 文件被用来创建用户界面。现在,让我们按照以下步骤创建这两个文件:

  1. 右键点击 Misc 文件夹并选择 No Data

  2. 右键点击 No Data 文件夹并选择 新建文件

  3. iOS 应已选中。选择 Cocoa Touch 类 然后点击 下一步

  4. 按照以下方式配置文件:

    NoDataView

    UIView

    Swift

    点击 下一步

  5. 点击 NoDataView 文件出现在项目导航器中。

  6. 右键点击 No Data 文件夹并创建一个新文件。

  7. iOS 应已选中。选择 视图 然后点击 下一步

  8. 将此文件命名为 NoDataView。点击 NoDataView XIB 文件出现在项目导航器中。

  9. 在项目导航器中点击 NoDataView 文件并声明和定义 NoDataView 类如下:

    class NoDataView: UIView {
       var view: UIView!
       @IBOutlet var titleLabel: UILabel!
       @IBOutlet var descLabel: UILabel!
       override init(frame: CGRect) { 
          super.init(frame: frame) 
          setupView()
       }
       required init?(coder: NSCoder) { 
          super.init(coder: coder)
          setupView()
       }
       func loadViewFromNib() -> UIView {
          let nib = UINib(nibName: "NoDataView", bundle: 
          Bundle.main)
          let view = nib.instantiate(withOwner: self, 
          options: nil) [0] as! UIView 
          return view
       }
       func setupView() {
          view = loadViewFromNib()
          view.frame = bounds
          view.autoresizingMask = [.flexibleWidth, 
          .flexibleHeight]
          addSubview(view)
       }
       func set(title: String, desc: String) {
          titleLabel.text = title
          descLabel.text = desc
       }
    }
    

此类是 UIView 类的子类,它将管理 NoDataView XIB 文件中的视图。让我们分解一下:

var view: UIView!
@IBOutlet var titleLabel: UILabel!
@IBOutlet var descLabel: UILabel!

view 将在初始化期间分配来自 NoDataView XIB 文件的视图。

titleLabeldescLabel 将被分配给两个将在下一节构建用户界面时放置在 NoDataView XIB 文件中的 UILabel 实例。

override init(frame: CGRect) { 
   super.init(frame:frame) 
   setupView()
}
required init?(coder: NSCoder) { 
   super.init(coder: coder)
   setupView()
}

NoDataView 类是 UIView 的子类。一个 UIView 对象有两个 init 方法:第一个处理视图的编程创建,第二个处理从设备上存储的应用程序包中加载 XIB 文件。在这里,两种方法都将调用 setupView()

func loadViewFromNib() -> UIView {
   let nib = UINib(nibName: "NoDataView", bundle: 
   Bundle.main)
   let view = nib.instantiate(withOwner: self, 
   options: nil) [0] as! UIView
   return view
}

此方法查找并加载 NoDataView XIB 文件从应用程序包中,并返回存储在其内部的 UIView 实例。

func setupView() {
   view = loadViewFromNib()
   view.frame = bounds
   view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
   addSubview(view)
}

此方法调用 loadViewFromNib(),配置视图使其与设备屏幕大小相同,使视图的宽度和高度灵活以适应大小和方向变化,并将其添加到设备视图层次结构中以便在屏幕上可见。

func set(title: String, desc: String) {
   titleLabel.text = title
   descLabel.text = desc
}

此方法设置 titleLabeldescLabel 属性的文本。

现在让我们设置 NoDataView XIB 文件。你可能需要参考 第十二章**,修改和配置单元格,它更详细地介绍了使用大小检查器和自动布局约束菜单。按照以下步骤操作:

  1. 在项目导航器中点击 NoDataView XIB 文件。

  2. 选择 NoDataView 并按 Enter 键。

  3. 点击库按钮以显示库。在过滤器字段中输入 label。一个 标签 对象出现在结果中。

  4. 将两个 标签 对象拖入 视图,一个标签对象在另一个标签对象上方。

  5. 选择顶部标签以表示标题。在属性检查器中更新以下值:

    Default (Label Color) 下的文本框中输入 TITLE GOES HERE

    Center

    System Bold 26.0

  6. 在选择相同的标签的情况下,在大小检查器中更新以下值:

    335

    36

  7. 选择底部标签。这将代表描述。在属性检查器中更新以下值:

    默认(标签颜色)下的文本字段中输入Description goes here

    Center

    System Thin 17.0

  8. 在大小检查器中更新以下值:

    335

    21

  9. 通过单击第一个标签并按住Shift键同时选择第二个标签来选择两个标签。

  10. 在两个标签仍然被选择的情况下,点击编辑菜单并选择嵌入 | 堆叠视图

  11. 选择Vertical

    Center

    8

  12. 使用 10

    10

    点击添加 2 个约束按钮。

  13. 在选择Stack View的情况下,点击对齐按钮。设置以下值:

    水平容器内(勾选)

    垂直容器内(勾选)

    点击添加 2 个约束按钮。

  14. 在文档大纲中选择文件所有者

  15. 打开连接检查器,将titleLabel连接到显示TITLE GOES HERE的标签。

  16. descLabel连接到另一个标签。

当你完成时,你应该看到以下内容:

Figure 17.25: 显示 NoDataView XIB 文件内容的编辑区域

Figure 17.25: 显示 NoDataView XIB 文件内容的编辑区域

你已经完成了NoDataView.xib的配置。现在,让我们将其全部组合起来,以便在没有任何餐厅提供所选菜系的位置时显示NoDataView。你将在下一节中完成此操作。

在餐厅列表屏幕上显示餐厅列表

你现在拥有了在餐厅列表屏幕上根据所选位置和菜系显示餐厅列表所需的一切。所以,现在是时候将其全部组合起来。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantListViewController文件。在selectedRestaurant属性之前添加以下内容以创建RestaurantDataManager实例并将其分配给manager属性:

    private let manager = RestaurantDataManager()
    var selectedRestaurant: RestaurantItem?
    
  2. private扩展内部添加以下方法以填充manageritems数组,并设置selectedCityselectedCuisine属性的背景视图;如果它们已设置,将selectedCity赋值给city,将selectedCuisine赋值给cuisine。否则,退出该方法。

    manager.fetch(location: city, selectedCuisine: cuisine)
    

    调用RestaurantDataManager实例的fetch(location:selectedCuisine:completion:)方法,该方法将适当的RestaurantItem实例加载到其restaurantItems数组中。

    { restaurantItems in
       if !restaurantItems.isEmpty {
          collectionView.backgroundView = nil
       } else {
          let view = NoDataView(frame: CGRect(x: 0, y: 0, 
          width: collectionView.frame.width, height: 
          collectionView.frame.height))
          view.set(title: "Restaurants", desc: 
          "No restaurants found.") 
          collectionView.backgroundView = view
       }
    

    如果RestaurantDataManager实例的restaurantItems数组不为空,则将collectionView实例的backgroundView设置为nil。否则,创建NoDataView实例,设置其标题和描述,并将其设置为collectionView实例的backgroundView属性。

    collectionView.reloadData()
    

    告诉collectionView刷新其视图。

  3. 按如下方式更新collectionView(_:cellForItemAt:)以设置RestaurantCell实例的属性:

    func collectionView(_ collectionView: 
    UICollectionView, cellForItemAt indexPath: IndexPath) 
    -> UICollectionViewCell {
       RestaurantItem instance from the restaurantItems array of the RestaurantDataManager instance corresponding to the RestaurantCell instance's position.
    
    

    cell.titleLabel.text = restaurantItem.name

    
    This sets the text of the `RestaurantCell` instance's `titleLabel` to the value of the `RestaurantItem` instance's `name`.
    
    

    if let cuisine = restaurantItem.subtitle {

    cell.cuisineLabel.text = cuisine

    }

    
    This sets the text of the `RestaurantCell` instance's `cuisineLabel` to the value of the `RestaurantItem` instance's `subtitle`.
    
    

    if let imageURL = restaurantItem.imageURL {

    if let url = URL(string: imageURL) {

    let data = try? Data(contentsOf: url)

    if let imageData = data {

    DispatchQueue.main.async {

    cell.restaurantImageView.image =

    UIImage(data: imageData)

    }

    }

    }

    }

    
    This downloads the picture of the restaurant from the URL specified in the `RestaurantItem` instance's `imageURL` and assigns it to the `image` property of the `RestaurantCell` instance's `imgRestaurant` property.
    
    

    返回 cell

    }

    
    Returns the collection view cell.Important InformationYou will notice that scrolling is jerky, and if there is no internet connection, the **Restaurant List** screen will be blank. This is because downloading images from the internet takes time, and is interrupting the rendering of the collection view. These issues will be fixed in *Chapter 24**, Swift Concurrency*.
    
  4. 按如下更新collectionView(_:numberOfItemsInSection:)以从manager获取要显示的集合视图数量:

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
       manager.numberOfRestaurantItems()
    }
    
  5. 按如下更新viewDidAppear()以在集合视图出现在屏幕上时调用createData()

    override func viewDidAppear(animated: Bool) {
       super.viewDidAppear(animated)
       createData()
    }
    

构建并运行你的应用。设置一个地点并点击一个菜系。如果有符合所选标准的餐厅,你将在NoDataView实例中看到它们被显示。

![图 17.26:iOS 模拟器显示餐厅列表屏幕img/Figure_17.26_B17469.jpg

图 17.26:iOS 模拟器显示餐厅列表屏幕

在你完成RestaurantListViewController类之前,还有一件事。如果所选城市在餐厅列表屏幕上显示,那就很好了。让我们添加代码在餐厅列表屏幕的导航栏顶部显示它,使用大标题。按照以下步骤操作:

  1. RestaurantListViewController文件中,在createData()之后在private扩展中添加以下方法,以在导航栏中显示所选城市:

    func setupTitle() {
       navigationController?.setNavigationBarHidden(
       false, animated: false)
       title = selectedCity?.cityAndState.uppercased()
       navigationController?.navigationBar.
       prefersLargeTitles = true
    }
    

    每个UIViewController实例都有一个title属性,如果导航栏可见,title也会可见。此方法显示导航栏并将RestaurantListViewController实例的title设置为包含城市和州名称的全大写字符串。

  2. viewDidLoad()方法中在createData()之后调用setupTitle()

    override func viewDidLoad(_ animated: Bool){
       super.viewDidLoad(animated)
       createData()
       setupTitle() method when the Restaurant List screen appears.
    

构建并运行你的应用。选择一个地点和菜系。你应该在餐厅列表屏幕的顶部看到城市和州的全大写字母:

![图 17.27:iOS 模拟器显示带有标题的餐厅列表屏幕img/Figure_17.27_B17469.jpg

图 17.27:iOS 模拟器显示带有标题的餐厅列表屏幕

你已经完成了餐厅列表屏幕的实现,你终于到达了本章的结尾。做得好!

摘要

你在本章中取得了许多成就。你首先学习了 JSON 格式,并创建了RestaurantDataManager类,这是一个可以从 JSON 文件加载数据的数据管理类。你配置了MapViewController类,使其从RestaurantDataManager实例获取数据,以在LocationViewController类中显示餐厅列表,并在用户选择位置后将其传递给ExploreViewController实例。当选择一种菜系时,ExploreViewController类将选择的位置和菜系传递给RestaurantListViewController实例。最后,你配置了RestaurantListViewController类,使其从RestaurantDataManager实例获取餐厅列表,并在NoDataView类和视图中显示它们,如果特定位置没有提供所选菜系的餐厅,则会显示该视图。

现在,你能够从 JSON 文件中加载数据并读取数据,并在你的应用中传递这些数据,以便在集合视图和地图视图中显示。你还学习了如何使用UITableViewController代理方法来处理与表格视图的用户交互。这在你创建自己的应用时将非常有用。

在下一章中,你将实现餐厅详情屏幕,该屏幕通过包含静态单元格的表格视图显示特定餐厅的详细信息。

第十八章:第十八章 在静态表格视图中显示数据

你已经走得很远了,你的应用在所有屏幕上都有数据,除了餐厅详情屏幕。

在本章中,你将配置 RestaurantDetailViewController 类,以便在 viewDidLoad() 中管理视图,当从 RestaurantListViewControllerMapViewController 实例将 RestaurantItem 实例传递到 RestaurantDetailViewController 实例时,将在餐厅详情屏幕上显示该 RestaurantItem 实例的数据。

到本章结束时,你将学会如何使静态单元格的表格视图显示数据,以及如何创建自定义地图图像。通过这样做,你将能够在自己的应用中实现这些功能。

本章将涵盖以下主题:

  • RestaurantDetailViewController 类设置出口

  • 在静态表格视图中显示数据

  • 将数据传递到 RestaurantDetailViewController 实例

技术要求

你将继续在上一章中修改的 LetsEat 项目上工作。

本章完成的 Xcode 项目位于本书代码包的 Chapter18 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,了解代码的实际效果:

bit.ly/3l2h6xq

让我们从在 RestaurantDetailViewController 类中创建出口开始,以便它能够管理餐厅详情屏幕中的视图。

为 RestaurantDetailViewController 类设置出口

你的应用在所有屏幕上都有数据,除了餐厅详情屏幕。这个屏幕可以通过在餐厅列表屏幕中点击餐厅或通过在地图屏幕中点击餐厅注释视图的呼出气泡按钮来访问。如果你构建并运行你的应用,点击餐厅列表屏幕中的餐厅会显示占位符餐厅详情屏幕:

![Figure 18.1: iOS 模拟器显示占位符餐厅详情屏幕img/Figure_18.01_B17469.jpg

Figure 18.1: iOS 模拟器显示占位符餐厅详情屏幕

在地图屏幕中餐厅注释视图的呼出气泡中的按钮上点击,会显示实际的餐厅详情屏幕,但它不包含任何餐厅数据:

![Figure 18.2: iOS 模拟器显示餐厅详情屏幕img/Figure_18.02_B17469.jpg

Figure 18.2: iOS 模拟器显示餐厅详情屏幕

为了解决这个问题,让我们为 RestaurantDetailViewController 类设置出口。在项目导航器中点击 RestaurantDetailViewController 文件。在类声明之后和 selectedRestaurant 属性声明之前添加以下出口:

// Nav Bar
@IBOutlet var heartButton: UIBarButtonItem!
// Cell One
@IBOutlet var nameLabel: UILabel!
@IBOutlet var cuisineLabel: UILabel!
@IBOutlet var headerAddressLabel: UILabel!
// Cell Two
@IBOutlet var tableDetailsLabel: UILabel!
// Cell Three
@IBOutlet var overallRatingLabel: UILabel!
// Cell Eight
@IBOutlet var addressLabel: UILabel!
// Cell Nine
@IBOutlet var locationMapImageView: UIImageView!

你刚刚设置的出口如下:

  • heartButton 是导航栏中心形按钮的输出。你在这本书中不会使用它,但这是你可以稍后自己工作的内容。

  • nameLabel 是显示餐厅第一单元格中名称的标签的输出。

  • cuisineLabel 是显示餐厅第一单元格中提供的菜系的标签的输出。

  • headerAddressLabel 是显示餐厅第一单元格中地址的标签的输出。

  • tableDetailsLabel 是显示餐厅第二单元格中餐桌详情的标签的输出。

  • overallRatingLabel 是显示餐厅第三单元格中整体评分的标签的输出。你将在 第二十一章 理解核心数据 中计算并设置此值。

  • addressLabel 是显示餐厅第八单元格中地址的标签的输出。

  • locationMapImageView 是显示餐厅第九单元格中位置地图的视图的输出。你将在本章后面编写生成此地图的方法。

现在你已经创建了输出,你将它们连接到 RestaurantDetail 故事板文件中的 UI 元素。按照以下步骤操作:

  1. 在项目导航器中展开 RestaurantDetail 文件夹。点击 RestaurantDetail 故事板文件。然后,点击 RestaurantDetailViewController 类:图 18.3:餐厅详情视图控制器身份检查器设置

    图 18.3:餐厅详情视图控制器身份检查器设置

    注意,一旦设置类,视图控制器的名称将更改为 餐厅详情视图控制器。与 位置视图控制器场景 中的表格视图不同,餐厅详情视图控制器场景 中的表格视图具有静态单元格,这意味着单元格的数量不是根据模型对象的数据动态生成的。如文档大纲所示,有九个单元格,并且每个单元格都已经配置了适当的视图对象。在文档大纲中单击每个表格视图单元格将显示该单元格在编辑区域。

  2. 点击连接检查器按钮。你将看到在 RestaurantDetailViewController 类中之前添加的所有输出:图 18.4:连接检查器显示 RestaurantDetailViewController 类的输出

    图 18.4:连接检查器显示 RestaurantDetailViewController 类的输出

  3. heartButton 输出拖动到导航栏中的心形图标:图 18.5:连接检查器显示 heartButton 输出

    图 18.5:连接检查器显示 heartButton 输出

  4. heartButton 输出口现在已连接。注意,在文档大纲中,视图的描述将更改为 Heart Button:![图 18.6:文档大纲显示 Heart Button 视图

    ![img/Figure_18.06_B17469.jpg]

    图 18.6:文档大纲显示 Heart Button 视图

  5. 点击最后一个 locationMapImageView 输出口,连接到最后单元格中的 Image View。注意,在文档大纲中,名称将从 Image View 更改为 Location Map Image View:![图 18.7:连接检查器显示 locationMapImageView 输出口

    ![img/Figure_18.07_B17469.jpg]

    图 18.7:连接检查器显示 locationMapImageView 输出口

  6. 点击并拖动 addressLabel 输出口到第八单元格中的 Label 以连接它们。注意,在文档大纲中,名称将从 Label 更改为 Address Label:![图 18.8:连接检查器显示 addressLabel 输出口

    ![img/Figure_18.08_B17469.jpg]

    图 18.8:连接检查器显示 addressLabel 输出口

  7. 点击第一个 cuisineLabel 输出口,连接到第一个单元格中的第二个 Label。注意,在文档大纲中,名称将从 Label 更改为 Cuisine Label:![图 18.9:连接检查器显示 cuisineLabel 输出口

    ![img/Figure_18.09_B17469.jpg]

    图 18.9:连接检查器显示 cuisineLabel 输出口

  8. headerAddressLabel 输出口拖动到第一个单元格中的第三个 Label 以连接它们。注意,在文档大纲中,名称将从 Label 更改为 Header Address Label:![图 18.10:连接检查器显示 headerAddressLabel 输出口

    ![img/Figure_18.10_B17469.jpg]

    图 18.10:连接检查器显示 headerAddressLabel 输出口

  9. nameLabel 输出口拖动到第一个单元格中的第一个 Label 以连接它们。注意,在文档大纲中,名称将从 Label 更改为 Name Label:![图 18.11:连接检查器显示 nameLabel 输出口

    ![img/Figure_18.11_B17469.jpg]

    图 18.11:连接检查器显示 nameLabel 输出口

  10. 点击第三个 overallRatingLabel 输出口,连接到内部带有大黑 0.0Label。注意,在文档大纲中,名称将从 Label 更改为 Overall Rating Label:![图 18.12:连接检查器显示 overallRatingLabel 输出口

    ![img/Figure_18.12_B17469.jpg]

    图 18.12:连接检查器显示 overallRatingLabel 输出口

  11. 点击第二个 tableDetailsLabel 输出口,连接到第二个单元格中三个红色按钮上方的 Label。注意,在文档大纲中,名称将从 Label 更改为 Table Details Label

![图 18.13:连接检查器显示 tableDetailsLabel 输出口

![img/Figure_18.13_B17469.jpg]

图 18.13:连接检查器显示 tableDetailsLabel 输出口

现在,RestaurantDetailViewController类的所有输出都已设置。在下一节中,你将修改RestaurantDetailViewController类以从RestaurantListViewControllerMapViewController实例接收餐厅数据,并在餐厅详情屏幕中显示它。

在静态表格视图中显示数据

你已经成功将RestaurantDetailViewController类中的所有输出连接到用户界面元素,以填充输出。相反,你将编写自定义方法来完成此操作。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantDetailViewController文件。

  2. 在现有的import语句之后添加导入MapKit框架的代码:

    import MapKit
    

    这是因为你将使用MapKit框架的属性和方法来生成一个地图图像,用于最后一个单元格中的图像视图。

  3. 添加一个包含设置标签的setupLabels()方法的私有扩展,代码相当简单;它从RestaurantItem实例获取值并将它们放入RestaurantDetailViewController实例的输出中,除了tableDetailsLabel,它只是分配了一个字符串。

  4. 在最后一个单元格中,你将显示一个地图图像。为此,你需要从地图区域生成图像,并将locationMapImageLabel输出设置为显示该图像。此图像还将显示你在setupLabels()和最后一个花括号之前使用的相同自定义注释图像:

    func createMap() {
       guard let annotation = selectedRestaurant, let long
       = annotation.long, let lat = annotation.lat else { 
          return 
       }
       let location = CLLocationCoordinate2D(latitude: 
       lat, longitude: long)
       takeSnapshot(with: location)
    }
    

    这个方法使用selectedRestaurant属性的latlong属性创建一个CLLocationCoordinate2D实例,并将其分配给location。然后,它调用takeSnapshot(with:)方法,将location作为参数传递。

  5. 你将看到一个错误,因为takeSnapShot(with:)尚未实现,所以请在createMap()函数之后添加以下代码以实现它:

    func takeSnapshot(with location:
    CLLocationCoordinate2D) {
       let mapSnapshotOptions = MKMapSnapshotter.Options()
       var loc = location
       let polyline = MKPolyline(coordinates: &loc, count:
       1 )
       let region = MKCoordinateRegion(polyline.
       boundingMapRect)
       mapSnapshotOptions.region = region 
       mapSnapshotOptions.scale = UIScreen.main.scale 
       mapSnapshotOptions.size = CGSize(width: 340, 
       height: 208) 
       mapSnapshotOptions.showsBuildings = true 
       mapSnapshotOptions.pointOfInterestFilter = 
       .includingAll
       let snapShotter = MKMapSnapshotter(options: 
       mapSnapshotOptions)
       snapShotter.start() { snapshot, error in 
          guard let snapshot = snapshot else {
             return 
          }
          UIGraphicsBeginImageContextWithOptions(
          mapSnapshotOptions.size, true, 0)
          snapshot.image.draw(at: .zero)
          let identifier = "custompin"
          let annotation = MKPointAnnotation()
          annotation.coordinate = location
          let pinView = MKPinAnnotationView(annotation: 
          annotation, reuseIdentifier: identifier) 
          pinView.image = UIImage(named: "custom-
          annotation")! 
          let pinImage = pinView.image
          var point = snapshot.point(for: location)
          let rect = self.locationMapImageView.bounds 
          if rect.contains(point) { 
             let pinCenterOffset = pinView.centerOffset
             point.x -= pinView.bounds.size.width / 2
             point.y -= pinView.bounds.size.height / 2
             point.x += pinCenterOffset.x
             point.y += pinCenterOffset.y
             pinImage?.draw(at: point)
          }
          if let image = 
          UIGraphicsGetImageFromCurrentImageContext() {
             UIGraphicsEndImageContext() 
             DispatchQueue.main.async {
                self.locationMapImageView.image = image
             }
          }
       }
    }
    

    这个方法的完整描述超出了本书的范围,但这里是对其功能的简单解释。给定一个位置,它在该位置拍摄地图的快照,并将你之前在RestaurantDetailViewController实例的locationMapImageView输出中使用的自定义注释添加到地图上。

  6. 你已经在setupLabels()方法定义之前的private扩展中编写了所有必需的方法,以在RestaurantDetailViewController类中显示所需的RestaurantItem实例详细信息,添加一个调用setupLabels()createMap()方法的initialize()方法:

    func initialize() { 
       setupLabels() 
       createMap()
    }
    
  7. 修改viewDidLoad()方法,在RestaurantDetailViewController实例加载其视图时调用initialize()方法:

    override func viewDidLoad() { 
       super.viewDidLoad() 
       initialize()
    }
    

回想一下,在 第十六章使用 MapKit 入门,你已经配置了 MapViewController 类,以便将 RestaurantItem 实例传递到 RestaurantDetailViewController 实例。构建并运行你的应用,转到 地图 屏幕。点击其中一个餐厅以显示呼出气泡。点击呼出气泡中的按钮,你应该在 餐厅详情 屏幕中看到餐厅详情:

图 18.14:iOS 模拟器显示餐厅详情屏幕

图 18.14:iOS 模拟器显示餐厅详情屏幕

如果你向下滚动,你将看到最后一个单元格中的地图图像:

图 18.15:iOS 模拟器显示餐厅详情屏幕中的地图

图 18.15:iOS 模拟器显示餐厅详情屏幕中的地图

你已经完成了对 RestaurantDetailViewController 类的修改,但你仍然需要从 RestaurantListViewController 实例将选中的 RestaurantItem 实例传递到 RestaurantDetailViewController 实例。你将在下一节中这样做。

将数据传递给 RestaurantDetailViewController 实例

你已经添加并连接了 RestaurantDetailViewController 类的输出端口。你还在这个类中添加了代码,从 RestaurantItem 实例获取餐厅数据,并使用它来填充其输出端口。你需要做的最后一件事是将选中的 RestaurantItem 实例从 RestaurantListViewController 实例传递到 RestaurantDetailViewController 实例。按照以下步骤操作:

  1. 在项目导航器中点击 RestaurantListViewController 文件。

  2. viewDidLoad() 之后添加以下代码:如果过渡标识符是 showDetail,则调用 showRestaurantDetail(segue:) 方法:

    override func prepare(for segue: UIStoryboardSegue, 
    sender: Any?) {
       if let identifier = segue.identifier {
          switch identifier {
          case Segue.showDetail.rawValue:
             showRestaurantDetail(segue: segue)
          default:
             print("Segue not added")
          }
       }
    }
    

    回想一下,你在 RestaurantListViewController 实例过渡到另一个视图控制器时添加了一个过渡,过渡标识符被检查。如果过渡标识符是 showDetail,则执行 showRestaurantDetail 方法。只有 showDetail 标识符之间的过渡,所以目标视图控制器必须是 RestaurantDetailViewController 实例。

  3. 你会看到一个错误,因为 showRestaurantDetail(segue:) 方法尚未实现。此方法将从 RestaurantListViewController 实例传递 RestaurantItem 实例到 RestaurantDetailViewController 实例。在 RestaurantListViewController 类的 private 扩展的开头大括号之后添加它:

    func showRestaurantDetail(segue: UIStoryboardSegue) {
       if let viewController = segue.destination as? 
       RestaurantDetailViewController, let indexPath = 
       collectionView.indexPathsForSelectedItems?.first {
          selectedRestaurant = manager.restaurantItem(at:
          indexPath.row)
          viewController.selectedRestaurant = 
          selectedRestaurant
       }
    }
    

    这种方法首先检查过渡目标是否是 RestaurantDetailViewController 的实例,并获取被点击的集合视图单元格的索引。然后,manager 返回存储在该索引处的 RestaurantItem 实例,并将其分配给 selectedRestaurant。然后,将 RestaurantDetailViewController 实例的 selectedRestaurant 属性设置为这个实例。

现在我们来看看Main故事板文件。它目前连接到一个占位符Main故事板文件,以删除占位符并连接到RestaurantDetail故事板文件。按照以下步骤操作:

  1. 点击Main故事板文件,在文档大纲中找到restaurantCell。然后,Ctrl + DragrestaurantCellRestaurantDetail故事板引用(你在*第十六章**,使用 MapKit 入门)中,如图所示:![图 18.16:编辑区域显示 RestaurantDetail 故事板引用

    ![img/Figure_18.16_B17469.jpg]

    图 18.16:编辑区域显示 RestaurantDetail 故事板引用

  2. 从出现的弹出菜单中选择显示:![图 18.17:带有“显示选中”的切换弹出菜单

    ![img/Figure_18.17_B17469.jpg]

    图 18.17:带有“显示选中”的切换弹出菜单

  3. 通过选择它们并在键盘上按Delete键来从故事板中删除占位符场景,因为它们不再需要:![图 18.18:编辑区域显示要删除的占位符场景

    ![img/Figure_18.18_B17469.jpg]

    图 18.18:编辑区域显示要删除的占位符场景

  4. 你将设置切换标识符为showDetail。如前所述,这将设置RestaurantDetailViewController实例的selectedRestaurant属性。选择你刚刚添加的切换:![图 18.19:餐厅列表视图控制器场景和 RestaurantDetail 故事板引用之间的切换

    ![img/Figure_18.19_B17469.jpg]

    图 18.19:餐厅列表视图控制器场景和 RestaurantDetail 故事板引用之间的切换

  5. 点击属性检查器按钮。在showDetail下:

![图 18.20:为 showDetail 切换设置的属性检查器

![img/Figure_18.20_B17469.jpg]

图 18.20:为 showDetail 切换设置的属性检查器

构建并运行你的项目。选择一个城市和一种菜系。点击餐厅列表屏幕中的其中一家餐厅。该餐厅的详细信息将出现在餐厅详情屏幕中:

![图 18.21:iOS 模拟器显示餐厅详情屏幕

![img/Figure_18.21_B17469.jpg]

图 18.21:iOS 模拟器显示餐厅详情屏幕

餐厅详情屏幕的实现现在已经完成。当你从地图餐厅列表屏幕中选择一家餐厅时,该餐厅的详细信息将在餐厅详情屏幕中显示。太棒了!

摘要

在本章中,你将RestaurantDetailViewController类中的出口连接到viewDidLoad(),以便在从RestaurantListViewControllerMapViewController实例到RestaurantDetailViewController实例的RestaurantItem实例时填充表格视图,使其能够在餐厅详情屏幕上显示该RestaurantItem实例的数据。

通过这样做,您已经学会了如何制作静态单元格的表格视图来显示数据,以及如何创建自定义地图图像,现在您可以在自己的应用中实现这些功能。

恭喜!您应用中的所有屏幕现在都显示数据了。然而,如果您查看餐厅详情屏幕,会发现没有餐厅的评分、评论或照片,也没有添加它们的方法。您将从下一章开始实现这一功能,在那里您将创建一个自定义控件,允许您为餐厅详情评论表单屏幕添加餐厅的星级评分。

第十九章:第十九章:开始使用自定义 UIControls

到目前为止,您的应用程序在其所有屏幕中都有数据,但 餐厅详情 屏幕是不完整的。您无法为餐厅设置星级评分,也无法添加照片或评论。

到目前为止,您一直在使用苹果的标准 UI 元素。在本章中,您将创建一个自定义的 UIControl 类的子类,用于以星级形式显示餐厅评分。您将修改这个子类,以便用户可以通过点击来为餐厅设置评分。之后,您将实现一个允许用户提交餐厅评论的评论表单。

到本章结束时,您将学会如何创建自定义的 UIControl 类,处理触摸事件,并为您的应用程序实现评论表单。

本章将涵盖以下主题:

  • 创建自定义 UIControl 子类

  • 在您的自定义 UIControl 子类中显示星级

  • 添加触摸事件支持

  • 实现取消按钮的撤销方法

  • 创建 ReviewFormViewController

技术要求

您将继续在上一章中修改的 LetsEat 项目上工作。

本章完成的 Xcode 项目位于本书代码包的 Chapter19 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,了解代码的实际应用:

bit.ly/3cRFcXa

让我们从学习如何创建一个将在屏幕上显示星级评分的自定义 UIControl 子类开始。

创建自定义 UIControl 子类

到目前为止,您只使用了苹果预定义的 UI 元素,例如标签和按钮。您只需点击库按钮,搜索您想要的元素,并将其拖入故事板即可。然而,在某些情况下,苹果提供的对象可能不合适或不存在。在这种情况下,您将需要自己构建。让我们回顾一下在应用程序浏览中看到的 餐厅详情 屏幕:

Figure 19.1: Restaurant Detail screen showing the star rating

图 19.1:显示星级评级的餐厅详情屏幕

您可以看到一组五个星级位于 RestaurantDetail 故事板文件和 ReviewForm 故事板文件上方,其中应该放置星级的空白视图对象。您将创建一个名为 RatingsView 的类,这是一个 UIControl 类的自定义子类,您将在两个场景中使用它。UIControl 类是 UIView 类的子类,它被用作 RatingsView 类的超类,因为 RatingsView 实例必须在用户点击时做出响应。

重要信息

您可以在 developer.apple.com/documentation/uikit/uicontrol 上了解更多关于 UIControl 的信息。

RatingsView 实例将显示评分,用户还可以选择半星星。让我们首先创建 UIControl 类的子类。按照以下步骤操作:

  1. 右键点击 Review Form 文件夹并选择 New File

  2. iOS 应该已经选中。选择 Cocoa Touch Class 然后点击 Next

  3. 按照以下配置文件:

    RatingsView

    UIControl

    Swift

    点击 Next

  4. 点击 RatingsView 文件将出现在项目导航器中。

现在您需要设置 RatingsView 旁边视图对象的身份。按照以下步骤操作:

  1. 在项目导航器中展开 RestaurantDetail 文件夹。点击 RestaurantDetail 故事板文件,并选择与 0.0 标签 并排的 View 对象,如图所示:图 19.2:编辑区域显示与 0.0 标签并排的视图对象

    图 19.2:编辑区域显示与 0.0 标签并排的视图对象

  2. 点击“身份检查器”按钮。在 RatingsView 下:

图 19.3:将类设置为 RatingsView 的身份检查器

图 19.3:将类设置为 RatingsView 的身份检查器

现在让我们修改 RatingsView 类以使其显示星星。你将在下一节中使用 Assets.xcassets 文件内的图形资源来完成此操作。

在自定义 UIControl 子类中显示星星

到目前为止,你已经在项目中创建了一个名为 RatingsView 的新 UIControl 子类。你还将 Restaurant Detail 屏幕旁边的视图对象的类分配给了 RatingsView 类。在本章的剩余部分,RatingsView 类的一个实例将被称为评分视图(与 UIButton 类的实例被称为按钮的方式相同)。在本节中,你将向 RatingsView 类添加一些代码,以便评分视图显示星星。按照以下步骤操作:

  1. 在项目导航器中点击 RatingsView 文件并删除所有注释代码。

  2. RatingsView 类声明之后输入以下内容以声明类的属性:

    private let filledStarImage = UIImage(named: 
    "filled-star")
    private let halfStarImage = UIImage(named: 
    "half-star")
    private let emptyStarImage = UIImage(named: 
    "empty-star")
    private var totalStars = 5
    var rating = 0.0
    

    前三个属性 filledStarImagehalfStarImageemptyStarImage 被分配了存储在 Assets.xcassets 文件中的星星图像。

    totalStars 属性决定了要绘制的星星总数。

    rating 属性用于存储餐厅评分。绘制的星星类型将由 rating 的值决定。例如,如果 rating3.5,评分视图将显示三个实心星星,一个半实心星星和一个空星星。

接下来,让我们创建一个将在屏幕上绘制评分视图的方法。所有 UIView 子类都有一个 draw(_:) 方法,它负责在屏幕上绘制它们的视图。你将覆盖 RatingsView 类的父类实现此方法。按照以下步骤操作:

  1. 在属性声明之后在类声明中添加以下代码:

    override func draw(_ rect: CGRect) {
       let context = UIGraphicsGetCurrentContext()
       context!.setFillColor(UIColor.systemBackground.
       cgColor)
       context!.fill(rect)
       let ratingsViewWidth = rect.size.width
       let availableWidthForStar = ratingsViewWidth /
       Double(totalStars)
       let starSidelength = (availableWidthForStar <= 
       rect.size.height) ? availableWidthForStar : 
       rect.size.height
       for index in 0..<totalStars {
          let starOriginX = (availableWidthForStar * 
          Double(index)) + ((availableWidthForStar - 
          starSidelength) / 2)
          let starOriginY = ((rect.size.height - 
          starSidelength) / 2)
          let frame = CGRect(x: starOriginX, 
          y: starOriginY, width: starSidelength,
          height: starSidelength)
          var starToDraw: UIImage!
          if (Double(index + 1) <= self.rating) {
             starToDraw = filledStarImage
          } else if (Double(index + 1) <= 
          self.rating.rounded()) {
             starToDraw = halfStarImage
          } else {
             starToDraw = emptyStarImage
          }
          starToDraw.draw(in: frame)
       }
    }
    

    让我们分解一下:

    let context = UIGraphicsGetCurrentContext()
    

    创建一个UIGraphicsGetCurrentContext的实例并将其分配给context。你可以将其视为一个画板,你将在上面组合 UI 元素。

    context!.setFillColor(UIColor.systemBackground.
    cgColor)
    

    context的填充颜色设置为默认系统背景颜色。

    context!.fill(rect)
    

    使用填充颜色填充由rect指定的矩形区域。

    let ratingsViewWidth = rect.size.width
    let availableWidthForStar = ratingsViewWidth / 
    Double(totalStars)
    let starSidelength = (availableWidthForStar <= 
    rect.size.height) ? availableWidthForStar : 
    rect.size.height
    

    这些语句确定每颗星星的大小。第一条语句获取评分视图的宽度并将其分配给ratingsViewWidth。下一条语句通过将评分视图的宽度除以需要绘制的星星数量来获取每个星星可用的宽度。这个值被分配给availableWidthForStar。对于第三条语句,想象每颗星星都被一个矩形包围。这条语句计算这个矩形的每一边应该有多长才能适应评分视图。如果availableWidthForStar小于或等于评分视图的高度,starSideLength被设置为availableWidthForStar;否则,它被设置为与评分视图的高度相同。

    重要信息

    第三条语句使用了三元运算符。更多信息可以在以下链接中找到:docs.swift.org/swift-book/LanguageGuide/BasicOperators.html

例如,假设评分视图的宽度为 200 点,高度为 50 点。availableWidthForStar将是 200/5 = 40。由于 40 <= 50 评估为truestarSideLength将被设置为40

for index in 0..<totalStars {

由于totalStars设置为5,这个for循环将重复五次。

let starOriginX = (availableWidthForStar * 
Double(index)) +  ((availableWidthForStar – 
starSidelength) / 2
let starOriginY = ((rect.size.height - starSidelength)
/ 2)
let frame = CGRect(x: starOriginX, y: starOriginY, 
width: starSidelength, height: starSidelength)

这些语句计算了在评分视图中绘制每个星星的矩形的位置和大小。然后将其分配给frame。原点值是从评分视图的左上角偏移的,宽度和高度设置为starSidelength

例如,对于第一颗星星,starOriginX是(40*0.0) + (40-40)/2 = 0starOriginY是(50 – 40)/2 = 5。因此,frame将是一个CGRect,其中x0y5width40height40

var starToDraw: UIImage!
if (Double(index + 1) <= self.rating) {
   starToDraw = filledStarImage
} else if (Double(index + 1) <= self.rating.rounded())
{ 
   starToDraw = halfStarImage
} else {
   starToDraw = emptyStarImage
}

根据评分视图的rating属性的值,这些语句确定要绘制的星星是填充的、半填充的还是空的。

例如,假设rating3.5

第一颗星星的索引为0。这意味着Double(0 + 1) <= 3.5 将等于 1.0 <= 3.5,这评估为true。这意味着绘制的第一颗星星将是一个填充的星星。第二颗和第三颗星星也是如此。

第四颗星星的索引为3。这意味着Double(3 + 1) <= 3.5 将等于 4.0 <= 3.5,这评估为falseelse子句评估Double(3 + 1) <= 4.0,这评估为true,所以绘制的第四颗星星将是一个半填充的星星。

第五颗星星的索引为4。这意味着Double(4 + 1) <= 3.5 将等于 5.0 <= 3.5,这评估为falseelse子句评估Double(4 + 1) <= 4.0,这也评估为false,所以绘制的第五颗星星将是一个空的星星。

starToDraw.draw(in: frame)

这个语句在指定的frame中绘制星星。

这就是RatingsView类所需的全部代码。现在,让我们向RestaurantDetailViewController类添加一个出口,以便它可以管理评分视图显示的内容。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantDetailViewController文件。

  2. overallRatingLabel出口之后输入以下代码:

    @IBOutlet var ratingsView: RatingsView!
    

    这在RestaurantDetailViewController类中创建了一个用于评分视图的出口。你现在有一个名为ratingsViewRatingsView类型的出口,稍后你将在故事板中将它连接到评分视图。

  3. ratingsView实例的rating属性分配3.5的方法。在initialize()方法之后,在你的private扩展中输入以下内容:

    func createRating() {
       ratingsView.rating = 3.5
    }
    
  4. 修改initalize()方法以调用createRating()方法:

    func initialize() { 
       setupLabels() 
       createMap() 
       createRating()
    }
    
  5. 打开RestaurantDetail故事板文件,选择ratingsView出口到评分视图:

图 19.4:连接检查器显示 ratingsView 出口

图 19.4:连接检查器显示 ratingsView 出口

构建并运行你的项目,并转到任何餐厅的餐厅详情屏幕。评分视图应该显示三颗半星:

图 19.5:iOS 模拟器显示显示 3.5 颗星的评分视图

图 19.5:iOS 模拟器显示显示 3.5 颗星的评分视图

你已经为餐厅详情屏幕创建并实现了评分视图。它看起来很棒,但到目前为止,评分视图在被点击时没有响应。你将在下一节中使其对触摸事件做出响应,以便用户可以选择评分。

添加对触摸事件的支持

目前,RestaurantDetailViewController类有一个出口ratingsView连接到餐厅详情屏幕中的评分视图。它显示三颗半星的评分,但你无法更改评分。你需要支持触摸事件,使评分视图能够对点击做出响应。

重要信息

你可以在developer.apple.com/documentation/uikit/touches_presses_and_gestures/handling_touches_in_your_view了解更多关于处理触摸事件的信息。

要支持触摸事件,你需要修改RatingsView类以跟踪用户在屏幕上的触摸,并使用它们来确定评分。按照以下步骤操作:

  1. 在项目导航器中点击RatingsView文件,并在draw(_:)方法之后添加以下属性:

    override var canBecomeFirstResponder: Bool {
       true
    }
    

    canBecomeFirstResponder 是一个 UIControl 属性,它确定一个对象是否可以成为第一个响应者。评分视图需要成为第一个响应者以响应用户的触摸事件。此方法默认返回 false,因为并非所有用户界面元素都需要响应用户的触摸。您重写此方法,使其返回 true,以便评分视图可以成为第一个响应者。

  2. 要跟踪用户在屏幕上的触摸,请在您刚刚添加的 canBecomeFirstResponder 属性之后添加以下代码:

    override func beginTracking(_ touch: UITouch, with 
    event: UIEvent?) -> Bool {
       guard self.isEnabled else { 
          return false
       } 
       super.beginTracking(touch, with: event)
       handle(with: touch)
       return true
    }
    

    让我们分解一下:

    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    

    这是 UIControl 类中声明的方法之一。当用户的触摸在 UIControl 实例的范围内时被调用。屏幕上触摸的位置、大小、移动和力量都存储在一个 UITouch 实例中。如果您想跟踪用户的触摸,则需要此方法返回 true。您重写此方法,以便在用户触摸评分视图时定义自定义行为。

    guard self.isEnabled else { 
       return false
    }
    

    在这个 guard 语句中检查 isEnabled 属性,以查看评分视图是否启用。如果评分视图未启用,则不会跟踪用户的触摸。

    super.beginTracking(touch, with: event)
    

    调用此方法的超类实现。这将处理父类所需的任何初始化。

    handle(with: touch) 
    

    您将传递 UITouch 实例到这个方法,它将为每个触摸执行。您将在下一步声明和定义此方法。

    return true
    

    当评分视图启用时跟踪用户的触摸。

  3. 您将看到一个错误,因为您还没有实现 handle(with:),所以请在文件中的所有其他代码之后为 RatingsView 创建一个 private 扩展,并在其中输入以下代码:

    private extension RatingsView {
       func handle(with touch: UITouch) {
          let starRectWidth = self.bounds.size.width / 
          Double(totalStars)
          let location = touch.location(in: self)
          var value = location.x / starRectWidth
          if (value + 0.5) < value.rounded(.up) {
             value = floor(value) + 0.5
          } else {
             value = value.rounded(.up)
          }
          updateRating(with: value)
       }
    }
    

    handle(with:) 将根据用户触摸的位置计算评分值。它接受一个 UITouch 实例作为参数。首先,starRectWidth 被分配为评分视图的 width,除以 5。接下来,将 UITouch 实例在评分视图内的位置分配给 location。然后,将 value 分配给 locationx 位置除以 starRectWidth。这意味着 value 将包含介于 0 和 5 之间的值范围。接下来,if 语句计算与触摸位置相对应的评分,并调用 updateRating(with:),将 value 传递给它。您将在下一步实现 updateRating(with:)

    为了理解 if 语句的工作原理,假设评分视图的 width200starRectWidth 将设置为 200/5 = 40。假设用户在位置 x = 130y = 17 处触摸屏幕,这对应于第三颗星和第四颗星之间的一个点。value 将被分配为 130/40 = 3.25。所以,if 语句将评估 (3.25 + 0.5 < 3.25.rounded(.up),这变成 (3.75 < 4.0),返回 true,因此 value 将被设置为 floor(3.25) + 0.5,这变成 3.0 + 0.5,即 3.5。所以,updateRating(with:) 将传递一个值为 3.5

  4. 你会看到一个错误,因为你还没有实现updateRating(with:),所以请在handle(with:)方法之后的private扩展中输入以下代码:

    func updateRating(with newValue: Double) {
       if (self.rating != newValue && newValue >= 0 && 
       newValue <= Double(totalStars)) { 
          self.rating = newValue 
       }
    }
    

    updateRating(with:)检查value是否不等于当前评分且在 0 到 5 之间。如果是,则将value分配给rating

    在前面的示例基础上,由于3.5位于 0 和 5 之间,如果它不等于当前rating的值,它将被分配给rating

  5. 评分视图在评分改变后需要重新绘制,以显示星星的正确状态。将rating属性声明修改如下:

    var rating = 0.0 {
       didSet {
          setNeedsDisplay()
       }
    }
    

    在这里,你已定义了rating属性的值。每次rating发生变化时,都会调用setNeedsDisplay()并重新绘制评分视图。由于屏幕只有在评分发生变化时才会重新绘制,因此可以带来一点性能上的好处。

你已经添加了所有必要的代码,使评分视图能够响应触摸。现在,你需要更新RestaurantDetailViewController类来设置评分视图的isEnabled属性。在项目导航器中点击RestaurantDetailViewController文件,并修改createRating()方法,如下所示:

func createRating() { 
   ratingsView.rating = 3.5 
   ratingsView.isEnabled = true
}

isEnabled属性设置为true允许评分视图成为第一响应者并开始跟踪触摸,这将触发handle(with:)根据触摸位置计算评分,进而调用updateRating(with:)来更新评分视图。

构建并运行你的项目。现在点击评分视图会根据点击的位置改变评分,如下所示:

![Figure 19.6:iOS 模拟器显示评分视图的触摸位置

![img/Figure_19.06_B17469.jpg]

图 19.6:iOS 模拟器显示评分视图的触摸位置

评分将变为一个半星。

最终,你将通过汇总用户在审查表屏幕上提交的所有评分来计算总体评分。如果你点击添加评论按钮,审查表屏幕会显示,但你无法将其关闭或设置评分。在下一节中,你将配置取消按钮,以便在点击时关闭审查表屏幕。

实现取消按钮的 unwind 方法

让我们看看审查表屏幕。添加评论按钮和审查表屏幕之间的转场已经为你准备好了。构建并运行你的项目,转到餐厅详情屏幕,并点击添加评论按钮:

![Figure 19.7:iOS 模拟器显示添加评论按钮

![img/Figure_19.07_B17469.jpg]

图 19.7:iOS 模拟器显示添加评论按钮

审查表屏幕被显示(注意顶部表格视图单元格中有一个空白区域,那里应该是一个评分视图,你将在稍后添加):

![Figure 19.8:iOS 模拟器显示审查表屏幕

![img/Figure_19.08_B17469.jpg]

图 19.8:iOS 模拟器显示的“审查表”屏幕

一旦审查表单屏幕出现在屏幕上,您就不能关闭它,因为保存取消按钮的动作尚未配置。就像您对位置屏幕所做的那样,您需要实现一个 unwind 方法来关闭审查表单屏幕。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantDetailViewController文件。

  2. private扩展中,在createRating()方法之前,按照以下方式实现 unwind 方法:

    @IBAction func unwindReviewCancel(segue:
    UIStoryboardSegue) {
    }
    

    审查表单屏幕过渡到餐厅详情屏幕时,将调用此方法。

  3. 打开ReviewForm故事板文件,并按Ctrl + Drag取消按钮拖动到场景工具栏中的退出图标,如图所示:图 19.9:显示设置取消按钮动作的表视图控制器场景

    图 19.9:显示设置取消按钮动作的表视图控制器场景

  4. 在弹出菜单中选择unwindReviewCancelWithSegue

图 19.10:弹出菜单,其中选择了 unwindReviewCancelWithSegue:

图 19.10:弹出菜单,其中选择了 unwindReviewCancelWithSegue:

构建并运行您的项目。现在,您可以通过点击取消按钮来关闭审查表单屏幕。

接下来,让我们看看保存按钮。您将为审查表单屏幕创建一个视图控制器,以便在点击保存按钮时处理审查表单屏幕字段内的数据。您将在下一节中这样做。

创建 ReviewFormViewController 类

为了处理用户输入,您将创建ReviewFormViewController类,使其成为审查表单屏幕的视图控制器。目前,您将配置此类以从审查表单屏幕的字段中获取所有值并在调试区域打印它们。您将在稍后的第二十一章**,理解核心数据中学习如何存储评论。按照以下步骤操作:

  1. 右键点击ReviewForm文件夹并选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch Class然后点击下一步

  3. 按照以下方式配置文件:

    ReviewFormViewController

    UITableViewController

    Swift

    点击下一步

  4. 点击ReviewFormViewController文件将在项目导航器中显示。

  5. 删除viewDidLoad()方法之后以及所有注释掉的代码。在类声明之后添加以下输出。它们对应于审查表单屏幕内的字段:

    @IBOutlet var ratingsView: RatingsView!
    @IBOutlet var titleTextField: UITextField!
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var reviewTextView: UITextView!
    
  6. 您还需要配置viewDidLoad()方法的动作:

    @IBAction func onSaveTapped(_ sender: Any) { 
       print(ratingsView.rating) 
       print(titleTextField.text as Any) 
       print(nameTextField.text as Any) 
       print(reviewTextView.text as Any) 
       dismiss(animated: true, completion: nil)
    }
    

    此方法将审查表单屏幕的字段内容打印到调试区域并关闭它。

现在,让我们按照以下方式连接ReviewForm故事板文件中的输出:

  1. 在项目导航器中点击ReviewForm故事板文件并点击ReviewFormViewController图 19.11:身份检查器,将类设置为 ReviewFormViewController

    图 19.11:身份检查器,类设置为 ReviewFormViewController

    注意,名称 Table View Controller Scene 将更改为 Review Form View Controller Scene

  2. 点击 RatingsView。视图的名称将更改为 Ratings View:![图 19.12:身份检查器,类设置为 RatingsView 图片

    图 19.12:身份检查器,类设置为 RatingsView

  3. 接下来,您将连接输出口。在文档大纲中点击 审查表单视图控制器 图标,然后点击连接检查器按钮:![图 19.13:连接检查器按钮 图片

    图 19.13:连接检查器按钮

  4. ratingsView 输出口连接到 Ratings View:![图 19.14:连接检查器显示 ratingsView 输出口 图片

    图 19.14:连接检查器显示 ratingsView 输出口

  5. titleTextField 输出口连接到第一个 文本字段:![图 19.15:连接检查器显示 titleTextField 输出口 图片

    图 19.15:连接检查器显示 titleTextField 输出口

  6. nameTextField 输出口连接到第二个 文本字段:![图 19.16:连接检查器显示 nameTextField 输出口 图片

    图 19.16:连接检查器显示 nameTextField 输出口

  7. reviewTextView 输出口连接到 文本视图:![图 19.17:连接检查器显示 reviewTextView 输出口 图片

    图 19.17:连接检查器显示 reviewTextView 输出口

  8. 最后,将 onSaveTapped: 动作连接到 保存 按钮:

![图 19.18:连接检查器显示设置 Save 按钮动作图片

图 19.18:连接检查器显示设置 Save 按钮动作

构建并运行您的应用。转到 审查表单 屏幕,设置一个评分,向字段添加一些示例文本,然后点击 保存 按钮:

![图 19.19:iOS 模拟器显示审查表单屏幕图片

图 19.19:iOS 模拟器显示审查表单屏幕

您将看到您输入的数据出现在调试区域:

![图 19.20:调试区域显示审查表单屏幕文本字段的内容图片

图 19.20:调试区域显示审查表单屏幕文本字段的内容

恭喜!审查表单屏幕现在能够接受用户输入。您将在 第二十一章**,理解核心数据 中学习如何保存和展示评论数据。

摘要

在本章中,您从头开始创建了一个新的自定义 UIControl 子类 RatingsView 并将其添加到 ReviewFormViewController 类中,这是一个用于 审查表单 屏幕的视图控制器,并配置了 取消保存 按钮动作,以便用户可以关闭审查表单屏幕或提交评论。

你现在已经很好地掌握了如何创建自定义的 UIControl 类,如何使它们响应用户交互,以及如何实现一个可以接受用户输入的审查表单。这在你编写自己的应用程序时将非常有用。

在下一章中,你将学习如何处理来自相机或照片库的照片,以及如何应用照片滤镜到你所拥有的照片上。

第二十章:第二十章:开始使用相机和照片库

在上一章中,你创建了 RatingsView 类并将其添加到 餐厅详情评论表单 屏幕中。你还通过 评论表单 屏幕启用了用户提交评论的功能,尽管目前提交的评论只是打印到调试区域。

在本章中,你将完成包含你想要使用的滤镜的 .plist 文件的实现,然后创建一个用于存储滤镜数据的滤镜对象类,并创建一个数据管理类来读取 .plist 文件并填充一个滤镜对象数组。接下来,你将创建一个包含应用滤镜到图像的方法的协议。之后,你将创建 UIImagePickerDelegate 协议的视图控制器,这允许你从相机或照片库中获取照片,并实现将选定的滤镜应用到照片上的方法。请注意,照片将不会被保存。你将在下一章学习如何保存评论和照片。

到本章结束时,你将学会如何将照片导入自己的应用,以及如何对它们应用滤镜。

本章将涵盖以下主题:

  • 理解滤镜

  • 照片滤镜 屏幕创建模型对象

  • 实现 ImageFiltering 协议

  • 照片滤镜 屏幕创建类

  • 实现图像选择器代理协议

  • 获取使用相机或照片库的权限

技术要求

你将继续在上一章中修改的 LetsEat 项目上工作。

本章的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter20 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,看看代码的实际效果:

bit.ly/3oZZ93P

让我们从学习照片滤镜以及如何将它们应用到图像开始吧。

理解滤镜

iOS 提供了一系列内置的滤镜,你可以使用这些滤镜来增强照片。这些滤镜通过 Core Image 库提供。Core Image 是一种图像处理和分析技术,它为静态和视频图像提供高性能处理。Core Image 中有超过 170 个滤镜可供使用,这让你能够将各种酷炫效果应用到你的照片上。

重要信息

你可以在 developer.apple.com/documentation/coreimage 上了解更多关于 Core Image 的信息。

对于这个应用,你将只使用 10 个滤镜。这些滤镜的详细信息在 .plist 文件中提供。按照以下步骤将此文件导入到你的应用中:

  1. 如果您还没有这样做,请在此链接下载并解压本书的代码包:github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition。您将在 Chapter20 文件夹中的 resources 文件夹内找到 FilterData.plist

  2. 在项目导航器中,在 PhotoFilter 文件夹内创建一个新的组,并将其命名为 Model

  3. FilterData.plist 拖动到 Model 文件夹。确保勾选 如果需要则复制项目 并点击 完成

  4. 在项目导航器中点击 FilterData.plist 以查看其内容:

图 20.1:显示 FilterData.plist 内容的编辑区域

图 20.1:显示 FilterData.plist 内容的编辑区域

如您所见,FilterData.plist 是一个字典数组。每个字典包含过滤器的名称和描述标签。在下一节中,您将了解如何使用 FilterData.plist 中的信息在您的应用中。

为照片滤镜屏幕创建模型对象

要将 FilterData.plist 中的信息获取到您的应用中,您将创建一个结构,FilterItem,它可以存储有关过滤器的详细信息,以及一个数据管理类,FilterManager,它将加载 FilterData.plist 并创建 FilterItem 实例的数组。这与将菜系和位置信息加载到您的应用中的方法类似。让我们首先创建 FilterItem 结构。按照以下步骤操作:

  1. PhotoFilter 文件夹中的 Model 文件夹上右键单击,并选择 新建文件

  2. iOS 应已选中。选择 Swift 文件 并点击 下一步

  3. 将此文件命名为 FilterItem。点击后,FilterItem 文件将出现在项目导航器中。

  4. FilterItem 文件中,在 import 语句之后输入以下代码以声明和定义 FilterItem 结构:

    struct FilterItem {
       let filter: String?
       let name: String?
       init(dict: [String: String]) {
          self.filter = dict["filter"] 
          self.name = dict["name"]
       }
    }
    

    此结构有两个属性和一个初始化器。filter 属性将存储过滤器名称,而 name 属性将存储简短的过滤器描述。初始化器接受一个字典作为参数,在创建此类的实例时设置 namefilter 属性。

现在您已经创建了 FilterItem 类,您将创建数据管理类 FilterDataManager。按照以下步骤操作:

  1. PhotoFilter 文件夹中的 Model 文件夹上右键单击,并选择 新建文件

  2. iOS 应已选中。选择 Swift 文件 并点击 下一步

  3. 将此文件命名为 FilterDataManager。点击后,FilterDataManager 文件将出现在项目导航器中。

  4. FilterDataManager 文件中,在 import 语句之后输入以下代码以声明和定义 FilterDataManager 类:

    class FilterDataManager: DataManager {
       func fetch() -> [FilterItem] {
          var filterItems: [FilterItem] = []
          for data in loadPlist(file: "FilterData") {
             filterItems.append(FilterItem(dict: 
             data as! [String: String]))
          }
          return filterItems
       }
    }
    

    FilterDataManager 类采用了你在 第十六章**,使用 MapKit 入门 中早期创建的 DataManager 协议。调用 fetch() 方法从 FilterData.plist 加载数据,创建 FilterItem 实例的数组,并返回它。

在下一节中,你将创建一个协议,其中包含一个将过滤器应用于图像的方法。

创建 ImageFiltering 协议

你需要一个将过滤器应用于图像的方法。你将创建一个协议 ImageFiltering,它实现了一个方法 apply(filter:to:) 来完成这个任务。任何采用此协议的类都将有权访问此方法,该方法将指定的过滤器应用于图像。按照以下步骤操作:

  1. 右键点击 PhotoFilter 文件夹并选择 新建文件

  2. iOS 应已选中。选择 Swift 文件 并点击 下一步

  3. 将此文件命名为 ImageFiltering。点击 ImageFiltering 文件将在项目导航器中显示。

  4. 修改此文件中的代码以声明和定义 ImageFiltering 协议:

    import UIKit framework provides the required infrastructure for your iOS app. You import UIKit instead of Foundation because support for the UIImage class is not available in Foundation.
    
    

    导入 CoreImage

    
    Core Image is an image processing and analysis technology that provides high-performance processing for still and video images. You import `CoreImage` as it is required to access the built-in photo filters.
    
    

    protocol ImageFiltering {

    func apply(filter: String, originalImage:

    UIImage) -> UIImage

    }

    
    Here, you declare a protocol named `ImageFiltering`. This protocol specifies a method, `apply(filter:originalImage:)`, that takes a filter name and an image as parameters.
    
    

    扩展 ImageFiltering {

    func apply(filter: String, originalImage:

    UIImage) -> UIImage {

    
    This extension of the `ImageFiltering` protocol contains the implementation of the `apply(filter:originalImage:)` method. This means that any class that adopts the `ImageFiltering` protocol will be able to execute this method.
    
    

    let initialCIImage = CIImage(image:

    originalImage, options: nil)

    
    This statement converts the original image to a `CIImage` instance so that you can apply filters to it, and assigns it to `initialCIImage`.
    
    

    let originalOrientation =

    originalImage.imageOrientation

    
    This statement stores the original image orientation in `originalOrientation`.
    
    

    guard let ciFilter = CIFilter(name: filter)

    else {

    print("filter not found")

    return originalImage

    }

    
    This `guard` statement gets the filter with the same name as `filter` and assigns it to `ciFilter`, and returns the original image if the filter is not found.
    
    

    ciFilter.setValue(initialCIImage, forKey:

    kCIInputImageKey)

    let context = CIContext()

    let filteredCIImage =

    (ciFilter.outputImage)!

    
    These statements apply the selected filter to `initialCIImage` and store the result in `filteredCIImage`.
    
    

    let filteredCGImage =

    context.createCGImage(filteredCIImage, from:

    filteredCIImage.extent)

    return UIImage(cgImage: filteredCGImage!,

    scale: 1.0, orientation:

    originalOrientation)

    
    These statements convert the `CIImage` instance stored in `filteredCIImage` back into a `UIImage` instance and returns it.
    

这完成了 ImageFiltering 协议和 apply(filter:originalImage:) 方法的实现。此时,你拥有以下内容:

  • FilterData.plist,其中包含应用内的照片滤镜数据。

  • FilterItem,一个可以存储过滤器和过滤器描述的类。

  • FilterDataManager,一个数据管理类,从 FilterData.plist 加载数据并生成 FilterItem 实例的数组。

  • ImageFiltering,一个包含方法 apply(filter:originalImage:) 的协议,该方法将过滤器应用于图像。

在下一节中,你将创建 Photo Filter 屏幕中 UI 元素的类,该屏幕允许你管理此屏幕及其内部的集合视图。

为 Photo Filter 屏幕创建类

到目前为止,你已经将 FilterData.plist 导入到你的应用中,创建了 FilterItemFilterDataManager 类,并创建了 ImageFiltering 协议。在本节中,你将设置 Photo Filter 屏幕的类,该屏幕允许你管理此屏幕及其内部的集合视图。

记住你已经在第十六章“MapKit 入门”中添加了PhotoFilter故事板文件到你的项目中。它包含一个场景,该场景由一个将包含用户选择的图片的大图像视图和一个将显示过滤器预览的集合视图组成。以下截图显示了完成实现后的样子:

![Figure 20.2: iOS Simulator showing the completed Photo Filter screen]

![img/Figure_20.02_B17469.jpg]

图 20.2:iOS 模拟器显示完成的 Photo Filter 屏幕

此屏幕的工作方式如下。当你点击餐厅详情屏幕中的添加照片按钮并选择照片时,Photo Filter屏幕将出现,显示所选照片,其下方有一个滚动列表的过滤器。滚动列表中的每个过滤器都在集合视图单元格中显示。点击滚动列表中的过滤器将应用所选过滤器到照片上。

在下一节中,你将创建并配置一个类来管理集合视图单元格。每个单元格将显示应用过滤器后照片的缩略图预览。

为集合视图单元格创建一个类

在项目导航器中的PhotoFilter故事板文件,你会看到集合视图已经在视图控制器场景中存在,但无法设置集合视图单元格的内容。你现在将创建一个类来管理它们。按照以下步骤操作:

  1. 右键点击PhotoFilter文件夹并选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch 类并点击下一步

  3. 按以下方式配置文件:

    FilterCell

    UICollectionViewCell

    Swift

    点击下一步

  4. 点击FilterCell文件将在项目导航器中显示。

  5. 将以下代码添加到该文件中,以声明和定义FilterCell类:

    import UIKit
    class FilterCell: UICollectionViewCell {
    FilterCell class has two properties: a label, nameLabel, and an image view, thumbnailImageView. The label will display the filter name, while the image view will display a thumbnail preview of the filter.This class also contains two methods, `awakeFromNib()` and  `set(filterItem:imageForThumbnail:)`. The `awakeFromNib()` method is called after the `FilterCell` instance has been loaded, and the two statements inside it round the corners of the image view. The `set(filterItem:imageForThumbnail:)` method takes `UIImage` and `FilterItem` instances as parameters, assigns the `name` property of the `FilterItem` instance to `nameLabel`, applies the filter specified by the `filter` property to the `UIImage` instance, and assigns the image with the filter applied to `thumbnailImageView`. Important InformationFor more details about `awakeFromNib()`, see this link: [`developer.apple.com/documentation/objectivec/nsobject/1402907-awakefromnib`](https://developer.apple.com/documentation/objectivec/nsobject/1402907-awakefromnib).
    
  6. 在项目导航器中点击PhotoFilter故事板文件。

  7. 在文档大纲中,选择FilterCell:![Figure 20.3: Identity inspector with Class set to FilterCell]

    ![img/Figure_20.03_B17469.jpg]

    ![Figure 20.3: Identity inspector with Class set to FilterCell]

  8. 点击属性检查器按钮。设置filterCell:![Figure 20.4: Attributes inspector with Identifier set to filterCell]

    ![img/Figure_20.04_B17469.jpg]

    ![Figure 20.4: Attributes inspector with Identifier set to filterCell]

  9. 点击连接检查器按钮。将nameLabelthumbnailImageView出口连接到相应的 UI 元素,如图所示:

![Figure 20.5: Connections inspector showing thumbnailImageView and nameLabel outlets]

![img/Figure_20.05_B17469.jpg]

![Figure 20.5: Connections inspector showing thumbnailImageView and nameLabel outlets]

现在你已经完成了集合视图单元格的设置。在下一节中,你将创建Photo Filter屏幕的视图控制器。这将允许你选择一张照片并选择要应用的照片的过滤器。

创建照片滤镜屏幕的视图控制器

到目前为止,你已经创建了FilterCell类来管理照片滤镜屏幕中的集合视图单元格。现在你将创建一个视图控制器来管理这个屏幕的内容。按照以下步骤操作:

  1. 右键点击PhotoFilter文件夹,选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch Class并点击下一步

  3. 配置文件,如下所示:

    PhotoFilterViewController

    UIViewController

    Swift

    点击“下一步”。

  4. 点击PhotoFilterViewController文件将在项目导航器中显示。删除viewDidLoad()方法之后的所有样板代码。

  5. 将以下代码添加到文件中,以声明和定义PhotoFilterViewController类及其属性:

    import UIKit
    AVFoundation framework. This framework contains methods for capturing, processing, synthesizing, controlling, importing, and exporting audiovisual media on Apple platforms.
    
    

    class PhotoFilterViewController: UIViewController {

    
    This statement declares the `PhotoFilterViewController` class, a subclass of the `UIViewController` class.
    
    

    @IBOutlet var mainImageView: UIImageView!

    
    This is an outlet for the image view that will display the user-selected photo with the filter applied. 
    
    

    @IBOutlet var collectionView: UICollectionView!

    
    This is an outlet for the collection view that will display thumbnail previews of each filter.
    
    

    private let manager = FilterDataManager()

    
    This statement assigns an instance of the `FilterDataManager` class to the  `manager` property.
    
    

    selectedRestaurantID:Int?

    
    Each restaurant has a unique numeric identifier. This property is used to store that identifier. You'll see how it's used when storing photos using `FilterItem` instances provided by `manager`.
    
    

    override func viewDidLoad()

    super.viewDidLoad()

    initialize()

    }

    
    This method calls an `initialize()` method when the `PhotoFilterViewController` instance loads its view. Note that this will generate an error, since `initialize()` hasn't been implemented yet.
    
  6. 如同之前一样,你将使用扩展来组织你的代码。在关闭花括号之后添加以下包含initialize()方法的private扩展:

    // MARK: - Private Extension
    private extension PhotoFilterViewController {
       func initialize() { 
          setupCollectionView() 
          checkSource()
       }
    }
    

    此扩展包含initialize()方法的实现,它调用两个其他方法。setupCollectionView()设置用于显示滤镜列表的集合视图。checkSource()检查用户对摄像头使用的授权状态。请注意,由于它们尚未实现,这些将生成错误。你将在下一步实现这些方法。

  7. initialize()方法之后的private扩展中实现setupCollectionView()checkSource()方法:

    func setupCollectionView() {
       let layout = UICollectionViewFlowLayout() 
       layout.scrollDirection = .horizontal 
       layout.sectionInset = UIEdgeInsets(top: 7, 
       left: 7, bottom: 7, right: 7) 
       layout.minimumInteritemSpacing = 0 
       layout.minimumLineSpacing = 7
       collectionView.collectionViewLayout = layout 
       collectionView.dataSource = self 
       collectionView.delegate = self
    }
    func checkSource() {
       let cameraMediaType = AVMediaType.video 
       let cameraAuthorizationStatus = 
       AVCaptureDevice.authorizationStatus(for: 
       cameraMediaType)
       switch cameraAuthorizationStatus { 
       case .notDetermined: 
          AVCaptureDevice.requestAccess(for: 
          cameraMediaType) { granted in 
             if granted {
                DispatchQueue.main.async {
                   self.showCameraUserInterface()
                }
             }
          }
       case .authorized: 
          self.showCameraUserInterface() 
       default: 
          break
       }
    }
    

    让我们分解一下:

    setupCollectionView() 
    

    设置用于显示滤镜缩略图预览的集合视图。在这里,你创建了一个UICollectionViewFlowLayout实例,设置滚动方向、部分内边距、项目间距和行间距属性,并将其分配给集合视图。之后,你将PhotoFilterViewController类设置为该集合视图的委托和数据源。请注意,你正在通过编程设置delegatedataSource,而不是使用故事板;两种方法都是可接受的。不要担心错误,它们出现是因为你尚未为该类采用UICollectionViewDataSourceUICollectionViewDelegate协议。你将在稍后修复这个问题。

    checkSource() 
    

    检查用户对摄像头使用的授权状态。可能的情形如下:

    .notDetermined表示用户尚未被询问是否访问摄像头。

    .authorized表示用户之前已经授予了摄像头访问权限。

    .restricted表示由于设备上设置的限制,用户无法获得访问权限。

    .denied表示用户之前已经拒绝了对该应用访问摄像头的权限。

    如果状态是.notDetermined,则应用程序将请求用户权限,如果授予权限,则调用showCameraUserInterface()方法。如果状态是.authorized,则调用showCameraUserInterface()方法。请注意,这将生成错误,因为showCameraUserInterface()尚未实现。如果状态是.restricted.denied,则属于default:情况,方法退出。

  8. 需要一些额外的辅助方法。在checkSource()方法之后,将以下代码添加到private扩展中来实现它们:

    func showApplyFilterInterface() {
       filters = manager.fetch()
       if let mainImage = self.mainImage { 
          mainImageView.image = mainImage 
          collectionView.reloadData()
       }
    }
    @IBAction func onPhotoTapped(_ sender: Any) {
       checkSource()
    }
    

    让我们分解一下:

    showApplyFilterInterface() 
    

    此方法将在用户从相机或相册中选择照片后调用。它调用FilterManager实例的fetch()方法,该方法加载FilterData.plist并将其内容放入FilterItem实例的数组中。然后,此数组被分配给PhotoFilterViewController实例的filters属性,该属性将用于稍后填充集合视图中的过滤器缩略图预览。接下来的语句将PhotoFilterViewController实例的mainImage属性分配给mainImageView,这是集合视图上方的图像视图的出口,如果已设置mainImage。最后的语句告诉集合视图重新绘制自己。

    onPhotoTapped() 
    

    此方法调用您之前实现的checkSource()方法,如果已授予授权,则调用showCameraUserInterface()方法。您将此分配给NSCameraUsageDescriptionNSMicrophoneUsageDescription键中的相机按钮在Info.plist中。要了解更多关于请求使用相机权限的信息,请访问developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_ios

  9. 您将采用UICollectionViewDataSource协议并实现所需的方法,以便集合视图显示过滤器的缩略图预览。在private扩展之后添加一个新扩展,并按以下方式实现它们:

    extension PhotoFilterViewController: 
    UICollectionViewDataSource {
       func collectionView(_ collectionView: 
       UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          filters.count
       }
       func collectionView(_ collectionView: 
       UICollectionView, cellForItemAt indexPath: 
       IndexPath) -> UICollectionViewCell {
          let cell = collectionView
          .dequeueReusableCell
          (withReuseIdentifier: "filterCell", 
          for: indexPath) as! FilterCell
          let filterItem = filters[indexPath.row]
          if let thumbnail = thumbnail {
             cell.set(filterItem: filterItem, 
             imageForThumbnail: thumbnail)
          }
          return cell
       }
    }
    collectionView(_:numberOfItemsInSection:) 
    

    确定集合视图应显示的项目数量,这与PhotoFilterViewController实例的filters数组中的FilterItems数量相同。

    collectionView(_:cellForItemAt:) 
    

    确定每个单元格中要放置的内容。在这里,您获取与集合视图中单元格位置对应的FilterItem实例,并将其与PhotoFilterViewController实例的thumbnail属性一起传递给set(filterItem:imageForThumbnail:)方法,该方法设置集合视图单元格的图像和标签。

  10. 您之前使用UICollectionViewFlowLayout实例设置了集合视图。现在,您将为集合视图单元格设置大小。在包含数据源方法的扩展之后添加以下扩展:

    extension PhotoFilterViewController: 
    UICollectionViewDelegateFlowLayout {
       func collectionView(_ collectionView: 
       UICollectionView, layout 
       collectionViewLayout: 
       UICollectionViewLayout, sizeForItemAt 
       indexPath: IndexPath) -> CGSize {
          let collectionViewHeight = 
          collectionView.frame.size.height 
          let topInset = 14.0
          let cellHeight = collectionViewHeight - 
          topInset
          return CGSize(width: 150, height: 
          cellHeight)
       }
    }
    

    collectionView(_:layout:sizeForItemAt:) 返回每个相册视图单元格应具有的大小。首先,将相册的高度分配给 collectionViewHeight。然后,将 topInset 的值设置为 14.0 点。通过从 collectionViewHeight 中减去 topInset 来计算相册视图单元格的高度。这导致相册视图单元格的顶部与相册视图的顶部之间有 14 点的间隙。最后,返回一个宽度设置为 150 点,高度设置为 cellHeightCGSize 实例,作为相册视图单元格的大小。以前,您使用大小检查器来完成此操作;现在,您以编程方式完成。

现在您将在这个类中连接的输出口和动作与 PhotoFilter 故事板文件中的 UI 元素连接。collectionView 是显示过滤器列表的相册的输出口。mainImageView 是位于其上方的图像视图,显示用户选择的图像。onPhotoTapped() 是用于导航栏中的相机按钮。您还将配置 取消 按钮以关闭 相册过滤器 屏幕。按照以下步骤操作:

  1. 在项目导航器中单击 PhotoFilter 故事板文件。选择 PhotoFilterViewController:![图 20.6:身份检查器,类设置为 PhotoFilterViewController 图 20.06:B17469.jpg

    图 20.6:身份检查器,类设置为 PhotoFilterViewController

  2. 选择连接检查器。从 collectionView 输出口拖动到文档大纲中的 集合视图:![图 20.7:显示 collectionView 输出口的连接检查器 图 20.07:B17469.jpg

    图 20.7:显示 collectionView 输出口的连接检查器

  3. mainImageView 输出口拖动到文档大纲中的 图像视图:![图 20.8:显示 mainImageView 输出口的连接检查器 图 20.08:B17469.jpg

    图 20.8:显示 mainImageView 输出口的连接检查器

  4. onPhotoTapped: 动作拖动到相机按钮:![图 20.9:显示 onPhotoTapped: 动作的连接检查器 图 20.09:B17469.jpg

    图 20.9:显示 onPhotoTapped: 动作的连接检查器

  5. 取消 按钮用于退出此屏幕,如果用户不想进行选择。您将 取消 按钮连接到上一章中实现的 unwind 方法,这将关闭此屏幕并将用户返回到 餐厅详情 屏幕。Ctrl + 拖动取消 按钮到场景工具栏中的退出图标:![图 20.10:显示取消按钮动作设置的相册过滤器视图控制器场景 图 20.10:B17469.jpg

    图 20.10:显示取消按钮动作设置的相册过滤器视图控制器场景

  6. 在弹出菜单中选择 unwindReviewCancelWithSegue:

![图 20.11:选择 unwindReviewCancelWithSegue: 的弹出菜单图 20.11:B17469.jpg

图 20.11:选择 unwindReviewCancelWithSegue 的弹出菜单

PhotoFilterViewController 类的所有输出口和操作都已连接。

接下来,你将实现以下方法:

要实现 showCameraUserInterface()UIImagePickerControllerDelegate 方法,请在项目导航器中点击 PhotoFilterViewController 文件,并在 UICollectionViewDelegateFlowLayout 扩展之后添加以下扩展:

extension PhotoFilterViewController: UIImagePickerControllerDelegate, 
UINavigationControllerDelegate {
   func showCameraUserInterface() {
      let imagePicker = UIImagePickerController()
      imagePicker.delegate = self
   #if targetEnvironment(simulator)
      imagePicker.sourceType = 
      UIImagePickerController.SourceType.photoLibrary
   #else
      imagePicker.sourceType = 
      UIImagePickerController.SourceType.camera
      imagePicker.showsCameraControls = true
   #endif
      imagePicker.mediaTypes = ["public.image"]
      imagePicker.allowsEditing = true 
      self.present(imagePicker, animated: true, 
      completion: nil)
   }
   func imagePickerControllerDidCancel(_ picker: 
   UIImagePickerController) {
      picker.dismiss(animated: true, completion: nil)
   }
   func imagePickerController(_ picker: 
   UIImagePickerController, 
   didFinishPickingMediaWithInfo info:
   [UIImagePickerController.InfoKey : Any]) {
      if let selectedImage = 
      info[UIImagePickerController.InfoKey
      .editedImage] as? UIImage {
         self.thumbnail = 
         selectedImage.preparingThumbnail(of: 
         CGSize(width: 100, height: 100))
         let mainImageViewSize = 
         mainImageView.frame.size
         self.mainImage = 
         selectedImage.preparingThumbnail(of: 
         mainImageViewSize)
      }
      picker.dismiss(animated: true){ 
         self.showApplyFilterInterface()
      }
   }
}

首先,让我们谈谈 showCameraUserInterface()。当点击相机按钮时,此方法被触发,在屏幕上显示图像选择器。这个图像选择器是标准的 iOS 图像选择器,当你想要使用图像时会出现,例如,将图像添加到 Facebook 帖子或推文中。

让我们分解一下:

let imagePicker = UIImagePickerController()

创建 UIImagePickerController 类的实例并将其分配给 imagePicker

imagePicker.delegate = self

imagePicker 实例的 delegate 属性设置为 PhotoFilterViewController 实例。

#if targetEnvironment(simulator)
   imagePicker.sourceType = 
   UIImagePickerController.SourceType.photoLibrary
#else
   imagePicker.sourceType = 
   UIImagePickerController.SourceType.camera
   imagePicker.showsCameraControls = true
#endif

这段代码块被称为条件编译块。它以 #if 编译指令开始,以 #endif 编译指令结束。如果你在模拟器上运行,只有设置 imagePicker 实例的 sourceType 属性为相册的语句会被编译。如果你在实际设备上运行,设置 imagePicker 实例的 sourceType 属性为相机并显示相机控制的语句会被编译。

重要信息

你可以在以下链接中了解更多关于条件编译块的信息:docs.swift.org/swift-book/ReferenceManual/Statements.html#ID538

imagePicker.mediaTypes = ["public.image"]

将相机界面设置为捕获静态图像。

imagePicker.allowsEditing = true 

表示用户允许编辑所选图像。

self.present(imagePicker, animated: true, completion: nil)

在屏幕上呈现 imagePicker

当图像选择器出现在屏幕上时,你可以选择一张照片或取消。如果你取消,将触发 imagePickerControllerDidCancel(_:) 并关闭图像选择器。

如果你选择了一张照片,imagePickerController(_:didFinishPickingMediaWithInfo:) 将被触发,并将返回并分配给 selectedImage 的照片。接下来,将使用 selectedImage 实例的 preparingThumbnail(of:) 方法创建一个宽度和高度为 100 点的小图像。然后,将此分配给 thumbnail 属性。之后,将使用 preparingThumbnail(of:) 方法从 selectedImage 创建与 mainImageView 相同大小的图像。这将分配给 mainImage 属性,并且图像选择器将被关闭。

重要信息

你可以在此链接中了解更多关于 preparingThumbnail(of:) 方法的知识:developer.apple.com/documentation/uikit/uiimage/3750835-preparingthumbnail

接下来,你需要实现 filterMainImage(filterItem:) 方法,这是一个用于将过滤器应用于 mainImageView 中图像的方法。在 UIImagePickerControllerDelegate 扩展之后添加包含此方法的扩展:

extension PhotoFilterViewController: ImageFiltering {
   func filterMainImage(filterItem: FilterItem) {
      if let mainImage = mainImage, let filter = 
      filterItem.filter {
         if filter != "None" {
            mainImageView.image = 
            self.apply(filter: filter, 
            originalImage: mainImage)
         } else {
            mainImageView.image = mainImage
         }
      }
   }
}

这使得 PhotoFilterViewController 类采用 ImageFiltering 协议。请记住,任何采用此协议的类都会获得 apply(filter:originalImage:) 方法。filterMainImage(filterItem:) 方法使用此方法将选定的过滤器应用于存储在 PhotoFilterViewController 实例的 mainImage 属性中的照片,并将结果分配给 mainImageView 输出口,以便在屏幕上可见。如果你选择了 None 过滤器,则 mainImage 将分配给 mainImageView 输出口。

你仍然需要知道用户选择了哪个过滤器,因此你将使 PhotoFilterViewController 类采用 UICollectionViewDelegate 协议并实现识别在集合视图中被点击的单元格的方法。在 ImageFiltering 扩展之后添加包含此方法的以下扩展:

extension PhotoFilterViewController: 
UICollectionViewDelegate {
   func collectionView(_ collectionView: 
   UICollectionView, didSelectItemAt 
   indexPath: IndexPath) { 
      let filterItem = self.filters[indexPath.row] 
      filterMainImage(filterItem: filterItem)
   }
}

当用户在集合视图中点击单元格时,会调用 collectionView(_:didSelectItemAt:) 方法。然后,被点击的单元格对应的 FilterItem 将传递给 filterMainImage(filterItem:)

PhotoFilterViewController 类的实现现在已经完成,但请记住,你必须请求使用相机或访问照片库的权限。你将修改项目中的 Info.plist 文件,以便当你的应用程序尝试访问相机或照片库时向用户显示消息。

获取使用相机或照片库的权限

如前所述,苹果规定,如果你的应用程序希望访问相机或照片库,则必须通知用户。如果你不这样做,你的应用程序将被拒绝,并且不允许在 App Store 上发布。

你将修改项目中的 Info.plist 文件,以便当你的应用程序尝试访问相机或照片库时显示消息。按照以下步骤操作:

  1. 在项目导航器中点击Info.plist文件以显示键列表。将鼠标指针移至任何现有键上,并点击+按钮:图 20.12:编辑区域显示 Info.plist 的内容

    图 20.12:编辑区域显示 Info.plist 的内容

  2. 应该会出现一个字段,允许你输入一个额外的键:图 20.13:编辑区域显示用于输入键的字段

    图 20.13:编辑区域显示用于输入键的字段

  3. 输入以下键:

    NSPhotoLibraryUsageDescription
    NSCameraUsageDescription
    
  4. 对于每个键的值,输入一个字符串,向用户解释你为什么希望使用相机或照片库:

图 20.14:添加了额外键的 Info.plist

图 20.14:添加了额外键的 Info.plist

构建并运行项目。转到餐厅详情页面,点击添加照片按钮。你应该会看到以下提示:

图 20.15:iOS 模拟器显示相机访问提示

图 20.15:iOS 模拟器显示相机访问提示

点击确定。图片选择器将出现:

图 20.16:iOS 模拟器显示图片选择器

图 20.16:iOS 模拟器显示图片选择器

选择一张照片,照片滤镜屏幕将显示照片和一系列应用了不同滤镜的缩略图。点击一个滤镜将应用其效果到照片上:

图 20.17:iOS 模拟器显示照片滤镜屏幕

图 20.17:iOS 模拟器显示照片滤镜屏幕

你已经修改了项目中的info.plist文件,现在你的应用程序在使用相机或照片库之前会请求权限。你可以使用取消按钮关闭照片滤镜屏幕并返回到餐厅详情屏幕。不过,你目前还不能使用保存按钮,你将在下一章实现其功能。

摘要

在本章中,你完成了FilterData.plist的实现,这是一个包含你想要使用的滤镜的.plist文件,创建了FilterItem类来存储滤镜数据,并创建了FilterManager数据管理类来读取.plist文件并填充FilterItem实例的数组。然后,你创建了一个协议ImageFiltering,其中包含一个应用于图像的方法。接着,你创建了FilterCellPhotoFilterViewController类来管理集合视图单元格,并且PhotoFilterViewController类采用了UIImagePickerDelegate协议,并添加了方法,以便你可以在应用程序中使用来自相机或照片库的照片。最后,你在PhotoFilterViewController中添加了代码,以将选定的滤镜应用到图片上。

现在,你可以编写自己的应用程序,从相机或照片库导入照片,并对其应用滤镜。

注意,选中的图片无法保存。你将在下一章学习如何使用 Core Data 保存评论和图片,这样在退出并重新启动应用后它们会再次出现。

第二十一章:第二十一章:理解 Core Data

您的应用几乎完成了!每个屏幕都像您在第九章**,设置用户界面中看到的演示应用一样工作。然而,您还需要完成最后一件事。在第十九章**,开始使用自定义 UIControls中,您实现了评论表单屏幕,允许您为特定餐厅输入评论。在前一章中,您实现了照片滤镜屏幕,允许您从相机或相册获取照片并为其添加滤镜。但目前还没有保存评论或照片的方法,当应用关闭时它们都会丢失。

在本章中,您将使用 Core Data 在您的应用中保存评论和照片。首先,您将了解 Core Data 及其不同组件。接下来,您将为评论和照片创建数据模型,并为您的应用创建相应的模型对象。然后,您将为您的应用设置 Core Data 组件。

您将了解用于保存特定餐厅的评论和照片的机制,使用餐厅标识符。之后,您将更新 ReviewFormViewControllerPhotoFilterViewController 类以保存特定餐厅的评论和照片,并修改 RestaurantDetailViewController 类以加载和显示特定餐厅的评论。您还将计算并显示该餐厅的整体评分。

最后,您将独立修改 RestaurantDetailViewController 类以加载和显示特定餐厅的照片。

到本章结束时,您将了解 Core Data 的工作原理。您还将能够设置 Core Data 组件,并使用数据管理类在您的应用和 Core Data 组件之间建立接口。您还将学会使用 Core Data 保存和加载评论和照片,您将在自己的应用中实现这些功能。

本节将涵盖以下主题:

  • 介绍 Core Data

  • 为您的应用实现 Core Data 组件

  • 理解保存和加载的工作原理

  • 更新 ReviewFormViewController 类以保存评论

  • 更新 PhotoFilterViewController 类以保存照片

  • 餐厅详情 屏幕中显示保存的评论和照片

  • 计算餐厅的整体评分

技术要求

您将继续在上一章中修改的 LetsEat 项目上工作。

本章的完成 Xcode 项目位于本书代码包的 Chapter21 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频,了解代码的实际应用:

bit.ly/3o81yKK

让我们先了解核心数据组件及其工作原理。

介绍核心数据

Core Data 是苹果将应用数据保存到设备的一种机制。它提供持久性、撤销/重做、后台任务、视图同步、版本控制和迁移。你可以使用 Xcode 的数据模型编辑器定义数据类型和关系,Core Data 将自动生成数据类型的类定义。然后,Core Data 可以根据类定义创建和管理对象实例。

重要提示

你可以通过这个链接了解更多关于核心数据的信息:developer.apple.com/documentation/coredata

Core Data 提供了一套称为 Core Data 堆栈的类,用于管理和持久化对象实例,如下所示:

  • NSManagedObjectModel

    描述应用类型,包括它们的属性和关系。

  • NSManagedObject

    一个用于根据NSManagedObjectModel中的数据实现应用类型实例的类。

  • NSManagedObjectContext

    跟踪应用类型实例的变化。

  • NSPersistentStoreCoordinator

    从存储中保存和检索应用类型实例。

  • NSPersistentContainer

    同时设置模型、上下文和存储协调器。

    重要提示

    你可以通过这个链接了解更多关于 Core Data 堆栈的信息:developer.apple.com/documentation/coredata/core_data_stack

在下一节中,你将实现应用所需的核心数据组件以保存评论或照片。

实现应用的核心数据组件

在实现应用的核心数据组件之前,让我们思考一下保存评论或照片需要做什么。

想象一下你正在使用 Microsoft Word 保存评论或照片。你首先创建一个新的 Word 文档模板,其中包含评论或照片的相关字段。然后,你根据模板创建新的 Word 文档并填写数据。你进行必要的更改,比如更改评论的文本,或者更改应用于照片的效果。在这个阶段,你还没有保存文件。当你对你的文档满意时,你将其保存到电脑的硬盘上。下次你想查看你的评论或照片时,你会在硬盘上搜索相关文档,双击它以在 Word 中打开,以便再次查看。

现在你已经了解了需要做什么,让我们回顾实现它所需的步骤。首先,你需要为评论或照片创建一个数据模型。你通过在 Xcode 的数据模型编辑器中创建实体来完成此操作,这些实体类似于 Microsoft Word 模板。实体可以具有属性,这些属性类似于 Microsoft Word 模板中的字段。

Xcode 可以从这个数据模型创建一个 NSManagedObjectModel 类。然后,Core Data 将使用这个 NSManagedObjectModel 类来创建 NSManagedObject 实例,类似于 Microsoft Word 模板被用来创建 Microsoft Word 文件。

这些 NSManagedObject 实例被放置在一个 NSManagedObjectContext 实例中,你的应用程序可以访问它们,类似于在 Microsoft Word 中打开 Microsoft Word 文件。然后,当你调用 NSManagedObject 实例时,你可以像在 Microsoft Word 中编辑 Microsoft Word 文档一样修改它们。

当你完成审查或照片后,NSManagedObjectContext 实例中的 NSManagedObject 实例将被保存到你的 iOS 设备上的一个文件中,称为 持久化存储。这类似于当你完成 Word 文档时将 Word 文档保存到硬盘上。

NSPersistentStoreCoordinator 实例管理持久化存储和 NSManagedObjectContext 实例之间的信息流。

你将使用 NSPersistentContainer 类来为你的应用程序创建 NSManagedObjectModelNSManagedObjectContextNSPersistentStoreCoordinator 的实例。

重要提示

你可以在此链接中了解更多关于如何设置 Core Data 栈的信息:developer.apple.com/documentation/coredata/setting_up_a_core_data_stack

在下一节中,你将使用 Xcode 的数据模型编辑器创建表示审查或照片的实体和属性。

创建数据模型

目前,当你点击 保存审查表单 屏幕时,你输入的字段中的数据只是打印到调试区域,而在 照片滤镜 屏幕上点击 保存 不会做任何事情。第一步是为存储来自 审查表单 屏幕的数据和来自 照片滤镜 屏幕的照片的对象创建类定义。你将使用 Xcode 的数据模型编辑器创建用于审查和照片的实体,Xcode 将自动生成类定义。让我们首先创建审查的实体。按照以下步骤操作:

  1. 在项目导航器中右键点击 Misc 文件夹并选择 Core Data

  2. 右键点击 Core Data 组并选择 新建文件

  3. 在过滤器字段中输入 data,并选择 数据模型。点击 下一步图 21.1:选择数据模型模板

    图 21.1:选择数据模型模板

  4. 将文件命名为 LetsEatModel 并点击 创建。数据模型编辑器将在编辑区域显示:图 21.2:显示数据模型编辑器的编辑区域

    图 21.2:显示数据模型编辑器的编辑区域

  5. 点击 添加实体 按钮:图 21.3:添加实体按钮

    图 21.3:添加实体按钮

  6. 实体出现在 ENTITIES 部分。此实体的 属性出现在实体的右侧:![图 21.4:选择实体的数据模型编辑器 图片

    图 21.4:选择实体的数据模型编辑器

  7. 双击 Review:![图 21.5:将实体重命名为 Review 的数据模型编辑器 图片

    图 21.5:将实体重命名为 Review 的数据模型编辑器

  8. 点击 nameString:![图 21.6:显示 Review 实体的 name 属性的数据模型编辑器 图片

    图 21.6:显示 Review 实体的 name 属性的数据模型编辑器

  9. 你还需要为 restaurantID 中的每个字段创建一个属性,以便将评论与餐厅关联起来。添加以下属性和类型:![图 21.7:要添加到 Review 实体的属性 图片

    图 21.7:要添加到 Review 实体的属性

    当创建 Review 实例时,date 将自动设置。

  10. 检查完成时,你的 Review 实体的属性看起来应该像这样:![图 21.8:Review 实体的属性 图片

    图 21.8:Review 实体的属性

  11. 添加一个额外的属性,uuid,类型为 UUID。这被用作每个 Review 实例的键值。点击数据模型检查器,在 Review 实例下必须有一个键值。![图 21.9:显示 uuid 属性可选未勾选的数据模型检查器 图片

    图 21.9:显示 uuid 属性可选未勾选的数据模型检查器

  12. 添加第二个实体,称为 RestaurantPhoto,具有以下属性。Core Data 无法存储 UIImage 对象,因此当您需要将 photo 属性设置为 UIImage 对象以在您的应用程序中显示时,photo 属性的类型被设置为 UIImage 对象。当创建 RestaurantPhoto 实例时,date 将自动设置。您将使用 restaurantID 将照片与餐厅关联起来,并使用 uuid 作为键值:![图 21.10:RestaurantPhoto 实体的属性 图片

    图 21.10:RestaurantPhoto 实体的属性

  13. 对于 uuid,不要忘记在数据模型检查器中取消选择 Optional

![图 21.11:显示 uuid 属性可选未勾选的数据模型检查器图片

图 21.11:显示 uuid 属性可选未勾选的数据模型检查器

您已经完成了为您的应用程序创建所需的实体。构建您的应用程序。Xcode 将自动创建 ReviewRestaurantPhoto 实体的类文件,但它们在项目导航器中不可见。为了更容易地使用它们,您将为每个实体创建一个模型对象,从下一节的 ReviewItem 开始。

创建 ReviewItem

您已使用 Xcode 的数据模型编辑器创建了两个实体来存储评论和照片数据。然后,Xcode 将从数据模型自动生成两个 NSManagedObject 类定义,ReviewRestaurantPhoto,但您在项目导航器中看不到它们。

您将创建两个模型对象,ReviewItemRestaurantPhotoItem,它们将与 ReviewRestaurantPhoto 实例协同工作。现在让我们创建 ReviewItem。按照以下步骤操作:

  1. 在项目导航器中右键点击 ReviewForm 文件夹,并选择 Model

  2. ReviewForm 文件夹内的 Model 文件夹中右键点击,并选择 New File

  3. iOS 应已选中。选择 Swift File 然后点击 Next

  4. 将此文件命名为 ReviewItem。点击后,ReviewItem 文件将出现在项目导航器中。

  5. 按照以下方式修改文件:

    import UIKit
    struct ReviewItem {
       var date: Date?
    var rating: Double? 
    var title: String? 
       var name: String?
       var customerReview: String?
       var restaurantID: Int64?
       var uuid = UUID()
    }
    extension ReviewItem {
       init(review: Review) {
          self.date = review.date
          self.rating = review.rating
          self.title = review.title
          self.name = review.name
    self.customerReview = review.customerReview 
          self.restaurantID = review.restaurantID
    if let reviewUUID = review.uuid { 
             self.uuid = reviewUUID
          }
       }
    }
    

如您所见,ReviewItem 结构的属性与 Review 实体的属性相同。初始化器创建一个 ReviewItem 实例,并将 Review 的属性映射到 ReviewItem 实例的属性上。

在下一节中,您将创建第二个模型对象 RestaurantPhotoItem,它将是 RestaurantPhoto 实体的模型对象。

创建 RestaurantPhotoItem

创建 RestaurantPhotoItem 的过程与创建 ReviewItem 类似。按照以下步骤操作:

  1. PhotoFilter 文件夹内的 Model 文件夹中右键点击,并选择 New File

  2. iOS 应已选中。选择 Swift File 然后点击 Next

  3. 将此文件命名为 RestaurantPhotoItem。点击 Create

  4. 按照以下方式修改文件:

    import ReviewItem structure, the properties of the RestaurantPhotoItem structure are the same as the RestaurantPhoto entity's attributes. There is one additional computed property, photoData, which is used to store the representation of photo in binary data format, as Core Data can't store UIImage instances.The initializer creates a `RestaurantPhotoItem` instance and maps the attributes from the `RestaurantPhoto` entity to properties in `RestaurantPhotoItem` instance. Note the conversion from binary data to `UIImage` when setting the value for `photo`.
    

现在,您已经声明并定义了 ReviewItemRestaurantPhotoItem,让我们在下一节创建一个 Core Data 管理器,该管理器将为您的应用设置 Core Data 组件。

创建 Core Data 管理器

到目前为止,Xcode 已经从数据模型自动生成了 ReviewRestaurantData 类定义,并且您已经声明并定义了相应的模型对象,ReviewItemRestaurantPhotoItem。现在您将创建一个 CoreDataManager 类,该类将为您的应用设置 Core Data 组件。按照以下步骤操作:

  1. Misc 文件夹内的 Core Data 文件夹中右键点击,并选择 New File

  2. iOS 应已选中。选择 Swift File 然后点击 Next

  3. 将此文件命名为 CoreDataManager。点击后,CoreDataManager 文件将出现在项目导航器中。

  4. import 语句后添加以下代码:

    import CoreData
    

    这使您能够访问 Core Data 库。

  5. 添加以下代码以声明和定义 CoreDataManager 结构:

    struct CoreDataManager { 
       let container: NSPersistentContainer
       init() {
          container = NSPersistentContainer(name: 
          "LetsEatModel")
          container.loadPersistentStores { 
             (storeDesc, error) in 
             error.map {
                print($0)
             }
          }
       }
    }
    

    这将创建并初始化 NSManagedObjectModelNSPersistentStoreCoordinatorNSManagedObjectContext 的实例。

  6. 要创建一个将在整个应用中可用的 CoreDataManager 结构实例,请在项目导航器中点击 AppDelegate 文件,并在闭合花括号后添加以下代码:

    extension CoreDataManager {
       static var shared = CoreDataManager()
    }
    

接下来,您将添加创建 ReviewRestaurantPhoto 实例的方法,使用 ReviewItemRestaurantPhotoItem 实例填充它们,并将它们保存到持久存储中。按照以下步骤操作:

  1. 在项目导航器中点击 CoreDataManager 文件。在初始化器之后添加以下代码以实现 addReview(_:) 方法:

    func addReview(_ reviewItem: ReviewItem) {
       let review = Review(context: 
       container.viewContext)
       review.date = Date()
       if let reviewItemRating = reviewItem.rating { 
          review.rating = reviewItemRating
       }
       review.title = reviewItem.title 
       review.name = reviewItem.name
       review.customerReview = 
       reviewItem.customerReview
       if let reviewItemRestID = 
       reviewItem.restaurantID { 
          review.restaurantID = reviewItemRestID  
       }
       review.uuid = reviewItem.uuid
       save()
    }
    

    此方法接受一个 ReviewItem 实例作为参数,并从 NSManagedObjectContext 实例中获取一个空的 Review 实例。ReviewItem 实例的属性被分配给 Review 实例的属性,并调用 save() 方法将 NSManagedObjectContext 实例的内容保存到持久存储中。请注意,您将看到错误,因为您尚未实现 save() 方法。现在忽略此错误。

  2. addReview(_:) 方法之后添加以下代码以实现 addPhoto(_:) 方法:

    func addPhoto(_ restPhotoItem: 
    RestaurantPhotoItem) {
       let restPhoto = RestaurantPhoto(context: 
       container.viewContext)
       restPhoto.date = Date()
       restPhoto.photo = restPhotoItem.photoData
       if let restPhotoID = 
          restPhotoItem.restaurantID { 
          restPhoto.restaurantID = restPhotoID 
       }
       restPhoto.uuid = restPhotoItem.uuid
       save()
    }
    

    此方法与 addReview(_:) 类似。它接受一个 RestaurantPhotoItem 实例作为参数,并从 NSManagedObjectContext 实例中获取一个空的 RestaurantPhoto 实例。RestaurantPhotoItem 实例的属性被分配给 RestaurantPhoto 实例的属性,并调用 save() 方法将 NSManagedObjectContext 实例的内容保存到持久存储中。请注意,您将看到错误,因为您尚未实现 save() 方法。同样,现在忽略此错误。

  3. 通过在最后的括号之前添加以下代码来实现 save() 方法:

    private func save() {
       do {
          if container.viewContext.hasChanges {
             try container.viewContext.save()
          }
       } catch let error {
          print(error.localizedDescription)
       }
    }
    

    do-catch 块将 NSManagedObjectContext 实例的内容保存到持久存储中。如果保存不成功,将在调试区域打印错误消息。

当您想要从持久存储中检索评论和照片时,您将使用 restaurantID 作为标识符来获取特定餐厅的评论和照片。现在让我们实现所需的这些方法。在 addPhoto(_:) 方法之后添加以下代码以实现 fetchReviews(by:)fetchPhotos(by:) 方法:

func fetchReviews(by identifier: Int) -> 
[ReviewItem] {
   let moc = container.viewContext
   let request = Review.fetchRequest()
   let predicate = NSPredicate(format: 
   "restaurantID = %i", identifier)
   var reviewItems: [ReviewItem] = []
   request.sortDescriptors = 
   [NSSortDescriptor(key: "date", 
   ascending: false)]
   request.predicate = predicate 
   do {
      for review in try moc.fetch(request) {
         reviewItems.append(ReviewItem(review: 
         review))
      }
      return reviewItems
   } catch {
      fatalError("Failed to fetch reviews: 
      \(error)")
   }
}

func fetchRestPhotos(by identifier: Int) -> 
[RestaurantPhotoItem] {
   let moc = container.viewContext
   let request = RestaurantPhoto.fetchRequest()
   let predicate = NSPredicate(format: 
   "restaurantID = %i", identifier) 
   var restPhotoItems: [RestaurantPhotoItem] = [] 
   request.sortDescriptors = 
   [NSSortDescriptor(key: "date", 
   ascending: false)]
   request.predicate = predicate 
   do {
      for restPhoto in try moc.fetch(request) {
         restPhotoItems.append(RestaurantPhotoItem
         (restaurantPhoto: restPhoto))
      }
      return restPhotoItems
   } catch {
      fatalError("Failed to fetch restaurant 
      photos: \(error)")
   }
}

让我们分解一下,从 fetchReviews(by:) 开始:

let moc = container.viewContext

这获取了对 NSManagedObjectContext 实例的引用。

let request = Review.fetchRequest() 

这从持久存储中创建一个 Review 实例。

let predicate = NSPredicate(format: "restaurantID = %i", identifier)

这将创建一个具有指定 restaurantIDReview 实例。

var reviewItems: [ReviewItem] = []

这将创建一个数组,reviewItems,您将使用它来存储获取请求的结果。

request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]

这按日期对获取请求的结果进行排序,最近的条目排在前面。

request.predicate = predicate

这设置了获取请求的谓词。

do {
   for review in try moc.fetch(request) {
      reviewItems.append(ReviewItem(review: 
      review))
   }
   return reviewItems
} catch {
   fatalError("Failed to fetch reviews: \(error)")
}

do-catch 块执行获取请求并将结果放置在 items 数组中。如果失败,您的应用程序将崩溃,并在调试区域打印错误消息。

fetchPhotos(by:)fetchReview(by:) 的工作方式相同,但返回一个 RestaurantPhotoItems 实例的数组。

您已经创建了一个 CoreDataManager 类,用于向持久存储中添加数据并检索数据。构建并运行您的应用以测试错误。它应该与之前一样工作。

您已经在您的应用中实现了 Core Data 的所有组件。接下来,您将配置 RestaurantDetailViewController 以使用 Core Data 在 餐厅详情 屏幕中显示评论和照片。您将从学习 餐厅详情 屏幕如何显示特定餐厅的评论和照片开始。

提示

由于这是一个较长的章节,您可能希望在这里休息一下。

理解保存和加载的工作原理

让我们回顾一下您到目前为止所做的工作。您已经使用数据模型编辑器创建了 ReviewRestaurantPhoto 实体,并为它们创建了相应的模型对象,分别命名为 ReviewItemRestaurantPhotoItem。您创建了 CoreDataManager 类来从持久存储中添加和获取 ReviewRestaurantPhoto 实例。CoreDataManager 类使用餐厅标识符将评论和餐厅照片与特定餐厅关联起来,但它从哪里来?

打开您项目中的 Misc 文件夹,然后打开 JSON 文件夹。如果您点击其中的任何 JSON 文件,您会看到每个餐厅都有一个唯一的数字标识符。例如,The Tap Trailhouse 餐厅的标识符是 145237,如图下所示:

Figure 21.12: Editor area showing contents for Boston.json

图 21.12:编辑区域显示 Boston.json 的内容

当您将餐厅照片和评论保存到持久存储中时,您将连同此标识符一起保存它们。然后,当在 RestaurantDetailViewController 中显示特定餐厅时,将使用一个 ReviewDataManager 实例来检索该餐厅的评论和餐厅照片,并在集合视图中显示它们,如图中所示:

Figure 21.13: iOS Simulator showing Restaurant Detail screen with reviews and restaurant photos

图 21.13:iOS 模拟器显示带有评论和餐厅照片的餐厅详情屏幕

如果没有评论或照片,您将使用 NoDataView 来通知用户没有评论或照片,如图中所示:

Figure 21.14: iOS Simulator showing Restaurant Detail screen without reviews or restaurant photos

图 21.14:iOS 模拟器显示没有评论或餐厅照片的餐厅详情屏幕

RestaurantItem 有一个属性,restaurantID,用于存储餐厅标识符。当 RestaurantDataManager 加载 JSON 文件并创建 RestaurantItem 实例数组时,每个餐厅的标识符从 JSON 文件中获取并存储在 restaurantID 属性中。

在下一节中,您将更新 ReviewFormViewController 以将带有餐厅标识符的评论保存到持久存储中。

更新 ReviewFormViewController 类以保存评论

当点击 保存 按钮时,onSaveTapped(_:) 方法将评论保存到持久存储中。按照以下步骤操作:

  1. 在项目导航器中点击 ReviewFormViewController 文件。在输出声明之前向 ReviewFormViewController 类添加以下属性以存储餐厅标识符:

    var selectedRestaurantID: Int?
    
  2. 创建一个 private 扩展,将 onSaveTapped(_:) 方法移动到其中,并按以下方式修改:

    private extension ReviewFormViewController {
       @IBAction func onSaveTapped(_ sender: Any) {
    onSaveTapped(_:) will now create a ReviewItem instance, assign data obtained from the CoreDataManager.shared.addReview(reviewItem) to save the review to the persistent store.
    

注意,目前没有机制将餐厅标识符传递给 ReviewFormViewController。在下一节中,你将看到如何从 RestaurantDetailViewController 获取餐厅标识符并将其传递给 ReviewFormViewController

将 RestaurantID 传递给 ReviewFormViewController 实例

ReviewFormViewController 如何获取该标识符?你必须从 RestaurantDetailViewController 将标识符值传递给 ReviewFormViewController,以便它可以保存带有该餐厅标识符的评论。正如你在 第十七章**,入门 JSONFiles 中所做的那样,你将使用转场标识符来确定哪个转场正在发生,然后实现方法在两个视图控制器之间传递标识符值。按照以下步骤操作:

  1. 打开 RestaurantDetail 故事板文件,并选择用于转到 ReviewForm 场景的转场:图 21.15:显示 Restaurant Detail 和 Review Form 屏幕之间转场的编辑区域

    图 21.15:显示 Restaurant Detail 和 Review Form 屏幕之间转场的编辑区域

  2. 在属性检查器中设置 showReview 并按 Enter 键:图 21.16:属性检查器,标识符设置为 showReview

    图 21.16:属性检查器,标识符设置为 showReview

  3. 在项目导航器中点击 RestaurantDetailViewController 文件。在 viewDidLoad() 之后添加以下代码以实现 prepare(for:sender:) 方法:

    override func prepare(for segue: 
    UIStoryboardSegue, sender: Any?) {
       if let identifier = segue.identifier {
          switch identifier  {
          case Segue.showReview.rawValue:
             showReview(segue: segue)
          default:
             print("Segue not added")
          }
       }
    }
    

    prepare(for:sender:) 方法检查是否具有 showReview 转场标识符。如果有,则在从 showReview(segue:) 转换之前执行 showReview(segue:) 方法。由于 showReview(segue:) 方法尚未实现,你将在下一节中添加它。

  4. private 扩展中添加 showReview(segue:) 方法,在 createRating() 方法之前:

    func showReview(segue: UIStoryboardSegue) {
       guard let navController = segue.destination as? 
       UINavigationController, let viewController = 
       navController.topViewController as? 
       ReviewFormViewController else {
          return
       }
       viewController.selectedRestaurantID = 
       selectedRestaurant?.restaurantID
    }
    

    这将 ReviewFormViewControllerrestaurantID 属性设置为所选餐厅的标识符。

  5. 在项目导航器中点击 ReviewFormViewController 文件。在 viewDidLoad() 方法内部添加以下代码以将餐厅标识符打印到调试区域:

    super.viewDidLoad()
    ReviewFormViewController.
    

构建并运行你的项目,设置位置,并点击 All。点击一个餐厅,然后点击 添加评论 按钮。在 评论表单 屏幕中输入评论并点击 保存

![图 21.17:iOS 模拟器显示 Review Form 屏幕的保存按钮图片

图 21.17:iOS 模拟器显示审查表单屏幕的 Save 按钮

餐厅标识符将出现在调试区域:

![图 21.18:调试区域显示餐厅标识符图片

图 21.18:调试区域显示餐厅标识符

您已成功将餐厅标识符从 RestaurantDetailViewController 传递到 ReviewFormViewController。现在,让我们为照片做同样的事情。在下一节中,您将更新 PhotoFilterViewController 以在点击 Save 按钮时将带有餐厅标识符的照片保存到持久存储中。

更新 PhotoFilterViewController 类以保存照片

使 PhotoFilterViewController 类能够将照片保存到持久存储的代码与您在 ReviewFormViewController 类中为保存评论所实现的代码类似。现在,您将更新 PhotoFilterViewController 类以在点击 Save 按钮时保存照片。按照以下步骤操作:

  1. 在项目导航器中点击 PhotoFilterViewController 文件。在 initialize() 方法之后的 private 扩展内添加以下方法:

    func saveSelectedPhoto() {
       if let mainImage = self.mainImageView.image {
          var restPhotoItem = 
          RestaurantPhotoItem()
          restPhotoItem.date = Date()
          restPhotoItem.photo = 
          mainImage.preparingThumbnail(of: 
          CGSize(width: 100, height: 100))
          if let selRestID = selectedRestaurantID 
          {
             restPhotoItem.restaurantID = 
             Int64(selRestID)
          }
          CoreDataManager.shared
          .addPhoto(restPhotoItem)
       }
       dismiss(animated: true, completion: nil)
    }
    

    请记住,mainImageViewsaveSelectedPhoto() 方法中大型图像视图的输出。首先检查 mainImageViewimage 属性是否已设置。如果是,则将图像分配给 mainImage。接下来,创建一个 RestaurantPhotoItem 实例并将其分配给 restPhotoItem,并将当前日期分配给 restPhotoItem 实例的 date 属性。使用 mainImage 实例的 preparingThumbnail(of:) 方法创建图像的较小版本,并将其分配给 restPhotoItem 实例的 photo 属性。之后,将 restPhotoItem 实例的 restaurantID 属性设置为所选餐厅的标识符。最后,调用 CoreDataManager.shared.addPhoto(_:) 方法将照片保存到持久存储,并关闭 Photo Filter 屏幕。

  2. 您需要在 onPhotoTapped(_:) 方法之后的 private 扩展中触发此方法:

    @IBAction func onSaveTapped(_ sender: Any) { 
       saveSelectedPhoto()
    }
    

    此方法将在稍后连接到 Save 按钮。

  3. onSaveTapped(_:) 方法分配给 PhotoFilter 场景文件,并点击 Photo Filter View Controller 图标在 Photo Filter View Controller Scene 中。打开连接检查器。从 onSaveTapped 动作拖动到 Save 按钮:

![图 21.19:连接检查器显示 onSaveTapped: 被分配到 Save 按钮图片

图 21.19:连接检查器显示 onSaveTapped: 被分配到 Save 按钮

在您能够保存之前,您需要将餐厅标识符传递给 PhotoFilterViewController。正如您之前所做的那样,您将使用 segue 标识符来确定哪个 segue 正在发生,然后实现方法在两个视图控制器之间传递标识符值。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantDetail故事板文件,并选择用于转到相片滤镜屏幕的切换:![图 21.20:编辑区域显示餐厅详情和相片滤镜屏幕之间的切换已选中]

    ![img/Figure_21.20_B17469.jpg]

    图 21.20:编辑区域显示餐厅详情和相片滤镜屏幕之间的切换已选中

  2. 在属性检查器中设置showPhotoFilter并按回车键:![图 21.21:属性检查器,标识符设置为显示相片滤镜]

    ![img/Figure_21.21_B17469.jpg]

    图 21.21:属性检查器,标识符设置为显示相片滤镜

  3. 在项目导航器中点击RestaurantDetailViewController文件。更新prepare(for:sender:)方法,如下所示:

    override func prepare(for segue: UIStoryboardSegue,
    sender: Any?){
       if let identifier = segue.identifier {
          switch identifier {
          case Segue.showReview.rawValue:
             showReview(segue: segue)
    prepare(for:sender:) method check to see if the segue has the showPhotoFilter segue identifier. If it does, the showPhotoFilter(segue:) method is executed prior to transitioning from the showPhotoFilter(segue:) has not been implemented yet.
    
  4. 在你的private扩展内部showReview()方法之后添加showPhotoFilter(segue:)方法:

    func showPhotoFilter(segue: UIStoryboardSegue) {
       guard let navController = segue.destination as? 
       UINavigationController, let viewController = 
       navController.topViewController as? 
       PhotoFilterViewController else {
          return
       }
       viewController.selectedRestaurantID = 
       selectedRestaurant?.restaurantID
    }
    

    这将PhotoFilterViewControllerrestaurantID属性设置为所选餐厅的标识符。

构建并运行你的项目,设置位置,并点击所有。点击一个餐厅,然后点击添加照片按钮。选择一张照片,应用滤镜,然后点击保存

![图 21.22:iOS 模拟器显示带有已选保存按钮的相片滤镜屏幕]

![img/Figure_21.22_B17469.jpg]

图 21.22:iOS 模拟器显示带有已选保存按钮的相片滤镜屏幕

照片将被保存到持久存储中,并且你将返回到餐厅详情屏幕。

在这一点上,你可以保存评论和照片。太棒了!在下一节中,你将添加代码从持久存储中加载评论和照片以在餐厅详情屏幕上显示。

在餐厅详情屏幕上显示已保存的评论和照片

RestaurantDetail.storyboard中,你会看到集合视图已经设置为在静态表格单元格中显示照片和评论。你所需要做的就是实现用于显示评论的相应视图控制器。你将从用于显示评论的视图和集合视图单元格开始。按照以下步骤操作:

  1. 在项目导航器中右键单击LetsEat文件夹,并选择Reviews

  2. 右键单击文件夹并选择新建文件

  3. iOS应该已经选中。选择Cocoa Touch 类,然后点击下一步

  4. 按照以下方式配置文件:

    ReviewCell

    UICollectionViewCell

    Swift

    点击下一步

  5. 点击项目导航器中出现的ReviewCell文件。在花括号之间输入以下代码:

    import UIKit
    class ReviewCell: UICollectionViewCell {
       @IBOutlet var titleLabel: UILabel!
       @IBOutlet var dateLabel: UILabel!
       @IBOutlet var nameLabel: UILabel!
       @IBOutlet var reviewLabel: UILabel!
       @IBOutlet var ratingsView: RatingsView!
    }
    

ReviewCell现在具有集合视图单元格中所有输出属性。让我们接下来创建ReviewsViewController。按照以下步骤操作:

  1. 右键单击Reviews文件夹,并选择新建文件

  2. iOS应该已经选中。选择Cocoa Touch 类,然后点击下一步

  3. 按照以下方式配置文件:

    ReviewsViewController

    UIViewController

    Swift

    点击下一步

  4. 点击项目导航器中出现的ReviewsViewController文件。按照以下方式修改此文件:

    import UIKit
    class ReviewsViewController: UIViewController {
       ReviewsViewController is straightforward. You have an outlet for a collection view, collectionView. selectedRestaurantID stores the restaurant identifier. reviewItems contains an array of ReviewItem instances. dateFormatter is an instance of the DateFormatter class that will be used to format the date for display. Don't worry about the errors, as you'll be typing in the implementation of the initialize() and setupDefaults() methods in the next step.Important InformationTo learn more about the `DateFormatter` class, visit this link: [`developer.apple.com/documentation/foundation/dateformatter`](https://developer.apple.com/documentation/foundation/dateformatter).
    
  5. 添加一个包含以下代码的private扩展,如图所示:

    private extension ReviewsViewController {
       func initialize() {
          setupCollectionView()
       }
       func setupCollectionView() {
          let flow = UICollectionViewFlowLayout()
          flow.sectionInset = UIEdgeInsets(top: 7,left: 7,
          bottom: 7, right: 7)
          flow.minimumInteritemSpacing = 0 
          flow.minimumLineSpacing = 7 
          flow.scrollDirection = .horizontal 
          collectionView.collectionViewLayout = flow
       }
    }
    

    private扩展包含initialize()setupCollectionView()方法的实现。initialize()只是调用setupCollectionView()setupCollectionView()用于配置集合视图的流和间距,与您之前编写的代码类似。

  6. setupCollectionView()之后添加以下方法以实现checkReviews()

    func checkReviews() {
       let viewController = self.parent as? 
       RestaurantDetailViewController
       if let restaurantID = 
       viewController?.selectedRestaurant?
       .restaurantID {
          reviewItems = 
          CoreDataManager.shared
          .fetchReviews(by: restaurantID)
          if !reviewItems.isEmpty {
             collectionView.backgroundView = nil
          } else {
             let view = NoDataView(frame: 
             CGRect(x: 0, y: 0, width: 
             collectionView.frame.width,
             height: 
             collectionView.frame.height))
          view.set(title: "Reviews", desc: 
          "There are currently no reviews")
          collectionView.backgroundView = view
          }
       }
       collectionView.reloadData()
    }
    

    此方法将检索指定餐厅标识符的所有餐厅评论。让我们分解一下:

    let viewController = self.parent as? 
    RestaurantDetailViewController
    

    此语句将RestaurantDetailViewController分配给一个临时常量viewController

    if let restaurantID = 
    viewController?.selectedRestaurant?.restaurantID { 
    

    此语句将显示在restaurantID中的餐厅的餐厅标识符分配给此语句。

    reviewItems = CoreDataManager.shared
    .fetchReviews(by: restaurantID)
    

    此语句从持久存储中获取与给定restaurantID匹配的评论数组,并将其分配给reviewItems

    if !reviewItems.isEmpty {
       collectionView.backgroundView = nil
    } else {
       let view = NoDataView(frame: 
       CGRect(x: 0, y: 0, width: 
       collectionView.frame.width,
       height: 
       collectionView.frame.height))
       view.set(title: "Reviews", desc: 
       "There are currently no reviews")
       collectionView.backgroundView = view
    }
    

    如果有关于这家餐厅的评论,则将集合视图的背景视图设置为nil;否则,创建一个NoDataView实例,将titledesc属性分别设置为"Reviews""There are currently no reviews",并将其分配给集合视图的背景视图。

    collectionView.reloadData()
    

    这段代码告诉集合视图在屏幕上重新绘制自己。

  7. 通过添加以下扩展来实现集合视图的数据源方法:

    extension ReviewsViewController: 
    UICollectionViewDataSource {
       func collectionView(_ collectionView: 
       UICollectionView, numberOfItemsInSection 
       section: Int) -> Int {
          reviewItems.count
       }
       func collectionView(_ collectionView: 
       UICollectionView, cellForItemAt indexPath: 
       IndexPath) -> UICollectionViewCell {
          let cell = collectionView
          .dequeueReusableCell 
          (withReuseIdentifier: "reviewCell", 
          for: indexPath) as! ReviewCell
          let reviewItem = reviewItems[indexPath.item] 
          cell.nameLabel.text = reviewItem.name 
          cell.titleLabel.text = reviewItem.title 
          cell.reviewLabel.text = 
          reviewItem.customerReview 
          if let reviewItemDate = reviewItem.date {
             cell.dateLabel.text = 
             dateFormatter.string(from: 
             reviewItemDate)
          }
          if let reviewItemRating = reviewItem.rating 
          {
             cell.ratingsView.rating = 
             reviewItemRating
          }
          return cell
       }
    }
    

    这与您之前所做的工作类似。集合视图中要显示的单元格数量与reviewItems数组中的项目数量相同。您使用相应的ReviewItem实例的属性设置每个单元格的内容。

  8. 通过添加以下扩展来实现集合视图的流布局代理方法:

    extension ReviewsViewController: UICollectionViewDelegateFlowLayout {
       func collectionView(_ collectionView: 
       UICollectionView, layout collectionViewLayout: 
       UICollectionViewLayout, sizeForItemAt 
       indexPath: IndexPath) -> CGSize {
          let edgeInset = 7.0
          if reviewItems.count == 1 {
             let cellWidth = 
             collectionView.frame.size.width - 
             (edgeInset * 2) 
             return CGSize(width: cellWidth, height: 
             200)
          } else {
             let cellWidth = 
             collectionView.frame.size.width - 
             (edgeInset * 3)
             return CGSize(width: cellWidth, height: 
             200)
          }
       }
    }
    

    此方法返回要显示的集合视图单元格的大小。如果reviewItems数组中只有一个项目,则单元格的宽度设置为集合视图的宽度—14 点;否则,它设置为集合视图的宽度—21 点。高度设置为200点。

ReviewsViewController现在已完成。现在,您将完成RestaurantDetail故事板文件的实现。按照以下步骤操作:

  1. 在项目导航器中点击RestaurantDetail故事板文件。选择ReviewsViewController,然后按Return图 21.23:类设置为 ReviewsViewController 的 Identity inspector

    图 21.23:类设置为 ReviewsViewController 的 Identity inspector

    场景名称将更改为Reviews View Controller Scene

  2. 选择ReviewCell,然后按Return图 21.24:类设置为 ReviewCell 的 Identity inspector

    图 21.24:类设置为 ReviewCell 的 Identity inspector

  3. 在文档大纲中选择视图。点击 Identity inspector 按钮,设置RatingsView,然后按Return图 21.25:类设置为 RatingsView 的 Identity inspector

    图 21.25:类设置为 RatingsView 的 Identity inspector

  4. 如果尚未设置,选择reviewCell:![图 21.26:属性检查器,将标识符设置为 reviewCell 图片

    图 21.26:属性检查器,将标识符设置为 reviewCell

  5. 点击连接检查器。如果尚未设置,从dateLabel出口拖动到显示的标签:![图 21.27:连接检查器显示 dateLabel 出口 图片

    图 21.27:连接检查器显示 dateLabel 出口

  6. 如果尚未设置,从nameLabel出口拖动到显示的标签:![图 21.28:连接检查器显示 nameLabel 出口 图片

    图 21.28:连接检查器显示 nameLabel 出口

  7. 如果尚未设置,从reviewLabel出口拖动到显示的标签:![图 21.29:连接检查器显示 reviewLabel 出口 图片

    图 21.29:连接检查器显示 reviewLabel 出口

  8. 如果尚未设置,从titleLabel出口拖动到显示的标签:![图 21.30:连接检查器显示 titleLabel 出口 图片

    图 21.30:连接检查器显示 titleLabel 出口

  9. 如果尚未设置,从ratingsView出口拖动到显示的评分视图:![图 21.31:连接检查器显示 ratingsView 出口 图片

    图 21.31:连接检查器显示 ratingsView 出口

  10. 选择如所示到集合视图collectionView出口,如果尚未设置:![图 21.32:连接检查器显示 collectionView 出口 图片

    图 21.32:连接检查器显示 collectionView 出口

  11. 选择到评论视图控制器图标的delegatedataSource出口:

![图 21.33:连接检查器显示 dataSource 和 delegate 出口图片

图 21.33:连接检查器显示 dataSource 和 delegate 出口

构建并运行您的应用。您应该看到之前添加的评论:

![图 21.34:iOS 模拟器显示包含评论的餐厅详情屏幕图片

图 21.34:iOS 模拟器显示包含评论的餐厅详情屏幕

用于显示评论的集合视图和集合视图单元格的视图控制器实现现在已完成,您的应用现在可以显示之前使用评论表单屏幕输入的评论。如果您有多个评论,您可以左右滑动以查看每个评论。由于每个评论都有一个评分,您可以使用它们来计算并添加一个餐厅的整体评分。让我们修改应用以执行此操作。

计算餐厅的整体评分

要执行此操作,请使用CoreDataManager。按照以下步骤操作:

  1. 在项目导航器中(在Misc文件夹中的Core Data文件夹内)点击CoreDataManager文件。在addReview(_:)方法之前添加以下方法:

    func fetchRestaurantRating(by identifier: Int) -> 
    Double { 
       let reviewItems = fetchReviews(by: identifier)
       let sum = reviewItems.reduce(0, {$0 + 
       ($1.rating ?? 0)}) 
       return sum / Double(reviewItems.count)
    }
    

    在这个方法中,从持久存储中检索特定餐厅的所有评论,并分配给reviewsreduce()方法接受一个闭包,用于将所有评论评分相加。最后,计算平均评分值并返回。

    重要信息

    你可以在以下链接中了解更多关于reduce()方法的信息:developer.apple.com/documentation/swift/array/2298686-reduce

  2. 在项目导航器中(位于RestaurantDetail文件夹内)点击RestaurantDetailViewController文件。更新createRating()方法,如下所示:

    func createRating() {
       ratingsView.isEnabled = selectedRestaurant instance's restaurantID property to restaurantID. If successful, the CoreDataManager.shared.fetchRestaurantRating() method is called, which gets all the reviews with restaurantID's restaurant identifier value, and calculates the average rating. ratingValue is then set to the average rating and used to update the ratings view's rating property, which determines the number of stars displayed in the roundedValue is then calculated from ratingValue to return a number with 1 decimal point, and is used to set the text property for overallRatingLabel.
    
  3. 当用户在viewDidLoad()方法中添加新的评论时,也需要更新总体评分:

    override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       createRating()
    }
    

    这将在关闭评论表单屏幕并重新出现餐厅详情屏幕时重新计算评分。

构建并运行你的项目,你现在应该能看到有评论的餐厅的总体评分,以及相应的星级评分,如下所示:

Figure 21.35: iOS Simulator showing Restaurant Detail screen with overall ratings

Figure 21.35: iOS Simulator showing Restaurant Detail screen with overall ratings

图 21.35:iOS 模拟器显示包含总体评分的餐厅详情屏幕

还有一件事要做,那就是添加图片评论。你的挑战是添加图片评论,并在评论集合视图下方显示它们。这样做的方式与你之前添加评论的方式非常相似。本章涵盖了你需要知道的所有内容,如果你遇到困难,请随时使用本章的完整项目文件,这些文件可以在本书的代码包的Chapter21文件夹中找到,可从github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition下载。你还可以观看本章的 CiA 视频,位于bit.ly/3o81yKK

摘要

在本章中,你学习了 Core Data 及其不同组件。你为你的应用创建了名为ReviewRestaurantPhoto的数据模型,并为你的应用创建了相应的模型对象,名为ReviewItemRestaurantPhotoItem。之后,你实现了CoreDataManager来为你的应用设置 Core Data 组件。

你更新了ReviewFormViewControllerPhotoFilterViewController,将评论和图片与餐厅标识符一起保存到持久存储中。你修改了RestaurantDetailViewController,根据餐厅标识符加载特定餐厅的评论,并在集合视图中显示它们。你还计算并显示了该餐厅的总体评分。

最后,你自己修改了RestaurantDetailViewController,根据餐厅标识符加载特定餐厅的图片,并在集合视图中显示它们。

你现在对 Core Data 的工作原理有了基本的了解。你也能够设置 Core Data 组件,并使用数据管理类在你的应用和 Core Data 组件之间启用接口。你还知道如何使用 Core Data 保存和加载评论和照片,你现在将能够在自己的应用中实现这些功能。

你已经完成了漫长旅程的终点,现在你已经完成了构建应用主要功能的工作。所有屏幕都正常工作,评论和照片都得到了持久化。做得太棒了!

这本书的第三部分到此结束。在下一部分,你将了解到苹果在 iOS 15 中引入的酷炫新功能,以及如何将它们添加到你的应用中,从下一章开始,我们将介绍如何让你的应用为苹果 Mac 做准备。

第四部分:功能

欢迎来到本书的 第四部分。在本部分中,你将实现最新的 iOS 15 功能。首先,你将修改你的应用以在 iPhone 和 iPad 上运行,并使其在 Mac 上也能运行。接下来,你将学习如何开发 SwiftUI 应用,这是为所有 Apple 平台开发应用的一种全新的方法。之后,你将学习如何使用 Swift Concurrency 实现异步和并行编程,以及如何使用 SharePlay 实现共享体验。最后,你将了解如何使用内部和外部测试人员测试你的应用,并将其上传到 App Store。

本部分包括以下章节:

  • 第二十二章**,Mac Catalyst 入门

  • 第二十三章**,SwiftUI 入门

  • 第二十四章**,Swift Concurrency 入门

  • 第二十五章**,Swift Concurrency 入门

  • 第二十六章**,测试并将你的应用提交到 App Store

到本部分结束时,你将能够在你自己的应用中实现酷炫的 iOS 15 功能。你还将能够测试并发布你自己的应用到 App Store。让我们开始吧!

第二十二章:第二十二章: 开始使用 Mac Catalyst

苹果的 Mac Catalyst 功能允许您创建 iPad 应用的 Mac 版本。这使您能够共享两个平台相同的项目和源代码,从而更容易维护。在 WWDC2021 期间,苹果宣布了对 Mac Catalyst 的更新,允许您为 Mac 添加更多功能,例如使用Command + P进行键盘导航和打印。本章将重点介绍如何使您现有的 iPhone 应用在 iPad 上运行,以便您可以创建其 Mac 版本。通过这样做,您将能够接触到超过 1 亿活跃的 Mac 用户群体。

在本章中,您将修改您的应用,使其在 iPad 和 Mac 上运行。首先,您将修复应用中的某些用户界面问题。接下来,您将学习如何使您的应用用户界面在 iPad 上工作,利用 iPad 更大的屏幕尺寸。然后,您将使用您的应用 iPad 版本来创建 Mac 版本。

到本章结束时,您将能够使您现有的 iOS 应用在所有 iOS 设备上运行良好,并且还能够从您的 iPad 应用中创建 Mac 应用。

以下内容将涵盖:

  • 修复用户界面问题

  • 在所有 iOS 设备上运行您的应用

  • 在 Mac 上运行您的应用

技术要求

您将继续在上一章中修改的LetsEat项目中工作。

本章的完成版 Xcode 项目位于本书代码包的Chapter22文件夹中,可在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际运行情况:

bit.ly/3IbY41R

让我们从对用户界面进行一些修改开始,使其看起来更好。

修复用户界面问题

您会发现的一件事是 iOS 应用永远不会真正完成。您总会找到改进和精炼应用的方法。构建并运行您的应用,并将其与应用导游中显示的设计(在第九章设置用户界面)进行比较。您将注意到,在仔细检查后,与应用导游中显示的屏幕相比,您的应用屏幕有细微的差异,需要进行更改。让我们从您的应用的探索屏幕开始:

图 22.1:iOS 模拟器显示探索屏幕

图 22.1:iOS 模拟器显示探索屏幕

探索屏幕所需的更改如下。参考数字以查看需要更改的部分:

  • 导航栏(1)在应用导游中不存在,需要将其删除。

  • 收集视图单元格(2)有尖锐的边缘。您将为单元格实现圆角,以匹配应用导游中显示的单元格。

  • 标签栏按钮是蓝色(3)。您将更改标签栏按钮颜色为红色,以匹配应用导游。

现在我们来看看你的应用程序的 位置 屏幕:

![图 22.2:iOS 模拟器显示位置屏幕图片

图 22.2:iOS 模拟器显示位置屏幕

应用程序导览中显示的 位置 屏幕顶部的较大标题缺失,你必须添加它。

如你所见,只需要进行四个小的修改,这些修改很容易实现。你将从修改 Explore 屏幕开始。按照以下步骤操作:

  1. 在项目导航器中 Explore 文件夹内的 ExploreViewController 文件中。

  2. viewDidLoad() 方法之后添加一个 viewWillAppear() 方法,并在该方法内部添加代码以隐藏导航控制器中的导航栏:

    override func viewWillAppear(_ animated: Bool) { 
       super.viewWillAppear(animated)
       navigationController?.setNavigationBarHidden(true, 
       animated: false)
    }
    

    注意,如果你将此代码添加到 viewDidLoad() 中,导航栏仅在 Explore 屏幕首次出现时隐藏,并在从 位置 屏幕或 餐厅列表 屏幕切换回 Explore 屏幕时重新出现。

  3. 要在项目导航器中 Explore 文件夹内的 View 文件夹中的 ExploreCell 文件(在 Explore 文件夹内)上圆角化集合视图单元格,并在出口声明之后添加以下方法:

    override func awakeFromNib() {
       super.awakeFromNib() 
       exploreImageView.layer.cornerRadius = 9 
       exploreImageView.layer.masksToBounds = true
    } 
    
  4. 要更改标签栏按钮的颜色,在项目导航器中点击 AppDelegate 文件,并在最后一个花括号之后添加一个包含以下方法的 private 扩展:

    private extension AppDelegate {
       func initialize() {
          setupDefaultColors()
       }
       func setupDefaultColors() { 
          UITabBar.appearance().tintColor = .systemRed
          UITabBarItem.appearance().
          setTitleTextAttributes(
          [NSAttributedString.Key.foregroundColor: 
          UIColor.systemRed], for: 
          UIControl.State.selected)
          UINavigationBar.appearance().tintColor = 
          .systemRed 
       }
    }
    

    AppDelegate 文件包含了 AppDelegate 类的声明和定义。这个类处理应用程序事件,例如,当应用程序启动、发送到后台、终止等情况。你可以在启动时在这里添加代码来配置你的应用程序。

    如你之前所做的那样,你将使用一个 initialize() 方法来调用所有其他设置方法。在这种情况下,initialize() 方法调用了 setupDefaultColors() 方法。

    setupDefaultColors() 方法将更改标签栏和导航栏中项的色调颜色为红色。它使用了 appearance() 方法,该方法为已创建或将要创建的每个标签和导航栏设置全局属性。

    重要信息

    关于 appearance() 方法的更多信息可以在这里找到:developer.apple.com/documentation/uikit/uiappearance

  5. 你必须在应用程序启动时调用 initialize() 方法,因此修改 application(_:didFinishLaunchingWithOptions:) 方法如下:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       initialize()
       return true
    }
    
  6. 在项目导航器中点击 Main 故事板文件。在 Explore 视图控制器场景 下,点击 Explore Image View。选择属性检查器,在 视图 下,将 内容模式 改为 填充宽高:![图 22.3:主故事板文件显示设置为填充宽高的 Explore Image View 图片

    图 22.3:主故事板文件显示设置为填充宽高的 Explore Image View

    这允许图像占据整个图像视图框架,并显示你在步骤 3中编码的圆角。

    构建并运行你的应用。探索屏幕应该看起来像这样:

    图 22.4:iOS 模拟器显示更新后的探索屏幕

    图 22.4:iOS 模拟器显示更新后的探索屏幕

    你会发现导航栏消失了,每个单元格的角落都变得圆润,当选择探索地图按钮图标和标题时,现在会变成红色。

  7. 接下来,你将更新LocationViewController类。在项目导航器中,点击Location文件夹内的LocationViewController文件,并修改initialize()方法,为title属性设置一个可以在导航栏中显示的标题。此代码将标题设置为Select a location,并在屏幕顶部用大号字体显示。

    构建并运行你的应用,然后点击位置按钮。位置屏幕应该看起来像这样:

图 22.5:iOS 模拟器显示更新后的位置屏幕

图 22.5:iOS 模拟器显示更新后的位置屏幕

你会在屏幕顶部看到用大号字体写的选择位置,而取消完成按钮现在也是红色的。

太好了!你已经完成了 iPhone 上应用的界面清理工作。前面提到的四个问题已经得到解决,你的应用屏幕现在看起来与应用导览中显示的屏幕完全一样。正如你所见,即使是微小的变化也能让你的应用更具视觉吸引力。

到目前为止,你一直在 iPhone 模拟器中运行你的应用。在下一节中,你将在 iPad 模拟器中运行你的应用,以查看需要哪些更改。然后你将修改你的应用,以便用户界面可以利用 iPad 更大的屏幕。

使你的应用在所有 iOS 设备上运行

在你能够从现有的 iOS 应用创建 Mac 应用之前,你需要修改用户界面以适应 iPad。为了查看你需要进行哪些更改,你将在 iPad 模拟器上构建并运行你的应用。按照以下步骤操作:

  1. 如果模拟器正在运行,请关闭它。在方案菜单中的模拟器列表中选择iPad Pro (9.7 英寸),并运行你的应用:图 22.6:选择 iPad Pro (9.7 英寸)的方案菜单

    图 22.6:选择 iPad Pro (9.7 英寸)的方案菜单

  2. iPad 模拟器将启动并如以下截图所示:

图 22.7:iPad 模拟器显示探索屏幕

图 22.7:iPad 模拟器显示探索屏幕

如你所见,探索屏幕上的集合视图自动占据整个屏幕宽度,集合视图单元格的大小与 iPhone 上的大小相同。尽管你可以为 iPhone 和 iPad 使用完全相同的用户界面,但如果你能根据每个设备进行定制会更好。

要做到这一点,您需要添加一些代码,以便您的应用程序可以识别其正在运行的设备类型。接下来,您将更新应用程序的用户界面以适应 iPad 的大屏幕,并使应用程序根据设备类型自动切换用户界面。

让我们看看如何在下一节中使您的应用程序检测其正在运行的设备类型。

识别设备类型

您需要向您的应用程序添加一些代码,以便它知道正在运行的设备。按照以下步骤操作:

  1. 右键单击Misc文件夹并选择新建文件

  2. iOS应该已经选中。选择Swift 文件然后点击下一步

  3. 将此文件命名为Device。点击Device文件出现在项目导航器中。

  4. 按照所示修改文件以创建一个Device枚举:

    import UIKit
    enum Device {
       static var isPhone: Bool {
    UIDevice.current.userInterfaceIdiom == 
          .phone
       }
       static var isPad: Bool {
    UIDevice.current.userInterfaceIdiom == 
          .pad
       }
    }
    

这里,使用枚举而不是类或结构体,因为您无法意外地创建其实例。UIDevice类表示应用程序正在运行的设备。UIDevice.current.userInterfaceIdiom返回.phone如果应用程序正在 iPhone 上运行,如果应用程序正在 iPad 上运行,则返回.pad。因此,当应用程序在 iPhone 上运行时,isPhone返回true,当应用程序在 iPad 上运行时,isPad返回true

除了设备类型之外,您还必须考虑设备方向。例如,横向模式的 iPhone 比纵向模式的 iPhone 更宽,尽管它们是同一部 iPhone。让我们在下一节中学习如何使用尺寸类别来处理设备方向。

理解尺寸类别

虽然您现在可以识别应用程序正在运行的设备类型,但您还必须考虑设备方向对用户界面的影响。由于屏幕尺寸种类繁多,无论是纵向还是横向,这可能会很具挑战性。为了使这更容易,您将使用尺寸类别而不是设备的物理分辨率。

重要信息

关于尺寸类别的更多信息,请参阅此链接:developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/

尺寸类别是自动分配给视图的特性。定义了两个类别来描述视图的高度和宽度;常规(扩展空间)和紧凑(受限空间)。让我们看看不同设备上全屏视图的尺寸类别:

Figure 22.8: 不同 iOS 设备的尺寸类别

图 22.8:不同 iOS 设备的尺寸类别

当您设计用户界面时,不仅要考虑设备类型,还要考虑尺寸类别。在下一节中,您将学习如何根据设备和尺寸类别设置集合视图单元格的大小。

更新探索屏幕

对于 探索 屏幕,假设你决定在 iPad 上显示三列,对于紧凑型宽度尺寸类显示两列,对于常规宽度尺寸类显示三列。你将添加方法来根据设备和方向设置集合视图单元格的大小。按照以下步骤操作:

  1. 在项目导航器中点击 ExploreViewController 文件,并修改 private 扩展内的 initialize() 方法,如下所示:

    func initialize() { 
       manager.fetch() 
       setupCollectionView() is not declared or defined yet. You'll do that next.
    
  2. setupCollectionView() 方法将在 initialize() 方法中使用,以向集合视图添加 UICollectionViewFlowLayout 实例:

    func setupCollectionView() {
       let flow = UICollectionViewFlowLayout()
       flow.sectionInset = UIEdgeInsets(top: 7, left: 7, 
       bottom: 7, right: 7)
       flow.minimumInteritemSpacing = 0
       flow.minimumLineSpacing = 7
       collectionView.collectionViewLayout = flow
    }
    

    此方法创建了一个 UICollectionViewFlowLayout 类的实例,将集合视图的所有边缘内边距设置为 7 点,将最小项目间距设置为 0 点,将最小行间距设置为 7 点,并将其分配给集合视图。记住,你最初使用大小检查器为集合视图设置了这些值,如第十章**,构建用户界面

  3. 在闭合花括号之后添加一个扩展,包含将设置集合视图单元格大小和集合视图部分标题的方法:

    extension ExploreViewController: UICollectionViewDelegateFlowLayout {
       func collectionView(_ collectionView: 
       UICollectionView, layout collectionViewLayout: 
       UICollectionViewLayout, sizeForItemAt indexPath: 
       IndexPath) -> CGSize {
          var columns: CGFloat = 2 
          if Device.isPad ||
          (traitCollection.horizontalSizeClass !=  
          .compact) {
             columns = 3
          } 
          let viewWidth = collectionView.frame.size.width
          let inset = 7.0 
          let contentWidth = viewWidth - inset * 
          (columns + 1)
          let cellWidth = contentWidth / columns
          let cellHeight = cellWidth
          return CGSize(width: cellWidth, height: 
          cellHeight)
       }
       func collectionView(_ collectionView: 
       UICollectionView, layout collectionViewLayout: 
       UICollectionViewLayout, 
       referenceSizeForHeaderInSection section: Int) -> 
       CGSize {
          return CGSize(width: collectionView.frame.width,
          height: 100)
       }
    }
    

    这些方法在 UICollectionViewDelegateFlowLayout 协议中声明,并定义了集合视图中的项目大小和间距。它们将覆盖大小检查器中的设置。让我们来分解它们:

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    

    此方法返回一个 CGSize 实例,集合视图单元格的大小应设置为。

    var columns: CGFloat = 2
    

    columns 变量决定了屏幕上显示的列数,并且最初被设置为 2

    if Device.isPad || (traitCollection.horizontalSizeClass != .compact) {
    

    检查应用是否正在 iPad 上运行或 horizontalSizeClass 属性不是 .compact

    columns = 3
    

    如果应用正在 iPad 上运行或水平尺寸类不是 .compact,则将 columns 设置为 3

    let viewWidth = collectionView.frame.size.width
    

    获取屏幕宽度并将其分配给 viewWidth

    let inset = 7.0
    let contentWidth = viewWidth - inset * (columns + 1)
    

    减去边缘内边距所占用的空间,以便确定单元格大小。

    let cellWidth = contentWidth / columns 
    

    通过将 contentWidth 除以 columns 来获取单元格的宽度,并将其分配给 cellWidth

    let cellHeight = cellWidth
    

    将单元格的高度设置为与单元格的宽度相同。

    return CGSize(width: cellWidth, height: cellHeight)
    }
    

    返回单元格大小。

假设你正在 iPhone 13 Pro Max 上以竖屏模式运行。columns 被设置为 2viewWidth 被分配为 iPhone 屏幕的宽度,即 414 点。contentWidth 被设置为 414 - (7 x 3) = 393cellWidth被设置为contentWidth/columns=196.5,而 cellHeight被设置为cellWidth,因此返回的 CGSize将是(196.5, 196.5)`,使得一行可以容纳两个单元格。

当将相同的 iPhone 旋转到横屏模式时,columns 设置为 3viewWidth 将被分配为 iPhone 屏幕的高度,即 896 点。contentWidth 被设置为 896 - (7 x 4) = 868cellWidth被设置为contentWidth/columns=289.3,而 cellHeight被设置为cellWidth,因此返回的 CGSize将是(289.3, 289.3)`,使得三行可以容纳三个单元格。

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 

此方法返回集合视图部分标题应设置的大小。

return CGSize(width: collectionView.frame.width, height: 100)

集合视图部分标题的宽度将取决于设备方向,但高度始终为100

在 iPad 模拟器上构建并运行你的应用。你应该看到显示三列:

![图片 22.9:iPad 模拟器显示更新后的探索屏幕图片 22.09

图 22.9:iPad 模拟器显示更新后的探索屏幕

在 iPhone 13 Pro Max 模拟器上构建并运行你的应用,你应该看到显示两列:

![图片 22.10:iPhone 13 Pro Max 模拟器显示横幅模式下的更新后的探索屏幕图片 22.10

图 22.10:iPhone 13 Pro Max 模拟器显示横幅模式下的更新后的探索屏幕

在模拟器菜单中选择设备 | 向左旋转,你会看到显示三列:

![图片 22.11:iPhone 13 Pro Max 模拟器显示横幅模式下的更新后的探索屏幕图片 22.11

图 22.11:iPhone 13 Pro Max 模拟器显示横幅模式下的更新后的探索屏幕

在模拟器菜单中选择设备 | 向右旋转以返回垂直方向。

你已经在下一节中完成了对RestaurantListViewController类的修改。

更新餐厅列表屏幕

你已经修改了探索屏幕以自动适应应用运行的设备。现在你将为餐厅列表屏幕做同样的操作。如果你在 iPad 模拟器上构建并运行,这是餐厅列表屏幕的样子:

![图片 22.12:iPad 模拟器显示餐厅列表屏幕图片 22.12

图 22.12:iPad 模拟器显示餐厅列表屏幕

如你所见,只有两列,它们之间有一个很大的空白区域。假设你希望在 iPad 上显示三列,一个列用于紧凑型宽度尺寸类,两个列用于常规宽度尺寸类。按照以下步骤操作:

  1. 在项目导航器中点击Restaurants文件夹内的RestaurantListViewController文件。在扩展中的所有其他代码之前,在private扩展中创建一个initialize()方法:

    func initialize() { 
       createData() 
       setupTitle() 
       setupCollectionView()
    }
    

    createData()setupTitle()方法都在viewDidAppear()中被调用,但稍后你会修改viewDidAppear()以调用initialize()。你会看到一个错误,因为setupCollectionView()方法尚未声明或定义。

  2. initialize()方法之后,在private扩展中声明并定义setupCollectionView()方法:

    func setupCollectionView() {
       let flow = UICollectionViewFlowLayout()
       flow.sectionInset = UIEdgeInsets(top: 7, left: 7, 
       bottom: 7, right: 7)
       flow.minimumInteritemSpacing = 0
       flow.minimumLineSpacing = 7
       collectionView.collectionViewLayout = flow
    }
    

    就像之前一样,setupCollectionView()创建了一个UICollectionViewFlowLayout类的实例,配置它,并将其分配给集合视图。

  3. 在闭合花括号之后添加一个包含UICollectionViewDelegateFlowLayout方法的扩展:

    extension RestaurantListViewController: 
    UICollectionViewDelegateFlowLayout {
       func collectionView(_ collectionView: 
       UICollectionView, layout collectionViewLayout: 
       UICollectionViewLayout, sizeForItemAt indexPath: 
       IndexPath) -> CGSize {
          var columns: CGFloat = 0 
          if Device.isPad { 
             columns = 3
          } else {
             columns = 
             traitCollection.horizontalSizeClass 
             == .compact ? 1 : 2 
          }
          let viewWidth = collectionView.frame.size.width 
          let inset = 7.0
          let contentWidth = viewWidth - inset * 
          (columns + 1)
          let cellWidth = contentWidth / columns
          let cellHeight = 312.0
          return CGSize(width: cellWidth, height: 
          cellHeight)
       }
    }
    

    这里实现的collectionView(_:layout:sizeForItemAt:)方法几乎与ExploreViewController类中的实现完全相同,但cellHeight被设置为312点,而不是设置为cellWidth。请注意,如果你没有在 iPad 上运行你的应用,columns将设置为1用于紧凑宽度尺寸类,并设置为2用于常规宽度尺寸类。

  4. 通过移除对createData()setupTitle()方法的调用,并添加对initialize()方法的调用来更新viewDidAppear()

    override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       initialize()
    }
    

    在 iPad 模拟器上构建和运行你的应用,然后转到餐厅列表屏幕,如图所示:

图 22.13:iPad 模拟器显示更新后的餐厅列表屏幕

图 22.13:iPad 模拟器显示更新后的餐厅列表屏幕

现在有三个列,宽白间隙消失了。现在在 iPhone 13 Pro Max 模拟器上构建和运行你的应用。餐厅列表屏幕应显示单列:

图 22.14:iPhone 13 Pro Max 模拟器显示横幅模式的更新后的餐厅列表屏幕

图 22.14:iPhone 13 Pro Max 模拟器显示横幅模式的更新后的餐厅列表屏幕

在模拟器菜单中选择设备 | 向左旋转,你应该看到两列:

图 22.15:iPhone 13 Pro Max 模拟器显示横幅模式的更新后的餐厅列表屏幕

图 22.15:iPhone 13 Pro Max 模拟器显示横幅模式的更新后的餐厅列表屏幕

在模拟器菜单中选择设备 | 向右旋转,然后退出模拟器。

探索屏幕和餐厅列表屏幕已更新,现在你的应用在 iPad 上看起来很好。现在它是一个完美的候选者,可以制作成 Mac 应用。让我们看看如何在下一节中从现有的 iPad 应用构建 Mac 应用。

更新应用以在 macOS 上运行

你已经修改了你的应用屏幕,使其在所有 iOS 设备上都能良好运行。现在你将学习如何让你的应用在 Mac 上运行。

苹果在 WWDC2021 期间更新了 Mac Catalyst,这使得从现有的 iPad 应用构建具有 Mac 特定优化的 Mac 应用成为可能。正如你将看到的,这两个应用将共享相同的项目和源代码。

重要信息

观看以下链接中的视频,了解苹果在 WWDC2021 期间宣布的 Mac Catalyst 的最新更新:developer.apple.com/videos/play/wwdc2021/10052/

关于 Mac Catalyst 的更多信息可在developer.apple.com/mac-catalyst/找到。

在开始之前,请注意,这仅在您拥有免费或付费的 Apple 开发者账户时才有效。如果您使用从 GitHub 下载的Chapter24文件夹中的项目文件github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition,您必须为您的应用设置开发团队,以便它能在您的 Mac 上运行。按照以下步骤操作:

  1. 在项目导航器中选择您的项目:图 22.16:项目导航器显示选中的 LetsEat 项目

    图 22.16:项目导航器显示选中的 LetsEat 项目

  2. 通用选项卡中,勾选Mac复选框:图 22.17:显示在通用面板中勾选的 Mac 复选框

    图 22.17:显示在通用面板中勾选的 Mac 复选框

  3. 启用 Mac 支持?对话框中,点击启用图 22.18:启用 Mac 支持?对话框

    图 22.18:启用 Mac 支持?对话框

  4. 注意现在Mac复选框已被勾选:图 22.19:显示已勾选 Mac 复选框的编辑区域

    图 22.19:显示已勾选 Mac 复选框的编辑区域

    您的应用将重新编译以在您的 Mac 上运行。注意显示“专为 iPad 设计”运行目标复选框。如果您拥有 Apple Silicon Mac,您可以选择此目标以在您的 Mac 上本地运行未修改的 iPad 应用。

  5. 您的 Mac 已被设置为运行目标。构建并运行您的应用。

  6. 如果您的项目构建失败,请点击问题导航器按钮并检查错误信息:图 22.20:问题导航器显示错误信息

    图 22.20:问题导航器显示错误信息

    如果您看到这里显示的错误,这是因为您需要一个免费或付费的开发者账户才能在真实硬件上运行您的应用。

  7. 检查您的开发者账户是否已添加到 Xcode 的Xcode | 首选项 | 账户中。

    小贴士

    第一章,熟悉 Xcode中介绍了如何将您的开发者账户添加到 Xcode 中。

  8. 点击签名与能力选项卡。在团队下拉菜单中选择您的付费或免费开发者账户:图 22.21:显示在签名与能力选项卡中团队下拉菜单的编辑区域

    图 22.21:显示在签名与能力选项卡中团队下拉菜单的编辑区域

  9. 再次构建和运行,您应该能看到您的应用在您的 Mac 上运行:

图 22.22:LetsEat Mac 应用

图 22.22:LetsEat Mac 应用

您的应用现在已在您的 Mac 上运行!太棒了!

如果您仍然看到错误,请尝试将Bundle Identifier的值更改为一个唯一的值,并首先在您的 iOS 设备上运行您的应用。

你还需要做一些额外的工作来使其成为一个真正优秀的 Mac 应用程序,但这超出了本书的范围。苹果公司有一个非常棒的教程,介绍了如何做到这一点,链接如下:developer.apple.com/tutorials/mac-catalyst

摘要

在本章中,你学习了如何将现有的 iOS 应用程序构建成一个 Mac 应用程序。

你从优化应用程序在 iPhone 上的用户界面开始。接下来,你添加了一些代码,使你的应用程序能够检测它正在运行的设备,并修改了应用程序的屏幕,使其能够在所有 iOS 设备上运行。最后,你使用 Mac Catalyst 从你的 iPad 应用程序构建了一个 Mac 应用程序。你的应用程序现在在 iPhone、iPad 和 Mac 上都运行得很好。

现在,你能够让你的现有 iPhone 应用程序在 iPad 上运行良好,并且也可以从你的 iPad 应用程序中创建 Mac 应用程序。正如你所看到的,一旦你有一个 iPhone 应用程序,你就可以相对轻松地让它同时在 iPad 和 Mac 上运行。

在下一章中,你将学习一种全新的构建应用程序的方法,使用 SwiftUI,这是一种为任何苹果平台编写应用程序的现代方式。

第二十三章:第二十三章:SwiftUI 入门

在前面的章节中,您使用故事板为 Let's Eat 应用程序创建了 用户界面UI)。这个过程涉及将表示视图的对象拖动到故事板中,在视图控制器文件中创建输出,并将两者连接起来。

本章将重点介绍 SwiftUI,这是一种简单且创新的方法,可以在所有 Apple 平台上创建应用程序。SwiftUI 使用声明性的 Swift 语法来指定用户界面,而不是使用故事板,并与新的 Xcode 设计工具协同工作,以保持代码和设计的同步。动态类型、深色模式、本地化和无障碍功能都自动支持。

在本章中,您将使用 SwiftUI 构建一个简化版的 Let's Eat 应用程序。此应用程序将仅包含您一直在工作的 LetsEat 项目。您将创建一个新的 SwiftUI Xcode 项目。

您将首先添加并配置 SwiftUI 视图,通过添加和配置一个用于 餐厅详情 屏幕的地图视图,将 UIKit 和 SwiftUI 视图一起创建。最后,您将创建 餐厅详情 屏幕。

到本章结束时,您将学习如何构建一个 SwiftUI 应用程序,该应用程序可以读取模型对象,以列表形式展示它们,并允许导航到包含地图视图的第二屏幕。然后您可以为您的项目实现此功能。

以下内容将涵盖:

  • 创建 SwiftUI Xcode 项目

  • 创建 餐厅列表 屏幕

  • 添加模型对象和配置导航

  • 使用 UIKit 和 SwiftUI 视图一起

  • 创建餐厅详情屏幕

技术要求

您将为本章创建一个新的 SwiftUI Xcode 项目。

本章节的资源文件和完成的 Xcode 项目位于本书代码包的 Chapter23 文件夹中,您可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频,以查看代码的实际运行效果:

bit.ly/3DnHuIN

让我们从下一节开始创建一个新的 SwiftUI Xcode 项目,用于您的 SwiftUI 应用程序。

创建 SwiftUI Xcode 项目

创建 SwiftUI Xcode 项目的方式与常规 Xcode 项目相同,但您需要配置它以使用 SwiftUI 而不是故事板。正如您将看到的,用户界面完全由代码生成,您在修改代码时可以立即看到用户界面的变化。

重要信息

您可以在 developer.apple.com/videos/play/wwdc2020/10119 上观看 Apple 在 WWDC 2020 上的 SwiftUI 演示视频。

您可以在 developer.apple.com/videos/play/wwdc2021/10018/ 上观看 WWDC 2021 中 SwiftUI 新功能的视频。

您可以在网上找到 Apple 官方的 SwiftUI 文档,网址为developer.apple.com/xcode/swiftui/

让我们从创建一个新的 SwiftUI Xcode 项目开始。按照以下步骤操作:

  1. 创建一个新的 Xcode 项目。

  2. 点击iOS。选择App模板,然后点击下一步图 23.1:选择 iOS App 模板的项目模板屏幕

    图 23.1:选择 iOS App 模板的项目模板屏幕

  3. The LetsEatSwiftUI

  4. 界面: SwiftUI

    其他设置应该已经设置好了。确保所有复选框都没有勾选。完成后点击下一步

  5. 选择保存LetsEatSwiftUI项目的位置并点击创建

  6. 您的项目将显示在屏幕上,项目导航器中选中了ContentView文件。您将在编辑器区域的左侧看到此文件的内容,以及包含预览的画布在右侧:图 23.3:显示 LetsEatSwiftUI 项目的 Xcode

    图 23.3:显示 LetsEatSwiftUI 项目的 Xcode

  7. ContentView文件包含将生成您应用初始视图的代码。点击方案菜单并选择iPhone SE(第二代)以便使用iPhone SE(第二代)的屏幕预览视图:图 23.4:选择 iPhone SE(第二代)的方案菜单

    图 23.4:选择 iPhone SE(第二代)的方案菜单

  8. 在画布中点击简历按钮以生成预览:图 23.5:显示简历按钮的画布

    图 23.5:显示简历按钮的画布

  9. 确认您的应用预览显示在画布中:图 23.6:显示应用预览的画布

    图 23.6:显示应用预览的画布

    如果画布不可见,从调整编辑器选项菜单中选择画布以显示它。如果您使用的是 MacBook,您可以在触摸板上使用捏合手势来调整模拟图像的大小。

  10. 如果您需要更多的工作空间,点击导航器和编辑器按钮以隐藏导航器和编辑器区域,并将编辑器区域中的边框拖动以调整画布大小:

图 23.7:显示导航按钮、编辑按钮和边框的 Xcode 界面

图 23.7:显示导航按钮、编辑按钮和边框的 Xcode 界面

现在,让我们看看ContentView文件。此文件包含两个结构,ContentViewContentView_PreviewsContentView结构描述了视图的内容和布局,并符合View协议。ContentView_Previews结构声明了ContentView结构的预览。预览在画布中显示。

要查看此操作的实际效果,将Hello, World!文本更改为Lets Eat,如图所示:

struct ContentView: View {
   var body: some View { 
      Text("Lets Eat").padding()
   }
}

画布中的预览会更新以反映您的更改:

图 23.8:显示带有文本视图的应用预览的画布

图 23.8:画布显示带有文本视图的应用预览

您已成功创建了您的第一个 SwiftUI 项目!现在让我们创建“餐厅列表”屏幕,从一个将显示特定餐厅数据的视图开始。

创建餐厅列表屏幕

当使用故事板时,您使用属性检查器修改视图的属性。在 SwiftUI 中,您可以修改代码或画布中的预览。如您所见,更改ContentView文件中的代码将立即更新预览,而修改预览将更新代码。

让我们自定义ContentView结构以显示特定餐厅的数据。按照以下步骤操作:

  1. 点击库按钮。在过滤器字段中输入tex,然后拖动一个Let's Eat文本:![图 23.9:库中要拖动的文本对象 图片

    图 23.9:库中要拖动的文本对象

  2. Xcode 已自动为这个文本视图向ContentView文件中添加了代码。确认您的代码看起来像这样:

    struct ContentView: View {
       var body: some View { 
          "Lets Eat" string, and both text views are enclosed in a VStack view. A VStack view contains subviews that are arranged vertically, and it is similar to a vertically oriented stack view.
    
  3. 您将使用波士顿的 Tap Trailhouse 餐厅作为示例数据。修改VStack视图中的文本视图,以显示 Tap Trailhouse 餐厅提供的名称和菜系:

    struct ContentView: View {
       var body: some View { 
          VStack {
             Text("The Tap Trailhouse").padding()
             Text("Brewery, Burgers, American")
          }
       }
    }
    
  4. 确认更改已反映在预览中:![图 23.10:应用预览显示 Tap Trailhouse 的名称和菜系 图片

    图 23.10:应用预览显示 Tap Trailhouse 的名称和菜系

  5. 您将使用 SwiftUI 图像视图来显示餐厅的照片。按照所示修改您的代码以将图像视图添加到您的VStack视图中:

    struct ContentView: View {
       var body: some View { 
          VStack {
             Text("The Tap Trailhouse").padding()
             Text("Brewery, Burgers, American")
             systemName. This parameter allows you to choose one of the images in Apple's SF Symbols library. You'll replace this SF Symbols image with a photo later.Important InformationYou can learn more about the SF Symbols library here: [`developer.apple.com/sf-symbols/`](https://developer.apple.com/sf-symbols/).
    
  6. 确认您的画布现在显示两个文本视图和一个图像视图,如下所示:![图 23.11:应用预览显示两个文本视图和一个图像视图 图片

    图 23.11:应用预览显示两个文本视图和一个图像视图

  7. 要更改文本的外观,您使用修饰符而不是属性检查器。这些是改变对象外观或行为的方法。按照所示更新您的代码以设置文本视图的样式和颜色:

    struct ContentView: View {
       var body: some View { 
          VStack {
             Text("The Tap Trailhouse")
                .font(.headline)
             Text("Brewery, Burgers, American")
                .font(.subheadline)
                .foregroundColor(.secondary)
             Image(systemName: "photo")
          }
       }
    }
    

    注意预览中文本的变化。

  8. 为了确保您的视图保持在屏幕中间,您将在HStack视图中嵌入它,并在两侧添加Spacer对象。HStack视图包含水平排列的子视图,它类似于水平方向的堆叠视图。Spacer对象是一个在HStack视图中水平扩展的灵活空间。在您的VStack视图中Command + 点击并从弹出菜单中选择嵌入到 HStack 中:![图 23.12:编辑区域显示已选择嵌入到 HStack 的弹出菜单 图片

    图 23.12:编辑区域显示已选择嵌入到 HStack 的弹出菜单

  9. 确认您的代码看起来像这样:

    struct ContentView: View {
       var body: some View {
          HStack {
             VStack {
                Text("The Tap Trailhouse")
                   .font(.headline)
                Text("Brewery, Burgers, American")
                   .font(.subheadline)
                   .foregroundColor(.secondary)
                Image(systemName: "photo")
             }
          }
       }
    }
    
  10. 按照所示在HStack视图中添加两个Spacer对象以在屏幕上水平居中视图:

    HStack {
       Spacer()
       VStack {
          Text("The Tap Trailhouse")
             .font(.headline)
          Text("Brewery, Burgers, American")
             .font(.subheadline)
             .foregroundColor(.secondary)
          Image(systemName: "photo")
       }
       Spacer()
    }
    

你的视图现在已完成。你将在下一节中将此视图用作 Restaurant List 屏幕中的单元格。

添加模型对象和配置导航

现在你有一个可以用来显示餐厅详细信息的视图。你将使用此视图作为 SwiftUI 列表中的单元格,这是一个容器,用于以单列形式呈现数据。你还将配置模型对象以填充此列表。按照以下步骤操作:

  1. HStack 视图上按 Command + click 并选择 Embed in List 以在画布中显示包含五个单元格的列表:图 23.13:显示已选择“嵌入列表”的弹出菜单的编辑区域

    图 23.13:显示已选择“嵌入列表”的弹出菜单的编辑区域

  2. 确保你的代码看起来像这样:

    struct ContentView: View {
       var body: some View {
          HStack view is no longer needed. Note that no delegates and data sources are required to display data in the list.
    
  3. 打开你从 github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition 下载的代码包中的 Chapter23 文件夹中的 resources 文件夹。将 RestaurantItem.swift 文件拖到项目导航器中,并在提示时点击 Finish 以将其添加到你的项目中。

  4. 在项目导航器中点击 RestaurantItem 文件,你应该在其中看到以下代码:

    import Foundation
    import MapKit
    struct RestaurantItem: Identifiable {
       var id = UUID()
       var name: String
       var address: String
       var city: String
       var cuisines: [String] = []
       var lat: CLLocationDegrees
       var long: CLLocationDegrees
       var imageURLString: String
       var title: String {
          return name
       }
       var subtitle: String {
          if cuisines.isEmpty { return "" }
          else if cuisines.count == 1 { return 
          cuisines.first! }
          else { return cuisines.joined(
          separator: ", ")}
       }
    }
    let testData = [
    RestaurantItem(name: "The Tap Trailhouse", address: "17 Union St", city: "Boston", cuisines: ["Brewery","Burgers","American"], lat: 42.360847, long: -71.056819, imageURLString: "https://resizer.otstatic.com/v2/profiles/legacy/145237.jpg"),
    RestaurantItem(name: "o ya", address: "9 East Street", city: "Boston", cuisines: ["Japanese","Sushi","Int'l"], lat: 42.351353, long: -71.056941, imageURLString: "https://resizer.otstatic.com/v2/profiles/legacy/28066"),
    RestaurantItem(name: "Skipjack's Boston", address: "199 Clarendon St.", city: "Boston", cuisines: ["American", "Burgers","Brewery"], lat: 42.349887, long: -71.07484, imageURLString: "https://resizer.otstatic.com/v2/profiles/legacy/11656"),
    RestaurantItem(name: "The Elephant Walk", address: "900 Beacon Street", city: "Boston", cuisines: ["Panasian", "Vietnamese","Int'l"], lat: 42.346541, long: -71.105827, imageURLString: "https://resizer.otstatic.com/v2/profiles/legacy/1635"),
    RestaurantItem(name: "Metropolis Cafe", address: "584 Tremont Street", city: "Boston", cuisines: ["Mediterranean", "Int'l","Tapas"], lat: 42.3432, long: -71.0727, imageURLString: "https://resizer.otstatic.com/v2/profiles/legacy/2829")
    ]
    

    RestaurantItem 文件包含一个结构 RestaurantItem 和一个数组 testData

    RestaurantItem 结构与你在 LetsEat 项目中使用的 RestaurantItem 类类似。要在列表中使用此结构,你必须使其符合 Identifiable 协议。此协议指定列表项必须有一个 id 属性,可以识别特定项。每个 RestaurantItem 实例在创建时都会分配一个 UUID 实例,以确保每个 id 都是唯一的。

    重要信息

    你可以在此链接中了解更多关于 Identifiable 协议的信息:developer.apple.com/documentation/swift/identifiable

    testData 是一个包含五个 RestaurantItem 实例的数组,代表波士顿地区的五家餐厅。它执行与本书早期章节中使用的 JSON 文件相同的功能。

  5. 在项目导航器中点击 ContentView 文件。在 ContentView 结构的开头大括号后添加一个 restaurantItems 属性来保存列表的数据。

    struct ContentView: View {
       var restaurantItems: [RestaurantItem] = []
       var body: some View {
    
  6. 按照所示修改你的代码,用测试数据填充你的列表,并在每个单元格中显示餐厅的数据:

    struct ContentView: View {
       var restaurantItems: [RestaurantItem] = []
       var body: some View {
          List(ContentView structure stores an array of RestaurantItem instances in the restaurantItems property. This array is passed to the list. For every item in the restaurantItems array, a view is created and assigned with data from the item's properties. The image for each restaurant is downloaded from the URL stored in the item's imageURLString property, and displayed using the new AsyncImage view introduced in iOS 15\. Since there are five items in the array, five views appear in the canvas. Important InformationYou can learn more about the `AsyncImage` view at this link:[`developer.apple.com/documentation/swiftui/asyncimage`](https://developer.apple.com/documentation/swiftui/asyncimage)The `ContentView_Previews` structure passes in the `testData` array (stored in the `RestaurantItem` file) to the `ContentView` structure, which is then used to populate the view.
    
  7. 当你对代码进行重大更改时,画布的自动更新会暂停。如果需要,点击 Resume 按钮以继续。请注意,单元格大小已更改以适应餐厅图片的大小。

接下来,你将实现导航,以便当单元格被点击时,会显示一个第二屏幕,显示特定餐厅的详细信息。按照以下步骤操作:

  1. 按照所示修改你的代码,将你的列表包裹在之前在应用中使用的 UINavigation 类中。

  2. 添加一个修改器以设置列表的 title 属性,在屏幕顶部显示 Boston, MA

          .mask(RoundedRectangle(cornerRadius: 9))
       }
       Spacer()
    }.navigationTitle("Boston, MA")    
    
  3. 将单元格包裹在 destination 属性中,该属性指定了单元格被点击时呈现的视图。目前指定的视图是一个显示餐厅名称的文本视图。

    使用 .fixedSize() 修改器确保文本不会被截断。

  4. 注意画布中的列表已自动显示展开箭头:![图 23.14:应用预览显示展开箭头 图片

    图 23.14:应用预览显示展开箭头

  5. 要在应用中查看它是否按预期工作,请点击画布中的 实时预览 按钮:![图 23.15:画布显示实时预览按钮 图片

    图 23.15:画布显示实时预览按钮

  6. 点击预览中的任何单元格以显示包含被点击餐厅名称的文本:![图 23.16:应用预览显示已选择的单元格 图片

    图 23.16:应用预览显示已选择的单元格

    这是一种确保你的列表按预期工作的好方法。

  7. 视图代码开始看起来杂乱,因此你将单元格提取到其自己的单独视图中。Command + click NavigationLink 视图并选择 提取子视图:![图 23.17:编辑区域显示已选择“提取子视图”的弹出菜单 图片

    图 23.17:编辑区域显示已选择“提取子视图”的弹出菜单

  8. 所有单元格的视图代码都已移动到名为 ExtractedView 的单独视图中:![图 23.18:编辑区域显示突出显示的提取视图名称 图片

    图 23.18:编辑区域显示突出显示的提取视图名称

  9. 更改方法调用和提取视图的名称为 RestaurantCell。你的代码应该看起来像这样:

       var body: some View {
          NavigationView { 
             List(restaurantItems) { restaurantItem in
                RestaurantCell()
             }.navigationTitle("Boston, MA")
          }
       }
    }
    struct ContentView_Previews: PreviewProvider {
       static var previews: some View {
          ContentView(restaurantItems: testData)
       }
    }
    struct RestaurantCell: View {
       var body: some View {
          NavigationLink(destination:
    

    不要担心错误,你将在下一步中修复它。

  10. RestaurantCell 视图中添加一个属性以保存 RestaurantItem 实例:

    struct RestaurantCell: View {
       var restaurantItem: RestaurantItem
    
  11. ContentView 结构中添加代码以将 RestaurantItem 实例传递给 RestaurantCell 视图,如下所示:

    struct ContentView: View {
       var restaurantItems: [RestaurantItem] = []
       var body: some View {
          NavigationView {
             List(restaurantItems) { restaurantItem in 
                RestaurantCell(restaurantItem: 
                restaurantItem)
             }.navigationTitle("Boston, MA"
          }
       }
    }
    
  12. 确认预览仍然按预期工作。

你已经完成了 UIKit 和 SwiftUI 视图的实现,以创建一个你将在 餐厅详情 屏幕中使用的地图视图。

使用 UIKit 和 SwiftUI 视图一起

到目前为止,你已经创建了 餐厅列表 屏幕并且点击此屏幕中的每个单元格都会在第二个屏幕上显示餐厅的名称。你将修改你的应用,以便在点击 餐厅列表 屏幕中的单元格时显示 餐厅详情 屏幕但在此之前,你将创建一个显示地图的 SwiftUI 视图。

当使用故事板时,你所需要做的就是从库中拖动一个地图视图到故事板中的视图。SwiftUI 没有内置的地图视图,但你可以使用与故事板中相同的地图视图来渲染地图。实际上,你可以通过将它们包裹在一个符合 UIViewRepresentable 协议的 SwiftUI 视图中来使用任何视图子类。现在让我们创建一个可以显示地图视图的自定义视图。按照以下步骤操作:

  1. 选择 文件 | 新建 | 文件 以打开模板选择器。

  2. iOS 应该已经选中。在 用户界面 部分,点击 SwiftUI 视图 并点击 下一步图 23.19:选择 SwiftUI 视图的文件模板屏幕

    图 23.19:选择 SwiftUI 视图的文件模板屏幕

  3. 将新文件命名为 MapView 并点击,MapView 文件将出现在项目导航器中。

  4. MapView 文件中,导入 MapKit,并使 MapView 结构符合如上所示的 UIViewRepresentable 协议。不用担心出现的错误,你将在接下来的几个步骤中修复它:

    import SwiftUI
    UIViewRepresentable protocol is a wrapper that allows you to use any UIKit view in your SwiftUI view hierarchy.Important InformationTo learn more about the `UIViewRepresentable` protocol, visit this link: [`developer.apple.com/documentation/swiftui/uiviewrepresentable`](https://developer.apple.com/documentation/swiftui/uiviewrepresentable).
    
  5. 你需要两个方法来符合 UIViewRepresentable 协议:一个 makeUIView(context:) 方法用于创建一个 MKMapView,一个 updateUIView(_:context:) 方法用于配置它并响应任何更改。按照如下所示修改你的代码,用创建并返回一个空的 MKMapView 实例的 makeUIView(context:) 方法替换 body 属性:

    struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView { 
          MKMapView(frame: .zero)
       }
    }
    
  6. 按照如下所示修改你的代码,在 makeUIView(context:) 方法之后添加一个 updateUIView(_:context:) 方法。这会将地图视图的区域设置为以 The Tap Trailhouse 的位置为中心:

    func updateUIView(_ uiView: MKMapView, context: 
    Context) {
       let coordinate = CLLocationCoordinate2D
       (latitude: 42.360847, longitude: -71.056819)
       let span = MKCoordinateSpan(latitudeDelta: 
       0.001, longitudeDelta: 0.001)
       let region = MKCoordinateRegion(center: 
       coordinate, span: span)
       uiView.setRegion(region, animated: true)
    }
    

    注意,这是你在 Let's Eat 应用中为 地图 屏幕创建区域时使用的方法。

  7. 错误现在消失了,画布中出现了一个空白地图视图。这是因为预览处于静态模式,并且只渲染 SwiftUI 视图。你需要打开实时预览来查看地图。点击 实时预览 按钮,你应该会在片刻后看到以 The Tap Trailhouse 的位置为中心的波士顿地图:图 23.20:显示地图的应用预览

    图 23.20:显示地图的应用预览

    如果不起作用,请检查您的网络连接,并点击预览上方的 重试继续 按钮。

  8. 纬度和经度值目前是硬编码的。在 MapView 结构声明之后声明两个属性来保存纬度和经度值,如下所示:

    struct MapView: UIViewRepresentable {
       var lat: CLLocationDegrees
       var long: CLLocationDegrees
    
  9. 修改 updateUI(_:context:) 方法,使用这些属性而不是硬编码的值:

    func updateUIView(_ view: MKMapView, context: Context) {
       let coordinate = CLLocationCoordinate2D( 
       latitude: lat, longitude: long)
    
  10. 更新 MapView_Previews 结构,传入示例纬度和经度值,如下所示。这将生成你在预览中看到的相同地图:

    struct MapView_Previews: PreviewProvider {
       static var previews: some View {
          MapView(lat: 42.360847, long: -71.056819)
       }
    }
    
  11. 在画布中检查地图是否仍然显示(你可能需要点击 继续)。

你已经创建了一个显示餐厅位置的 SwiftUI 地图视图。现在,让我们看看如何在下一节中制作完整的餐厅详情屏幕。

完成餐厅详情屏幕

现在,你有一个显示地图的 SwiftUI 地图视图。接下来,你将创建一个新的 SwiftUI 视图来表示餐厅详情屏幕,并将地图视图添加到其中。按照以下步骤操作:

  1. 选择文件 | 新建 | 文件以打开模板选择器。

  2. iOS应该已经选中。在用户界面部分,点击SwiftUI 视图并点击下一步

  3. 将新文件命名为RestaurantDetail并点击,使RestaurantDetail文件出现在项目导航器中。

  4. 声明并定义RestaurantDetailRestaurantDetail_Previews结构,如下所示:

    import SwiftUI
    struct RestaurantDetail: View {  
       RestaurantDetail structure contains a Vstack view enclosing a map view and a second Vstack view. The map view displays a map showing the restaurant's location. The second Vstack view encloses four text views. These display the restaurant's name, cuisines, address, and city. A Spacer object pushes the first Vstack view to the top of the screen. A RestaurantItem instance is assigned to the selectedRestaurant property, and data from this instance is used to populate the RestaurantDetail structure's views.To create the preview in the canvas, the `RestaurantDetail_Previews` structure passes in the first `RestaurantItem` instance in the `testData` array. Note that the `RestaurantDetail` instance is enclosed in a `NavigationView` instance to make the navigation bar appear in the preview.
    
  5. 预览显示在餐厅文本视图上方的地图视图,但不会渲染地图。与之前一样,点击实时预览按钮。

  6. 画布现在显示带有渲染地图的餐厅详情屏幕:图 23.21:应用预览显示餐厅详情屏幕

    图 23.21:应用预览显示餐厅详情屏幕

    你已经使用 SwiftUI 完成了餐厅详情屏幕的实现。现在,你将修改餐厅列表屏幕中的列表,以便在单元格被轻触时显示餐厅详情屏幕。

  7. 在项目导航器中点击ContentView文件,并修改RestaurantCell结构的代码,以便在单元格被轻触时使用RestaurantDetail结构作为目标:

    var body: some View {
        NavigationLink(destination: 
        RestaurantDetail(selectedRestaurant: 
        restaurantItem )){
            Spacer()
    
  8. 点击画布中的实时预览按钮。在餐厅列表屏幕中轻触一行。你会看到该餐厅的餐厅详情屏幕出现:

图 23.22:应用预览显示餐厅详情屏幕

图 23.22:应用预览显示餐厅详情屏幕

如你所见,应用预览在画布中运行良好。如果你想在模拟器中运行,你需要在ContentView结构中做一个小改动。在项目导航器中点击ContentView文件,并将testdata数组分配给restaurantItems属性,如下所示:

struct ContentView: View {
   var restaurantItems: [RestaurantItem] = testData
   var body: some View {

构建并运行你的应用,它将在模拟器中显示:

图 23.23:iOS 模拟器显示餐厅列表屏幕

图 23.23:iOS 模拟器显示餐厅列表屏幕

你已经完成了构建一个简单的 SwiftUI 应用!太棒了!

摘要

在本简要的 SwiftUI 介绍中,你看到了如何使用 SwiftUI 构建简化版的 Let's Eat 应用。

你首先通过添加和配置 SwiftUI 视图来创建 UIKit 和 SwiftUI 视图,通过添加和配置用于餐厅详情屏幕的地图视图来实现这一点。最后,你创建了餐厅详情屏幕,并将你之前创建的地图视图添加到其中。

现在,你已经知道了如何使用 SwiftUI 创建一个读取模型对象、在列表中展示它们并允许导航到包含地图视图的第二屏幕的应用程序。你可以在自己的项目中实现这一点。

在下一章中,你将学习关于Swift 并发的内容,这是在 Swift 中处理异步操作的新方法。

第二十四章:第二十四章:Swift 并发入门

苹果公司在 WWDC2021 上推出了 Swift 并发,为 Swift 5.5 添加了对结构化异步和并行编程的支持。这使得您可以编写更易读、更易于理解的并发代码。

在本章中,您将学习 Swift 并发的基本概念。接下来,您将检查一个没有并发的应用,并探讨其问题。然后,您将在您的 Let's Eat 应用中使用 RestaurantListViewController 类来使用 async/await 加载餐厅图片。

到本章结束时,您将了解 Swift 并发的工作原理以及如何更新自己的应用以使用它。

本节将涵盖以下主题:

  • 理解 Swift 并发

  • 检查没有并发的应用

  • 使用 async/await 更新应用

  • 使用 async-let 提高效率

  • RestaurantListViewController 更新为使用 async/await

技术要求

您将使用一个示例应用,BreakfastMaker,来帮助您理解 Swift 并发的概念。在本章的后面部分,您将继续在您在 第二十二章**,Mac Catalyst 入门 中修改的 LetsEat 项目中工作。

本章的完成版 Xcode 项目位于本书代码包的 Chapter24 文件夹中,您可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

观看以下视频,看看代码的实际效果:

bit.ly/3d4YWH5

让我们从下一节学习 Swift 并发开始。

理解 Swift 并发

在 Swift 5.5 中,苹果添加了对以结构化方式编写异步和并行代码的支持。

异步代码允许您的应用挂起和恢复代码。这允许您的应用在执行如从互联网下载数据等操作的同时更新用户界面。

并行代码允许您的应用同时运行多个代码片段。

重要信息

您可以在 developer.apple.com/news/?id=2o3euotz 找到 WWDC2021 期间苹果所有 Swift 并发视频的链接。

您可以在 docs.swift.org/swift-book/LanguageGuide/Concurrency.html 阅读苹果的 Swift 并发文档。

为了让您了解 Swift 并发的工作原理,想象一下您正在为早餐制作一个水煮蛋三明治。以下是制作的一种方法:

  1. 将两片面包放入烤面包机中。

  2. 等待两分钟,直到面包烤熟。

  3. 在一个装有水的碗中放入一个鸡蛋,并将碗放入微波炉中。

  4. 等待六分钟,直到鸡蛋煮熟。

  5. 制作您的三明治。

总共需要八分钟。现在考虑这个事件序列。你只是盯着烤面包机和微波炉吗?你可能会在面包在烤面包机和鸡蛋在微波炉中时使用手机。换句话说,你可以在面包和鸡蛋准备的过程中做其他事情。因此,事件序列更准确地描述如下:

  1. 将两片面包放入烤面包机中。

  2. 用手机使用两分钟,直到面包烤好。

  3. 在一个装有水的碗中放一个鸡蛋,然后将碗放入微波炉中。

  4. 用手机使用六分钟,直到鸡蛋煮熟。

  5. 制作你的三明治。

在这里,你可以看到你与烤面包机和微波炉的交互可以被挂起,然后恢复,这意味着这些操作是异步的。操作仍然需要八分钟,但你可以在那段时间内做其他事情。

另一个需要考虑的因素是,你不需要等待面包烤完才能把鸡蛋放入微波炉。这意味着你可以修改步骤序列如下:

  1. 将两片面包放入烤面包机中。

  2. 当面包正在烤的时候,把一个鸡蛋放在一个装有水的碗中,然后把碗放入微波炉中。

  3. 用手机使用六分钟,直到鸡蛋煮熟。

  4. 制作你的三明治。

烤面包和煮鸡蛋现在是并行进行的,这为你节省了两分钟。太好了!但是请注意,你还有更多的事情要跟踪。

现在你已经理解了异步和并行操作的概念,让我们在下一节研究没有并发功能的应用程序所遇到的问题。

检查没有并发功能的程序

你已经看到了异步和并行操作如何帮助你更快地准备早餐,并允许你在操作过程中使用手机。现在让我们看看一个模拟准备早餐过程的示例应用程序。最初,这个应用程序没有实现并发,所以你可以看到这如何影响应用程序。按照以下步骤操作:

  1. 如果还没有这样做,请在此链接下载本书的代码包中的Chapter24文件夹:github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

  2. Chapter24文件夹中打开resources文件夹,你会看到两个文件夹,BreakfastMaker-startBreakfastMaker-complete。第一个文件夹包含你将在本章中修改的应用程序,第二个文件夹包含完成的应用程序。

  3. 打开BreakfastMaker-start文件夹,然后打开BreakfastMaker Xcode 项目。在项目导航器中点击Main故事板文件。你应该会看到四个标签和一个按钮在视图控制器场景中,如图所示:![图 24.1:显示视图控制器场景的主故事板文件

    ![img/Figure_24.01_B17469.jpg]

    图 24.1:显示视图控制器场景的主故事板文件

    应用将显示一个屏幕,显示烤面包、鸡蛋和三明治的状态,以及准备三明治所需的时间。应用还将显示一个按钮,你可以使用它来测试用户界面的响应性。

  4. 在项目导航器中点击ViewController文件。你应该在编辑器区域看到以下代码:

    import UIKit
    class ViewController: UIViewController {
       @IBOutlet var toastLabel: UILabel!
       @IBOutlet var eggLabel: UILabel!
       @IBOutlet var sandwichLabel: 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 = "Poaching egg..."
          eggLabel.text = poachEgg()
          sandwichLabel.text = makeSandwich()
          let endTime = Date().timeIntervalSince1970
          elapsedTimeLabel.text = "Elapsed time is 
          \(((endTime - startTime) * 100).rounded() 
          / 100) seconds"
       }
       func makeToast() -> String {
          sleep(2)
          return "Toast done"
       }
       func poachEgg() -> String {
          sleep(6)
          return "Egg done"
       }
       func makeSandwich() -> String {
          return "Sandwich done"
       }
       @IBAction func testButton(_ sender: UIButton) {
          print("Button tapped")
       }
    }
    

    如你所见,这段代码模拟了之前章节中描述的做早餐的过程。让我们来分解一下:

    @IBOutlet var toastLabel: UILabel!
    @IBOutlet var eggLabel: UILabel!
    @IBOutlet var sandwichLabel: UILabel!
    @IBOutlet var elapsedTimeLabel: UILabel!
    

    这些输出连接到Main故事板文件中的四个标签。当你运行应用时,这些标签将显示烤面包、鸡蛋和三明治的状态,以及显示完成整个过程所需的时间。

    override  func viewDidAppear(_ animated: Bool) {
    

    当视图控制器视图出现在屏幕上时,会调用此方法。

    let startTime = Date().timeIntervalSince1970
    

    这将startTime设置为当前时间,以便应用可以稍后计算制作三明治所需的时间。

    toastLabel.text = "Making toast..."
    

    这使得toastLabel显示文本Making toast...

    toastLabel.text = makeToast()
    

    这调用makeToast()方法,该方法等待两秒来模拟烤面包所需的时间,然后返回文本Toast done,将通过toastLabel显示。

    eggLabel.text = "Poaching egg..."
    

    这使得eggLabel显示文本Poaching egg...

    eggLabel.text = poachEgg()
    

    这调用poachEgg()方法,该方法等待六秒来模拟煮鸡蛋所需的时间,然后返回文本Egg done,将通过eggLabel显示。

    sandwichLabel.text = makeSandwich()
    

    这调用makeSandwich()方法,该方法返回文本Sandwich done,将通过sandwichLabel显示。

    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")
    }
    

    每次屏幕上的按钮被点击时,都会在调试区域显示Button tapped

构建并运行应用,并在用户界面出现时立即点击按钮:

图 24.2:iOS 模拟器运行 BreakfastMaker 应用,显示要点击的按钮

图 24.2:iOS 模拟器运行 BreakfastMaker 应用,显示要点击的按钮

你应该注意以下问题:

  • 初始点击没有效果,你只能在大约八秒后在调试区域看到Button tapped

  • Making toast...Poaching egg...永远不会显示,而Toast doneEgg done只在大约八秒后出现。

发生这种情况的原因是因为你的应用代码在makeToast()poachEgg()方法运行时没有更新用户界面。你的应用确实注册了按钮点击,但在makeToast()poachEgg()完成执行后才能处理它们并更新标签。这些问题不会给你的应用提供良好的用户体验。

你现在已经体验了没有实现并发性的应用所呈现的问题。在下一节中,你将使用async/await修改应用,使其能够在makeToast()poachEgg()方法运行时更新用户界面。

使用 async/await 更新应用

如您之前所见,当makeToast()poachEgg()方法正在运行时,应用会无响应。为了解决这个问题,您将在应用中使用async/await

在方法声明中写入async关键字表示该方法异步。这看起来是这样的:

func methodName() async -> returnType {

在方法调用之前写入await关键字标记了一个可能暂停执行的点,从而允许其他操作运行。这看起来是这样的:

await methodName()

重要信息

您可以观看 Apple 的 WWDC2021 视频,讨论 async/await:developer.apple.com/videos/play/wwdc2021/10132/

您将修改您的应用以使用async/await。这将使它能够挂起makeToast()poachEgg()方法以处理按钮点击并更新用户界面,然后之后继续执行这两个方法。按照以下步骤操作:

  1. 按照所示修改makeToast()poachEgg()方法,使它们的代码体异步:

    func makeToast() -> String {
       Task represents a unit of asynchronous work. Task has a static method, sleep(nanoseconds:), which pauses execution for a specified duration, measured in nanoseconds. Multiplying by 1,000,000,000 converts the duration to seconds. The await keyword indicates this code can be suspended to allow other code to run.
    
  2. makeToast()poachEgg()方法都会出现错误。点击任一错误图标以显示错误信息:![图 24.3:带有错误图标的错误 图片

    图 24.3:带有错误图标的错误

    错误显示是因为您在不支持并发的方法中调用异步方法。您需要将async关键字添加到方法声明中,以指示它是异步的。

  3. 对于每个方法,点击方法声明中的async关键字。

  4. 确认您完成后的代码看起来像这样:

    func makeToast() async -> String {
       try! await Task.sleep(nanoseconds: 2 * 1_000_000_000)
       return "Toast done"
    }
    func poachEgg() async -> String {
       try! await Task.sleep(nanoseconds: 6 * 1_000_000_000)
       return "Egg done"
    }
    
  5. makeToast()poachEgg()方法中的错误应该已经消失,但在viewDidAppear()方法中会出现新的错误。点击一个错误图标以查看错误信息,该信息将与您之前看到的相同。这是因为您在不支持并发的方法中调用异步方法。

  6. 点击修复按钮,将出现更多错误。

  7. 现在忽略方法声明中的那个,点击makeToast()方法调用旁边的错误图标以查看错误信息:![图 24.4:带有 makeToast()错误图标的错误 图片

    图 24.4:带有 makeToast()错误图标的错误

    此错误信息显示是因为您在调用异步函数时没有使用await

  8. 在方法调用之前点击await关键字。

  9. 对于poachEgg()方法调用旁边的错误,重复步骤 7步骤 8await关键字也将插入到poachEgg()方法调用中。

  10. 点击viewDidLoad()方法声明中的错误图标以查看错误信息:![图 24.5:带有错误图标的错误 图片

    图 24.5:带有错误图标的错误

    这个错误显示出来是因为你不能使用async关键字使viewDidAppear()方法异步,因为这个功能在父类中不存在。

  11. 为了解决这个问题,你将移除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 = "Poaching egg..."
          eggLabel.text = await poachEgg() 
          sandwichLabel.text = makeSandwich()
          let endTime = Date().timeIntervalSince1970
          elapsedTimeLabel.text = "Elapsed time is 
          \(((endTime - startTime) * 100).rounded() 
          / 100) seconds"
       }
    }
    

构建并运行应用,并在看到用户界面后立即点击按钮。注意,Button tapped现在立即出现在调试区域,并且标签按预期更新。这是因为应用现在能够挂起makeToast()poachEgg()方法以响应用户点击并更新用户界面,然后稍后恢复它们。太棒了!

然而,如果你查看经过的时间,你会发现应用准备早餐的时间比之前稍微长一点:

![Figure 24.6: iOS 模拟器运行 BreakfastMaker 应用,显示经过的时间

![img/Figure_24.06_B17469.jpg]

![Figure 24.6: iOS 模拟器运行 BreakfastMaker 应用,显示经过的时间

这部分是由于挂起和恢复方法所需的开销,但还有一个因素在起作用。尽管makeToast()poachEgg()方法现在是异步的,但poachEgg()方法只有在makeToast()方法执行完毕后才开始执行。在下一节中,你将看到如何使用async-let来并行运行makeToast()poachEgg()方法。

使用 async-let 提高效率

即使你的应用现在对按钮点击做出响应,并且能够在makeToast()poachEgg()方法运行时更新用户界面,这两个方法仍然会顺序执行。这里的解决方案是使用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 = "Poaching egg..."
   async let tempEgg = poachEgg()
   await toastLabel.text = tempToast
   await eggLabel.text = tempEgg
   sandwichLabel.text = makeSandwich()
   let endTime = Date().timeIntervalSince1970
   elapsedTimeLabel.text = "Elapsed time is 
   \(((endTime - startTime) * 100).rounded() 
   / 100) seconds"
}

构建并运行应用。你会看到经过的时间现在比之前短:

![Figure 24.7: iOS 模拟器运行 BreakfastMaker 应用,显示经过的时间

![img/Figure_24.07_B17469.jpg]

Figure 24.7: iOS 模拟器运行 BreakfastMaker 应用,显示经过的时间

这是因为使用async-let允许makeToast()poachEgg()方法并行运行,并且poachEgg()方法不再等待makeToast()方法完成后再开始执行。酷!

重要信息

关于 Swift 并发还有很多东西可以学习,比如结构化并发和 actors,但这些内容超出了本章的范围。你可以在 developer.apple.com/wwdc21/10134 上了解更多关于结构化并发的信息,以及在 developer.apple.com/wwdc21/10133 上了解更多关于 actors 的信息。

在下一节中,你将更新 Let's Eat 应用中的 RestaurantListViewController 类,以便在获取餐厅图像时使用 async/await

将 RestaurantListViewController 更新为使用 async/await

当你运行你的 Let's Eat 应用时,你可能会注意到当 餐厅列表 屏幕显示餐厅列表时会有延迟。这是因为用于下载餐厅图像的代码不是异步的,当餐厅图像正在下载时,应用无法执行其他工作。

下载餐厅图像数据并将其转换为图像的代码位于 RestaurantListViewController 类定义中的 collectionView(_:cellForItemAt:) 方法内。你将修改此代码,使其异步执行。

打开你在 第二十二章**,使用 Mac Catalyst 入门 中修改的 LetsEat 项目,在项目导航器中打开 RestaurantListViewController 文件(位于 Restaurants 文件夹内)。按照以下方式更新 collectionView(_:cellForItemAt:) 方法:

   if let imageURL = restaurantItem.imageURL {
      Task {
         guard let url = URL(string: imageURL)
         else {
            return
         }
let (imageData, response) = try await 
         URLSession.shared.data(from: url)
guard let httpResponse = response as? 
HTTPURLResponse, httpResponse.statusCode 
         == 200 else {
            return
         }
guard let cellImage = UIImage(data: 
         imageData) else {
            return
         }
         cell.restaurantImageView.image = cellImage
         }
      }
   return cell
}

让我们分解一下:

Task {

这创建了一个异步工作的单元。

guard let url = URL(string: imageURL)
else {
   return
}

这个 guard 语句从 RestaurantItem 实例的 imageURL 属性创建一个 URL,并将其分配给 url,如果不能这样做则返回。

let (imageData, response) = try await 
URLSession.shared.data(from: url)

这异步从存储在 url 中的 URL 下载数据,并将其分配给 imageData。服务器的响应分配给 response

guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
   return
}

这个 guard 语句检查服务器响应代码是否为 200(这意味着下载成功),如果不是则返回。

guard let cellImage = UIImage(data: imageData) else {
   return
}

这个 guard 语句从存储在 imageData 中的数据创建一个 UIImage 实例,并将其分配给 cellImage,如果不能这样做则返回。

cell.restaurantImageView.image = cellImage

这将 cellImage 中存储的 UIImage 分配给 restaurantCell 实例的 restaurantImageView 属性,它将在 restaurantImageView 属性中显示。

return cell

这返回 restaurantCell 实例。

构建并运行你的应用。你会发现 餐厅列表 屏幕比以前更响应,滚动也更平滑:

![图 24.8:iOS 模拟器显示带有下载图像的餐厅列表屏幕]

](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ios15-prog-bgn/img/Figure_24.08_B17469.jpg)

图 24.8:iOS 模拟器显示带有下载图像的餐厅列表屏幕

如果你禁用你的互联网连接,餐厅列表 屏幕仍然可以工作,但它将显示默认的占位符图像:

![图 24.9:iOS 模拟器显示带有默认图像的餐厅列表屏幕]

图片

图 24.9:iOS 模拟器显示带有默认图片的餐厅列表屏幕

重要信息

你可以在 developer.apple.com/wwdc21/10095 找到更多关于如何使用 URLSession 与 async/await 的信息。

你已经在你的应用 RestaurantListViewController 类中成功实现了异步代码。太棒了!关于 Swift 并发还有很多东西要学习,比如结构化并发和 actors,但这些超出了本章的范围。

摘要

在本章中,你学习了 Swift 并发以及如何在 BreakfastMakerLet's Eat 应用中实现它。

你首先学习了 Swift 并发的基本概念。接下来,你检查了一个没有并发的应用并探讨了它的问题。之后,你通过使用 async/await 在应用中实现了并发。然后,你通过使用 async-let 使你的应用更加高效。最后,你更新了 Let's Eat 应用中的 RestaurantListViewController 类,以使用 async/await 来加载餐厅图片。

你现在已经理解了 Swift 并发的基础知识,并且将能够在你自己的应用中使用 async/awaitasync-let

在下一章中,你将学习关于 SharePlay 的内容,这是一种为你的应用用户共享群体体验的绝佳方式。

第二十五章:第二十五章:SharePlay 入门

苹果公司在 2021 年的 WWDC 上推出了 SharePlay,该功能允许用户通过将您的应用集成到 FaceTime 中使用 Group Activities 框架来共享体验。

在本章中,您将通过向其中添加 Group Activities 支持来实现 SharePlay 的示例应用。您将从学习 SharePlay 的工作原理开始。接下来,您将使用 Group Activities 框架探索您将添加 SharePlay 支持的应用。然后,您将学习如何为该应用创建自定义群组活动以及如何管理群组活动会话。最后,您将使用两台 iOS 设备在应用中测试 SharePlay 体验。

到本章结束时,您将了解 SharePlay 的工作原理以及如何更新您的应用以使用它。

将涵盖以下主题:

  • 理解 SharePlay

  • 探索 ShareOrder 应用

  • 创建自定义群组活动

  • 管理群组活动会话

  • ShareOrder 应用中测试 SharePlay

技术要求

您将在名为 ShareOrder 的示例应用中实现和测试 Group Activities 框架。您将需要一个付费的 Apple 开发者账户,以及至少两台安装了 iOS 15.1 或更高版本的 iOS 设备,并安装了 ShareOrder 应用。您还可以使用一台安装了 macOS 12.1 或更高版本的 Mac 和一台安装了 iOS 15.1 或更高版本的 iOS 设备。

本章的完成 Xcode 项目位于本书代码包的 Chapter25 文件夹中,可以在此处下载:

github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition

查看以下视频以查看代码的实际运行情况:

bit.ly/3I9zb6Y

让我们从下一节开始学习 SharePlay。

理解 SharePlay

SharePlay 是苹果公司在 2021 年的 WWDC 上推出的。它允许 FaceTime 会话中的参与者共享用户体验。例如,一个用户可能希望与另一个用户一起观看视频。用户需要做的只是与另一个用户进行 FaceTime 通话,启动视频应用,并启动 SharePlay。相同的应用将为另一个用户启动并播放相同的视频,SharePlay 确保视频在两个用户之间保持同步。

您还可以创建自定义 SharePlay 体验。一个例子是在 2021 年的 WWDC 上展示的 DrawTogether 应用。在演示中,三个用户最初加入了一个 FaceTime 会话。一个用户启动了 DrawTogether 应用,并在应用中启动了 SharePlay 会话。其他用户看到了一个包含 加入 按钮的 SharePlay 提示。当按下 加入 按钮时,DrawTogether 应用将为其他用户启动,用户在屏幕上绘制的任何内容都会出现在其他用户的屏幕上。

重要信息

您可以在此处查看*DrawTogether*应用程序的工作原理及其实现方式:developer.apple.com/videos/play/wwdc2021/10187.

您可以在此处下载*DrawTogether*应用程序:developer.apple.com/documentation/groupactivities/drawing_content_in_a_group_session.

SharePlay 由 Group Activities 框架提供支持。此框架使用 FaceTime 来同步应用程序的活动,并邀请其他参与者加入这些活动。表示共享活动的对象必须遵守GroupActivity协议。在群组活动开始后,使用GroupSession对象在所有参与者之间同步应用程序行为。

重要信息

您可以在此处找到 WWDC 2021 期间所有与 Apple 的 Group Activities 相关的视频链接:

developer.apple.com/videos/wwdc2021/?q=group%20activities

您可以在此处阅读 Apple 的 Group Activities 文档:

developer.apple.com/documentation/GroupActivities

在本章中,您将通过以下步骤在名为*ShareOrder*的示例应用程序中实现 Group Activities 框架:

  1. 将 Group Activities 功能添加到*ShareOrder*应用程序中。

  2. 创建并配置一个遵守GroupActivity协议的*ShareOrder*结构。此结构将包含描述群组活动的元数据。

  3. 使用一个按钮配置*ShareOrder*应用程序的用户界面以激活群组活动。

  4. 实现一个允许您的应用程序加入群组活动会话的GroupSession对象。

  5. 实现一个允许您的应用程序发送和接收消息的GroupSessionMessenger对象。这些消息用于同步用户在应用程序中的操作。

在这样做之前,让我们看看下一节中*ShareOrder*应用程序是如何工作的。

探索ShareOrder应用程序

您将要工作的应用程序*ShareOrder*是一个简单的应用程序,用于记录和显示您在餐厅想要订购的内容。让我们构建并运行此应用程序以查看其工作原理。请按照以下步骤操作:

  1. 如果您还没有这样做,请在此链接下载本书的代码包中的Chapter25文件夹:github.com/PacktPublishing/iOS-15-Programming-for-Beginners-Sixth-Edition.

  2. 打开Chapter25文件夹,您将看到两个文件夹,ShareOrder-startShareOrder-complete。第一个文件夹包含您在本课中将要修改的应用程序,第二个文件夹包含完成的应用程序。

  3. 打开 ShareOrder-start 文件夹并打开 ShareOrder Xcode 项目。在项目导航器中点击 Main 故事板文件。你应该在导航栏中看到一个 + 按钮,以及填充其余屏幕的表格视图。图 25.1:显示 Main 故事板文件的 ShareOrder 应用

    图 25.1:显示 Main 故事板文件的 ShareOrder 应用

    应用启动时将显示一个显示空表格视图的屏幕。点击 + 按钮将弹出一个对话框,允许你输入一个订单,然后该订单将出现在表格视图中。

  4. 在项目导航器中点击 ViewController 文件。你应该在编辑器区域看到以下代码:

    import UIKit
    class ViewController: UIViewController {
       var orders: [String] = []
       @IBOutlet var tableView: UITableView!
       override func viewDidLoad() {
          super.viewDidLoad()
          title = "ShareOrder"
          tableView.register(UITableViewCell.self, 
          forCellReuseIdentifier: "orderCell")
       }
       @IBAction func addOrder(_ sender: UIBarButtonItem)
       {
          let alert = UIAlertController(title: "New
          Order", message: "Add a new order", 
          preferredStyle: .alert)
          let saveAction = UIAlertAction(title: "Save", 
          style: .default) {
             [unowned self] action in
             guard let textField = 
             alert.textFields?.first, 
             let orderToSave = textField.text else {
                return
             }
             self.orders.append(orderToSave)
             self.tableView.reloadData()
          }
          let cancelAction = UIAlertAction(title: 
          "Cancel", style: .cancel)
          alert.addTextField()
          alert.addAction(saveAction)
          alert.addAction(cancelAction)
          present(alert, animated: true)
       }
    }
    extension ViewController: UITableViewDataSource {
       func tableView(_ tableView: UITableView, 
       numberOfRowsInSection section: Int) -> Int {
          orders.count
       }
       func tableView(_ tableView: UITableView, 
       cellForRowAt indexPath: IndexPath) -> 
       UITableViewCell {
          let cell = tableView.dequeueReusableCell
          (withIdentifier: "orderCell", for: indexPath)
          cell.textLabel?.text = orders[indexPath.row]
          return cell
       }
    }
    

    让我们分解一下:

    var orders: [String] = []
    

    此属性包含一个订单数组,其类型为 String。此数组将是应用中表格视图的数据源。

    @IBOutlet var tableView: UITableView!
    

    此出口连接到 Main 故事板文件中视图控制器场景中的表格视图。

    viewDidLoad() 方法:

    此方法中的代码将导航栏的标题设置为 ShareOrder,并为表格视图单元格注册重用标识符 orderCell

    addOrder(_:) 方法:

    此方法连接到 orders 数组并重新加载表格视图。点击 orders 数组。表格视图中的每个单元格将显示 orders 数组中的对应字符串。

    小贴士

    你可能希望回顾 第十五章**,开始使用表格视图,,它更详细地介绍了表格视图数据源方法。

构建并运行应用。点击 + 按钮,在警告框的文本字段中输入一些文本,然后点击 保存。它将出现在表格视图中,如下所示:

图 25.2:iOS 模拟器运行 ShareOrder 应用

图 25.2:运行 ShareOrder 应用的 iOS 模拟器

现在你已经熟悉了 ShareOrder 应用及其工作原理,你将为它添加群组活动支持。完成此操作后,在 SharePlay 会话中添加订单将使订单出现在所有参与者的屏幕上。你将在下一节中创建一个用于 ShareOrder 应用的自定义群组活动。

创建自定义群组活动

你已经看到 ShareOrder 应用允许你添加将在屏幕上显示的订单。你将为该应用添加一个群组活动,允许参与者在 SharePlay 会话中添加订单,这些订单将出现在每个参与者的屏幕上。需要一个自定义对象来表示此活动。实现此功能所需的步骤如下:

  • 将群组活动权限添加到 ShareOrder 应用中。

  • 创建一个名为 ShareOrder 的新结构,使其符合 GroupActivity 协议,并配置群组活动元数据。

  • ShareOrder 应用的用户界面中添加一个按钮,并为该按钮添加一个动作以激活群组活动。

    重要信息

    您可以通过此链接了解有关创建自定义组活动的更多信息:developer.apple.com/documentation/groupactivities/inviting-participants-to-share-an-activity.

让我们从将组活动权限添加到 ShareOrder 应用程序开始。

添加组活动权限

因为您的应用将在不同设备之间进行交互,所以它必须具有 com.apple.developer.group-session 权限。您将使用 Xcode 将此权限添加到您的应用中。请注意,您需要付费的 Apple 开发者账户才能完成此操作。请按照以下步骤操作:

  1. 在项目导航器中单击 ShareOrder 项目(项目导航器中最顶部的项目)并单击 ShareOrder 目标。在编辑区域中选择 签名与能力 选项卡。验证是否已将 团队 设置为付费的 Apple 开发者账户。图 25.3:Xcode 签名与能力选项卡,已设置账户

    图 25.3:Xcode 签名与能力选项卡,已设置账户

    小贴士

    第一章**,熟悉 Xcode 中提供了如何设置开发团队以及在 iOS 设备上运行您的应用的说明。

    第二十六章**,测试并将您的应用提交到 App Store 中提供了如何获取付费 Apple 开发者账户的说明。

  2. 将运行 iOS 15 的 iOS 设备连接到您的计算机,并验证 ShareOrder 应用程序是否可以在您的设备上运行。

  3. 签名和能力 选项卡中,单击 + 按钮以向 ShareOrder 应用程序添加能力。图 25.4:Xcode 签名和能力选项卡,突出显示 + 按钮

    图 25.4:Xcode 签名和能力选项卡,突出显示 + 按钮

  4. 在出现的窗口中,搜索并双击组活动功能以将其添加到 ShareOrder 应用程序:

图 25.5:将组活动功能添加到 ShareOrder 应用程序

图 25.5:将组活动功能添加到 ShareOrder 应用程序

您已将组活动权限添加到 ShareOrder 应用程序。接下来,您将在下一节中创建一个 ShareOrder 结构来表示组活动。

创建 ShareOrder 结构

您将为 ShareOrder 应用程序实现的活动将使在 SharePlay 会话期间添加的订单出现在所有参与者的屏幕上。您需要一个对象来表示此活动。您将通过创建一个 ShareOrder 结构来实现此对象。此结构将遵循 GroupActivity 协议,并包含描述活动的元数据。要创建和配置 ShareOrder 结构,请按照以下步骤操作:

  1. 在项目导航器中单击 ViewController 文件,并按如下所示导入 GroupActivities 框架:

    import UIKit
    import GroupActivities
    
  2. 在最后一个花括号之后添加以下扩展来创建和配置ShareOrder结构:

    extension ViewController {
       struct ShareOrder: GroupActivity {
          var metadata: GroupActivityMetadata {
             var metadata = GroupActivityMetadata()
             metadata.title = NSLocalizedString("Share 
             Order", comment: "Title of group activity")
             metadata.type = .generic
             return metadata
          }
       }
    }
    

此结构表示ShareOrder应用的共享体验。它符合GroupActivity协议,该协议提供上下文和元数据以启动活动相关的会话。在此,您使用名为metadata的计算属性来设置元数据标题和注释。对于自定义活动,元数据类型设置为.generic

您已创建并配置了ShareOrder结构。在下一节中,您将在ShareOrder应用的用户界面中添加一个按钮,并配置此按钮以便在 FaceTime 会话期间激活群组活动。

激活自定义群组活动

您已创建并配置了用于表示ShareOrder应用自定义群组活动的ShareOrder结构。现在,您将在ShareOrder应用的导航栏中添加一个按钮,以便在与其他参与者进行 FaceTime 会话时激活此群组活动。按照以下步骤操作:

  1. 在项目导航器中点击Main故事板文件,然后点击图书馆按钮。

  2. 在图书馆的筛选字段中,搜索一个栏按钮项对象并将其拖动到+按钮旁边的导航栏中:图 25.6:选择栏按钮项的图书馆

    图 25.6:选择栏按钮项的图书馆

  3. 在选择栏按钮项后,点击属性检查器按钮。在person.2.fill下:图 25.7:属性检查器显示设置为 person.2.fill 的栏按钮项

    图 25.7:属性检查器显示设置为 person.2.fill 的栏按钮项

    这设置了栏按钮项的图标。

  4. 点击调整编辑器选项按钮并选择ViewController文件将出现在辅助编辑器中。

  5. 从您刚刚添加的栏按钮项控制+拖动到辅助编辑器中addOrder(_:)方法之前。

    将会出现一个弹出对话框。

  6. activateGroupActivity中点击连接图 25.8:名称设置为 activateGroupActivity 的弹出对话框

    图 25.8:名称设置为 activateGroupActivity 的弹出对话框

  7. 将以下代码添加到activateGroupActivity(_:)方法中,以便在点击栏按钮项时激活群组活动:

    @IBAction func activateGroupActivity(_ sender: Any) {
       Task {
          do {
             try await ShareOrder().activate()
          } catch {
             print("Unable to activate")
          }
       }
    }
    

    这将向 FaceTime 会话中的所有参与者显示一个带有加入按钮的 SharePlay 提示。

  8. 点击x按钮关闭辅助编辑器。

您刚刚添加了一个按钮,当点击时将激活您的群组活动。在下一节中,您将学习如何在ShareOrder应用中管理群组活动会话。

管理群组活动会话

你已经创建了ShareOrder结构来表示*ShareOrder*应用的组活动,并且你已经在应用的导航栏中添加了一个按钮,用于在 FaceTime 会话期间激活组活动。现在你需要添加代码以允许参与者加入这个组活动会话,并使所有参与者保持同步。实现此功能所需的步骤如下:

让我们看看如何在下一节中实现和配置*ShareOrder*应用的GroupSession对象。

实现一个GroupSession对象

当前*ShareOrder*应用中有一个表示组活动的对象,以及一个在 FaceTime 会话期间激活组活动的按钮。你将在你的应用中添加代码以实现一个GroupSession对象,这将允许参与者加入组活动会话。为了为*ShareOrder*应用实现和配置GroupSession对象,请按照以下步骤操作:

  1. 在项目导航器中点击ViewController文件。在orders属性之后添加一个可选属性来保存组活动会话的实例:

    var orders: [String] = [] 
    var groupSession: GroupSession<ShareOrder>?
    
  2. 将以下代码添加到viewDidLoad()中,以创建一个异步任务来接收一个组会话:

    override func viewDidLoad() {
       super.viewDidLoad()
       title = "ShareOrder"
       tableView.register(UITableViewCell.self, 
       forCellReuseIdentifier: "orderCell")
    Task is a unit of asynchronous work, and is covered in *Chapter 24*, *Getting Started with Swift Concurrency*.You'll see an error because `configureGroupSession(_:)` has not yet been implemented.
    
  3. 将以下代码添加到ShareOrder结构定义之后的扩展中,以实现configureGroupSession(_:)方法:

    func configureGroupSession(_ groupSession: 
    GroupSession<ShareOrder>) {
       orders.removeAll()
       self.groupSession = groupSession
    }
    

    此方法会移除orders数组中的所有订单,然后将接收到的组活动会话分配给groupSession属性。

  4. 通过添加代码来修改configureGroupSession(_:)方法,以便加入组活动会话:

    func configureGroupSession(_ groupSession: 
    GroupSession<ShareOrder>){
       orders.removeAll()
       self.groupSession = groupSession
       ShareOrder project at this point.
    

现在你已经实现和配置了GroupSession对象,你将在下一节中实现和配置*ShareOrder*应用的GroupSessionMessenger对象。

实现一个GroupSessionMessenger对象

*ShareOrder*应用的用户加入 SharePlay 会话后,他们添加到应用中的任何订单都会出现在每个人的屏幕上。这种同步由一个GroupSessionMessenger对象处理,该对象可以向组活动会话中的其他设备发送和接收消息。请注意,保持消息大小较小非常重要,因为大的消息大小可能会导致应用崩溃。对于此应用,消息将仅包含提交的订单的字符串,这将相当小。为了为*ShareOrder*应用实现GroupSessionMessenger对象,请按照以下步骤操作:

  1. ViewController文件中,在groupSession属性之后添加一个新的可选属性以保存GroupSessionMessenger的一个实例:

    var groupSession: GroupSession<ShareOrder>?
    var messenger: GroupSessionMessenger?
    
  2. 修改configureGroupSession(_:)方法以创建GroupSessionMessenger的一个实例并将其分配给messenger属性:

    func configureGroupSession(_ groupSession: 
    GroupSession<ShareOrder>){
       orders.removeAll()
       self.groupSession = groupSession
    let messenger = GroupSessionMessenger(session: 
       groupSession)
       self.messenger = messenger
       groupSession.join()
    }
    
  3. messenger属性赋值后添加一个异步任务以接收GroupSessionMessenger消息:

    let messenger = GroupSessionMessenger(session: 
    groupSession)
    self.messenger = messenger
    GroupSessionMessenger message contains a string representing an order. You will implement a handle(_:) method and pass the message to this method to be processed. You'll see an error because the handle(_:) method has not yet been implemented.
    
  4. configureGroupSession(_:)方法之后实现handle(_:)方法,如下所示:

    func handle(_ message: String) {
       self.orders.append(message)
       self.tableView.reloadData()
    }
    

    此方法将消息中的字符串追加到orders数组中,并重新加载表格视图。

  5. 修改addOrder(_:)方法如下,以便在参与者点击+按钮时发送GroupSessionMessenger消息:

    self.orders.append(orderToSave)
    if let messenger = messenger {
       Task {
          do {
             try await messenger.send(orderToSave)
          } catch {
             print("Failed to send")
          }
       }
    }
    self.tableView.reloadData()
    

您添加到addOrder(_:)方法中的代码创建了一个异步任务,该任务将包含订单的消息发送给 FaceTime 会话中的所有其他参与者。当消息被接收时,它将由handle(_:)方法处理,该方法将订单添加到orders数组中并重新加载表格视图。

在您的 iOS 15 设备上构建和运行ShareOrder应用。它应该像之前一样工作。在下一节中,您将通过使用 FaceTime 会话中的两台 iOS 设备来测试ShareOrder应用中的 SharePlay 功能。

在 ShareOrder 应用中测试 SharePlay

您已经添加了实现 SharePlay 所需的所有代码。为了测试它,您需要两台运行 iOS 15.1 或更高版本的 iOS 设备,并且已安装ShareOrder应用。您也可以使用运行 macOS 12.1 Monterey 或更高版本的 Mac 作为其中一台设备。您将在两台设备之间启动 FaceTime 会话并启动 SharePlay 会话。您应该能够从任何设备添加订单,并且您添加的任何订单都将出现在两个屏幕上。按照以下步骤操作:

  1. 在您的第二台 iOS 设备上安装ShareOrder应用。

  2. 在两台设备之间启动 FaceTime 通话并在第一台设备上启动ShareOrder应用。![图 25.9:在 FaceTime 通话期间在第一台设备上运行 ShareOrder 图片

    图 25.9:在 FaceTime 通话期间在第一台设备上运行 ShareOrder

  3. 点击按钮以激活群组活动。您应该在第二台设备上看到一个 SharePlay 提示。![图 25.10:在第一台设备上点击按钮触发第二台设备上的 SharePlay 提示 图片

    图 25.10:在第一台设备上点击按钮会在第二台设备上触发 SharePlay 提示

  4. 在 SharePlay 提示中点击加入按钮,ShareOrder应用将在第二台设备上启动。使用+按钮添加一个订单,它将出现在两台设备的屏幕上。

![图 25.11:相同的订单出现在两台设备上图片

图 25.11:相同的订单出现在两台设备上

恭喜!您已经在ShareOrder应用中实现了 SharePlay 功能!

摘要

在本章中,您通过向ShareOrder应用添加群组活动支持来实现 SharePlay 功能。

你首先学习了 SharePlay 的工作原理。接下来,你探索了 ShareOrder 应用程序以了解其工作方式。之后,你为该应用程序创建了一个自定义的群组活动,并添加了代码来管理群组活动会话。最后,你使用两台 iOS 设备在应用程序中测试了 SharePlay 体验。

现在,你已经了解了 SharePlay 的基础知识,并将能够将自定义群组活动添加到自己的应用程序中。

在下一章中,你将学习如何测试并将你的应用程序提交到 App Store。

第二十六章:第二十六章: 测试并提交您的应用至 App Store

恭喜!您已经到达了本书的最后一章!

在本书的整个过程中,您已经学习了 Swift 编程语言以及如何使用 Xcode 构建整个应用。然而,您之前只是在 iOS 模拟器或使用免费 Apple 开发者账户的设备上运行您的应用。

在本章中,您将首先学习如何获取付费 Apple 开发者账户。接下来,您将了解证书、标识符、测试设备注册和配置文件。之后,您将学习如何创建 App Store 列表并提交您的应用至 App Store。最后,您将学习如何使用内部和外部测试员对您的应用进行测试。

到本章结束时,您将学会如何构建和提交应用到 App Store,以及如何对您的应用进行内部和外部测试。

以下主题将涵盖:

  • 获取 Apple 开发者账户

  • 探索您的 Apple 开发者账户

  • 提交您的应用至 App Store

  • 进行内部和外部测试

技术要求

您需要一个 Apple ID 和一个付费 Apple 开发者账户来完成本章。

本章没有项目文件,因为它旨在说明如何提交应用,而不是针对任何特定应用。

重要信息

要查看 App Store 的最新更新,请访问developer.apple.com/app-store/whats-new/

苹果公司推出了一个名为Xcode Cloud的新连续集成和交付服务,内置于 Xcode 13 中。要了解更多信息,请访问此链接:developer.apple.com/xcode-cloud/

要了解如何提交您的应用的更多信息,请访问developer.apple.com/app-store/submissions/

让我们从下一节学习如何获取付费 Apple 开发者账户开始,这是提交至 App Store 所必需的。

获取 Apple 开发者账户

如您在前面章节所见,您只需要一个免费 Apple ID 即可在设备上测试您的应用。但应用将只工作几天,您将无法添加如使用 Apple ID 登录或上传您的应用到 App Store 等高级功能。为此,您需要一个付费 Apple 开发者账户。按照以下步骤购买个人/个体工商户 Apple 开发者账户:

  1. 前往developer.apple.com/programs/并点击注册按钮。

  2. 滚动到屏幕底部并点击开始注册

  3. 当提示时,输入您的 Apple ID 和密码。

  4. 信任此浏览器?屏幕上,点击现在不

  5. 点击在网页上继续注册 >

  6. 确认您的个人信息屏幕上,输入您的个人信息,完成后点击继续

  7. 选择你的实体类型屏幕上,选择个人/个体经营者。点击继续

  8. 审查和接受屏幕上,勾选页面底部的复选框并点击继续

  9. 完成购买屏幕上,点击购买

  10. 按照屏幕上的指示完成购买。一旦你购买了账户,请访问developer.apple.com/account/并使用你购买开发者账户时使用的相同 Apple ID 登录。你应该会看到以下类似的内容:

![图 26.1:登录付费 Apple 开发者账户的 Apple 开发者网站图片

图 26.1:登录付费 Apple 开发者账户的 Apple 开发者网站

现在你已经拥有了付费的 Apple 开发者账户,接下来我们将学习如何在下一节中配置应用所需的各项设置。

探索你的 Apple 开发者账户

你的 Apple 开发者账户包含了开发并提交应用所需的一切。你可以查看你的会员状态,添加和组织你的开发团队成员,访问开发者文档,下载测试软件等。尽管所有这些功能都不在本书的范围之内,但本节将仅涵盖你需要做的以将你的应用提交到 App Store。

首先,你将获得 Apple 开发者证书,这些证书将安装在你的 Mac 上。这些证书将用于对应用进行数字签名。接下来,你需要注册你的应用 App ID 以及你将测试应用的设备。之后,你将能够生成配置文件,允许你的应用在测试设备上运行,并允许你提交应用到 App Store。下一节我们将学习证书签名请求,这是获取你将在 Mac 上安装的 Apple 开发者证书所必需的。

生成证书签名请求

在你编写将提交到 App Store 的应用之前,你需要在运行 Xcode 的 Mac 上安装一个开发者证书。证书标识了应用的作者。要获取此证书,你需要创建一个证书签名请求CSR)。以下是创建 CSR 的步骤:

  1. 在你的 Mac 上打开实用工具文件夹并启动钥匙串访问

  2. 钥匙串访问菜单中选择证书助手 | 从证书颁发机构请求证书...:![图 26.2:钥匙串访问应用 图片

    图 26.2:钥匙串访问应用

  3. 用户电子邮件地址字段中,输入你用于注册 Apple 开发者账户的 Apple ID 电子邮件地址。在通用名称字段中,输入你的名字。在请求是:下选择保存到磁盘并点击继续:![图 26.3:证书助手屏幕 图片

    图 26.3:证书助手屏幕

  4. 将 CSR 保存到你的硬盘上。

  5. 点击 完成

现在您已经有了 CSR,让我们看看您将如何使用它来获取 开发证书(用于在您的测试设备上测试)和 分发证书(用于 App Store 提交)在下一节中。

创建开发和分发证书

一旦您有了证书签名请求,您就可以用它来创建开发和分发证书。开发证书用于您想在测试设备上测试您的应用程序时,分发证书用于您想将应用程序上传到 App Store 时。以下是创建开发和分发证书的步骤:

  1. 登录您的 Apple 开发者账户,并点击 Certificates, IDs & Profiles:![图 26.4:已登录付费 Apple 开发者账户的 Apple 开发者网站 图片

    图 26.4:已登录付费 Apple 开发者账户的 Apple 开发者网站

  2. 您将看到 证书 屏幕。点击 + 按钮:![图 26.5:显示 + 按钮的证书屏幕 图片

    图 26.5:显示 + 按钮的证书屏幕

  3. 点击 Apple Development 单选按钮,然后点击 继续:![图 26.6:显示 Apple Development 单选按钮的创建新证书屏幕 图片

    图 26.6:显示 Apple Development 单选按钮的创建新证书屏幕

  4. 点击 选择文件:![图 26.7:上传证书签名请求屏幕 图片

    图 26.7:上传证书签名请求屏幕

  5. 上传证书签名请求 下的 选择文件 中选择,选择您之前保存到硬盘上的 CSR 文件,然后点击 选择

  6. 点击 继续:![图 26.8:已上传证书的上传证书签名请求屏幕 图片

    图 26.8:已上传证书的上传证书签名请求屏幕

  7. 您的证书将自动生成。点击 下载 以将生成的证书下载到您的 Mac 上:![图 26.9:下载您的证书屏幕 图片

    图 26.9:下载您的证书屏幕

  8. 双击下载的证书以将其安装到您的 Mac 上。

  9. 再次重复 步骤 3-8,但这次,在 步骤 3 中选择 Apple Distribution

![图 26.10:显示 Apple Distribution 单选按钮的创建新证书屏幕图片

图 26.10:显示 Apple Distribution 单选按钮的创建新证书屏幕

太好了!您现在拥有了开发和分发证书。下一步是为您的应用程序注册 App ID 以在 App Store 中识别它。您将在下一节中学习如何操作。

注册 App ID

当您在 第一章 熟悉 Xcode 中创建项目时,您为它创建了一个包标识符(也称为 App ID)。App ID 用于在 App Store 中识别您的应用。在将应用上传到 App Store 之前,您需要在您的开发者账户中注册此 App ID。以下是注册 App ID 的方法:

  1. 登录您的 Apple 开发者账户,并点击 证书、标识符和配置文件

  2. 点击 标识符

  3. 点击 + 按钮 图片

    图 26.11:标识符屏幕

  4. 点击 App ID 并点击 继续图 26.12:注册新标识符屏幕

    图片

    图 26.12:注册新标识符屏幕

  5. 点击 App 并点击 继续图 26.13:标识符类型屏幕

    图片

    图 26.13:标识符类型屏幕

  6. Let's Eat Packt Publishing App ID。勾选 显式 按钮,并在字段中输入您的应用的 包标识符。确保此值与您创建项目时使用的包标识符相同。完成后,点击 继续 按钮:图 26.14:描述和包标识符屏幕

    图片

    图 26.14:描述和包标识符屏幕

  7. 点击 注册

图 26.15:注册屏幕

图片

图 26.15:注册屏幕

您的 App ID 现已注册。太酷了!接下来,您将在下一节中注册将在其上测试应用的设备。

注册您的设备

要在您的个人设备上运行应用进行测试,您需要在您的开发者账户中注册它们。以下是注册设备的方法:

  1. 登录您的 Apple 开发者账户,并点击 证书、标识符和配置文件

  2. 点击 设备

  3. 点击 + 按钮 图 26.16:设备注册屏幕

    图片

    图 26.16:设备注册屏幕

  4. 注册新设备 屏幕出现:图 26.17:注册新设备屏幕

    图片

    图 26.17:注册新设备屏幕

    您需要 设备名称设备 ID 来注册您的设备。

  5. 将您的设备连接到您的 Mac。启动 Xcode,从 窗口 菜单中选择 设备和模拟器。在左侧窗格中选择设备并复制 标识符 值:图 26.18:设备和模拟器窗口

    图片

    图 26.18:设备和模拟器窗口

  6. 设备名称 字段中为设备输入一个名称,并将标识符值粘贴到 设备 ID (UDID) 字段中。点击 继续

图 26.19:注册新设备屏幕

图片

图 26.19:注册新设备屏幕

您已成功注册测试设备。太好了!下一步是创建配置文件。需要一个iOS 应用开发配置文件,以便您的应用能够在测试设备上运行,并且需要一个iOS 应用商店分发配置文件,用于上传到应用商店的应用。您将在下一节中创建开发和分发配置文件。

创建配置文件

您需要创建两个配置文件。iOS 应用开发配置文件是应用在测试设备上运行所必需的。iOS 应用商店分发配置文件用于将您的应用提交到应用商店。以下是创建开发配置文件的步骤:

  1. 登录您的 Apple 开发者账户,并点击证书、标识和配置文件

  2. 点击配置文件

  3. 点击+按钮:图 26.20:配置文件屏幕

    图 26.20:配置文件屏幕

  4. 点击iOS 应用开发并点击继续图 26.21:注册新的配置文件屏幕

    图 26.21:注册新的配置文件屏幕

  5. 选择您要测试的应用的App ID并点击继续图 26.22:选择 App ID 屏幕

    图 26.22:选择 App ID 屏幕

  6. 选择一个开发证书并点击继续图 26.23:选择开发证书屏幕

    图 26.23:选择开发证书屏幕

  7. 选择您将测试此应用的全部设备并点击继续图 26.24:选择设备屏幕

    图 26.24:选择设备屏幕

  8. 为配置文件输入一个名称并点击生成图 26.25:生成开发配置文件屏幕

    图 26.25:生成开发配置文件屏幕

  9. 点击下载按钮下载配置文件。

  10. 双击配置文件进行安装。

接下来,您将创建一个分发配置文件:

  1. 点击所有配置文件链接返回上一页:图 26.26:所有配置文件链接

    图 26.26:所有配置文件链接

  2. 点击+按钮:图 26.27:配置文件屏幕

    图 26.27:配置文件屏幕

  3. 点击应用商店并点击继续图 26.28:注册新的配置文件屏幕

    图 26.28:注册新的配置文件屏幕

  4. 选择您要发布到应用商店的应用的App ID并点击继续图 26.29:选择 App ID 屏幕

    图 26.29:选择 App ID 屏幕

  5. 选择一个分发证书并点击继续图 26.30:选择分发证书屏幕

    图 26.30:选择分发证书屏幕

  6. 为配置文件输入一个名称并点击生成图 26.31:生成分发配置文件屏幕

    图 26.31:生成分发配置文件屏幕

  7. 点击下载按钮下载配置文件。

  8. 双击配置文件以安装它。

您已经完成了提交应用到 App Store 所需的所有步骤。让我们在下一节中了解更多关于提交过程的信息,我们将以 ShareOrder 应用为例。

将您的应用提交到 App Store

您现在可以提交您的应用到 App Store!在本节中,我们将使用 ShareOrder 应用作为示例。让我们回顾一下到目前为止您所做的工作。您已创建了开发和分发证书,注册了 App ID 和测试设备,并生成了开发和分发配置文件。

要在您的测试设备上测试您的应用,您将使用开发证书、App ID、注册的测试设备和开发配置文件。要提交您的应用到 App Store,您将使用分发证书、App ID 和分发配置文件。您将配置 Xcode 以自动为您管理这些操作。

在您提交应用之前,您必须创建应用图标并获取应用的截图。然后您可以创建 App Store 列表,生成要上传的存档构建,并完成 App Store Connect 信息。苹果公司将对您的应用进行审查,如果一切顺利,它将出现在 App Store 上。

在下一节中,我们将看看如何为您的应用创建图标,这些图标将在应用安装到设备屏幕上时显示。

为您的应用创建图标

在您上传应用到 App Store 之前,您必须为它创建一个图标集。以下是创建应用图标集的方法:

  1. 为您的应用创建一个 1,024 x 1,024 像素的图标。

  2. 使用如 appicon.co 这样的网站生成所有不同的图标大小并下载图标集。

  3. 在项目导航器中点击 Assets.xcassets 并用你下载的图片集替换 AppIcon

图 26.32:显示 AppIcon 图片集的 Assets.xcassets 文件

图 26.32:显示 AppIcon 图片集的 Assets.xcassets 文件

当您在模拟器或设备上运行您的应用并退出应用时,您应该能在主屏幕上看到应用的图标。真 neat!

让我们看看如何创建截图。您需要它们来提交 App Store,以便客户可以看到您的应用的外观。您将在下一节中这样做。

为您的应用创建截图

您需要应用的截图,这些截图将用于您的 App Store 列表。要创建它们,请在模拟器中运行您的应用并点击截图按钮。它将被保存在桌面上:

图 26.33:模拟器显示截图按钮

图 26.33:模拟器显示截图按钮

使用iPhone 13 Pro MaxiPhone 8 PlusiPad Pro (12.9 英寸)(第二代)iPad Pro (12.9 英寸)(第五代)模拟器,并为每个模拟器获取一些截图,展示您应用的所有不同功能。您必须使用所有这些模拟器的原因是您将需要不同屏幕尺寸上运行的应用截图,这将在下一节中更详细地讨论,您将在那里学习如何创建 App Store 列表。App Store 列表包含将在 App Store 中显示的所有关于您应用的信息,因此客户可以在下载或购买您的应用之前做出明智的决定。

创建 App Store 列表

现在您已经有了您应用的图标和截图,您将创建 App Store 列表。这允许客户在下载之前查看有关您应用的信息。按照以下步骤操作:

  1. 前往appstoreconnect.apple.com并选择My Apps图 26.34:App Store Connect 网站

    图 26.34:App Store Connect 网站

  2. 点击屏幕左上角的+按钮,选择New App图 26.35:新建应用按钮和菜单

    图 26.35:新建应用按钮和菜单

  3. 将显示一个包含字段列表的New App屏幕:

图 26.36:应用详情屏幕

图 26.36:应用详情屏幕

输入您的应用详情:

Platforms:您的应用支持的所有平台(iOS、macOS 和/或 tvOS)。

Name:您的应用名称。

Primary Language:您的应用使用的语言。

BundleID:您之前创建的 bundleID。

SKU:您用于引用应用的任何参考数字或字符串。

用户访问:管理您的开发者账户团队中谁可以在 App Store Connect 中查看此应用。如果您是团队中唯一的成员,只需将其设置为Full Access

完成后点击Create

应用现在将列在您的账户中,但您仍然需要上传应用及其所有相关信息。要上传应用,您需要创建一个存档构建,您将在下一节中学习如何进行。

创建存档构建

您将创建一个存档构建,该构建将提交给 Apple 以放置在 App Store 上。这还将用于您的内部和外部测试。创建存档构建的步骤如下:

  1. 打开 Xcode,在项目导航器中选择项目名称,并选择1.01图 26.37:显示 General 面板的编辑区域

    图 26.37:显示 General 面板的编辑区域

  2. 选择Signing & Capabilities面板。确保Automatically manage signing已勾选。这将允许 Xcode 创建证书、应用 ID 和配置文件,并注册连接到您的 Mac 的设备。在Team菜单中选择您的付费开发者账户:![图 26.38:显示 Signing & Capabilities 面板的编辑区域 图 26.38:B17469.jpg

    图 26.38:显示签名与能力面板的编辑区域

  3. 将构建目标选择为任何 iOS 设备:![图 26.39:已选择任何 iOS 设备的方案菜单 图 26.39:B17469.jpg

    图 26.39:已选择任何 iOS 设备的方案菜单

  4. 如果您的应用未使用加密,通过添加ITSAppUsesNonExemptEncryptionInfo.plist文件,将其类型设置为Boolean,并将值设置为NO来更新您的Info.plist文件:![图 26.40:已选择 Info.plist 的项目导航器 图 26.40:B17469.jpg

    图 26.40:已选择 Info.plist 的项目导航器

  5. 产品菜单中选择存档:![图 26.41:已选择存档的产品菜单 图 26.41:B17469.jpg

    图 26.41:已选择存档的产品菜单

  6. Organizer窗口出现,并已选择存档标签。您的应用将显示在此屏幕上。选择它并点击分发应用按钮:![图 26.42:已选择分发应用的 Organizer 窗口 图 26.42:B17469.jpg

    图 26.42:已选择分发应用的 Organizer 窗口

  7. 选择App Store Connect并点击下一步:![图 26.43:分发方法选择屏幕 图 26.43:B17469.jpg

    图 26.43:分发方法选择屏幕

  8. 选择上传并点击下一步:![图 26.44:目的地选择屏幕 图 26.44:B17469.jpg

    图 26.44:目的地选择屏幕

  9. 保持默认设置并点击下一步:![图 26.45:分发选项选择屏幕 图 26.45:B17469.jpg

    图 26.45:分发选项选择屏幕

  10. 选择自动管理签名并点击下一步:![图 26.46:签名屏幕 图 26.46:B17469.jpg

    图 26.46:签名屏幕

  11. 如果提示输入密码,请输入 Mac 账户密码并点击始终允许

  12. 点击上传:![图 26.47:内容审查屏幕 图 26.47:B17469.jpg

    图 26.47:内容审查屏幕

  13. 等待上传完成。

  14. 点击完成

![图 26.48:存档上传完成屏幕图 26.48:B17469.jpg

图 26.48:存档上传完成屏幕

到此为止,将通过 App Store 分发的应用的构建已上传。在下一节中,您将学习如何上传截图并完成将在 App Store 与应用一起显示的应用信息。

在 App Store Connect 中完成信息

您的应用已上传,但您仍需要在 App Store Connect 中完成有关您应用的详细信息。以下是步骤:

  1. 访问appstoreconnect.apple.com并选择我的应用

  2. 选择您刚刚创建的应用程序:![图 26.49:已选择您的应用的 Apps 屏幕 图 26.49:B17469.jpg

    图 26.49:已选择您的应用的 Apps 屏幕

  3. 在屏幕左侧选择应用信息,并确保所有信息都是正确的:图 26.50:应用信息屏幕

    图 26.50:应用信息屏幕

  4. 对于定价和可用性应用隐私部分也做同样的操作:图 26.51:应用定价和可用性屏幕

    图 26.51:应用定价和可用性屏幕

  5. 在屏幕左侧选择准备提交。在应用预览和截图部分,拖入你之前拍摄的截图。在iPhone 13 Pro Max 显示部分使用 iPhone 13 Pro Max 的截图,在iPhone 8 Plus 显示部分使用 iPhone 8 Plus 的截图,并在相应的部分使用 iPad 的截图:图 26.52:准备提交屏幕显示应用预览和截图部分

    图 26.52:准备提交屏幕显示应用预览和截图部分

  6. 滚动并填写促销文本描述关键词支持 URL营销 URL字段:图 26.53:版本信息部分

    图 26.53:版本信息部分

  7. 滚动到构建部分,你会看到你之前上传的存档构建。如果你看不到它,点击+按钮,选择一个构建,然后点击完成图 26.54:构建选择屏幕

    图 26.54:构建选择屏幕

  8. 确认你的构建在构建部分:图 26.55:构建部分

    图 26.55:构建部分

  9. 滚动到通用应用信息部分并填写所有必填信息:图 26.56:显示编辑按钮的通用应用信息部分

    图 26.56:显示编辑按钮的通用应用信息部分

  10. 滚动到应用审查信息部分。如果你想向应用审查员提供任何额外信息,请将其放在此处:图 26.57:应用审查信息部分

    图 26.57:应用审查信息部分

  11. 滚动到版本发布部分并保持默认设置:图 26.58:版本发布部分

    图 26.58:版本发布部分

  12. 滚动到屏幕顶部并点击提交审查按钮:图 26.59:提交审查按钮

    图 26.59:提交审查按钮

  13. 确认应用状态已更改为等待审查

图 26.60:显示等待审查的应用状态

图 26.60:显示等待审查的应用状态

您需要等待苹果审查应用,如果您的应用被批准或拒绝,您将收到一封电子邮件。如果您的应用被拒绝,将有一个链接带您到苹果解决方案中心页面,其中描述了您的应用被拒绝的原因。修复问题后,您可以更新存档并重新提交。

您现在知道如何将您的应用提交到 App Store 了!太棒了!

在下一节中,您将学习如何对您的应用进行内部和外部测试,这对于确保应用高质量且无错误至关重要。

测试您的应用

苹果有一个名为TestFlight的设施,允许您在将应用发布到 App Store 之前将其分发给测试者。您需要下载 TestFlight 应用,可在developer.apple.com/testflight/找到,以测试您的应用。您的测试者可以是您的内部团队成员(内部测试者)或公众(外部测试者)。让我们在下一节中首先看看如何允许内部团队成员测试您的应用。

在内部测试您的应用

当应用处于开发早期阶段时,内部测试是很好的。它仅涉及您的内部团队成员。苹果不会审查内部测试者的应用。您可以发送构建给最多 100 名测试者进行内部测试。按照以下步骤操作。

  1. 前往appstoreconnect.apple.com并选择我的应用

  2. 选择您想要测试的应用。

  3. 点击TestFlight标签:图 26.61:TestFlight 标签

    图 26.61:TestFlight 标签

  4. 点击内部测试旁边的+按钮以创建一个新的内部测试组:图 26.62:显示+按钮的 TestFlight 屏幕

    图 26.62:显示+按钮的 TestFlight 屏幕

  5. 将会出现创建新内部组对话框。为您的内部测试组命名并点击创建图 26.63:创建新内部组对话框

    图 26.63:创建新内部组对话框

  6. 在您的测试组创建后,点击+按钮将用户添加到您的组:图 26.64:显示+按钮的测试组屏幕

    图 26.64:显示+按钮的测试组屏幕

  7. 选择所有您想要发送测试构建的用户,然后点击添加。他们将受邀测试所有可用的构建:图 26.65:添加测试者屏幕

    图 26.65:添加测试者屏幕

  8. 验证测试者是否已添加:

图 26.66:显示测试者部分的 TestFlight 屏幕

图 26.66:显示测试者部分的 TestFlight 屏幕

内部测试将仅涉及您的团队成员。如果您想与大量测试者进行测试,您需要进行外部测试,这将在下一节中描述。

在外部测试您的应用

当应用处于开发最后阶段时,外部测试是好的。你可以选择任何人为外部测试者,并且你可以将构建发送给最多 10,000 名测试者。苹果可能会审查外部测试者的应用。以下是步骤:

  1. 前往appstoreconnect.apple.com并选择我的应用

  2. 选择你想要测试的应用。

  3. 点击TestFlight标签页:

  4. 点击外部测试旁边的+按钮:图 26.67:显示+按钮的 TestFlight 屏幕

    图 26.67:显示+按钮的 TestFlight 屏幕

  5. 输入测试组的名称并点击创建图 26.68:创建新组屏幕

    图 26.68:创建新组屏幕

  6. 点击测试者旁边的+按钮并选择添加新测试者图 26.69:添加新测试者按钮和菜单

    图 26.69:添加新测试者按钮和菜单

  7. 输入测试者的姓名和电子邮件地址,苹果将在构建准备好测试时自动通知他们:图 26.70:添加新测试者到组屏幕

    图 26.70:添加新测试者到组屏幕

  8. 构建部分,点击+图 26.71:构建部分

    图 26.71:构建部分

  9. 选择你的一个构建并点击下一步图 26.72:选择构建屏幕

    图 26.72:选择构建屏幕

  10. 输入测试信息并点击下一步图 26.73:测试信息屏幕

    图 26.73:测试信息屏幕

  11. 输入要测试的内容并点击提交审查

图 26.74:要测试的内容

图 26.74:要测试的内容屏幕

就像向 App Store 提交应用一样,你需要等待苹果审查应用,如果你的应用被拒绝,苹果的解决方案中心页面将会有关于为什么你的应用被拒绝的详细信息。然后你可以修复问题并重新提交。

太好了!你现在知道如何内部和外部测试你的应用,并且你已经到达了这本书的结尾!

摘要

你现在已经完成了构建应用并将其提交到 App Store 的整个过程。恭喜!

您首先学习了如何获取 Apple 开发者账户。接下来,您学习了如何生成证书签名请求来创建允许您在自用设备上测试应用并将其发布到 App Store 的证书。您学习了如何创建一个捆绑标识符来在 App Store 上唯一标识您的应用,并注册您的测试设备。之后,您学习了如何创建开发和生产配置文件,以便应用能在您的测试设备上运行并上传到 App Store。接下来,您学习了如何创建 App Store 列表并提交您的发布构建到 App Store。最后,您学习了如何使用内部和外部测试员对您的应用进行测试。

您现在知道如何构建和提交应用到 App Store,并对您的应用进行内部和外部测试。

一旦应用提交了审查,您能做的就是等待苹果审查您的应用。如果应用被拒绝,请不要担心——这发生在所有开发者身上。通过“问题解决中心”与苹果合作解决问题,并做好研究,了解什么是对苹果可接受的,什么是不可以接受的。

在您的应用上架 App Store 之后,请随时通过 Twitter 联系我(@shah_apple)和 Craig Clayton(@thedevme),让我们知道您的进展——我们非常想看看您所构建的内容。

posted @ 2025-09-29 10:35  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报