使用SwiftUI绘图组提高性能


今年圣诞节又来了。

去年,我使用Core Animation创建了一棵圣诞树。
https://qiita.com/shiz/items/10cb712a26620f2e3bdc

所以
今年,我想使用Swift UI创建一棵圣诞树。

我在SwiftUI中使用了很多元素,但是
我要注意drawingGroup

创建适合形状的树形部分

首先,我们将为树制作必要的部分。
将形状定义为符合Shape协议的结构
合并它们以构建视图。

スクリーンショット 2019-12-01 10.46.06.png

我提到了以下站点。
https://www.hackingwithswift.com/quick-start/swiftui/how-to-draw-polygons-and-stars

path方法中生成Path类。
可以使用类似于UIBezierPath的方式来设置路径规范。

<详细信息>

代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct Star: Shape {
    let corners: Int
    let smoothness: CGFloat

    func path(in rect: CGRect) -> Path {
        guard corners >= 2 else { return Path() }

        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)

        var currentAngle = -CGFloat.pi / 2

        let angleAdjustment = .pi * 2 / CGFloat(corners * 2)

        let innerX = center.x * smoothness
        let innerY = center.y * smoothness

        var path = Path()

        path.move(to: CGPoint(x: center.x * cos(currentAngle), y: center.y * sin(currentAngle)))

        var bottomEdge: CGFloat = 0

        for corner in 0..<corners * 2  {
            let sinAngle = sin(currentAngle)
            let cosAngle = cos(currentAngle)
            let bottom: CGFloat

            if corner.isMultiple(of: 2) {
                bottom = center.y * sinAngle
                path.addLine(to: CGPoint(x: center.x * cosAngle, y: bottom))
            } else {
                bottom = innerY * sinAngle
                path.addLine(to: CGPoint(x: innerX * cosAngle, y: bottom))
            }
            if bottom > bottomEdge {
                bottomEdge = bottom
            }
            currentAngle += angleAdjustment
        }
        let unusedSpace = (rect.height / 2 - bottomEdge) / 2
        let transform = CGAffineTransform(translationX: center.x, y: center.y + unusedSpace)
        return path.applying(transform)
    }
}

星期四

接下来是木制部分。

スクリーンショット 2019-12-01 10.46.46.png

树形部分

首先,在绿色部分中定义三角形Triangle。

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Triangle: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            let middle = rect.midX
            let width: CGFloat = rect.size.width
            let height = rect.height
            path.move(to: CGPoint(x: middle, y: 0))
            path.addLine(to: CGPoint(x: middle + (width / 2), y: height))
            path.addLine(to: CGPoint(x: middle - (width / 2), y: height))
            path.addLine(to: CGPoint(x: middle, y: 0))
        }
    }
}

白色线性装饰物

在树上创建装饰。

这次使用addQuadCurve
有点弯曲。

https://developer.apple.com/documentation/swiftui/path/3271274-addquadcurve

<详细信息>

代码

1
2
3
4
5
6
7
8
9
struct Slope: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: rect.minX, y: rect.midY))
            path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY),
                              control: CGPoint(x: rect.midX * 0.8, y: rect.midY * 0.8))
        }
    }
}

球形装饰物

接下来,进行球形装饰。
这是Circle
使用GradientLinierGradient
颜色是渐变。

https://developer.apple.com/documentation/swiftui/gradient
https://developer.apple.com/documentation/swiftui/lineargradient

指定初始化Gradient时要渐变的颜色。
您可以直接指定要更改的位置,但是
如果未指定,
似乎该框架会自动对其进行调整。

LinearGradient
指定Gradient以及开始和结束位置。

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
struct BallView: View {
    let gradientColors = Gradient(colors: [Color.pink, Color.purple])
    var body: some View {
        let linearGradient = LinearGradient(
            gradient: gradientColors,
            startPoint: .top, endPoint: .bottom)
        return Circle()
            .fill(linearGradient)
    }
}

并结合多个这些以创建一个View。

使用

GeometryReader
BallView通常放置在View中。

https://developer.apple.com/documentation/swiftui/geometryreader

