swift原文链接 https://swiftui-lab.com/swiftui-animations-part7/

In part 6of the Advanced SwiftUI Animations series, I covered the CustomAnimation protocol, one of the many new additions introduced at WWDC ’23. But the fun does not end there. Now it is time to look at a new view: PhaseAnimator.

在高级 SwiftUI 动画系列的第 6 部分中,我介绍了该协议 CustomAnimation ,这是 WWDC '23 上引入的众多新增功能之一。但乐趣并不止于此。现在是时候看一个新视图了: PhaseAnimator .

If you have been playing around with this new toy, you may have notice there is also a phaseAnimator() modifier. This is just a convenient way of using the view, with a small restriction. We’ll see an example of how to use the modifier at the end of the article.

如果你一直在玩这个新玩具,你可能会注意到还有一个 phaseAnimator() 修饰符。这只是一种使用视图的便捷方式,但有一点限制。我们将在文章末尾看到一个如何使用修饰符的示例。

 


PhaseAnimator PhaseAnimator(相位动画师)

The PhaseAnimator view let us animate a view through a series of phases. Phases are specified with types that adopt the Sequence protocol. But do not worry, a common Swift array will do.

PhaseAnimator 视图允许我们通过一系列阶段对视图进行动画处理。阶段是使用采用协议 Sequence 的类型指定的。但别担心,一个普通的 Swift 数组就可以了。

Endless Animation 无尽的动画

In its simplest form, the view will animate indefinitely, with a default spring animation for each phase. Here are two examples. One has true and false as its phases, the other iterates through numbers 10, 20, 30, 40. These are just some examples, but you could use other types. For example, a collection of enum values. I’ll show such an example later on.

在最简单的形式中,视图将无限期地进行动画处理,每个阶段都有一个默认的弹簧动画。这里有两个例子。一个有真假作为其阶段,另一个遍历数字 10、20、30、40。这些只是一些示例,但您可以使用其他类型。例如,枚举值的集合。我稍后会展示这样一个例子。
image#center#25%

struct ExampleView: View {
    var body: some View {
        HStack(spacing: 30) {

            PhaseAnimator([true, false]) { phase in
                RoundedRectangle(cornerRadius: phase ? 10 : 30)
                    .fill(.green)
                    .frame(width: 120, height: 120)
                    .overlay{ Text(phase ? "true" : "false") }
            }
            
            PhaseAnimator([10, 20, 30, 40]) { phase in
                RoundedRectangle(cornerRadius: phase)
                    .fill(.blue)
                    .frame(width: 120, height: 120)
                    .overlay { Text("\(Int(phase))") }
            }
        }
        .font(.largeTitle).fontWeight(.bold).foregroundColor(.white)
    }
}

Triggered Animation 触发动画

If you only need the animator to complete a single cycle (i.e., go through each phase only once), you can add the trigger parameter. This will make the PhaseAnimator start a cycle when the trigger value changes, but it will not repeat until the trigger value is modified again.

如果只需要动画师完成一个周期(即,每个阶段只经历一次),则可以添加参数 trigger 。当值更改时,这将使 PhaseAnimator 开始成为一个循环,但在 trigger 再次修改该 trigger 值之前不会重复。
image#center#25%

struct ExampleView: View {
    @State var animate = false
    
    var body: some View {
        VStack {
            PhaseAnimator([10, 20, 30, 40, 50, 60], trigger: animate) { phase in
                RoundedRectangle(cornerRadius: phase)
                    .fill(.yellow)
                    .frame(width: 120, height: 120)
                    .overlay { Text("\(Int(phase))") }
            }
            .font(.largeTitle).fontWeight(.bold).foregroundColor(.white)

            Button("Animate") {
                animate.toggle()
            }
        }
    }
}

Note that the animator starts with a phase value of 10 and ends when the phase loops back to 10 again.

请注意,动画器从相位值 10 开始,并在相位再次循环回 10 时结束。

Animation Types 动画类型

By default, all phases use the .spring animation, but you can add a closure that returns which animation to apply at each phase change. In this example, each phase change uses easeInOut, but with a different duration: 5/phase. With this expression, the higher the phase value, the shorter the animation will be. In this example, it goes from 5/10 = 0.5 seconds for phase 10 to 5/60 = 0.08333 seconds for phase 60.

