今年もクリスマスが近いてきました。
昨年はCore Animationを使ってクリスマスツリーを作成してみました。
https://qiita.com/shiz/items/10cb712a26620f2e3bdc
そこで
今年はSwiftUIを使ってクリスマスツリーを作成したいと思います。
SwiftUIの要素はたくさん使用していますが
drawingGroupに注目したいと思います。
Shapeに適合させツリーのパーツを作成する
まずツリーに必要なパーツを作っていきます。
図形をShapeプロトコルに適合させたstructとして定義して
それを組み合わせてViewを構築します。
星
こちらは下記のサイトを参照させて頂きました。
https://www.hackingwithswift.com/quick-start/swiftui/how-to-draw-polygons-and-stars
pathメソッドの中でPathクラスを生成します。
Pathの指定はUIBezierPathと似たような形で設定できます。
コード
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)
}
}
木
次に木の部分です。
木の形の部分
まずは緑の部分の三角形Triangleを定義します。
コード
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を使用して
ちょっと曲線にしています。
コード
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に
GradientとLinierGradientを使って
色をグラデーションしています。
https://developer.apple.com/documentation/swiftui/gradient
https://developer.apple.com/documentation/swiftui/lineargradient
Gradientの初期化時にグラデーションさせたい色を指定します。
変化させる位置を直接指定することもできますが
指定しない場合は
フレームワークで自動で調整してくれるようです。
LinearGradientは
Gradientと開始と終了位置を指定します。
コード
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を使って
Viewの中に規則的にBallViewを配置しています。
三角形をはみ出さないように
maskを使用してBallViewを描画する範囲を限定しています。
https://developer.apple.com/documentation/swiftui/view/3278595-mask
コード
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を活用することで
少し回転させて木にかかっているようにしています。
コード
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())
}
}
}
}
土台
次に土台の部分を作ります。
Rectangleの中に白い線のShapeを載せます。
白い線はShapeで作成します。
コード
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))
}
}
}
上記で作った白い線とRectangleをZStackでグループにします。
コード
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
コード
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)
}
}
}
}
背景
次に背景を作成していきます。
Circleをランダムな大きさとopacityと位置に配置します。
コード
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))
}
}
}
}
}
背景にアニメーションを設定する
せっかくなので背景にアニメーションをつけて
もう少し豪華(?)にしてみます。
今回はinteractiveSpringというAnimationを利用しました。
※
値は色々と触ってみて
こんな感じなのかなと思った値を設定しているので
適当です。
画面表示時にアニメーションを起こすための処理
SwiftUIのアニメーションを設定する上で注意したい点として
単純にアニメーションを設定しただけでは
アニメーションが起動しません。
これを画面表示時に発生させるためには
例えば@Stateを付けたの変数を
onAppearの中で変更することで
Viewの中の値を動的に変更させて再レンダリングさせるなどの
処理が必要になります。
コード
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
}
}
}
}
パフォーマンスを向上させる
画面としては上記で完成ですが
一つ問題があります。
上記のアニメーションの処理を行ったことによって
メモリの使用量がどんどん増えていきます。
※ この後もずっと増えていきます。
これは
ZStackの中の各Viewが描画をする際に
それぞれでレイヤーを構築します。
そうするとその分のメモリを使用する結果
CPUへの負荷大きくなります。
これはアプリのパフォーマンスの低下を招くことがあります。
そこでdrawingGroupを使って負荷を減らすことができます。
drawingGroup
このメソッドは
Viewの中の全てのViewを
画面上には見えないオフスクリーン上で
Metal APIを使用して
一つのイメージにまとめて描画し
最終的な内容を画面に出力するようにしてくれます。
こうすることでメモリへの負荷を軽減させて
パフォーマンスを向上させることができます。
コード
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
}
}
}
}
※
一つ注意点として
drawingGroupのドキュメントに下記のような記載あります。
Views backed by native platform views don’t render into the image.
native platform viewsには
drawingGroupは効果がないようです。
このnative platform viewsとは
何を指すのかわからなかったのですが
twitter上でAppleの方が回答されていた内容によると
NSViewやUIViewのことのようです。
まとめ
アドベントカレンダーのネタとして
クリスマスツリーを作っていく中で
SwiftUIの機能についていくつか見ていきました。
SwiftUIは宣言的に小さい部品を作り
それを組み合わせていくことができるので
再利用性が高く読みやすいなと改めて感じました。