不要伸出三角形
我正在使用mask来限制BallView绘制的程度。
https://developer.apple.com/documentation/swiftui/view/3278595-mask

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct BallsSlopeView<Mask: View>: View {
    let drawArea: Mask
    var body: some View {
        GeometryReader { gr in
            ForEach(1...10, id: \.self) { index in
                BallView()
                    .position(
                        self.getPosition(at: index,
                                         midX: gr.frame(in: .local).maxX,
                                         midY: gr.frame(in: .local).maxY))
                    .frame(height: 20)
            }
        }
        .mask(drawArea)
    }

    private func getPosition(at index: Int, midX: CGFloat, midY: CGFloat) -> CGPoint{
        let x = midX * CGFloat(1 - CGFloat(index) * 0.1)
        let y = midY * CGFloat(1 - CGFloat(index) * 0.05)
        return CGPoint(x: x, y: y)
    }
}

结合

最后,结合上面制作的零件。

ZStack将所有内容分组并堆叠。
在白色装饰部分
通过利用rotationEffect
我旋转一点,使其挂在树上。

https://developer.apple.com/documentation/swiftui/scaledshape/3273943-rotationeffect

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct TreeView: View {
    var body: some View {
        GeometryReader { gr in
            ZStack(alignment: .center) {
                Triangle()
                    .foregroundColor(Color.green)
                Slope()
                    .stroke(lineWidth: 20)
                    .mask(Triangle())
                    .foregroundColor(Color.white)
                Slope()
                    .stroke(lineWidth: 20)
                    .rotationEffect(Angle.degrees(300))
                    .mask(Triangle())
                    .foregroundColor(Color.white)
                BallsSlopeView(drawArea: Triangle())
            }
        }
    }
}

底座

スクリーンショット 2019-12-01 10.47.10.png

接下来,制作基础部分。

将白线Shape放在

Rectangle内。

Shape创建白线。

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct FoundationLine: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: 0, y: rect.maxY / 3))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY / 3))
            path.move(to: CGPoint(x: 0, y: rect.maxY * 2 / 3))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY * 2 / 3))
            path.move(to: CGPoint(x: rect.width / 3, y: 0))
            path.addLine(to: CGPoint(x: rect.width / 3, y: rect.maxY))
            path.move(to: CGPoint(x: rect.width * 2 / 3, y: 0))
            path.addLine(to: CGPoint(x: rect.width * 2 / 3, y: rect.maxY))
        }
    }
}

将白线和上面创建的RectangleZStack分组。

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct FoundationView: View {
    var body: some View {
        GeometryReader { gr in
            ZStack {
                Rectangle()
                    .foregroundColor(Color.red)
                FoundationLine()
                    .stroke(lineWidth: 3)
                    .foregroundColor(Color.white)
                    .mask(Rectangle())
            }
            .frame(width: gr.size.width / 3, height: gr.size.width / 3)
        }
    }
}

组装一棵圣诞树

中,组合您到目前为止所做的。
调整位置,使星星和个别树木一点一点地重叠。


如果是这样
树木的三角形如何重叠
因为它是反向的(上部顶点部分似乎在顶部重叠)
重叠的方式由zIndex更改。
https://developer.apple.com/documentation/swiftui/view/3278679-zindex

<详细信息>

代码

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
27
28
29
30
31
32
33
struct ChristmasTree: View {
    var body: some View {
        GeometryReader { gr in
            VStack(spacing: -12) {
                VStack(spacing: -(gr.size.width * 0.1)) {
                    Star(corners: 5, smoothness: 0.5)
                        .foregroundColor(Color.yellow)
                        .frame(width: gr.size.width * 0.3,
                               height: gr.size.width * 0.3)
                        .zIndex(2)
                    ZStack {
                        VStack(spacing: -(gr.size.width / 5)) {
                            TreeView()
                                .frame(width: gr.size.width * 0.6)
                                .zIndex(3)
                            TreeView()
                                .frame(width: gr.size.width * 0.7)
                                .zIndex(2)
                            TreeView()
                                .frame(width: gr.size.width * 0.8)
                                .zIndex(1)
                        }
                        .frame(height: gr.size.height * 0.5)
                        .foregroundColor(Color.green)
                    }
                    .zIndex(1)
                }
                FoundationView()
                    .frame(height: gr.size.height * 0.2)
            }
        }
    }
}

背景

スクリーンショット 2019-12-01 10.50.50.png

接下来,我们将创建背景。

Circle放置在随机大小,不透明度和位置中。

<详细信息>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Particles: View {
    var body: some View {
        GeometryReader { gr in
            ZStack {
                ForEach(0...200, id: \.self) { _ in
                    Circle()
                        .foregroundColor(.red)
                        .opacity(.random(in: 0.1...0.4))
                        .frame(width: .random(in: 10...100),
                               height: .random(in: 10...100))
                        .position(x: .random(in: 0...gr.size.width),
                                  y: .random(in: 0...gr.size.height))
                }
            }
        }
    }
}

