在 iOS 14 使用 matchedGeometryEffect 简单为 App 建立绚丽的视图动画

2,223 阅读9分钟

在 iOS 14 中,Apple 为 SwiftUI 框架引入了很多新功能,像是 LazyVGrid 以及 LazyHGrid。其中 matchedGeometryEffect 非常引人注目,这个功能让开发者只需要几行程式码,就能够创造绚丽的视图动画。SwiftUI 框架已经让开发者可以简单地使用动画来呈现视图的变化,而 matchedGeometryEffect 修饰器 (modifier) 更将视图动画 (view animations) 的实作提升到另一个境界。

对所有手机 App 来说,我们经常需要在多个视图之间转换,因此一个令人喜欢的视图转换绝对可以提昇整体的使用者体验。有了 [matchedGeometryEffect](https://developer.apple.com/documentation/swiftui/view/matchedgeometryeffect(id:in:properties:anchor:issource:)),你只需要描述两个视图的外观,修饰器就会自动计算两个视图的差异,并且自动为大小和位置的变化加上动画。

可能你会觉得十分困惑,但别担心,介绍完整个范例 App 之后,你就会明白我在说什麽了。

**编者备注:**本文摘自 Mastering SwiftUI 一书。如果你想深入学习 SwiftUI 动画及 SwiftUI 框架,请到 AppCoda 网站 购买完整书本。

重温 SwiftUI 动画

在我们开始介绍 matchedGeometryEffect 之前,让我们先来看一下如何使用 SwiftUI 来实作动画。下面的图片显示了一个视图开始和结束的状态。当你点击左边的圆形视图,它应该会变大且往上移动;相反地,当你点击右边的视图时,它就会回到原本的大小和位置。

image

要实作这个可点击的圆形视图非常简单。开启一个新的 SwiftUI 专案后,如此更新 ContentView 结构:

struct ContentView: View {
    @State private var expand = false
    var body: some View {
        Circle()
            .fill(Color.green)
            .frame(width: expand ? 300 : 150, height: expand ? 300 : 150)
            .offset(y: expand ? -200 : 0)
            .animation(.default)
            .onTapGesture {
                self.expand.toggle()
            }
    }
}

我们用一个状态变数 expand,来记录 Circle 视图目前的状态。当状态改变时,我们会透过 .frame.offset 这两个修饰器,来修改视图框 (frame) 的大小和位移 (offset)。如果在预览画面中执行这个 App ,你应该可以在点击圆形视图时看到动画效果。

swiftui-matchedgeometryeffect-animation-circle

了解 matchedGeometryEffect 修饰器

那麽到底什麽是 matchedGeometryEffect 呢?这个功能如何简化实作视图动画的步骤?让我们再来看一下第一张图片、以及圆形视图动画的程式码。我们需要找出开始及结束状态时的确切数值差异。在这个例子当中,就是视图框的大小及位移。

有了 matchedGeometryEffect 修饰器,你不再需要找出两个状态之间的差异了。你只需要描述两个视图:一个是开始的状态,而另一个是结束的状态matchedGeometryEffect 会自动添加在两个视图之间大小和位置的差异。

要使用 matchedGeometryEffect 建立跟之前一样的动画效果,你需要先宣告一个命名空间 (namespace) 变数:

@Namespace private  var shapeTransition

然后,如此重写 body 的部分:

var body: some View {
    if expand {
        // Final State
        Circle()
            .fill(Color.green)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(width: 300, height: 300)
            .offset(y: -200)
            .animation(.default)
            .onTapGesture {
                self.expand.toggle()
            }
    } else {
        // Start State
        Circle()
            .fill(Color.green)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(width: 150, height: 150)
            .offset(y: 0)
            .animation(.default)
            .onTapGesture {
                self.expand.toggle()
            }
    }
}

在这段程式码中,我们建立了两个圆形视图,一个表示开始状态,而另一个表示结束状态。当它一开始被初始化之后,我们会得到一个 Circle 视图,位置置中以及宽度为 150 点。当 expand 状态变数从 false 变成 true 时, App 会显示另一个 Circle 视图,其位置是从中间往上 200 点以及宽度为 300 点。

对于两个 Circle 视图,我们都添加了 matchedGeometryEffect 修饰器,并且指定了相同的 ID 和命名空间。透过这样的设定,SwiftUI 可以计算两个视图之间大小和位置的差异,并且添加视图转换。随著后续加上的 animation 修饰器,SwiftUI 框架会自动动画化视图转换。

ID 以及命名空间的用途,是用来标记属于同一个转换的视图,所以两个 Circle 视图会使用相同的 ID 和命名空间。

以上我们介绍了如何使用 matchedGeometryEffect,来实作两个视图之间的动画转换效果。如果你有使用过Keynote 的 Magic Move,这个新的修饰器跟 Magic Move非常类似。我建议你使用 iPhone 模拟器来执行这个 App,以便测试这个动画效果。在我写这篇文章时, Xcode 12 中有一个 bug,导致我们无法在预览画面测试这个动画效果。

从圆形变为圆角长方形

现在,让我们来试试实作另一个视图转换动画。这一次,我们会将一个圆形变化成为一个圆角长方形 (rounded rectangle)。圆形位置于萤幕的上端,而圆角长方形则位于萤幕的底端。

swiftui-matchedgeometryeffect-morphing

我们可以使用刚刚学到的技巧,准备两个视图:一个圆形视图和一个圆角长方形视图,matchedGeometryEffect 修饰器就会处理视图转换的部分。现在,让我们将 body 变数的 ContentView 结构改成这样:

VStack {
    if expand {
        // Rounded Rectangle
        Spacer()
        RoundedRectangle(cornerRadius: 50.0)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
            .padding()
            .foregroundColor(Color(.systemGreen))
            .animation(.easeIn)
            .onTapGesture {
                expand.toggle()
            }
    } else {
        // Circle
        RoundedRectangle(cornerRadius: 50.0)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(width: 100, height: 100)
            .foregroundColor(Color(.systemOrange))
            .animation(.easeIn)
            .onTapGesture {
                expand.toggle()
            }
        Spacer()
    }
}

我们同样使用 expand 状态变数,来切换圆形视图和圆角长方形视图。这段程式码跟之前的范例非常相似,只是我们在这边加上了 VStackSpacer 来定位视图。或许你会问,为什麽要使用 RoundedRectangle 来建立圆形物件呢?主要原因是这样可以让视图转换就会更顺畅。

在这两个视图中,我们都加上了 matchedGeometryEffect 修饰器,并且指定了一样的 ID 以及命名空间,我们要做的事就完成了。修饰器会自动比较两个视图的差异,并且使用动画呈现这个改变。如果你在预览画面或是 iPhone 模拟器上执行这个 App,会看到视图在圆形和圆角长方形之间完美的转换。这就是 matchedGeometryEffect 的威力。

swiftui-matchedgeometryeffect-button-transition

不过,你可能注意到这个修饰器无法执行改变颜色的动画。没错,matchedGeometryEffect 只能用来处理位置和大小的变化。

练习一

让我们来做一个小小的练习,来测试你对 matchedGeometryEffect 的了解。你的任务是要建立如下图的动画视图转换。开始时,它是一个橘色的圆形视图,圆形视图被点击后,就会转换成全萤幕的背景图。你可以在专案档中找到完整程式码。

swiftui-matchedgeometryeffect-full-screen

使用动画转换来交换两个视图

现在你应该对 matchedGeometryEffect 有了基础的认识,让我们继续来看看它如何帮助我们建立一些绚丽的动画。在这个范例中,我们会交换两个圆形视图的位置,并且套用修饰器来建立顺畅的视图转换。

swiftui-animation-matchedgeometryeffect-swap

我们会使用一个状态变数,来储存交换的状态,并建立一个命名空间变数给 matchedGeometryEffect来使用。让我们在 ContentView 宣告这些参数:

@State private  var swap = false

@Namespace private  var dotTransition

橘色圆形预设位在萤幕的左边,而绿色圆形则在萤幕的右边。当使用者点击任意一个圆形图案,就会触发互换的动画。使用 matchedGeometryEffect 时,你不用了解交换动画是如何达成的。要建立视图转换,你只需要做到以下事情:

  1. 建立橘色和绿色圆形交换之前的版面配置
  2. 建立两个圆形在交换之后的版面配置

如果你要将版面配置转换成程式码,你可以如此编写 body 变数:

if swap {
    // After swap
    // Green dot on the left, Orange dot on the right
    HStack {
        Circle()
            .fill(Color.green)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "greenCircle", in: dotTransition)
        Spacer()
        Circle()
            .fill(Color.orange)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "orangeCircle", in: dotTransition)
    }
    .frame(width: 100)
    .animation(.linear)
    .onTapGesture {
        swap.toggle()
    }
} else {
    // Start state
    // Orange dot on the left, Green dot on the right
    HStack {
        Circle()
            .fill(Color.orange)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "orangeCircle", in: dotTransition)
        Spacer()
        Circle()
            .fill(Color.green)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "greenCircle", in: dotTransition)
    }
    .frame(width: 100)
    .animation(.linear)
    .onTapGesture {
        swap.toggle()
    }
}

