有一位来自德国的著名工业设计师Dieter Rams。你可能没有听说过他,但你肯定已经看过他的作品- 从iPod到iMac和Mac Pro,多年来他的设计极大地启发了Apple自己的设计。他曾经说过:“好的设计使事物变得可理解和令人难忘;出色的设计可以使人留下深刻的印象。”

修饰符顺序

在Swift中的修饰符根据顺序有关。例如

1
2
3
4
5
6
Button("Tap Me") {
// do nothing
}
.background(Color.blue)
.frame(width: 200, height: 200)
.foregroundColor(.white)

1
2
3
4
5
6
Button("Tap Me") {
// do nothing
}
.frame(width: 200, height: 200)
.background(Color.blue)
.foregroundColor(.white)

所代表的含义有所不同。可以理解为代码由上往下运行。

多个动画修饰符控制动画

当同时出现多个动画发生时,可以根据修饰符顺序的原则使用多个动画修饰符。

1
2
3
4
5
6
7
8
9
Button("Tap Me") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))

动画手势

SwiftUI允许我们将手势附加到任何视图,并且这些手势的效果也可以动画化。稍后,我们将更详细地介绍手势,但现在让我们尝试一些相对简单的操作:可以在屏幕上拖动的卡片,但是放开后,它会卡回到其原始位置。

首先,我们的初始布局:

1
2
3
4
5
6
7
struct ContentView: View {
var body: some View {
LinearGradient(gradient: Gradient(colors: [.yellow, .red]), startPoint: .topLeading, endPoint: .bottomTrailing)
.frame(width: 300, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}

这样可以在屏幕中央绘制类似卡片的视图。我们想根据手指的位置在屏幕上移动它,这需要三个步骤。

首先,我们需要某种状态来存储其拖动量:

@State private var dragAmount = CGSize.zero
其次,我们要使用该大小来影响卡在屏幕上的位置。SwiftUI为此提供了一个专用的修饰符offset(),它使我们能够调整视图的X和Y坐标而无需在其周围移动其他视图。你可以根据需要输入离散的X和Y坐标,但是-绝非偶然- offset()也可以CGSize直接采用。

因此,第二步是将此修改器添加到牌梯度中:

1
.offset(dragAmount)

现在重要的部分到了:我们可以创建一个DragGesture并将其附加到卡上。拖动手势在这里对我们有用的两个额外的修饰符:onChanged()让我们在用户移动手指onEnded()时运行闭合,并且在用户将手指从屏幕上抬起并结束拖动时运行闭合。

这两个闭包都被赋予了一个参数,该参数描述了拖动操作-它的开始位置,当前位置,移动距离等等。对于onChanged()修改器,我们将阅读拖动的平移,该平移告诉我们拖动距起点有多远–我们可以直接将其分配给,dragAmount以便视图随手势一起移动。对于onEnded()我们要完全忽略输入,因为我们将设置dragAmount回零。

因此,现在将此修饰符添加到线性渐变中:

1
2
3
4
5
.gesture(
DragGesture()
.onChanged { self.dragAmount = $0.translation }
.onEnded { _ in self.dragAmount = .zero }
)

如果运行代码,你会看到现在可以拖动渐变卡了,放开拖动时,它将跳回到中心。卡的偏移量由确定dragAmount,该偏移量又由拖动手势设置。

现在一切正常,我们可以通过一些动画使该动作栩栩如生,我们有两个选择:添加一个隐式动画以使拖动和释放具有动画效果,或者添加一个显式动画以使释放成为动画。

要查看前者的实际效果,请将此修改器添加到线性渐变中:

.animation(.spring())
拖动时,由于弹簧动画的作用,卡会稍有延迟地移到拖动位置,但是如果突然移动,它也会轻轻地过冲。

要看到明确的动画在行动,删除animation()修改和改变现有的onEnded()拖拽手势的代码如下:

1
2
3
4
5
.onEnded { _ in
withAnimation(.spring()) {
self.dragAmount = .zero
}
}

现在,这张卡将立即跟随你的拖动(因为没有被动画化),但是当你放开它时,它将进行动画处理。

如果我们将偏移动画与拖动手势并稍加延迟相结合,则无需大量代码就可以创建非常有趣的动画。

为了证明这一点,我们可以将文本“ Hello SwiftUI”编写为一系列单独的字母,每个字母的背景颜色和偏移量都由某个状态控制。字符串是稍稍的字符数组看中,所以我们可以得到一个真正的从这样的字符串数组:Array(“Hello SwiftUI”)。

无论如何,请尝试一下,看看你的想法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct ContentView: View {
let letters = Array("Hello SwiftUI")
@State private var enabled = false
@State private var dragAmount = CGSize.zero

var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num) / 20))
}
}
.gesture(
DragGesture()
.onChanged { self.dragAmount = $0.translation }
.onEnded { _ in
self.dragAmount = .zero
self.enabled.toggle()
}
)
}
}

