SwiftUI-项目构建与技巧提升指南-全-

SwiftUI 项目构建与技巧提升指南(全)

原文:zh.annas-archive.org/md5/5f402d2b24e6f22ff5b57b80832447ff

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎来到 通过构建项目提升 SwiftUI 技能。这本书是你在使用 Swift 和 SwiftUI 构建项目方面的终极伴侣,这两项技术是 Apple 平台开发的前沿技术。无论你是刚开始你的旅程,还是已经有所经验,这本全面指南旨在帮助你将技能提升到新的水平。

Swift 和 SwiftUI 已经彻底改变了开发者为 iPhone、iPad、Mac 和 Apple Watch 创建应用的方式。凭借它们直观的语法、强大的功能和无缝集成,它们为构建令人惊叹且性能卓越的用户界面提供了无限可能。

在这本书中,我们将深入探讨 Swift 和 SwiftUI 的核心概念,揭示它们的复杂性并探索它们的全部潜力。通过一系列动手项目,你将获得实践经验,并更深入地理解如何利用这些技术构建现实世界中的应用。

无论你对创建动态用户界面、集成后端服务、实现动画和过渡,还是探索数据持久性和可访问性等高级主题感兴趣,这本书都能满足你的需求。每个项目都经过精心设计,旨在解决应用开发的一个特定方面,让你在学习过程中能够学习和应用新技术。

随着你逐章前进,你不仅将提高你的编码技能,还将学习最佳实践和设计模式,这些将使你能够编写干净、可维护和可扩展的代码。你将发现 SwiftUI 的声明性语法的力量,它强大的数据绑定能力,以及它与其他 Apple 框架的无缝集成。

为了方便你的学习,每个项目都配有详细的说明、代码示例和逐步指导。你还将找到经验丰富的开发者的技巧、窍门和见解,以帮助你克服常见挑战并充分利用 SwiftUI 丰富的生态系统。

到这本书结束时,你将在 Swift 和 SwiftUI 方面打下坚实的基础,并拥有应对自己项目的信心。你将具备构建令人印象深刻、用户友好的应用所需的技能,这些应用能够充分利用 Apple 平台的全部潜力。

因此,无论你是渴望进入激动人心的应用开发世界的初学者,还是寻求扩展知识的经验丰富的开发者,通过构建项目提升 SwiftUI 技能 将成为你掌握 Swift 和 SwiftUI 以及构建出色应用的必备资源。准备好提升你的技能,开始一段充满创意和创新的激动人心的旅程。让我们开始吧!

随着你阅读本书的章节,你将更深入地理解 Swift 和 SwiftUI 及其在各个 Apple 平台上的应用。每个章节都旨在提供逐步指导和动手实践,让你能够构建真实世界的项目,并成为 Apple 生态系统中的熟练开发者。让我们共同踏上这段激动人心的旅程,掌握 Swift 和 SwiftUI 开发的技艺。

本书面向的对象

本书面向对编程概念有基本了解、对 Swift 有实际操作经验的 iOS、iPadOS、macOS 和 watchOS 开发者。它也适合希望过渡到 SwiftUI 并探索在多个 Apple 平台构建项目的经验丰富的开发者。无论你是学生、专业人士还是爱好者,本书都将作为一项宝贵的资源,帮助你扩展技能并释放 Swift 和 SwiftUI 的潜力。

本书涵盖的内容

第一章, Swift 和 SwiftUI 回顾,回顾了 Swift 编程语言和 SwiftUI 框架的基础知识。它将为熟悉这些概念的人提供复习,并为初学者提供一个坚实的基础。

第二章, iPhone 项目 – 税务计算器设计,探讨了 iPhone 税务计算器应用的设计和布局。了解如何创建直观且视觉上吸引人的用户界面,以及 UI 元素和用户交互模式。

第三章, iPhone 项目 – 税务计算器功能,展示了如何实现 iPhone 税务计算器应用的功能,处理用户输入,执行计算,并显示准确的结果,以提供无缝的用户体验。

第四章, iPad 项目 – 照片库概览,提供了 iPad 照片库应用的概述。了解创建引人入胜和沉浸式照片浏览体验所需的关键功能和用户界面组件。

第五章, iPad 项目 – 照片库增强视图,展示了如何通过添加功能和高级用户界面元素来增强 iPad 照片库应用,实现导航模式,并融入丰富的交互,以提升用户体验。

第六章, Mac 项目 – 应用商店栏,展示了如何在 Mac 平台上设计应用商店的导航和工具栏元素。你将了解布局选项、定制以及创建跨 Mac 应用程序的一致用户界面。

第七章, Mac 项目 – 应用商店主体,涵盖了为 Mac 应用商店应用构建主要内容区域的方法。你将学习如何实现搜索功能、显示应用列表和管理用户评论,以创建无缝且引人入胜的用户体验。

第八章观看项目 – 健身伴侣设计,展示了如何设计适用于 Apple Watch 的健身伴侣应用的用户界面。针对小屏幕进行优化,利用触摸交互创建引人入胜且直观的体验。

第九章观看项目 – 健身伴侣 UI,实现了适用于 Apple Watch 的健身伴侣应用的用户界面组件和功能。您将学习如何跟踪和显示健身指标,提供通知,并支持锻炼会话以增强用户的健身之旅。

为了充分利用本书

为了充分利用本书,假设您对编程概念有基本的了解,并对 Swift 和 SwiftUI 有实际的操作理解。iOS、iPadOS、macOS 或 watchOS 开发的经验将有益但不是必需的。此外,您应该能够访问装有 macOS 的计算机,因为本书主要关注 Apple 平台上的开发。

本书涵盖的软件和硬件包括以下内容:

  • 软件:Xcode 12(或更高版本) – Apple 平台的集成开发环境(IDE)

  • 操作系统:macOS 10.15 Catalina(或更高版本) – 必需的 Xcode 安装和开发

  • Swift 5.0(或更高版本) – 用于在 Apple 平台上开发应用程序的编程语言

  • SwiftUI – 用于在 Apple 平台上构建用户界面的框架

请确保您拥有进行本书中示例和练习所需的必要软件和硬件。

关于额外的安装说明,建议在 macOS 系统上安装 Xcode。您可以从 Mac App Store 或 Apple 开发者网站下载并安装 Xcode。如果在安装过程中遇到任何问题,请参阅 Apple 提供的官方文档。

对于使用本书电子版的读者,我们建议您亲自输入代码示例,而不是复制粘贴。这将帮助您更好地理解和内化所展示的概念,同时最大限度地减少由于格式或语法差异而遇到错误的可能性。

享受您使用 Swift 和 SwiftUI 学习和构建项目的旅程!如果您有任何问题或需要进一步的帮助,请参阅书中的资源或联系作者或出版社。快乐编码!

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects。如果代码有更新,它将在 GitHub 仓库中更新。

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

使用的约定

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

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在这个例子中,我们创建了一个ColorCircle,这是一个符合 Animatable 协议的自定义视图。”

代码块设置如下:

 if ( MAX_COUNT == counter ){
print( "Counter is 10" )
print( "Well done" )
}

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”

小贴士或重要注意事项

看起来是这样的。

联系我们

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

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

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问www.packtpub.com/support/errata并填写表格。

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

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

分享您的想法

一旦您阅读了《通过构建项目提升 SwiftUI 技能》,我们很乐意听听您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。

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

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走?您的电子书购买是否与您选择的设备不兼容?

别担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何时间、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日免费内容的专属访问权限

按照以下简单步骤获取好处:

  1. 扫描下面的二维码或访问以下链接!![免费 PDF 二维码]

https://packt.link/free-ebook/9781803242071

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱

第一章:Swift 和 SwiftUI 回顾

首先,我想感谢你阅读我的书,无论是购买、借阅,还是你在亚马逊预览中偷看,我都感谢你。

本章将回顾 Swift 和 SwiftUI。我们将首先介绍为即将到来的项目在整个书中使用的编码标准以及 Swift 和 SwiftUI 的历史。然后,我们将查看完成本书中项目的要求。编码标准可能会对程序员产生很大的分歧,但它们实际上不应该这样。如果你有任何不同意见,请随时在@SonarSystems上发推文并告诉我原因。但不要让这一点影响本书以及你可以从中获得的内容。

之后,我们将查看一些具体的 SwiftUI 代码示例以及预览,以结束回顾。我们将查看如何使用视图和控制;这些是应用程序用户界面的视觉构建块。我们将贯穿全书使用它们在屏幕上绘制和组织应用程序的内容。接下来,我们将查看布局和展示,了解我们如何将视图组合在堆栈中,动态创建视图的组和列表,以及定义视图展示和层次结构。希望你喜欢这一章!

如果你有任何问题,欢迎加入我的 Discord:discord.gg/7e78FxrgqH

在本章中,我们将涵盖以下主题:

  • 什么是 Swift?

  • 什么是 SwiftUI?

  • 理解和实现视图

  • 理解和实现布局

到本章结束时,你将学习 Swift 和 SwiftUI 的历史,以及如何从 SwiftUI 实现基本组件;这将为我们在本书中创建的项目奠定基础。

技术要求和标准

本书要求您从 Apple 的 App Store 下载 Xcode 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索Xcode,选择并下载最新版本。打开 Xcode,并遵循任何额外的安装说明。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 的 14 版本具有以下功能/要求:

  • 包含 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK。

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本在设备上的调试。

  • 需要 macOS Monterey 12.5 或更高版本的 Mac。

从以下 GitHub 链接下载示例代码:

github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

这里是硬件要求:

  • 你需要一个 Intel 或 Apple Silicon Mac

  • 4GB RAM 或更高

这里是软件要求:

  • macOS 11.3(Big Sur 或更高版本)

  • Xcode 14

  • iOS 16 用于 iPad/iPhone 真实设备测试

  • watchOS 9.0 用于 Watch 真实设备测试

  • tvOS 16.0 用于 Apple TV 真实设备测试

这里有一些额外的要求:

  • Swift 的中级知识

  • 对其他面向对象编程语言(如 C++ 或 Objective-C)的中级知识

重要提示

虽然你可以在 Xcode 提供的模拟器中测试应用程序,但强烈建议你在真实设备上测试它们。

使用的标准

在本节中,我们将探讨本书中使用的编码标准。保持一致的标准并了解这些标准很重要。

我们为什么需要编码标准?

编写好的代码很重要,好的代码不仅仅是运行良好的代码,而是易于维护和阅读的代码。好的代码是一种艺术形式。

在以下章节中,我们将介绍在 Swift 编程语言中使用的标准,这些标准将贯穿整本书。如果你不完全同意这些标准,那也无所谓,但我认为列出这些标准很重要,以防你遇到从未见过的内容,比如 Yoda 条件——你们中有谁使用它们吗?如果是的话,请在 @SonarSystems 上给我发推文。

缩进

你应该始终缩进你的代码,使其与同一层级的其他代码对齐。使用实际的制表符进行缩进代码。你可以在以下代码片段中看到这一点:

if ( 10 == counter ){
    print( "Counter is 10" )
    print( "Well done" )
}
else
{ print( "Wrong" ) }

这很有帮助,因为你可以很容易地看到代码在层次结构中的位置。Xcode 提供了一个方便的快捷键来缩进代码;只需在键盘上按 ^ + I 即可。

括号风格

当你为结构编写代码时,应该始终使用 Allman 括号(以 Eric Allman 命名),即使只有一行(在许多语言中,如 C++,一行代码不需要括号,但为了易于阅读,应该使用它们)。如果你在结构中只有一行代码,将括号和代码放在同一行上。我更喜欢不在结构的第 一行放置开括号。唯一我会将括号放在同一行的情况是,结构中还没有代码;那时,将开括号和闭括号放在同一行,并在它们之间留一个空格。

以下代码片段显示了首选的括号风格:

if ( 10 == counter ){
    print( "Counter is 10" )
    print( "Well done" )
}
else
{ print( "Wrong" ) }

这很有帮助,因为它有助于维护代码,并在试图弄清楚结构开始和结束的地方时提高可读性。这在快速处理大量文件并试图找出问题时特别有用。

空格使用

空格和行的使用取决于你在代码中使用的内容。删除任何尾随空格(行尾的空格)。你可以在以下代码片段中看到这一点:

print( "Well done" )

一些编辑器会自动删除尾随空格,但有些不会。这可能会导致合并冲突。你应该与你的同事和开源社区的人协调,了解大家使用的策略,并确保你们都使用相同的策略。

逗号和冒号的使用

当使用逗号/冒号时,在逗号/冒号后放置一个空格。你可以在以下示例中看到这一点:

func Greet( person: String, alreadyGreeted: Bool ) -> String

这使得阅读更容易。

括号内的空格

ifelseelse ifforwhile和其他控制结构(某些语言可能有其他控制结构,因此只需将这些标准应用于这些结构)的左右括号两侧各放置一个空格。您可以在以下代码片段中看到这一点:

if ( 10 == counter )

这些空格使理解更容易,尤其是在快速浏览时。

一元运算符

当在语句中使用一元运算符,如++时,在其后放置一个空格。您可以在以下代码片段中看到这一点:

for ( i = 0; i < 5; i++ )

这些空格使代码保持一致性,并使其更容易浏览。

函数的括号空格

在定义函数时,在左右括号内部放置一个空格。您可以在以下代码片段中看到这一点:

func GetString( id: Int )

这些空格使代码保持一致且易于阅读。

函数调用间距

调用一个函数也遵循与定义函数完全相同的规则。您可以在以下代码片段中看到这一点:

Adder( num1: 10, num2: 5 )

这样读起来更容易,也更美观。

方括号间距

当使用方括号时,在左右括号内部不要使用任何空格。您可以在以下代码片段中看到这一点:

let vectors : [[Int]] = [[1, 2, 3], [4, 5, 6]]

虽然这与括号不同,但这样看起来更好。行长度通常不应超过 80 个字符,但如果有助于可读性,则可以例外。

类型转换

当进行类型转换时,始终将类型放在括号内,而不是变量,并在括号内部使用一个空格,但不在关闭括号外部使用空格。您可以在以下代码片段中看到这一点:

( Int )age

这样,它更容易阅读,也更美观。它还使在嵌套时确定变量和类型转换之间的关系更容易。

命名约定

本节将介绍本书中使用的命名约定。始终使用有意义的但不要太长的名称。尽可能让代码自我说明,例如,不要使用scoreForThePlayerForLevel1这样的名称,而是使用scorePlayerLevel1)。

  • EpicFunction

  • epicVariable

  • 常量 - 所有字母大写

所有属于对象(如类)的变量都应该以下划线开头 - 例如,_localVariable.

这很有帮助,因为它使代码保持一致,从而提高效率、可读性和可维护性。它帮助他人更好地理解您的代码。它看起来也很不错。

Yoda 条件

当比较变量和值时,始终将值与变量进行比较,而不是将变量与值进行比较。您可以在以下代码块中看到这一点:

if ( 10 == counter )

这很有帮助,因为它可以防止您意外地将值分配给变量而不是进行比较,从而得到一个真值,并使 if 语句等于真。

注释

这是一个大多数人都不喜欢的领域,很多人会错过或者拖到最后一刻才意识到这需要大量的努力,所以我会建议你在编写代码的过程中随时添加注释。

在代码中注释时,使用 // 进行单行注释,使用 /* */ 进行多行注释(根据你使用的编程语言,注释的方式可能会有所不同),并且所有字母都应该小写。你可以在下面的示例中看到这一点:

/*    This checks if the counter is 10
    If successful then inform user
*/
if ( 10 == counter )
{
    print( "Counter is 10" )
    // Print congratulation message
    print( "Well done" )
}

当试图理解你的代码做了什么时,这很有用,尤其是如果它很复杂,或者你在长时间之后阅读它。当其他人/程序员试图理解你的代码时,这也很有用,因为你的编码方式可能与其他人不同,注释对于帮助他们理解正在发生的事情至关重要。

没有魔法数字

魔法数字是一个看起来像是随机放置在代码中且没有任何上下文或明显意义的数字。这就是我们所说的反模式,因为阅读和理解代码变得非常困难,难以维护。代码必须是故意的,而且仅仅通过快速浏览,你就能理解它;这是代码质量的基础。

使用常量和变量。使用 MAX_COUNT 而不是像 10 这样的任意数字。你可以在下面的代码片段中看到这一点:

if ( MAX_COUNT == counter ){
    print( "Counter is 10" )
    print( "Well done" )
}

这使得代码库更容易管理和理解。

在本节中,我们探讨了我在编码中使用的标准。我认为解释它们非常重要,不一定是为了说服你使用我选择的标准,而是为了在你看到书中任何似乎与你的正常标准不同之处时有一个参考点。最重要的是,与其他任何与你协作的队友保持标准的一致性。这是最重要的事情;使用的具体标准是次要的,但一致性是首要的。

什么是 Swift?

在本节中,我们将介绍 Swift 是什么,它的历史,以及它在宏观层面上的工作方式。如果你是专家,只想阅读关于 SwiftUI 的内容,请随意跳过这一节。

Swift 是由苹果公司和开源社区创建的一种编程语言。它是一种通用、编译和多种模式的编程语言。它于 2014 年发布,作为苹果之前语言 Objective-C 的替代品。由于 Objective-C 自 20 世纪 80 年代初以来几乎保持不变,它缺少了许多现代语言所拥有的功能。因此,Swift 的创建开始了;它席卷了苹果开发者生态系统,成为了一种非常受欢迎的编程语言。全球各地的公司都需要它,并为那些知道如何利用其巨大功能的人提供优厚的报酬。根据以下图表中显示的 PYPL 指数,Swift 是最受欢迎的 10 种语言之一,因此对于任何程序员来说都是必备的:

图 1.1 – 编程语言流行指数(来源:https://www.stackscale.com/wp-content/uploads/2022/09/PYPL-index-popular-programming-languages-2022.jpg)

图 1.1 – 编程语言流行指数(来源:www.stackscale.com/wp-content/uploads/2022/09/PYPL-index-popular-programming-languages-2022.jpg

Swift 已被用于创建许多应用程序,包括但不限于以下:

  • LinkedIn

  • Firefox

  • WordPress

  • Wikipedia

  • Lyft

Apple 的 Cocoa 和 Cocoa Touch 框架与 Swift 无缝协作。此外,它还与 Apple 之前的编程语言 Objective-C 无缝协作,Objective-C 已被开发者使用了几十年。这使得它成为最现代且易于使用的语言之一。那些需要等待框架发布/更新以适应所选语言的年代已经过去了;相反,已经有成千上万的 Objective-C 项目存在,可以在等待期间使用。

Swift 是使用 低级虚拟机 (LLVM) 编译器框架构建的,该框架是开源的,自 Xcode 6 版本以来捆绑提供,并于 2014 年发布。它使用 Apple 设备上的 Objective-C 运行时库,因此允许用 C、Objective-C、C++ 和 Swift 编写的代码在单个应用程序中协同工作。以下图示从宏观层面解释了 Swift、LLVM 和不同架构之间编程语言的关系:

图 1.2 – LLVM 和语言关系(来源:https://miro.medium.com/max/1024/1*VWogVHhCagxopvAKVFjBeA.jpeg)

图 1.2 – LLVM 和语言关系

(来源:miro.medium.com/max/1024/1*VWogVHhCagxopvAKVFjBeA.jpeg

LLVM 使用 Clang 作为前端,Clang 是 C、C++、CUDA 或 Swift 的 swiftc 编译器。然后它将代码转换为 LLVM 使用的格式,进而将其转换为机器代码,机器代码随后在硬件上运行/执行。

如需了解更多关于 LLVM 的信息,请自由使用以下链接:

为了从宏观角度理解作为 Swift 开发者可用的功能,请查看以下图表:

图 1.3 – Swift 语言参考

图 1.3 – Swift 语言参考

(来源:gogeometry.com/software/swift/swift-language-reference-mind-map.jpg

上述图表是一个思维导图,展示了 Swift 提供的所有高级功能及其子功能以及它们如何相互关联。

在本节中,我们介绍了 Swift 是什么,它是如何工作的,以及它的流行程度。

什么是 SwiftUI?

在本节中,我们将介绍 SwiftUI 是什么,以及我们将利用本书中的哪些功能来创建我们的项目。如果您对 SwiftUI 感到舒适并且只想查看项目,那么请随意跳过本章的其余部分。

SwiftUI 是建立在 Swift 编程语言之上的用户界面框架。它为创建应用程序的用户界面提供了许多组件;以下是对这些组件的宏观列表:

  • 视图和控制

  • 形状

  • 布局容器

  • 集合容器

  • 呈现容器

除了前面列表中的组件外,SwiftUI 还为我们提供了事件处理器,允许我们的应用程序对点击、手势以及可能从用户那里接收到的所有类型的输入做出反应。该框架提供了从模型到用户与之交互的视图和控制器的数据流管理的工具。

现在,我们将探讨 Swift 的不同核心功能,包括您可以带走、修改并在自己的项目中使用的示例。

视图和控制

视图和控制是您应用程序 UI 的基础块。使用视图,我们可以构建您希望为应用程序创建的 UI。其复杂性可以是您想要的任何程度,简单或极其复杂——完全取决于您——我们将在接下来的章节中更详细地探讨这一点。

视图可以是以下任何一种:

  • 文本

  • 图片

  • 形状

  • 自定义绘图

  • 所有这些的组合

控件使用户能够与适应其使用平台和上下文的 API 进行交互。

形状

SwiftUI 中的形状是二维对象,如圆形和矩形。还可以利用自定义路径来设置您自己的形状/结构的参数;我们将在接下来的章节中更详细地探讨形状。

形状提供了添加样式的功能,包括但不限于以下内容:

  • 环境感知颜色

  • 丰富的渐变

  • 前景中的材质效果

  • 背景

  • 形状的轮廓

布局容器

布局负责组织应用 UI 的元素。堆叠和网格用于根据内容或界面尺寸的变化更新和修改其中子视图的位置。布局可以嵌套在彼此之中;这可以做到任意多级,从而允许你创建复杂的布局。还可以设计自定义布局以提供更多灵活性;我们将在稍后更深入地探讨布局容器。

收集容器

收集可以用来组装具有复杂功能的动态视图。例如,你可以创建一个允许你滚动浏览大量数据的 List 视图。列表自动提供基本功能,但除此之外,你还可以通过最小配置添加其他功能,如滑动、双击和下拉刷新。

如果只需要简单的网格或堆叠配置,请使用布局容器;我们将通过代码示例进一步说明这些。

展示容器

展示容器用于为应用 UI 提供结构。这使用户能够更容易地导航,在应用中跳转。当其复杂性增加且包含更多视图时,这尤其有用。例如,你可以通过NavigationStack启用在一系列视图之间前后导航,以及通过TabView选择要显示的视图。

在本节中,我们介绍了 SwiftUI 在宏观层面提供的不同功能;我们还讨论了它们的子功能和用途。

理解和实现视图

在本节中,我们将探讨视图以及如何在 SwiftUI 中实现它们。我们还将查看如何组合这些视图。

视图是应用用户界面的基本构建块。视图对象在其矩形边界内渲染内容并处理与该内容的任何交互。

在以下各节中,我们将展示每种类型视图的源代码和示例。如果你需要更多信息,请访问苹果的文档developer.apple.com/documentation/uikit/views_and_controls

文本视图是什么?

在我们的应用中显示文本是非常常见的,我们通过使用文本视图来实现这一点,它绘制一个字符串。默认情况下,它分配了一个最适合其显示平台的字体;然而,你可以使用font(_:)视图修饰符来更改字体。

下面的代码片段显示了实现文本视图所使用的代码:

var body: some View{
    VStack
    {
        Text( "Hello World" )
    }
    .padding( )
}

文本视图被插入到VStack中用于填充目的,但这不是必需的。

前面的代码展示了如何通过传递一个字符串来使用 Text 视图简单地显示所需显示的内容。

下图显示了前面代码的输出:

图 1.4 – 文本视图预览

图 1.4 – 文本视图预览

下一节将介绍图片视图及其实现方法。

图片视图是什么?

图片视图可以用来在 SwiftUI 布局中渲染图片。图片是提供更多上下文和改善整体用户体验的绝佳方式。图片视图可以从你的包、系统图标、UIImage等加载图片,但最常用的方法是加载从你的包和系统图标。

以下代码片段显示了实现图片视图所使用的代码:

var body: some View{
    VStack
    {
        Image( systemName: "cloud.heavyrain.fill" )
    }
    .padding( )
}

在前面的代码中,我们使用系统图标实现了一个图片视图,但你也可以轻松指定自己的图片文件。我们将在本章后面这样做。

在这个例子中,使用了系统图标,但使用你的包和UIImage的过程是类似的。

以下图显示了前面代码的输出:

图 1.5 – 图片视图预览

图 1.5 – 图片视图预览

下一节将介绍我们可用的不同形状视图及其使用代码。

形状视图是什么?

SwiftUI 为我们提供了五种常用的基本形状。这些形状是矩形、圆角矩形、圆形、椭圆形和胶囊。后三种在行为上非常微妙地不同,这取决于你提供的尺寸。

以下代码展示了如何简单地实现以下任何一种形状:

  • Rectangle

  • RoundedRectangle

  • Capsule

  • Ellipse

  • Circle

这里是代码片段:

var body: some View{
    VStack
    {
        Rectangle( )
            .fill( .white )
            .frame( width: 128, height: 128 )
        RoundedRectangle( cornerRadius: 30, style: .continuous )
            .fill( .blue )
            .frame( width: 128, height: 128 )
        Capsule( )
            .fill( .red )
            .frame( width: 128, height: 50 )

        Ellipse( )
            .fill( .orange )
            .frame( width: 128, height: 50 )
        Circle( )
            .fill( .yellow )
            .frame( width: 128, height: 50 )
    }
    .padding( )
}

这些形状被插入到VStack中以垂直排列,并在它们周围添加了填充。

前面的代码展示了如何实现不同的形状视图以及每个视图所需的参数。

以下图显示了前面代码的输出:

图 1.6 – 形状视图

图 1.6 – 形状视图

在下一节中,我们将探讨如何使用我们已覆盖的视图来组合和创建更复杂的视图。

自定义和组合视图是什么?

对于所有前端开发者来说,开发过程中最关键的部分之一是实现用户界面(UI)。我们可以通过组合我们迄今为止利用的预制的内置视图来创建一个简单的 UI,但有时这还不够;经常会有开发者需要程序化地绘制自定义视图以满足 UI 需求,如果我们无法绘制这些视图,就会产生问题。我们能够利用 SwiftUI 的力量来创建自定义视图,这些视图实际上是之前我们了解的其他视图的组合。

以下代码展示了如何使用多个视图创建我们自己的自定义视图,该视图显示一个迷你个人资料:

import SwiftUIstruct Employee
{
    var name: String
    var jobTitle: String
    var emailAddress: String
    var profilePicture: String
}
struct ProfilePicture: View
{
    var imageName: String
    var body: some View
    {
        Image( imageName )
            .resizable( )
            .frame( width: 100, height: 100 )
            .clipShape( Circle( ) )
    }
}
struct EmailAddress: View
{
    var address: String
    var body: some View
    {
        HStack
        {
            Image( systemName: "envelope" )
            Text( address )
        }
    }
}
struct EmployeeDetails: View
{
    var employee: Employee
    var body: some View
    {
        VStack( alignment: .leading )
        {
            Text( employee.name )
                .font( .largeTitle )
                .foregroundColor( .primary )
            Text( employee.jobTitle )
                .foregroundColor( .secondary )
            EmailAddress( address: employee.emailAddress )
        }
    }
}
struct EmployeeView: View
{
    var employee: Employee
    var body: some View
    {
        HStack
        {
            ProfilePicture( imageName: employee.profilePicture )
            EmployeeDetails( employee: employee )
        }
    }
}
struct ContentView: View
{
    let employee = Employee( name: "Frahaan Hussain", jobTitle: "CEO & Founder", emailAddress: "frahaan@hussain.com", profilePicture: "FrahaanHussainIMG" )
    var body: some View
    {
        EmployeeView( employee: employee )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

前面的代码展示了如何组合视图来创建自定义视图,以及如何组合自定义视图来创建更复杂的视图。这也使得迷你视图可重用。

以下图显示了前面代码的输出:

图 1.7 – 自定义视图(迷你个人资料)

图 1.7 – 自定义视图(迷你个人资料)

在本节中,我们探讨了如何将前几节中的视图组合起来,以创建可重用和可组合的对象,并创建更复杂的视图。

在下一节中,我们将探讨布局,以帮助我们在应用中组织内容。

理解和实现布局

本节将介绍如何使用布局来安排我们的视图,以实现更动态的用户体验。

SwiftUI 布局允许我们作为开发者使用提供的布局工具来安排应用界面的视图。布局告诉 SwiftUI 如何放置一组视图,以及它需要多少空间来完成这项工作以提供所需的布局。

布局可以是以下任何一种,但不仅限于:

  • 懒加载堆叠

  • 间隔符:

    • ScrollViewReader
  • 网格:

    • PinnedScrollableViews

在以下几节中,我们将向您展示每种布局的源代码和示例。

注意

如果您需要更多信息,请访问苹果的文档:developer.apple.com/documentation/uikit/view_layout

什么是懒加载堆叠?

懒加载堆叠是按行排列其子视图的视图,这些行垂直扩展,仅在需要时创建项目。

SwiftUI 提供了两种不同类型的懒加载堆叠,LazyVStackLazyHStack。默认情况下,VStackHStack会预先加载所有内容,如果您在滚动视图中使用它们,这将会很慢,因为这些视图可以包含大量内容。如果您想以懒加载的方式加载内容,即仅在内容出现在视图中时才加载,而不是在视图通常可见但内容不可见时加载,您应适当使用LazyVStackLazyHStack

根据苹果的解释,懒指的是堆叠视图只有在需要时才创建项目。这意味着这些堆叠视图的性能默认已经进行了优化。

LazyVStackLazyHStack仅在 iOS 14.0+、iPadOS 14.0+、macOS 11.0+、Mac Catalyst 14.0+、tvOS 14.0+和 watchOS 7.0+中可用。更多信息可以在以下链接中找到:

以下代码展示了我们如何使用LazyVStack来垂直组织视图,同时在大数据量下保持高效:

import SwiftUIstruct ContentView: View
{
    var body: some View
    {
        ScrollView
        {
            LazyVStack
            {
                ForEach( 1...1000, id: \.self )
                {
                    value in
                    Text( "Line \( value )" )
                }
            }
        }
        .frame( height: 256 )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

前面的代码实现了一个包含1000个文本视图的LazyVStack。文本视图通过循环添加,使其更简单、更高效。

以下图显示了前面代码的输出:

图 1.8 – 懒加载堆叠

图 1.8 – 懒加载堆叠

在下一节中,我们将探讨间隔符,它们帮助我们分散内容。

什么是间隔符?

间隔创建了一个无内容的自适应视图,它可以尽可能多地扩展。例如,当放置在 HStack 中时,间隔会水平扩展,允许堆栈扩展,将视图移出堆栈,在堆栈的大小限制内。

在以下代码中,我们实现了文本和间隔:

import SwiftUIstruct ContentView: View
{
    var body: some View
    {
        Text( "Label 1" )
        Spacer( ).frame( height: 64 )
        Text( "Label 2" )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

前面的代码使用了一个高度为 64 的间隔来分隔两个文本视图。

下图显示了前面代码的输出:

图 1.9 – 间隔预览

图 1.9 – 间隔预览

在下一节中,我们将探讨 ScrollViewReader,它使我们能够移动到任何位置。

ScrollView 和 ScrollViewReader 是什么?

ScrollView 允许用户在可滚动区域内查看内容。用户可以通过执行平台特定的滚动手势来调整内容的可见部分。ScrollView 可以水平和垂直滚动,但不支持缩放。如果您想以编程方式将 ScrollView 移动到特定位置,您应该在其中添加一个 ScrollViewReader。这提供了一个名为 scrollTo() 的方法,只需提供其锚点即可移动到父 ScrollView 中的任何视图。

所有这些都可以通过几行简单的代码实现。您可以通过以下方式查看此代码:

import SwiftUIstruct ContentView: View
{
    var body: some View
    {
        let colors: [Color] = [.red, .green, .blue, .white, .yellow]
        ScrollView
        {
            ScrollViewReader
            {
                value in
                Button( "Go to Number 45" )
                {
                    value.scrollTo( 45 )
                }
                .padding( )
                ForEach( 0..<1000 )
                {
                    i in
                    Text( "Example \( i )" )
                        .font( .title )
                        .frame( width: 256, height: 256 )
                        .background( colors[i % colors.count] )
                        .id( i )
                }
            }
        }
        .frame( height: 512 )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

前面的代码在 ScrollView 中实现了 ScrollViewReader。在其中,我们添加了一个按钮来滚动到具有 id 值为 45 的文本视图。我们使用循环显示了 1000 个具有唯一 ID 的文本视图。

下图显示了前面代码的输出:

图 1.10 – ScrollViewReader 预览

图 1.10 – ScrollViewReader 预览

在下一节中,我们将探讨网格,它允许我们以表格形式组织我们的内容。

网格是什么?

当 SwiftUI 首次发布时,它没有内置集合视图。开发者只剩下两个选择——要么自己构建,要么使用第三方解决方案。在 2020 年的 WWDC 上,苹果为 SwiftUI 框架引入了新功能。其中之一是解决网格视图的需求。现在,SwiftUI 提供了两个新的组件,LazyVGridLazyHGrid。一个是用于垂直网格,另一个是用于水平网格。

以下代码显示了如何实现两行网格:

import SwiftUIstruct ContentView: View
{
    let items = 1...50
    let rows =
    [
        GridItem( .fixed( 32 ) ),
        GridItem( .fixed( 32 ) )
    ]
    var body: some View
    {
        ScrollView( .horizontal )
        {
            LazyHGrid( rows: rows, alignment: .center )
            {
                ForEach( items, id: \.self )
                {
                    item in
                    Image(systemName: "\( item ).circle.fill" )
                        .font( .largeTitle )
                }
            }
            .frame( height: 128 )
        }
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

在前面的代码片段中,我们使用循环实现了一个两行图像网格。使用了 GridItem 组件;苹果公司表示这些是懒加载网格中行或列的描述。这实际上意味着什么?嗯,它本质上是一种指定我们想要多少列/行的方法,从而设置布局模式。LazyGrid 在迭代显示的项目并相应地定位它们时使用这种布局模式。如果您有网页背景,您可以将它想象成响应式网站中的网格系统。

下图显示了前面代码的输出:

图 1.11 – 网格预览

图 1.11 – 网格预览

下一节将介绍 PinnedScrollableViews,它允许某些视图在页面上的其他视图滚动过它们时保持粘性。

什么是 PinnedScrollableView?

SwiftUI 可以在 ScrollView 内提供 PinnedScrollableView。固定视图是粘性视图,可以应用于头部或底部。

以下代码展示了我们如何将视图固定以提供上下文,当其他视图滚动过它时:

import SwiftUIstruct MyCell: View
{
    var body: some View
    {
        VStack
        {
            Rectangle( )
                .fill( Color.red )
                .frame( width: 128, height: 128 )
            HStack
            {
                Text( "Line text" )
                    .foregroundColor( .yellow )
                    .font( .headline )
            }
            Text( "PinnedScrollableViews" )
                .foregroundColor( .green )
                .font( .subheadline )
        }
    }
}
struct ContentView: View
{
    var stickyHeaderView: some View
    {
        RoundedRectangle( cornerRadius: 25.0, style: .continuous )
            .fill( Color.gray )
            .frame( maxWidth: .infinity )
            .frame( height: 64 )
            .overlay(
                Text( "Section" )
                    .foregroundColor( Color.white )
                    .font( .largeTitle )
            )
    }
    var body: some View
    {
        NavigationView
        {
            ScrollView
            {
                LazyVStack( alignment: .center, spacing: 50, pinnedViews: [.sectionHeaders], content:
                {
                    ForEach( 0...50, id: \.self )
                    {
                        count in
                        Section( header: stickyHeaderView )
                        {
                            MyCell( )
                        }
                    }
                } )
            }
        }
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

在前面的代码片段中,我们实现了一个粘性视图,它作为其他视图动态移动过它时的部分标题,但当达到另一个部分时,粘性视图会从屏幕上移除,并且它有自己的粘性视图。

以下图显示了前面代码的输出:

图 1.12 – PinnedScrollableViews 预览

图 1.12 – PinnedScrollableViews 预览

在本节中,我们介绍了我们可访问的不同布局。这些布局使我们能够以更令人愉悦的方式组织数据。在下一节中,我们将查看本书中我们将创建的项目不同设备预览。

设备预览

我们将克服的许多障碍之一是四个主要苹果产品类别之间的差异。这些类别如下:

  • Mac – iMac、Mac Pro、MacBook、任何运行 macOS 的设备,甚至是 Hackintosh

  • iPad – 迷你、常规、Air、Pro,所有这些

  • iPhone – 迷你、Pro、Pro Max,所有 iPhone

  • Apple Watch – 小型、大型、旧款或新款

你会立即注意到它们越来越小。它们自然有不同的用途;Apple Watch 不能取代 Mac,反之亦然。这就是为什么接下来的八个章节被分成对,每个产品类别一个。我们将揭示我们在创建应用程序时面临的设计决策和限制。让我们看看每个产品类别的设置:

Mac

图 1.13 – Mac 设置

图 1.13 – Mac 设置

iPad

图 1.14 – iPad 设置

图 1.14 – iPad 设置

iPhone

图 1.15 – iPhone 设置

图 1.15 – iPhone 设置

Apple Watch:

图 1.16 – Apple Watch 设置

图 1.16 – Apple Watch 设置

乍一看,就可以立即看出它们之间存在差异。这正是我们将在本书剩余部分讨论的差异。下一节将总结本章内容,然后转向我们的第一个项目。

概述

在本章中,我们介绍了 Swift 和 SwiftUI 的历史,以及我们在宏观层面提供的功能,以及 Swift 和 SwiftUI 在技术层面的工作原理。然后,我们探讨了 Swift 和 SwiftUI 之间的区别以及提供了哪些功能,并附上了代码示例供您带走并在自己的项目中使用。之后,我们审视了使用 Swift 和 SwiftUI 开发应用程序所需的必要条件。接着,我们审视了本书中使用的编码标准,为任何您不熟悉的编码风格提供了一个参考点。然后,我们探讨了 SwiftUI 提供的视图和控制,用于创建我们自己的用户体验,包括通过组合基础知识创建的自定义视图。最后,我们探讨了如何使用布局来组织这些视图,并检查了设备预览。

在我们接下来的章节中,我们将探讨设计我们的第一个项目,即我们将创建的税务计算器应用程序。

第二章:iPhone 项目 – 税收计算器设计

在上一章中,我们对 Swift 和 SwiftUI 进行了回顾。我们研究了要求、使用的编码标准和 SwiftUI 组件的基础。我们将在以下章节中使用这些内容。

在本章中,我们将着手设计我们的第一个项目,一个税收计算器。我们将评估设计此类应用程序的要求,并讨论设计规格,以便我们更好地理解所需的内容以及它们如何相互配合。然后,我们将开始我们的应用程序编码过程,构建 UI,这将连接在一起,使应用程序在下一章中完全运行。这个项目将教会我们 SwiftUI 组件的基础以及如何与外部代码库交互。我们将在以下章节中讨论所有这些内容:

  • 技术要求

  • 理解设计规格

  • 构建“计算器”UI

到本章结束时,您将更好地理解所需的内容和我们的应用程序设计。您还将拥有一个骨架 UI,它将作为下一章使计算器工作的基础。

在下一节中,我们将提供我们应用程序设计规格的详细说明,并查看应用程序的外观原型。

技术要求

本章要求您从 Apple 的 App Store 下载 Xcode 版本 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索Xcode,然后选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 14 版本具有以下功能/要求:

  • 包含适用于 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK。

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本的设备调试。

  • 需要运行 macOS Monterey 12.5 或更高版本的 Mac。

从以下 GitHub 链接下载示例代码:

github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

在下一节中,我们将提供我们应用程序设计规格的详细说明,并查看应用程序的外观原型。

理解设计规格

在本节中,我们将查看我们的税收计算器应用程序的设计规格。本节描述了我们将实现的功能。确定所需功能的最佳方法是站在用户的角度,确定他们将如何使用应用程序,并将其分解为单独的步骤。

我们应用程序的功能如下:

  • 收入录入 – 输入收入的能力。

  • 工资摘要 – 将被征税的金额摘要以及剩余的收入。

  • 税收分解 – 指定工资所缴纳的税额分解,即税率。

  • 不同类型的税收 – 能够计算不同类型税收的分解,例如收入、房地产交易、遗产和印花税。

  • 税收地理区域 – 能够计算不同地理区域(包括国家和州)的税收分解。

  • 前两个的结合 – 在不同地理区域计算不同税种的分解能力。

  • 用户系统 – 允许用户创建账户以存储税收计算,查看税收如何随着新税法的变化而变化,等等。

现在我们已经列出了我们希望的理想功能,接下来,对我们来说,确定哪些功能是绝对必要的非常重要。为了做到这一点,我们必须了解我们产品的最终用途。对我来说,创建这个税收计算器的目的不是为了发布它并使其服务于数百万的人,而是作为一个个人项目供我们使用。这是为了展示在本书的背景下 SwiftUI 的基本实现。基于这一点,我知道并不是所有功能都是必需的;实际上,如果某些功能被省略并分配给你作为开发者的额外任务,那将是有用的。基于所有这些,以下是我们将实现的核心功能:

  • 收入输入

  • 工资摘要

  • 税收分解

其余的功能将留给你在完成这一章和下一章后作为练习来实现。下一节将涵盖我们应用程序的验收标准。

验收标准

我们将在下一章的末尾讨论我们应用程序的强制性要求,我们绝对希望在最终产品中看到这些要求。如果可能的话,我们应该尝试使它们可衡量。让我们现在就来做这件事:

  • 错误检测:

    • 非数字NaN)值

    • 等于或小于 0 的值

  • 提供税前和税后的工资

  • 一个饼图来直观地展示分解

  • 进度条来进一步扩展分解

  • 导航,使用户能够轻松地在页面之间切换

开发测试用例,以测试应用程序的验收标准。使用这种方法可以让你看到最终用户将使用应用程序的条件以及需要达到的功能水平,以便被认为是成功的。

线框

设计布局最有用的工具之一是线框。线框是布局外观的概述。以下图显示了我们的应用程序首页使用线框将看起来是什么样子:

图 2.1 – 封面线框预览

图 2.1 – 封面线框预览

以下图显示了我们的结果页面将如何看起来:

图 2.2 – 结果页面线框预览

图 2.2 – 结果页面线框预览

在下一节中,我们将构建我们应用程序的界面,并确保它看起来与我们在线框图中设计的一样。虽然我们将以相同的方式构建它,但可能会有一些小的差异。这将为下一章中将其全部连接在一起奠定基础。

构建计算器 UI

我们现在将构建计算器应用程序的 UI。计算器有两个主要部分,第一部分是封面页,在启动时加载。一旦用户输入收入并点击计算税,他们将被带到结果页,这是第二部分。在这个页面上,将显示税计算的结果及其分解。自然地,我们将从第一部分,封面页开始,但在那之前,我们将创建我们的项目。按照以下步骤操作:

  1. 打开 Xcode 并选择创建新的 Xcode 项目

图 2.3 – 创建新的 Xcode 项目

图 2.3 – 创建新的 Xcode 项目

  1. 现在,我们将为我们的应用程序选择模板。由于我们正在创建一个 iPhone 应用程序,我们将从顶部选择iOS,然后选择App并点击下一步

图 2.4 – Xcode 项目模板选择

图 2.4 – Xcode 项目模板选择

  1. 我们现在将选择我们的项目选项。在这里,只有两个关键的事情需要选择/设置。确保界面设置为SwiftUI;这将是我们系统使用的 UI。将语言设置为Swift;这是我们应用程序使用的编程语言:

图 2.5 – Xcode 项目选项

图 2.5 – Xcode 项目选项

  1. 一旦你按下下一步,你可以选择在哪里创建你的项目,如下截图所示:

图 2.6 – Xcode 项目保存目录

图 2.6 – Xcode 项目保存目录

  1. 一旦你找到了你想要创建项目的位置,点击右下角的创建。Xcode 会以如下截图所示的方式展示你的项目:

图 2.7 – 新 Xcode 项目概览

图 2.7 – 新 Xcode 项目概览

在下一节中,我们将使用 SwiftUI 实现我们的应用程序的封面页,并在实现过程中进一步了解 Xcode IDE。

封面页

在本节中,我们将实现封面页的 UI。作为一个提醒,以下是它的样子:

图 2.8 – 封面页线框预览

图 2.8 – 封面页线框预览

封面页上有三个主要元素。作为一个小任务,看看你是否能找出它们是什么。如果你不知道确切的 UI 组件名称,不用担心;我们将在接下来的章节中查看这些组件。

文本

文本组件是 SwiftUI 提供的最简单的组件之一。它允许你显示一串字符/数字,这对于标题和信息提供非常有用。我们将使用它为下一个组件TextField提供上下文。如果没有文本组件,用户不知道TextField的用途。以下图显示了首页上的标签,告诉用户以下文本输入字段用于什么:

图 2.9 – 首页标签

图 2.9 – 首页标签

文本字段

文本字段允许用户输入可以由数字和任何字符组成的文本。我们的文本字段将用于输入一个数字,因此它只会接受数字。这是一个我们将配置的功能。一些应用程序在文本字段中放置背景文本,为TextField的用途提供上下文;然而,我们选择使用标签组件来提供上下文,因此不需要这个。以下图显示了用户可以使用它来输入他们的薪水的TextField

图 2.10 – 首页 TextField

图 2.10 – 首页 TextField

按钮

当你想让用户明确触发某些功能时,会使用按钮。在我们的案例中,我们希望用户在准备好计算他们的税计算时按下按钮。自然地,作为开发者,我们必须进行错误检查,以检查按钮是否可以在TextField为空或输入了错误类型的数据时被按下。我们将通过错误消息来处理这个问题,而不是显示税计算。如果你查看以下截图,你会看到这个按钮的样子:

图 2.11 – 首页按钮

图 2.11 – 首页按钮

在下一节中,我们将把之前讨论的元素使用 SwiftUI 添加到我们的应用程序中。

添加首页组件

在本节中,我们将把之前列出的组件添加到我们的首页中。寻找ContentView文件,它通常可以在项目导航器中找到,通常在左侧,如下面的截图所示:

图 2.12 – 项目导航器

图 2.12 – 项目导航器

查看以下代码并将其添加到ContentView文件中:

import SwiftUIstruct ContentView: View
{
    @State private var salary: String = ""
    var body: some View
    {
        VStack
        {
            Text( "Annual Salary" )
            TextField( "Salary", text: $salary )
            Button
            { }
            label:
            { Text("Calculate Tax") }
        }
        .padding( )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

使用前面的代码,我们能够渲染一个TextTextFieldButton。这将形成允许用户输入他们的薪水并点击按钮来计算税项分解的基础。我们使用一个名为salary的变量来存储TextField的数据。让我们看看最终的结果:

图 2.13 – 未添加样式的元素预览

图 2.13 – 未添加样式的元素预览

如您所见,Text组件看起来相当不错,但TextField没有明显的边界。我在里面放了一个占位符,因为没有它,用户甚至不知道TextField在哪里。接下来,Button的样式不正确。让我们通过以下更新的代码修复这两个问题:

VStack{
    Text( "Annual Salary" )
    TextField( "", text: $salary )
        .border( Color.black, width: 1 )
    Button
    { }
    label:
    { Text("Calculate Tax") }
        .buttonStyle( .borderedProminent )
}
.padding( )

使用前面的代码,我们在TextField上添加了一个宽度为1的黑色边框,并移除了占位符文本。接下来,我们为按钮的Text组件添加了按钮样式。我们使用了borderedProminent样式,这正是我们需要的。所有这些更改导致以下结果:

图 2.14 – 更新后的代码预览

图 2.14 – 更新后的代码预览

预览显示我们非常接近了。对于我们插入到TextField中的数据类型,它不需要这么宽。让我们将其缩小。按照以下方式修改TextField

TextField( "", text: $salary )                .frame( width: 200.0 )
                .border( Color.black, width: 1 )

我们为TextField添加了宽度200,使其更适合我们的需求。到目前为止,我们已经以编程方式更改了应用组件的属性。然而,您也可以使用 Xcode UI 调整属性。这样做很简单:通过将鼠标光标悬停在代码中的组件上并单击代码(就像您要编辑它一样)来在代码中选择一个组件。现在,在右侧,将出现一个包括属性检查器的面板。

图 2.15 – 属性检查器

图 2.15 – 属性检查器

如果属性检查器面板没有出现,请转到视图 | 检查器 | 属性

图 2.16 – 手动打开属性检查器

图 2.16 – 手动打开属性检查器

我们几乎完成了;我们只剩下三个 UI 组件。界面目前相当紧凑。让我们将组件展开,使其看起来更美观。添加以下代码以分散组件:

VStack{
    Text( "Annual Salary" )
        .padding(.bottom, 75.0)
    TextField( "", text: $salary )
        .frame( width: 200.0 )
        .border( Color.black, width: 1 )
        .padding( .bottom, 75.0 )
    Button
    { }
    label:
    { Text( "Calculate Tax" ) }
        .buttonStyle( .borderedProminent )
}
.padding( )

在前面的代码中,我们在TextTextField组件的底部添加了填充,以便均匀分布所有三个项目。请随意尝试填充值,以使 UI 看起来符合您的需求。

重要提示

如果你有四个组件,并且想要为前三个添加填充,那么在均匀分布需要填充的组件数量方面,公式将是n - 1n是组件的总数。

我们的前页现在看起来如下:

图 2.17 – 带填充的预览

图 2.17 – 带填充的预览

目前,如果我们启动我们的应用并点击TextField,将出现一个常规键盘,如下面的截图所示:

图 2.18 – 常规键盘预览的首页

图 2.18 – 常规键盘预览的首页

对于需要输入姓名或地址的文本字段来说,这没问题,但这个字段只需要输入薪资,因此只需要数字输入。让我们更新我们的代码,将键盘类型设置为decimalPad

TextField( "", text: $salary )    .frame( width: 200.0 )
    .border( Color.black, width: 1 )
    .padding( .bottom, 75.0 )
    .keyboardType( .decimalPad )

重要提示

有一个 numberPad 选项,但它不允许输入小数,所以我们将继续使用 decimalPad 类型。

如果你现在运行这个应用,它会显示以下内容:

图 2.19 – 前页面小数键盘预览

图 2.19 – 前页面小数键盘预览

键盘类型也可以在 属性检查器 中更改。这是一个快速查看所有可用键盘类型的好地方:

图 2.20 – 键盘类型

图 2.20 – 键盘类型

重要提示

想了解更多关于键盘类型的信息,请查看developer.apple.com/documentation/swiftui/view/keyboardtype(_)

如果键盘在模拟器中没有显示,这是因为你的 Mac 已经有一个键盘,模拟器决定你不需要显示它。但这是可以覆盖的。你可以使用 + K 键盘快捷键来打开,或者 **+ + K 来关闭它,或者前往 I/O | 键盘 | 切换 软件键盘

图 2.21 – 切换软件键盘

图 2.21 – 切换软件键盘

现在模拟器中的软件键盘将出现。这应该只需要做一次。我们已经完成了主页面的设计。目前,这里没有功能,但将在下一章中实现。但我们还没有完成设计。我们现在将实现结果页面的设计。

实现结果页面

在本节中,我们将实现结果页面的 UI。作为提醒,这里是这样看的:

图 2.22 – 结果页面线框预览

图 2.22 – 结果页面线框预览

结果页面有三个主要部分。每个部分由两个或更多组件组成。作为一个小任务,看看你是否能弄清楚它们是什么。如果你不知道确切的 UI 组件名称,不要担心,我们将在下一节中查看它们。

图表摘要部分

图表摘要部分由两个主要组件组成,一个文本组件和一个饼图。SwiftUI 不提供饼图,所以我们将使用外部库。我们将使用由 Andras Samu 创建的 ChartView 库,可以在以下位置找到:github.com/AppPear/ChartView

本节将直观展示税收计算的简单分解。

图 2.23 – 图表摘要线框

图 2.23 – 图表摘要线框

文本摘要部分

在文本摘要部分,有四个文本组件。第一个组件通知用户以下 Text 组件用于显示 税前 薪资标题。第二个组件告诉用户以下文本组件用于显示 税后 薪资标题。这并不包括税收如何分割的分解。这将在下一节中介绍:

图 2.24 – 文本摘要线框

图 2.24 – 文本摘要线框

单个分解部分

单个分解部分显示税和工资的分解情况。有六个组成部分,三个 Text 组成部分和三个 ProgressView 组成部分。每个部分配对在一起形成三个子部分,基本工资国家保险。这种设计简单但可扩展。一旦创建,我给你一个任务,添加税的进一步分解,例如学生贷款和养老金:

图 2.25 – 单个分解线框

图 2.25 – 单个分解线框

在下一节中,我们将添加构成结果页面的元素,然后结束本章。

添加结果页面组件

在本节中,我们将添加之前讨论的组件到我们的结果页面。然而,首先我们必须通过 Andras Samu 集成 ChartView 框架。按照以下步骤操作:

  1. 前往 文件 | 添加包…

图 2.26 – Xcode 添加包…选项

图 2.26 – Xcode 添加包…选项

  1. 使用以下网址搜索 ChartView 框架:github.com/AppPear/ChartView

  2. 选择 2.0.0-beta.2,或您最新的版本。然后,点击右下角的 添加包。在我的例子中,它被灰色显示,因为我已经添加了它:

图 2.27 – 搜索 ChartView 包

图 2.27 – 搜索 ChartView 包

  1. 点击 添加包 将成功将包添加到项目中。

  2. 现在,我们将为结果页面创建一个新的 SwiftUI 视图。在您的 项目导航器 面板内右键点击计算器文件夹,并选择 新建文件…

图 2.28 – 新文件…

图 2.28 – 新文件…

  1. 接下来,我们将选择我们想要添加的文件类型,对我们来说是一个 SwiftUI 视图(选择此选项提供了一个 SwiftUI 模板,这节省了我们每次重新输入 SwiftUI 文件结构的时间和精力),在 用户界面 部分:

图 2.29 – SwiftUI 视图选择

图 2.29 – SwiftUI 视图选择

  1. 最后,我们必须重命名我们的 ResultsView 并点击 创建

图 2.30 – 视图命名

图 2.30 – 视图命名

  1. 打开 ResultsView 文件,通过在文件顶部添加以下代码来导入 SwiftUICharts 框架:

    import SwiftUICharts
    
  2. 接下来,我们需要创建图表本身。这样做非常简单,多亏了 ChartView 库。首先,添加图表将使用的数据。目前,我们将添加一些硬编码的测试数据:

    struct ResultsView: View{    var taxBreakdown: [Double] = [5, 10, 15]    var body: some View    {    }}
    

数组中的值将代表基本工资、税和国家保险。

  1. 接下来,我们将使用 ChartView 实现我们的饼图:

    struct ResultsView: View{    var taxBreakdown: [Double] = [5, 10, 15]    var body: some View    {        PieChart( )            .data( taxBreakdown )            .chartStyle( ChartStyle( backgroundColor: .white,                                     foregroundColor: ColorGradient( .blue, .purple) ) )    }}
    

上述代码将产生以下结果:

图 2.31 – 添加饼图

图 2.31 – 添加饼图

  1. 要查看ResultsView,您需要使用实时预览窗口。默认情况下,它应该出现。如果它没有出现,请使用以下键盘快捷键:+ + Return。现在,Xcode 将看起来像以下截图:

图 2.32 – 实时预览窗口位置

图 2.32 – 实时预览窗口位置

  1. 目前,饼图延伸到边缘。让我们将饼图放入一个带有填充的VStack中。按照以下方式编辑代码:

    struct ResultsView: View{    var taxBreakdown: [Double] = [5, 10, 15]    var body: some View    {        VStack        {            PieChart( )                .data( taxBreakdown )                .chartStyle( ChartStyle( backgroundColor: .white,                                         foregroundColor: ColorGradient( .blue, .purple ) ) )        }.padding( )    }}
    

上述代码中的更改现在将使图表看起来像以下这样:

图 2.33 – 带填充的饼图

图 2.33 – 带填充的饼图

  1. 让我们添加一个显示36Text组件。将以下代码添加到主体中:

    var body: some View{    VStack    {        Text( "Summary" )            .font( .system( size: 36 ) )            .fontWeight( .bold )        PieChart( )            .data( taxBreakdown )            .chartStyle( ChartStyle( backgroundColor: .white,                                     foregroundColor: ColorGradient(.blue, .purple ) ) )    }.padding( )}
    

以下输出显示了我们在饼图上方添加的新摘要文本。

图 2.34 – 摘要标题

图 2.34 – 摘要标题

  1. 在饼图下方,我们将添加四个更多的文本组件,用于税前税后:每个子部分的标题和一个实际的数字。目前,我们将使用硬编码的值。按照以下方式更新代码:

    var body: some View{    VStack    {        Text( "Summary" )            .font( .system( size: 36 ) )            .fontWeight( .bold )        PieChart( )            .data( taxBreakdown )            .chartStyle( ChartStyle( backgroundColor: .white,                                     foregroundColor: ColorGradient(.blue, .purple ) ) )        Text( "Before Tax" )            .font( .system( size: 32 ) )        Text( "£100,000.00" )            .font( .system( size: 32 ) )        Text( "After Tax" )            .font( .system( size: 32 ) )        Text( "£65,000.00" )            .font( .system( size: 32 ) )    }.padding( )}
    

上述代码将显示以下内容:

图 2.35 – 税前和税后文本

图 2.35 – 税前和税后文本

  1. 目前,页面底部的文本组件挤在一起。让我们在每个文本组件的顶部和底部添加填充以分散它们。显然,您可以使用属性检查器来完成此操作,但我们将以编程方式完成:

    Text( "Before Tax" )    .font( .system( size: 32 ) )    .padding(.vertical)Text( "£100,000.00" )    .font( .system( size: 32 ) )    .padding(.vertical)Text( "After Tax" )    .font( .system( size: 32 ) )    .padding(.vertical)Text( "£65,000.00" )    .font( .system( size: 32 ) )    .padding(.vertical)
    

在上述代码中添加填充后,我们将有一个看起来像这样的结果页面:

图 2.36 – 填充后的结果

图 2.36 – 填充后的结果

  1. 接下来,我们需要添加进度条,这些进度条将代表工资、税收和国民保险。我们将使用ProgressView组件与一个Text组件结合来显示税收分解。在之前添加的Text组件之后,添加以下代码:

    Text( "Post Tax Salary" )ProgressView( "", value: 20, total: 100 )Text( "Tax" )ProgressView( "", value: 20, total: 100 )
    

上述代码添加了两个ProgressViewText组件对,显示了税后工资和税收。这将导致以下结果:

图 2.37 – 添加的分解组件

图 2.37 – 添加的分解组件

  1. 你可能已经注意到我们只添加了三个ProgressView组件中的两个。这样做的原因是为了展示一个错误。所以,现在在之前添加的代码之后添加以下代码:

    Text( "National Insurance" )ProgressView( "", value: 20, total: 100 )
    

这将导致以下错误:Group组件,这将使视图将其检测为一个整体。

  1. 我们将按照以下方式将三个ProgressViewText组件分组:

    Group{    Text( "Post Tax Salary" )    ProgressView( "", value: 20, total: 100 )    Text( "Tax" )    ProgressView( "", value: 20, total: 100 )    Text( "National Insurance" )    ProgressView( "", value: 20, total: 100 )}
    

这将解决令人烦恼的 10 限制错误,并导致以下结果:

图 2.38 – 分组的组件

图 2.38 – 分组的组件

现在我们完成这个部分后,来看看整个代码:

import SwiftUIimport SwiftUICharts
struct ResultsView: View
{
    var taxBreakdown: [Double] = [5, 10, 15]
    var body: some View
    {
        VStack
        {
            Text( "Summary" )
                .font( .system( size: 36 ) )
                .fontWeight( .bold )
            PieChart( )
                .data( taxBreakdown )
                .chartStyle( ChartStyle( backgroundColor: .white,
                                         foregroundColor: ColorGradient( .blue, .purple ) ) )
            Text( "Before Tax" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "£100,000.00" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "After Tax" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "£65,000.00" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Group
            {
                Text( "Post Tax Salary" )
                ProgressView( "", value: 20, total: 100 )
                Text( "Tax" )
                ProgressView( "", value: 20, total: 100 )
                Text( "National Insurance" )
                ProgressView( "", value: 20, total: 100 )
            }
        }.padding( )
    }
}
struct ResultsView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ResultsView( )
    }
}

在这个庞大的章节中,我们涵盖了大量的内容。我们首先添加了一个外部框架,我们发现它非常容易集成且功能强大。这个框架使我们能够轻松实现饼图。这非常有用,因为苹果在 Swift 和 SwiftUI 中并没有提供所有基本功能,因此能够添加外部代码库使得开发过程更加轻松。之后,我们实现了饼图、文本摘要和进度视图,以进一步说明税收分解。

摘要

在本章中,我们讨论了我们的税收计算器应用程序的设计。我们研究了线框图,并将每个元素分解为 SwiftUI 组件。然后,我们将 SwiftUI 组件实现以匹配线框图中的设计。我们还审视了构建此应用程序的要求以及设计规范,这些规范探讨了税收计算器应用程序可能具有的功能。然后,我们将这些功能简化为我们应用程序将提供的核心功能。我们在设计规范中进一步推进,以确定我们希望应用程序执行的操作的验收标准。我们还探讨了如何集成外部库以提供额外的功能。

在下一章中,我们将探讨实现税收计算后端功能并将两个视图结合起来的方法。

第三章:iPhone 项目 – 税费计算器功能

在本章中,我们将致力于在我们的第一个项目——税费计算器中实现税费计算和页面导航功能。在前一章中,我们探讨了计算器的设计,并将其分解为两个视图以及所有所需的组件。然后,我们使用 SwiftUI 实现了所有组件。在前一章的结尾,我们实际上只拥有一个花哨的线框图。现在,我们将实现所有功能,以在两个视图之间提供导航,计算税费分解并显示计算结果。

本章将分为以下部分:

  • ContentView 导航到 ResultsView

  • 输入验证

  • 计算税费分解

  • 额外任务

到本章结束时,你将创建一个功能齐全的税费计算器,可以作为基础。当达到本章的结尾时,我将提供练习来在税费计算器中实现更多高级功能。代码由你使用、修改和按需分发。这将很好地过渡到我们的下一个项目,即 iPad 图库应用。

技术要求

本章要求您从 Apple 的 App Store 下载 Xcode 版本 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索 Xcode,选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 14 版本具有以下功能/要求:

  • 包含 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本的设备上的调试

  • 需要 macOS Monterey 12.5 或更高版本的 Mac

  • 有关技术细节的更多信息,请参阅 第一章

本章和前一章使用的代码文件,作为基础,可以在此处找到:github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

从 ContentView 导航到 ResultsView

在本节中,我们将最终实现从 ContentViewResultsView 以及返回的导航系统。

如果你回忆起前一章,当查看 ResultsView 的 UI 时,我们被迫使用实时预览窗口而不是运行应用程序。原因是没有任何从 ContentView 导航到 ResultsView 的功能。我们已添加了计算税费的按钮,但我们需要实现触发导航的按钮代码。

首先,我们需要将 ContentView 中的 VStack 包装在 NavigationView 中。NavigationView 允许我们展示一系列视图,这对于导航非常有用,因为它实际上记录了所有之前视图的历史,从而允许一个简单且可扩展的导航系统:

import SwiftUIstruct ContentView: View
{
    @State private var salary: String = ""
    var body: some View
    {
        NavigationView
        {
            VStack
            {
                Text( "Annual Salary" )
                    .padding(.bottom, 75.0)
                TextField( "", text: $salary )
                    .frame( width: 200.0 )
                    .border( Color.black, width: 1 )
                    .padding( .bottom, 75.0 )
                    .keyboardType( .decimalPad )
                Button
                { }
                label:
                { Text( "Calculate Tax" ) }
                    .buttonStyle( .borderedProminent )
            }
            .padding( )
        }
    }
    func GoToResultsView( )
    {
        ResultsView( )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

我们现在已经实现了NavigationView,这允许我们使用现有的代码和设计进行导航。我们需要一个额外的代码片段 – 一个GoToResultsView函数。一个空版本已经被添加到前面的代码中。我们将在本章的后面使用它。

到目前为止,这不会对我们的应用程序产生任何影响,因为我们需要修改按钮以成为NavigationLink,它基本上是一个高级按钮,允许我们在视图之间导航。幸运的是,它可以以我们希望的方式样式化,但首先让我们实现一个基本的NavigationLink。为此,将ContentView中的按钮代码替换为以下代码:

NavigationLink( destination: ResultsView( ), label:{
    Text( "Calculate Tax" )
} )

让我们分解我们刚刚添加的代码。它接受的第一个参数是应用程序视图系统的目标,即将被推入堆栈的新视图。我们指定了ResultsView,但你也可以轻松指定一个简单的组件,这将创建它自己的视图。下一个参数称为label;这与按钮本身中指定的标签类似。目前,我们有一个Text组件,它看起来像这样:

图 3.1 – 基本 NavigationLink 按钮

图 3.1 – 基本 NavigationLink 按钮

让我们为NavigationLink按钮添加样式并修改代码,使其看起来如下:

NavigationLink( destination: ResultsView( ), label:{
    Text( "Calculate Tax" )
        .bold( )
        .frame( width: 200, height: 50 )
        .background( Color.blue )
        .foregroundColor( Color.white )
        .cornerRadius( 10 )
} )

我们为按钮添加了五个主要样式方面。让我们看看每一个:

  • .bold(): 使文本加粗

  • .frame(width: 200, height: 50): 设置按钮的大小

  • .background(Color.blue): 将背景颜色设置为蓝色

  • .foregroundColor(Color.white): 将文本颜色设置为白色

  • .cornerRadius(10): 使按钮的角落圆润,给人一种更自然、类似 iOS 的感觉

在将这些样式应用到应用程序以及更具体地说,按钮上之后,它将看起来像以下图示:

图 3.2 – 导航 Link 按钮样式

图 3.2 – 导航 Link 按钮样式

现在我们将添加一个导航标题。这提供了一个很好的统一方法来为视图添加标题/头部。这很简单 – 将以下代码添加到VStack的底部,紧接在填充之后:

.padding( ).navigationTitle( "Main Page" )

运行应用程序将显示以下内容:

图 3.3 – 导航标题

图 3.3 – 导航标题

通过点击计算税费按钮导航到ResultsView将显示,左上角的返回按钮文本现在与添加的导航标题相同。这可以在以下屏幕截图中看到:

图 3.4 – 更新后的返回按钮文本

图 3.4 – 更新后的返回按钮文本

现在我们将更新ResultsView以使用导航标题而不是Text组件作为页面的标题。首先,我们需要从ResultsView中移除以下代码中的Text组件:

Text( "Summary" )    .font( .system( size: 36 ) )
    .fontWeight( .bold )

现在,由于ResultsView没有标题,让我们添加导航链接。像之前一样,在VStack之后,紧接在填充之后添加以下代码:

.padding( ).navigationBarTitle( "Summary" )

这个小小的添加将使 ResultsView 发生如下变化:

图 3.5 – ResultsView 导航标题

图 3.5 – ResultsView 导航标题

因此,我们已经添加了大量代码。这是我们继续之前,ContentViewResultsView 代码应该看起来的样子:

ContentView

import SwiftUIstruct ContentView: View
{
    @State private var salary: String = ""
    var body: some View
    {
        NavigationView
        {
            VStack
            {
                Text( "Annual Salary" )
                    .padding(.bottom, 75.0)
                TextField( "", text: $salary )
                    .frame( width: 200.0 )
                    .border( Color.black, width: 1 )
                    .padding( .bottom, 75.0 )
                    .keyboardType( .decimalPad )
                NavigationLink( destination: ResultsView( ), label:
                {
                    Text( "Calculate Tax" )
                        .bold( )
                        .frame( width: 200, height: 50 )
                        .background( Color.blue )
                        .foregroundColor( Color.white )
                        .cornerRadius( 10 )
                } )
            }
            .padding( )
            .navigationTitle( "Main Page" )
        }
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

ResultsView

import SwiftUIimport SwiftUICharts
struct ResultsView: View
{
    var taxBreakdown: [Double] = [5, 10, 15]
    var body: some View
    {
        VStack
        {
            PieChart( )
                .data( taxBreakdown )
                .chartStyle( ChartStyle( backgroundColor: .white,
                                         foregroundColor: ColorGradient( .blue, .purple ) ) )
            Text( "Before Tax" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "£100,000.00" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "After Tax" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "£65,000.00" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Group
            {
                Text( "Post Tax Salary" )
                ProgressView( "", value: 20, total: 100 )
                Text( "Tax" )
                ProgressView( "", value: 20, total: 100 )
                Text( "National Insurance" )
                ProgressView( "", value: 20, total: 100 )
            }
        }
        .padding( )
        .navigationBarTitle( "Summary" )
    }
}
struct ResultsView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ResultsView( )
    }
}

哇,我们做了很多。花点时间给自己鼓掌。所有这些都使我们能够实现一个无缝且熟悉的导航系统。我们学习了如何实现导航系统,以便轻松地导航到 ResultsView 并返回 ContentView

在下一节中,我们将验证薪水以确保我们不会传递任何错误的数据。

验证薪水输入

到目前为止,如果您按下 计算税 按钮,它会将用户从首页带到结果页面。然而,它不考虑输入,所以即使没有薪水,它也会转到下一页。以下验证检查必须完成,才能使其成为一个可接受的价值:

  • 它是否包含一个数值?

  • 该值是否大于 0(这排除了负数)?

你可能想知道为什么我们不能只检查薪水是否大于 0,因为我们选择了十进制键盘。这主要有两个原因:

  • 用户可以以使输入变为 4.5.6.. 的方式插入小数点。

  • 尽管由于键盘是十进制键盘,用户不能直接输入文本,但他们仍然可以从其他应用程序复制并粘贴到我们的计算器中,从而破坏只允许数字的 TextField。您可能认为禁用粘贴是值得的,但保留此功能很重要,因为用户可能确实希望粘贴一个数字而不是手动输入,尤其是如果它不是一个简单的较小数字。

如果没有正确处理,当用户按下 计算税 按钮时,应用程序将崩溃。我们将如何在以下章节中解释实现这些条件的方式。

使用变量跟踪薪水是否有效

我们将在薪水字符串后面添加一个布尔变量来跟踪薪水是否有效;如果是,则显示 ResultsView。只有当按下 计算税 按钮时,才会检查薪水字符串。在主体之前添加以下代码:

@State private var isSalaryValid: Bool = false

isActive 导航链接

现在我们将之前创建的 isSalaryValid 变量与 NavigationLink 通过 isActive 参数链接起来。更新 NavigationLink 代码如下:

NavigationLink( destination: ResultsView( ), isActive: $isSalaryValid, label:

isActive 是一个简单的概念——如果为 true,则立即带您到目标视图,如果为 false,则不发生任何操作。

在以下部分,我们将验证薪水。

验证薪水

首先,我们将重写点击手势并调用一个名为 GoToResultsView 的自定义函数。将以下代码添加到所有文本属性的最后:

.onTapGesture{
    GoToResultsView( )
}

最后,我们必须检查字符串是否有效,然后导航到结果视图。添加以下函数来处理此操作:

func GoToResultsView( ){
    if ( Float( salary ) != nil )
    {
        if ( Float( salary )! > 0 )
        { isSalaryValid = true }
    }
}

让我们展开这个函数来看看它做了什么:

  • if ( Float( salary ) != nil ):检查薪水是否是一个数字。这是通过将字符串转换为 Float 来实现的。如果失败,它将是 nil – 这允许我们检查这一点。这也包括验证空字符串。

  • if ( Float( salary )! > 0 ):检查薪水是否大于零。感叹号表示变量薪水肯定是一个 Float,以避免任何问题,因为我们已经检查过我们可以这样做。

  • isSalaryValid = true:将 isSalaryValid 检查变量设置为 true,这会触发 ResultsView 并加载它。

所有这些添加都应该在以下 ContentView 文件中产生以下代码:

import SwiftUIstruct ContentView: View
{
    @State private var salary: String = ""
    @State private var isSalaryValid: Bool = false
    var body: some View
    {
        NavigationView
        {
            VStack
            {
                Text( "Annual Salary" )
                    .padding(.bottom, 75.0)
                TextField( "", text: $salary )
                    .frame( width: 200.0 )
                    .border( Color.black, width: 1 )
                    .padding( .bottom, 75.0 )
                    .keyboardType( .decimalPad )
                NavigationLink( destination: ResultsView( ), isActive: $isSalaryValid, label:
                {
                    Text( "Calculate Tax" )
                        .bold( )
                        .frame( width: 200, height: 50 )
                        .background( Color.blue )
                        .foregroundColor( Color.white )
                        .cornerRadius( 10 )
                        .onTapGesture
                        {
                            GoToResultsView( )
                        }
                } )
            }
            .padding( )
            .navigationTitle( "Main Page" )
        }
    }
    func GoToResultsView( )
    {
        if ( Float( salary ) != nil )
        {
            if ( Float( salary )! > 0 )
            { isSalaryValid = true }
        }
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

使用前面的代码,我们验证了薪水,并使用一个变量来跟踪验证状态,这使得当它被验证时,我们可以导航到 ResultsView

在下一节中,我们将把薪水在 ContentViewResultsView 之间传递。

将薪水传递到 ResultsView

到目前为止,当我们点击 ResultsView 时,它仍然使用的是虚拟数据。让我们通过传递上一节中验证的薪水来改变这一点。当使用 State 和 Bind 时,这样做很简单。ContentView 中的薪水变量已经是一个状态变量,这意味着当它改变时,与之关联的视图部分也会改变,反之亦然。当我们更改 TextField 中的薪水文本时,我们的应用程序会更新状态变量。我们可以在 ResultsView 中使用一个绑定变量,这允许我们在视图之间传递数据。

首先,在 ResultsView 中,在 taxBreakdown 数组上方添加以下行:

@Binding var salary: String

这只是声明了一个类型为 Stringsalary 变量,其格式与 ContentView 中的 salary 变量相同。@Binding 只是表明它期望传递这个值。

现在,我们必须更新 NavigationLink 以将 salary 变量从 ContentView 传递到 ResultsView,如下所示:

NavigationLink( destination: ResultsView( salary: $salary ), isActive: $isSalaryValid, label:

尽管我们已经完成了所有的绑定,如果我们尝试运行应用程序,将会抛出以下错误:

图 3.6 – 预览错误

图 3.6 – 预览错误

这个错误与预览视图有关,通常它出现在代码的右侧。没有这个,预览无法运行。为了修复这个错误,我们可以为预览提供一个默认的硬编码值。按照以下方式更新代码:

struct ResultsView_Previews: PreviewProvider{
    static var previews: some View
    {
        ResultsView( salary: .constant( "100" ) )
    }
}

现在我们运行应用程序后,我们没有得到任何错误,因为我们解决了缺失参数的错误,可以继续从我们传递的薪水计算税项分解。

注意

如果你想要测试确保变量已经传递,你可以使用断点或 print 语句。我将让你作为一个额外任务来完成这个。

计算税项分解

在上一章中,我们传递了salary变量,但我们仍然需要计算税款。我将按照 2023/2024 年的英国所得税率进行计算,但这可以很容易地适应任何其他税制。以下表格中列出了 2023/2024 年的税率:

收入 税率
低于£12,570 0% 个人免税额
£12,571 至£37,700 20% 基本税率
£37,701 至£150,000 40% 高税率
超过£150,000 45% 附加税率

表 3.1 – 税率区间

由于存在括号且没有单一的固定税率,我们需要进行一些计算来确定应扣除的确切税款。除了所得税外,还有国家保险税。国家保险税并不简单,因为存在不同的类别,但让我们将其保持在简单的 13%,这大致就是其数值。

注意

计算税款肯定还有更多方面,例如养老金缴纳、学生贷款等;然而,我们将就此为止。

税款计算

让我们在ResultsView中实现一个公式来计算所得税。在主体开始处添加以下代码:

var body: some View {let salaryNum = Double( salary )!
var incomeTax: Double = 0
if ( salaryNum > 12570 )
{
    if ( salaryNum > 37700 )
    {
        if ( salaryNum > 150000 )
        {
            incomeTax += ( 37700 - 12571 ) * 0.2
            incomeTax += ( 150000 - 37701 ) * 0.4
            incomeTax += ( salaryNum - 150000 ) * 0.45
        }
        else
        {
            incomeTax += ( 37700 - 12571 ) * 0.2
            incomeTax += ( salaryNum - 37700 ) * 0.4
        }
    }
    else
    { incomeTax += ( salaryNum - 12570 ) * 0.2 }
}
return VStack {
….
}
}

我们检查每个税率区间并相应地计算税款。接下来,我们将计算国家保险。首先,在incomeTax下方添加另一个 double 变量:

let salaryNum = Double( salary )!var incomeTax: Double = 0
var nationalInsuranceTax: Double = 0

现在,我们可以简单地通过将工资乘以0.13来计算13%。在所得税计算下方添加以下代码:

nationalInsuranceTax = salaryNum * 0.13

现在我们已经计算了所有税款,我们可以继续计算税后工资。这很简单——我们只需从工资中减去所得税和国家保险税。在之前的代码下方添加以下代码:

let postTaxSalary = salaryNum - incomeTax - nationalInsuranceTax

让我们将这些值格式化为字符串,这些字符串将在显示税款时使用。在post-tax salary计算下方添加以下代码:

let salaryString = String( format:"£%.2F", salaryNum )let postTaxSalaryString = String( format: "£%.2F", postTaxSalary )
let incomeTaxString = String( format: "£%.2F", incomeTax )
let nationalInsuranceTaxSting = String( format: "£%.2F",
nationalInsuranceTax )

这将从相应的数字中创建一个字符串,四舍五入到两位小数。接下来,将硬编码的饼图数组taxBreakdown移动到格式化字符串下方,并按如下方式更新:

let taxBreakdown: [Double] = [postTaxSalary, incomeTax, nationalInsuranceTax]

我们使用 let 而不是 var,因为它不需要被更改。

现在,我们终于准备好开始更新 UI 中的每个组件。首先,我们将更新Text组件,这些组件显示税前税后金额,如下所示:

Text( "Before Tax" )    .font( .system( size: 32 ) )
    .padding(.vertical)
Text( salaryString )
    .font( .system( size: 32 ) )
    .padding(.vertical)
Text( "After Tax" )
    .font( .system( size: 32 ) )
    .padding(.vertical)
Text( postTaxSalaryString )
    .font( .system( size: 32 ) )
    .padding(.vertical)

最后,更新ProgressView以显示税款和工资的正确百分比和值:

Text( "Post Tax Salary" )ProgressView( postTaxSalaryString, value: postTaxSalary / salaryNum * 100, total: 100 )
Text( "Tax" )
ProgressView( incomeTaxString, value: incomeTax / salaryNum * 100, total: 100 )
Text( "National Insurance" )
ProgressView( nationalInsuranceTaxSting, value: 13, total: 100 )

postTaxSalaryincomeTax变量除以salaryNum并乘以100的原因是为了计算出ProgressView的百分比。在运行之前,更新要返回的VStack。似乎代码中存在一个问题,导致编译器出现冲突。为了解决这个问题,我们需要明确指出要绘制的内容,通过返回它来解决问题:

return VStack

如果我们运行我们的应用程序并插入£100,000 的工资,ResultsView将如下所示:

图 3.7 – 结果视图已更新

图 3.7 – 结果视图已更新

那肯定很累,这么多代码。为了参考,以下是移动到下一步之前 ContentViewResultsView 代码应该看起来像什么:

ContentView:

import SwiftUIstruct ContentView: View
{
    @State private var salary: String = ""
    @State private var isSalaryValid: Bool = false
    var body: some View
    {
        NavigationView
        {
            VStack
            {
                Text( "Annual Salary" )
                    .padding(.bottom, 75.0)
                TextField( "", text: $salary )
                    .frame( width: 200.0 )
                    .border( Color.black, width: 1 )
                    .padding( .bottom, 75.0 )
                    .keyboardType( .decimalPad )
                NavigationLink( destination: ResultsView( salary: $salary ), isActive: $isSalaryValid, label:
                {
                    Text( "Calculate Tax" )
                        .bold( )
                        .frame( width: 200, height: 50 )
                        .background( Color.blue )
                        .foregroundColor( Color.white )
                        .cornerRadius( 10 )
                        .onTapGesture
                        {
                            GoToResultsView( )
                        }
                } )
            }
            .padding( )
            .navigationTitle( "Main Page" )
        }
    }
    func GoToResultsView( )
    {
        if ( nil != Float( salary ) )
        {
            if ( Float( salary )! > 0 )
            { isSalaryValid = true }
        }
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView( )
    }
}

ResultsView:

import SwiftUIimport SwiftUICharts
struct ResultsView: View
{
    @Binding var salary: String
    var body: some View
    {
        let salaryNum = Double( salary )!
        var incomeTax: Double = 0
        var nationalInsuranceTax: Double = 0
        if ( salaryNum > 12570 )
        {
            if ( salaryNum > 37700 )
            {
                if ( salaryNum > 150000 )
                {
                    incomeTax += ( 37700 - 12571 ) * 0.2
                    incomeTax += ( 150000 - 37701 ) * 0.4
                    incomeTax += ( salaryNum - 150000 ) * 0.45
                }
                else
                {
                    incomeTax += ( 37700 - 12571 ) * 0.2
                    incomeTax += ( salaryNum - 37700 ) * 0.4
                }
            }
            else
            { incomeTax += ( salaryNum - 12570 ) * 0.2 }
        }
        nationalInsuranceTax = salaryNum * 0.13
        let postTaxSalary = salaryNum - incomeTax - nationalInsuranceTax
        let salaryString = String( format:"£%.2F", salaryNum )
        let postTaxSalaryString = String( format: "£%.2F", postTaxSalary )
        let incomeTaxString = String( format: "£%.2F", incomeTax )
        let nationalInsuranceTaxSting = String( format: "£%.2F", nationalInsuranceTax )
        let taxBreakdown: [Double] = [postTaxSalary, incomeTax, nationalInsuranceTax]
        return VStack
        {
            PieChart( )
                .data( taxBreakdown )
                .chartStyle( ChartStyle( backgroundColor: .white,
                                         foregroundColor: ColorGradient( .blue, .purple ) ) )
            Text( "Before Tax" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( salaryString )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( "After Tax" )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Text( postTaxSalaryString )
                .font( .system( size: 32 ) )
                .padding(.vertical)
            Group
            {
                Text( "Post Tax Salary" )
                ProgressView( postTaxSalaryString, value: postTaxSalary / salaryNum * 100, total: 100 )
                Text( "Tax" )
                ProgressView( incomeTaxString, value: incomeTax / salaryNum * 100, total: 100 )
                Text( "National Insurance" )
                ProgressView( nationalInsuranceTaxSting, value: 13, total: 100 )
            }
        }
        .padding( )
        .navigationBarTitle( "Summary" )
    }
}
struct ResultsView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ResultsView( salary: .constant( "100" ) )
    }
}

在下一节中,我们将修复 ContentView 中出现的错误;看看您是否能找出错误是什么。

修复 ContentView 绑定错误

ContentView 中存在一个错误。可以通过以下步骤触发:

  1. 通过点击 计算 按钮导航到 ResultsView

  2. 通过点击左上角的返回箭头回到 ContentView

  3. 输入无效数据,可以是以下任何一种:

    1. 删除 TextField 中的所有文本

    2. 空格字符

    3. 任何非数字字符

    4. 两个或多个小数点

完成这些步骤后,将出现以下错误:

图 3.8 – 无效输入错误

图 3.8 – 无效输入错误

这是因为变量绑定到 ResultsView,它使用数值来使用该值。由于输入不再是数值,它崩溃了。问题是 Double( salary ) 返回 nil 并强制使用 ! 来分配,这导致它崩溃。我们将默认将 salaryNum 变量的值设置为 0,如果它不是 nil,则将工资字符串作为 Double 进行转换。按照以下方式更新代码:

var salaryNum: Double = 0if ( nil != Double( salary ) )
{
    salaryNum = Double( salary )!
}

或者,您可以使用合并运算符来减少前面的检查,如下所示:let salaryNum = Double( salary ) ?? 0。有关合并运算符的更多信息,请访问 docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators/#Nil-Coalescing-Operator

首先,我们将变量更改为 var 而不是 let,因为它需要可修改性,以便检查是否等于 nil。然后,我们检查它不是 nil 并相应地分配值。分配 0 不会产生任何影响,因为此时视图不可见。现在,如果您按照前面的步骤运行应用程序,它不会崩溃。

在下一节中,我们将重命名 ContentView

将 ContentView 重命名为 FrontView

在这个小节中,我们将把 ContentView 的名称改为 FrontViewContentView 的默认名称对我们来说没有提供太多信息。我们将将其重命名为 FrontView。您可以将文件重命名并手动将 ContentView 的每个实例更改为 FrontView,这在这样一个规模的应用程序中不会太繁琐。然而,在一个更大、更复杂的应用程序中,这将耗费很多宝贵的开发时间。我们可以使用 Xcode 的重命名工具来帮助我们。

简单地转到应用程序代码中任何对 ContentView 的引用,右键单击它,然后转到 重构 | 重命名…

图 3.9 – 重命名… 按钮

图 3.9 – 重命名… 按钮

在下一个视图中,将名称设置为 FrontView 并按 Enter 键:

图 3.10 – Xcode 重命名工具

图 3.10 – Xcode 重命名工具

重命名文件和更新所有引用就像那样简单。这也适用于变量和函数;请随时使用。

我们现在已经完成了计算器应用程序;在下一节中,您将看到一些额外的任务,供您完成。

额外任务

现在应用程序已经完成,以下是一些任务列表,用于增强您的应用程序并测试您对本章所学概念的理解:

  • 提交无效薪水时显示错误

  • 使用逗号格式化薪水和税收组件

  • 抽象税级和百分比以使应用程序更具动态性

  • 饼图标签

  • 不同的税收类型

  • 公司税

  • 税收继承

  • 为当前薪水计算器提供更多输入,例如养老金贡献和助学贷款

  • 返回按钮样式

备注

如果在任何时候您需要帮助,请随时加入我的 Discord 群组 discord.gg/7e78FxrgqH

我们将总结本章所涵盖的内容。然而,在总结之前,我将提供额外的代码,供您在空闲时自行实现额外任务。

不同的税收选项

要将不同的税收选项添加到您的代码中,您可以通过修改 ResultsView 来包括选择不同税收选项的拾取器或分段控件。以下是如何修改您的代码以添加税收选项拾取器的示例:

import SwiftUIstruct FrontView: View {
    @State private var salary: String = ""
    @State private var isSalaryValid: Bool = false
    @State private var selectedTaxOption: TaxOption = .option1 // Default tax option
    enum TaxOption: String, CaseIterable {
        case option1 = "Income Tax"
        case option2 = "Dividen Tax"
        case option3 = "Corporation Tax"
        // You can add more tax options if needed
    }
    var body: some View {
        NavigationView {
            VStack {
                Text("Annual Salary")
                    .padding(.bottom, 75.0)
                TextField("", text: $salary)
                    .frame(width: 200.0)
                    .border(Color.black, width: 1)
                    .padding(.bottom, 75.0)
                    .keyboardType(.decimalPad)
                Picker("Tax Option", selection: $selectedTaxOption) {
                    ForEach(TaxOption.allCases, id: \.self) { option in
                        Text(option.rawValue)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                .padding(.bottom, 75.0)
                NavigationLink(destination: ResultsView(salary: $salary, taxOption: selectedTaxOption), isActive: $isSalaryValid) {
                    Text("Calculate Tax")
                        .bold()
                        .frame(width: 200, height: 50)
                        .background(Color.blue)
                        .foregroundColor(Color.white)
                        .cornerRadius(10)
                        .onTapGesture {
                            goToResultsView()
                        }
                }
            }
            .padding()
            .navigationTitle("Main Page")
        }
    }
    func goToResultsView() {
        if let salaryFloat = Float(salary), salaryFloat > 0 {
            isSalaryValid = true
        }
    }
}
struct ResultsView: View {
    var salary: String
    var taxOption: FrontView.TaxOption
    var body: some View {
        VStack {
            Text("Results")
                .font(.title)
                .padding()
            Text("Salary: \(salary)")
                .padding()
            Text("Tax Option: \(taxOption.rawValue)")
                .padding()
            // Calculate and display tax results based on the selected tax option
            Spacer()
        }
        .navigationTitle("Results")
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        FrontView()
    }
}

In this example, I’ve added the `TaxOption` enum to define different tax options. The `selectedTaxOption` property is used to store the selected tax option. I’ve added a picker (using a segmented control style) to allow the user to select the tax option.

When the user taps `ResultsView`, where you can calculate and display the tax results based on the selected option.

## Tax geography

To add the ability to calculate tax breakdowns for different geographies, including countries and states, you can modify `ResultsView` and introduce a selection mechanism for geographies. Here’s an example of how you can modify your code to achieve that:

import SwiftUIstruct FrontView: View {

@State private var salary: String = ""

@State private var isSalaryValid: Bool = false

@State private var selectedGeography: Geography = .country("USA") // 默认地理位置

enum Geography {

case country(String)

case state(String, String) // 国家,州

var displayText: String {

switch self {

case .country(let country):

return country

case .state(let country, let state):

return "(state), (country)"

}

}

}

var body: some View {

NavigationView {

VStack {

Text("年度薪水")

.padding(.bottom, 75.0)

TextField("", text: $salary)

.frame(width: 200.0)

.border(Color.black, width: 1)

.padding(.bottom, 75.0)

.keyboardType(.decimalPad)

Picker("地理位置", selection: $selectedGeography) {

Text("USA").tag(Geography.country("USA"))

Text("加利福尼亚,美国").tag(Geography.state("USA", "California"))

// 根据需要添加更多地理位置选项

}

.pickerStyle(SegmentedPickerStyle())

.padding(.bottom, 75.0)

NavigationLink(destination: ResultsView(salary: $salary, geography: selectedGeography), isActive: $isSalaryValid) {

Text("计算税")

.bold()

.frame(width: 200, height: 50)

.background(Color.blue)

.foregroundColor(Color.white)

.cornerRadius(10)

.onTapGesture {

goToResultsView()

}

}

}

.padding()

.navigationTitle("主页")

}

}

func goToResultsView() {

if let salaryFloat = Float(salary), salaryFloat > 0 {

isSalaryValid = true

}

}

}

struct ResultsView: View {

var salary: String

var geography: FrontView.Geography

var body: some View {

VStack {

Text("结果")

.font(.title)

.padding()

Text("工资: (salary)")

.padding()

Text("地区: (geography.displayText)")

.padding()

// 根据所选地区计算并显示税收分解

Spacer()

}

.navigationTitle("结果")

}

}

struct ContentView_Previews: PreviewProvider {

static var previews: some View {

FrontView()

}

}


In this example, I’ve introduced the `Geography` enum, which represents different geographies. It can be either a country or a state within a country. The `selectedGeography` property is used to store the selected geography.

In `FrontView`, I’ve added a picker (using a segmented control style) to allow the user to select the geography. You can add more geography options as needed.

When the user taps `ResultsView`, where you can calculate and display the tax breakdown based on the selected geography.

In `ResultsView`, I’ve added a `displayText` computed property to format and display the selected geography. You can modify the code to calculate and display the tax breakdown based on the selected geography.

Note

This code provides a basic structure for adding tax breakdowns based on geographies. The actual tax calculations and breakdowns would need to be implemented based on the tax laws and rules of the specific countries and states involved.

Here’s the updated code for `ResultsView` with the tax breakdown based on the given logic and the addition of the `Geography` selection:

ResultsView

import SwiftUIimport SwiftUICharts

struct ResultsView: View {

@Binding var salary: String

var geography: FrontView.Geography

var body: some View {

let salaryNum: Double = Double(salary) ?? 0

let taxBreakdown = calculateTaxBreakdown(for: salaryNum, in: geography)

let salaryString = formatCurrency(salaryNum)

let postTaxSalaryString = formatCurrency(taxBreakdown.postTaxSalary)

let incomeTaxString = formatCurrency(taxBreakdown.incomeTax)

let nationalInsuranceTaxString = formatCurrency(taxBreakdown.nationalInsuranceTax)

return VStack {

PieChart()

.data(taxBreakdown.chartData)

.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.blue, .purple)))

Text("税前")

.font(.system(size: 32))

.padding(.vertical)

Text(salaryString)

.font(.system(size: 32))

.padding(.vertical)

Text("税后")

.font(.system(size: 32))

.padding(.vertical)

Text(postTaxSalaryString)

.font(.system(size: 32))

.padding(.vertical)

Group {

Text("税后工资")

ProgressView(postTaxSalaryString, value: taxBreakdown.postTaxPercentage, total: 100)

Text("税收")

ProgressView(incomeTaxString, value: taxBreakdown.incomeTaxPercentage, total: 100)

Text("国家保险")

ProgressView(nationalInsuranceTaxString, value: taxBreakdown.nationalInsurancePercentage, total: 100)

}

}

.padding()

.navigationBarTitle("总结")

}

private func calculateTaxBreakdown(for salary: Double, in geography: FrontView.Geography) -> TaxBreakdown {

var incomeTax: Double = 0

var nationalInsuranceTax: Double = 0

if salary > 12570 {

if salary > 37700 {

if salary > 150000 {

incomeTax += (37700 - 12571) * 0.2

incomeTax += (150000 - 37701) * 0.4

incomeTax += (salary - 150000) * 0.45

} else {

incomeTax += (37700 - 12571) * 0.2

incomeTax += (salary - 37700) * 0.4

}

} else {

incomeTax += (salary - 12570) * 0.2

}

}

nationalInsuranceTax = salary * 0.13

let postTaxSalary = salary - incomeTax - nationalInsuranceTax

let chartData: [(String, Double)] = [("税后工资", postTaxSalary), ("税收", incomeTax), ("国家保险", nationalInsuranceTax)]

let totalSalary = salary + nationalInsuranceTax

let postTaxPercentage = postTaxSalary / totalSalary * 100

let incomeTaxPercentage = incomeTax / totalSalary * 100

let nationalInsurancePercentage = nationalInsuranceTax / totalSalary * 100

return TaxBreakdown(postTaxSalary: postTaxSalary, incomeTax: incomeTax, nationalInsuranceTax: nationalInsuranceTax, chartData: chartData, postTaxPercentage: postTax

Percentage, incomeTaxPercentage: incomeTaxPercentage, nationalInsurancePercentage: nationalInsurancePercentage)

}

private func formatCurrency(_ value: Double) -> String {

let formatter = NumberFormatter()

formatter.numberStyle = .currency

formatter.currencySymbol = "£"

return formatter.string(from: NSNumber(value: value)) ?? ""

}

}

struct TaxBreakdown {

var postTaxSalary: Double

var incomeTax: Double

var nationalInsuranceTax: Double

var chartData: [(String, Double)]

var postTaxPercentage: Double

var incomeTaxPercentage: Double

var nationalInsurancePercentage: Double

}

struct ResultsView_Previews: PreviewProvider {

static var previews: some View {

ResultsView(salary: .constant("100"), geography: .country("USA"))

}

}


在这个更新的代码中,`ResultsView`现在接受一个类型为`FrontView.Geography`的`geography`参数。添加了`calculateTaxBreakdown(for:in:)`函数,用于根据工资和选定的地理位置计算税收分解。

税收分解存储在`TaxBreakdown`结构体中,该结构体包括税后工资、所得税、国家保险税、图表数据和每个税类百分比。

`formatCurrency(_:)`函数用于使用英镑符号(£)格式化货币值。

计算出的税收分解用于填充`PieChart`和`ProgressView`组件,以显示税收分解和百分比。

请注意,提供的代码中的税收计算逻辑基于初始逻辑,可能不准确或不适用于现实世界的税收计算。它作为将税收分解计算集成到您的 SwiftUI 代码中的示例结构。

# 摘要

在本章中,我们实现了计算器的所有功能。我们将上一章中实现的所有 UI 组件链接起来。首先,我们提供了一种导航到`ResultsView`和从`ResultsView`返回的方法。然后,我们检查工资输入以确保它大于零且不包含任何无效字符。一旦验证通过,我们就将从`ContentView`传递工资到`ResultsView`。使用工资,我们在`ResultsView`中计算税收分解,修复了一个令人烦恼的错误,并将`ContentView`重命名为`FrontView`。最后,我们还为我们的税收计算器应用实现了一些额外的任务。

在下一章中,我们将开始我们的下一个应用,这将是一个适用于 iPad 的相册。我们将利用已经学到的许多技能,所以请随时花点时间回顾一下你还没有完全理解的内容。




# 第四章:iPad 项目 – 照片库概述

在前两章中,我们为 iPhone 创建了一个税务计算器应用程序。我们从零开始实现它,并查看技术要求、设计规范、线框和代码实现。我们将在本章和下一章中使用这些新获得的知识,但请放心,我们将回顾所有必要的方面,以防你直接跳到本章。

在本章中,我们将着手设计我们的第二个项目,一个用于展示大屏幕的 iPad 照片库应用程序。我们将评估设计此类应用程序的要求,并讨论设计规范,以便我们更好地了解所需内容以及它们如何组合在一起。然后我们将开始编码我们的应用程序,构建 UI,这将在下一章中与画廊的增强视图连接。本项目将涵盖 SwiftUI 组件的基础。我们将在以下章节中讨论所有这些内容:

+   要求

+   设计规范

+   构建画廊 UI

到本章结束时,你将更好地了解我们照片库应用程序中所需的功能/组件,我们将如何利用 iPad 的更大屏幕(与 iPhone 相比),以及我们应用程序的设计。到本章结束时,我们将实现第一页的线框,这将为下一章奠定基础。

# 技术要求

本章要求您从 Apple 的 App Store 下载 Xcode 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索 Xcode 并下载最新版本。打开 Xcode 并按照安装说明进行操作。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 14 版本具有以下功能/要求:

+   包含 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK。

+   支持在 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本中进行设备调试。

+   需要 macOS Monterey 12.5 或更高版本的 Mac。

+   如需有关技术细节的更多信息,请参阅*第一章*。

本章的代码文件可以在以下位置找到:[`github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects`](https://github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects)。

在下一节中,我们将阐明我们应用程序设计规范的具体内容,并查看应用程序外观的草图。

# 理解设计规范

在本节中,我们将查看我们画廊应用程序的设计规范。本节描述了我们将要在我们的画廊应用程序中实现的功能。确定所需功能的最有效方法是站在用户的角度,确定他们将如何使用该应用程序,并将其分解为单独的步骤。

我们希望展示的应用程序功能如下:

+   **高亮视图**:这是用户将看到的主要视图,展示了所有图像。

+   **增强视图**:这显示了图像的较大版本,以及如描述和日期等信息。

+   **全屏模式**:在全屏模式下查看图像,没有任何额外信息。

+   **全屏点击查看更多信息**:在全屏模式下单次点击将显示照片的标题。

+   **集合**:不同的图像集合或相册:

    +   **侧面板**:以水平模式显示所有集合名称。

+   **删除和重命名**:这允许用户删除图像并重命名它。

+   **编辑**:图像编辑功能:

    +   使用 Apple Pencil 进行绘图。

+   **分享**:分享图像的能力。

+   **显示模式切换**:更改在突出显示页面上的图像显示方式,作为列表或使用瓷砖。

+   **注释和高亮**:允许用户注释和高亮图像的部分。

+   图像和信息来自外部源,如本地数据库或在线。

现在我们已经列出了我们希望拥有的理想功能,对我们来说,确定哪些功能是至关重要的非常重要。为此,我们必须了解我们产品的目的。对我来说,创建这个照片画廊应用的目的是为了展示一些图像并提供一个增强模式,以提供更多信息。我知道并非所有功能都是必需的,如果某些功能被省略,并作为额外的任务分配给你作为开发者去完成,这可能是有用的。这些额外任务是可选的,并且只有在完成此项目后才能执行。考虑到这一点,以下是我们将实现的核心功能:

+   高亮视图

+   增强视图

+   分享

在完成本章和下一章之后,其余的功能将留给你作为练习。下一节将涵盖我们应用的接受标准。

## 接受标准

我们将讨论我们希望在最终产品中看到的接受标准,这些标准将在下一章的结尾完成。如果可能的话,我们应该尝试使它们可衡量。让我们现在就做这件事:

+   在画廊视图中使用小图像,提供整个画廊的概览。

+   一个可滚动的视图,用于浏览所有我们的图像。

+   一种增强视图,用于显示额外信息,从而允许可选参数。

+   在分享图像时应该出现一个本机共享菜单。

+   从高亮视图导航到增强视图。

+   从增强视图导航到全屏视图。

开发测试用例,以测试应用的接受标准。使用这种方法,我们可以看到最终用户将使用应用的条件以及需要达到的成功水平。

## 线框图

设计布局最有用的工具之一是线框设计。线框是布局的概述。以下图像显示了我们的应用突出显示页面将如何使用线框来呈现:

![图 4.1 – 竖屏模式的突出显示视图线框](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.01_B18783.jpg)

图 4.1 – 竖屏模式的突出显示视图线框

在 iPad 应用程序中,拥有横屏模式非常重要。以下图像显示了我们的突出显示视图在横屏模式下的线框:

![图 4.2 – 横屏模式的突出显示视图线框](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.02_B18783.jpg)

图 4.2 – 横屏模式的突出显示视图线框

以下两幅图像显示了增强视图的横屏和竖屏线框:

![图 4.3 – 竖屏模式的增强视图线框](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.03_B18783.jpg)

图 4.3 – 竖屏模式的增强视图线框

![图 4.4 – 横屏模式的增强视图线框](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.04_B18783.jpg)

图 4.4 – 横屏模式的增强视图线框

在下一节中,我们将构建我们应用程序的界面,并确保它看起来与我们设计在线框图中的样子一致。虽然我们将以相同的方式构建它,但可能会有一些小的差异。这将为下一章中将其全部连接起来奠定基础。

# 构建画廊 UI

我们现在将构建画廊应用程序的 UI。画廊有三个主要部分,首先是突出显示页面,它在启动时加载并显示所有图片。一旦用户点击图片,用户将被带到增强页面,这是第二部分。在这个页面上,显示图片的大版本以及更多信息。最后,最后一部分是全屏模式,它简单地以全屏显示所选图片。自然地,我们将从第一部分,突出显示页面开始,但在那之前,我们将创建我们的项目。按照以下步骤操作:

1.  打开 Xcode 并选择**创建一个新的** **Xcode 项目**:

![图 4.5 – 创建新的 Xcode 项目](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.05_B18783.jpg)

图 4.5 – 创建新的 Xcode 项目

1.  现在,我们将选择我们应用程序的模板。由于我们正在创建 iPad 应用程序,我们将从顶部选择**iOS**,然后选择**App**,点击**下一步**:

![图 4.6 – Xcode 项目模板选择](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.06_B18783.jpg)

图 4.6 – Xcode 项目模板选择

1.  现在,我们将选择我们项目的选项。只有两个关键的事情需要设置。确保**界面**设置为**SwiftUI**。这将是我们系统将使用的 UI。将**语言**设置为**Swift**;这是我们应用程序使用的编程语言:

![图 4.7 – Xcode 项目选项](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.07_B18783.jpg)

图 4.7 – Xcode 项目选项

1.  一旦点击**下一步**,您可以选择在哪里创建您的项目:

![图 4.8 – Xcode 项目保存目录](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.08_B18783.jpg)

图 4.8 – Xcode 项目保存目录

1.  一旦你选择了位置,点击右下角的 **创建**。Xcode 以其全部荣耀显示你的项目:

![图 4.9 – 新 Xcode 项目的概述](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.09_B18783.jpg)

图 4.9 – 新 Xcode 项目的概述

在下一节中,我们将设置我们的项目为仅限 iPad。

## 设置项目为 iPad

你可能已经注意到在预览中,它目前设置为 iPhone 项目,或者更准确地说,是 iOS 项目。这意味着它在 iPhone 和 iPad 上运行,并且默认预览显示 iPhone 视图。我们希望我们的项目仅限 iPad,因此按照以下步骤将我们的项目设置为仅限 iPad:

1.  让我们立即更改它。在 **项目导航器** 中选择项目:

![图 4.10 – 项目在项目导航器中](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.10_B18783.jpg)

图 4.10 – 项目在项目导航器中

1.  现在从 **TARGETS** 部分选择 **Photo Gallery**:

![图 4.11 – 目标选择](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.11_B18783.jpg)

图 4.11 – 目标选择

1.  下一步是移除 iPhone 和 Mac 目的地。这样做很简单;选择每个目的地并按减号按钮:

![图 4.12 – 移除目的地](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.12_B18783.jpg)

图 4.12 – 移除目的地

1.  在从目的地中移除 iPhone 和 Mac 之后,它应该看起来如下:

![图 4.13 – 支持的目的地](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.13_B18783.jpg)

图 4.13 – 支持的目的地

在下一节中,我们将使用 SwiftUI 实现我们应用程序的高亮页面。

## 高亮页面

在本节中,我们将实现高亮页面的 UI。作为一个提醒,它将看起来像这样:

![图 4.14 – 竖屏模式下高亮视图的线框](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.14_B18783.jpg)

图 4.14 – 竖屏模式下高亮视图的线框

高亮页面上有一个主要元素。作为一个小任务,看看你是否能找出它是什么。如果你不知道确切的 UI 组件名称,不用担心;我们将在下一节中查看它。

### 图像组件

图像组件是 SwiftUI 提供的核心组件之一。它允许你显示图像,可以用来提供某物的视觉表示或装饰一段文本。我们将使用它来显示我们画廊中的所有图像作为高亮页面上的高亮。以下图像显示了高亮页面上的图像。正如你从 *图 4**.14* 记忆的那样,高亮页面充满了小图像。这些图像将是可点击的,将用户带到增强视图。

![图 4.15 – 高亮视图中的图像组件](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.15_B18783.jpg)

图 4.15 – 高亮视图中的图像组件

在下一节中,我们将使用 SwiftUI 将我们之前讨论过的图像组件添加到我们的应用程序中。

## 添加高亮页面组件

在本节中,我们将向我们的高亮页面添加图像组件,该页面目前命名为 **ContentView**:

1.  首先,让我们重命名 `HighlightView`。这样做很简单:打开 **ContentView**,在代码中右键单击任何对 **ContentView** 的引用,然后转到 **重构 | 重命名**。我将使用第一个引用,如下面的截图所示:

![图 4.16 – 重命名按钮](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.16_B18783.jpg)

图 4.16 – 重命名按钮

1.  接下来,将显示以下屏幕:

![图 4.17 – 重命名屏幕](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.17_B18783.jpg)

图 4.17 – 重命名屏幕

1.  将名称从 `HighlightView` 改变,如您所见,所有对 **ContentView** 的其他引用都会自动更新。最后,点击右上角的 **重命名** 按钮:

![图 4.18 – 更改视图名称](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.18_B18783.jpg)

图 4.18 – 更改视图名称

1.  我们已经将视图重命名,包括文件名,如下所示,在**项目导航器**中:

![图 4.19 – 项目导航器中更新的文件名](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.19_B18783.jpg)

图 4.19 – 项目导航器中更新的文件名

1.  有一个额外的步骤,这是可选的。那就是重命名 `ContentView_Previews` 结构。虽然这不是关键,但我强烈建议重命名它,以保持所有名称引用的一致性。使用之前的过程,重命名 `ContentView_Previews` 为 `HighlightView_Previews`。该结构的位置在 `HighlightView` 文件的底部(之前命名为 `ContentView`):

![图 4.20 – 重命名预览结构](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.20_B18783.jpg)

图 4.20 – 重命名预览结构

在下一节中,我们将查看列布局并确定哪一种最适合我们的应用程序。

### 列布局

在我们开始编写高亮页面代码之前,让我们讨论三种主要的列布局类型:

+   **灵活布局**:允许你指定列数,并根据屏幕大小动态地分配空间。

+   **固定布局**:创建具有固定尺寸的列,这比较受限。

+   **自适应列**:正如其名所示,它们会根据内容的大小进行调整。你设置最小尺寸,自适应系统会根据屏幕大小计算单行可以放置多少项。自然地,当 iPad 处于横屏模式时,显示的项数会比竖屏模式多。

每种布局都有其用例,但为了真正展示我们的照片库,我们将使用自适应列系统。在下一节中,我们将以编程方式实现高亮视图。

## 实现高亮视图

由于我们创建了一个新的项目,编码标准并不符合我的个人偏好。因此,首先,我将更改标准。请随意花几分钟时间做同样的事情:

1.  首先,我们需要将图片添加到我们的项目中。这样做很简单。从 **项目导航器** 中选择 **Assets**:

![图 4.21 – 项目导航器中的资源位置](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.21_B18783.jpg)

图 4.21 – 项目导航器中的资源位置

1.  现在**Assets**视图将出现。导入图像/资产可以通过以下两种方式之一完成:

    1.  将文件拖放到**Assets**部分:

![图 4.22 – 拖放 Assets](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.22_B18783.jpg)

图 4.22 – 拖放 Assets

1.  右键点击**Assets**部分并选择**导入**:

![图 4.23 – 导入按钮](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.23_B18783.jpg)

图 4.23 – 导入按钮

1.  一旦导入资产,**Assets**视图将如下所示:

![图 4.24 – 导入的 Asset(s)](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.24_B18783.jpg)

图 4.24 – 导入的 Asset(s)

小贴士

我只导入了一张图像,并将多次使用它,但您可以使用不同的图像。

接下来,我们将创建一个字符串数组。每个字符串将是我们图库中图像的名称。如前所述,我将使用相同的图像,因此所有字符串都将相同。根据您的需求进行更新。

注意

我正在使用缩略图作为我的以开发者为中心的播客 FireDEV。请随意使用,并每周四通过以下链接收听我的播客:

Spotify: [`open.spotify.com/show/387RiHksQE33KYHTitFXhg`](https://open.spotify.com/show/387RiHksQE33KYHTitFXhg)

Apple Podcasts: [`podcasts.apple.com/us/podcast/firedev-fireside-chat-with-industry-professionals/id1602599831`](https://podcasts.apple.com/us/podcast/firedev-fireside-chat-with-industry-professionals/id1602599831)

Google Podcasts: [`podcasts.google.com/feed/aHR0cHM6Ly9hbmNob3IuZm0vcy83Yjg2YTNiNC9wb2RjYXN0L3Jzcw`](https://podcasts.google.com/feed/aHR0cHM6Ly9hbmNob3IuZm0vcy83Yjg2YTNiNC9wb2RjYXN0L3Jzcw).

1.  在`HighlightView`结构体的开头添加以下代码,在主体之前:

    ```swift
    private let images: [String] =[    "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV","FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV","FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV","FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV"]
    ```

我们添加的代码只是一个与图像文件对应的字符串数组。在 Swift 中指定资源时,不需要包含文件类型,例如,`.png`。

1.  接下来,我们将创建一个变量,用于设置列的排列方式。之前,我们讨论了不同的列布局,并选择了自适应列。在之前的代码下方添加以下代码:

    ```swift
    private let adaptiveColumns =[    GridItem( .adaptive( minimum: 300 ) )]
    ```

此代码将网格项设置为自适应,最小尺寸为 300 像素,这意味着无论 iPad 的大小如何,图像都易于查看,不会太小或太大。

1.  现在,我们将显示图像。在主体内部添加以下代码,我们将介绍每一部分的作用:

    ```swift
    ScrollView{    LazyVGrid( columns: adaptiveColumns, spacing: 20 )    {        ForEach( images.indices )        { i in            Image( images[i] )                .resizable( )                .scaledToFill( )                .frame( width: 300, height: 300 )        }    }}
    ```

在我们运行它之前,让我们看看代码的每一部分都做了什么:

+   `LazyVGrid( columns: adaptiveColumns, spacing: 20 )`: 创建一个具有自适应列且每项之间间隔为 20 的懒加载垂直网格。

+   `ForEach(` `images.indices )`

`{ i in`: 遍历每个图像文件名,`i`代表索引号,从 0 开始,稍后将用于引用图像。

+   `Image(` `images[i] )`

`.``resizable( )`

`.``scaledToFill( )`

`.frame(width: 300, height: 300)`: 使用`images`数组中的文件名创建一个图像。`resizable`允许修改图像的大小。在应用任何缩放或尺寸之前设置`resizable()`修饰符很重要,否则它将不起作用。`scaledToFill`用于保持图像的宽高比并将其缩放以填充其父元素。最后,使用`frame`参数设置图像的宽度和高度。

1.  运行应用程序将产生以下结果:

![图 4.25 – 高亮视图预览](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.25_B18783.jpg)

图 4.25 – 高亮视图预览

1.  现在让我们旋转我们的设备/模拟器,看看它看起来如何:

![图 4.26 – 横向布局预览](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.26_B18783.jpg)

图 4.26 – 横向布局预览

1.  通过按*⌘* *+ 左/右箭头键*或通过按模拟器顶部的旋转按钮,如图下所示,可以实现模拟器的旋转:

![图 4.27 – 旋转按钮](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/ele-swiftui-skl-bd-proj/img/Figure_4.27_B18783.jpg)

图 4.27 – 旋转按钮

如我们所见,我们画廊应用的高亮视图看起来非常好。它在纵向和横向模式下都能完美工作。使用网格而不是自定义实现的美妙之处在于它处理了旋转,这节省了时间和精力,同时保持了与用户习惯的设计标准一致。

这就是本章的最后一段代码。以下是`HighlightView`文件的全部代码:

```swift
import SwiftUIstruct HighlightView: View
{
    private let images: [String] =
    [
        "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV"
    ]
    private let adaptiveColumns =
    [
        GridItem( .adaptive( minimum: 300 ) )
    ]
    var body: some View
    {
        ScrollView
        {
            LazyVGrid( columns: adaptiveColumns, spacing: 20 )
            {
                ForEach( images.indices )
                { i in
                    Image( images[i] )
                        .resizable( )
                        .scaledToFill( )
                        .frame( width: 300, height: 300 )
                }
            }
        }
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        HighlightView( )
    }
}

我们首先研究了我们的照片画廊应用的设计。由于它是在 iPad 上,从设计角度来看它是独特的。我们研究了高亮视图、增强视图和全屏模式的线框图。我们使用 SwiftUI 实现了高亮视图,并确保它正确旋转。

摘要

在本章中,我们讨论了 iPad 上我们的照片画廊应用的设计。我们研究了线框图,并将每个元素分解为 SwiftUI 组件。然后我们实现了 SwiftUI 组件以匹配高亮视图线框图的设计。我们还研究了构建此应用程序的要求,以及照片画廊应用可能具有的设计规范。然后我们将其简化为应用的核心功能。我们通过验收标准扩展了设计规范,以展示我们希望我们的应用能够做什么。

在我们下一章中,我们将探讨基于本章讨论的线框图实现增强视图,并将其与高亮视图连接起来。

第五章:iPad 项目 – 照片库增强视图

在本章中,我们将致力于在我们的照片库项目中实现增强视图和页面导航功能。在前一章中,我们探讨了照片库的设计及其独特的工作方式,因为它是为大设备开发的。然后,我们将其分解为两个视图和全屏模式。之后,我们实现了第一个视图,即突出视图。为此,我们确定了所需的组件。然后,我们使用 SwiftUI 实现了所有组件。在前一章的结尾,我们只有一个突出视图的精美线框,没有与其他视图的连接。现在,我们将创建增强视图并实现所有功能,以在视图之间提供导航并在突出视图和增强视图之间发送图像元数据。

本章将分为以下部分:

  • EnhancedView 设计概述

  • 更新 HighlightView

  • 测试 EnhancedView

  • 额外任务

到本章结束时,你将创建一个功能齐全的照片库,利用 iPad 的巨大屏幕空间,你可以根据自己的需求进行修改、调整和使用。当本章结束时,我将给你一些练习,以将更高级的功能实现到照片库中。这将很好地过渡到我们的下一个项目,即 Mac App Store。

技术要求

本章要求您从 Apple 的 App Store 下载 Xcode 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索 Xcode,然后选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 14 版本具有以下功能/要求:

  • 包含适用于 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK。

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本在设备上的调试。

  • 需要 Mac 运行 macOS Monterey 12.5 或更高版本。

如需有关技术细节的更多信息,请参阅第一章

本章的代码文件可以在此处找到:

github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects/tree/main/Code/Chapter%205%20-%20iPad%20Project%20-%20Photo%20Gallery%20Enhanced%20View

在下一节中,我们将探讨 EnhancedView。我们将将其分解为我们可以实现的部分。

增强视图设计概述

在本节中,我们将实现 EnhancedView。如果你还记得,在前一章中,我们讨论了 EnhancedView 的设计。以下图示显示了 EnhancedView 的竖屏和横屏模式,以供提醒:

图 5.1 – 竖屏模式下的增强视图线框预览

图 5.1 – EnhancedView 线框预览(纵向模式)

图 5.2 – 横屏模式下的 EnhancedView 线框预览

图 5.2 – 横屏模式下的 EnhancedView 线框预览

在我们编写应用程序代码之前,我们将把 EnhancedView 分解成构成它的元素。作为一个小任务,看看你是否能弄清楚它们是什么。如果你不知道确切的 UI 组件名称,不要担心;我们将在以下几节中查看这些组件。

重要提示

这些组件在纵向和横向方向上都是相同的。

文本组件

Text 组件是 SwiftUI 提供的最简单的组件之一。它允许你显示一串字符/数字,这对于标题和信息提供非常有用。我们将用它三次来实现以下功能:

  • 图片标题

  • 日期

  • 图片描述

以下图显示了 Text 组件:

图 5.3 – 图片标题文本组件

图 5.3 – 图片标题文本组件

图 5.4 – 图像日期文本组件

图 5.4 – 图像日期文本组件

图 5.5 – 图片描述文本组件

图 5.5 – 图片描述文本组件

图像组件

Image 组件是 SwiftUI 提供的核心组件之一。它允许你显示一个图像,可以用来提供视觉表示或文本的主体。我们将使用它来显示从 HighlightView 中选择的图像的大版本。以下图显示了 EnhancedView 中的图像:

图 5.6 – 图像组件

图 5.6 – 图像组件

在下一节中,我们将创建 EnhancedView 并使用我们在应用中使用 SwiftUI 讨论过的组件来实现它。

添加 EnhancedView 组件

在本节中,我们将添加之前讨论过的组件来创建我们的 EnhancedView。然而,我们首先需要创建 EnhancedView 文件。这样做很简单;按照以下步骤操作:

  1. 现在我们将创建一个新的 SwiftUI 视图用于结果页面。在您的 项目导航器 面板内的相册文件夹上右键单击,并选择 新建文件...

图 5.7 – 选择新建文件

图 5.7 – 选择新建文件

  1. 接下来,我们将选择要添加的文件类型,它是一个 SwiftUI 视图(选择此选项将提供一个 SwiftUI 模板,这可以节省我们每次重新输入 SwiftUI 文件结构的麻烦)在 用户界面 部分:

图 5.8 – SwiftUI 视图选择

图 5.8 – SwiftUI 视图选择

  1. 最后,我们必须重命名我们的 EnhancedView 并按 创建

图 5.9 – 命名我们的视图

图 5.9 – 命名我们的视图

到目前为止,我们已经查看过线框及其组成的组件。最后,我们创建了我们的 EnhancedView 文件。

更新 HighlightView

我首先要做的是更新代码以符合我的编码标准;您可以自由地这样做。现在,我们将添加五个状态变量。一个将用于跟踪图像是否被点击,其他四个将用于从HighlightView传递数据到EnhancedView。像这样在自适应列之后添加代码:

private let adaptiveColumns =[
    GridItem( .adaptive( minimum: 300 ) )
]
@State private var isClicked: Bool = false
@State private var imageFile: String = ""
@State private var imageName: String = ""
@State private var imageDate: String = ""
@State private var imageDescription: String = ""

现在,更新主体以匹配以下代码:

var body: some View{
    NavigationView
    {
        ScrollView
        {
            LazyVGrid( columns: adaptiveColumns, spacing: 20 )
            {
                ForEach( images.indices )
                { i in
                    NavigationLink( destination: EnhancedView( imageFile: $imageFile, imageName: $imageName, imageDate: $imageDate, imageDescription: $imageDescription ), isActive: $isClicked, label:
                        {
                        Image( images[i] )
                            .resizable( )
                            .scaledToFill( )
                            .frame( width: 300, height: 300 )
                            .onTapGesture {
                                imageFile = images[i]
                                imageName = "FireDEV Podcast"
                                imageDate = "22/09/2022"
                                imageDescription = "Aspiring entrepreneurs and industry professionals alike can learn a lot from a fireside chat with interesting people in the industry. From small indie developers to CEOs of major companies, these chats provide an opportunity to gain insight into the unique stories of success that have led these individuals to their current positions. Through conversations about their experiences and challenges, we can gain valuable knowledge about their successes, failures, and the strategies they used to reach their goals. We can also gain insight into their motivations and the values that drive their decisions. By engaging in a fireside chat with these industry leaders, we can gain a better understanding of the industry and the people within it, and gain valuable knowledge that can help us to reach our own goals."
                                isClicked = true
                            }
                    } )
                }
            }
        }
    }
    .navigationViewStyle( StackNavigationViewStyle( ) )
}

我们刚刚添加了很多代码,所以让我们逐一解释:

  • NavigationView {: 我们实现了一个导航系统,允许我们导航到EnhancedView并返回。仅使用此代码为NavigationView将导致分割系统,因此最后我们添加了后续代码以移除分割视图模式。

  • .navigationViewStyle( StackNavigationViewStyle( ) ): 与新的NavigationView结合使用,我们将每个图像包裹在一个NavigationLink中,这使得它可以点击,从而我们可以导航到另一个视图。

  • NavigationLink( destination: EnhancedView( imageFile: $imageFile, imageName: $imageName, imageDate: $imageDate, imageDescription: $imageDescription ), isActive: $isClicked, label: 在NavigationLink中,我们有几个参数。让我们逐一分析每个参数的作用:

    • destination: EnhancedView(: 设置点击图像时导航到的视图。

    • imageFile: $imageFile: 将之前创建的imageFile状态变量传递给EnhancedView。这个变量是图像的路径/文件名。这将作为字符串存储。

    • imageName: $imageName: 将imageName状态变量传递给EnhancedView。这个变量是图像的名称/标题。

    • imageDate: $imageDate: 将之前创建的imageDate状态变量传递给EnhancedView。这个变量是图像的日期。这可能是指创建日期、编辑日期或其他相关日期。

    • imageDescription: $imageDescription: 将imageDescription状态变量传递给EnhancedView。这个变量是图像的描述。

  • isActive: $isClicked: 跟踪图像是否被点击。如果是,它将导航到EnhancedView

  • label: 虽然我们没有使用任何形式的文本作为按钮,但图像将被用作标签,它将作为导航的可点击标签/图像。

下一步是添加图像的可点击功能,允许用户从HighlightView导航到EnhancedView。更新NavigationLink中的图像代码如下:

Image( images[i] )    .resizable( )
    .scaledToFill( )
    .frame( width: 300, height: 300 )
    .onTapGesture {
        imageFile = images[i]
        imageName = "FireDEV Podcast"
        imageDate = "22/09/2022"
        imageDescription = "Aspiring entrepreneurs and industry professionals alike can learn a lot from a fireside chat with interesting people in the industry. From small indie developers to CEOs of major companies, these chats provide an opportunity to gain insight into the unique stories of success that have led these individuals to their current positions. Through conversations about their experiences and challenges, we can gain valuable knowledge about their successes, failures, and the strategies they used to reach their goals. We can also gain insight into their motivations and the values that drive their decisions. By engaging in a fireside chat with these industry leaders, we can gain a better understanding of the industry and the people within it, and gain valuable knowledge that can help us to reach our own goals."
        isClicked = true
    }

我们添加了一个onTapGesture函数。这个函数的目的是将图像的元数据分配给之前创建的@State变量。所有元数据变量都是硬编码的,除了imageFile,它使用一个数组。您可以自由扩展当前数组,使其成为一个多维数据容器,以存储每个图像的独特元数据。最后,我们将isClicked设置为true;这告诉视图在点击NavigationLink时导航到指定的视图,即EnhancedView

所有这些更改将导致以下HighlightView的代码:

import SwiftUIstruct HighlightView: View
{
    private let images: [String] =
    [
        "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV", "FireDEV"
    ]
    private let adaptiveColumns =
    [
        GridItem( .adaptive( minimum: 300 ) )
    ]
    @State private var isClicked: Bool = false
    @State private var imageFile: String = ""
    @State private var imageName: String = ""
    @State private var imageDate: String = ""
    @State private var imageDescription: String = ""
    var body: some View
    {
        NavigationView
        {
            ScrollView
            {
                LazyVGrid( columns: adaptiveColumns, spacing: 20 )
                {
                    ForEach( images.indices )
                    { i in
                        NavigationLink( destination: EnhancedView( imageFile: $imageFile, imageName: $imageName, imageDate: $imageDate, imageDescription: $imageDescription ), isActive: $isClicked, label:
                            {
                            Image( images[i] )
                                .resizable( )
                                .scaledToFill( )
                                .frame( width: 300, height: 300 )
                                .onTapGesture {
                                    imageFile = images[i]
                                    imageName = "FireDEV Podcast"
                                    imageDate = "22/09/2022"
                                    imageDescription = "Aspiring entrepreneurs and industry professionals alike can learn a lot from a fireside chat with interesting people in the industry. From small indie developers to CEOs of major companies, these chats provide an opportunity to gain insight into the unique stories of success that have led these individuals to their current positions. Through conversations about their experiences and challenges, we can gain valuable knowledge about their successes, failures, and the strategies they used to reach their goals. We can also gain insight into their motivations and the values that drive their decisions. By engaging in a fireside chat with these industry leaders, we can gain a better understanding of the industry and the people within it, and gain valuable knowledge that can help us to reach our own goals."
                                    isClicked = true
                                }
                        } )
                    }
                }
            }
        }
        .navigationViewStyle( StackNavigationViewStyle( ) )
    }
}
struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        HighlightView( )
    }
}

这部分内容有很多要理解!在继续之前,请随意再次查看本节。记住,您可以通过 GitHub 仓库在线访问,以便轻松复制和粘贴:github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

实现 EnhancedView

首先,我们需要实现处理旋转功能的代码。在EnhancedView结构上方添加以下代码:

struct DeviceRotationViewModifier: ViewModifier{
    let action: ( UIDeviceOrientation ) -> Void
    func body( content: Content ) -> some View
    {
        content
            .onAppear( )
            .onReceive( NotificationCenter.default.publisher( for: UIDevice.orientationDidChangeNotification ) )
                { _ in
                    action( UIDevice.current.orientation )
                }
    }
}
extension View
{
    func onRotate( perform action: @escaping ( UIDeviceOrientation ) -> Void ) -> some View
        {
            self.modifier( DeviceRotationViewModifier( action: action ) )
        }
}

我们添加的先前代码允许我们在旋转屏幕时重新渲染内容。这将在检测设备方向时很快被使用。现在,我们可以在EnhancedView结构的开始处添加@Binding变量,这允许我们传递元数据:

@Binding var imageFile: String@Binding var imageName: String
@Binding var imageDate: String
@Binding var imageDescription: String

现在,我们将添加两个变量,第一个用于检测设备的方向,第二个用于检测设备屏幕尺寸(以像素为单位)。前者将用于确定正确的布局,后者将用于调整组件大小。添加以下代码:

@State private var orientation = UIDeviceOrientation.unknownlet screenSize: CGRect = UIScreen.main.bounds

在主体中,我们将创建一个组,其中包含两个布局,一个用于纵向,一个用于横屏。添加以下代码,我们将讨论正在发生的一切:

Group{
    if ( orientation.isLandscape )
    {
        LazyHStack
        {
            VStack
            {
                Image( imageFile )
                    .resizable( )
                    .scaledToFit( )
            }.frame( width: screenSize.width * 0.5 )
            VStack
            {
                Text( imageName )
                    .fontWeight(.bold)
                Text( imageDate )
                Text( imageDescription )
            }.frame( width: screenSize.width * 0.5 )
        }
    }
    else
    {
        LazyVStack
        {
            VStack
            {
                Image( imageFile )
                    .resizable( )
                    .scaledToFit( )
            }.frame( height: screenSize.height * 0.5 )
            VStack
            {
                Text( imageName )
                    .fontWeight( .bold )
                Text( imageDate )
                Text( imageDescription )
            }.frame( height: screenSize.height * 0.5 )
        }
    }
}
.onRotate
{ newOrientation in
    orientation = newOrientation
}

让我们逐行分析我们刚刚添加的代码。首先,我们检查设备处于哪种方向。默认情况下,我们检查它是否为横屏。如果不是,则必须是纵向,并且我们相应地处理组件大小和位置:

if ( orientation.isLandscape )

接下来,我们创建一个用于存储横屏方向组件的懒加载水平堆叠:

LazyHStack

接下来,我们创建两个垂直堆叠。框架宽度设置为屏幕宽度的一半。这实际上创建了一个等分的分屏设计。如果您想要自定义分割,可以随意修改乘数。在第一个垂直堆叠中,我们放置图片,设置为resizablescaledToFit。在第二个垂直堆叠中,我们放置文本元数据,使用简单的Text组件:

VStack{
    Image( imageFile )
        .resizable( )
        .scaledToFit( )
}.frame( width: screenSize.width * 0.5 )
VStack
{
    Text( imageName )
        .fontWeight(.bold)
    Text( imageDate )
    Text( imageDescription )
}.frame( width: screenSize.width * 0.5 )

else语句中,我们简单地使用LazyVStack,因为它与纵向方向相关。所做的唯一其他更改是调整框架大小:它不再与屏幕宽度相关联,而是与高度相关联。其余部分保持不变。

最后,我们在Group组件中添加一个检测器,它简单地检测设备是否已旋转,并更新用于检测绘制模式的orientation变量:

.onRotate{ newOrientation in
    orientation = newOrientation
}

剩下的唯一事情是更新EnhancedView文件底部的预览提供者。按照以下方式更新代码:

struct EnhancedView_Previews: PreviewProvider{
    static var previews: some View
    {
        EnhancedView( imageFile: .constant( "" ), imageName: .constant( "" ), imageDate: .constant( "" ), imageDescription: .constant( "" ) )
    }
}

上述代码仅为预览传递添加了一组默认参数。我将其留空,因为我使用了模拟器进行测试,但您可以根据需要输入占位符数据,以确保您可以使用预览正确添加文本。

在本节中,我们实现了EnhancedView代码。在下一节中,我们将查看结果。请随意修改横屏和竖屏的布局,使其独特。

测试 EnhancedView

在本节中,我们最终将测试我们的应用程序。启动它将带我们到HighlightView;点击任何图片,它将带您到EnhancedView。竖屏模式如下所示:

图 5.10 – 竖屏模式

图 5.10 – 竖屏模式

旋转应用程序将产生以下输出:

图 5.11 – 横屏模式

图 5.11 – 横屏模式

现在,我们的应用程序已经完成,并具有一个导航菜单,用于返回到HighlightView

注意

如果您需要旋转模拟器的帮助,请参阅上一章。

额外任务

现在应用程序已经完成,以下是一些额外的任务列表,供您完成以增强您的应用程序:

  • 使用不同的源数据:

    • 不同的图片

    • 不同的标题

    • 不同的描述

    • 不同的日期

  • 从互联网加载图片

  • 从互联网加载元数据

  • 将支持设备的范围扩展到 iPhone,从而为您提供考虑跨平台设计的机遇

  • 无额外信息使图片全屏

  • 全屏点击获取更多信息:在全屏模式下单次点击将显示照片的标题

  • 收藏夹:不同的图片集

  • 一侧面板,显示所有收藏夹名称,在竖屏模式下隐藏,通过按钮激活;在横屏模式下始终可见

  • 删除和重命名:允许用户从相册中删除图片并重命名它们

  • 分享:分享图片的能力

  • 不同的显示模式:列表和网格视图

我们将总结本章所涵盖的内容,但首先,我们将查看实现一些额外任务的代码。

全屏模式

为了将全屏模式添加到EnhancedView,我们将添加一个新的@State变量,称为isFullScreen。我们将使用此变量在全屏模式和常规模式之间切换。此外,我们还需要向图片添加onTapGesture,以便当图片被点击时,它会切换全屏模式。以下是修改后的代码:

import SwiftUIstruct EnhancedView: View
{
    @Binding var imageFile: String
    @Binding var imageName: String
    @Binding var imageDate: String
    @Binding var imageDescription: String
    @State private var orientation = UIDeviceOrientation.unknown
    @State private var isFullScreen: Bool = false
    let screenSize: CGRect = UIScreen.main.bounds
    var body: some View
    {
        Group
        {
            if isFullScreen {
                Image(imageFile)
                    .resizable()
                    .scaledToFit()
                    .edgesIgnoringSafeArea(.all)
                    .onTapGesture {
                        self.isFullScreen.toggle()
                    }
            }
            else if orientation.isLandscape {
                LazyHStack
                {
                    VStack
                    {
                        Image(imageFile)
                            .resizable()
                            .scaledToFit()
                            .onTapGesture {
                                self.isFullScreen.toggle()
                            }
                    }.frame(width: screenSize.width * 0.5)
                    VStack
                    {
                        Text(imageName)
                            .fontWeight(.bold)
                        Text(imageDate)
                        Text(imageDescription)
                    }.frame(width: screenSize.width * 0.5)
                }
            }
            else {
                LazyVStack
                {
                    VStack
                    {
                        Image(imageFile)
                            .resizable()
                            .scaledToFit()
                            .onTapGesture {
                                self.isFullScreen.toggle()
                            }
                    }.frame(height: screenSize.height * 0.5)
                    VStack
                    {
                        Text(imageName)
                            .fontWeight(.bold)
                        Text(imageDate)
                        Text(imageDescription)
                    }.frame(height: screenSize.height * 0.5)
                }
            }
        }
    }
}
struct EnhancedView_Previews: PreviewProvider
{
    static var previews: some View
    {
        EnhancedView(imageFile: .constant(""), imageName: .constant(""), imageDate: .constant(""), imageDescription: .constant(""))
    }
}

让我们看看这个修改后的代码做了什么:

  • 添加一个新的@State变量isFullScreen,用于跟踪视图是否处于全屏模式。

  • Group的开始处添加一个新的条件,如果isFullScreentrue,则显示全屏模式的图片。在这个条件下,我们使用.edgesIgnoringSafeArea(.all)确保图片占据整个屏幕,并使用.onTapGesture在图片被点击时切换isFullScreen

  • 通过添加.onTapGesture在横屏和竖屏模式下修改现有的Image视图,当图片被点击时切换isFullScreen

这导致图片在被点击时占据整个屏幕,再次点击时恢复到原始大小。

收藏夹

要添加集合(相册)并显示这些集合名称的侧边栏,我们需要对代码进行几处修改:

  1. 创建一个数据结构来表示图片集合。

  2. 修改 HighlightView 以在横幅模式下在侧边栏中显示集合列表。

  3. 显示所选集合的图片。

这就是你可以这样做的方法:

import SwiftUI// Data structure representing an image collection
struct ImageCollection {
    let name: String
    let images: [String]
}
struct HighlightView: View {
    // Sample data
    private let collections: [ImageCollection] = [
        ImageCollection(name: "Collection 1", images: ["FireDEV", "FireDEV", "FireDEV"]),
        ImageCollection(name: "Collection 2", images: ["FireDEV", "FireDEV", "FireDEV", "FireDEV"]),
        ImageCollection(name: "Collection 3", images: ["FireDEV", "FireDEV"])
    ]
    private let adaptiveColumns = [GridItem(.adaptive(minimum: 300))]
    @State private var isClicked: Bool = false
    @State private var imageFile: String = ""
    @State private var imageName: String = ""
    @State private var imageDate: String = ""
    @State private var imageDescription: String = ""
    @State private var selectedCollection: ImageCollection?
    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                if geometry.size.width > geometry.size.height {
                    // Horizontal mode, show side panel
                    HStack {
                        // Side Panel
                        List(collections, id: \.name) { collection in
                            Button(action: {
                                selectedCollection = collection
                            }) {
                                Text(collection.name)
                            }
                        }
                        .frame(width: geometry.size.width * 0.25)
                        // Images
                        ScrollView {
                            LazyVGrid(columns: adaptiveColumns, spacing: 20) {
                                if let images = selectedCollection?.images {
                                    ForEach(0..<images.count, id: \.self) { i in
                                        Image(images[i])
                                            .resizable()
                                            .scaledToFill()
                                            .frame(width: 300, height: 300)
                                    }
                                }
                            }
                        }
                    }
                } else {
                    // Vertical mode, just show images
                    ScrollView {
                        LazyVGrid(columns: adaptiveColumns, spacing: 20) {
                            if let images = selectedCollection?.images {
                                ForEach(0..<images.count, id: \.self) { i in
                                    Image(images[i])
                                        .resizable()
                                        .scaledToFill()
                                        .frame(width: 300, height: 300)
                                }
                            }
                        }
                    }
                    .onAppear {
                        // Select the first collection by default
                        if selectedCollection == nil {
                            selectedCollection = collections.first
                        }
                    }
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        HighlightView()
    }
}

让我们解释一下这些更改:

  • 我们添加了一个名为 ImageCollection 的结构体,它代表一个带有名称的图片集合。

  • 我们更新了 collections 属性,使其成为一个 ImageCollections 数组的数组。

  • 我们移除了旧的 images 数组,因为它现在已经是集合的一部分了。

  • 我们使用 GeometryReader 来确定视图是在水平模式还是垂直模式。在水平模式下,显示一个包含集合名称列表的侧边栏。

  • 在水平模式下,点击侧边栏中的集合名称会更新 selectedCollection 状态变量,进而更新侧边栏右侧显示的图片。

  • 在垂直模式下,仅显示所选集合的图片。默认情况下,第一个集合被选中。

这段代码演示了如何创建一个自适应布局,在水平模式下显示侧边栏,并根据所选集合调整其内容。

摘要

在本章中,我们使用线框图设计了我们的 EnhancedView。这些线框图帮助我们分解视图为它们的组件。然后,我们实现了 SwiftUI 组件以匹配线框图中的设计。尽管这些组件在纵向和横向方向上相同,但我们相应地配置了它们的定位和大小。确保每个受支持的定位都符合行业标准,这一点非常重要。我们还更新了 HighlightView 以将数据传递给 EnhancedView。这些数据用于在 EnhancedView 中添加的组件中显示内容。然后,我们介绍了额外的任务供你执行;在继续之前,请随意再次查看本章。我们现在已经完成了第二个应用程序,它现在可供你修改和使用。

在下一章中,我们将开始我们的下一个应用程序,即 Mac 的 App Store。我们将自然地研究其设计,并将其分解以帮助我们理解和实现适用于我们下一个平台的应用程序。

第六章:Mac 项目 – 应用商店侧边栏

在前四章中,我们为 iPhone 创建了一个税务计算器应用程序,为 iPad 创建了一个照片画廊应用程序。我们从零开始实现它们,考虑了技术要求、设计规范、线框和代码实现。我们将使用本章和下一章中涵盖的技能来创建应用商店侧边栏,但请放心:我们将回顾所有必要的方面,以防你直接跳到本章。

在本章中,我们将着手设计我们的第三个项目,一个展示其大屏幕且保持单一方向的 Mac 应用商店应用程序。我们将评估设计此类应用程序的要求,并讨论设计规范,以便我们更好地理解所需内容以及如何将这些内容整合在一起。然后,我们将开始本章应用程序的编码过程,构建侧边栏,下一章将涵盖应用商店应用程序的主体。本项目将涵盖 SwiftUI 组件的基础。

本章涵盖了以下主要主题:

  • 理解设计规范

  • 可接受标准

  • 构建侧边栏 UI

到本章结束时,你将更好地理解我们应用程序的设计和所需内容,以及一个将作为下一章中画廊工作基础的骨架用户界面。

技术要求

本章要求你从 Apple 的应用商店下载 Xcode 14 或更高版本。要在应用商店中安装 Xcode,只需在应用商店中搜索 Xcode,然后选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 14 版本具有以下功能/要求:

  • 包含 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本的设备调试

  • 需要运行 macOS Monterey 12.5 或更高版本的 Mac

如需有关技术细节的更多信息,请参阅第一章

本章的代码文件可在此处找到:github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

在下一节中,我们将提供关于我们应用程序设计规范的清晰说明,并查看应用程序的外观原型。

理解设计规范

在本节中,我们将查看我们应用商店应用程序的设计规范,并描述我们将要实现的功能。确定所需功能的最有效方法是站在用户的角度,确定他们将如何使用应用程序,然后将其分解为单独的步骤。

我们的应用程序可能有许多功能,如下所示:

  • 侧边栏 – 用于应用程序的不同部分。

  • 突出横幅 - 展示每日应用。

  • 特色瓷砖 - 展示较少的应用或更具体的类别应用。

  • 新应用和更新部分。

  • 搜索 - 能够在整个集合中搜索特定应用。

  • 账户管理。

  • 应用审查。

  • 应用报告。

  • 应用页面 - 显示所选应用的描述和日期等信息,类似于我们之前创建的相册应用中的EnhancedView

  • 来自外部源的应用图像和信息,例如本地数据库或在线。

  • 分享 - 能够分享图片。

  • 创建收藏/稍后下载列表。

现在我们已经列出了我们希望的理想功能,接下来,对我们来说,确定哪些功能是绝对必要的非常重要。为了做到这一点,我们必须了解我们产品的最终用途。对我来说,创建这个应用商店应用的目的在于展示侧边栏及其与主体部分的集成。我们不会实现应用页面,因为它与相册的EnhancedView非常相似,这将被设定为一个额外任务,你可以使用前两章的内容作为辅助。基于这一点,我知道并非所有功能都是必需的,有些如果省略并作为额外任务分配给你作为开发者去完成,将会很有用。因此,以下是我们将实施的核心功能:

  • 侧边栏 - 用于应用的各个部分。

  • 突出横幅 - 展示每日应用。

  • 特色瓷砖 - 展示较少的应用或更具体的类别应用。

  • 新应用和更新部分。

其余的功能将在你完成这一章和下一章后成为你的练习。下一节将涵盖我们应用的验收标准。

理解验收标准

我们将在下一章的末尾讨论我们应用必须满足的要求,我们绝对希望在最终产品中看到这些要求。如果可能的话,我们应该尝试使它们具有可衡量性,所以现在就让我们列出它们:

  • 带有文本组件及其旁边图标的侧边栏,以增强视觉上下文。

  • 可滚动的主体视图,用于滚动浏览应用和主体中的突出显示应用。

  • 突出横幅将占据其父容器的全部宽度,以展示应用。

  • 展示特定类别中其他特定应用的瓷砖。

  • 显示其余应用的图像和文本。

开发测试用例,以测试应用的验收标准。使用这种方法可以让我们看到应用将被最终用户使用的条件以及需要达到的成功水平。

我们将开发测试方法来检查可测试和最终可衡量的验收标准。这将使我们能够看到应用将被用于哪些用例,以及需要达到的水平才能被认为是成功的。

线框设计

设计布局最有用的工具之一是线框设计。线框是布局外观的概述。以下图显示了我们将使用线框实现的整个应用程序的前页面:

图 6.1 – App Store 线框图

图 6.1 – App Store 线框图

在下一节中,我们将构建我们应用程序的界面,并确保它看起来与我们设计在线框图中的样子一致。虽然我们将以相同的方式构建它,但可能会有一些细微的差异。在本章中,我们将重点关注侧边栏,并在下一章完成主体部分。

构建 SideBar UI

现在,我们将构建侧边栏的 UI。首先,让我们创建我们的项目。按照以下步骤操作:

  1. 打开 Xcode 并选择 创建新的 Xcode 项目

图 6.2 – 创建新的 Xcode 项目

图 6.2 – 创建新的 Xcode 项目

  1. 现在,我们将选择我们应用程序的模板。由于我们正在创建一个 iPad 应用程序,我们将从顶部选择 iOS,选择 App,然后点击 下一步

图 6.3 – Xcode 项目模板选择

图 6.3 – Xcode 项目模板选择

  1. 现在,我们将选择我们项目的选项。在这里,只有两个关键的事情需要选择/设置。确保界面设置为 SwiftUI,这是我们系统将使用的 UI,并且语言设置为Swift,这是我们应用程序显然使用的编程语言:

图 6.4 – Xcode 项目选项

图 6.4 – Xcode 项目选项

  1. 一旦你按下 下一步,你可以选择创建项目的地方,如下面的图所示:

图 6.5 – Xcode 项目保存目录

图 6.5 – Xcode 项目保存目录

  1. 一旦你找到了你想要创建的位置,就点击右下角的创建,如图 图 6**.5 所示。Xcode 以其全部的荣耀显示了你的项目,如下面的图所示:

图 6.6 – 新 Xcode 项目概览

图 6.6 – 新 Xcode 项目概览

在下一节中,我们将使用 SwiftUI 实现我们应用程序的侧边栏。

探索侧边栏组件

在本节中,我们将实现侧边栏的用户界面。作为提醒,它将看起来如下:

图 6.7 – App Store 线框图

图 6.7 – App Store 线框图

侧边栏有两个主要元素。作为一个小任务,看看你是否能找出它们是什么。如果你不知道确切的 UI 组件名称,不要担心;我们将在接下来的几节中查看这些组件。

标签项

标签项组件简单地显示侧边栏中的项目,可以用作按钮来导航应用程序。它允许您显示一串字符、数字,甚至图标,所有这些都可以相互结合使用。对我们来说,我们将它们用作侧边栏内的虚拟按钮:

图 6.8 – 侧边栏项

图 6.8 – 侧边栏项

搜索栏

SearchBar 组件允许用户在一系列组件中进行搜索。对我们来说,我们将使用它作为虚拟搜索组件,该组件将在 App Store 中的所有应用中进行搜索。虽然搜索栏本身不是侧边栏的一部分,但我们将在实现侧边栏的同时实现它:

图 6.9 – 搜索栏

图 6.9 – 搜索栏

重命名视图

在本节中,我们将向我们的高亮页面添加图像组件,该页面目前命名为 ContentView,将其重命名为 MainView。如果您已经知道如何重命名,请随意跳过这些步骤。这样做很简单:打开 ContentView,在代码中右键单击任何对 ContentView 的引用,然后转到 重构 | 重命名...,如图下所示:

图 6.10 – 重命名按钮

图 6.10 – 重命名按钮

接下来,将显示以下屏幕:

图 6.11 – 重命名屏幕

图 6.11 – 重命名屏幕

将名称从 MainView 更改。您可以看到所有其他需要更改的引用,这很有用。最后,按下右上角的 重命名 按钮,如图下所示:

图 6.12 – 重命名按钮

图 6.12 – 重命名按钮

我们现在已经重命名了我们的视图,包括文件,如图 项目导航器 中所示:

图 6.13 – 项目导航器中更新的文件名

图 6.13 – 项目导航器中更新的文件名

有一个额外的步骤是可选的。这个步骤是将 ContentView_Previews 结构重命名。虽然不是至关重要,但我强烈建议将其重命名为 MainView_Previews,以保持所有名称引用的一致性。使用前面的步骤,将 ContentView_Previews 结构重命名为 MainView_Previews。该结构位于 MainView 文件(之前命名为 ContentView)的底部:

图 6.14 – 重命名预览结构

图 6.14 – 重命名预览结构

在本节中,我们查看了一下应用程序的设计,特别是 SideBar UI 的设计。我们还重命名了 ContentView。在下一节中,我们将实现侧边栏的代码。

实现侧边栏

由于我们创建了一个全新的项目,编码标准并不符合我的个人偏好。因此,首先,我将更改标准。请随意花几分钟时间做同样的事情。

导航视图

让我们实现一个NavigationView来提供一个分屏布局,这将允许我们编写侧边栏代码。这样做很简单。删除当前的主体代码,并用以下代码替换:

var body: some View{
    NavigationView
    {
        List
        {
            Label( "Discover", systemImage: "" )
            Label( "Arcade", systemImage: "" )
            Label( "Create", systemImage: "" )
            Label( "Categories", systemImage: "" )
            Label( "Favorites", systemImage: "" )
            Label( "Updates", systemImage: "" )
        }
    }
}

在我们的NavigationView中,我们创建了一个List,其中包含一组Labels,这些标签将作为我们的虚拟按钮。默认情况下,我们不提供系统图像,但我们需要指定一些内容,否则将导致错误。

上述代码将导致以下结果:

图 6.15 – 标签预览

图 6.15 – 标签预览

虽然我们不需要实现图标,但应用程序将极大地受益于它们的加入。添加图片很容易;你只需使用systemImage参数。或者,你可以按照第四章中“实现 HighlightView”部分的步骤提供自己的图片,并使用image参数实现它们。找到/搜索系统图像的最佳方法是下载SF Symbols,你可以在developer.apple.com/sf-symbols/找到它。一旦安装,你可以轻松地搜索系统图像。要使用系统图像,只需复制以下系统图像的名称:

图 6.16 – 系统图像名称

图 6.16 – 系统图像名称

随意使用你认为最适合你应用程序的系统图像。我选择了以下图像:

List{
    Label( "Discover", systemImage: "star" )
    Label( "Arcade", systemImage: "gamecontroller" )
    Label( "Create", systemImage: "paintbrush" )
    Label( "Categories", systemImage: "square.grid.3x3.square" )
    Label( "Favorites", systemImage: "heart" )
    Label( "Updates", systemImage: "square.and.arrow.down" )
}

运行应用程序将显示以下结果:

图 6.17 – 系统图标

图 6.17 – 系统图标

搜索栏

虽然SearchBar将被放置在顶部栏中,但它包含在侧边栏代码中。添加搜索栏非常简单。首先,在主体之前添加以下代码,这将存储SearchBar的用户输入:

@State private var searchText = ""

接下来,将以下代码添加到List组件中:

.searchable( text: $searchText )

我们刚刚添加的代码添加了一个SearchBar并将其文本链接到我们创建的searchText变量。列表组件代码现在将如下所示:

List{
    Label( "Discover", systemImage: "star" )
    Label( "Arcade", systemImage: "gamecontroller" )
    Label( "Create", systemImage: "paintbrush" )
    Label( "Categories", systemImage:
"square.grid.3x3.square" )
    Label( "Favorites", systemImage: "heart" )
    Label( "Updates", systemImage: "square.and.arrow.down" )
}.searchable( text: $searchText )

运行应用程序将产生以下结果:

图 6.18 – 搜索栏

图 6.18 – 搜索栏

在本节中,我们实现了SearchBar UI 的代码。在下一节中,我们将探讨一些额外功能,这些功能将有助于使应用程序焕发生机。

实现额外功能

尽管在这个项目的范围内我们已经完成了SideBar,但我还想向你展示如何实现按下SearchBar上的Enter键的事件,以及如何使SideBar标签可点击。

搜索栏 Enter 事件

我们希望用户在选择了SearchBar后能够按下Enter键来触发一个事件。这个事件可以弹出一个结果列表或新页面。请随意将其作为一个额外任务来实现。

实现这一点的代码非常简单。在.``searchable代码之后添加以下代码:

}.searchable( text: $searchText )    .onSubmit( of: .search )
    {
        print( searchText )
    }

当用户提交搜索时,括号内的代码将会执行。为了测试,我们打印了 searchText 变量,它将打印出你输入的内容。请随意运行它。

可点击标签事件

目前,侧边栏内的标签没有事件功能。在本章的范围内,我们不需要它,但我将向你展示如何使标签可点击。实际上非常简单。只需为每个标签添加一个 onTapGesture 函数,如下所示:

Label( "Discover", systemImage: "star" ).onTapGesture
{
    print( "Discover" )
}

我只将其添加到了第一个标签上,但你可以随意将其添加到其他标签上。

在本节中,我们实现了代码,允许用户在搜索栏中按下 Enter 键时触发 Submit 事件。我们还添加了检测标签被点击的代码。

摘要

在本章中,我们讨论了我们的应用商店应用程序的设计。我们研究了线框图,并将每个元素分解为 SwiftUI 组件。然后我们实现了 SwiftUI 组件,以匹配线框图中的 SideBar UI 设计。我们还研究了构建此应用程序的要求和设计规范。然后我们将它们简化为核心功能,这是我们应用将提供的功能。我们还实现了额外功能,以允许额外的输入,使应用程序更加完善。

在下一章中,我们将探讨实现我们的应用商店应用程序的主体。

第七章:Mac 项目 – 应用商店主体

在本章中,我们将致力于实现应用商店项目的主体部分。在前一章中,我们研究了应用商店的设计,特别是侧边栏设计。然后,我们将侧边栏分解为我们应用程序需求所需的所有必要组件。然后,我们使用 SwiftUI 实现了所有组件。在前一章的结尾,我们只有一个带有一些可选事件跟踪的侧边栏,但主体部分没有内容。主要部分将是可滚动的,并使用图标和横幅展示应用程序。现在,我们将分析主体部分,将其分解为其包含的所有组件,并实现所有组件以提供类似应用商店的感觉。

本章将分为以下部分:

  • 主体部分概述

  • 实现主体部分

  • 额外任务

到本章结束时,您将创建一个具有可滚动视图的应用商店模板来展示应用程序。这将为进一步扩展应用商店应用程序或使用我们已实现的内核结构将项目转向完全不同的东西提供一个坚实的基础。随着本章的结束,我将提供练习以在应用商店中实现更高级的功能。这将很好地过渡到我们的第四个也是最后一个项目,即 Apple Watch 健身伴侣应用程序。

技术要求

本章要求您从 Apple 的 App Store 下载 Xcode 版本 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索 Xcode,选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,您就可以开始了。

Xcode 版本 14 具有以下功能/要求:

  • 包含适用于 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本在设备上进行调试

  • 需要 macOS Monterey 12.5 或更高版本的 Mac

有关技术细节的更多信息,请参阅 第一章

本章的代码文件可在此处找到:

github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

在下一节中,我们将阐明我们应用程序设计的规范,并查看应用程序的外观原型。

主体部分概述

在本节中,我们将再次查看第六章 * 的线框,并将它们分解为其单个组件。本节提供了应用商店和主体部分的线框图像。这些图像描述了应用商店和主体部分的布局和设计。

图 7.1 – 应用商店视图

图 7.1 – App Store 视图

图 7.2 – 应用商店主体

图 7.2 – 应用商店主体

在我们编写应用程序代码之前,我们将把主体分解成构成它的元素。作为一个小任务,看看你是否能弄清楚这些是什么,但如果你不知道确切的 UI 组件名称,也不要担心。我们将在下一节中查看这些组件。

图像组件

图像组件是 SwiftUI 提供的核心组件之一。它允许你显示图像,可以用来提供视觉表示或辅助文本内容。我们将以两种主要方式使用它,首先是通过高亮横幅展示特定的应用程序,其次是通过显示应用程序列表/网格。

图 7.3 – 应用图标

图 7.3 – 应用图标

图 7.4 – 横幅

图 7.4 – 横幅

文本组件

文本组件是 SwiftUI 提供的最简单的组件之一。它允许你显示一串字符/数字,这对于标题和信息提供非常有用。我们将用于以下内容:

  • 应用标题

  • 章节描述

在接下来的章节中,我们将继续使用我们之前讨论的 SwiftUI 组件来开发应用程序的主体。这个实现将非常精确,并注重细节。

实现主体

在本节中,我们将通过实现应用程序的主体来完成本书的第三个项目。我们的第一步将是编写高亮横幅的代码,然后是应用程序图标。

编写高亮横幅

首先,我们将添加高亮横幅的代码。横幅将简单地覆盖整个页面的宽度;我们将为它添加一些间距以增加美观。在页面中添加多个横幅以突出不同的应用,并使用轮播横幅,通过滑动等过渡效果在一个部分中展示多个横幅是很常见的。我们将实现一个单独的横幅;然而,添加更多是非常简单的。按照以下步骤操作:

  1. 让我们先添加一个横幅图片。我的图片是 728x90 像素。请随意修改以适应你的需求。从 项目导航器 中选择 资产

图 7.5 – 项目导航器中的资产位置

图 7.5 – 项目导航器中的资产位置

  1. 现在,资产 视图将出现。将图像导入 资产 可以有两种方式

    1. 将文件拖放到 资产 部分中:

图 7.6 – 拖放资产

图 7.6 – 拖放资产

  1. 右键单击 资产 部分并选择 导入…

图 7.7 – 导入… 按钮

图 7.7 – 导入… 按钮

一旦导入资产,资产 视图将如下所示:

图 7.8 – 导入的资产

图 7.8 – 导入的资产

注意

我正在使用横幅为我的面向开发者的播客 FireDEV。你可以自由使用,并请在以下链接中收听我的播客,每周四更新:

  1. 我们将在List代码之后添加一个Image组件:

    }.searchable( text: $searchText )    .onSubmit( of: .search )    {        print( searchText )    }Image( "FireDEV Banner" )
    

这将导致以下结果:

图 7.9 – 添加了横幅

图 7.9 – 添加了横幅

  1. 如果你尝试调整窗口大小,会有一些限制。我们需要使横幅可调整大小并保持其原始宽高比。更新图像代码如下:

    Image( "FireDEV Banner" )    .resizable( )    .padding( .horizontal )    .scaledToFit( )
    

我们使图像可调整大小,这样它可以根据窗口的大小改变大小。这对于用户在运行应用商店时在不同屏幕尺寸上运行非常有用,他们可能不会总是全屏显示。然后我们添加了水平填充以确保它不接触左右边缘。如果你喜欢,可以省略此步骤,或者你可以指定一个固定的填充量。最后,我们将它设置为scaledToFit,这保持了原始宽高比。扭曲从来都不是一个好主意。所有这些结果如下:

图 7.10 – 横幅已更新

图 7.10 – 横幅已更新

  1. 目前,横幅始终位于中心。我们希望将其置于视图顶部。为了实现这一结果,我们将之前添加的图像代码包裹在一个具有topLeading对齐方式的ScrollView中。更新代码如下:

    ScrollView{    Color.clear    Image( "FireDEV Banner" )        .resizable( )        .padding( .horizontal )        .scaledToFit( )}
    

我们还添加了一个Color.clear指令以确保没有背景颜色,所有这些结果都产生了一个很棒的横幅:

图 7.11 – 横幅定位在顶部

图 7.11 – 横幅定位在顶部

突出横幅已完成,可以将其转换为轮播图。接下来,应用组将被编码以展示应用图标列表。

编写应用组代码

我们现在将实现显示应用组的代码。这些将包含代表应用图标的图像和一个代表应用名称的标签。你可以自由地向每个组添加更多组件,并按你的喜好排列。我已经将应用图标添加到资源中。我遵循了之前的步骤来添加图像。你可以自由地参考那些步骤:

  1. 首先,在正文之前添加以下代码:

    private let adaptiveColumns =[    GridItem( .adaptive( minimum: 300 ) )]
    

这将在我们的网格中使用,并确保项目具有至少 300 像素的最小尺寸。这对于我们不想让它们变得太小以至于用户看不到它们来说非常有用。

  1. 在上一节中添加的横幅代码下方添加以下代码:

    LazyVGrid( columns: adaptiveColumns, spacing: 20 ){    ForEach ( 0..<20 )    { index in        VStack( alignment: .leading )        {            Image( "FireDEV Logo" )            Label( "FireDEV", systemImage: "" )        }    }}
    

在我们运行应用程序之前,让我们分解代码。

  • 我们使用adaptiveColumns创建一个LazyVGrid,并将spacing设置为20。你可以根据需要更改列大小的间距。

  • 接下来,我们使用一个运行 20 次的ForEach循环。你可以随意用上一个项目中使用的方法,将代码替换为一个项目数组。

  • 然后,我们在图像下方创建了一个Label组件。该标签将用作应用程序的名称。

  • 最后,我们创建了一个Image组件和一个Label组件,并省略了systemImage参数,因为我们不需要它。然而,你必须放些东西,因此使用了空引号。

运行应用程序将产生以下结果:

图 7.12 – 应用程序组

图 7.12 – 应用程序组

  1. 我们几乎完成了这个应用程序。应用程序的名称有点小。让我们把它做得更大,并更新Label如下:

    Label( "FireDEV", systemImage: "" )    .font( .system( size: 36 ) )
    

最后,运行这将产生以下图示:

图 7.13 – 标签字体大小增加

图 7.13 – 标签字体大小增加

作为回顾,以下是MainView的完整代码:

////  ContentView.swift
//  App Store
//
//  Created by Frahaan on 21/02/2023.
//
import SwiftUI
struct MainView: View
{
    @State private var searchText = ""
    private let adaptiveColumns =
    [
        GridItem( .adaptive( minimum: 300 ) )
    ]
    var body: some View
    {
        NavigationView
        {
            List
            {
                Label( "Discover", systemImage: "star" )
                .onTapGesture
                {
                    print( "Discover" )
                }
                Label( "Arcade", systemImage: "gamecontroller" )
                Label( "Create", systemImage: "paintbrush" )
                Label( "Categories", systemImage: "square.grid.3x3.square" )
                Label( "Favorites", systemImage: "heart" )
                Label( "Updates", systemImage: "square.and.arrow.down" )
            }.searchable( text: $searchText )
                .onSubmit( of: .search )
                {
                    print( searchText )
                }
            ScrollView
            {
                Color.clear
                Image( "FireDEV Banner" )
                    .resizable( )
                    .padding( .horizontal )
                    .scaledToFit( )
                LazyVGrid( columns: adaptiveColumns, spacing: 20 )
                {
                    ForEach ( 0..<20 )
                    { index in
                        VStack( alignment: .leading )
                        {
                            Image( "FireDEV Logo" )
                            Label( "FireDEV", systemImage: "" )
                                .font( .system( size: 36 ) )
                        }
                    }
                }
            }
        }
    }
}
struct MainView_Previews: PreviewProvider
{
    static var previews: some View
    {
        MainView( )
    }
}

代码也包含在本书的 GitHub 仓库中。

在本节中,我们实现了 App Store 应用程序的主体,从而完成了我们的第三个项目。主要有两个部分 – 首先,我们实现了一个高亮横幅,可以在整个视图中多次使用来展示不同的应用程序。然后,我们实现了一个应用程序组的网格。尽管应用程序信息是硬编码的,但它可以被抽象成一个数组,可以存储每个应用程序的更多信息。在下一节中,我们将总结本章内容。

额外任务

现在应用程序已经完成,以下是一些额外的任务列表,供你完成以增强你的应用程序:

  • 一个应用程序页面,用户点击应用程序图标时将导航到该页面

  • 多个高亮横幅

  • 将横幅更新为轮播图

  • 侧边栏中该部分的多个页面

  • 从数组或外部源(如数据库)中提取数据用于应用程序元数据

在下一节中,我们将总结本章所涵盖的内容,但首先,我们将回顾代码以帮助完成本项目的额外任务。

搜索功能

要向应用程序添加搜索功能,你可以使用 SwiftUI 提供的.searchable修饰符。以下是添加了搜索功能的修改后的代码:

////  ContentView.swift
//  App Store
//
//  Created by Frahaan on 21/02/2023.
//
import SwiftUI
struct MainView: View {
    @State private var searchText = ""
    private let adaptiveColumns = [
        GridItem(.adaptive(minimum: 300))
    ]
    var body: some View {
        NavigationView {
            List {
                Label("Discover", systemImage: "star")
                    .onTapGesture {
                        print("Discover")
                    }
                Label("Arcade", systemImage: "gamecontroller")
                Label("Create", systemImage: "paintbrush")
                Label("Categories", systemImage: "square.grid.3x3.square")
                Label("Favorites", systemImage: "heart")
                Label("Updates", systemImage: "square.and.arrow.down")
            }
            .searchable(text: $searchText) // Add searchable modifier
            .onSubmit(of: .search) {
                print(searchText)
            }
            ScrollView {
                Color.clear
                Image("FireDEV Banner")
                    .resizable()
                    .padding(.horizontal)
                    .scaledToFit()
                LazyVGrid(columns: adaptiveColumns, spacing: 20) {
                    ForEach(0..<20) { index in
                        VStack(alignment: .leading) {
                            Image("FireDEV Logo")
                            Label("FireDEV", systemImage: "")
                                .font(.system(size: 36))
                        }
                    }
                }
            }
        }
    }
}
struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

在这个修改后的代码中,我在List视图中添加了.searchable(text: $searchText)修饰符,这启用了搜索功能。searchText变量用作搜索文本输入的绑定。

我还向List视图添加了.onSubmit(of: .search)修改器来处理搜索提交。在这个示例中,它将搜索文本打印到控制台,但你可以根据需求自定义操作。

通过这些修改,用户将能够输入搜索查询并根据输入的文本过滤列表中的项目。

应用页面

当用户点击应用时,为了提供一个包含更多信息的高级页面,你可以创建一个新的视图来显示所选应用的详细信息。以下是一个如何修改代码以实现此功能的示例:

import SwiftUIstruct AppDetailsView: View {
    let appName: String
    var body: some View {
        VStack {
            Text(appName)
                .font(.largeTitle)
                .padding()
            // Add more detailed information about the app here
            Spacer()
        }
        .navigationBarTitle(appName)
    }
}
struct MainView: View {
    @State private var searchText = ""
    @State private var selectedApp: String? = nil
    private let adaptiveColumns = [
        GridItem(.adaptive(minimum: 300))
    ]
    var body: some View {
        NavigationView {
            List {
                Label("Discover", systemImage: "star")
                    .onTapGesture {
                        print("Discover")
                    }
                Label("Arcade", systemImage: "gamecontroller")
                    .onTapGesture {
                        selectedApp = "Arcade"
                    }
                Label("Create", systemImage: "paintbrush")
                    .onTapGesture {
                        selectedApp = "Create"
                    }
                Label("Categories", systemImage: "square.grid.3x3.square")
                    .onTapGesture {
                        selectedApp = "Categories"
                    }
                Label("Favorites", systemImage: "heart")
                    .onTapGesture {
                        selectedApp = "Favorites"
                    }
                Label("Updates", systemImage: "square.and.arrow.down")
                    .onTapGesture {
                        selectedApp = "Updates"
                    }
            }
            .searchable(text: $searchText)
            .onSubmit(of: .search) {
                print(searchText)
            }
            ScrollView {
                Color.clear
                Image("FireDEV Banner")
                    .resizable()
                    .padding(.horizontal)
                    .scaledToFit()
                LazyVGrid(columns: adaptiveColumns, spacing: 20) {
                    ForEach(0..<20) { index in
                        VStack(alignment: .leading) {
                            Image("FireDEV Logo")
                            Label("FireDEV", systemImage: "")
                                .font(.system(size: 36))
                                .onTapGesture {
                                    selectedApp = "FireDEV"
                                }
                        }
                    }
                }
            }
        }
        .sheet(item: $selectedApp) { app in
            AppDetailsView(appName: app)
        }
    }
}
struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

在这个修改后的代码中,我添加了一个新的AppDetailsView,它接受所选应用名称作为参数,并显示关于应用的更多详细信息。你可以根据需求自定义此视图的内容。

我还添加了一个新的@State变量selectedApp来跟踪所选应用名称。当用户在列表或标签中点击应用时,相应的应用名称被分配给selectedApp,并且使用.sheet(item: $selectedApp)以表单的形式展示AppDetailsView。当用户关闭表单时,selectedApp被设置回nil

AppDetailsView中,我仅为了演示目的显示了应用名称。你可以添加更多信息并根据你应用的需求自定义布局。

通过这些修改,当用户点击应用时,将出现一个新的表单,显示有关所选应用的增强页面。

摘要

在本章中,我们成功实现了应用商店应用的主体。我们首先分析了线框图,并将每个元素分解成 SwiftUI 组件。然后,我们仔细实现了 SwiftUI 组件,以匹配线框图中的设计。我们实现了一个可滚动的堆叠,带有高亮横幅和应用图标。我们还在本章的末尾查看了一些额外的任务实现。

在下一章中,我们将开始着手我们的第四个也是最后一个应用,这是一个适用于 Apple Watch 的健身伴侣应用。我们的重点将放在分析其设计和将其分解,以便更好地理解我们如何在下一个平台上实现这个应用。

第八章:观看项目 – 健身伴侣设计

在前六章中,我们已经为我们的苹果设备创建了各种应用程序。这些章节教会了我们如何为 iPhone、iPad 和 Mac 设置项目。它们还展示了小屏幕和大屏幕之间的设计差异。在本章中,我们将为 Apple Watch 设计一个健身伴侣应用程序。由于手表屏幕尺寸较小,我们需要简化设计。在开始编码过程之前,我们将评估需求并讨论设计规范。

首先,我们将评估为 Apple Watch 设计健身伴侣应用程序所需的需求。然后,我们将讨论设计规范,这将使我们更好地了解所需的内容以及如何将这些内容整合在一起。接下来将是编码过程,我们将在这两章中构建健身应用程序。本项目将涵盖 SwiftUI 组件的基础。我们将在以下章节中讨论所有这些内容:

  • 理解设计规范

  • 构建健身应用程序

在本章中,我们将更好地理解我们应用程序的需求和设计。在前几章中,我们在 SwiftUI 组件、设计和 Xcode 导航方面建立的基础将作为下一章的强大起点。请继续关注我们的进展,因为我们将继续在这些基础上构建。

技术要求

本章要求您从苹果的 App Store 下载 Xcode 版本 14 或更高版本。

要安装它,只需在 App Store 中搜索 Xcode,然后选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,您就可以开始了。

Xcode 14 版本具有以下功能/要求:

  • 包含 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK。

  • 支持 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本在设备上的调试。

  • 需要 macOS Monterey 12.5 或更高版本的 Mac。

您可以从以下 GitHub 链接下载示例代码:github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

在下一节中,我们将提供我们应用程序设计规范的明确说明,并查看应用程序的外观原型。

理解设计规范

本节概述了我们健身伴侣应用程序的设计规范。我们的目标是实现增强用户体验的功能。为了实现这一目标,我们站在用户的角度来确定他们将如何使用应用程序。然后我们将这个过程分解成单个步骤,以确定必要的功能。通过这样做,我们可以确保我们的健身应用程序将易于使用且高效。

在本节中,我们将查看我们健身伴侣应用程序的设计规范,并描述我们将要实现的功能。确定所需功能的最有效方法是站在用户的角度,确定他们将如何使用应用程序,并将其分解为单个步骤。

我们的健身应用程序已经设计了几项功能,以帮助用户实现他们的健身目标。我们相信我们的应用程序将为用户提供无缝且有效的健身体验。

我们希望我们的应用程序拥有的功能如下:

  • 当前时间

  • 活动/锻炼时间

  • 每分钟心率(BPM)

  • 总消耗卡路里

  • 活动

  • 可滑动视图

  • 开始新的锻炼

  • 暂停锻炼

  • 结束锻炼

  • 锁定锻炼

  • 目标

  • 不同的锻炼

在列出理想功能后,确定哪些是必需的至关重要。理解我们产品的最终用途是关键。对我来说,创建这个健身伴侣应用程序的目的是为了提供一个坚实的基础,以便以后添加更高级的功能。我们不会实现前面列表中的所有功能,因为这有利于尝试自己实现它们,作为将所学概念付诸实践的任务。因此,以下是我们将实现的核心功能:

  • 当前时间

  • 活动/锻炼时间

  • 每分钟心率(BPM)

  • 总消耗卡路里

  • 活动

  • 可滑动视图

  • 开始新的锻炼图标

  • 暂停锻炼图标

  • 结束锻炼图标

  • 锁定锻炼图标

读完本章和下一章后,你将准备好独立处理剩余的功能。下一节将概述我们应用程序的验收标准,为你提供确保其成功的必要指南。

我们应用程序的验收标准

在本节中,我们将概述我们应用程序的强制性要求。这些要求对于最终产品至关重要,并且必须是可衡量的。我们需要确保满足这些要求,以交付一个成功的应用程序。让我们开始吧:

  • 当前时间 – 这将显示你的时区实际时间。

  • 活动/锻炼时间 – 这将是一个实时计时器,显示当前的锻炼时间。

  • 每分钟心率(BPM) – 这个标签将链接到一个变量,以显示用户每分钟的心跳次数。

  • 总消耗卡路里 – 这将显示消耗的卡路里,并将链接到卡路里变量。

  • 活动 – 这将用于显示当前活动;例如,跑步、游泳、瑜伽等。

  • 可滑动视图 – 这将允许我们在单页上有两个独立的屏幕,并根据我们的需求增加。

  • 开始新的锻炼 – 一个由图像和文本项组成的按钮将允许用户开始锻炼。

  • 暂停锻炼图标 – 这个按钮也将由图像和文本项组成,将用于暂停锻炼。

  • 结束锻炼图标 – 另一个按钮,与前面的两个类似,将用于结束锻炼。

  • 锁定锻炼图标 – 最后,这个按钮将用于锁定锻炼。

为了确保应用程序的成功,开发测试用例来衡量验收标准至关重要。这些测试用例应模拟最终用户使用应用程序的真实场景和条件。通过这样做,我们可以准确测量应用程序需要达到的性能水平,以便被认为是成功的。因此,创建详细的测试用例或场景是确保应用程序达到预期标准所必需的。

我们应用的线框

线框是设计布局的一个基本工具。它提供了布局外观的概述。健身应用当前活动的线框如图所示:

图 8.1 – 我们手表应用的线框

图 8.1 – 我们手表应用的线框

以下图显示了允许你开始、停止和暂停活动的视图线框:

图 8.2 – 活动按钮线框

图 8.2 – 活动按钮线框

我们现在已经看到了我们健身应用的线框。这些线框将作为构建我们应用程序 UI 的初始基础。

在下一节中,我们将构建我们应用程序的界面,并验证它是否与我们在线框中创建的设计相匹配。虽然我们将遵循相同的过程,但可能会有细微的差异。我们本章的重点将是初始视图,第二个视图将在下一章中讨论。

构建健身应用

我们现在将构建侧边栏的 UI。首先,让我们创建我们的项目。按照以下步骤操作:

  1. 打开 Xcode 并选择创建一个新的 Xcode 项目

图 8.3 – 创建一个新的 Xcode 项目

图 8.3 – 创建一个新的 Xcode 项目

  1. 现在我们将选择我们应用程序的模板。由于我们正在创建一个 Apple Watch 应用程序,我们将从顶部选择WatchOS,然后选择应用,并点击下一步

图 8.4 – Xcode 项目模板选择

图 8.4 – Xcode 项目模板选择

  1. 现在我们将选择我们项目的选项。在这里,只有一个关键的选择/设置。确保仅查看应用被选中:

图 8.5 – Xcode 项目选项

图 8.5 – Xcode 项目选项

  1. 一旦你按下下一步,你可以选择在哪里创建你的项目,如图所示:

图 8.6 – Xcode 项目保存目录

图 8.6 – Xcode 项目保存目录

  1. 一旦你找到了你想要创建它的位置,点击右下角的创建。Xcode 以其全部荣耀显示了你的项目,如图所示:

图 8.7 – 新 Xcode 项目概览

图 8.7 – 新 Xcode 项目概览

在本节中,我们设置了我们的 WatchOS 项目。现在我们已经设置好了,我们将实现健身应用第一页的界面。

活动详情

在本节中,我们将实现健身应用的第一个页面,它将代表当前活动的详细信息。作为提醒,请参考图 8**.1 来查看其外观。

当前活动屏幕有五个主要元素。作为一个小任务,看看你是否能找出它们是什么。如果你不知道确切的 UI 组件名称,不要担心;我们将在下一节中查看这些组件。

文本

文本组件可以显示一串字符、数字,甚至图标,所有这些都可以相互结合使用。对我们来说,它将用于显示以下五个组件:

  1. 当前时间:

图 8.8 – 当前时间标签

图 8.8 – 当前时间标签

  1. 活动运行时间:

图 8.9 – 活动时间标签

图 8.9 – 活动时间标签

  1. 每分钟节拍(BPM):

图 8.10 – BPM 标签

图 8.10 – BPM 标签

  1. 消耗卡路里:

图 8.11 – 卡路里标签

图 8.11 – 卡路里标签

  1. 当前活动:

图 8.12 – 当前活动标签

图 8.12 – 当前活动标签

重要提示

要重命名视图,请参考前一章中的重命名视图部分以回顾该概念。

在本节中,我们分析了验收标准以及我们的健身伴侣应用的要求。我们还分解了线框图,使我们能够确定应用程序的工作方式和结构。我们将利用这些知识在下一节中进行。

实现当前活动 UI

在本节中,我们将使用新设置的项目开始我们的健身手表应用的编码。我们将实现当前活动 UI,它将显示当前活动的信息。

由于我们创建了一个全新的项目,编码标准并不符合我的个人偏好。因此,首先,我将更改标准。请随意花几分钟时间做同样的事情。

如果你直接运行新创建的应用程序,你会注意到我们已经在右上角显示了当前时间,如下面的截图所示:

图 8.13 – 默认时间

图 8.13 – 默认时间

对我们来说是个好消息,因为时间已经默认设置好了。可能存在你想移除时间的情况,但既然我们不这么做,我们可以继续。现在,我们将继续实现当前活动的文本项。实现剩余的文本项实际上非常简单。从 VStack 中移除所有代码,并添加以下标签:

var body: some View{
    VStack( )
    {
        Text( "00:10:44" )
        Text( "120 BPM" )
        Text( "110 Calories" )
        Text( "Running" )
    }
    .padding( )
}

这将导致以下布局:

图 8.14 – 添加的文本项

图 8.14 – 添加的文本项

首先,我们将设置 VStack 的对齐方式为左对齐,如下所示:

VStack( alignment: .leading )

尽管所有内容都存在,但它既不动态也不具有样式。首先,我们将使其变得动态。为此,创建五个变量来存储以下内容:

  • 时间计数器 – 这将每秒增加一次;它可以修改为更频繁的计数。

  • 计时字符串 – 这将使用时间计数器并将其转换为 00:00:00 格式。

  • BPM – 这将存储 BPM 数值。

  • 卡路里 – 这将存储锻炼过程中消耗的卡路里数。

  • 活动 – 这将通知用户哪个锻炼是活跃的。

以下是相应的代码:

@State private var counter = 0@State private var timerString = "00:00:00"
@State private var bpm = 120
@State private var calories = 110
@State private var activity = "Running"

接下来,我们将创建一个每秒运行一次的计时器:

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

现在,是时候将这些变量和计时器链接到适当的组件了。首先,向计时器文本组件添加一个onReceive事件,如下所示:

Text( timerString )    .onReceive( timer )
    { time in
        counter += 1
        let hours = counter / 3600
        let minutes = ( counter % 3600 ) / 60
        let seconds = counter % 3600 % 60
        timerString = String( hours ) + ":"
+ String( minutes ) + ":" +
String( seconds )
    }

让我们看看我们刚才做了什么。onReceive事件将计时器作为参数,用于观察计时器触发的发布事件频率。在每次遍历中,我们将计数器增加一,因此计数器是已过去秒数的数量。然后,我们为小时、分钟和秒创建常量。我们进行一些简单的数学计算,以确定计时器已经运行了多少小时、分钟和秒。最后,我们将timerString格式化以显示小时:分钟:秒。运行应用程序将显示以下结果:

图 8.15 – 动态链接的变量

图 8.15 – 动态链接的变量

你可能会注意到,时间目前是以0:0:0的格式显示,而不是00:00:00。修复这个问题非常简单;我们需要在每个字符串上添加一个格式化器,以便使用两位小数格式化小时、分钟和秒。更新timerString如下:

timerString = String( format: "%02d", hours ) + ":" + String( format: "%02d", minutes ) + ":" + String( format: "%02d", seconds )

现在,应用程序将显示如下:

图 8.16 – 格式化的 timerString

图 8.16 – 格式化的 timerString

现在看起来更好了。BPM、卡路里和当前活动标签都很好;我们只需要修改当前活动时间标签。需要做三件事:将其放大,将颜色改为黄色,并在下方添加一些填充。完成所有这些都很简单。修改当前活动时间文本项,如下所示:

Text( timerString )    .font( .title2 )
    .foregroundColor( Color.yellow )
    .padding( .bottom )
    .onReceive( timer ) …

一旦运行应用程序,我们将看到我们已经完成了本章的代码。

图 8.17 – 当前活动时间样式化

图 8.17 – 当前活动时间样式化

在本节中,我们为我们的健身伴侣应用程序实现了当前活动屏幕。你了解到,尽管 Apple WatchOS 是四个 SDK 中最新的,看起来有些困难,但实际上它和其它的一样简单易用。在下一章中,我们将实现一个滑动视图来添加活动按钮屏幕。

摘要

在本章中,我们讨论了我们的健身伴侣应用程序的设计。我们研究了线框图,并将每个元素分解为 SwiftUI 组件。然后,我们将 SwiftUI 组件实现以匹配线框图中的设计,用于当前活动屏幕。我们还研究了构建此应用程序的需求和设计规范,然后将其简化为应用程序将提供的核心功能。

接下来,我们通过创建线框图并将每个元素分解为 SwiftUI 组件来设计我们的健身伴侣应用程序。我们将这些组件实现以匹配当前活动屏幕的线框图设计。我们还审查了构建应用程序的需求和设计规范,并将其简化以专注于它将提供的核心功能。总有一些功能是很好但不可避免地要从首次发布中裁剪掉的,或者许多人称之为 MVP最小可行产品)。这正是我们所做的。限制范围以防止其变得过大且失控是至关重要的。

在下一章中,我们将探讨实现健身伴侣应用程序的活动按钮屏幕。

第九章:智能手表项目 – 健身伴侣 UI

在本章中,我们将为健身伴侣项目实现活动按钮屏幕。在前一章中,我们研究了健身伴侣的设计,特别是 当前活动 屏幕设计。然后我们将屏幕分解为所有必需的组件。然后我们使用 SwiftUI 实现了所有组件。在前一章的结尾,我们只有一个无法滑动的单屏。主要部分将是可滑动的,向用户提供控制当前活动的按钮列表。然后,我们将分析活动按钮屏幕,将其分解为其组成的所有组件,并实现所有组件以提供类似健身应用程序的感觉。

本章将分为以下部分:

  • 活动按钮屏幕概述

  • 实现活动按钮屏幕

  • 额外任务

到本章结束时,你将创建一个适用于 WatchOS 的健身伴侣应用程序。这将作为一个模板,带有可滑动的屏幕,向用户展示活动信息。它将作为进一步扩展健身应用程序或使用我们已实现的内核结构将项目转向不同方向的一个坚实基础。随着我们接近本章的结尾,我将提供练习,以在健身伴侣应用程序中实现更多高级功能。这将本书的第四个也是最后一个项目,为你提供一个使用 Swift 进行 iOS UI 开发的 360 度视角。

技术要求

本章要求你从 Apple 的 App Store 下载 Xcode 14 或更高版本。

要安装 Xcode,只需在 App Store 中搜索 Xcode,选择并下载最新版本。打开 Xcode 并遵循任何额外的安装说明。一旦 Xcode 打开并启动,你就可以开始了。

Xcode 14 版本具有以下功能/要求:

  • 它包括 iOS 16、iPadOS 16、macOS 12.3、tvOS 16 和 watchOS 9 的 SDK

  • 它支持在 iOS 11 或更高版本、tvOS 11 或更高版本和 watchOS 4 或更高版本上的设备调试

  • 你需要一个运行 macOS Monterey 12.5 或更高版本的 Mac

从以下 GitHub 链接下载示例代码:

github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

活动按钮屏幕概述

在本节中,我们将再次查看第 8 章 中的线框,并将其分解为其组成部分。以下图展示了活动按钮屏幕:

图 9.1 – 活动按钮屏幕

图 9.1 – 活动按钮屏幕

在我们编写应用程序代码之前,我们将把活动按钮屏幕分解成构成它的元素。作为一个小任务,看看你是否能弄清楚这些是什么。如果你不知道确切的 UI 组件名称,不要担心;我们将在下一节中查看组件。

图像组件

图像组件是 SwiftUI 提供的核心组件之一。它允许你显示图像,可以用来提供视觉表示或辅助文本内容。我们将使用它来显示控制当前活动的按钮图标。以下图示显示了应用程序中的图标:

图 9.2 – 锁定图像

图 9.2 – 锁定图像

图 9.3 – 新图像

图 9.3 – 新图像

图 9.4 – 结束图像

图 9.4 – 结束图像

图 9.5 – 暂停图像

图 9.5 – 暂停图像

这些图像不仅包含图标,还包含背景。这是在本章后面将进一步探讨的内容。

文本组件

我们将使用文本组件来显示按钮标题。有关更多信息,请参阅第二章

在下一节中,我们将使用前几节中讨论的组件来实现活动按钮屏幕的代码。

实现活动按钮屏幕

在本节中,我们将实现我们的应用程序的活动按钮屏幕,从而完成本书的第四个也是最后一个项目。在我们这样做之前,我们必须实现一个可滑动页面系统。第一页将包含上一章的实现,第二页将是活动按钮。当然,你可以使用它扩展到所需的任何页面数。

可滑动页面

在本节中,我们将实现我们的可滑动页面。幸运的是,在 SwiftUI 中实现尽可能多的功能非常简单。只需将我们的当前VStack包裹在MainView内部的TabView中,如下所示:

TabView{
    VStack( alignment: .leading )
    {
        Text( timerString )
            .font( .title2 )
            .foregroundColor( Color.yellow )
            .padding( .bottom )
            .onReceive( timer )
        { time in
            counter += 1
            let hours = counter / 3600
            let minutes = ( counter % 3600 ) / 60
            let seconds = counter % 3600 % 60
            timerString = String( format: "%02d", hours ) + ":"
+ String( format: "%02d", minutes ) + ":" +
String( format: "%02d", seconds )
        }
        Text( String( bpm ) + " BPM" )
        Text( String( calories ) + " Calories" )
        Text( activity )
    }
    .padding( )
}

在前面的代码中,我们实现了一个TabView,用于在我们的健身伴侣应用程序中创建多个页面。

现在,如果你运行它,看起来会一样。然而,如果你尝试滑动屏幕,你会注意到一点反弹。这是因为只有一个页面。现在,让我们添加一个虚拟的第二页来帮助我们测试我们新的TabView。在VStack之后,添加一个Text组件,如下所示:

TabView{
    VStack( alignment: .leading )
    {
        …
    }
    .padding( )
    Text( "Second Page" )
}

现在我们已经在我们的TabView中实现了第二页。如果你尝试在页面上滑动,它会跳转到下一页。

TabView内的每个视图都被视为一个单独的页面。老实说,就这么简单。运行我们的应用程序将产生以下结果,显示底部有两个点,表示有两个页面:

图 9.6 – 第一页

图 9.6 – 第一页

从右向左滑动将显示第二页,如下所示:

图 9.7 – 第二页

图 9.7 – 第二页

在本节中,我们使用 TabView 组件添加了一个额外的页面。这使得我们能够添加另一个用户可以通过滑动手势导航的页面。在下一节中,我们将添加活动按钮。

活动按钮

在本节中,我们将实现 TabView 的第二页上的活动按钮。我们将为每个按钮的背景和图标本身使用自定义颜色。

让我们继续创建这些自定义颜色:

  1. 导航到项目导航器中的资产部分:

图 9.8 – 资产文件夹

图 9.8 – 资产文件夹

  1. 资产部分,右键单击空白区域并选择新建 颜色集

图 9.9 – 新建颜色集按钮

图 9.9 – 新建颜色集按钮

  1. 属性检查器中设置颜色的名称:

图 9.10 – 新建颜色集按钮

图 9.10 – 新建颜色集按钮

  1. 选择任何外观深色来设置颜色。这确保了在所有颜色模式下,将使用所需的颜色:

图 9.11 – 任何外观

图 9.11 – 任何外观

  1. 现在确保内容设置为sRGB输入方法设置为8 位十六进制。然后设置十六进制值:

图 9.12 – 设置颜色

图 9.12 – 设置颜色

  1. 重复以上步骤为所有列出的颜色:

    • endColour

    • 十六进制颜色值:#FF161C

  2. endColourBackground

  3. 十六进制颜色值:#390B0C

  4. lockColour

  5. 十六进制颜色值:#06F5E7

  6. lockColourBackground

  7. 十六进制颜色值:#113330

  8. newColour

  9. 十六进制颜色值:#86FE01

  10. newColourBackground

  11. 十六进制颜色值:#1E3400

  12. pauseColour

  13. 十六进制颜色值:#BBA700

  14. pauseColourBackground

  15. 十六进制颜色值:#342F00

  16. 完成后,资产屏幕应如下所示:

图 9.13 – 添加的颜色

图 9.13 – 添加的颜色

尽管颜色已经创建,但我们不能直接在我们的代码中使用它们。让我们解决这个问题。这样做相当简单。在 MainView 中,我们将扩展 Color 功能以支持我们的颜色。在这样做之前,我认为提到为什么我在提到功能时将颜色拼写成这样是谨慎的。这是美式拼写,并且在 Swift 中使用。为了扩展功能,我们必须这样拼写,但因为我来自英国,我个人使用的拼写是 Colour。现在澄清了这一点。让我们扩展 Swift 颜色。在 MainView 结构之上添加以下代码:

extension Color{
    static let lockColour = Color( "lockColour" )
    static let lockColourBackground = Color( "lockColourBackground" )
    static let newColour = Color( "newColour" )
    static let newColourBackground = Color( "newColourBackground" )
    static let endColour = Color( "endColour" )
    static let endColourBackground = Color( "endColourBackground" )
    static let pauseColour = Color( "pauseColour" )
    static let pauseColourBackground = Color( "pauseColourBackground" )
}

尽管我们已经将其添加到MainView中,但扩展 Swift 颜色允许我们在项目的任何地方使用它。这也值得提一下,这意味着仅限于我们的项目,并且不会超出我们项目的范围扩展到其他项目。我们的项目很小,所以将其放在MainView内部是完全可以接受的。然而,将此类扩展放在特定文件中是常见的做法。如果有许多颜色扩展,它们可以有自己的颜色文件。这超出了本项目范围。

下面是对前面代码的简要概述:我们通过使用之前在资产中设置的名称的静态变量扩展了颜色。

重要提示

变量名称不需要与颜色名称相同。但保持它们相同是良好的实践。这使得它们更容易维护。

每个按钮由三个组件组成:

  • 背景

  • 图标

  • 文本

对于背景,我们将使用一个矩形组件。更具体地说,我们将使用圆角矩形组件,因为它允许我们设置圆角半径。请随意更改设计并使用矩形或任何其他形状。对于图标,我们将使用图像组件并使用内置图标。请随意使用您自己的图像或查看之前讨论的 SF Symbols,在第六章实现侧边栏,查看所有内置图标。文本是所有组件中最简单的,将使用基本的文本组件。

按钮最难的部分是背景和图标。这是因为它们是重叠的。文本放置在图像下方,这使得添加变得容易。我们最初将专注于获取背景和图标的代码。我们将使用ZStack将图标放置在背景上方。用以下代码替换第二页的占位符文本组件:

ZStack{
    RoundedRectangle( cornerRadius: 18, style:
.continuous )
}

我们创建了一个圆角半径为 18 的圆角矩形。请随意增加数字以获得更圆滑的圆角,或降低它。将样式设置为.continuous会使圆角看起来更平滑,这始终是好事。让我们看看这会产生什么:

图 9.14 – 简单圆角矩形

图 9.14 – 简单圆角矩形

目前,它看起来与本章开头所示图中的按钮没有任何相似之处。只是缺少两件事——背景颜色和使尺寸更小。我们将使用之前创建的lockColourBackground颜色,并将尺寸设置为宽度70和高度64

ZStack{
    RoundedRectangle( cornerRadius: 18, style:
.continuous )
        .foregroundColor( .lockColourBackground )
        .frame( width: 70, height: 64 )
}

下面的图显示了图像背景:

图 9.15 – 圆角颜色样式

图 9.15 – 圆角颜色样式

背景现在看起来更像我们的设计。下一步是向矩形内添加图标。这样做很简单。在圆角矩形组件之后添加带有图标的图像:

ZStack{
    RoundedRectangle( cornerRadius: 18, style:
.continuous )
        .foregroundColor( .lockColourBackground )
        .frame( width: 70, height: 64 )
    Image( systemName: "drop.fill" )
}

运行应用程序现在将产生以下结果:

图 9.16 – 背景中的泪滴图标

图 9.16 – 背景中的泪滴图标

我们使用SF Symbols获取泪滴图标。请随意使用您认为合适的任何图标,甚至您自己的图像。我们需要更改图标样式。有两个主要方面需要更新,即颜色和大小。按照以下方式更新图像:

Image( systemName: "drop.fill" )    .resizable( )
    .foregroundColor( .lockColour )
    .aspectRatio( contentMode: .fit )
    .frame( width: 16 )

我们首先将其设置为resizable,这允许我们更改大小。接下来,我们使用之前创建的一种颜色设置颜色。然后,我们确保宽高比设置为适合,这样我们就可以在不扭曲图像的情况下调整大小。最后,我们设置大小,因为我们有一个固定的宽高比。设置宽度会自动设置相应的高度。运行应用程序会产生以下结果:

图 9.17 – 锁定按钮

图 9.17 – 锁定按钮

接下来,我们将为我们的按钮添加文本。文本不在图标或矩形内部,而是位于其下方。但我们仍然希望它可点击,因此我们将所有按钮内容包裹在一个VStack中,并添加Text组件,如下所示:

VStack{
    ZStack
    {
        RoundedRectangle( cornerRadius: 18, style:
.continuous )
            .foregroundColor( .lockColourBackground )
            .frame( width: 70, height: 64 )
        Image( systemName: "drop.fill" )
            .resizable( )
            .foregroundColor( .lockColour )
            .aspectRatio( contentMode: .fit )
            .frame( width: 16 )
    }
    Text( "Lock" )
}

将按钮包裹在VStack中的原因有两个:

  • 我们希望所有内容都可点击(将在下一部分实现)。

  • 由于将有多个按钮,VStack在技术上是没有图标的按钮,没有矩形或文本组件。非常酷!

让我们使VStack可点击,然后我们将查看结果。首先,我们需要一个函数来调用它。我们可以使用内联函数,但我们将创建一个专用函数。这为我们提供了代码库中的良好抽象。在主体之前,添加以下代码:

func Lock( ){ print( "Lock button is pressed" ); }
var body: some View
{
     ...
}

这个函数很简单。点击时,会在终端记录一条消息。现在用onTapGesture函数更新VStack

VStack{
    ZStack
    {
        RoundedRectangle( cornerRadius: 18, style:
.continuous )
            .foregroundColor( .lockColourBackground )
            .frame( width: 70, height: 64 )
        Image( systemName: "drop.fill" )
            .resizable( )
            .foregroundColor( .lockColour )
            .aspectRatio( contentMode: .fit )
            .frame( width: 16 )
    }
    Text( "Lock" )
}.onTapGesture { Lock( ) }

这已经很多了。让我们运行我们的应用程序并查看结果:

图 9.18 – 完成的锁定按钮

图 9.18 – 完成的锁定按钮

随意点击按钮。它将记录一条消息。在完成这个项目并实现剩余的按钮之前,以下是到目前为止的代码:

////  ContentView.swift
//  Fitness Companion Watch App
//
//  Created by Frahaan on 03/04/2023.
//
import SwiftUI
extension Color
{
    static let lockColour = Color( "lockColour" )
    static let lockColourBackground = Color( "lockColourBackground" )
    static let newColour = Color( "newColour" )
    static let newColourBackground = Color( "newColourBackground" )
    static let endColour = Color( "endColour" )
    static let endColourBackground = Color( "endColourBackground" )
    static let pauseColour = Color( "pauseColour" )
    static let pauseColourBackground = Color( "pauseColourBackground" )
}
struct MainView: View
{
    @State private var counter = 0
    @State private var timerString = "00:00:00"
    @State private var bpm = 120
    @State private var calories = 110
    @State private var activity = "Running"
    let timer = Timer.publish( every: 1, on: .main, in: .common ).autoconnect( )
    func Lock( )
    { print( "Lock button is pressed" ); }
    var body: some View
    {
        TabView
        {
            VStack( alignment: .leading )
            {
                Text( timerString )
                    .font( .title2 )
                    .foregroundColor( Color.yellow )
                    .padding( .bottom )
                    .onReceive( timer )
                { time in
                    counter += 1
                    let hours = counter / 3600
                    let minutes = ( counter % 3600 ) / 60
                    let seconds = counter % 3600 % 60
                    timerString = String( format: "%02d", hours ) + ":" + String( format: "%02d", minutes ) + ":" + String( format: "%02d", seconds )
                }
                Text( String( bpm ) + " BPM" )
                Text( String( calories ) + " Calories" )
                Text( activity )
            }
            .padding( )
            VStack
            {
                ZStack
                {
                    RoundedRectangle( cornerRadius: 18, style: .continuous )
                        .foregroundColor( .lockColourBackground )
                        .frame( width: 70, height: 64 )
                    Image( systemName: "drop.fill" )
                        .resizable( )
                        .foregroundColor( .lockColour )
                        .aspectRatio( contentMode: .fit )
                        .frame( width: 16 )
                }
                Text( "Lock" )
            }.onTapGesture { Lock( ) }
        }
    }
}
struct MainView_Previews: PreviewProvider
{
    static var previews: some View
    {
        MainView( )
    }
}

我们几乎完成了这一章;唯一剩下的事情是添加剩余的按钮。首先,让我们在主体上方添加剩余的功能回调,如下所示:

func Lock( ){ print( "Lock button is pressed" ); }
func New( )
{ print( "New button is pressed" ); }
func End( )
{ print( "End button is pressed" ); }
func Pause( )
{ print( "Pause button is pressed" ); }
var body: some View
{
    ...
}

现在我们已经实现了回调函数,我们将实现按钮。每个按钮本身实际上很简单,因为它与锁定按钮相同,但有一些以下更改:

  • RoundedRectangleforegroundColor

  • 图像的图标

  • 图像的foregroundColor

  • 文本

  • onTapGesture回调

仅复制VStack代码实际上就是复制按钮。我们会得到以下结果:

图 9.19 – 添加的额外按钮

图 9.19 – 添加的额外按钮

如果你没有看到差异,我不怪你。很难看到实际上发生了什么,但如果你看屏幕底部,有五个点,表示现在有五个页面。记住,在实现第二个页面时,我们声明根中的每个组件都将是一个页面。因此,我们想要通过将它们放入一个 2x2 网格中来将这些按钮组合在一起。为了在同一行上组织组件,我们可以使用HStack。我们只需要一行上的两个,所以首先,我们将前两个按钮,即VStacks放入一个HStack中。这将导致三个页面,第一个是上一章中实现的页面,以及每个HStack的两个页面,这不是我们想要的。一个小改动就可以修复。你能猜到是什么吗?只需将两个HStacks放入一个单独的VStack中。这将使它们堆叠在一起,从而形成一个网格。这个代码如下:

VStack{
    HStack
    {
        VStack
        {
            ZStack
            {
                RoundedRectangle( cornerRadius: 18, style: .continuous )
                    .foregroundColor( .lockColourBackground )
                    .frame( width: 70, height: 64 )
                Image( systemName: "drop.fill" )
                    .resizable( )
                    .foregroundColor( .lockColour )
                    .aspectRatio( 1.0, contentMode: .fit )
                    .frame( width: 32 )
            }
            Text( "Lock" )
        }.onTapGesture { Lock( ) }
        VStack
        {
            ZStack
            {
                RoundedRectangle( cornerRadius: 18, style: .continuous )
                    .foregroundColor( .newColourBackground )
                    .frame( width: 70, height: 64 )
                Image( systemName: "plus" )
                    .resizable( )
                    .foregroundColor( .newColour )
                    .aspectRatio( 1.0, contentMode: .fit )
                    .frame( width: 32 )
            }
            Text( "New" )
        }.onTapGesture { New( ) }
    }
    HStack
    {
        VStack
        {
            ZStack
            {
                RoundedRectangle( cornerRadius: 18, style: .continuous )
                    .foregroundColor( .endColourBackground )
                    .frame( width: 70, height: 64 )
                Image( systemName: "xmark" )
                    .resizable( )
                    .foregroundColor( .endColour )
                    .aspectRatio( 1.0, contentMode: .fit )
                    .frame( width: 32 )
            }
            Text( "End" )
        }.onTapGesture { End( ) }
        VStack
        {
            ZStack
            {
                RoundedRectangle( cornerRadius: 18, style: .continuous )
                    .foregroundColor( .pauseColourBackground )
                    .frame( width: 70, height: 64 )
                Image( systemName: "pause" )
                    .resizable( )
                    .foregroundColor( .pauseColour )
                    .aspectRatio( 1.0, contentMode: .fit )
                    .frame( width: 32 )
            }
            Text( "Pause" )
        }.onTapGesture { Pause( ) }
    }
}

是时候运行我们的应用程序了:

图 9.20 – 按钮网格系统

图 9.20 – 按钮网格系统

你可能已经准备好完成这一章,然后结束这一天。请耐心等待——网格的顶部对我来说太靠近时间了。让我们添加我们刚刚实现的顶部VStack

.padding( .top, 20.0 )

这就是它,我们应用程序的最终构建和运行——鼓掌,请!

图 9.21 – 按钮网格系统

图 9.21 – 按钮网格系统

我们现在到了项目的结尾,我们的应用程序看起来很棒。在我们总结之前,请随意访问 GitHub 仓库以双重检查你的代码库:github.com/PacktPublishing/Elevate-SwiftUI-Skills-by-Building-Projects

在本节中,我们添加了活动按钮。我们通过在我们的健身应用程序中实现另一个页面来做到这一点。我们利用基于网格的系统来布局我们的按钮。我们使用了各种核心组件与堆叠相结合来组织它们。在下一节中,我们将总结本章,最终总结这本书。但首先,我们将查看一些代码来帮助你完成额外任务。

不同的练习

要将不同的练习添加到健身伴侣应用程序中,你可以通过引入一个新的数据结构来存储练习信息并相应地更新 UI 来修改MainView。以下是如何进行这些更改的示例:

import SwiftUIextension Color {
    static let lockColour = Color("lockColour")
    static let lockColourBackground = Color("lockColourBackground")
    static let newColour = Color("newColour")
    static let newColourBackground = Color("newColourBackground")
    static let endColour = Color("endColour")
    static let endColourBackground = Color("endColourBackground")
    static let pauseColour = Color("pauseColour")
    static let pauseColourBackground = Color("pauseColourBackground")
}
struct Exercise {
    let name: String
    let image: String
}
struct MainView: View {
    @State private var counter = 0
    @State private var timerString = "00:00:00"
    @State private var bpm = 120
    @State private var calories = 110
    @State private var activity = "Running"
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    let exercises = [
        Exercise(name: "Running", image: "person.running"),
        Exercise(name: "Cycling", image: "bicycle"),
        Exercise(name: "Swimming", image: "figure.walk"),
        // Add more exercises here
    ]
    func Lock() {
        print("Lock button is pressed")
    }
    func New() {
        print("New button is pressed")
    }
    func End() {
        print("End button is pressed")
    }
    func Pause() {
        print("Pause button is pressed")
    }
    var body: some View {
        TabView {
            VStack(alignment: .leading) {
                Text(timerString)
                    .font(.title2)
                    .foregroundColor(Color.yellow)
                    .padding(.bottom)
                    .onReceive(timer) { time in
                        counter += 1
                        let hours = counter / 3600
                        let minutes = (counter % 3600) / 60
                        let seconds = counter % 3600 % 60
                        timerString = String(format: "%02d", hours) + ":" + String(format: "%02d", minutes) + ":" + String(format: "%02d", seconds)
                    }
                Text(String(bpm) + " BPM")
                Text(String(calories) + " Calories")
                Text(activity)
            }
            .padding()
            VStack {
                HStack {
                    ForEach(exercises, id: \.name) { exercise in
                        VStack {
                            ZStack {
                                RoundedRectangle(cornerRadius: 18, style: .continuous)
                                    .foregroundColor(.newColourBackground)
                                    .frame(width: 70, height: 64)
                                Image(systemName: exercise.image)
                                    .resizable()
                                    .foregroundColor(.newColour)
                                    .aspectRatio(contentMode: .fit)
                                    .frame(width: 32)
                            }
                            Text(exercise.name)
                        }
                        .onTapGesture {
                            activity = exercise.name
                        }
                    }
                }
                HStack {
                    VStack {
                        ZStack {
                            RoundedRectangle(cornerRadius: 18, style: .continuous)
                                .foregroundColor(.lockColourBackground)
                                .frame(width: 70, height: 64)
                            Image(systemName: "lock.fill")
                                .resizable()
                                .foregroundColor(.lockColour)
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 32
)
                        }
                        Text("Lock")
                    }.onTapGesture { Lock() }
                    VStack {
                        ZStack {
                            RoundedRectangle(cornerRadius: 18, style: .continuous)
                                .foregroundColor(.endColourBackground)
                                .frame(width: 70, height: 64)
                            Image(systemName: "xmark")
                                .resizable()
                                .foregroundColor(.endColour)
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 32)
                        }
                        Text("End")
                    }.onTapGesture { End() }
                    VStack {
                        ZStack {
                            RoundedRectangle(cornerRadius: 18, style: .continuous)
                                .foregroundColor(.pauseColourBackground)
                                .frame(width: 70, height: 64)
                            Image(systemName: "pause")
                                .resizable()
                                .foregroundColor(.pauseColour)
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 32)
                        }
                        Text("Pause")
                    }.onTapGesture { Pause() }
                }
            }
            .padding(.top, 20.0)
        }
    }
}
struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

在这个修改后的代码中,我们引入了一个新的Exercise结构体,用于存储每个练习的名称和图片名称。你可以通过创建新的Exercise实例来向exercises数组添加更多练习。

在视图中,我们使用了ForEach循环来遍历练习并动态显示它们。每个练习由一个包含图片和文本标签的VStack表示。当点击一个练习时,活动状态会更新为所选练习的名称。

你可以根据具体需求自定义练习图片,并向Exercise结构体添加更多属性。

活动计时器

要添加一个可以启动、停止和暂停的活动计时器的功能,您可以通过引入额外的状态变量和操作来修改MainView。以下是如何进行这些更改的示例:

import SwiftUIstruct Exercise {
    let name: String
    let image: String
}
struct MainView: View {
    @State private var counter = 0
    @State private var isTimerRunning = false
    @State private var isTimerPaused = false
    @State private var timerString = "00:00:00"
    @State private var bpm = 120
    @State private var calories = 110
    @State private var activity = "Running"
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    let exercises = [
        Exercise(name: "Running", image: "person.running"),
        Exercise(name: "Cycling", image: "bicycle"),
        Exercise(name: "Swimming", image: "figure.walk"),
        // Add more exercises here
    ]
    func lock() {
        print("Lock button is pressed")
    }
    func startTimer() {
        isTimerRunning = true
        isTimerPaused = false
    }
    func pauseTimer() {
        isTimerRunning = false
        isTimerPaused = true
    }
    func stopTimer() {
        isTimerRunning = false
        isTimerPaused = false
        counter = 0
        timerString = "00:00:00"
    }
    var body: some View {
        TabView {
            VStack(alignment: .leading) {
                Text(timerString)
                    .font(.title2)
                    .foregroundColor(Color.yellow)
                    .padding(.bottom)
                    .onReceive(timer) { time in
                        if isTimerRunning && !isTimerPaused {
                            counter += 1
                            let hours = counter / 3600
                            let minutes = (counter % 3600) / 60
                            let seconds = counter % 3600 % 60
                            timerString = String(format: "%02d", hours) + ":" + String(format: "%02d", minutes) + ":" + String(format: "%02d", seconds)
                        }
                    }
                Text(String(bpm) + " BPM")
                Text(String(calories) + " Calories")
                Text(activity)
            }
            .padding()
            VStack {
                HStack {
                    ForEach(exercises, id: \.name) { exercise in
                        VStack {
                            ZStack {
                                RoundedRectangle(cornerRadius: 18, style: .continuous)
                                    .foregroundColor(.newColourBackground)
                                    .frame(width: 70, height: 64)
                                Image(systemName: exercise.image)
                                    .resizable()
                                    .foregroundColor(.newColour)
                                    .aspectRatio(contentMode: .fit)
                                    .frame(width: 32)
                            }
                            Text(exercise.name)
                        }
                        .onTapGesture {
                            activity = exercise.name
                        }
                    }
                }
                HStack {
                    VStack {
                        ZStack {
                            RoundedRectangle(cornerRadius: 18, style: .continuous)
                                .foregroundColor(.lockColourBackground)
                                .frame(width: 70, height: 64)
                            Image(systemName: "lock.fill")
                                .resizable()
                                .foregroundColor(.lockColour)
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 32)
                        }
                        Text("Lock")
                    }
                    .onTapGesture { lock() }
                    VStack {
                        if isTimerRunning
 {
                            Button(action: pauseTimer) {
                                ZStack {
                                    RoundedRectangle(cornerRadius: 18, style: .continuous)
                                        .foregroundColor(.pauseColourBackground)
                                        .frame(width: 70, height: 64)
                                    Image(systemName: "pause")
                                        .resizable()
                                        .foregroundColor(.pauseColour)
                                        .aspectRatio(contentMode: .fit)
                                        .frame(width: 32)
                                }
                            }
                            .buttonStyle(PlainButtonStyle())
                        } else {
                            Button(action: startTimer) {
                                ZStack {
                                    RoundedRectangle(cornerRadius: 18, style: .continuous)
                                        .foregroundColor(.pauseColourBackground)
                                        .frame(width: 70, height: 64)
                                    Image(systemName: "play.fill")
                                        .resizable()
                                        .foregroundColor(.pauseColour)
                                        .aspectRatio(contentMode: .fit)
                                        .frame(width: 32)
                                }
                            }
                            .buttonStyle(PlainButtonStyle())
                        }
                        Text(isTimerRunning ? "Pause" : "Start")
                    }
                    VStack {
                        ZStack {
                            RoundedRectangle(cornerRadius: 18, style: .continuous)
                                .foregroundColor(.endColourBackground)
                                .frame(width: 70, height: 64)
                            Image(systemName: "xmark")
                                .resizable()
                                .foregroundColor(.endColour)
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 32)
                        }
                        Text("End")
                    }
                    .onTapGesture { stopTimer() }
                }
            }
            .padding(.top, 20.0)
        }
    }
}
struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

在这个修改后的代码中,我们进行了以下更改:

  • 我们引入了isTimerRunningisTimerPaused状态变量来跟踪计时器的状态。

  • 我们添加了startTimer()pauseTimer()stopTimer()操作来分别处理启动、暂停和停止计时器。

  • 我们修改了“开始/暂停”按钮,使其在isTimerRunning状态之间切换。

  • 我们更新了计时器的onReceive闭包,使其仅在计时器正在运行且未暂停时增加计数器并更新计时器字符串。

  • 当按下结束按钮时,我们添加了重置计数器和计时器字符串的功能。

通过这些更改,您现在可以在您的健身伴侣应用程序中启动、暂停和停止计时器。

摘要

在本章中,我们成功地将活动按钮屏幕添加到我们的健身伴侣应用程序中。我们首先分析了线框图,并将每个元素分解为 SwiftUI 组件。从那里,我们实现了这些组件以匹配线框图中的设计。通过这个过程,我们更深入地理解了如何使用堆栈组合核心 SwiftUI 组件来创建复杂的按钮。我们最终还查看了一些额外任务的实现。

我想表达我对您花时间阅读这本书的感激之情。有时候我会质疑写另一本编程书的目的是什么,但最终,这是一项值得的努力。我真诚地希望您能从中获得一些东西。如果您有任何问题或想直接联系我,请随时使用以下列出的任何平台:

posted @ 2025-10-24 10:07  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报