默认情况下,所有阶段都使用动画 .spring ,但您可以添加一个闭包,以返回在每次阶段变化时要应用的动画。在此示例中,每个相位变化都使用 easeInOut,但持续时间不同:5/phase。使用此表达式时,相位值越高,动画越短。在此示例中,它从阶段 10 的 5/10 = 0.5 秒到阶段 60 的 5/60 = 0.08333 秒。
image#center#25%

struct ExampleView: View {
    var body: some View {
        PhaseAnimator([10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]) { phase in
            RoundedRectangle(cornerRadius: phase)
                .fill(.blue)
                .frame(width: 120, height: 120)
                .overlay { Text("\(Int(phase))") }
        } animation: { phase in
            return Animation.easeInOut(duration: 5/phase)
        }
        .font(.largeTitle).fontWeight(.bold).foregroundColor(.white)
    }
}

To save you a headache, please note that the first animation to actually be executed, is the one for phase 20 (not 10). This is because the view appears with the first phase (10) and then animates towards the second phase (20). At the end, the view will animate towards the first phase. This could be a little confusing at first, so bear it in mind.

为了避免麻烦,请注意,实际执行的第一个动画是第 20 阶段(而不是第 10 阶段)的动画。这是因为视图与第一阶段 (10) 一起出现,然后向第二阶段 (20) 进行动画处理。最后,视图将向第一阶段动画化。一开始这可能有点令人困惑,所以请记住。

Suppose we simplify the example to have less phases: 10, 20, 30, 40, 50, 60 and an easeInOut animation with 10/phase. The graphic shows how the animation will occur. The arrow numbers indicate the duration of the animation (10/phase):

假设我们简化示例以具有较少的阶段:10、20、30、40、50、60 和 10/阶段的 easeInOut 动画。该图显示了动画的发生方式。箭头数字表示动画的持续时间(10/阶段):
image#center#25%

If you want to test this out, use the example from the Triggered Animation section above and add the following animation closure to the PhaseAnimator:

如果要对此进行测试,请使用上面“触发动画”部分中的示例,并将以下动画闭包添加到: PhaseAnimator

animation: { phase in

    let _ = print("phase = \(phase)")

    return Animation.easeInOut(duration: 10/phase)
}

This will print to the console the phase values the closure gets. You will see the following values, in this order: 20, 30, 40, 50, 60, 10. That is, it starts with 20 and ends with 10!

这会将闭包获得的相位值打印到控制台。您将按以下顺序看到以下值:20、30、40、50、60、10。也就是说,它以 20 开头,以 10 结尾!

A Little Trouble Ahead 前方有点麻烦

Consider this simple example:

请看这个简单的例子:
image#center#25%

Unfortunately, the following code will not produce the above animation. It will require a small tweak:

遗憾的是,以下代码不会生成上述动画。它需要一个小的调整:

struct ExampleView: View {
    let moonPhases = "????????".map { String($0) }
    
    var body: some View {
        PhaseAnimator(moonPhases) { phase in
            Text(phase)            
        } animation: { _ in
          .easeInOut(duration: 1.0)
        }
        .font(.system(size: 120))
    }
}

The Text view does not animate when the string changes value. I’m almost certain this is a bug in the framework, but in any case, it has multiple workarounds:

当字符串更改值时,文本视图不会进行动画处理。我几乎可以肯定这是框架中的一个错误,但无论如何,它有多种解决方法:

One way to solve it is using the id() modifier:

解决此问题的一种方法是使用 id() 修饰符:

Text(phase).id(phase)

By changing the id in each phase of the animation, we are destroying and recreating the Text view, forcing SwiftUI to produce a transition animation. Because the default transition is .opacity, we will obtain the desired result. But if you want to have some fun, you can change the transition to something else:

通过更改动画每个阶段的 id,我们将销毁并重新创建文本视图,从而强制 SwiftUI 生成过渡动画。因为默认的过渡是 .opacity ,我们将得到所需的结果。但是,如果您想获得一些乐趣,则可以将过渡更改为其他内容:
image#center#25%

Text(phase).id(phase).transition(.scale)