设置背景动画

因为这很重要,所以请在背景中添加动画
让我们使其更加豪华(?)。

这次,我使用了名为interactiveSpringAnimation

*
尝试各种值
我设置了一个我认为是这样的值,所以
合适的。

https://developer.apple.com/documentation/swiftui/animation/3344959-interactivespring

显示屏幕时引起动画的处理

设置SwiftUI动画时的注意事项
只需设置动画
动画无法开始。

在显示屏幕时生成此
例如,带有@State的变量是
通过更改onAppear
在View中动态更改值并重新呈现,等等。
需要处理。

<详细信息>

代码

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
27
28
29
30
31
32
33
34
35
struct Particles: View {
    // レンダリングを起こすために必要
    @State private var scaling = false

    var animation: Animation {
        Animation
            .interactiveSpring(response: 5, dampingFraction: 0.5)
            .repeatForever()
            .speed(.random(in: 0.05...0.9))
            .delay(.random(in: 0...2))
    }

    var body: some View {
        GeometryReader { gr in
            ZStack {
                ForEach(0...200, id: \.self) { _ in
                    Circle()
                        .foregroundColor(.red)
                        .opacity(.random(in: 0.1...0.4))
                        .animation(self.animation)
                        // この値を変化させることで再レンダリングを起こしている
                        .scaleEffect(self.scaling ? .random(in: 0.1...2) : 1)
                        .frame(width: .random(in: 10...100),
                               height: .random(in: 10...100))
                        .position(x: .random(in: 0...gr.size.width),
                                  y: .random(in: 0...gr.size.height))
                }
            }
            .onAppear {
                // レンダリングを起こすために必要
                self.scaling = true
            }
        }
    }
}

提高性能

屏幕已在上方完成,但
有一个问题。

通过处理上述动画
内存使用量将稳定增长。

drawNo_480.gif

*此后它将继续增加。

这是
当ZStack中的每个视图绘制时
在每个图层上构建图层。

那么使用那么多内存的结果
CPU上的负载将增加。

这可能会导致应用性能下降。

因此可以使用drawingGroup减少负载。

drawingGroup

https://developer.apple.com/documentation/swiftui/group/3284805-drawinggroup

此方法是
视图中的所有视图
在屏幕外看不见的屏幕外
使用Metal API
在一张图片中绘制在一起
它将最终内容输出到屏幕。

这将减少内存上的负载
您可以提高性能。

<详细信息>

代码

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
27
28
29
30
31
32
33
34
35
36
37
struct Particles: View {
    // レンダリングを起こすために必要
    @State private var scaling = false

    var animation: Animation {
        Animation
            .interactiveSpring(response: 5, dampingFraction: 0.5)
            .repeatForever()
            .speed(.random(in: 0.05...0.9))
            .delay(.random(in: 0...2))
    }

    var body: some View {
        GeometryReader { gr in
            ZStack {
                ForEach(0...200, id: \.self) { _ in
                    Circle()
                        .foregroundColor(.red)
                        .opacity(.random(in: 0.1...0.4))
                        .animation(self.animation)
                        // この値を変化させることで再レンダリングを起こしている
                        .scaleEffect(self.scaling ? .random(in: 0.1...2) : 1)
                        .frame(width: .random(in: 10...100),
                               height: .random(in: 10...100))
                        .position(x: .random(in: 0...gr.size.width),
                                  y: .random(in: 0...gr.size.height))
                }
            }
            // ここに設定をする
            .drawingGroup()
            .onAppear {
                // レンダリングを起こすために必要
                self.scaling = true
            }
        }
    }
}

draw_480.gif

*
要注意的一点
drawingGroup文档具有以下描述。

1
Views backed by native platform views don’t render into the image.

本机平台视图
drawingGroup似乎没有任何作用。

此本机平台视图是什么?
我不知道这是什么意思,但是
根据苹果公司在推特上的回答
看来是NSView或UIView。

https://twitter.com/jsh8080/status/1137045666939768833

概括

作为出现日历的资料
制作圣诞树时
我们已经研究了Swift UI的一些功能。

Swift UI声明性地制作了小零件
因为您可以将其合并
我再次感到它是高度可重用且易于阅读的。