swift原文链接 https://swiftui-lab.com/swiftui-animations-part5/
This fifth part of the Advanced SwiftUI Animations series will explore the Canvas
view. Technically, it is not an animated view, but when combined with TimelineView from Part 4, it brings a lot of interesting possibilities, as shown in this digital rain example:
Canvas
视图。从技术上讲,它不是一个动画视图,但当与第 4 部分的 TimelineView 结合使用时,它带来了许多有趣的可能性,如这个数字雨示例所示:I had to delay this article several weeks because the Canvas view was a little unstable. We are still in the beta period, so that was to be expected. However, the crashes produced by the view made some of the examples here not ready for sharing. Although not all issues were resolved, every example now runs smoothly. At the end of the article, I will point out some of the workarounds I found.
我不得不将这篇文章推迟几周,因为 Canvas 视图有点不稳定。我们仍处于测试阶段,所以这是意料之中的。但是,视图产生的崩溃使得此处的一些示例尚未准备好共享。虽然并非所有问题都得到了解决,但现在每个示例都运行得很顺利。在本文的最后,我将指出我发现的一些解决方法。
A Simple Canvas 简单的画布
In a nutshell, the canvas is a SwiftUI view that gets drawing instructions from a rendering closure. Unlike most closures in the SwiftUI API, it is not a view builder. This means there are no restrictions to the swift language we can use.
简而言之,画布是一个 SwiftUI 视图,它从渲染闭包中获取绘图指令。与 SwiftUI API 中的大多数闭包不同,它不是视图构建器。这意味着我们可以使用的 swift 语言没有任何限制。
The closure receives two parameters: context and size. The context uses a new SwiftUI type GraphicsContext, which packs a lot of methods and properties that will let us draw just about anything. Here’s a basic example of how a Canvas can be arranged:
闭包接收两个参数:context 和 size。上下文使用新的 SwiftUI 类型 GraphicsContext,它包含许多方法和属性,可以让我们绘制几乎任何内容。下面是如何排列 Canvas 的基本示例:
struct ContentView: View {
var body: some View {
Canvas { context, size in
let rect = CGRect(origin: .zero, size: size).insetBy(dx: 25, dy: 25)
// Path
let path = Path(roundedRect: rect, cornerRadius: 35.0)
// Gradient
let gradient = Gradient(colors: [.green, .blue])
let from = rect.origin
let to = CGPoint(x: rect.width + from.x, y: rect.height + from.y)
// Stroke path
context.stroke(path, with: .color(.blue), lineWidth: 25)
// Fill path
context.fill(path, with: .linearGradient(gradient,
startPoint: from,
endPoint: to))
}
}
}
The Canvas initializer has other parameters (opaque, colorMode and rendersAsynchronously). Refer to Apple’s documentation) to learn more about them.
Canvas 初始值设定项具有其他参数(opaque、colorMode 和 rendersAsynchronously)。请参阅 Apple 的文档以了解有关它们的更多信息。
The GraphicsContext
The GraphicsContext is full of methods and properties, but it is not my intention to make this post a reference where I list every one of them. It is a long list that can be a little overwhelming. However, when I was updating the Companion for SwiftUI app, I did have to go through all of them. That gave me an overall view. I will try to categorize what’s available, so you get the same picture.
GraphicsContext 充满了方法和属性,但我无意将这篇文章作为我列出每个方法和属性的参考。这是一个很长的清单,可能有点让人不知所措。但是,当我更新 Companion for SwiftUI 应用程序时,我确实必须完成所有这些操作。这给了我一个整体的看法。我将尝试对可用的内容进行分类,以便您获得相同的图片。
[Drawing Images and Text
绘制图像和文本](swiftui-lab.com/swif...)[Drawing Symbols (aka SwiftUI views)
绘制符号(又名 SwiftUI 视图)](swiftui-lab.com/swif...)[Mutating the Graphics Context
改变图形上下文](swiftui-lab.com/swif...)[Reusing CoreGraphics Code
重用 CoreGraphics 代码](swiftui-lab.com/swif...)
Paths 路径
The first thing you need to do to draw a path, is to create it. Since the first version of SwiftUI, a path can be created and modified in many ways. Some of the available initializers are:
要画出路径,你需要做的第一件事就是创建它。从 SwiftUI 的第一个版本开始,可以通过多种方式创建和修改路径。一些可用的初始值设定项包括:
let path = Path(roundedRect: rect, cornerSize: CGSize(width: 10, height: 50), style: .continuous)
let cgPath = CGPath(ellipseIn: rect, transform: nil)
let path = Path(cgPath)
let path = Path {
let points: [CGPoint] = [
.init(x: 10, y: 10),
.init(x: 0, y: 50),
.init(x: 100, y: 100),
.init(x: 100, y: 0),
]
$0.move(to: .zero)
$0.addLines(points)
}
Paths can also be created from a SwiftUI shape. The Shape protocol has a path method you may use to create one:
也可以从 SwiftUI 形状创建路径。Shape 协议有一个路径方法,您可以使用它来创建一个:
let path = Circle().path(in: rect)
Of course, this also works with custom shapes:
当然,这也适用于自定义形状:
let path = MyCustomShape().path(in: rect)
Filling a Path
To fill a path, use the context.fill()
method:
若要填充路径,请使用以下 context.fill()
方法:
fill(_ path: Path, with shading: GraphicsContext.Shading, style: FillStyle = FillStyle())
The shading indicates how to fill the shape (with a color, a gradient, a tiled image, etc.). Use the FillStyle type if you need to indicate the style to use (i.e., even odd/antialiased properties).
底纹指示如何填充形状(使用颜色、渐变、平铺图像等)。如果需要指示要使用的样式(即偶数奇数/抗锯齿属性),请使用 FillStyle 类型。
Stroking a Path
To stroke a path, use one of these GraphicsContext methods:
若要描边路径,请使用以下 GraphicsContext 方法之一:
stroke(_ path: Path, with shading: GraphicsContext.Shading, style: StrokeStyle)
stroke(_ path: Path, with shading: GraphicsContext.Shading, lineWidth: CGFloat = 1)
You may specify a shading (color, gradient, etc) to indicate how to stroke the line. Use style if you need to specify dash, line cap, join, etc. Alternatively, you can just specify a line width.
您可以指定阴影(颜色、渐变等)来指示如何描边线条。如果需要指定破折号、线帽、连接等,请使用样式。或者,您可以只指定线宽。
For a full example of how to stroke and fill a shape, see the example above (section A Simple Canvas).
有关如何描边和填充形状的完整示例,请参阅上面的示例(A Simple Canvas 部分)。
Images and Text 图像和文本
Images and Text are drawn using the context draw()
method, in one of its two versions:
图像和文本是使用 context draw()
方法绘制的,在其两个版本之一中:
draw(image_or_text, at point: CGPoint, anchor: UnitPoint = .center)
draw(image_or_text, in rect: CGRect)
In the case of images, there is an additional optional parameter for the second draw() version, style:
对于图像,第二个 draw() 版本还有一个额外的可选参数 style:
draw(image, in rect: CGRect, style: FillStyle = FillStyle())
Before one of these elements can be drawn, they must be resolved. By resolving, SwiftUI will take into account the environment (e.g., color scheme, display resolution, etc.). In addition, resolving these elements expose some interesting properties that may be further used in our drawing logic. For example, the resolved text will tell us the final size of the text for the specified font. Or we can also change the shading of the resolved element before drawing it. To learn more about the available properties and methods, check ResolvedImage and ResolvedText.
在绘制其中一个元素之前,必须对它们进行解析。通过解析,SwiftUI 将考虑环境(例如,配色方案、显示分辨率等)。此外,解析这些元素会暴露一些有趣的属性,这些属性可能会进一步用于我们的绘图逻辑。例如,解析的文本将告诉我们指定字体的文本的最终大小。或者我们也可以在绘制之前更改已解析元素的阴影。若要了解有关可用属性和方法的详细信息,请查看 ResolvedImage 和 ResolvedText。
Use the context resolve()
method to get a ResolvedImage
from Image
, and ResolvedText
from Text
.
使用 context resolve()
方法获取 ResolvedImage
from Image
和 ResolvedText
from Text
。
Resolving is optional, the draw()
method also accepts Image
and Text
(instead of ResolvedImage
and ResolvedText
). In that case, draw()
will resolve them automatically. This is convenient if you don’t have any use for the resolved properties and methods.
解析是可选的,该 draw()
方法还接受 Image
and Text
(而不是 ResolvedImage
和 ResolvedText
)。在这种情况下, draw()
将自动解决它们。如果对解析的属性和方法没有任何用处,这将很方便。
In this example, text is resolved. We use its size to figure out the gradient, and the shading to apply such gradient:
在此示例中,对文本进行解析。我们用它的大小来计算渐变,并使用阴影来应用这种渐变:
struct ExampleView: View {
var body: some View {
Canvas { context, size in
let midPoint = CGPoint(x: size.width/2, y: size.height/2)
let font = Font.custom("Arial Rounded MT Bold", size: 36)
var resolved = context.resolve(Text("Hello World!").font(font))
let start = CGPoint(x: (size.width - resolved.measure(in: size).width) / 2.0, y: 0)
let end = CGPoint(x: size.width - start.x, y: 0)
resolved.shading = .linearGradient(Gradient(colors: [.green, .blue]),
startPoint: start,
endPoint: end)
context.draw(resolved, at: midPoint, anchor: .center)
}
}
}
Symbols 符号
When talking about Canvas, symbols refer to just any SwiftUI. Do not confuse with SF Symbols, which is a completely different thing. The Canvas view has a way of referencing a SwiftUI view, resolve it into a symbol, and then draw it.
在谈论 Canvas 时,符号仅指任何 SwiftUI。不要与 SF 符号混淆,这是完全不同的事情。Canvas 视图可以引用 SwiftUI 视图,将其解析为元件,然后绘制它。
Views to resolved, are passed in a ViewBuilder closure, as shown in the example below. In order to reference a view, it needs to be tagged with a unique hashable identifier. Note that a resolved symbol can be drawn more than once on a Canvas.
要解析的视图在 ViewBuilder 闭包中传递,如下例所示。为了引用视图,需要使用唯一的可哈希标识符对其进行标记。请注意,解析的符号可以在 Canvas 上多次绘制。
struct ExampleView: View {
var body: some View {
Canvas { context, size in
let r0 = context.resolveSymbol(id: 0)!
let r1 = context.resolveSymbol(id: 1)!
let r2 = context.resolveSymbol(id: 2)!
context.draw(r0, at: .init(x: 10, y: 10), anchor: .topLeading)
context.draw(r1, at: .init(x: 30, y: 20), anchor: .topLeading)
context.draw(r2, at: .init(x: 50, y: 30), anchor: .topLeading)
context.draw(r0, at: .init(x: 70, y: 40), anchor: .topLeading)
} symbols: {
RoundedRectangle(cornerRadius: 10.0).fill(.cyan)
.frame(width: 100, height: 50)
.tag(0)
RoundedRectangle(cornerRadius: 10.0).fill(.blue)
.frame(width: 100, height: 50)
.tag(1)
RoundedRectangle(cornerRadius: 10.0).fill(.indigo)
.frame(width: 100, height: 50)
.tag(2)
}
}
}
The ViewBuilder can also use a ForEach. The same example can be rewritten like this:
ViewBuilder 还可以使用 ForEach。同样的例子可以像这样重写:
struct ExampleView: View {
let colors: [Color] = [.cyan, .blue, .indigo]
var body: some View {
Canvas { context, size in
let r0 = context.resolveSymbol(id: 0)!
let r1 = context.resolveSymbol(id: 1)!
let r2 = context.resolveSymbol(id: 2)!
context.draw(r0, at: .init(x: 10, y: 10), anchor: .topLeading)
context.draw(r1, at: .init(x: 30, y: 20), anchor: .topLeading)
context.draw(r2, at: .init(x: 50, y: 30), anchor: .topLeading)
context.draw(r0, at: .init(x: 70, y: 40), anchor: .topLeading)
} symbols: {
ForEach(Array(colors.enumerated()), id: \.0) { n, c in
RoundedRectangle(cornerRadius: 10.0).fill(c)
.frame(width: 100, height: 50)
.tag(n)
}
}
}
}
Animated Symbols 动画符号
I was pleasantly surprised when I tested what would happen if the View resolved as a symbol, is animated. Guess what, the Canvas will continuously redraw it to keep the animation going:
当我测试如果将视图解析为符号并动画化时会发生什么,我感到惊喜。你猜怎么着,Canvas 会不断地重新绘制它以保持动画的运行:
struct ContentView: View {
var body: some View {
Canvas { context, size in
let symbol = context.resolveSymbol(id: 1)!
context.draw(symbol, at: CGPoint(x: size.width/2, y: size.height/2), anchor: .center)
} symbols: {
SpinningView()
.tag(1)
}
}
}
struct SpinningView: View {
@State private var flag = true
var body: some View {
Text("?")
.font(.custom("Arial", size: 72))
.rotationEffect(.degrees(flag ? 0 : 360))
.onAppear{
withAnimation(.linear(duration: 1.0).repeatForever(autoreverses: false)) {
flag.toggle()
}
}
}
}
Mutating the Graphic Context 改变图形上下文
The graphics context can be mutated, using one of the following methods:
可以使用以下方法之一对图形上下文进行更改:
addFilter addFilter(添加过滤器)?changes=l_7_8_3)
If you are familiar with AppKit’s NSGraphicContext
, or CoreGraphic’s CGContext
you may be used to pushing (saving) and popping (restoring) graphics context states from a stack. The Canvas GraphicsContext works a little differently. If you want to make a temporary change to the context, you have several options.
如果您熟悉 AppKit 或 CoreGraphic NSGraphicContext
, CGContext
您可能习惯于从堆栈中推送(保存)和弹出(恢复)图形上下文状态。Canvas
GraphicsContext 的工作方式略有不同。如果要对上下文进行临时更改,可以使用多个选项。
To illustrate that, let’s see the following example. We need to draw three houses in three colors. Only the house in the middle, needs to be blurred:
为了说明这一点,让我们看看下面的例子。我们需要用三种颜色画三栋房子。只有中间的房子,需要模糊:
All the examples below will use the following CGPoint
extension:
下面的所有示例都将使用以下 CGPoint
扩展名:
extension CGPoint {
static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
}
Here are three ways of achieving the same result:
以下是实现相同结果的三种方法:
Sort Operations Accordingly 相应地对操作进行排序
When possible, you may choose to sort draw operations in a way that works for you. In this case, drawing the blurred house last, solves the problem. Otherwise, as soon as you add the blur filter, all draw operations will continue to blur.
如果可能,您可以选择以适合您的方式对绘制操作进行排序。在这种情况下,最后绘制模糊的房子可以解决问题。否则,一旦添加模糊滤镜,所有绘制操作都将继续模糊。
Sometimes this may not be possible, and even if it is, it may turn into code that is hard to read. If that’s the case, check the other options.
有时这可能无法实现,即使这是不可能的,它也可能变成难以阅读的代码。如果是这种情况,请检查其他选项。
struct ExampleView: View {
var body: some View {
Canvas { context, size in
// All drawing is done at x4 the size
context.scaleBy(x: 4, y: 4)
let midpoint = CGPoint(x: size.width / (2 * 4), y: size.height / (2 * 4))
var house = context.resolve(Image(systemName: "house.fill"))
// Left house
house.shading = .color(.red)
context.draw(house, at: midpoint - CGPoint(x: house.size.width, y: 0), anchor: .center)
// Right house
house.shading = .color(.blue)
context.draw(house, at: midpoint + CGPoint(x: house.size.width, y: 0), anchor: .center)
// Center house
context.addFilter(.blur(radius: 1.0, options: .dithersResult), options: .linearColor)
house.shading = .color(.green)
context.draw(house, at: midpoint, anchor: .center)
}
}
}
Work On a Copy 处理副本
Since the graphics context is a value type, you can simply create a copy. All changes made on the copy, will not affect the original context. As soon as you’re done, you can resume drawing on the original (unchanged) context.
由于图形上下文是值类型,因此只需创建副本即可。对副本所做的所有更改都不会影响原始上下文。完成后,您可以继续在原始(未更改)上下文上绘图。
struct ExampleView: View {
var body: some View {
Canvas { context, size in
// All drawing is done at x4 the size
context.scaleBy(x: 4, y: 4)
let midpoint = CGPoint(x: size.width / (2 * 4), y: size.height / (2 * 4))
var house = context.resolve(Image(systemName: "house.fill"))
// Left house
house.shading = .color(.red)
context.draw(house, at: midpoint - CGPoint(x: house.size.width, y: 0), anchor: .center)
// Center house
var blurContext = context
blurContext.addFilter(.blur(radius: 1.0, options: .dithersResult), options: .linearColor)
house.shading = .color(.green)
blurContext.draw(house, at: midpoint, anchor: .center)
// Right house
house.shading = .color(.blue)
context.draw(house, at: midpoint + CGPoint(x: house.size.width, y: 0), anchor: .center)
}
}
}
Use Layers 使用图层
Finally, you may use the context method drawLayer. The method has a closure that receives a copy of the context you can work with. All changes to the layer context will not affect the original context:
最后,您可以使用上下文方法 drawLayer。该方法具有一个闭包,用于接收可以使用的上下文的副本。对图层上下文的所有更改都不会影响原始上下文:
struct ExampleView: View {
var body: some View {
Canvas { context, size in
// All drawing is done at x4 the size
context.scaleBy(x: 4, y: 4)
let midpoint = CGPoint(x: size.width / (2 * 4), y: size.height / (2 * 4))
var house = context.resolve(Image(systemName: "house.fill"))
// Left house
house.shading = .color(.red)
context.draw(house, at: midpoint - CGPoint(x: house.size.width, y: 0), anchor: .center)
// Center house
context.drawLayer { layerContext in
layerContext.addFilter(.blur(radius: 1.0, options: .dithersResult), options: .linearColor)
house.shading = .color(.green)
layerContext.draw(house, at: midpoint, anchor: .center)
}
// Right house
house.shading = .color(.blue)
context.draw(house, at: midpoint + CGPoint(x: house.size.width, y: 0), anchor: .center)
}
}
}
Reusing CoreGraphics Code 重用 CoreGraphics 代码
If you already have CoreGraphics drawing code, you may use it. The Canvas context has a withCGContext
method to rescue you in such case:
如果您已经有 CoreGraphics 绘图代码,则可以使用它。在这种情况下,Canvas 上下文有一种 withCGContext
方法可以拯救您:
struct ExampleView: View {
var body: some View {
Canvas { context, size in
context.withCGContext { cgContext in
// CoreGraphics code here
}
}
}
}
Animating the Canvas 为画布添加动画
By wrapping the Canvas inside a TimelineView, we can achieve some pretty interesting animations. Basically, with each timeline update, you get the chance to draw a new frame of the animation.
通过将 Canvas 包装在 TimelineView 中,我们可以实现一些非常有趣的动画。基本上来说,每次时间线更新,你都有机会绘制动画的新帧。
The rest of the article asumes you are already familiar with TimelineView, but If you are not, you may check Part 4 of this series to learn more.
本文的其余部分假定您已经熟悉 TimelineView,但如果您不熟悉,可以查看本系列的第 4 部分以了解更多信息。
In the following example, our Canvas draws an analog clock for a given date. By putting the Canvas inside a TimelineView, and using the timeline update date, we get the animated clock. A part of the following screen capture is accelerated, to show how the minute and hour clock hands move, which otherwise would not be noticeable.
When we create animations with Canvas, it is common to use the TimelineSchedule .animation
. This updates as fast as possible, redrawing our Canvas several times per second. However, when possible, we should use the minimumInterval
parameter to limit the number of updates per second. This will be less demanding on the CPU. For example, in this case there is no visually noticeable difference between using .animation
, and .animation(minimumInterval: 0.06)
. However, on my testing hardware, CPU usage goes down from 30% to 14%. Using a higher minimumInterval may start to become visually noticeable, so you may have to do some trial an error, to find the best value.
当我们使用 Canvas 创建动画时,通常使用 TimelineSchedule .animation
。这会尽可能快地更新,每秒重新绘制我们的 Canvas 几次。但是,如果可能,我们应该使用该 minimumInterval
参数来限制每秒的更新次数。这对 CPU 的要求会降低。例如,在本例中,使用 .animation
和 之间在视觉上没有明显的区别 .animation(minimumInterval: 0.06)
。但是,在我的测试硬件上,CPU 使用率从 30% 下降到 14%。使用较高的 minimumInterval 可能会开始在视觉上变得明显,因此您可能需要进行一些错误试验,以找到最佳值。
To further improve performance, you should consider if there are parts of the Canvas
that do not need constant redrawing. In our example, only the clock hands move, the rest remains static. It is wise then, to split it into two overlapping canvases. One that draws everything except the clock hands (outside the TimelineView
), and another just for the clock hands, inside the TimelineView. By implementing that change, CPU goes down from 16% to 6%:
为了进一步提高性能,您应该考虑是否有不需要经常重绘的部分 Canvas
。在我们的示例中,只有时钟指针移动,其余的保持静止。因此,明智的做法是将其分成两幅重叠的画布。一个绘制除时钟指针(在 之外) TimelineView
之外的所有内容,另一个只绘制时钟指针,在 TimelineView 内。通过实施该更改,CPU 从 16% 下降到 6%:
struct Clock: View {
var body: some View {
ZStack {
ClockFaceCanvas()
TimelineView(.animation(minimumInterval: 0.06)) { timeline in
ClockHandsCanvas(date: timeline.date)
}
}
}
}
By careful analyzing our Canvas, and with little changes, we managed to improved CPU usage to be 5 times less demanding (from 30% to 6%). By the way, if you can live with a seconds clock hand that updates every second, you would further reduce CPU usage to less than 1%. You should experiment to find the best trade-off for your view.
通过仔细分析我们的 Canvas,并且几乎没有改动,我们设法将 CPU 使用率提高了 5 倍(从 30% 降低到 6%)。顺便说一句,如果你能忍受每秒更新一次的秒针,你会进一步将CPU使用率降低到1%以下。您应该进行试验,以找到适合您的观点的最佳权衡。
The full code for the clock can be found here.
时钟的完整代码可以在这里找到。
Divide and Conquer 分而治之
Once we learn about Canvas, we might be tempted to draw everything in it. However, sometimes the best option is to choose what to do and where. A good example is this Matrix Digital Rain animation below:
The full code for the digital rain can be found here.
数字降雨的完整代码可以在这里找到。
Let’s analyze what’s in it. We have columns of characters appearing, growing in numbers of characters, slowly sliding down and finally reducing its characters until they disappear. Each column is drawn with a gradient. There is also a sense of depth, by making the columns near to the observer slide faster and slightly larger. To increase the effect, the further back a column is, the more out of focus (blurred) it appears.
让我们分析一下其中的内容。我们出现了一列列字符,字符数量增加,慢慢向下滑动,最后减少字符,直到它们消失。每列都用渐变绘制。还有一种深度感,通过使靠近观察者的列滑动得更快、稍大一些。为了增加效果,柱子越靠后,它看起来越失焦(模糊)。
Implementing all these requirements inside the Canvas is absolutely possible. However, the task becomes much easier, if we split these tasks (divide and conquer). As we have seen already in the Animated Symbols section of this article, an animated SwiftUI view can be drawn into the Canvas with a single draw()
call. So not everything has to be dealt with inside the Canvas.
在 Canvas 中实现所有这些要求是绝对可能的。但是,如果我们拆分这些任务(分而治之),任务就会变得容易得多。正如我们在本文的动画元件部分已经看到的那样,只需一次 draw()
调用即可将动画 SwiftUI 视图绘制到 Canvas 中。因此,并非所有事情都必须在 Canvas 中处理。
Each column is implemented as a separate SwiftUI view. Stacking character and drawing with a gradient is handled by the view. When we use a gradient on the Canvas, the starting/ending point or any other geometry parameter is relative to the entire Canvas. For the column gradient, it is easier to implement it inside the view, as it will be relative to the view’s origin.
每一列都作为单独的 SwiftUI 视图实现。使用渐变堆叠字符和绘图由视图处理。当我们在 Canvas 上使用渐变时,起点/终点或任何其他几何参数都是相对于整个 Canvas 的。对于列渐变,在视图中实现它更容易,因为它将相对于视图的原点。
Each column has many parameters: position (x, y, z), characters, how many characters from the top are removed, etc. These values are advanced after each TimelineView update.
每列都有许多参数:位置(x、y、z)、字符、从顶部删除的字符数等。这些值在每次 TimelineView 更新后都会提前。
Finally the Canvas is in charge of resolving each view, drawing them in their (x, y) positions, and adding a blur and scale effect based on its z value. I added some comments to the code to help you navigate through it, should you be interested.
最后,Canvas 负责解析每个视图,将它们绘制在它们的 (x, y) 位置,并根据其 z 值添加模糊和缩放效果。如果您有兴趣,我在代码中添加了一些注释,以帮助您浏览它。
Canvas Crashes Canvas 崩溃
Unfortunately, at the time of this writing, I have experienced some crashes with the Canvas. Fortunately, they improved a lot with each beta release. I’m hoping they all get sorted out by the time iOS15 is officially released. The message is usually something like this:
不幸的是,在撰写本文时,我遇到了一些 Canvas 崩溃的情况。幸运的是,他们在每个测试版中都有很大的改进。我希望它们在 iOS15 正式发布时都能得到解决。消息通常是这样的:
-[MTLDebugRenderCommandEncoder validateCommonDrawErrors:]:5252: failed assertion `Draw Errors Validation
Fragment Function(primitive_gradient_fragment): argument small[0] from buffer(3) with offset(65460) and length(65536) has space for 76 bytes, but argument has a length(96).
I managed to workaround these crashes, using at least one of these actions:
我设法解决了这些崩溃问题,至少使用了以下操作之一:
Reduce the amount of drawing. In the digital rain example, you may reduce the number of columns.
减少绘图量。在数字雨示例中,您可以减少列数。Use simpler gradients. Originally, the digital rain columns had three color gradients. The crash disappeared when I reduced it to two.
使用更简单的渐变。最初,数字雨柱有三种颜色渐变。当我将其减少到两个时,崩溃就消失了。Update that Canvas less frequently. Using a slower TimelineView, can prevent the crash.
更新该 Canvas 的频率较低。使用较慢的 TimelineView 可以防止崩溃。
I am not saying you cannot use gradients with more than two colors, but that is just one place where you can look, if you find yourself in a situation where the Canvas crashes. If that does not fix your problem, I suggest you start to remove drawing operations until the app no longer crashes. That can lead you to find what is causing the crash. Once you know what it is, you may try to do it differently.
我并不是说你不能使用两种以上颜色的渐变,但这只是你可以看的一个地方,如果你发现自己处于 Canvas 崩溃的情况。如果这不能解决您的问题,我建议您开始删除绘图操作,直到应用程序不再崩溃。这可能会导致您找到导致崩溃的原因。一旦你知道它是什么,你可以尝试以不同的方式去做。
If you encounter this problem, I encourage you to report it to Apple. If you want, you can reference my own report: FB9363322.
如果您遇到此问题,我鼓励您向Apple报告。如果你愿意,你可以参考我自己的报告:FB9363322。
Summary 总结
I hope this post helped you add a new tool to your SwiftUI animation toolbox. This concludes the fifth part animations series. At least for this year… Who knows what WWDC’22 will bring!
我希望这篇文章能帮助您将新工具添加到您的 SwiftUI 动画工具箱中。动画系列的第五部分到此结束。至少今年......谁知道 WWDC'22 会带来什么!