【SwiftUI】Timerを使ってコマ送りアニメを作ってみた

2022.02.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

無性にTimerを使ってコマ送りアニメを作りたかったので作ってみました。

環境

  • Xcode 13.2.1

デモ

作ったものはこんな感じのコマ送りです。

アセット

proposal0 ~ proposal8までの計9の画像を用意しました。

コード

ContentViewのコードはこのようになっています。

import SwiftUI

struct ContentView: View {

    @State var index: Int = 0
    private let timer = Timer.publish(every: 0.3, on: .main, in: .common).autoconnect()

    var body: some View {

        Image("proposal\(index)")
            .resizable()
            .frame(width: 300, height: 300)
            .padding()
            .onReceive(timer) { _ in
                index += 1
                if index >= 8 {
                    timer.upstream.connect().cancel()
                }
            }
    }
}

index

今回コマ送りに使用するImageのインデックスとして使用します。

@State var index: Int = 0

@Stateをプロパティに付けることで、そのプロパティの変更がSwiftUIによってモニタリングされます。

モニタリングしたプロパティに変更があった場合、対象プロパティを参照しているViewは自動的に再描画されるので、indexの値が増える度に画像が切り替わっていくような動きを実現できます。

Timer

カウントダウンアプリなどで使われるTimerです。scheduledTimerがお馴染みですが、今回はTimerPublisherを使ってみました。

private let timer = Timer.publish(every: 0.3, on: .main, in: .default).autoconnect()

このTimerPublisherは現在の時刻を与えられた間隔で繰り返しpublishしてくれるPublisherになります。

ちなみにpublishとは、発行する、発表するという意味になります。

このTimerPublisherがイベントの発行を開始するには、autoconnect()またはconnect()を呼ぶ必要があり、autoconnect()を呼ぶことで接続処理を自動的に行なってくれます。

publish

Timer.publishの引数についてですが、下記にようになっています。

static func publish(every interval: TimeInterval, tolerance: TimeInterval? = nil, on runLoop: RunLoop, in mode: RunLoop.Mode, options: RunLoop.SchedulerOptions? = nil) -> Timer.TimerPublisher
  • every : 何秒毎にイベントを発行するか
  • tolerance : この値を設定するとTimerは発火のタイミングを遅らせることが出来ます。デフォルト値はnil
  • on : RunLoopを実行するスレッド
  • mode : RunLoop.Modeの設定
  • options : 実行するループスケジューラーの任意オプションになります。

今回は、Timer.publish(every: 0.3, on: .main, in: .common)と使用していますが、0.3秒毎にmainスレッドでcommonのモードでイベントを発行するという意味になります。

Image

コマ送りを表現するためのImageです。indexの値が変更されるとImageも変更されます。

Image("proposal\(index)")
    .resizable()
    .frame(width: 300, height: 300)
    .padding()

onReceive

onRecieveは第一引数にPublisherを渡すことで、クロージャーの中ではそのPublisherがイベントを発行する度に実行したい処理を追加します。

今回は、純粋にindexを増やす処理を記述しています。また、画像はproposal8までしかないのでindex8以上になるとtimer.upstream.connect().cancel()TimerPublisherのイベント発行をキャンセルしています。

.onReceive(timer) { _ in
    index += 1
    if index >= 8 {
        timer.upstream.connect().cancel()
    }
}

おわりに

Timerを使ってコマ送りアニメを作ることができました!

さて、プロポーズの結果はいかに?

参考