我们使用 HStack 来将两个圆形配置成水平排列,并且利用 Spacer 在两个圆形中间建立一些空间。当 swap 变数设定成 true 之后,绿色圆形会被放在橘色圆形的左边。相反地,绿色圆形则会被放在橘色圆形的右边。

如你所见,我们只需要描述不同状态下圆形视图的配置,matchedGeometryEffect 就会处理馀下的事情。我们在每个 Circle 视图加上修饰器,不过这次有一点不同,因为我们有两个不同的 Circle视图需要配置,我们使用了两个不同的 ID 来建立 matchedGeometryEffect 修饰器。我们将橘色圆形的识别名称设定为 orangeCircle,而绿色圆形则是设定为 greenCircle

现在,如果你在模拟器执行这个 App,应该可以在点击任何一个圆形时看到互换的动画。

练习二

在刚刚的练习中,我们在两个圆形上使用了 matchedGeometryEffect,并交换它们的位置。这个练习会使用到一样的技巧,不过这次是要使用两张图片。下面的图展示了这个范例,点击交换按钮时,这个 App 就会用漂亮的动画来交换这两张图片。

swiftui-swap-photos

你可以随意使用自己的图片。我在范例中使用了这些 Unsplash.com 的免费图片:

总结

引进 matchedGeometryEffect 修饰器之后,视图动画的实作提昇到了另一个层次。你可以用更少的程式码来创造漂亮的视图动画。即便你是 SwiftUI 的新手,你也可以从这个新修饰器中得益,并且让你的 App 更棒。

文末推荐:iOS热门文集