65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Swift 中使用 iOS 视图动画和过渡

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2018年9月26日

CPOL
viewsIcon

6024

在 Swift 中使用 iOS 视图动画和过渡

如果您使用 Apple 设备,您可能已经看到过第三方应用程序或 Apple 自带应用程序中的许多动画。如果您曾希望知道如何做到这一点,但又觉得它太复杂了,那么您的愿望即将实现。跟我一起学习,您将发现,在 iOS 中创建动画不仅不复杂,而且很有趣。

动画能吸引用户的注意力,让他们专注于屏幕上的重要内容。动画还能突出屏幕上的变化,并帮助用户学习导航您的应用程序。但说实话,大家都喜欢动画。它们就是那么酷。

您需要的工具

要跟随本教程,假设您是一名 iOS 开发人员,希望探索动画的世界,您将需要一些前提条件:

  • 一台运行最新 macOS X Sierra 或更高版本操作系统的 Mac。
  • Xcode 8 或更高版本。
  • Swift 4 语言和 Auto Layout 的基本知识。

如果您不具备这些条件,可以继续获取它们,我在这里等着...

入门

现在您已经准备好了所需的一切,我已经在小项目中设置好了供您使用。您可以 在这里 下载,或者如果您更喜欢查看 Git 仓库,也可以这样做。

下载或克隆后,请打开项目文件。您应该会看到类似这样的文件结构:

打开 Main.storyboard。您会看到一个黑金主题的视图控制器。

它恰如其分地命名为 **Next Level Designs**,包含一个标签、两个 UIViews:一个金色,一个绿色,以及底部的“Transition”按钮。金色的 UIView 叠加在绿色的上面;这就是为什么您目前看不到它的原因。

您可以运行项目,看看设计在实际运行中的效果。我们将使用这个视图控制器来演示视图过渡。

让我们开始吧。

视图过渡

正如您可能猜到的,我们将要过渡的是 UIViews。我们的目标很简单:当我们点击 Transition 按钮时,我们希望在金色 UIView 和绿色 UIView 之间发生过渡,再次点击时则反之。

但在我们能够做到这一点之前,我们必须将一些 outlet 连接到自定义类。打开 Main.storyboard,选择 **Next Level View Controller**,然后点击 Xcode 右上角的 Assistant Editor(那个看起来像两个重叠圆圈的图标)。

我们首先要连接的是我们的两个 UIViews,由于一个叠加在另一个上面,连接它们的最简单方法是从左侧的 Document Outline 进行。

从那里,您可以选择 **Option + 拖动** 或 **右键单击拖动** 到 Xcode 文件中,就在 viewDidLoad() 方法的上方。

将金色视图命名为 goldenView,然后点击“Connect”。

您可以通过查看右侧 Attributes Inspector 中的背景颜色来确认您正在连接的 UIView

greenView 执行相同的操作,最后在 viewDidLoad() 方法之后连接 Transition 按钮。

确保它是一个 Action 且类型为 UIButton

现在一切就绪,准备好向苹果公司那些热心人士寻求我们驻场过渡 API 的帮助吧。

过渡 API

如果您熟悉 UIKit Animations API,Transition API 与之非常相似,并且实现起来相当简单。但是,有一些区别,我们将一一介绍。

首先要知道的是,Transition API 有两种实现方式,表面上看起来非常相似,因为它们做的事情相同。然而,它们的运作方式略有不同。

在我们的 transition() action 方法中,如果您开始输入 UIView.transition,XCode 的自动完成功能会给您一些选项:

我们对前两个选项感兴趣,为了更好地解释它们的工作原理,我们将同时使用它们来过渡我们的 UIViews

UIView.transition(with: …)

让我们从第二个选项开始,即带有 with: 参数的那个。它是比较直接的。

视图过渡是预定义的,这意味着它们在一定程度上受到限制,但非常强大且易于实现。要实现过渡,您只需做一件事:将一个过渡触发器与一个预定义的过渡配对。

自然,您接下来的问题是:“这很好,但是什么是过渡触发器?什么是预定义的过渡?我该如何做?”

请耐心等待,一切都会适时揭晓。让我们先从过渡触发器开始。

有三个触发器应该会引起视图过渡:

  • isHidden - 这个触发视图的 isHidden 属性,使其从视图中消失。
  • addSubview() - 这个方法将一个子视图添加到视图的视图层级结构中。
  • removeFromSuperview() - 这个方法的作用相反,将一个视图从其父视图的视图层级结构中移除。

触发器放在 animations: 闭包中。

