😸

【SwiftUI】タイマーを別ファイル化して再利用する方法

2025/02/19に公開

事の始まり

SwiftUIを最近勉強し始めたのですが、タイマーを使ったチュートリアルで大体整理されることなく必要なところにいちいちタイマーの記述をしているのが気になりました。
そのままでもそんなに記述が煩雑になるわけではないのですが、なんとなく別のファイルに分けて再利用したほうがいいんじゃないかと思って書き残します。

これまでの記述

いろいろ書き方はあるみたいですが、私は下記のような書き方をしていました。
更新時間を変えたいだけなのに、面倒です。

import Combine

var timer = Timer.publish(every: 1, on: .main, in: .common)
var cancellableSet = Set<AnyCancellable>()

func startTimer() {
    cancellableSet.removeAll() //念の為のタイマー解除
    timer = Timer.publish(every: 1, on: .main, in: .common)

    timer
        .autoconnect()
        .sink { _ in
            // 実行したい処理
            print("タイマー実行中") 
        }
        .store(in: &cancellableSet)
}

// タイマーの速度を変更する
func changeTimer(newInterval: TimeInterval) {
    cancellableSet.removeAll() //前のタイマー解除
    timer = Timer.publish(every: newInterval, on: .main, in: .common) // 新しい間隔で再定義

    timer
        .autoconnect()
        .sink { _ in
            // 実行したい処理
            print("時間変更後のタイマー実行中")
        }
        .store(in: &cancellableSet)
}

改善提案

timerを別ファイルに切り出す

TimerClass.swiftファイルを作成して以下を記述しました。

TimerClass.swift
import Foundation
import Combine
import SwiftUI

class TimerClass {

    private var timerPublisher: Timer.TimerPublisher
    private var cancellableSet: Set<AnyCancellable> = []
    private var timerInterval: TimeInterval
    private var task: () -> Void // // 外部から指定された関数用
    
    // 初期化
    init(interval: TimeInterval, task: @escaping () -> Void) {
        self.timerInterval = interval
        self.task = task
        self.timerPublisher = Timer.publish(every: interval, on: .main, in: .common)
        startTimer()
    }
    
    // タイマー開始
    func startTimer() {
        stopTimer()
        timerPublisher
            .autoconnect()
            .sink { [weak self] _ in
                self?.executeTimerTask()
            }
            .store(in: &cancellableSet)
    }
    
    // タイマー停止
    func stopTimer() {
        cancellableSet.removeAll()
    }
    
    // 実行する関数を指定する
    func updateTask(newTask: @escaping () -> Void) {
        self.task = newTask
    }
    
    // 実行間隔の変更
    func updateTimerInterval(newInterval: TimeInterval) {
        guard newInterval > 0 else {
            assertionFailure("Timer interval must be greater than 0")
            return
        }
        timerInterval = newInterval
        timerPublisher = Timer.publish(every: newInterval, on: .main, in: .common)
        startTimer()
    }
    
    // タイマーで実行される関数
    private func executeTimerTask() {
        //print("Timer executed at \(Date())")
        task() // 実行したい処理
    }
}

使用方法

// 1秒ごと実行するタイマーを作成
let timerManager = TimerClass(interval: 1) { [weak self] in
    DispatchQueue.main.async {
        //タイマーで実行したい処理
        print("1秒間隔で実行しています。")
        self?.objectWillChange.send()
    }
}

// タイマー開始
timerManager.startTimer()

// 実行間隔を変更(5秒間隔にする)
timerManager.updateTimerInterval(newInterval: 5)

// 実行する関数を変更
timerManager.updateTask {
    print("Updated Task!")
}

// タイマーを停止
timerManager.stopTimer()

私の環境ではタイマーに合わせてViewの更新がしたかったので「DispatchQueue.main.async」やら「self?.objectWillChange.send()」をつけています。

まとめ

実はタイマーの切り出しは特に問題なかったのですが、切り出した瞬間にViewの更新がされなくなりかなりハマりました。
だいぶChatGPTに助けてもらいましたが今のところよく動いています。
便利な世の中ですね。

記事は書いたことがなかったので、正直プログラムの中身をどこまで書けばいいのかもさっぱりです。見にくかったら申し訳ありませんん。。。

Discussion