如果运行该代码,你会发现可以拖动任意字母以使整个字符串都跟随该字符串,只是短暂的延迟会导致类似蛇的效果。当你释放拖动时,SwiftUI还将添加颜色更改,即使字母移回中心也可以在蓝色和红色之间进行动画显示。

显示和隐藏的渐变动画

SwiftUI最强大的功能之一是能够自定义视图的显示和隐藏方式。之前,你已经了解了如何使用常规if条件有条件地包含视图,这意味着当条件发生变化时,我们可以从视图层次结构中插入或删除视图。

过渡控制插入和删除的方式,我们可以使用内置过渡,以不同方式组合它们,甚至创建完全自定义的过渡。

为了说明这一点,这里有一个VStack带有按钮和一个矩形的:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView: View {
var body: some View {
VStack {
Button("Tap Me") {
// do nothing
}

Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
}
}

我们可以使矩形仅在满足特定条件时显示。首先,我们添加一些可以操纵的状态:

1
@State private var isShowingRed = false

接下来,我们将该状态用作显示矩形的条件:

1
2
3
4
5
if isShowingRed {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}

最后,我们可以isShowingRed在按钮的操作中在true和false之间切换:

1
self.isShowingRed.toggle()

如果运行该程序,则会看到按下按钮会显示并隐藏红色方块。没有动画。它只是出现而突然消失。

我们可以使用来包装状态更改withAnimation(),从而获得SwiftUI的默认视图过渡,如下所示:

1
2
3
withAnimation {
self.isShowingRed.toggle()
}

有了较小的更改,应用程序现在就可以淡入和淡出红色矩形,同时还可以向上移动按钮以腾出空间。看起来不错,但我们可以使用transition()修饰符做得更好。

例如,我们可以通过在矩形上添加transition()修饰符来使矩形放大和缩小:

1
2
3
4
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.transition(.scale)

现在点击按钮看起来更好:矩形会随着按钮的腾出而扩大,然后再次点击时会缩小。

如果要尝试,还可以尝试其他几种转换。一个有用的是.asymmetric,它使我们可以在显示视图时使用一个过渡,而在消失时使用另一个过渡。要进行尝试,请使用以下命令替换矩形的现有过渡:

1
.transition(.asymmetric(insertion: .scale, removal: .opacity))

使用ViewModifier构建自定义过渡动画

为SwiftUI创建全新的过渡是可能的,而且实际上出乎意料的容易,这使我们可以使用完全自定义的动画添加和删除视图。

.modifier过渡使此功能成为可能,该过渡接受我们想要的任何视图修饰符。要注意的是,我们需要能够实例化修饰符,这意味着它必须是我们自己创建的修饰符。

为了尝试这一点,我们可以编写一个视图修改器,让我们模仿Keynote中的Pivot动画-它使新幻灯片从其左上角旋转入。用SwiftUI讲,这意味着创建一个视图修改器,使我们的视图从一个角旋转,而不会逃脱它应该位于的边界。SwiftUI实际上为我们提供了修改器来做到这一点:rotationEffect()让我们在2D空间中旋转视图,并clipped()阻止将视图绘制到其矩形空间的外部。

rotationEffect()与相似rotation3DEffect(),但它始终绕Z轴旋转。但是,它也使我们能够控制旋转的锚点 -视图的哪一部分应固定在旋转中心。SwiftUI为我们提供了一个UnitPoint用于控制锚,它可以让我们指定确切的X / Y点的许多内置选项旋转或使用一个类型- ,.topLeading,.bottomTrailing,.center等等。

让我们通过创建一个CornerRotateModifier结构来构造所有代码,这些结构具有一个锚点来控制旋转的位置,并控制一个旋转量:

1
2
3
4
5
6
7
8
struct CornerRotateModifier: ViewModifier {
let amount: Double
let anchor: UnitPoint

func body(content: Content) -> some View {
content.rotationEffect(.degrees(amount), anchor: anchor).clipped()
}
}

clipped()那里的添加意味着当视图旋转时,不会绘制位于其自然矩形之外的零件。

我们可以使用.modifier过渡直接尝试一下,但这有点笨拙。一个更好的主意是将其包装到的扩展中AnyTransition,使它在其最前端的角从-90旋转到0:

1
2
3
4
5
6
7
8
extension AnyTransition {
static var pivot: AnyTransition {
.modifier(
active: CornerRotateModifier(amount: -90, anchor: .topLeading),
identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
)
}
}

有了这个,我们现在可以使用以下方法将透视动画附加到任何视图:

1
.transition(.pivot)

参考资料

查看下一天的SwiftUI学习笔记

关于100days英文课程