至于过渡,有七种:

  • .transitionFlipFromLeft
  • .transitionFlipFromRight
  • .transitionFlipFromTop
  • .transitionFlipFromBottom
  • .transitionCurlUp
  • .transitionCurlDown
  • .transitionCrossDissolve

这些过渡效果正如其名,我们稍后会看到。它们放在 options: 参数中。

现在您已经跟上进度了,让我们开始过渡吧。在您的 UIView.transition(with: …) 实现中,将 with: 参数设置为 goldenView,因为它在最上面,将 duration: 设置为 0.5,在 options: 中输入 [.transitionFlipFromRight],然后在 animations: 闭包中,将 goldenViewisHidden 属性设置为 true

您的实现应该如下所示:

UIView.transition(with: goldenView,
                         duration: 0.5,
                         options: [.transitionFlipFromRight],
                         animations: {
                           
                           self.goldenView.isHidden = true
       },
                         completion: nil)

构建并运行项目,点击 Transition 按钮,看看会发生什么。

恭喜您完成了第一次视图过渡。给自己一个击掌!

然而,我们的过渡项目远未完成。过渡只发生一次,这并非我们想要的效果。我们需要找到一种方法使其来回过渡。

在同一个文件中,在我们的 UIViews outlets 的正下方,定义一个私有常量,命名为 isFlipped,并将其初始化为 false

private var isFlipped: Bool = false

接下来,告诉我们的过渡方法,按钮被点击时应该翻转哪个 UIView

我们需要翻转的卡片是 goldenView(当 isFlippedtrue 时)和 greenView(当 isFlippedfalse 时)。

在我们的按钮 action 方法中,添加一个基于 isFlipped 属性的三元运算符,该运算符决定要翻转的卡片是 goldenView 还是 greenView

@IBAction func transition(_ sender: UIButton) {
       isFlipped = !isFlipped

然而,我们目前的 isFlipped 属性是静态的,这意味着它将始终为 false。所以我们需要在每次点击按钮时更改 isFlipped 的值。

要做到这一点,在 cardToFlip 的声明上方添加以下内容:

isFlipped = !isFlipped

最后一件事,在 UIView.transition(with: …) 方法中,将 goldenView 的所有实例替换为 cardToFlip

整个方法现在应该如下所示:

@IBAction func transition(_ sender: UIButton) {
       isFlipped = !isFlipped
				let cardToFlip = isFlipped ? goldenView : greenView
				UIView.transition(with: cardToFlip!,
                         duration: 0.5,
                         options: [.transitionFlipFromRight],
                         animations: {
                           
                           cardToFlip!.isHidden = true
       },
                         completion: nil)
   }

构建并运行项目。现在注意卡片可以相互交替过渡。

哦,那是什么?卡片正确翻转然后消失了?当然会!因为我们告诉 cardToFlip 隐藏了,但没有告诉它从隐藏状态中出来。

要解决这个问题,我们必须使用 completion 闭包,并将 cardToFlipisHidden 属性重新设置为 false。您的 completion 闭包现在应该如下所示:

completion: { _ in cardToFlip?.isHidden = false })

再次构建并运行项目。

您应该遇到了另一个问题,这次是 goldenView 过渡出视图,然后在一段时间后尴尬地重新出现。我知道您可能在想什么,不,我没有和您开玩笑。这实际上是一个关于视图层级的教学时刻。

我不会深入探讨视图层级,因为这不是本文的主题。但基本上,视图也可以充当其他视图的容器,从而创建视图层级。

在我们的特定案例中,我们的两个 UIViews 包含在视图控制器的根视图中,因此是它的子视图。goldenView 最初放置在 greenView 的前面,并且除非更改顺序,否则将保持在那里。

幸运的是,有一个内置方法可以帮助我们。

首先,我们必须捕获我们的 bottomCard。所以,在 cardToFlip 的三元运算符下方,为 bottomCard 添加另一个。

let bottomCard = isFlipped ? greenView : goldenView

接下来,在 completion 闭包中,在告诉 cardToFlip 停止隐藏的行上方,添加以下代码行,将 bottomCard 提到前面:

self.view.bringSubview(toFront: bottomCard!)

您的整个 action 方法现在应该如下所示:

@IBAction func transition(_ sender: UIButton) {
       isFlipped = !isFlipped
let cardToFlip = isFlipped ? goldenView : greenView
       let bottomCard = isFlipped ? greenView : goldenView
UIView.transition(with: cardToFlip!,
                         duration: 0.5,
                         options: [.transitionFlipFromRight],
                         animations: {
                           
                           cardToFlip?.isHidden =  true
       },
                         completion: { _ in
self.view.bringSubview(toFront: bottomCard!)
                           cardToFlip?.isHidden = false
       })
   }

