SwiftUI学习100天(Day86 - 项目 17,第一部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

当 Apple 推出 iPhone X 时,他们放弃了 iPhone 早期就存在的东西:主页按钮。这个简单的硬件自最初发布以来就一直存在,作为一种帮助用户返回主屏幕的方式,无论他们在做什么和使用什么应用程序——它让整个设备变得不那么可怕。
但随着我们习惯于使用越来越大的玻璃面板,Apple 开始更加依赖手势:我们获得了手势识别器、滑动以终止应用程序的能力、下拉和上拉系统功能的菜单等等。
但在 iPhone X 上,苹果真的把事情提升到了一个新的水平,因为没有主页按钮,几乎所有的东西都变成了一个手势。Apple 甚至在 WWDC18 上发表演讲,鼓励开发人员更多地思考手势,Apple 人机界面设计团队的 Chan Karunamuni 说了一些关于手势非常重要的事情:“当感觉非常好时,有时人们甚至会说感觉很自然,或者很神奇”
你想构建感觉自然的应用程序吗?你当然。我们正在构建的这个新应用程序将在很大程度上依赖于手势,在使用它几秒钟后,你将以光速使用手势。这正是我们的目标:感觉如此自然以至于你很难想象它们以其他方式工作的手势。
今天你有四个主题需要完成,你将在其中学习手势、触觉、命中测试等。

Flashzilla:简介
在这个项目中,我们将构建一个应用程序,帮助用户使用抽认卡学习事物——卡片的一面写着一个东西,比如“购买”,另一面写着另一个东西,比如“comprar”。当然,这是一个数字应用程序,所以我们不需要担心“另一面”,而是可以让闪存卡的详细信息在点击时出现。
这个项目的名称实际上是我的第一个 iOS 应用程序的名称——我很久以前发布的一个应用程序是为 iPhoneOS 编写的,因为 iPad 还没有发布。Apple 在应用程序审查期间实际上拒绝了该应用程序,因为它的产品名称中有“Flash”,而当时 Apple 真的很想在他们的 App Store 附近安装 Flash!时代变了……
无论如何,我们在这个项目中有很多有趣的东西要学习,包括手势、触觉、计时器等等,所以请使用 App 模板创建一个新的 iOS 项目,并将其命名为 Flashzilla。与往常一样,在我们开始构建真实的东西之前,我们有一些技术需要介绍,所以让我们开始吧……

如何在 SwiftUI 中使用手势
SwiftUI 为我们提供了很多处理视图的手势,并且很好地消除了大部分艰苦的工作,因此我们可以专注于重要的部分。最常见的是我们的朋友onTapGesture()
,但还有其他几个,还有一些有趣的手势组合方式值得一试。
我将跳过onTapGesture()
简单的部分,
因为我们之前已经介绍过很多次,但在我们尝试更大的事情之前,我想补充一点,你可以将一个count
参数传递给它们,让它们处理双击、三次点击,以及更多,像这样:
好的,让我们看看比简单的点击更有趣的东西。要处理长按,你可以使用onLongPressGesture()
,如下所示:
与点击手势一样,长按手势也可以自定义。例如,你可以指定按下的最短持续时间,这样你的操作关闭只会在经过特定秒数后触发。例如,这只会在两秒后触发:
你甚至可以添加第二个闭包,只要手势状态发生变化就会触发。这将被赋予一个布尔参数作为输入,它将像这样工作:
只要你按下 change 闭包,它的参数设置为 true 就会被调用。
如果你在手势被识别之前释放(因此,如果你在使用 2 秒识别器时在 1 秒后释放),将调用更改闭包并将其参数设置为 false。
如果你按住识别器的整个长度,那么 change 闭包将被调用,其参数设置为 false(因为手势不再处于飞行状态),你的完成闭包也会被调用。
使用这样的代码亲自尝试一下:
对于更高级的手势,可以在
gesture
结构体使用gesture()
修饰符: DragGesture
、LongPressGesture
、MagnificationGesture
、RotationGesture
和TapGesture
。这些都有特殊的修饰符,onEnded()
和
onChanged()
也经常
如此,你可以使用它们在手势处于飞行中(对于onChanged()
)或完成(对于onEnded()
)时采取行动。
例如,我们可以将放大手势附加到视图,以便放大和缩小视图来放大和缩小视图。这可以通过创建两个@State
属性来存储缩放量,在scaleEffect()
修改器中使用它,然后在手势中设置这些值来完成,如下所示:
使用 RotationGesture
旋转视图可以采用完全相同的方法
,除了现在我们使用rotationEffect()
修饰符
:
事情开始变得更有趣的地方是手势冲突时——当你有两个或更多手势可能同时被识别时,例如,如果你将一个手势附加到视图,并将相同的手势附加到其父项。
例如,这将一个附加onTapGesture()
到文本视图及其父视图:
在这种情况下,SwiftUI 将始终优先考虑子视图的手势,这意味着当你点击上面的文本视图时,你会看到“Text taped”。但是,如果你想改变它,你可以使用修饰符highPriorityGesture()
来强制触发父母的手势,如下所示:
或者,你可以使用修饰符simultaneousGesture()
告诉 SwiftUI 你希望父手势和子手势同时触发,如下所示:
这将同时打印“Text tapped”和“VStack tapped”。
最后,SwiftUI 允许我们创建手势序列,其中一个手势只有在另一个手势首先成功时才会激活。这需要更多思考,因为手势需要能够相互引用,所以你不能直接将它们附加到视图。
这是一个显示手势序列的示例,你可以在其中拖动一个圆圈,但前提是你先长按它:
手势是制作流畅、有趣的用户界面的绝佳方式,但请确保向用户展示手势是如何工作的,否则他们只会感到困惑!



使用 UINotificationFeedbackGenerator 和 Core Haptics 产生振动
尽管 SwiftUI 没有内置任何触觉功能,但我们可以很容易地使用 UIKit 和 Core Haptics 添加触觉功能——这两个框架直接内置到系统中,并且在所有现代 iPhone 上都可用。如果你以前没有听说过这个术语,“触觉”涉及设备中的小型电机,以产生轻敲和振动等感觉。
UIKit 有一个非常简单的触觉实现,但这并不意味着你应该排除它:它可以很简单,因为它专注于内置的触觉,例如“成功”或“失败”,这意味着用户可以来学习某些事情的感觉。也就是说,如果你使用成功触觉,那么一些用户——尤其是那些更依赖触觉的用户,例如盲人用户——将能够知道操作的结果,而无需你的应用程序的任何进一步输出。
要试用 UIKit 的触觉,请将此方法添加到ContentView
:
你可以用一个简单的onTapGesture()
来触发它
,比如这个:
尝试用.success
或者.error
替换.warning
,看看你是否能分辨出它们的区别—— 我认为.success
和.warning
相似但不同。
对于更高级的触觉,Apple 为我们提供了一个称为 Core Haptics 的完整框架。这件事感觉像是背后的 Apple 团队真正的爱心劳动,我认为它是 iOS 13 中引入的真正隐藏的宝石之一——当然,我一看到发行说明就扑向了它!
Core Haptics 让我们可以通过组合轻击、连续振动、参数曲线等来创建高度可定制的触觉。我不想在这里深入探讨,因为这有点偏离主题,但我至少想给你一个例子,这样你就可以自己尝试一下。
首先在 ContentView.swift 的顶部附近添加这个新导入:
接下来,我们需要创建一个CHHapticEngine
作为属性的实例——这是负责创建振动的实际对象,因此我们需要在需要触觉效果之前先创建它。
因此,将此属性添加到ContentView
:
我们将在主视图出现后立即创建它。创建引擎时,你可以附加处理程序以帮助在活动停止时恢复活动,例如当应用程序移至后台时,但在这里我们将保持简单:如果当前设备支持触觉,我们将启动引擎,如果失败则打印错误。
将此方法添加到ContentView
:
现在是有趣的部分:我们可以配置参数来控制触觉应该有多强 ( .hapticIntensity
) 以及它有多“锐利” ( .hapticSharpness
),然后将它们放入具有相对时间偏移的触觉事件中。“锐度”是一个奇怪的术语,但一旦你尝试过它就会更有意义——锐度值为 0 与值为 1 相比确实感觉单调。至于相对时间,这让我们可以创建很多单个序列中的触觉事件。
现在将此方法添加到ContentView
:
要尝试我们的自定义触觉,请将 ContentView
的
为:body
属性修改
这确保了触觉系统已启动,因此点击手势可以正常工作。
如果你想进一步试验触觉,请将let intensity
、let sharpness
和let event
行替换为你想要的任何触觉。例如,如果你将这三行代码替换为下一个代码,你将获得几次先增加然后降低强度和清晰度的点击:



使用 allowsHitTesting() 禁用用户交互
SwiftUI 有一个先进的命中测试算法,它使用视图的框架,通常也使用它的内容。例如,如果你将点击手势添加到文本视图,那么文本视图的所有部分都是可点击的——如果你恰好按下了空格所在的位置,则无法点击文本。另一方面,如果你将相同的手势附加到一个圆上,那么 SwiftUI将忽略圆的透明部分。
为了证明这一点,下面是一个使用一个ZStack
与矩形重叠的圆
,两者都带有onTapGesture()
修饰符:
如果你尝试这样做,你会发现在圆圈内点击会打印“Circle tapped”,但在圆圈后面的矩形上会打印“Rectangle tapped”——即使圆圈实际上与矩形具有相同的框架。
SwiftUI 让我们以两种有用的方式控制用户交互,第一种是修饰符allowsHitTesting()
。当它附加到一个参数设置为 false 的视图时,该视图甚至不被认为是可点击的。不过,这并不意味着它是惰性的,只是它不会捕捉到任何点击——视图后面的东西将被点击。
尝试像这样将它添加到我们的圆圈中:
现在点击圆圈将始终打印“Rectangle tapped!”,因为圆圈将拒绝响应点击。
控制用户交互性的另一种有用方法是使用修饰符contentShape()
,它让我们可以为某些东西指定可点击的形状。默认情况下,圆的可点击形状是相同大小的圆,但你可以指定不同的形状,如下所示:
contentShape()
修饰符真正有用的地方是当你点击附加到带有间隔符的堆栈的动作时,因为默认情况下,SwiftUI 不会在点击堆栈间隔符时触发动作。
这是你可以尝试的示例:
如果你运行它,你会发现你可以点击“Hello”标签和“World”标签,但不能点击它们之间的空间。但是,如果我们contentShape(Rectangle())
在 VStack
上使用
,那么堆栈的整个区域都可以点击,包括间隔:


