SwiftUIで使えるConfetti(紙吹雪)ビューを作ってSPMパッケージで公開した
これはInfocom Advent Calendar 2023 1日目の記事です.
いま作っているiOSアプリでConfetti(紙吹雪)の演出が必要なのですが,ライブラリが見つからなかったので作りました.
コードはこちらに置いてあります.
使い方
まず,XcodeにこのSPMパッケージを追加してください.
- Xcodeのメニューから
File
>Add Package Dependencies...
を選択してください. - 検索ボックスに
https://github.com/ottijp/confetti-swiftui
と入力してください. confetti-swiftui
が表示されるので選択してください.Dependency Rule
からUp to Next Major Version
を選択してください.(任意ですが推奨)
次に,使いたいSwiftUIのビューでConfetti
モジュールをインポートし,ビュースタックにConfettiView
を追加してください.
表示やユーザインタラクションを透過的に作っているので,他のビューとZStackで重ねて使うことを想定しています.
import SwiftUI
import Confetti
struct SampleView1: View {
var body: some View {
ZStack {
Text("Confetti")
ConfettiView()
}
}
}
表示タイミングの制御や紙吹雪を降らせる時間を設定する方法はREADMEに書いているので読んでください.
工夫した点
XYZの3軸回転
紙吹雪の生成・表示にはSpriteKitを使っているのですが,SKShapeNode
はZ軸での回転にしか対応しておらず,3D感に乏しい描画になりました.
そこで,SKTranformNode
でSKNode
をラップすることで,XYZの3軸での回転に対応するようにしました.
private func createConfettiNode() -> SKNode {
let node = SKShapeNode(path: .init(rect: CGRect(origin: .zero, size: CGSize(width: 100, height: 50)), transform: nil), centered: true)
node.fillColor = SKColor.blue
node.strokeColor = .clear
// z-rotation action
let rotationActionZ = SKAction.customAction(withDuration: abs(2.0)) { (node: SKNode, time: CGFloat) -> Void in
(node as! SKShapeNode).zRotation = (time / 2.0) * 2 * CGFloat(Double.pi)
}
// add actions to node
node.run(SKAction.repeatForever(rotationActionZ))
return node
}
private func createConfettiNode() -> SKNode {
let node = SKShapeNode(path: .init(rect: CGRect(origin: .zero, size: CGSize(width: 100, height: 50)), transform: nil), centered: true)
node.fillColor = SKColor.red
node.strokeColor = .clear
let tranformNode = SKTransformNode()
tranformNode.addChild(node)
// z-rotation action
let rotationActionX = SKAction.customAction(withDuration: abs(1.5)) { (node: SKNode, time: CGFloat) -> Void in
(node as! SKTransformNode).xRotation = (time / 1.5) * 2 * CGFloat(Double.pi)
}
// y-rotation action
let rotationActionY = SKAction.customAction(withDuration: abs(3.0)) { (node: SKNode, time: CGFloat) -> Void in
(node as! SKTransformNode).yRotation = (time / 3.0) * 2 * CGFloat(Double.pi)
}
// z-rotation action
let rotationActionZ = SKAction.customAction(withDuration: abs(2.0)) { (node: SKNode, time: CGFloat) -> Void in
(node as! SKTransformNode).zRotation = (time / 2.0) * 2 * CGFloat(Double.pi)
}
// add actions to node
tranformNode.run(SKAction.repeatForever(rotationActionX))
tranformNode.run(SKAction.repeatForever(rotationActionY))
tranformNode.run(SKAction.repeatForever(rotationActionZ))
return tranformNode
}
奥行き感
せっかくなのでもう少し奥行き感を出すために,大きさ(スケール)の時間変化や大きさ(Z軸方向の距離)による落下スピードの変化などを入れ,3D空間に紙吹雪が舞って見えるように工夫しました.
// move action
// biger(near) faster, smaller(far) slower
let moveSpeed = pow(size.width, 1.2) * 8
let moveAction = SKAction.move(by: CGVector(dx: cos(direction) * moveSpeed, dy: sin(direction) * moveSpeed), duration: 1.0)
// scale action
let scaleAction = SKAction.scale(by: scaleSpeed, duration: 1.0)
// add actions to node
transformNode.run(SKAction.repeatForever(moveAction))
transformNode.run(SKAction.repeatForever(scaleAction))
ユーザインタラクションと表示の透過
紙吹雪のビュー自体をタップすることはないでしょうし,紙吹雪の表示中も後ろにあるビューは見え続け操作できることが望まれると考え,表示を透過させユーザインタラクションを無効にしました.これは,以下のようにすることで実現できました.
SpriteView
のビューモディファイアで.background(.clear)
を追加する.SpriteView
のビューモディファイアで.allowHitTesting(false)
を追加する.- 作成した
SKScene
のクラスのdidMove()
で自身の背景色とSKView
の背景色設定を行う.
public var body: some View {
GeometryReader {
SpriteView(scene: ConfettiScene(size: $0.size, emissionDuration: emissionDuration), options: [.allowsTransparency])
.background(.clear)
.ignoresSafeArea()
.allowsHitTesting(false)
}
}
override func didMove(to view: SKView) {
// make background transparent
backgroundColor = .clear
view.allowsTransparency = true
view.backgroundColor = .clear
}
SPMパッケージ化
SPMパッケージの作成方法は以前ブログに書いたので,そちらを参照してください.
cf. SPM (Swift Package Manager) でアルファベット読み仮名変換ライブラリを作る | ottijp blog
今後のアップデート案
紙吹雪のカラースキームをいくつか用意して選択できるようにしたり,紙吹雪の生成量や落下速度などをプロパティでコントロールできるようにしたいです.