现在再次构建并运行项目。我保证这次它会奏效。😄

看!这就是我们想要的效果。

还有一件事:当一个过渡是由添加或移除视图到层级结构触发时,**即** addSubview()removeFromSuperview()**,您需要在 with: 参数中使用视图的父视图。**

当过渡由 isHidden 触发时,视图充当自己的容器视图。

现在让我们看看过渡 API 的另一个变体。

UIView.transition(from: …)

这个变体,表面上看起来更简单。然而,正如您稍后会发现的,有一些底层功能在发生。

我们将对我们的 UIViews 应用与之前相同的过渡,所以请注释掉之前的 UIView.transition(with: …) 实现。

和之前一样,在按钮的 action 方法中,在 bottomCard 的定义正下方,开始输入 UIView.transition,并从 Xcode 自动完成中选择第一个变体——带有 from: 参数的那个。

我们希望从 cardToFlip 过渡到 bottomCard。所以我们将 cardToFlip 用于 from: 参数,将 bottomCard 用于 to: 参数。让我们将 duration: 参数设置为相同的 0.5 秒,并使用 [.transitionCurlUp] 作为 options: 参数。暂时将 completion: 设置为 nil

您的过渡方法现在应该如下所示:

UIView.transition(from: cardToFlip!,
                         to: bottomCard!,
                         duration: 0.5,
                         options: [.transitionCurlUp],
                         completion: nil)

生成并运行项目。

第一次过渡如预期发生,但当您第二次点击按钮时,编译器会给出 **⚠ 致命错误**。

您可能已经注意到,这个变体没有像另一个那样拥有 animations: 闭包,而我们需要 animations: 闭包来触发过渡。好吧,正如我之前提到的,这个变体在后台有一些自动处理的过程。

如果您查看底部的变量视图,您会注意到 bottomCard(在本例中是 goldenView)为 nil

发生的事情是:自动过渡触发器将 goldenView 从视图层级结构中移除了。

在苹果官方 文档 中,他们指出,在此变体中,fromView: 在过渡过程中会从其父视图中移除。

好吧,这都可以,但我们不能就这样忍受这个 **致命错误**。我们必须以某种方式修复它,有两条途径可以做到。

一种方法是在 completion: 闭包中将 cardToFlip 添加回其父视图的视图层级结构,然后像之前一样将 bottomCard 提到前面。但您已经学会了如何做,所以让我们来探索另一个,**更快捷**(双关语😏)的选项。

options: 参数集中,添加另一个名为 .showHideTransitionViews 的选项。它的作用只是隐藏 fromView,而不是将其从视图层级结构中移除。

您的过渡方法现在应该如下所示:

UIView.transition(from: cardToFlip!,
                         to: bottomCard!,
                         duration: 0.5,
                         options: [.transitionCurlUp, .showHideTransitionViews],
                         completion: nil)

再次构建并运行项目。

我们的过渡现在工作正常,但是,还有一个小问题。整个页面而不是视图会向上翻卷,而我们肯定不希望这样。在此过渡方法的变体中,过渡仅发生在父视图上,并且不幸的是,无法使过渡仅发生在子视图上。这就是自动化带来的局限性。

唯一的变通方法是将我们的两个 UIViews 嵌入另一个大小和位置相同的 UIView 中,使其充当它们的父视图,而不是主根视图。

要做到这一点,打开 Main.storyboard,然后在文档大纲中,通过 **Command + 单击** 来选中两个 UIViews。然后转到 **Editor/Embed In/View**。

您现在应该看到一个比其他两个稍大的白色视图。您应该注意到左侧的文档大纲中,我们的两个 UIViews 现在是新 UIView 的子视图。

调整新 UIView 的大小可能会很棘手,所以我将引导您完成。

  • 首先,抓住白色 UIView 的左边缘,并将其拉到主根视图的边界内。
  • 接下来,选择 goldenViewgreenView,并将它们同时移动到白色 UIView 的左上角,并使它们与白色 UIView 的顶部和左边缘完美对齐。
  • 现在,抓住白色 UIView 的右下角,并将其调整大小以适应 goldenViewgreenView 的尺寸。
  • 最后,从文档大纲中选择白色 UIView,然后同时将所有三个 UIViews 移动到 goldenViewgreenView 之前的位置。

最后一次构建并运行项目,并欣赏您工作的成果。

一如既往,现在由您来尝试和探索其他过渡选项,看看您最喜欢哪一个。

您可以 在这里 下载完整的教程项目,或在 GitHub 上查看仓库,并与您自己的项目进行比较。

© . All rights reserved.