If you want to learn more about the effects of using .id(), check my old post Identifying SwiftUI views.

如果您想了解更多关于使用 .id() 效果的信息,请查看我的旧帖子识别 SwiftUI 视图。

Destroying and recreating the view for each phase may be a bit too much, but there are other ways to force SwiftUI to animate the Text view without destroying. We can add a visual effect that is small enough to be noticeable. SwiftUI will be forced to redraw the view (and animate it), but the effect will not produce any evident output (apart from the animation). In this example we will change the opacity of the view by a very small amount. Odd and even phases will have an opacity of 0.99 and 1.0 respectively. Because opacity is different in each phase change, the view will animate. Now we are no longer animating a transition, but the actual text change.

销毁和重新创建每个阶段的视图可能有点太多了,但还有其他方法可以强制 SwiftUI 在不销毁的情况下对文本视图进行动画处理。我们可以添加一个足够小的视觉效果,以引起注意。SwiftUI 将被迫重绘视图(并对其进行动画处理),但效果不会产生任何明显的输出(动画除外)。在此示例中,我们将对视图的不透明度进行非常小的更改。奇数相位和偶数相位的不透明度分别为 0.99 和 1.0。由于每个相变中的不透明度都不同,因此视图将进行动画处理。现在,我们不再对过渡进行动画处理,而是对实际的文本进行更改。

Text(phase)
    .opacity(moonPhases.firstIndex(of: phase)! % 2 == 0 ? 1.0 : 0.99)

I have only seen this problem with Text views, but be aware, in case you come across the same problem in other scenarios. Fortunately, the cases when you will need to implement a workaround are very few.

我只在文本视图中看到过这个问题,但请注意,以防您在其他情况下遇到同样的问题。幸运的是,需要实施解决方法的情况很少。

Using .phaseAnimator() 使用 .phaseAnimator()

For convenience, you may also use the phaseAnimator() method. It receives the same parameters as PhaseAnimator, but the @ViewBuilder closure has an additional parameter with the view to modify.

为方便起见,您也可以使用该 phaseAnimator() 方法。它接收与 PhaseAnimator 相同的参数,但 @ViewBuilder 闭包有一个附加参数,用于修改视图。

Some animations can be equally achieved with both options, but the modifier is more limited. It allows you to use the phase value to modify a view, but it does not allow you to change its calling parameters, as is the case in the first examples where the cornerRadius parameter is set according to the phase.

使用这两个选项可以同样实现某些动画,但修改器更有限。它允许您使用阶段值来修改视图,但不允许您更改其调用参数,就像第一个示例中的情况一样,其中 cornerRadius 参数是根据阶段设置的。
image#center#25%

enum CardinalPoint: Double, CaseIterable {
    case north = 0
    case east = 90
    case south = 180
    case west = 270
    case north_360 = 360
    
    // SF Symbol (↗) is 45 degrees rotated, so we substract it to compensate
    var angle: Angle { .degrees(self.rawValue - 45.0) }
}

struct ExampleView: View {

    var body: some View {
        
        Image(systemName: "location.circle.fill")
            .symbolRenderingMode(.palette)
            .foregroundStyle(.yellow, .pink)
            .font(.system(size: 120))
            .phaseAnimator(CardinalPoint.allCases) { content, phase in

                content
                    .rotationEffect(phase.angle)
                
            } animation: { phase in
                if phase == .north {
                    .linear(duration: 0)
                } else {
                    .bouncy(extraBounce: 0.2)
                }
            }
    }
}

This example also shows how you can use enum cases as your animator’s phases.
此示例还演示了如何使用枚举事例作为动画师的阶段。

 


Continuing the Journey 继续旅程

In this new part 7 of the series (the second this year), we have seen how to work with PhaseAnimator. In the next installment, we’ll dive deep into the world of KeyframeAnimator, unlocking even more exciting possibilities for your SwiftUI animations.

在该系列的第 7 部分(今年的第二部分)中,我们已经了解了如何使用 PhaseAnimator .在下一期中,我们将深入探讨 KeyframeAnimator 的世界,为您的 SwiftUI 动画解锁更多激动人心的可能性。

Made with in Shangrao,China By 老雷

Copyright © devler.cn 1987 - Present

赣ICP备19009883号-1