原文:
zh.annas-archive.org/md5/f60f394cc5d588672efa3ad099fe1fda译者:飞龙
第十四章:第十四章:为您的应用创建 App Clip
iOS 14 带来的主要功能之一是 App Clips。App Clips 为用户提供了一种快速的新方式来发现和使用您应用提供的内容。通过从二维码、链接、NFC 标签或其他机制触发 App Clip,它可以在几秒钟内出现在用户的设备上(即使没有安装您的应用),并使您应用的一些功能立即变得可用。
在本章中,我们将学习什么是 App Clip,它们用于什么,以及用户在使用它们时的体验将如何。我们将回顾用户可以用来触发它们的各种选项。然后,我们将开发一个 App Clip,并学习如何使用 App Store Connect 的新功能对其进行配置。最后,我们将学习如何使用本地体验来测试它们。到本章结束时,您将能够开发自己的 App Clips 并将您的应用提升到新的水平。
让我们总结本章的主题:
-
介绍 App Clips
-
开发您的第一个 App Clip
-
测试您的 App Clip 体验
让我们开始吧!
技术要求
本章的代码包包括三个起始项目,分别称为AppClipExample_start、AppClipExample_configure_start和AppClipExample_test。您可以在本书的代码包仓库中找到它们:
github.com/PacktPublishing/Mastering-iOS-14-Programming-4th-Edition
介绍 App Clips
App Clips 允许用户以快速和轻量级的方式发现应用。使用 App Clips,用户可以快速使用您应用的功能,而无需在他们的手机上安装应用。App Clip 是从您的应用中提取的一小部分功能,用户可以在不安装您的应用的情况下发现和使用。用户可以通过不同的触发器打开您的 App Clip,例如二维码、NFC 标签、消息中的链接、地图中的位置和网站中的智能横幅。App Clip 将以 App Clip Card 的形式出现在用户的首页上。App Clip Card 描述了您的 App Clip 的功能,以便用户可以选择打开和使用 App Clip 或将其关闭。让我们看看 App Clip Card 的样子:
图 14.2 -- App Clip 流程和步骤
上一张图片解释了 App Clip 的不同阶段:
-
调用方法:App Clip 调用方法是用户如何触发和打开 App Clip 的方式。在我们的例子中,用户使用他们的设备摄像头扫描放置在自行车上的二维码,App Clip 就会在他们的主屏幕上打开。在这种情况下,调用方法是二维码。我们将在本章后面更详细地探讨这些内容。
-
用户旅程:在调用之后,App Clip 会向用户提供一些选项供他们选择(例如,1 小时租赁 2 美元,24 小时租赁 5 美元)。用户在 App Clip 内进行他们想要的选项。
-
账户和支付:在我们的自行车租赁示例中,我们的 App Clip 需要识别哪个用户在租赁自行车,并且用户需要为这项服务付费。一些 App Clip 可能不需要注册用户账户或支付即可工作;这一步是可选的。
-
完整应用推荐:当做出自行车租赁的决定并且用户准备继续时,您的 App Clip 可以建议用户下载您的完整应用,这样他们下次使用您的服务时就可以使用它而不是 App Clip。建议整个应用是一个可选步骤,但建议这样做。
现在我们已经概述了 App Clip 遵循的高级步骤,我们将更仔细地查看可用的调用方法。
App Clips 调用方法
我们已经看到,为了显示 App Clip,用户需要调用或发现它。之前,我们讨论了这可以通过二维码、NFC 标签、消息中的链接等方式完成。以下是可用选项的总结:
-
App Clip 代码:每个 App Clip 代码都包含一个二维码和一个 NFC 标签,以便用户可以使用他们的相机扫描它或点击它。
-
NFC 标签。
-
二维码。
-
Safari App 标签。
-
消息中的链接。
-
在地图中放置卡片。
-
iOS 14 新的 App Library 中的"最近使用过的 App Clips"类别。
在本节中,我们学习了什么是 App Clip,当用户使用它时他们的旅程是什么,以及可以用来触发它的不同调用方法。在下一节中,我们将为咖啡店构建和配置一个 App Clip。
开发您的第一个 App Clip
在本节中,我们将从一个现有的应用开始,并逐步向其中添加 App Clip。打开本书代码包中的 AppClipExample_start 项目。如果您启动该应用,您将看到我们有一个咖啡店应用,我们可以订购三种不同类型的饮料,查看订单,并通过 Apple Pay 或输入我们的信用卡详细信息进行支付:
Figure 14.3 -- 我们应用的主要屏幕 -- 菜单、支付和信用卡控制器
注意,这个示例应用的目的是帮助我们构建有趣的部分:App Clip。一些功能,如信用卡和 Apple Pay 支付,并未完全实现;它们只是模拟了这个功能。
在我们跳入 App Clip 流程之前,让我们花一点时间来回顾一下项目的结构和内容:
Figure 14.4 -- 初始项目结构
该应用包含一个名为 AppClipExample 的单一目标。在该目标内部,我们拥有三个 ViewControllers(MenuViewController、PaymentViewController 和 CreditCardViewController)以及一些额外的视图(MenuView 和 MenuItemButton)。它仅包含一个名为 Item 的单一模型文件,该文件帮助我们处理菜单产品。我们还有其他一些常见文件,例如 AppDelegate 和 Assets -- 简短且简单。然而,重要的是要记住这一点,因为当我们开始添加我们的 App Clip 时,这种架构将会演变。
在我们继续之前,请确保您在项目中使用的是您自己的 Apple 开发者账户设置。在 AppClipExample 目标中,执行以下操作:
-
选择您自己的开发团队。
-
将 App ID 更改为
{yourDomain}.AppClipExample。
在下一节中,我们将为我们的咖啡店应用创建 App Clip。我们将首先为 App Clip 创建一个新的目标。然后,我们将学习如何在我们的应用和其 App Clip 之间共享代码和图像(以及如何在不想共享完全相同的代码时创建异常)。最后,在测试之前,我们将学习如何在 App Store Connect 中配置 App Clip 的体验。
创建 App Clip 的目标
为了创建 App Clip,Xcode 项目需要为其创建一个目标。目前,我们的项目只有一个目标:AppClipExample。让我们继续创建一个用于 App Clip 的新目标。按照以下步骤操作:
-
在 Xcode 中,点击文件 | 新建 | 目标。
-
在出现的模态窗口中,选择iOS | 应用 | App Clip ,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.05_B14717.jpg
图 14.5 -- 添加 App Clip 目标
-
按下一步。现在,你可以配置 App Clip 目标的一些初始值。
-
按照以下方式输入名称
MyAppClip:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.06_B14717.jpg图 14.6 -- App Clip 目标选项
-
当你点击完成 时,你会看到一个新弹窗:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.07_B14717.jpg
图 14.7 -- 激活新方案
-
按激活,以便该方案可用于构建和调试。现在,看看项目结构;你会看到为 App Clip 添加了一个新的目标:
图 14.8 -- App Clip 的新目标
但这并不是对项目所做的唯一更改。当 Xcode 添加新的 App Clip 目标时,在幕后做了几件事情:
-
它为构建和运行 App Clip 及其测试创建了一个新的方案。
-
它在App Clip 目标设置 | 签名与能力 选项卡中添加了一个名为按需安装能力的新功能。此功能将捆绑标识为 App Clip。
-
在同一选项卡中,你还可以检查 App Clip 的 Bundle 标识符是否包含与完整应用的 Bundle 标识符相同的根。因此,如果你的应用 Bundle 标识符是
{yourDomain}.AppClipExample,则 App Clip 将具有{yourDomain}.AppClipExample.Clip。这是因为 App Clip 只对应一个父应用,因此它们共享部分 Bundle 标识符。 -
它还添加了
_XCAppClipURL。如果你编辑 App Clip 的方案,你会看到一个名为该名称的环境变量。默认值是https://example.com。但是,为了激活它,你需要激活变量名称附近的选择框。激活后,App Clip 将在启动时作为scene(_ scene: UIScene, continue userActivity: NSUserActivity)的一部分接收此 URL,以便你可以测试你想要触发的流程,具体取决于接收到的 URL。
此外,Xcode 还为你的主应用目标创建了一个新的构建阶段,该阶段将 App Clip 内嵌其中:
图 14.9 -- 内嵌 App Clip 构建阶段
因此,正如你所见,尽管创建 App Clip 的目标是相对直接的,但在其内部还有很多事情在进行。现在你已经知道了所有细节。让我们在 iOS 模拟器上启动 App Clip(记得在启动时选择 MyAppClip App Clip 目标)。你将看到一个空白屏幕。这是正常的------我们仍然需要添加一些代码并准备我们的 App Clip!我们将在下一节中这样做。
与 App Clip 共享资源和代码
App Clips 通常需要从主应用中重用代码和资源。它们通常包含符合整个应用的一些功能。在我们的案例中,我们将创建一个显示主应用中所有内容的 App Clip,但不包括信用卡屏幕。为了提供一个快速且易于使用的 App Clip 体验,我们只允许用户查看菜单、查看他们的订单并使用 Apple Pay 支付;我们不希望他们在 App Clip 中输入任何信用卡详情。
让我们考虑从主应用中需要的所有文件和资源,并将它们添加到 App Clip 的目标中。让我们从资产开始。按照以下步骤操作:
-
在项目导航器中,点击
Assets.xcassets文件,并在应用和 App Clip 的Assets文件中,你可以删除MyAppClip文件夹内的Assets文件。否则,你将有两个AppIcon引用(每个资产文件中各一个),你将得到一个编译错误:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.11_B14717.jpg图 14.11 -- 删除 MyAppClip 内部的第二个 Assets 文件
将主应用的
Assets文件移动到项目顶部并重命名为SharedAssets也是一个好的实践。这样做可以让其他开发者知道该文件适用于所有目标:图 14.12 -- 项目顶部的 SharedAssets
一旦你做了这些更改,确保你可以构建和编译这两个目标;也就是说,应用和 App Clip。
现在,让我们包括 App Clip 目标以及我们需要的所有代码。之前我们提到,我们想要在主应用中找到的相同功能,除了信用卡屏幕。
-
在项目导航器中,选择以下文件并将它们添加到 App Clip 目标中:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.13_B14717.jpg
图 14.13 -- 与 App Clip 目标共享代码文件
注意我们如何在
ViewController、Views和Model文件夹中共享所有文件,除了CreditCardViewController。你现在已经共享了 App Clip 所需的所有图像和代码。然而,你仍然需要重用一些内容:故事板流程。
-
打开你的
AppClipExample目标中的Main.storyboard文件。稍微缩小一下,选择除了CreditCardViewController之外的所有内容(我们不希望在 App Clip 中包含这个):https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.14_B14717.jpg图 14.14 -- 复制你的 App 的 Main.storyboard 文件内容
-
在复制了上一张截图中的高亮元素之后,将它们粘贴到你的 MyAppClip 目标的
Main.storyboard文件中。 -
现在,选择 Navigation Controller 选项,并在右侧的 Options (选项)面板中勾选 Is Initial View Controller (是否为初始视图控制器)选项:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.15_B14717.jpg
图 14.15 -- 为你的 App Clip 指定入口点
现在,你的 App Clip 已经有了足够的代码、资源和流程,可以尝试运行它。
-
选择 MyAppClip 目标并启动它。此时应该没有问题地编译和运行。
然而,存在问题。如果你启动 App Clip 并下单,你会注意到我们仍然显示了 使用信用卡支付 按钮。之前我们提到,我们希望我们的 App Clip 只使用 Apple Pay 来简化服务,正如苹果公司的建议。在下一节中,我们将通过学习如何使用 Active Compilation Conditions 有条件地使用代码的一部分,根据执行它的目标来达到这一点。
使用活动编译条件
在上一节中,我们学习了如何在应用和 App Clip 之间共享代码和资源。这次,当 App Clip 执行特定文件时,我们需要"删除"一些代码。具体来说,我们希望在 App Clip 执行时隐藏 PaymentViewController。
要做到这一点,我们需要与 Active Compilation Conditions(活动编译条件)一起工作。按照以下步骤操作:
-
在
APPCLIP列表中,如图下所示截图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.16_B14717.jpg图 14.16 -- 添加活动编译条件
-
在设置了
APPCLIP标志后,继续打开PaymentViewController文件。在viewDidLoad()方法的末尾添加以下代码:swift#if APPCLIP buttonPayByCard.isHidden = true #endif通过这段代码,我们告诉编译器,只有在我们执行 App Clip 目标时,才添加这一行。
-
让我们试一试。执行应用和 App Clip,比较两个屏幕。当 App Clip 启动时,你不应该看到 使用信用卡支付 按钮:
图 14.17 -- App Clip(左侧)与 app(右侧)对比
如你所见,我们通过使用 Active Compilation Conditions 实现了显示 UI 不同部分的目标。
这太好了!我们已经配置了一个完美的 App Clip,它可以运行并显示我们希望用户看到的内容。在下一节中,我们将深入到这个过程的关键部分:调用 App Clip。
配置、链接和触发您的 App Clip
在本节中,随着 App Clip 准备就绪,我们将学习如何配置、链接和触发 App Clip。
用户可以通过使用各种调用来触发 App Clip,以下是一些示例:
-
扫描物理位置处的 NFC 标签或视觉代码
-
在 Siri 建议(基于位置的建议)中轻触
-
在 Maps 应用中轻触链接
-
在网站上轻触智能应用横幅
-
在 Messages 应用中轻触某人分享的链接(仅作为文本消息)
为了确保这些调用能够正常工作,您必须配置 App Clip 以处理链接,并且还需要配置 App Store 的 Connect App Clip Experiences。我们现在就来完成这项工作。
重要提示
当用户安装 App Clip 对应的应用时,完整的应用将替换 App Clip。从那时起,所有的调用都将启动完整的应用而不是 App Clip。因此,您的完整应用必须处理所有可能的调用并提供 App Clip 的功能。
App Clip 需要一个入口点,以便用户能够发现和启动它。在本节中,我们将回顾三个主题:
-
配置链接处理
-
配置 App Clip 体验
-
配置智能应用横幅
到本节结束时,我们的项目将有一个完全配置好的 App Clip 准备就绪。让我们开始吧!
配置链接处理
我们的第一步是配置我们的 Web 服务器和 App Clip 以处理链接。您可以使用本章代码包中的项目 AppClipExample_configure_start 来帮助完成这项工作。
如果您想在您的网站上显示您的 App Clip,您需要执行以下步骤:
-
在您的 Web 服务器上配置
apple-app-site-association文件。 -
向您的 App Clip 添加关联域名权限。
-
在您的 App Clip 中处理
NSUserActivity。 -
首先,让我们按照以下方式配置
apple-app-site-association文件:swift{ "appclips" : { "apps" : ["<Application Identifier Prefix>.<Bundle Identifier>"] }, ... }此文件应位于服务器的根目录中。如果您已经设置了通用链接,那么您应该已经有这个文件了。您需要向其中添加高亮显示的代码,以便您能够引用您的 App Clip。请记住使用您自己的应用程序标识前缀和包标识符。
-
接下来,让我们添加关联域名权限。在 项目导航器 窗口中,选择项目和 App Clip 目标,然后转到 签名与能力:![Figure_14.18 -- 签名与能力
![Figure_14.18_B14717.jpg]
图 14.18 -- 签名与能力
-
接下来,添加一个新的关联域名,如图中所示:![Figure_14.19 -- 添加关联域名
![Figure_14.19_B14717.jpg]
图 14.19 -- 添加关联域名
现在您的服务器和 App Clip 已经配置好了,让我们来处理
NSUserActivity。 -
继续编辑 App Clip 方案。在
_XCAppClipURL变量下,将其分配以下值:https://myappclip.com/test?param=value。使用这个值设置,让我们学习如何处理它。
-
在
SceneDelegate.swift文件中。添加以下实现:swiftfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { for activity in connectionOptions.userActivities { self.scene(scene, continue: activity) } } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { // 1 guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL, let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true) else { return } print(url) //2 guard let path = components.path, let params = components.queryItems else { return } print(path) print(params) // Handle your own navigation based on path and params }这两种方法正在处理当 App Clip 被 URL 类型的元素触发时将接收到的
NSUserActivity信息。看看在scene(...)方法中,我们是如何检查活动是否为NSUserActivityTypeBrowsingWeb类型,然后检查URL、path和components元素。在这里,你可以将你的 App Clip 导航到正确的元素。如果你启动 App Clip 并检查控制台的输出,你会看到这个:swifthttps://myappclip.com/test?param=value /test [param=value]
如你所见,我们正在处理在 _XCAppClipURL 目标环境变量中定义的测试 URL,并从中提取所需的路径和组件。当你想根据传入的 URL 在你的 App Clip 中处理不同的流程时,你可以这样测试。
如果你的应用是用 SwiftUI 构建的,那么你可以这样处理:
swift
var body: some Scene {
WindowGroup {
ContentView().onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
guard let url = activity.webpageURL else { return }
// Navigate to proper flow based on the url
}
}
}
通过在 ContentView 中定义 onContinueUserActivity(NSUserActivityTypeBrowsingWeb),你可以使用传递的 activity 对象并从中提取传入的 URL。通过分析 URL,你可以链接到 App Clip 的正确部分。
现在我们已经配置了我们的服务器和 App Clip 以处理链接,让我们继续配置我们的 App Clip 体验。
配置我们的 App Clip 体验
随着 App Clip 和你的服务器准备好处理链接,我们可以开始配置我们的 App Clip 体验。App Clip 体验在 App Store Connect 中定义,并定义了 App Clip 卡片和不同场景下你想要处理的链接。App Clip 卡片看起来像这样:
图 14.20 -- App Clip 卡片
如前一个截图所示,App Clip 卡片包含以下内容:
-
一个头部图像,描述你的应用或 App Clip 的主要功能:在这个例子中,我们展示了有人在准备咖啡。
-
一个标题,描述 App Clip 的名称:Mamine Cafe。
-
一个副标题,描述 App Clip 提供的功能:三指点单咖啡。
-
一个按钮,描述要执行的操作(例如打开视图的 App Clip):查看。
-
额外信息页脚:App Clip 的主要应用以及一个链接到 App Store 以下载它的链接。
这个 App Clip 卡片是设备将显示给用户以便他们可以启动你的 App Clip 的内容。我们将在 App Store Connect 中进行配置。
一旦你通过 App Store Connect 网站创建了相应的应用并上传了包含 App Clip 的构建版本,你将能够配置你的 App Clip 体验:
图 14.21 -- App Clip 体验配置
如你所见,在默认的 App Clip 体验中,有三个主要配置项:
-
.png/.jpg。无透明度。 -
副标题的副本:最大字符数为 43。
-
行动号召:在这里,你可以选择打开、查看和播放。
您还可以点击 编辑高级体验 来配置不同的触发器和流程。如果您想从 NFC 标签或视觉码启动 App Clip,将您的 App Clip 与物理位置关联,或为多个业务创建 App Clip,那么您需要高级体验。首先,您需要指定将触发 App Clip 体验的 URL:
图 14.22 -- 调用 App Clip 体验的 URL 配置
在按下 下一步 后,您可以配置 App Clip 卡:
图 14.23 -- 配置高级 App Clip 卡
在这一点上,您可以配置卡的语言,甚至指定体验是否在特定位置触发。
添加高级 App Clip 体验可以让您的应用针对不同的 URL 显示不同的 App Clip。例如,如果您有一个咖啡店应用,您可以有一个用于显示菜单的 App Clip,一个用于立即订购咖啡的 App Clip,一个用于显示客户积分卡的 App Clip,等等。
在本节中,我们学习了如何在 App Store Connect 中配置 App Clip 及其体验。现在,让我们学习如何配置智能应用横幅,以便您可以在网站上触发横幅,让用户显示您的应用和 App Clip。
配置智能应用横幅
通过在您的网站上添加智能应用横幅,您为用户提供了一种快速且原生的方式来发现和启动您的应用。您需要在您的网站 HTML 文件中添加以下元标签(您希望横幅显示的位置):
swift
<meta name="apple-itunes-app" content="app-id=myAppStoreID, app-clip-bundle-id=appClipBundleID, affiliate-data=myAffiliateData, app-argument=myAppArgument">
您需要将突出显示的值替换为您自己的。另外,请注意,当您启动 App Clip 时,app-argument 不可用。请记住,您应该将显示此横幅的任何页面的域名添加到您的应用和您的 App Clip 的关联域名权限中。
在本节中,我们学习了如何配置链接处理、App Clip 体验和智能应用横幅。在下一节中,我们将学习如何在开发过程中测试我们的 App Clip。
测试您的 App Clip 体验
一旦您完成开发并配置了您的 App Clip,就是时候测试一切,以确保您的 App Clip 体验按预期工作。有三种方法可以测试您的 App Clip 体验:
-
通过在 Xcode 中调试调用 URL(我们在这章中已经多次看到,当使用
_XCAppClipURL时)。 -
通过在 TestFlight 中为测试者创建 App Clip 体验(这样您的应用就准备好发布,并且是完整的)。
-
通过在设备上创建本地体验并在开发过程中测试来自 NFC 或视觉码的调用。
让我们更深入地探讨最后一点。让我们使用本书代码包中的 AppClipExample_test 项目,以便我们可以在其上测试我们的 App Clip 体验。
在开发过程中使用本地体验测试你的应用小部件体验的一个优点是,你不需要配置相关的域名,修改服务器,或处理 TestFlight。我们可以在本地完成所有这些操作。让我们开始吧:
-
首先,在任何设备上构建和运行你的应用和小部件。然后,在设备上,打开 设置 | 开发者 | 本地体验 并选择 注册本地体验... :https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_14.24_B14717.jpg
图 14.24 -- 本地体验设置
-
现在,你可以配置本地体验,如图下所示。请记住,为应用使用自己的值:
图 14.25 -- 本地体验数据
要启动应用小部件卡片,你可以使用任何允许你生成与上一屏幕(在 URL 前缀 下)中指定的相同 URL 的 QR 码或 NFC 标签的工具。完成此操作后,当你用设备扫描时,你的应用小部件卡片应该会出现。
重要提示
在本地体验中定义的捆绑 ID 必须与你的应用小部件的捆绑 ID 匹配。
应用小部件必须在设备上安装。
如果相机应用没有打开应用小部件,请尝试使用 iOS 控制中心的 QR 码扫描仪(如果你没有,可以通过前往 设置 | 控制中心 来添加它)。
在本节中,我们学习了如何配置本地体验,以便在开发过程中测试我们的应用小部件卡片。现在,让我们总结本章内容。
摘要
在本章中,我们回顾了 iOS 14 最好的新功能之一:应用小部件。我们解释了什么是应用小部件,用户的旅程是什么,开发应用小部件时应关注哪些功能,以及有哪些选项可以调用它们。
在学习基础知识后,我们为一家咖啡店应用开发并配置了我们的第一个应用小部件。我们对项目进行了重构,以便在应用和小部件之间共享代码和资源。然后,我们学习了如何使用 活动编译条件 来触发代码库的一部分,但仅限于应用小部件或应用本身,以及如何配置我们的应用和服务器以处理链接。
最后,我们学习了如何在 App Store Connect 中配置应用小部件体验,以及如何在开发过程中测试它们。
在下一章中,你将了解视觉框架。
第十五章:第十五章:使用 Vision 框架进行识别
Vision 框架已经对开发者开放了几年。苹果公司一直在为其引入更好的功能,从文本识别到图像识别。在 iOS 14 中,Vision 框架带来了对文本识别和其他现有功能的更多改进,但它还允许开发者执行两种不同的操作:手部和身体姿态识别。这些新功能为开发者带来的可能性是无限的!只需想想健身房应用、瑜伽应用、健康应用等等。
在本章中,我们将学习关于 Vision 框架的基础知识以及如何使用文本识别的新进展。我们还将了解新的手部关键点识别,构建一个能够检测四个手指和拇指尖端的演示应用。本章代码包还提供了一个类似的示例,展示了人体姿态识别。以下几节将讨论这些主题:
-
Vision 框架简介
-
在图像中识别文本
-
实时识别手部关键点
到本章结束时,你将能够充满信心地使用 Vision 框架,能够将本章中解释的技术应用于实现 Vision 提供的任何类型的识别,从图像中的文本识别到视频中手部和身体姿态的识别。
技术要求
本章的代码包包括一个名为 HandDetection_start 的入门项目,以及几个名为 Vision.playground 和 RecognitionPerformance_start.playground 的游乐场文件。它还包含一个名为 BodyPoseDetection_completed 的完成示例。你可以在代码包仓库中找到它们:
https://github.com/PacktPublishing/Mastering-iOS-14-Programming-4th-Edition
Vision 框架简介
自 App Store 开始以来,许多应用都利用摄像头通过图像和视频识别构建了出色的功能。想想现在可以扫描支票或信用卡的银行应用,这样用户就不需要输入所有数字。还有可以拍照名片并提取相关信息的网络应用。甚至你 iPhone 上的照片应用也能检测照片中的面孔并将它们分类。
Vision 框架为开发者提供了一套强大的功能,使其比以往任何时候都更容易实现这些功能:从文本和图像识别到条形码检测、面部关键点分析,现在,随着 iOS 14 的推出,还有手部和身体姿态识别。
Vision 还允许使用 Core ML 模型,以便开发者能够增强他们应用中的对象分类和检测。Vision 自 iOS 11 和 macOS 10.13 以来一直可用。
Vision 中有几个概念在所有类型的检测中都是通用的(文本检测、图像检测、条形码检测等),包括VNRequest、VNRequestHandler和VNObservation实体:
-
VNRequest是我们想要执行的任务。例如,VNDetectAnimalRequest将用于在图片中检测动物。 -
VNRequestHandler是我们想要检测的方式。它允许我们定义一个完成处理程序,在那里我们可以处理结果并按需塑造它们。 -
VNObservation封装了结果。
让我们看看一个结合所有这些概念并展示 Vision 如何轻松帮助我们检测图像中文字的示例。打开名为Vision.playground的沙盒。这个示例代码从一个特定的 URL 获取图像并尝试从中提取/检测任何文本。所使用的图像是这张:
图 15.01 -- 使用 Vision 提取文本的示例图像
如果我们尝试从这张图像中提取文本,我们应该得到像Swift 数据结构和算法或作者姓名,或标题下面的描述这样的结果。让我们回顾沙盒中的代码:
swift
import Vision
let imageUrl = URL(string: "http://marioeguiluz.com/img/portfolio/Swift%20Data%20Structures%20and%20Algorithms%20Mario%20Eguiluz.jpg")!
// 1\. Create a new image-request handler.
let requestHandler = VNImageRequestHandler(url: imageUrl, options: [:])
// 2\. Create a new request to recognize text.
let request = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let recognizedStrings = observations.compactMap { observation in
// Return the string of the top VNRecognizedText instance.
return observation.topCandidates(1).first?.string
}
// Process the recognized strings.
print(recognizedStrings)
}
// 3\. Select .accurate or .fast levels
request.recognitionLevel = .accurate
do {
// 4\. Perform the text-recognition request.
try requestHandler.perform([request])
} catch {
print("Unable to perform the requests: \(error).")
}
让我们逐条查看编号的注释:
-
首先,我们使用给定的图像 URL 创建一个
VNImageRequestHandler实例。我们实例化这个处理程序以在图像上执行 Vision 请求。记住,我们稍后需要调用perform(_:)来启动分析。 -
现在我们创建一个
request(VNRecognizeTextRequest)实例,我们将在之前实例化的requestHandler实例上执行它。你可以在一个requestHandler实例上执行多个请求。我们定义了一块代码,当请求完成时将执行该代码。在这个块中,我们从请求结果中提取观察结果(VNRecognizedTextObservation实例)。这些观察结果将包含从图像中分析出的文本的潜在结果(VNRecognizedText实例)。我们打印出每个观察结果中的topCandidate,根据 Vision 参数,这应该是最佳匹配。 -
我们可以指定请求的识别级别。在这个例子中,我们使用
.accurate(另一种选择是.fast)。我们将在稍后看到.fast的结果以及何时使用其中一个。 -
最后,我们在
requestHandler实例上执行请求,使用perform(_:)方法执行所有操作。
如果你执行代码,沙盒中的控制台将显示以下内容:
swift
["Erik Azar, Mario Eguiluz", "Swift Data", "Structure and", "Algorithms", "Master the most common algorithms and data structures,", "and learn how to implement them efficiently using the most", "up-to-date features of Swift", "Packt>"]
这些结果看起来很棒,对吧?如果你重新检查图像,我们会从其中提取正确的文本!作者姓名、标题(每行)、描述等等!看起来是个很棒的结果!但你有没有注意到,当你执行沙盒时,它需要一段时间才能完成?这是因为我们使用了.accurate选项。让我们看看如果我们使用.fast会发生什么。在沙盒代码中更改它:
swift
// 3\. Select .accurate or .fast levels
request.recognitionLevel = .fast
输出如下:
swift
["Swift Data", "Structure and", "Algorithms", "upto4atefeaturesofSwift3", "Packt>", "ErfkAz•r. M•rb Eguluz", "ml5tertket(w4VIthMsarodats5tr&KtUre", "learnItolpIettmeffK1WttIY5lt1fft", "LIJJ"]
这次,分析可以做得更快,但正如你所见,对于我们所期望的结果(我们希望正确地检测文本!)来说,结果要差得多。为什么有人会优先考虑速度而不是准确性呢?嗯,对于某些应用来说,速度是关键,为了它牺牲一些准确性是可以接受的。想想基于实时摄像头的翻译或者应用实时滤镜拍照。在这些场景中,你需要快速处理。我们将在本章后面进一步讨论这个问题。
这个游乐场示例应该能帮助你了解 Vision 所包含的惊人潜力。仅仅通过几行代码,我们就能够处理并提取图像中的文本,没有任何问题或复杂的操作。Vision 允许开发者做令人惊叹的事情。让我们在接下来的章节中更深入地探讨它,从对图像文本检测的更详细分析开始。
在图像中识别文本
自从 Vision 框架的第一个迭代以来,它一直在改进图像中检测文本的能力。在本节中,我们将学习一些最先进的技术,以在 iOS 14 上获得最佳结果。
在上一节中,我们看到了在 Vision 中,文本检测可以通过两种不同的方式发生,这取决于我们在请求中指定的recognitionLevel的值:.fast和.accurate。让我们看看它们的区别:
-
.accurate。它处理旋转文本或不同字体的效果不如.accurate方法。 -
.fast但更准确(当然!)它的工作方式与我们的大脑识别单词的方式相同。如果你读"m0untain"这个词,你的大脑可以从它中提取"mountain",并且知道 0(零)代表一个 o。如果你使用.fast,它按字符识别,0(零)在你的结果中仍然是 0(零),因为没有任何上下文被考虑。
在两种情况下,在初始识别阶段完成后,结果都会传递到一个传统的自然语言处理器进行语言处理,其结果是观察结果。整个过程仅在设备上发生。
那么,什么时候应该使用.fast呢?你可能想知道。嗯,有一些场景中它比.accurate更方便:
-
为了快速读取代码或条形码
-
当用户交互是一个关键方面,你希望从文本检测中得到快速响应时
为了展示识别级别的差异,让我们使用不同的技术分析相同的图像。你还将学习一些可以应用到你的项目中的有用技巧。按照这里给出的步骤进行:
-
请打开名为
RecognitionPerformance_start.playground的游乐场。代码与我们之前章节尝试的代码大致相同。现在我们使用的图像中包含一个 4 位数,代表书籍的序列号:
图 15.02 -- 作者名字下方的带有序列号(1015)的书籍封面
如果你仔细观察数字字体,你会发现对于计算机来说,判断某些数字是数字还是字母可能有些棘手。这是故意为之的。在这个例子中,我们将测试 Vision 的能力。
-
继续执行 playground 代码。控制台输出应该如下所示:
swift["Erik Azar, Mario Eguiluz", "1015", "Swift Data", "Structure and", "Algorithms", "Master the most common algorithms and data structures,", "and learn how to implement them efficiently using the most", "up-to-date features of Swift", "Packt>"] 1.9300079345703125 seconds
我们已经成功检索到书的序列号:1015。代码也在测量完成文本识别过程所需的时间。在我们的案例中,它花费了1.93 秒 (这可能会因计算机而异,也可能因执行而异)。我们能做得更好吗?让我们尝试一些可以帮助我们提高处理时间同时保持相同准确性的技术。我们将从感兴趣区域开始。
感兴趣区域
有时候,当我们使用 Vision 分析图像时,我们不需要处理整个图像。例如,如果我们处理的是一种特定的表格,我们事先知道名字总是位于文档的顶部,我们可能只想处理那个区域。如果我们只需要特定区域,处理整个表格只会浪费时间和资源。
让我们假设在之前的例子(书籍封面)中,我们想要提取的序列号总是位于左上角。我们如何加快 1.93 秒的处理时间?我们可以通过定义感兴趣区域来实现。定义感兴趣区域将告诉 Vision 只处理该区域,避免处理图像的其余部分。这将导致更快的处理时间。
regionOfInterest是VNRequest的CGRect属性:
-
它定义了一个矩形区域,请求将在该区域内执行。
-
矩形被归一化到图像的尺寸,这意味着感兴趣区域的宽度和高度从 0 到 1。
-
矩形的起点在图像的左下角,即(0,0)。右上角将是(1,1)。
-
默认值是
{``{0,0},{1,1}},它覆盖了从左下角(0,0)到右上角(1,1),宽度为 1,高度为 1:整个图像。
在以下图中,你可以看到我们需要定义的感兴趣区域来捕获序列号(1015):
图 15.03 -- 感兴趣区域
让我们把那个区域添加到上一节中的代码:
-
在
ScanPerformance_start.playground项目中,在将recognitionLevel设置为.accurate之后添加以下代码:swiftrequest.regionOfInterest = CGRect(x: 0, y: 0.8, width: 0.7, height: 0.2) -
现在启动 playground 并在控制台中检查结果:
swift["Erik Azar, Mario Eguiluz", "1015"] 1.2314139604568481 seconds与之前的结果相比,有几个不同之处:
-
我们不再提取那么多文本。现在我们定义了感兴趣区域,我们只提取该区域包含的单词/数字。
-
我们将处理时间从 1.93 秒减少到 1.23 秒。这提高了 36%。
-
-
现在我们尝试将感兴趣区域缩小,仅捕获序列号。将区域修改为以下:
swiftrequest.regionOfInterest = CGRect(x: 0, y: 0.8, width: 0.3, height: 0.1) -
启动游乐场。现在控制台输出如下:
swift.fast for recognitionLevel instead of .accurate, if what we want is speed? Let's see what happens. -
将此行修改为使用
.fast:swiftrequest.recognitionLevel = .fast -
保存并执行。检查控制台输出:
swift["Iois"] 0.5968900661468506 seconds
你可以看到这次,处理时间再次缩短,但结果完全不精确。我们检测到的不是1015,而是错误地得到了Iois。
然而,在具有领域知识的情况下,有一种常见的解决这种情况的方法。在我们的例子中,我们知道处理后的字符应该是数字。因此,我们可以调整从视觉输出的结果来改进结果并修复误分类。例如,查看以下调整:
-
字符"I"可以是"1。"
-
字符"o"可以是"0。"
-
字符"s"可以是"5。"
让我们在代码中实现这个功能:
-
在游乐场文件的最后,添加以下方法:
swiftextension Character { func transformToDigit() -> Character { let conversionTable = [ "s": "5", "S": "5", "o": "0", "O": "0", "i": "1", "I": "1" ] var current = String(self) if let alternativeChar = conversionTable[current] { current = alternativeChar } return current.first! } }我们通过添加一个名为
transformToDigit()的新方法来扩展Character类。这个新方法将帮助我们改进潜在的误分类。注意在方法本身中,我们有一个与形状相似的字母字符表,这些字母与数字相关。我们所做的是将这些字母转换成相应的数字。 -
现在让我们使用它。在
print(recognizedStrings)行下方,添加以下代码:swiftif let serialNumber = recognizedStrings.first { let serialNumberDigits = serialNumber.map { $0.transformToDigit() } print(serialNumberDigits) }我们正在获取视觉处理的结果;在我们的例子中,它是
"Iois",并且对于每个字符,我们对其应用我们新的transformToDigit()方法。 -
执行代码,你将在控制台看到以下结果:
swift["Iois"] ["1", "0", "1", "5"] 0.5978780269622803 seconds
看起来很棒!注意现在将"Iois"转换成"1" "0" "1" "5"后看起来好多了。同时,注意处理时间并没有增加太多;这个操作相对容易计算。
现在我们总结一下本节我们做了什么,以及每一步的改进。我们首先处理了一张整个图像,并使用.accurate识别级别,这花费了我们 1.93 秒。然后,我们应用了感兴趣区域,只处理我们感兴趣的图像部分,将处理时间减少到 1.23 秒。之后,我们将.accurate改为.fast。这一改变将处理时间减少到 0.59 秒,但结果是不正确的。最后,我们实现了一个简单的算法来改进结果,使它们与.accurate级别一样好。所以,最终我们得到了完美的结果,处理时间仅为 0.59 秒,而不是 1.93 秒!
在下一节中,你将了解 iOS14 的一个新功能,即手势检测。
实时识别手势地标
iOS 14 中 Vision 的一个新增功能是手部检测。这个新功能可以检测图像和视频中的手部,允许开发者以很高的精度找到视频帧或照片中手腕和各个手指的位置。
在本节中,我们将解释手部检测背后的基础知识,并通过一个示例项目演示其工作原理。让我们从我们将能够识别的手部特征点开始。
理解手部特征点
我们将能够在手中检测到 21 个特征点:
-
拇指 4 个点
-
每个手指 4 个点(总共 16 个点)
-
腕部 1 个点
如您所见,Vision 可以区分手指和拇指。在手指和拇指上,都有 4 个感兴趣点。以下图示显示了这些特征点的分布情况:
图 15.04 -- 手指和拇指特征点
注意,在手腕中间也有一个特征点。
对于四个手指,我们可以使用以下键单独访问它们:
-
小指 -
中指 -
无名指 -
indexFinger
在每个手指内部,我们可以访问四个不同的特征点:
-
指尖
-
DIP
-
PIP
-
MCP
注意,对于拇指,这些名称略有不同(TIP、IP、PIP 和 CMC)。在本节稍后我们将构建的示例代码中,我们将演示如何使用这些点和每个手指以及拇指。
Vision 能够同时检测不止一个手部。我们可以指定我们想要检测的最大手部数量。此参数将影响检测的性能。使用maximumHandCount设置限制。
为了性能和准确性,如果手部不在帧的边缘附近,如果光线条件良好,以及如果手部与摄像头角度垂直(因此整个手部都可见,而不仅仅是边缘),则更好。此外,考虑到有时脚部可能被识别为手部,因此请避免混淆。
理论就到这里;让我们直接进入代码示例!我们将构建一个演示应用程序,该程序将能够使用手机的正面视频摄像头检测手部特征点,并在检测到的点上显示叠加层。
实现手部检测
在本节中,我们将实现一个演示应用程序,该程序将能够使用手机的正面视频摄像头检测手部特征点。
该项目的代码包包含初始项目和最终结果。请打开名为HandDetection_start的项目。
该项目包含两个主要文件:一个名为CameraView.swift的UIView实例和一个名为CameraViewController.swift的UIViewController实例。
视图包含辅助方法来在坐标上绘制点。它将作为覆盖层绘制在摄像头视频流之上。只需知道,showPoints(_ points: [CGPoint], colour: UIColor) 方法将允许我们在视频摄像头流的覆盖层上绘制一个 CGPoint 结构体的数组。
视图控制器将是示例的核心,我们将在这里实现执行手部检测的相关代码。请打开 CameraViewController.swift 文件。让我们检查我们将逐步填充的代码框架。
在文件顶部,我们定义了四个属性:
-
handPoseRequest: VNDetectHumanHandPoseRequest。我们将在此视频流的顶部应用此请求,以检测每一帧中的手部关键点。如果我们检测到任何,我们将在覆盖层上显示一些点来显示它们。 -
videoDataOutputQueue,cameraView, 和cameraFeedSession。
使用 viewDidAppear 和 viewWillDisappear 方法,我们正在启动/创建和停止摄像头的 AVCaptureSession。
最后,在接下来的四个方法中,我们有四个待办事项注释,我们将逐一实现以创建此应用程序。让我们总结一下我们将要执行的待办事项:
-
待办事项 1: 只检测一只手。
-
待办事项 2: 创建视频会话。
-
待办事项 3: 在视频会话中执行手部检测。
-
待办事项 4: 处理并显示检测到的点。
我们将在以下小节中实现这四个任务。
检测手部
视觉不仅可以一次检测一只手。我们要求它检测的手越多,性能影响就越大。在我们的示例中,我们只想检测一只手。通过在请求中将 maximumHandCount 设置为 1,我们将提高性能。
让我们从在 // 待办事项 1 下方添加以下代码开始:
swift
// TODO 1: Detect one hand only.
handPoseRequest.maximumHandCount = 1
现在,让我们创建一个视频会话来捕获设备前置视频摄像头的视频流。
创建视频会话
对于第二个任务,我们将填充 setupAVSession() 方法内的代码。请将以下代码粘贴到方法中:
swift
// TODO 2: Create video session
// 1 - Front camera as input
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
fatalError("No front camera.")
}
// 2- Capture input from the camera
guard let deviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
fatalError("No video device input.")
}
首先,我们通过以下方式创建 videoDevice: AVCaptureDevice 实例,查询视频前置摄像头(如果存在!):
swift
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
fatalError("No front camera.")
}
然后,我们使用那个 videoDevice 生成一个 deviceInput: AVCaptureDeviceInput 实例,它将是用于流的视频设备,以下代码所示:
swift
guard let deviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
fatalError("No video device input.")
}
现在添加以下代码:
swift
let session = AVCaptureSession()
session.beginConfiguration()
session.sessionPreset = AVCaptureSession.Preset.high
// Add video input to session
guard session.canAddInput(deviceInput) else {
fatalError("Could not add video device input to the session")
}
session.addInput(deviceInput)
let dataOutput = AVCaptureVideoDataOutput()
if session.canAddOutput(dataOutput) {
session.addOutput(dataOutput)
// Add a video data output.
dataOutput.alwaysDiscardsLateVideoFrames = true
dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
dataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
} else {
fatalError("Could not add video data output to the session")
}
session.commitConfiguration()
cameraFeedSession = session
在创建 videoDevice 实例后,我们创建一个新的 session: AVCaptureSession 实例。会话创建后,我们将 videoDevice 作为输入,创建并配置一个输出以处理视频流。我们通过调用以下代码将类本身分配为 dataOutput AVCaptureVideoDataOutputSampleBufferDelegate:
swift
dataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
这意味着当前置视频摄像头捕获新的帧时,我们的会话将处理它们并将它们发送到我们的代理方法,我们将在下一步(待办事项 3)中实现。
在视频会话中执行手部检测
现在我们已经设置并配置了视频会话,是时候处理每一帧了,并尝试检测任何手部和它们的地标!我们需要实现captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection)方法。
在// TODO 3: Perform hand detection on the video session行下,添加以下代码:
swift
var thumbTip: CGPoint?
var indexTip: CGPoint?
var ringTip: CGPoint?
var middleTip: CGPoint?
var littleTip: CGPoint?
我们想要检测四个手指(食指、中指、无名指和小指)以及大拇指的指尖。因此,我们创建了五个类型为CGPoint的变量来存储它们的坐标,如果找到了的话。
在这些新行之后,添加以下代码:
swift
let handler = VNImageRequestHandler(cmSampleBuffer: sampleBuffer, orientation: .up, options: [:])
do {
try handler.perform([handPoseRequest])
guard let observation = handPoseRequest.results?.first else {
return
}
// Get observation points
} catch {
cameraFeedSession?.stopRunning()
fatalError(error.localizedDescription)
}
使用这段代码,我们要求 Vision 在sampleBuffer(视频流)上执行handPoseRequest。然后,我们使用guard(使用guard)来防止没有检测到观察结果的情况(这样如果视频帧中没有手,我们就会在这里停止)。
但是如果guard没有触发,这意味着我们有一些手部地标需要处理。在// Get observation points行之后添加以下代码:
swift
let thumbPoints = try observation.recognizedPoints(.thumb)
let indexFingerPoints = try observation.recognizedPoints(.indexFinger)
let ringFingerPoints = try observation.recognizedPoints(.ringFinger)
let middleFingerPoints = try observation.recognizedPoints(.middleFinger)
let littleFingerPoints = try observation.recognizedPoints(.littleFinger)
guard let littleTipPoint = littleFingerPoints[.littleTip], let middleTipPoint = middleFingerPoints[.middleTip], let ringTipPoint = ringFingerPoints[.ringTip], let indexTipPoint = indexFingerPoints[.indexTip], let thumbTipPoint = thumbPoints[.thumbTip] else {
return
}
现在我们正在从观察结果中提取与拇指和四个手指相关的任何recognizedPoints()实例。请注意,我们使用try来执行此操作,因为结果并不保证。使用提取出的识别点,我们稍后使用guard语句解开每个手指和大拇指的指尖。
在这个阶段,我们应该有五个变量,分别存储每个手指的指尖坐标以及大拇指的坐标。
尽管我们已经有了我们正在寻找的五个坐标,但我们仍然需要执行一个额外的步骤。Vision 坐标与AVFoundation坐标不同。让我们转换它们;在最后一个guard语句之后添加以下代码:
swift
thumbTip = CGPoint(x: thumbTipPoint.location.x, y: 1 - thumbTipPoint.location.y)
indexTip = CGPoint(x: indexTipPoint.location.x, y: 1 - indexTipPoint.location.y)
ringTip = CGPoint(x: ringTipPoint.location.x, y: 1 - ringTipPoint.location.y)
middleTip = CGPoint(x: middleTipPoint.location.x, y: 1 - middleTipPoint.location.y)
littleTip = CGPoint(x: littleTipPoint.location.x, y: 1 - littleTipPoint.location.y)
如您所见,两个系统中的x坐标是相同的,但y坐标不同。在 Vision 中,左下角是(0,0)。因此,我们只需要将 Vision 点的y坐标减去 1,就可以得到AVFoundation系统上的结果。
太棒了!在这个阶段,我们已经有了手部地标检测系统,并以AVFoundation的CGPoint坐标形式得到结果。最后一步是绘制这些点!
在catch块(它外面)之后添加以下代码,正好在func captureOutput(...)方法的末尾:
swift
DispatchQueue.main.sync {
self.processPoints([thumbTip, indexTip, ringTip, middleTip, littleTip])
}
我们在主线程中调用processPoints(...)方法,因为我们希望它在 UI 上工作,所以我们通过将这项工作调度到正确的线程来确保一切工作完美。接下来,让我们实现processPoints(...)方法。
处理和显示检测到的点
在captureOutput(...)方法内部检测到手部地标后,我们现在想要将它们绘制到相机叠加层中。用以下代码替换processPoints(...)的空实现:
swift
func processPoints(_ fingerTips: [CGPoint?]) {
// Convert points from AVFoundation coordinates to UIKit // coordinates.
let previewLayer = cameraView.previewLayer
let convertedPoints = fingerTips
.compactMap {$0}
.compactMap {previewLayer.layerPointConverted(fromCaptureDevicePoint: $0)}
// Display converted points in the overlay
cameraView.showPoints(convertedPoints, color: .red)
}
记得我们是如何使用CGPoints转换为AVFoundation坐标的吗?现在我们想要将这些点转换为UIKit预览层。我们正在对它们执行map操作,最后,我们调用cameraView辅助方法showPoints来显示它们。
一切现在都已就绪!是时候构建并运行应用程序了。你会看到自拍相机被触发,如果你将其对准你的手,你的手指和拇指的尖端应该会被红色圆点覆盖。试一试,你应该会得到以下类似的结果:
图 15.05 -- TIP 检测
然而,这种方法仍然存在一些问题!试试这个:让应用程序检测你的手,然后从摄像机的视图中移除手部 -- 红色圆点仍然在叠加层上!当没有检测到手时,它们没有被清理。
这有一个简单的解决方案。原因是,在captureOutput(...)方法内部,我们并不总是执行processPoints(...)方法。有时(guard语句)我们返回而不调用它。解决方案是将processPoints(...)块封装到defer中,将其移动到代码的开头,就在我们定义存储每个尖端坐标的五个属性之后。它应该看起来像这样:
swift
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
var thumbTip: CGPoint?
var indexTip: CGPoint?
var ringTip: CGPoint?
var middleTip: CGPoint?
var littleTip: CGPoint?
defer {
DispatchQueue.main.sync {
self.processPoints([thumbTip, indexTip, ringTip, middleTip, littleTip])
}
}
...
}
突出的代码是我们将其封装到defer中的部分(因此它将在返回方法之前始终执行)。再次执行应用程序,你会注意到当屏幕上没有手时,红色圆点也不会出现!我们正在使用空值调用processPoints,因此没有东西被绘制。通过这一步,我们就有了一个正在运行的手部关键点检测示例!恭喜!
身体姿态检测
Vision 还为 iOS 14 提供了身体姿态检测。身体姿态检测与手部检测非常相似,所以我们不会对其进行逐步演示。但本书的代码包中包含了一个类似本节的应用程序示例,但用于身体姿态检测。你可以查看名为BodyPoseDetection_completed的项目,并查看它与手部检测项目之间的细微差别。
在本节中,我们学习了新的 Vision 方法来检测手部关键点,以及如何使用手机的视频流作为输入来检测手部关键点(而不仅仅是检测静态图像中的手部)。我们还提供了一个类似的演示,可用于身体姿态检测。让我们跳到总结,完成本章。
总结
我们从学习每个 Vision 功能的基石开始本章:如何使用VNRequest实例、其对应的VNRequestHandler实例以及产生的VNObservation实例。
在学习基础知识之后,我们将它们应用于文本识别。我们通过使用.fast和.accurate比较了不同的识别级别。我们还了解了感兴趣区域及其如何影响视觉请求的性能。最后,通过应用领域知识、修复潜在的视觉错误和误读,我们在文本识别方面提高了我们的结果。
最后,我们学习了新的手部地标识别功能。但这次,我们还学习了如何将视觉请求应用于实时视频流。我们能够在来自设备前摄像头的视频流中检测到手部地标,并显示叠加层以显示结果。本章还提供了一个类似的示例,该示例可以应用于身体姿态识别。
在下一章中,我们将学习 iOS 14 的一个全新功能:小部件!
第十六章:第十六章:创建你的第一个部件
随着 iOS 14 的推出,苹果引入了 WidgetKit。现在,用户可以在主屏幕上使用部件。通过在主屏幕上显示少量有用的信息,部件为用户提供了一个长期期待的关键功能。一些例子包括查看股市价格、天气或交通状况、日历上的下一次会议等,只需在主屏幕上扫一眼即可。用例是无限的!
在本章中,你将了解 WidgetKit 的基本原理,以及部件设计和它们的限制性关键方面。然后,我们将从头开始构建一个部件。从一个非常简单、小尺寸的部件开始,我们将通过创建新尺寸、网络调用、动态配置、占位符视图等来扩展其功能!我们将在接下来的章节中讨论所有这些主题:
-
介绍部件和 WidgetKit
-
开发你的第一个部件
到本章结束时,你将能够创建自己的部件,使你的应用能够提供独特的全新功能,从而吸引用户下载并更积极地使用你的应用。
技术要求
本章的代码包包括一个名为CryptoWidget_1_small_start的入门项目及其后续部分。你可以在代码包仓库中找到它们:
github.com/PacktPublishing/Mastering-iOS-14-Programming-4th-Edition
介绍部件和 WidgetKit
在本节中,我们将学习 WidgetKit 的基础知识以及 iOS 14 中部件的选项和指南。
用户和开发者多年来一直在请求一个特定的功能:他们都想在主屏幕上拥有部件。部件使用户能够配置、个性化并在主屏幕上消费相关的小数据块。它们还使开发者能够提供可快速查看的内容,并为他们的应用增加价值。
下面是部件(在本例中为日历和提醒事项部件)在 iPhone 主屏幕上的预览:
图 16.1 -- 带有部件的 iOS 主屏幕
现在在 iOS 14 和 macOS 11 及更高版本中可以实现这一点。开发者可以使用WidgetKit 和 SwiftUI 的新部件 API在 iOS、iPadOS 和 macOS 上创建部件。
iOS 14 中的智能堆叠 包含一组不同的部件,包括用户经常打开的部件。如果用户启用智能旋转,Siri 可以在自定义堆叠中突出显示相关的部件。
iOS 13 及更早版本创建的部件
在 iOS 14 之前创建的部件无法放置在主屏幕上,但它们仍然可在今日视图和 macOS 通知中心中访问。
在介绍新部件功能之后,让我们看看在构建部件时有哪些选项,以及苹果的设计指南是什么。
部件选项
用户可以在 iOS 的主屏幕或 Today 视图、iPad 的 Today 视图或 macOS 的通知中心上放置小部件。
小部件有三种尺寸:小、中、大。每种尺寸应有不同的用途;小部件的大版本不应只是小尺寸版本字体和图像的放大,而是应该包含更多信息。小部件不同尺寸背后的理念是,尺寸越大,包含的信息应该越多。例如,天气小部件在小尺寸版本中只提供当前温度,但在中尺寸版本中还将包括每周天气预报。
用户可以在屏幕的不同部分排列小部件,甚至创建小部件堆叠来分组它们。
为了开发一个小部件,开发者需要为他们应用创建一个新的扩展:一个小部件扩展。他们可以使用时间线提供者来配置小部件。时间线提供者在需要时更新小部件信息。
假设一个小部件需要一些配置(例如,在天气应用中选择默认城市,或在大型天气小部件中显示多个城市)。在这种情况下,开发者应在小部件扩展中添加自定义 Siri 意图。创建自定义 Siri 意图会自动为小部件提供用户定制的界面。
小部件指南
当为 iOS 14 或 macOS 11 创建小部件时,请考虑以下设计指南:
-
将你的小部件聚焦于你应用的主要功能。例如,如果你的应用是关于股市的,你的小部件可以显示用户投资组合的总价值。
-
每个小部件的大小应显示不同数量的信息。如果你的骑行追踪小部件在小尺寸下显示今天燃烧的卡路里,它也可以在中尺寸下显示每天的周卡路里,并在大尺寸下添加额外的信息,例如行驶的公里数/英里数。
-
相比于固定信息,更倾向于动态信息,这些信息在一天中会变化;这将使你的小部件对用户更具吸引力。
-
相比于配置选项更多的小部件,更倾向于简单的小部件。
-
小部件提供点击目标和检测功能,使用户能够点击它们以在应用中打开详细信息。小型小部件支持单个点击目标;中型和大型小部件支持多个目标。尽量保持简单。
-
支持深色模式。如有需要,还可以考虑使用 SF Pro 作为字体和 SF Symbols。
在本节中,我们了解了新的小部件功能和 WidgetKit。我们涵盖了构建小部件时可用选项和设计指南。在下一节中,我们将从头开始构建一个简单的小部件,并逐步添加更多功能。
开发你的第一个小部件
在本节中,我们将使用一个现有应用,逐步创建其上的小部件。
我们将要开发的这款应用是一款加密货币行情应用,用户可以查看不同加密货币的最新价格。我们将创建一个小部件,让用户可以直接从主屏幕上查看加密货币的价格,这样他们就不必打开应用本身。
请打开本章代码包中名为 CryptoWidget_start 的项目。这是我们构建小部件的基础项目。在开始任务之前,让我们快速回顾一下基础项目本身。
构建并发布项目。应用显示加密货币价格列表:
图 16.2 -- 基础应用
你还可以进入每个硬币的详细视图,但仅出于演示目的,它不包含额外的信息。由于我们将使用现有的代码库,在修改它之前,让我们突出一些关键点:
-
项目被组织成三个文件组(除了默认生成的文件和应用代理):
Views、Model和Network。 -
Views文件夹包含项目的UIView文件。视图是使用 SwiftUI 创建的。当使用 WidgetKit 构建小部件时,SwiftUI 是推荐的方式。如果你不熟悉 SwiftUI,不要担心;在这个项目中,我们只会使用基本的视图。 -
在
Network文件夹内部,我们有一个名为DataManager.swift的类。这个类包含getData()方法,负责从 CoinMarketCap 的 API 中获取加密货币价格。你可以在他们的网站上创建一个免费的开发者账户以获取最新的价格。否则,演示应用使用一个演示密钥,为我们提供的加密货币提供历史价格。如果你创建自己的账户,你只需要用你自己的密钥替换这个密钥的值:let apiKeyValue = "b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c"。 -
Model文件夹包含与getData()方法结果一起工作的基本结构体:Coin和CoinList。这些结构体将包含来自 API 的加密货币符号和价格信息。
现在,让我们看看项目的主体视图,它位于 Views 文件夹内的 ContentView.swift 文件中。ContentView 结构体包含 @ObservedObject var dataManager = DataManager()。@ObservedObject 标签表示这个 SwiftUI 视图将观察 dataManager 结构体的变化,并将刷新/响应这些变化。记住,dataManager 是我们用来从网络上检索加密货币数据的类,所以我们的主要视图观察任何变化是有意义的。检查 ContentView 的主体:
swift
var body: some View {
NavigationView {
if dataManager.loading {
Text("Loading...")
} else {
CoinListView(data: dataManager.coins.data)
}
}
}
当 dataManager 处于加载状态时,视图将显示简单的 Loading... 文本,当 dataManager 加载完成并包含一些数据时,将显示 CoinListView。很简单!现在,如果你检查 CoinListView.swift 的实现,你会看到它是一个简单的列表,显示它接收到的每个硬币的信息:
swift
var body: some View {
VStack {
ForEach(data, id: \.symbol){ coin in
CoinRow(coin: coin)
}
}
}
目前没有什么太花哨的!到目前为止,我们有 dataManager,它调用 getData() 从 API 获取代币信息,以及 ContentView,在数据被调用时显示 Loading... 文本,并在获取到代币信息时显示代币详情列表。所有这些都是在几个类和几行代码中完成的...这就是 SwiftUI 的力量!现在我们已经对基础项目有了清晰的了解,让我们开始创建小部件扩展,以开始构建我们出色的加密货币小部件!
创建小部件扩展
将小部件添加到应用程序的第一步是创建一个小部件扩展。创建小部件扩展将为我们提供一个默认的小部件协议实现,这将帮助我们准备好基本组件。
在创建扩展之前,让我们回顾一下以下图中显示的小部件扩展的各个部分:
图 16.3 -- 小部件构建块
如前图所示,以下是小部件扩展构建块的解释:
-
如果小部件可以被用户配置,它将需要一个自定义 Siri 意图配置定义。例如,显示股票的小部件可以要求用户进行配置以选择要显示的股票。
-
需要一个提供者来提供要在小部件上显示的数据。提供者可以生成占位符数据(即在用户浏览小部件画廊或加载时显示),时间线(表示随时间变化的数据),以及快照(组成时间线的单元)。
-
需要一个 SwiftUI 视图来显示数据。
当创建小部件目标时,Xcode 将自动生成所有这些类的占位符。现在让我们来做这件事;按照以下步骤操作:
-
在名为
CryptoWidget_start的项目中,转到 文件 | 新建 | 目标 | 小部件扩展。 -
您可以使用
CryptoWidgetExtension作为产品名称,并勾选 包含配置意图 选项:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_16.04_B14717.jpg图 16.4 -- 小部件扩展选项
-
点击以下弹出窗口中的 激活。
如果您已经按照前面的步骤操作,那么您的项目现在应该包含一个具有以下文件夹结构的新目标:
图 16.5 -- 小部件目标结构
在创建小部件扩展时,Xcode 已经自动生成了两个重要的文件:CryptoWidgetExtension.swift 和 CryptoWidgetExtension.intentdefinition。现在让我们专注于 CryptoWidgetExtension.swift。打开它,让我们看一下。检查以下代码片段:
swift
@main
struct CryptoWidgetExtension: Widget {
let kind: String = "CryptoWidgetExtension"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
CryptoWidgetExtensionEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
如您所见,正如之前讨论的那样,我们有小部件的基本构建块:
-
一个名为
IntentConfiguration的意图配置,允许用户配置小部件。 -
一个提供数据给小部件的提供者:
Provider() -
一个用于显示数据的视图:
CryptoWidgetExtensionEntryView
CryptoWidgetExtension 结构被标记为 @main,这意味着它是小部件的入口点。其主体由 IntentConfiguration 和 CryptoWidgetExtensionEntryView 组成,该视图接收一个 entry 实例作为输入。
在同一文件中,我们还有 Provider 所需方法的自动生成定义(placeholder()、getSnapshot() 和 getTimeline()):
-
placeholder(...)方法将为小部件提供第一次渲染小部件时的初始视图。占位符将使用户对小部件的外观有一个大致的了解。 -
getSnapshot(...in context...)方法将为小部件提供一个值(输入),当小部件需要在短暂情况下显示时使用。context中的isPreview属性表示小部件正在小部件画廊中显示。在这些情况下,快照必须快速:这些场景可能需要开发人员使用占位符数据并避免网络调用,以便尽可能快地返回快照。 -
getTimeline(...)方法将为小部件提供一组值,以显示当前时间(以及可选的未来时间)。
我们将稍后使用另一个重要的修饰符。在 .description("这是一个示例小部件。") 之后,添加以下行:
swift
.supportedFamilies([.systemSmall])
这是我们配置此小部件可用不同大小的位置。在章节的后面部分,我们将添加中等大小的类型。
现在,让我们看看代码的另一个部分。在文件末尾,您将找到 Preview 部分:
swift
struct CryptoWidgetExtension_Previews: PreviewProvider {
static var previews: some View {
CryptoWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
这部分代码将允许我们使用 SwiftUI 显示预览,以显示我们在开发小部件时的外观。如果您启动预览,您将看到目前它只显示时间(如果您看不到预览选项卡,请转到 Xcode 顶部菜单的 编辑 | 画布):
![Figure 16.6 -- Editor canvas preview]
![img/Figure_16.06_B14717.jpg]
图 16.6 -- 编辑画布预览
这太棒了!我们可以实时编码并看到最终结果。让我们分析一下我们是如何获得带有时间的小部件视图的。看看我们是如何使用 CryptoWidgetExtensionEntryView 作为预览的主视图的?
swift
CryptoWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
此视图接收 SimpleEntry(仅包含日期)和一个普通的、空的 ConfigurationIntent。
然后,我们通过创建 previewContext 并将其分配为 .systemSmall 来对视图应用修饰符。通过这样做,我们在小部件预览中渲染视图!
CryptoWidgetExtensionEntryView 是如何使用 SimpleEntry 的?让我们检查实现:
swift
struct CryptoWidgetExtensionEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
嗯,它只是显示带有日期的文本!所以,总的来说,预览正在执行以下操作:
-
使用
SimpleEntry作为小部件的数据输入 -
使用
CryptoWidgetExtensionEntryView作为主视图来显示数据输入 -
使用
WidgetPreviewContext修饰符来使用小型小部件作为预览的画布
在心中牢记所有这些概念后,是时候开始创建我们自己的小部件了。让我们修改前面的结构体,以显示比特币的价值而不是简单的日期。
首先,如果我们想在部件中显示一个币的价值(例如比特币),我们需要一个条目来包含这些信息。让我们将Coin数组添加到SimpleEntry结构的属性中:
swift
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let coins: [Coin]
}
通过存储coins属性,条目可以在稍后向小部件的视图传递此信息。如果你尝试构建项目,你将得到如下错误:
swift
Cannot find type 'Coin' in scope
这是因为Coin文件只是主应用目标的一部分。我们需要选择Coin以及Views、Network和Model文件夹下的所有其他文件,并将它们添加到小部件的目标中:
图 16.7 -- 从主应用共享文件到小部件目标
在将上一张截图中的文件添加到小部件目标后,编译时将出现新的不同错误。所有这些错误的主要原因是你向Coin添加了一个新属性,现在在Provider结构体中有部分地方我们在初始化Coin实例时没有那个新属性。为了修复它,我们将向Provider实现中添加一些占位符数据(目前如此),在创建Provider内部的任何SimpleEntry实例时将其作为币传递。稍后,我们将使用来自 API 的真实数据而不是这些占位符数据。
在Provider结构体内部添加以下代码。其第一行将如下所示:
swift
struct Provider: IntentTimelineProvider {
let coins = [Coin(id: 1, name: "Bitcoin", symbol: "BTC", quote: Quote(USD: QuoteData(price: 20000))), Coin(id: 1, name: "Litecoin", symbol: "LTC", quote: Quote(USD: QuoteData(price: 200)))]
//...
我们正在创建一些假数据以生成一个包含比特币和莱特币一些值的Coin数组。现在,我们可以使用这个coins值将它们注入到Provider类内部创建SimpleEntry的三个地方:
-
首先,我们在
placeholder(...)方法内部注入它:swiftSimpleEntry(date: Date(), configuration: ConfigurationIntent(), coins: coins) -
然后,我们在
getSnapshot(...)方法内部注入它:swiftlet entry = SimpleEntry(date: Date(), configuration: configuration, coins: coins) -
然后,我们在
getTimeline(...)方法内部注入它:swiftlet entry = SimpleEntry(date: entryDate, configuration: configuration, coins: coins)
最后,你可能在CryptoWidgetExtension_Previews结构体内部遇到完全相同的问题。previews属性正在使用SimpleEntry在部件中显示它。你需要再次添加coins属性。只需使用此代码:
swift
CryptoWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), coins: [Coin(id: 1, name: "Bitcoin", symbol: "BTC", quote: Quote(USD: QuoteData(price: 20000))), Coin(id: 1, name: "Litecoin", symbol: "LTC", quote: Quote(USD: QuoteData(price: 200)))]))
.previewContext(WidgetPreviewContext(family: .systemSmall))
太好了!项目现在应该可以正确编译。尝试渲染预览以查看发生了什么。哎呀!你仍然应该在小的部件中看到日期/时间,而没有币值!为什么?我们将币值传递给小部件的条目,但小部件的视图还没有使用它。检查当前的实现:
swift
struct CryptoWidgetExtensionEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
我们在内部有包含币信息的entry,但我们只显示日期。我们需要修改视图以显示新的信息!在主应用中,我们有一个视图,给定一个币,显示其名称和价格。让我们使用它。更改CryptoWidgetExtensionEntryView的实现(更改已突出显示):
swift
struct CryptoWidgetExtensionEntryView : View {
var entry: Provider.Entry
var body: some View {
CoinDetail(coin: entry.coins[0])
}
}
现在,构建并刷新预览。太棒了!你应该在 Widget 上看到比特币的价格和名称,如下面的截图所示:
图 16.8 -- 显示比特币价格的 Widget
如果你想在模拟器中尝试,只需启动 Widget 目标。记住,你应该首先启动(至少一次)主应用。
在本节中,我们学习了如何向应用添加 Widget 扩展。然后,我们探讨了主要组件及其之间的关系:提供者、条目、Widget 视图和 SwiftUI 的预览系统。最后,我们修改了所有这些组件以适应我们的需求,并创建了我们的第一个小 Widget。在下一节中,我们将学习如何添加占位符预览以及如何添加中等尺寸的 Widget!
实现多尺寸 Widget
在上一节中,我们向项目中添加了一个 Widget 目标并创建了 Widget 的第一个视图,即小尺寸视图。现在让我们做一些修改,以便开发一个中等尺寸的 Widget,以及 Widget 的占位符预览。
如果你没有跟上上一节的内容,可以使用名为 CryptoWidget_1_small_widget 的项目。让我们首先向项目中添加一个占位符预览。在第一次渲染你的 Widget 时,WidgetKit 会将其渲染为占位符。为了渲染数据,它将使用以下方法请求提供者提供一个条目:
swift
func placeholder(in context: Context) -> SimpleEntry
但是,为了在开发过程中看到它的外观,我们可以使用 SwiftUI 创建它的预览。继续在 CryptoWidgetExtension.swift 文件中添加以下结构体:
swift
struct PlaceholderView : View {
let coins = [Coin(id: 1, name: "Bitcoin", symbol: "BTC", quote: Quote(USD: QuoteData(price: 20000))), Coin(id: 1, name: "Litecoin", symbol: "LTC", quote: Quote(USD: QuoteData(price: 200)))]
var body: some View {
CryptoWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), coins: coins)).redacted(reason: .placeholder)
}
}
看看我们如何使用主要的 Widget 视图 (CryptoWidgetExtensionEntryView) 作为占位符视图,并给它提供模拟数据?然而,有趣的部分是高亮显示的部分:.redacted(reason: .placeholder)。现在我们已经使用模拟数据创建了一个占位符视图,让我们创建它的预览并检查 redacted 修改符的效果。
移除 CryptoWidgetExtension_Previews 的实现,并添加这个新的实现,修改后的代码如下所示:
swift
struct CryptoWidgetExtension_Previews: PreviewProvider {
static var previews: some View {
Group {
CryptoWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), coins: [Coin(id: 1, name: "Bitcoin", symbol: "BTC", quote: Quote(USD: QuoteData(price: 20000))), Coin(id: 1, name: "Litecoin", symbol: "LTC", quote: Quote(USD: QuoteData(price: 200)))]))
.previewContext(WidgetPreviewContext(family: .systemSmall))
PlaceholderView()
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
}
首先,我们将之前的 CryptoWidgetExtensionEntryView 视图封装在 Group 中。这是因为现在我们想要显示一组预览,CryptoWidgetExtensionEntryView 和新的 Placeholder。
然后,我们添加了新创建的 Placeholder 视图,并像之前一样应用了一个小 Widget 的 previewContext。编译并继续预览渲染;你应该看到以下内容:
图 16.9 -- 带有红字修改符的占位符视图
你现在看到 .redacted(reason: .placeholder) 的效果了吗?SwiftUI 正在用占位符视图替换标签。创建你自己的小部件占位符视图非常简单!
目前,我们有一个小型小部件及其预览。让我们开始创建它的中等尺寸版本。更大的小部件应该利用额外的可用空间为用户提供额外的价值。你的中等或大尺寸小部件不应该只是简单的大尺寸版本。在我们的例子中,我们以小尺寸显示比特币的价格。现在,在中等尺寸,我们将一次性显示多种加密货币的价值。用户只需一眼就能获得市场的更大图景!
在上一节中,我们配置了supportedFamilies以允许小尺寸的小部件。我们还需要添加中等尺寸。你将在CryptoWidgetExtension结构体中找到它。将.systemMedium添加到supportedFamilies中,因此配置行应该看起来像这样:
swift
.supportedFamilies([.systemSmall, .systemMedium])
现在让我们为中等尺寸的小部件创建一个预览。请继续在CryptoWidgetExtension_Previews中现有小部件下方添加一个新的Group。在现有的Group{ ... }结束的地方添加以下代码(因此你应该有一个接一个的组):
swift
Group {
CryptoWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), coins: [Coin(id: 1, name: "Bitcoin", symbol: "BTC", quote: Quote(USD: QuoteData(price: 20000))), Coin(id: 1, name: "Litecoin", symbol: "LTC", quote: Quote(USD: QuoteData(price: 200))), Coin(id: 1, name: "Ethereum", symbol: "ETH", quote: Quote(USD: QuoteData(price: 1200)))]))
.previewContext(WidgetPreviewContext(family: .systemMedium))
PlaceholderView()
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
看看这个新的视图组与现有的视图是否相同,唯一的区别在于高亮的代码?我们现在在systemMedium预览中显示小部件及其占位符。如果你恢复渲染,你应该看到这两个新的预览(除了之前的小尺寸预览):
图 16.10 -- 中等尺寸小部件和占位符
你可以想象,对于一个用户来说,这个结果将会非常令人失望。我们显示的信息与小型小部件完全相同,但在他们的主页上占据了更多的空间(这对他们来说非常宝贵!)让我们通过更改系统显示中等尺寸版本时我们的CryptoWidgetExtensionEntryView的布局来改进这一点。我们可以利用额外的空间一次显示不止一种货币。删除CryptoWidgetExtensionEntryView的实现,并使用以下代码:
swift
struct CryptoWidgetExtensionEntryView : View {
var entry: Provider.Entry
//1
@Environment(\.widgetFamily) var family
//2
@ViewBuilder
var body: some View {
switch family {
//3
case .systemSmall where entry.coins.count > 0:
CoinDetail(coin: entry.coins[0])
//4
case .systemMedium where entry.coins.count > 0:
HStack(alignment: .center) {
Spacer()
CoinDetail(coin: entry.coins.first!)
Spacer()
CoinListView(data: entry.coins)
Spacer()
}
//5
default:
PlaceholderView()
}
}
}
让我们讨论代码中的编号注释:
-
我们使用
@Environment(\.widgetFamily)变量,它允许我们知道正在使用哪个小部件家族。基于这个信息,我们可以为不同尺寸使用不同的布局。 -
视图必须使用
@ViewBuilder声明其主体,因为它使用的视图类型是可变的。 -
我们使用
family(widgetFamily)属性来切换它,并为不同尺寸的小部件提供不同的视图。对于小型小部件,我们继续使用之前的CoinDetail视图。 -
对于中等尺寸的小部件,我们使用一种视图组合,使我们能够显示一种硬币的详细信息及其旁边的其他硬币列表。通过这种方式,我们增加了价值,并利用可用空间为用户提供更多信息。
-
最后,我们使用
Placeholder来处理开关的default情况。
现在你可以恢复预览以查看更改。中等尺寸的组应该看起来像这样:
图 16.11 -- 中等尺寸小部件
太棒了!我们现在有一个为中等尺寸家庭提供额外价值的不同小部件!我们还有一个重要的任务要完成。我们在小尺寸和中尺寸显示的数据只是示例数据。此外,我们没有让用户选择他们想要在小尺寸小部件中显示的代币;我们强制显示比特币,他们可能对此不感兴趣。
在下一节中,我们将学习如何为小部件提供动态配置(因此用户可以配置小部件的选项)以及如何显示真实数据。
提供小部件的数据和配置
到目前为止,我们有一个具有各种大小的小部件和一个显示加密货币示例数据的占位符视图。在本节中,我们将用来自 API 的真实数据替换这些示例数据,并且我们还将允许用户配置一些选项,以进一步个性化小部件。
如果你没有跟随前面的章节,你可以使用名为 CryptoWidget_2_medium_widget 的项目。
让我们先从小部件提供真实数据开始。提供条目(因此是数据)给小部件视图的实体是 Provider。某种方式上,我们需要 Provider 了解我们的数据源并向视图提供传入的数据。在我们的主应用中,负责提供数据的结构是 DataManager。请继续在 CryptoWidgetExtension.swift 文件中向 Provider 结构添加以下属性:
swift
@ObservedObject var dataManager = DataManager()
我们正在将 DataManager 实例添加到小部件的 Provider 中。请注意,我们使用 @ObservedObject 标签标记了这个属性。如果你之前没有在 SwiftUI 中使用过它,那么每当带有此标签的可观察属性发生变化时,它都会使依赖于它的任何视图无效。
每当 DataManager 发生变化时,依赖于它的视图将无效并刷新以反映这些更改。现在,我们可以从 Provider 中删除示例数据并使用数据管理器。删除以下行:
swift
let coins = [Coin(id: 1, name: "Bitcoin", symbol: "BTC", quote: Quote(USD: QuoteData(price: 20000))), Coin(id: 1, name: "Litecoin", symbol: "LTC", quote: Quote(USD: QuoteData(price: 200)))]
如果你构建项目,你将得到三个编译错误------每个提供者方法中都有一个,我们使用的是刚刚删除的 coins 属性。请继续使用 dataManager.coins.data 属性来代替已删除的代币属性。这个属性来自 dataManager,包含从 API 获取的真实数据。
现在,启动主应用,从设备中删除之前的小部件,并将其再次添加到主屏幕上。你应该会看到如下内容:
图 16.12 -- 小部件画廊
这是一个非常好的消息!这不再是示例数据;列表中现在有最多五个具有真实值的代币(记住,如果你使用的是本章开头讨论的沙盒端点,这些值可能不会是最新的)。
现在我们已经在 widget 中显示了真实值。下一步将是稍微改进一下小尺寸 widget。目前,小尺寸 widget 显示的是比特币的价格。但用户可能对其他加密货币感兴趣。我们将使用配置意图来允许用户输入配置值,并使我们的 widget 更加动态。
在本章开头,当我们向主应用添加 widget 扩展时,我们在 widget 扩展文件夹中选择了 CryptoWidgetExtension.intentdefinition。这是一个 Siri 意图定义文件,我们可以配置 widget 将接受的作为用户输入的选项。让我们为我们的特定情况配置意图定义。我们希望用户能够从预定义的代币名称列表中选择一个代币,以在小型 widget 中显示该代币的价格。
让我们先创建一个包含以下值的枚举:BTC、LTC 和 ETH:
-
点击
CryptoWidgetExtension.intentdefinition文件。在coinSelect中更改类型为 添加枚举。 -
这个操作将带你进入创建一个新的枚举。将枚举命名为
Coin Select并添加以下值:1.LTC2.ETH3.BTC它应该看起来像这样:
图 16.13 -- Coin Select 枚举配置
-
现在返回到意图的 配置 部分。你可以取消选择 运行时 Siri 可以请求值 选项。确保其他选项设置如以下截图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_16.14_B14717.jpg
图 16.14 -- 自定义意图配置
以这种方式配置自定义意图并创建了用于显示一些列表值的枚举后,我们就可以在 widget 中使用这个意图了。
-
返回到
CryptoWidgetExtension.swift文件并检查SimpleEntry定义。在每一个条目中,我们都可以访问到configuration属性(它是一个我们刚刚配置的ConfigurationIntent实例)。这意味着每次我们访问一个条目时,都可以访问到自定义意图的值。现在,在
CryptoWidgetExtensionEntryView中,我们有一个可用的entry(当然!这是我们想要显示的数据)。因此,我们可以访问它内部的配置意图。让我们利用它!我们将修改.systemSmallswitch 案例以使用配置意图信息并显示不同的代币,而不仅仅是显示比特币。 -
继续查找以下代码:
swiftcase .systemSmall where entry.coins.count > 0: CoinDetail(coin: entry.coins[0]) -
用这个新的替换它:
swiftcase .systemSmall where entry.coins.count > 0: switch entry.configuration.coinselect) to know which coin from the enum the user selected. Based on that, we are displaying a specific coin in the small-sized widget.Try to build the project. You may get a compile error. This error happens because the widget doesn't yet know about the custom Siri intent type (even though Xcode generated it for us). This error may be fixed in future versions of Xcode. If you have an error, check the following: -
前往主应用设置,在 支持意图 部分的
ConfigurationIntent意图下,如以下截图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_16.15_B14717.jpg图 16.15 -- 将你的意图添加到支持意图部分
-
再次构建项目,编译错误应该会消失。
-
如果您仍然有任何错误,请尝试以下操作:
a) 编译并运行主应用的目标。
b) 运行小部件的目标。从模拟器中删除小部件并再次添加(小尺寸的那个)。
-
现在,如果您在设备上的小尺寸小部件(或模拟器)上长按,您应该能够看到"编辑小部件"选项。它将显示您的新自定义意图,如下面的截图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_16.16_B14717.jpg
图 16.16 -- 小部件配置选项
-
尝试选择ETH 或LTC。然后,您的小部件将重新加载并显示该货币!这太棒了;我们现在有一个可配置的加密货币小部件。
在本节中,我们学习了如何使用 Siri 意图使小部件可配置,因此用户可以从主屏幕选择值并编辑小部件。
现在,还有一个我们尚未讨论的话题。在Provider结构体中,我们了解到getTimeline(...)方法将为小部件提供一系列值,以在一段时间内显示,以刷新显示的信息并保持最新。但我们没有讨论如何控制小部件实际刷新的时间,甚至是否在我们控制之下。我们将在下一节中学习这一点。
刷新小部件的数据
保持小部件更新需要消耗系统资源,并可能需要大量的电池使用。因此,系统将限制每个小部件在一天内可以执行更新的次数,以节省电池寿命。
带着这个想法,我们必须理解我们并没有完全控制我们小部件的刷新时间和频率,并且小部件并不总是处于活跃状态。我们能够给系统一些提示,关于何时对我们的小部件刷新是最理想的,但最终决定权在系统手中。
系统使用预算在一段时间内分配重新加载。这个预算受以下因素的影响:
-
小部件被展示给用户的次数有多少?
-
小部件上次重新加载是什么时候?
-
小部件的主要应用是否处于活跃状态?
为小部件分配的预算可以持续 24 小时。用户频繁访问的小部件每天可以刷新高达 70 次,这意味着它大约每 15 分钟更新一次。
您可以通过在您小部件的Timeline方法中提供尽可能多的信息来帮助 WidgetKit 估算您小部件的最佳预算。以下是一些示例:
-
一个遵循食谱的烹饪小部件可以在时间线上安排不同的步骤,在特定时间点显示烹饪步骤:预热烤箱 15 分钟,烹饪 30 分钟,休息 10 分钟,等等。这将导致一个在特定分钟(15 -- 30 -- 10)上时间间隔分开的条目时间线。WidgetKit 将尝试在这些时间点刷新您的 小部件,以显示适当的条目。
-
为了让小部件每两小时提醒用户喝水,你可以生成一个时间线来提醒用户每两小时喝一杯水。但你可以更有效率,避免在用户睡觉的夜间进行任何刷新。这将产生一个更有效率的时间线,并节省一些 WidgetKit 可以用来在真正需要时更频繁地刷新你的小部件的预算。
现在,在我们的特定示例中,让我们修改时间线,让 WidgetKit 每隔 5 分钟刷新我们的小部件(一个非常激进的要求!)!但我们知道加密货币非常波动,对于这个例子,我们希望尽可能多地刷新价格。按照以下步骤操作:
-
现在请打开名为
CryptoWidget_4_timeline的项目,该项目位于本章的代码包中。首先,让我们在DataManager中创建一个新的方法,允许我们通过完成块获取最新的加密货币数据。 -
将以下方法添加到结构体中:
swiftfunc refresh(completionHandler: @escaping (CoinList) -> Void) { guard let url = URL(string: apiUrl) else { return } var request = URLRequest(url: url) request.setValue(apiKeyValue, forHTTPHeaderField: apiKeyHeader) URLSession.shared.dataTask(with: request){ (data, _, _) in print("Update coins") guard let data = data else { return } let coins = try! JSONDecoder().decode(CoinList.self, from: data) DispatchQueue.main.async { print(coins) self.coins = coins self.loading = false completionHandler(coins) } }.resume() }看看这个方法与
getData()的相似之处,但这个方法不是private的,并且它还返回coins,以便我们可以在需要时在completion处理器中使用。 -
接下来,转到名为
CryptoWidgetExtension.swift的文件,并修改Provider结构体中的getTimeline(...)方法,用以下实现替换:swiftfunc getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { print("New Timeline \(Date())") dataManager.refresh { (coins) in let currentDate = Date() let futureDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! let timeline = Timeline(entries: [SimpleEntry(date: Date(), configuration: configuration, coins: coins.data)], policy: .after(futureDate)) completion(timeline) } }
让我们看看方法中发生了什么:
-
首先,我们正在使用我们创建的新方法
refresh(...)来获取加密货币的最新值。 -
一旦我们在完成处理程序中准备好了硬币,我们就在未来创建一个日期,这个日期是 15 分钟之后。
-
然后,我们创建一个包含
coins最新值的SimpleEntry的Timeline,以及一个刷新策略。刷新策略设置为在 15 分钟后创建一个新的时间线(futureDate)。通常,15 分钟是 WidgetKit 再次更新你的小部件所需的最短时间。如果你尝试更低的值,你可能不会得到任何结果。因此,为了总结这个方法,当 WidgetKit 请求我们时间线时,我们调用我们的 API 来获取最新的加密货币值,然后我们将它们包装在一个准备显示的小部件视图中,并设置一个"15 分钟后"的刷新策略。
-
现在,尝试从模拟器或你的设备中删除应用程序和小部件。安装应用程序和扩展,并在主屏幕上添加一个小部件。当你添加小部件时,你应该在日志中看到时间线方法的第一个语句,类似于以下内容:
swiftNew Timeline 2021-01-23 20:51:51 +0000然后,15 分钟后,你应该再次看到它出现。刷新策略已经启动,我们再次提供了一个带有最新值的刷新版本:
swiftNew Timeline 2021-01-23 21:06:52 +0000
太棒了!我们现在知道如何刷新我们的小部件了!最后提醒一下:除了 .after 之外,还有更多的刷新策略。以下是选项:
-
TimelineReloadPolicy.after(Date): 在特定日期过后将生成一个新的时间线。 -
TimelineReloadPolicy.atEnd:在当前时间线的最后一条条目通过之后,将生成一个新的时间线。 -
TimelineReloadPolicy.never:小部件的应用将负责让 WidgetKit 知道何时下一个时间线准备就绪。
在本节中,我们学习了 WidgetKit 如何决定何时刷新您的小部件,以及我们如何提供时间线和刷新策略,以便系统更好地了解我们希望何时更新小部件。现在,让我们通过总结来结束本章。
总结
我们从学习小部件和 WidgetKit 的基础知识开始本章。我们了解了通用指南、基本选项及其目的。在介绍之后,我们直接进入了开发我们的第一个小部件。我们首先向现有应用中添加了一个小型小部件。
然后,我们在小部件中添加了一个占位符视图,以便用户对首次加载时小部件的外观有一个良好的概念。之后,我们创建了一个更大、中等大小的版本,它能够显示比小型小部件多得多的信息,并提供更多的价值。
最后,我们学习了如何在 Siri 自定义意图的帮助下使小部件可由用户配置。通过使用自定义意图,用户能够向小部件提供某些配置值以个性化体验。
在本章中,你学习了如何创建小部件并充分利用 WidgetKit。在下一章中,我们将学习关于 ARKit 的知识,这是苹果公司的增强现实框架。
第十七章:第十七章: 使用增强现实
苹果在 iOS 11 中发布的一个主要功能是ARKit 。ARKit 允许开发者仅用少量代码就创建惊人的增强现实(AR)体验。苹果一直在努力改进 ARKit,在 2018 年的 WWDC 上发布了 ARKit 2,在 2019 年的 WWDC 上发布了 ARKit 3,在 2020 年的 WWDC 上发布了 ARKit 4。
在本章中,你将学习什么是 ARKit,它是如何工作的,你可以用它做什么,以及如何实现一个使用多个 ARKit 功能(如图像跟踪)的 AR 艺术画廊。我们还将了解一些来自 SpriteKit 和 SceneKit 的基本概念。
本章涵盖了以下主题:
-
理解 ARKit
-
使用 ARKit 快速查看
-
探索 SpriteKit
-
探索 SceneKit
-
实现 AR 画廊
到本章结束时,你将能够将 ARKit 集成到你的应用程序中,并实现你自己的 ARKit 体验。
理解 ARKit
在本节中,我们将了解增强现实(AR)和 ARKit。增强现实(AR)是一个长期以来一直吸引着应用程序开发者和设计师兴趣的话题。尽管实现出色的 AR 体验并不容易,但许多应用程序并没有达到预期的炒作。像照明和检测墙壁、地板和其他对象这样的小细节一直都非常复杂,而这些细节的错误会对 AR 体验的质量产生负面影响。
增强现实应用程序通常至少具有以下一些功能:
-
它们显示了一个相机视图。
-
内容在相机视图中以叠加的形式显示。
-
内容能够适当地响应设备的移动。
-
内容附着在世界的特定位置。
尽管这个功能列表很简单,但它们并不都是容易实现的。AR 体验在很大程度上依赖于读取设备的运动传感器,以及使用图像分析来确定用户的确切移动方式,并了解世界 3D 地图应该是什么样子。
ARKit 是苹果提供给开发者创建出色 AR 体验的力量的方式。ARKit 负责所有的运动和图像分析,以确保你可以专注于设计和实现优秀的内容,而不是被构建 AR 应用程序所涉及的复杂细节所拖慢。
不幸的是,ARKit 对运行 ARKit 应用程序的设备有较高的硬件要求。只有配备苹果 A9 芯片或更新的设备才能运行 ARKit。这意味着任何比 iPhone 6s 或第一代 iPad Pro 更旧的设备都无法运行 ARKit 应用程序。
在接下来的几节中,我们将首先了解 ARKit 如何在设备上渲染内容,以及它是如何跟踪周围的物理环境,以提供最佳的 AR 体验。
理解 ARKit 如何渲染内容
ARKit 本身只负责与跟踪用户所处的物理世界相关的庞大计算。要在 ARKit 应用中渲染内容,您必须使用以下三种渲染工具之一:
-
SpriteKit
-
SceneKit
-
Metal
在本章的后面部分,您将快速了解 SpriteKit 和 SceneKit,并最终使用 SceneKit 来实现您的 AR 画廊。如果您已经熟悉任何可用的渲染技术,那么在使用 ARKit 时应该会感到非常自在。
在您的应用中实现 ARKit 不仅限于手动渲染您想在 AR 中显示的内容。在 iOS 12 中,苹果增加了一个名为 ARKit Quick Look 的功能。您可以在您的应用中实现一个特殊的视图控制器,负责放置您提供的 3D 模型。如果您正在实现允许用户在现实世界中预览产品或其他对象的特性,这将非常理想。
理解 ARKit 如何跟踪物理环境
要理解 ARKit 如何渲染内容,您必须了解 ARKit 如何理解用户所处的物理环境。当您实现一个 AR 体验时,您使用一个 ARKit 会话。ARKit 会话由 ARSession 的一个实例表示。每个 ARSession 都使用 ARSessionConfiguration 的一个实例来描述它在环境中应执行的跟踪。以下图表展示了在 ARKit 会话中涉及的所有对象之间的关系:
![图 17.1 -- ARKit 会话组件
![img/Figure_17.01_B14717.jpg]
图 17.1 -- ARKit 会话组件
上述图表显示了会话配置如何传递给会话。然后,会话被传递给一个负责渲染场景的视图。如果您使用 SpriteKit 来渲染场景,该视图是一个 ARSKView 的实例。当您使用 SceneKit 时,这将是一个 ARSCNView 的实例。视图和会话都有一个代理,它将在 ARKit 会话期间通知某些事件。您将在实现您的 AR 画廊时了解更多关于这些代理的信息。
在会话上,您可以配置几种不同的跟踪选项。最基本的跟踪配置之一是 AROrientationTrackingConfiguration。此配置仅跟踪设备的方向,而不是用户在环境中的移动。这种跟踪使用三个自由度来监控设备。更具体地说,这种跟踪跟踪设备的 x 、y 和 z 方向。如果您的实现中可以忽略用户的移动,例如 3D 视频,这种跟踪方式非常合适。
更复杂的跟踪配置是ARWorldTrackingConfiguration,也称为世界跟踪。此类配置跟踪用户的移动以及设备的方向。这意味着用户可以绕着 AR 对象走动,从不同的侧面观察它。世界跟踪使用设备的运动传感器来确定用户的移动和设备的方向。这对于短距离和小范围的移动非常准确,但不足以跟踪长时间和距离的移动。为了确保 AR 体验尽可能精确,世界跟踪还会执行一些高级计算机视觉任务,以分析摄像头流来确定用户在环境中的位置。
除了跟踪用户的移动外,世界跟踪还使用计算机视觉来理解 AR 会话存在的环境。通过检测摄像头流中的某些兴趣点,世界跟踪可以比较和分析这些点相对于用户运动的位置,以确定对象的距离和大小。这种技术还允许世界跟踪检测墙壁和地板等。
世界跟踪配置将学习到的关于环境的所有信息存储在ARWorldMap中。此地图包含所有代表会话中存在的不同对象和兴趣点的ARAnchor实例。
您可以在应用程序中使用几种其他特殊跟踪类型。例如,您可以在具有TrueDepth 摄像头的设备上使用ARFaceTrackingConfiguration来跟踪用户的脸部。如果您想在 iOS 12 中添加到 iPhone X 及其后续版本中的 Apple Animoji 功能,这种跟踪方式非常完美。
您还可以配置会话,使其自动检测场景中的某些对象或图像。为此,您可以使用ARObjectScanningConfiguration来扫描特定项目,或使用ARImageTrackingConfiguration来识别静态图像。
在本节中,您已经学习了 AR 和 ARKit 的基础知识,包括 ARKit 如何在设备上渲染内容,以及它是如何跟踪周围物理环境的。在您开始实现 ARKit 会话之前,让我们探索新的ARKit 快速预览功能,看看它对您允许您的应用程序用户在 AR 中预览项目有多简单。
使用 ARKit 快速预览
在本节中,我们将了解 ARKit 快速预览功能,这是苹果公司的一项功能,允许用户使用设备的摄像头预览虚拟 3D 或 AR 模型。
AR 为最终用户带来的一个巨大好处是,现在可以在现实世界中预览某些对象。例如,当您购买新的沙发时,您可能想看看它在现实世界中的样子。当然,在 iOS 11 中使用 ARKit 实现此类功能是可能的,许多开发者已经做到了,但这并不像可能的那样简单。
iOS 用户可以使用名为 Quick Look 的功能预览内容。Quick Look 可以用于预览某些类型的内容,而无需启动任何特定应用程序。这对用户来说很方便,因为他们可以通过在 Quick Look 中预览来确定某个特定文档是否是他们正在寻找的文档。
在 iOS 12 中,苹果将 USDZ 文件格式添加到可以使用 Quick Look 预览的内容类型中。苹果的 USDZ 格式是基于皮克斯的 USD 格式的一种 3D 文件格式,用于表示 3D 对象。使用 Quick Look 预览 3D 模型不仅限于应用中;ARKit Quick Look 还可以集成到网页上。开发者可以在他们的网页上使用特殊的 HTML 标签来链接 USDZ,Safari 将在 ARKit 快速查看视图控制器中显示模型。
在实现你的 AR 画廊之前,了解 iOS 上 AR 的工作方式是一个好主意,可以通过实现 ARKit 快速查看视图控制器来展示苹果在 developer.apple.com/arkit/gallery/ 提供的其中一个模型。要下载你喜欢的模型,你只需要在你的 Mac 上导航到这个页面并点击一个图像。USDZ 文件应该会自动开始下载。
小贴士
导航到支持 ARKit 的设备的 ARKit 画廊,并点击其中一个模型,以查看 Safari 中的 ARKit 快速查看看起来是什么样子。
在本节中,我们解释了什么是快速查看。现在让我们在下一节中将其用于我们自己的应用中。
实现 ARKit 快速查看视图控制器
从苹果的画廊获取 USDZ 文件后,还确保捕获属于此文件的图像。为了测试目的,对模型进行截图应该是可以的。确保通过将截图放大到两倍和三倍大小来准备不同所需的图像尺寸。
在 Xcode 中创建一个新的项目,并为你的项目选择一个名称。本书代码包中的示例项目名为 ARQuickLook。将你的准备好的图像添加到 Assets.xcassets 文件中。此外,将你的 USDZ 文件拖动到 Xcode 中,并确保在导入文件时勾选你的应用复选框,将其添加到应用目标中:
图 17.2 -- 导入 USDZ 模型
接下来,打开故事板文件,将一个图像视图拖动到视图控制器中。为图像添加适当的约束,使其在视图控制器中居中,并设置其宽度和高度为200点。确保在属性检查器 中勾选用户交互启用复选框,并将你的模型图像设置为图像视图的图像。
完成此操作后,打开ViewController.swift,为图像视图添加@IBOutlet,并将故事板中的图像连接到这个出口。如果关于出口的细节现在有点模糊,请参考代码包中的示例项目以刷新记忆。示例项目中的图像视图使用了一个名为guitarImage的出口。
为 USDZ 模型实现快速查看的下一步是在图像视图上添加一个轻点手势识别器,然后当用户轻点图像时触发快速查看视图控制器。
快速查看使用委托从数据源对象中预览一个或多个项目。它还使用委托来获取快速查看预览应该动画的源视图。这种流程适用于所有可以使用快速查看预览的文件类型。
要开始实现快速查看,你必须导入QuickLook框架。将以下import语句添加到ViewController.swift的顶部:
swift
import QuickLook
接下来,通过在viewDidLoad()中添加以下代码来为图像设置轻点手势识别器:
swift
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(presentQuicklook))
guitarImage.addGestureRecognizer(tapGesture)
下一步是实现presentQuicklook()。这个方法将创建一个快速查看视图控制器,设置委托和数据源,然后将快速查看视图控制器呈现给用户。将以下实现添加到ViewController类中:
swift
@objc func presentQuicklook() {
let previewViewController = QLPreviewController()
previewViewController.dataSource = self
previewViewController.delegate = self
present(previewViewController, animated: true,
completion: nil)
}
这种实现应该不会给你带来任何惊喜。QLPreviewController是UIViewController的子类,负责显示从其数据源接收到的内容。它以与其他视图控制器相同的方式呈现,通过调用present(_:animated:completion:)。
最后一步是实现数据源和委托。将以下扩展添加到ViewController.swift中:
swift
extension ViewController: QLPreviewControllerDelegate {
func previewController(_ controller: QLPreviewController,
transitionViewFor item: QLPreviewItem) -> UIView? {
return guitarImage
}
}
extension ViewController: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller:
QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController,
previewItemAt index: Int) -> QLPreviewItem {
let fileUrl = Bundle.main.url(forResource:
"stratocaster", withExtension: "usdz")!
return fileUrl as QLPreviewItem
}
}
你添加的第一个扩展使ViewController遵守QLPreviewControllerDelegate协议。当预览控制器即将展示 3D 模型时,它想知道即将发生的转换的源视图是哪个。建议从这个方法返回充当快速查看操作的预览视图。在这种情况下,预览是 3D 模型的图像。
第二个扩展充当快速查看数据源。当你为 ARKit 实现快速查看时,你只能返回一个项目。所以,当预览控制器询问预览中的项目数量时,你应该始终返回1。数据源中的第二个方法提供了预览控制器中应该预览的项目。你在这里需要做的就是获取你希望预览的项目文件 URL。在示例应用中,使用了苹果画廊中的 Stratocaster 模型。如果你的模型有不同的名称,请确保使用正确的文件名。
在获取指向应用包中图像的 URL 后,应将其作为QLPreviewItem实例返回给预览控制器。幸运的是,URL 实例可以自动转换为QLPreviewItem实例。
如果你现在运行你的应用,你可以点击 3D 模型的图像来开始预览它。你可以单独预览图像,或者选择在 AR 中预览它。如果你点击这个选项,预览控制器会告诉你移动你的设备。
为了将你周围的世界进行映射,ARKit 需要一些环境样本。当你移动你的设备时,确保不要只是倾斜它,而是要物理移动它。这样做将帮助 ARKit 发现你周围可追踪的特征。
一旦 ARKit 收集了你周围足够的数据,你就可以将 3D 模型放置在环境中,通过捏合来缩放它,旋转它,并在空间中移动它。请注意,模型会自动放置在平坦的表面,如桌子或地板上,而不是尴尬地漂浮在空中:
![图 17.3 -- 在场景周围移动设备
![img/Figure_17.03_B14717.jpg]
图 17.3 -- 在场景周围移动设备
还要注意,ARKit 对你的物体应用了非常逼真的光照。ARKit 收集的环境视觉数据被用来创建一个光照图,并将其应用于 3D 模型,使其能够正确地融入物体放置的上下文中:
![图 17.4 -- 放置在现实世界中的 AR 模型
![img/Figure_17.04_B14717.jpg]
图 17.4 -- 放置在现实世界中的 AR 模型
虽然这样玩 ARKit 很有趣,但创建自己的 AR 体验更有趣。由于 ARKit 支持多种渲染技术,如 SpriteKit 和 SceneKit,接下来的两个部分将花一点时间解释 SpriteKit 和 SceneKit 的基础知识。你不会学习如何使用这些框架构建完整的游戏或世界。相反,你将学习足够的内容,以便在 ARKit 应用中开始实现任一渲染引擎。
探索 SpriteKit
在本节中,我们将探讨SpriteKit 。SpriteKit 主要被开发者用来构建二维游戏。SpriteKit 已经存在一段时间了,并且它帮助开发者多年来创建了许多成功的游戏。SpriteKit 包含一个完整的物理仿真引擎,并且可以同时渲染许多精灵。精灵代表游戏中的一个图形。精灵可以是玩家的图像,也可以是硬币、敌人,甚至是玩家行走的地面。当在 SpriteKit 的上下文中提到精灵时,指的是屏幕上可见的节点之一。
由于 SpriteKit 内置了物理引擎,它可以检测物体之间的碰撞,对它们施加力,等等。这和 UIKit Dynamics 的功能非常相似。
为了渲染内容,SpriteKit 使用场景。这些场景可以被认为是游戏的水平或主要构建部分。在 AR 的上下文中,你会发现你通常只需要一个场景。SpriteKit 场景负责更新场景的位置和状态。作为开发者,你可以通过 SKScene 的 update(_:) 方法挂钩到帧的渲染。每当 SpriteKit 即将为你或 ARKit 场景渲染新帧时,都会调用此方法。确保此方法的执行时间尽可能短是很重要的,因为 update(_:) 方法的慢速实现会导致帧率下降,这是被认为不好的。你应该始终努力保持每秒 60 帧的稳定帧率。这意味着 update(_:) 方法应该始终在不到 1/60 秒的时间内完成其工作。
要开始探索 SpriteKit,请在 Xcode 中创建一个新项目,并选择 SpriteKitDefault,如下截图所示:
![图 17.5 -- 创建 SpriteKit 项目]
图 17.5 -- 创建 SpriteKit 项目
当 Xcode 为你生成此项目时,你应该会注意到一些之前没有见过的文件:
-
GameScene.sks -
Actions.sks
这两个文件对于 SpriteKit 游戏来说就像故事板对于常规应用一样。你可以使用这些文件来设置游戏场景的所有节点,或者设置可重用的动作,这些动作可以附加到你的节点上。我们现在不会深入这些文件,因为它们非常具体于游戏开发。
如果你构建并运行 Xcode 提供的示例项目,你可以轻触屏幕来在屏幕上创建新的精灵节点。每个节点在消失前都会执行一点动画。这本身并不特别,但它包含了很多有价值的信息。例如,它展示了如何向场景中添加内容以及如何对其进行动画处理。让我们看看这个项目是如何设置的,这样你就可以在将来想要使用 SpriteKit 构建 AR 体验时应用这些知识。
创建 SpriteKit 场景
SpriteKit 游戏使用一种特殊的视图来渲染其内容。这个特殊视图始终是 SKView 的一个实例或子类。如果你想在 SpriteKit 中使用 ARKit,你应该使用 ARSKView,因为这个视图实现了某些特殊的 AR 相关行为,例如渲染相机视频流。
视图本身通常不会在管理游戏或其子视图方面做太多工作。相反,包含视图的 SKScene 负责执行这项工作。这与其他应用中通常使用视图控制器的方式类似。
当你创建了一个场景后,你可以告诉 SKView 显示这个场景。从这一刻起,你的游戏就开始运行了。在之前创建的游戏项目示例代码中,以下行负责加载和显示场景:
swift
if let scene = SKScene(fileNamed: "GameScene") {
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
当你创建场景时,你可以选择是否想要使用 .sks 文件或以编程方式创建场景。
当你打开 Xcode 为你创建的 GameScene.swift 文件时,大部分代码应该是相当容易理解的。当场景被添加到视图中时,会创建并配置几个 SKNode 实例。这个文件中最有趣的代码行如下:
swift
spinnyNode.run(SKAction.repeatForever(SKAction.rotate(byAng
le: CGFloat(Double.pi), duration: 1)))
spinnyNode.run(SKAction.sequence([SKAction.wait(forDuration
: 0.5), SKAction.fadeOut(withDuration: 0.5),
SKAction.removeFromParent()]))
这些行设置了当你点击屏幕时添加的旋转方块的动画序列。在 SpriteKit 中,动作是设置动画的首选方式。你可以分组、链式组合动作以实现相当复杂的效果。这是 SpriteKit 提供的许多强大工具之一。
如果你仔细检查一下代码,你会发现每次用户在屏幕上点击、移动手指或抬起手指时,都会创建 spinnyNode 的副本。每次交互都会产生一个略微不同的 spinnyNode 副本,因此你可以通过观察其外观来确定为什么将 spinnyNode 添加到场景中。
研究这段代码,尝试操作它,并确保你理解它的工作原理。你不必成为 SpriteKit 专家,但在这个部分,我们已经回顾了它的基础知识,以便你可以开始使用它。让我们看看 SceneKit 是如何准备和实现你的 AR 画廊的。
探索 SceneKit
如果你正在寻找一个对 3D 游戏有出色支持的游戏框架,SceneKit 是一个很好的选择。SceneKit 是苹果公司用于创建 3D 游戏的框架,其结构设置与 SpriteKit 非常相似。
当然,SceneKit 与 SpriteKit 完全不同,因为它用于 3D 游戏,而不是 2D 游戏。因此,SceneKit 在创建视图和将它们定位在屏幕上的方式上也非常不同。例如,当你想要创建一个简单的对象并将其放置在屏幕上时,你会看到诸如几何和材质之类的术语。这些术语应该对游戏程序员来说很熟悉,但如果你是 AR 爱好者,你可能需要习惯这些术语。
本节将指导你设置一个简单的 SceneKit 场景,它将非常类似于你稍后将要实现的 AR 画廊的一部分。这应该为你提供足够的信息,以便开始尝试使用 SceneKit。
创建基本的 SceneKit 场景
为了练习你的 SceneKit 知识,创建一个新的项目,而不是选择 Game 模板,而是选择 Single View Application 模板。当然,你可以自由探索当你选择带有 SceneKit 的 Game 模板时 Xcode 为你创建的默认项目,但这对于 AR 画廊来说并不特别有用。
创建你的项目后,打开主故事板并查找 SceneKit 视图。将此视图拖到视图控制器中。你应该注意到你刚刚添加到视图控制器的视图已经完全替换了默认视图。因此,ViewController 上的 view 属性将不是一个普通的 UIView;它将是一个 SCNView 的实例。这是将用于渲染 SceneKit 场景的视图。
在 ViewController.swift 的 viewDidLoad() 中添加以下代码,将 view 属性从 UIView 转换为 SCNView:
swift
guard let sceneView = self.view as? SCNView
else { return }
现在,记得在顶部添加 import SceneKit,以便 SCNView 能够编译。
与 SpriteKit 的工作方式类似,SceneKit 使用场景来渲染其节点。在 viewDidLoad() 中的 guard 之后立即创建一个 SCNScene 实例,如下所示:
swift
let scene = SCNScene()
sceneView.scene = scene
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = UIColor.black
上述代码创建了一个简单的场景,将用于渲染所有元素。除了创建场景外,还启用了几个调试功能来监控场景的性能。此外,请注意,场景视图上的 allowsCameraControl 属性被设置为 true。这将允许用户在场景中移动虚拟相机,通过在场景中滑动来探索场景。
每个 SceneKit 场景都像通过相机看一样。你必须自己添加这个相机,并且必须根据你的目的适当地设置它。SceneKit 使用相机的事实非常方便,因为当你使用 ARKit 运行场景时,你即将设置的相机将被设备的实际相机所取代。
在 viewDidLoad() 中添加以下代码行以创建和配置相机:
swift
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
设置基本相机并不复杂。你只需要一个 SCNNode 来添加相机,以及一个 SCNCamera,它将用于通过它查看你的场景。请注意,相机是通过 SCNVector3 对象定位的。SceneKit 场景中的所有节点都使用此对象来表示它们在三维空间中的位置。
除了使用模拟相机外,SceneKit 还模拟真实的光照条件。当你使用 ARKit 运行场景时,光照条件将由 ARKit 自动管理,使你的物体看起来就像真正是环境的一部分。然而,当你创建一个普通场景时,你需要自己添加灯光。添加以下代码行以实现一些环境光照:
swift
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.orange
scene.rootNode.addChildNode(ambientLightNode)
你可以向 SceneKit 场景添加不同类型的灯光。你可以像这个示例一样使用环境光,但也可以添加定向光,它聚焦于特定方向,聚光灯,或者照亮所有方向的点光源。
现在你已经设置了光照和相机,你可以在场景中添加一个对象。你可以在场景中使用几个预制的形状,也称为几何体。或者,你也可以将整个 3D 模型导入到场景中。如果你查看 Xcode 生成的默认 SceneKit 应用,如果你使用Game模板创建一个新项目,你可以看到一个飞机的 3D 模型被导入。
在你稍后将要构建的 AR 画廊中,艺术品将通过附加到它们所属的艺术品上的数字信息标签进行增强。为了练习构建这样的标签,你将在你的 SceneKit 场景中添加一个矩形形状,或者平面,并在其上方放置一些文本。
添加以下代码以创建一个简单的白色平面、渲染平面的节点,并将其添加到场景中:
swift
let plane = SCNPlane(width: 15, height: 10)
plane.firstMaterial?.diffuse.contents = UIColor.white
plane.firstMaterial?.isDoubleSided = true
plane.cornerRadius = 0.3
let planeNode = SCNNode(geometry: plane)
planeNode.position = SCNVector3(x: 0, y: 0, z: -15)
scene.rootNode.addChildNode(planeNode)
如果你现在构建并运行你的应用,你会看到一个位于相机前面的白色方块。通过在场景上滑动,你可以使相机在平面上移动,从所有可能的角度查看它。请注意,尽管只设置了 15 宽和 10 高,但平面看起来相当大。你可能已经猜到这些数字代表屏幕上的点,就像在其他应用中一样。在 SceneKit 中,没有点的概念。所有的大小和距离值都必须以米为单位指定。这意味着你做的所有事情都是相对于其他对象或它们的真实世界大小进行的。当你将 SceneKit 知识应用到 ARKit 时,使用真实大小是至关重要的。
要在你的新创建的平面上添加一些文本,请使用以下代码:
swift
let text = SCNText(string: "Hello, world!", extrusionDepth:
0)
text.font = UIFont.systemFont(ofSize: 2.3)
text.isWrapped = true
text.containerFrame = CGRect(x: -6.5, y: -4, width: 13,
height: 8)
text.firstMaterial?.diffuse.contents = UIColor.red
let textNode = SCNNode(geometry: text)
planeNode.addChildNode(textNode)
上述代码创建了一个文本几何体。由于 SceneKit 中的所有值都是以米为单位,文本的大小将比你可能预期的要小得多。为了确保文本在平面上正确定位,启用了文本换行,并使用containerFrame来指定文本的边界。由于文本字段的起点将位于显示的平面的中心,因此x 和y位置从中心向负方向偏移,以确保文本出现在正确的位置。你可以尝试调整这个框架来看看会发生什么。配置好文本后,将其添加到一个节点中,然后将该节点添加到平面节点中。
如果你现在运行你的应用,你将能看到在之前创建的白色平面上渲染的**Hello, World!**文本。这个示例很好地展示了你接下来将要创建的内容。让我们直接开始构建你的 AR 画廊!
实现增强现实画廊
由于 ARKit 中存在的一些优秀功能,创建一个出色的 AR 体验已经变得简单得多。然而,如果你想要构建用户会喜欢的 AR 体验,还有一些事情需要牢记。
某些条件,如光照、环境,甚至用户正在做什么,都可能影响 AR 体验。在本节中,你将实现一个 AR 画廊,你将亲身体验 ARKit 既是惊人的神奇,有时又有点脆弱。
首先,你需要在 ARKit 中设置一个会话,以便你可以实现图像跟踪来在世界中找到某些预定义的图像,你将在找到的图片上方显示一些文本。然后,你将实现另一个功能,允许用户将应用中的画廊艺术作品放置在自己的房间里。
如果你想跟随步骤实现 ARKit 画廊,请确保从书籍的代码包中获取 ARGallery_start 项目。在您开始实现 AR 画廊之前,先探索一下起始项目。准备好的用户界面包含一个 ARSCNView 实例;这是将用于渲染 AR 体验的视图。为了准备用户添加自己的图像到画廊,已添加了一个集合视图,并添加了一个用于错误信息的视图,以通知用户某些可能出错的事情。
你会发现到目前为止项目相当基础。现有的所有代码只是设置了集合视图,并添加了一些代码来处理 AR 会话期间的错误。让我们来实现图像跟踪,好吗?
添加图像跟踪
当你将图像跟踪添加到你的 ARKit 应用中时,它将不断扫描环境以寻找与你在应用中添加的图像相匹配的图像。如果你想让用户在他们的环境中寻找特定的图像,以便你可以提供更多关于它们的信息,或者作为寻宝游戏的一部分,这个功能非常棒。但更复杂的实现可能存在于教科书或杂志中,扫描特定页面会使整个页面作为独特体验的一部分活跃起来。
在您能够实现图像跟踪体验之前,您必须为您的用户提供一些图像以便在应用中找到。一旦内容准备就绪,您就可以构建 AR 体验本身了。
准备图像进行跟踪
将图像添加到您的应用中进行图像跟踪相对简单。最重要的是,你需要仔细关注你添加到应用中的图像。确保你添加的图像是高质量的并且色彩饱和。ARKit 将扫描图像中的特殊特征以尝试匹配,因此你的图像需要有足够的细节、对比度和颜色。一个平滑渐变的图像可能在你看来是一个可识别的图像,但对于 ARKit 来说可能很难检测。
要将图像添加到您的项目中,请转到 Assets.xcassets 文件夹,点击左下角的 + 图标,并选择 New AR Resource Group,如图下截图所示:
![Figure 17.6 -- 添加 AR 资源
![img/Figure_17.06_B14717.jpg]
Figure 17.6 -- 添加 AR 资源
在添加新的资源组后,你可以将图片拖入创建的文件夹中。ARKit 会一次性加载和监控每个资源组,所以请确保不要将太多图片添加到单个资源组中,因为这可能会对你的应用性能产生负面影响。苹果建议你将大约 25 张图片添加到单个资源组中。
在你将图片添加到资源组后,Xcode 将分析图片,并在它认为你的图片有问题时发出警告。通常,Xcode 会在你添加新图片时立即通知你,因为 ARKit 需要知道你想要检测的图像的物理尺寸。所以,如果你要检测特定的画作或杂志中的一页,你必须以厘米为单位添加这些资源的尺寸,就像它们在现实世界中存在的那样。
从代码包中开始的项目包含了一些准备好的图片,你可以探索这些图片来查看你可以在自己的应用中使用的一些图片类型示例。
小贴士
如果你想要添加一些自己的内容,可以拍摄家中或办公室的艺术品或图片。你可以使用 iOS 中的 Measure 应用来测量图片的物理尺寸,并将它们添加到你的 AR 相册项目中。确保你的图片色彩饱和,没有任何眩光或反射。
一旦你找到了一些优秀的内容用于你的 AR 相册,就到了构建体验本身的时候了。
构建图像跟踪体验
要实现图像跟踪,你需要设置一个使用 ARWorldTrackingConfiguration 的 ARSession 来检测图像并跟踪用户在环境中的移动。当场景中发现你准备好的其中一个图像时,会在图片上方添加一个 SCNPlane,并附上对图片本身的简短描述。
因为 ARKit 使用摄像头,所以你的应用必须明确提供访问摄像头的原因,以便用户理解为什么你的应用需要使用他们的摄像头权限。将 NSCameraUsageDescription 键添加到 Info.plist 文件中,并添加一段简短的文字说明为什么相册需要访问摄像头。
如果你打开 ViewController.swift,你会找到一个名为 artDescriptions 的属性。确保更新这个字典,包含你添加到资源组的图片名称,并为每张图片添加一个简短描述。
接下来,更新 viewDidLoad(),以便将 ViewController 设置为 ARSCNView 和 ARSession 的代理。添加以下代码行来完成此操作:
swift
arKitScene.delegate = self
arKitScene.session.delegate = self
场景代理和会话代理非常相似。会话代理提供了对场景中显示的内容的非常细粒度的控制,如果你自己构建渲染,你通常会广泛使用此协议。由于 AR 相册使用 SceneKit 渲染,采用 ARSessionDelegate 的唯一原因是为了响应会话跟踪状态的变化。
你应该采用的所有有趣的方法都是 ARSCNViewDelegate 的一部分。这个代理用于响应特定事件,例如,当场景中发现了新功能或添加了新内容时。
目前,你的 AR 画廊并没有做什么。你必须配置场景中的一部分 ARSession 来开始使用 ARKit。设置这一切的最佳时机是在视图控制器变得可见之前。因此,你应该在 viewWillAppear(_:) 中完成所有剩余的设置。将以下实现添加到 ViewController 中:
swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1
let imageSet = ARReferenceImage.referenceImages(
inGroupNamed: "Art", bundle: Bundle.main)!
// 2
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.vertical, .horizontal]
configuration.detectionImages = imageSet
// 3
arKitScene.session.run(configuration, options: [])
}
代码解释如下:
-
这个方法的第一步是从应用程序包中读取参考图像。这些是你添加到
Assets.xcassets中的图像。 -
接下来,创建
ARWorldTrackingConfiguration,并配置它来跟踪水平和垂直平面,以及参考图像。 -
最后,配置被传递到会话的
run(_:options:)方法中。
如果你现在运行你的应用程序,你应该已经提示了相机使用,你应该看到错误处理正在工作。尝试用手遮住相机,这应该会显示一个错误消息。
如果一个视图不再可见,保持 AR 会话活跃是非常浪费的,所以如果应用程序关闭或包含 AR 场景的视图控制器变得不可见,暂停会话是一个好主意。将以下方法添加到 ViewController 中以实现这一点:
swift
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
arKitScene.session.pause()
}
在当前设置中,AR 会话检测你的图像,但没有做任何事情来可视化这一点。当你添加的图像之一被识别时,ARSCNViewDelegate 会收到通知。具体来说,当在场景代理上添加一个新的 SCNNode 时,会调用 renderer(_:didAdd:for:) 方法。例如,当 AR 会话发现一个平坦的表面时,它会为 ARPlaneAnchor 添加一个节点,或者当它检测到你正在跟踪的图像之一时,会添加一个 ARImageAnchor 的节点。由于这个方法可能因不同原因而被调用,因此添加逻辑来区分可能导致在场景中添加新的 SCNNode 的各种原因是非常重要的。
因为 AR 画廊将实现其他几个可能触发添加新节点的功能,你应该将针对每种不同类型的锚点想要采取的不同操作分离到专门的方法中。将以下方法添加到 ARSCNViewDelegate 中以在检测到的图像旁边添加信息平面:
swift
func placeImageInfo(withNode node: SCNNode, for anchor:
ARImageAnchor) {
let referenceImage = anchor.referenceImage
// 1
let infoPlane = SCNPlane(width: 15, height: 10)
infoPlane.firstMaterial?.diffuse.contents = UIColor.white
infoPlane.firstMaterial?.transparency = 0.5
infoPlane.cornerRadius = 0.5
// 2
let infoNode = SCNNode(geometry: infoPlane)
infoNode.localTranslate(by: SCNVector3(0, 10, -
referenceImage.physicalSize.height / 2 + 0.5))
infoNode.eulerAngles.x = -.pi / 4
// 3
let textGeometry = SCNText(string:
artDescriptions[referenceImage.name ?? "flowers"],
extrusionDepth: 0.2)
textGeometry.firstMaterial?.diffuse.contents =
UIColor.red
textGeometry.font = UIFont.systemFont(ofSize: 1.3)
textGeometry.isWrapped = true
textGeometry.containerFrame = CGRect(x: -6.5, y: -4,
width: 13, height: 8)
let textNode = SCNNode(geometry: textGeometry)
// 4
node.addChildNode(infoNode)
infoNode.addChildNode(textNode)
}
上述代码应该对你来说有些熟悉。首先,创建一个 SCNPlane 实例。然后,将这个平面添加到 SCNNode。这个节点稍微移动以定位在检测到的图像上方。这个平移使用 SCNVector3 以便可以转换到三维。节点也稍微旋转以创建一个看起来不错的效果。
接下来,为 renderer(_:didAdd:for:) 添加以下实现:
swift
func renderer(_ renderer: SCNSceneRenderer, didAdd node:
SCNNode, for anchor: ARAnchor) {
if let imageAnchor = anchor as? ARImageAnchor {
placeImageInfo(withNode: node, for: imageAnchor)
}
}
此方法检查发现的锚点是否为图像锚点;如果是,则调用 placeImageInfo(withNode:for:) 来显示信息标志。
现在运行您的应用!当您找到您添加到资源组中的图像之一时,应该会在其上方出现一个信息框,如下面的截图所示:
图 17.7 -- 图像上方的 AR 盒
真的很棒,对吧?让我们更进一步,允许用户将收藏视图中的某些图片放置在场景中的任何位置。
在 3D 空间中放置您自己的内容
为了让 AR 画廊更加生动,能够将一些新的艺术品添加到环境中会很好。使用 ARKit,这样做变得相对简单。在实现此类功能时,需要考虑一些注意事项,但总体而言,苹果公司让 ARKit 成为一个对开发者来说易于使用的平台。
当用户在屏幕底部的收藏视图中点击其中一个图像时,他们点击的图像应该被添加到环境中。如果可能,图像应该附着在用户周围的墙壁之一上。如果这不可能,图像仍然会被添加,但会漂浮在空间的中间。
要构建此功能,您应该实现 collectionView(_:didSelectItemAt:),因为当用户在收藏视图中点击其中一个项目时,会调用此方法。当此方法被调用时,代码应获取用户在环境中的当前位置,然后插入一个新的 ARAnchor,该锚点对应于新项目应添加的位置。
此外,为了检测附近的垂直平面,例如墙壁,需要进行一些碰撞测试以查看是否存在垂直平面位于用户前方。添加以下 collectionView(_:didSelectItemAt:) 的实现:
swift
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
//1
guard let camera =
arKitScene.session.currentFrame?.camera
else { return }
//2
let hitTestResult = arKitScene.hitTest(CGPoint(x: 0.5, y:
0.5), types: [.existingPlane])
let firstVerticalPlane = hitTestResult.first(where: {
result in
guard let planeAnchor = result.anchor as? ARPlaneAnchor
else { return false }
return planeAnchor.alignment == .vertical
})
//3
var translation = matrix_identity_float4x4
translation.columns.3.z = -
Float(firstVerticalPlane?.distance ?? -1)
let cameraTransform = camera.transform
let rotation = matrix_float4x4(cameraAdjustmentMatrix)
let transform = matrix_multiply(cameraTransform,
matrix_multiply(translation, rotation))
//4
let anchor = ARAnchor(transform: transform)
imageNodes[anchor.identifier] = UIImage(named:
images[indexPath.row])!
arKitScene.session.add(anchor: anchor)
storeWorldMap()
}
尽管这个片段中只有四个步骤,但发生了很多事情。让我们回顾一下:
-
首先,从 AR 会话的当前帧中获取相机,以便稍后用于确定用户在场景中的位置。
-
接下来,执行碰撞测试以查看场景中是否已经检测到任何平面。由于此碰撞测试将返回垂直和水平平面,因此结果被过滤以找到碰撞测试中发现的第一个垂直平面。
-
由于每个
ARAnchor的位置都表示为从世界原点开始的变换,因此第三步是确定应用于将新艺术品放置在正确位置的变换。世界原点是 AR 会话首次变得活跃的地方。在创建默认平移后,调整平移的z 值,以便对象被添加到用户前面或最近的垂直平面上。接下来,通过摄像头检索用户的当前位置。在下一步中,需要调整摄像头的旋转,因为摄像头不跟随设备的方向。这意味着摄像头将始终假设x轴沿着设备的长度运行,从顶部开始向下移动到主指示器区域。一个计算属性已经添加到 AR 画廊入门项目中,以确定如何调整旋转。 -
在为锚点设置正确的变换属性后,创建一个
ARAnchor实例。然后将用户点击的唯一标识符和图像存储在imageNodes字典中,以便在新的锚点在场景中注册后添加图像到场景。
要将图像添加到场景中,你应该实现一个辅助方法,该方法将从rendered(_:didAdd:for:)中调用,类似于你为显示图像跟踪功能的信息卡添加的辅助方法。将以下代码添加到ViewController中来实现此辅助方法:
swift
func placeCustomImage(_ image: UIImage, withNode node:
SCNNode) {
let plane = SCNPlane(width: image.size.width / 1000,
height: image.size.height / 1000)
plane.firstMaterial?.diffuse.contents = image
node.addChildNode(SCNNode(geometry: plane))
}
为了更容易地看到是否存在合适的垂直平面,你可以实现一个辅助方法来可视化 AR 会话发现的平面。将以下代码添加到ViewController类中来实现此辅助方法:
swift
func vizualise(_ node: SCNNode, for planeAnchor:
ARPlaneAnchor) {
let infoPlane = SCNPlane(width:
CGFloat(planeAnchor.extent.x), height:
CGFloat(planeAnchor.extent.z))
infoPlane.firstMaterial?.diffuse.contents =
UIColor.orange
infoPlane.firstMaterial?.transparency = 0.5
infoPlane.cornerRadius = 0.2
let infoNode = SCNNode(geometry: infoPlane)
infoNode.eulerAngles.x = -.pi / 2
node.addChildNode(infoNode)
}
之前的方法接受一个节点和锚点来创建一个新的SCNPlane,并将其添加到新平面锚点被发现的确切位置。
实现此功能的最后一步是在需要时调用辅助方法。更新renderer(_:didAdd:for:)的实现如下:
swift
func renderer(_ renderer: SCNSceneRenderer, didAdd node:
SCNNode, for anchor: ARAnchor) {
if let imageAnchor = anchor as? ARImageAnchor {
placeImageInfo(withNode: node, for: imageAnchor)
} else if let customImage = imageNodes[anchor.identifier]
{
placeCustomImage(customImage, withNode: node)
} else if let planeAnchor = anchor as? ARPlaneAnchor {
vizualise(node, for: planeAnchor)
}
}
如果你现在运行你的应用,你应该会看到在 ARKit 检测到的平坦区域出现橙色方块。请注意,ARKit 需要纹理和视觉标记才能正常工作。如果你尝试检测一个实心白色墙面,由于缺乏纹理,ARKit 可能无法正确识别墙面。然而,砖墙或带有一些图形的壁纸墙面应该适用于此目的。
以下截图显示了一个示例,其中图像被附加到墙上,同时显示平面指示器:
图 17.8 -- 向 AR 平面上添加图像
这完成了你个人 AR 画廊的实现。关于你可以用 AR 做什么,还有很多东西要学习,所以请确保继续实验和学习,以便为你的用户提供令人惊叹的体验。
摘要
在本章中,你学到了很多。你对 AR 是什么,AR 的基本工作原理以及你可以用它做什么有了更深的了解。然后你学习了构成优秀 AR 体验的组件,并通过在应用程序中采用 Quick Look 来预览真实 AR 会话中的 AR 内容,实现了你的第一个小型 AR 体验。
然后你探索了在 AR 场景中渲染内容的不同方法。你快速浏览了 SpriteKit 和 SceneKit,并了解到 SpriteKit 是苹果的 2D 游戏开发框架。你还了解到 SceneKit 是苹果的 3D 游戏框架,这使得它在 AR 应用程序中使用极为合适。
然后你实现了一个使用图像跟踪和平面检测的 AR 画廊,允许用户将他们自己的内容添加到他们的画廊中。在这个过程中,你发现要让 ARKit 工作良好并不总是容易。不良的照明和其他因素可能会使 AR 体验远低于理想状态。
在下一章中,你将使用 Catalyst 创建一个 macOS 应用程序。
第十八章:第十八章:使用 Catalyst 创建 macOS 应用程序
在 2019 年的 WWDC 上,苹果向全球开发者推出了 Mac Catalyst。有了 Mac Catalyst,开发者可以轻松地将 iPad 应用程序带到 Mac 上。Catalyst 允许 iPad 应用程序无需太多努力即可移植到 Mac。这为 iPad 应用程序带来了全新的受众(Mac 用户),并扩大了 macOS 生态系统的可能性。
在本章中,我们将回顾 Mac Catalyst 的基础知识。我们将探索在 WWDC 2020 上引入的新功能,并使用 Catalyst 将 iPad 应用程序转换为 Mac 应用程序。我们将通过使用 Catalyst 的两种不同方式来实践这一点:缩放界面以匹配 iPad 和新的 优化界面以适应 Mac 选项。我们将比较它们之间的差异以及两种方法的优缺点。
在本章中,我们将涵盖以下主要主题:
-
探索 Mac Catalyst
-
探索新的 Mac Catalyst 功能
-
构建您的第一个 Mac Catalyst 应用程序
到本章结束时,您将能够将您的 iPad 应用程序迁移到 macOS,并在 Mac 生态系统中扩大您应用程序的受众和可能性。
技术要求
本章的代码包包括一个名为 todo_start 的入门项目及其完成版本。您可以在代码包仓库中找到它们:
github.com/PacktPublishing/Mastering-iOS-14-Programming-4th-Edition
探索 Mac Catalyst
Mac Catalyst 帮助开发者将他们的 iPad 应用程序带到 Mac 上。原生 Mac 应用程序可以与 iPad 应用程序共享代码,为用户和开发者创建一个统一的生态系统。
使用 Mac Catalyst,开发者可以将 iPad 上的触摸手势和控件适配到 Mac 应用程序中的鼠标和键盘控件。
当苹果在 Mac Catalyst 中为 Mac 添加对 UIKit 的支持时,它在 iPad 和 Mac 之间的兼容性方面迈出了巨大的一步。使用 SwiftUI 的应用程序则具有成为通用应用程序的优势,因此它们在两个系统上适应得更好。
一旦应用程序在 Mac Catalyst 的帮助下从 iPad 切换到 iPad + Mac,结果非常令人期待。有一个代码库可以服务于这两个平台。通过只有一个代码库,公司可以减少开发、维护和修复应用程序功能(针对两个系统)所需的时间和精力。
Mac Catalyst 也有一些缺点。目前并非每个 iOS 框架都得到支持。苹果每年都在不断增加。此外,一些第三方库可能不受支持,开发者有责任将它们从 Mac 系统中排除并寻找替代方案。
Mac Catalyst 的另一个缺点是,一些从 iPad 移植到 Mac 的应用程序可能会感觉有些脱离上下文。我指的是一些利用了重 iOS 外观和感觉的应用程序,并且直接移植到 Mac。UI 的某些元素在这两个系统中差异很大(复选框、弹出窗口、按钮的位置等)。一些应用程序可能需要一些额外的工作来将 UI 从 iPad 调整为 Mac 风格,但并非每个公司或团队都有资源、时间或意愿这样做。
为了帮助解决这个问题,Mac Catalyst 新增了一个名为优化界面以适应 Mac 的功能。与之前的缩放界面以匹配 iPad选项不同,Mac Catalyst 允许这个新功能自动将一些 UIKit 控件转换为更符合 Mac 风格的控件。
在本节中,我们学习了 Mac Catalyst 的基础知识。让我们在下一节中讨论 WWDC 2020 期间展示的 Mac Catalyst 的新改进。
探索新的 Mac Catalyst 功能
在 WWDC 2020 期间,苹果展示了新的优化界面以适应 Mac 方法。当我们使用这种方法将 iPad 应用程序移植到 Mac 时,它带来了与之前方法缩放界面以匹配 iPad的一些显著差异。差异如下:
-
内容以 1:1 渲染。使用缩放界面 时,视图在 Mac 上缩放到原始大小的 77%。这可能会在某些具有AutoLayout规则的视图中引起问题,这些规则可能会破坏或简单地改变 UI 的整体形状。现在,使用 1:1 渲染,iPad 应用程序和 Mac 应用程序将保持相同的尺寸和大小。这通过在 Mac 上不缩放文本来大大提高了文本质量;文本看起来更好,更容易阅读。
-
macOS 控件用于 UIKit 对应项。通过新的优化界面以适应 Mac选项,Catalyst 使用 Mac 风格的控件而不是 iPad 应用程序中的 UIKit 控件。通过这样做,Mac 上的应用程序 UI 对 Mac 用户来说看起来更加熟悉。
-
与前一点类似,Mac Catalyst 应用程序中使用了 macOS 字体间距和标准 macOS 间距,而不是 iPad 版本中定义的间距(它们是不同的)。
-
通过 Catalyst,许多 iOS 框架现在可用于 Mac。例如,
AVFoundation、NotificationCenter、ReplayKit、StoreKit、MessageUI以及更多。 -
在 iOS 上增加了对物理键盘事件的支持。现在它们在 Mac Catalyst 上也可用,游戏可以从中受益。
-
现在可用 tvOS 的焦点引擎。
-
tableViews和collectionViews中的.selectionFollowsFocus现在可用。 -
现在我们可以根据需要隐藏 Mac 上的光标。
-
新增了颜色轮和颜色选择器。
-
UISplitViewController现在支持三列。 -
完全支持
SFSymbols。 -
Mac Catalyst 的新扩展,如照片编辑扩展,现在可用。
-
由于 Catalyst,WidgetKit 的小部件也从 iPad 扩展到 Mac。
-
用户可以享受通用购买(在 iPad 上购买项目并在 Mac 应用程序中使用)。
-
新的工具栏样式。
在本章的后面部分,当使用这两种方法构建应用程序时,你将能够看到这些差异,并且你将应用必要的修复和步骤来避免在你的应用程序中出现这些问题。
在本节中,我们了解了 2020 年为 Mac Catalyst 推出的新功能。现在,让我们在下一节开始构建我们的第一个 Mac Catalyst 应用程序!
构建你的第一个 Mac Catalyst 应用程序
在本节中,我们将开始使用一个简单的 iPad 待办事项应用程序,并使用两种不同的技术将其转换为 macOS 应用程序。基本应用程序非常基础(你甚至无法向其中添加新的待办事项元素!)但它说明了从 iPad 到 Mac 转换时需要经历哪些类型的错误、UI 修改和方法。
我们将遵循以下步骤:
-
首先,我们将探索 iPad 应用程序本身,以了解其基本元素和组件。
-
然后,我们将使用第一种方法使其与 macOS 兼容:将界面缩放以匹配 iPad。
-
最后,我们将使用新的方法,优化界面以匹配 Mac 。我们将将其与缩放界面方法进行比较,以便匹配 iPad 方法,这样你将了解何时使用一个或另一个,这取决于你的应用程序。
让我们从探索我们的 iPad 待办事项应用程序开始!
探索 iPad 应用程序
在本节中,我们将快速查看基本应用程序及其组件,以便在理解我们在做什么的同时对其进行修改。
你可以在本书的代码包中找到代码。项目名称是todo_start。继续打开项目。构建并运行它。你应该在横幅模式下的 iPad 模拟器中看到类似这样的内容:
![图 18.1 -- 待办事项应用横幅模式
![img/Figure_18.01_B14717.jpg]
图 18.1 -- 待办事项应用横幅模式
如果你熟悉 iPad 应用程序,你将能够从这些屏幕截图中发现,此 iPad 应用程序的主要组件是SplitViewController。SplitViewController通常在其内部有两个或三个列(UIViewController实例)。在我们的例子中,我们有两个:左侧的侧边菜单和右侧的详细面板(在横幅模式下)。在纵向模式下,侧边菜单变为弹出菜单,详细面板是主视图。
让我们快速检查项目结构和突出显示其中的最重要文件:
-
MasterViewController.swift文件包含MasterViewController,它是SplitViewController的侧边菜单。它有一个表格视图及其相应的表格视图单元格(CategoryTableViewCell)。 -
DetailViewController.swift文件包含DetailViewController,它是SplitViewController的详细视图。它有一个表格视图,以及相应的表格视图单元格(EntryTableViewCell)。 -
Datasource.swift文件包含了项目的Datasource,它使用load() -> [Category]方法为视图控制器提供待办事项列表。它还包含了我们待办项目的模型。待办事项列表是通过类别(如工作、杂货或家庭)以及这些类别内的条目(如"给我的老板打电话")构建的。Datasource.swift文件包含代表这些模型的结构体:Category、Entry和Priority。在现实世界的应用中,你会将这些模型分别放入自己的文件/目录中,但为了简单起见,我们将它们保留在Datasource本身中。
因此,为了总结应用组件,侧边菜单(MasterViewController)以表格的形式显示待办事项类别的列表(Category 和 CategoryTableViewCell 实例)。当选择一个类别时,详细视图(DetailViewController)显示一个包含不同待办事项条目的表格(Entry 和 EntryTableViewCell 实例)。所有数据都由 Datasource 提供。
每个类别中待办事项的条目由包含每个待办事项不同信息的单元格表示(EntryTableViewCell):
![Figure 18.2 -- Entry cell]
![img/Figure_18.02_B14717.jpg]
图 18.2 -- 条目单元格
这些表格视图单元格包含以下内容:
-
一个
UISwitch用于表示待办事项是挂起还是完成。 -
一个
UIPickerView用于表示任务的优先级(高 、中 或低)。 -
一个
UILabel用于描述任务。 -
一个
UIButton用于设置任务中的闹钟。
在右上角还有一个额外的按钮:
![Figure 18.3 -- Add to-do button]
![img/Figure_18.03_B14717.jpg]
图 18.3 -- 添加待办事项按钮
此按钮表示允许用户向待办事项列表添加新条目的操作。
目前除了显示这些元素本身之外,没有任何功能,但你在本章后面会理解为什么每个元素都存在。这是一个简单易用的应用,对吧?现在让我们从 iPad 到 Mac 开始转换过程!
为 Mac 调整你的 iPad 应用
在本节中,我们将使用 Mac Catalyst 的 Scale Interface to Match iPad 方法将 iPad 应用转换为 Mac 兼容的应用。这是苹果首次引入的将 iPad 应用轻松转换为 Mac 应用的方法。
从当前部分打开项目并转到项目导航器。在 Deployment Info 部分勾选 Mac 复选框,并在弹出窗口中按 Enable:
![Figure 18.4 -- Enabling Mac support]
![img/Figure_18.04_B14717.jpg]
图 18.4 -- 启用 Mac 支持
确保选项设置为 Scale Interface to Match iPad。
现在,使用 Mac 作为目标设备构建并运行应用。你应该看到以下 UI:
![Figure 18.5 -- The Mac version of the to-do app]
![img/Figure_18.05_B14717.jpg]
图 18.5 -- 待办事项应用的 Mac 版本
这非常简单!诚然,我们的示例应用程序非常简单直接。但通过简单的点击,它已经兼容并且"可用"在 Mac 上。我们没有做任何工作!然而,尽管应用程序可用,但它没有 Mac 风格。让我们列出一些与传统 Mac 应用程序不同的元素:
-
Mac 应用程序不使用工具栏来包含诸如**+**符号之类的操作。这些操作通常位于右下角。
-
例如设置闹钟的按钮看起来不像 Mac 按钮。
-
Mac 应用程序不太使用这种类型的 Picker。
-
Mac 应用程序使用复选框而不是开关。
-
视图已缩放到原始尺寸的 77%。这可能会破坏您代码中的某些约束,您可能需要审查 UI 的部分。
您的 iPad 应用程序在 iPad 上具有越复杂的 UI,使用这种方法就越感觉不像 Mac。但我们不能抱怨太多;我们只是通过一键使其兼容!
这个迭代始终是移植您的 iPad 应用程序到 Mac 的第一步。现在我们有了 Mac 应用程序,我们将致力于改进 UI,使其看起来更像 Mac。为此,我们将使用苹果公司创建的新方法:Optimize Interface for Mac。这种方法有其优点和缺点,我们将在下一节中看到它们。
优化 iPad 应用程序以适应 Mac
在本节中,我们将使用 iPad 应用程序上的Optimize Interface for Mac选项,并学习如何将结果调整以适应我们应用程序上预期的 Mac 风格界面。
在项目导航器中,在Deployment Info 部分,将 Mac 选项更改为Optimize Interface for Mac:
图 18.6 -- 使用 Optimize Interface for Mac
选择此选项后,将目标更改为 Mac 并启动应用程序。您应该会收到以下崩溃信息:
swift
[General] UIPickerView is not supported when running Catalyst apps in the Mac idiom.
当我们使用此示例中显示的UIPickerView实例时,我们没有遇到任何问题。一个解决方案可能是使用 SwiftUI 的 Picker(在ComboBox下可用)。
我们现在将学习如何根据运行它的设备来使用或不在我们的应用程序中使用特定的组件。我们将在这个 iPad 上安装这个UIPickerView,但我们将从 Mac 版本中移除它(为了现在能够编译)。我们将通过使用 Storyboard 变体来实现这一点。
Storyboard 变体可以帮助我们根据某些参数(如设备、屏幕宽度、高度和色域)在视图控制器中安装或卸载特定组件。
让我们在应用程序在 Mac 上运行时从单元格中卸载UIPickerView。按照以下步骤操作:
-
打开
Main.storyboard文件并转到Detail View Controller 。选择entry 单元格原型:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_18.07_B14717.jpg图 18.7 -- Detail View Controller
-
现在,选择单元格的
UIPickerView,并在其属性检查器 窗口中,通过点击**+符号在 已安装**部分添加一个变体:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_18.08_B14717.jpg图 18.8 -- 添加已安装变体
-
在出现的弹出窗口中,从方言 选择器中选择Mac :https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_18.09_B14717.jpg
图 18.9 -- 添加 Mac 方言变体
-
现在,您想要取消选中新的变体,这样这个组件就不会在 Mac 方言中安装:
图 18.10 -- 卸载 Mac 方言变体
太好了!通过在故事板中使用变体,您可以根据运行它的设备和其它因素来指定安装某些组件的时间!现在尝试再次启动应用,以 Mac 为目标。这次,应用应该不会崩溃,您将看到以下屏幕:
图 18.11 -- 为待办事项应用 Mac 版本的第一次优化
太好了!我们成功地使用故事板变体来适配我们应用的 Mac 版本。理想情况下,您现在应该找到一个适用于 Mac 的替代UIPickerView(SwiftUI 的 Picker 是一个例子)。这将是您的家庭作业!
你可以在前面的屏幕截图中看到,当使用优化界面选项将 iPad 应用转换为 Mac 时,仍然存在一些常见问题:
-
在类别表格单元格中,字体大小与数字字体大小不同。在我们的 iPad 应用中,字体大小是相同的。以**工作(3)**的字体大小为例,仔细查看一下。
-
Mac 应用不使用工具栏中的按钮,如**+**。此类操作最常见的地方是窗口的右下角。
接下来让我们处理这两个问题。打开Main.storyboard文件,检查根视图控制器表格中标签使用的字体:
图 18.12 -- 根视图控制器单元格标签
如果您查看这两个标签中使用的字体大小,它们并不相同。第一个标签使用的是正文 字体。第二个标签使用的是系统 - 17.0 字体。但为什么在缩放界面以匹配 iPad 中它们看起来一样呢?原因是,在那个选项中,视图被缩放到原始大小的 77%,两种字体看起来都一样。但在优化界面以匹配 Mac 中,视图保持 1:1 的比例,预定义的文本样式会适应视图内容大小。因此,如果您打算使用带有优化界面的 iPad 应用在 Mac 上,最好的做法是在您的整个应用中使用这些预定义的样式。您将不必根据设备进行调整。
为了解决这个问题,请在标签属性检查器中将系统 -- 17.0 字体更改为正文字体:
图 18.13 -- 使用文本样式
现在在 Mac 目标上运行应用:
图 18.14 -- 在 Mac 上新的字体样式结果
如前一个截图所示,工作和**(3**)的字体大小现在相同。如果你在 iPad 上运行应用,它们也将相同。我们不再有任何差异。
在这个修复到位后,是我们时候在DetailViewController上隐藏工具栏了。Mac 应用不使用工具栏来显示单个动作,就像我们现在所做的那样:
图 18.15 -- 带有右侧动作按钮的工具栏
我们学习了如何使用故事板变体来显示/隐藏元素,但对于这个工具栏,我们将以编程方式来做。组件仍然会被安装,但我们将在 Mac 上隐藏它。打开DetailViewController文件,并更改viewWillAppear方法实现:
swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if traitCollection.userInterfaceIdiom == .mac {
navigationController?.setToolbarHidden(true, animated: false)
} else {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(createTapped))
navigationController?.setToolbarHidden(false, animated: animated)
}
}
检查高亮的代码。我们能够通过使用traitCollection的userInterfaceIdiom属性来检测我们正在哪个设备上启动应用。当它是.mac时,我们隐藏工具栏,并且当我们处于其他设备(如 iPad)上时,仅添加右侧的**+**按钮。
如果你构建并执行 Mac 目标上的应用,**+**按钮消失了。太好了!但现在我们无法创建新的待办事项!我们失去了对 Mac 上此按钮的访问。我们需要以不同的方式适应这种场景。
传统上,对于 Mac 界面,选择Main.storyboard文件。选择详细视图控制器的表格视图:
图 18.16 -- 详细视图控制器表格视图
现在我们将在控制器的底部为一个新的按钮腾出空间,但仅限于 Mac。请按照以下步骤操作:
-
进入详细视图控制器 的表格视图的大小检查器。
-
编辑将表格视图底部与控制器底部连接的约束,
-60:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_18.17_B14717.jpg图 18.17 -- 编辑表格视图底部约束
-
现在在视图控制器和表格之间添加一个新的
UIButton。将按钮标题设置为创建 。添加以下截图显示的四个约束:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ms-ios14-prog/img/Figure_18.18_B14717.jpg图 18.18 -- 添加新按钮
-
现在从表格拖动到新添加的创建 按钮,添加一个垂直间距 约束(你可以按住Ctrl拖动)。
-
我们想为按钮添加一个变体。我们希望按钮仅适用于 Mac 方言。所以,请为 Mac 添加一个变体,并取消选中默认选项(就像我们在本章之前为
UIPickerView所做的那样)。通过这样做,按钮将仅在 Mac 设备上可见:![图 18.19 -- 仅在 Mac 中安装新按钮![图 18.19 -- Mac 应用程序最终版本
图 18.19 -- 仅在 Mac 中安装新按钮
-
如果您操作得当,如果使用 iPad 作为预览设备,按钮应该会在故事板中消失。您可以将设备更改为Mac ,使用Mac 方言 (而不是 Mac 与 iPad 方言!),它将再次显示(您可以在故事板窗口的底部选项中这样做):![图 18.20 -- 设备预览选择
![图 18.20 -- 设备预览选择
图 18.20 -- 设备预览选择
-
最后,我们需要再次编辑此列表中第 2 项的约束条件。您已向其中添加了
-60常数。现在我们想将其恢复到0,就像之前一样。
现在请使用 iPad 目标执行应用程序。您应该仍然在右上角看到**+符号,并且看不到底部的创建**按钮。现在在 Mac 目标上执行它。您应该在控制台上得到以下错误:
swift
[LayoutConstraints] Unable to simultaneously satisfy constraints.
这是因为我们添加了两个不能共存的不同约束条件:
-
0 -
9
实际上,我们需要两个常数,一个用于 iPad 设备,另一个仅适用于 Mac。请将其中一个常数的优先级更改为250:
![图 18.21 -- 更改约束优先级
![图 18.21 -- Mac 的初始缩放版本
图 18.21 -- 更改约束优先级
通过这种方式,我们可以同时保留两个约束条件,但它们不会相互排斥。当没有安装 Mac 按钮时,该约束条件将不会生效,另一个约束条件将应用(将表视图底部与安全区域底部对齐)。请使用 Mac 目标执行应用程序:
![图 18.22 -- Mac 应用程序最终版本
![图 18.22 -- Mac 应用程序最终版本
图 18.22 -- Mac 应用程序最终版本
看起来很棒!现在我们有了两种不同的 UI 变体,一个用于 iPad,另一个更适合 Mac 标准。现在将其与缩放界面以匹配 iPad为我们提供的之前的 iPad 外观版本进行比较:
![图 18.23 -- Mac 的初始缩放版本
![图 18.23 -- Mac 的初始缩放版本
图 18.23 -- Mac 的初始缩放版本
如您所见,这相当不同!新版本感觉更符合 Mac 原生风格。按钮、工具栏、控件位置以及元素的整体缩放感觉对 Mac 版本来说要好得多。这需要做更多的工作,但结果是值得的。您始终可以将 iPad 应用程序的初始端口移植到 Mac,使用缩放界面以匹配 iPad ,然后稍后对优化界面以匹配 Mac进行工作!
在本节中,我们从简单的待办事项 iPad 应用开始。我们使用 Mac Catalyst 将其移植到 Mac。首先,我们使用了将界面缩放以匹配 iPad 选项,使应用一键即可在 Mac 上使用。但随后,我们希望改进 UI,使其更符合 Mac 标准,因此我们使用了新的优化界面以适应 Mac选项。这个选项不像缩放选项那样直接,我们不得不调整某些尺寸,删除一些在 Mac 上不可用的 UI 控件,并为 Mac 创建不同的变体。但结果看起来很棒!
现在我们用总结来结束这一章。
总结
我们以简短的 Mac Catalyst 介绍开始了这一章。我们解释了苹果如何通过 Mac Catalyst 为开发者提供了一种简单的方法,将 iPad 应用移植到 Mac 应用,以及这一新特性带来的所有好处。
然后,我们讨论了 2020 年 Mac Catalyst 的最新改进和变化。在这些新特性中,我们提到了优化界面以适应 Mac的含义,以及它是如何增强 iPad 应用,使其成为优秀的 Mac 应用的。
最后,以 iPad 应用为起点,我们使用 Mac Catalyst 的两种方法:将界面缩放以匹配 iPad 和优化界面以适应 Mac,创建了 Mac 版本。我们展示了它们的优缺点,并应用了你在使用这两种方法时最常遇到的修复和改进。通过将它们与同一应用进行比较,你对它们之间的主要区别以及何时应用哪一个或另一个有了了解。
在下一章,我们将学习如何以及何时测试你的代码。