ottijp blog

SwiftUIで使えるConfetti(紙吹雪)ビューを作ってSPMパッケージで公開した

  • 2023-12-01

これはInfocom Advent Calendar 2023 1日目の記事です.

いま作っているiOSアプリでConfetti(紙吹雪)の演出が必要なのですが,ライブラリが見つからなかったので作りました.

コードはこちらに置いてあります.

cf. ottijp/confetti-swiftui

使い方

まず,XcodeにこのSPMパッケージを追加してください.

  1. XcodeのメニューからFile > Add Package Dependencies...を選択してください.
  2. 検索ボックスにhttps://github.com/ottijp/confetti-swiftuiと入力してください.
  3. confetti-swiftuiが表示されるので選択してください.
  4. Dependency RuleからUp to Next Major Versionを選択してください.(任意ですが推奨)

add-package

次に,使いたい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感に乏しい描画になりました. そこで,SKTranformNodeSKNodeをラップすることで,XYZの3軸での回転に対応するようにしました.

Z軸での回転を行うSKShapeNodeを作る関数
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
}
XYZ軸での回転を行うSKFormatNodeを作る関数
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の背景色設定を行う.
作成したSKSceneクラスのオブジェクトを内包するビュー(抜粋)
public var body: some View {
  GeometryReader {
    SpriteView(scene: ConfettiScene(size: $0.size, emissionDuration: emissionDuration), options: [.allowsTransparency])
      .background(.clear)
      .ignoresSafeArea()
      .allowsHitTesting(false)
  }
}
作成したSKSceneのクラス(抜粋)
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

今後のアップデート案

紙吹雪のカラースキームをいくつか用意して選択できるようにしたり,紙吹雪の生成量や落下速度などをプロパティでコントロールできるようにしたいです.


ottijp
都内でアプリケーションエンジニアをしています
© 2024, ottijp