[Foundation] Timerを毎正時でfireさせる方法

毎正時にタイマーを fire させる方法を説明します。

Web で探しても意外と見つからなかったのでメモです。

MEMO
正時とは、「1時ぴったり」「2時ぴったり」のように 分以下の時間がつかない時間のことを言っています。

タイマーを使います

自アプリでずっと CPU を占有し続けるわけにはいかないので、OS が提供しているタイマーを使用します。

Timer についての Apple のドキュメントは、こちら

特定時間経過後に 通知 できるのがタイマー

タイマーの基本的機能は、特定時間経過後に 通知(fire) することです。

iPhone のタイマーアプリもこの機能です。

タイマーは、指定時間経過後に 通知(fire) として closure を呼ぶもの、指定セレクタを呼ぶもの、NSInvocation を実行するもの、色々と用意されています。

以下の API は 指定時間後に、closure を呼ばせることができるものです。

指定時間経過時に closure を呼ぶ API

init(timeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void)

TimeInterval は、以下のように定義され、単位は秒です。

TimeInterval 定義

typealias TimeInterval = Double

型は Double ですが、0.001 秒以上の精度を持つと説明されています。(おそらく最終的にはハードウェア制約から実精度が決まると思われます)

「30秒後」のようなアラームはこの API で実現できますが、「午後2時ぴったり」 というようなアラームは この API では実現できません。

特定の時間に通知することもできます

タイマーを使うと 指定時間の経過だけではなく、指定の時間に通知(fire) することもできます。

iPhone アプリでは、アラームと呼ばれる機能です。

こちらも、通知手段として複数の方法が用意されていますが、以下の API は、指定時間に、closure が実行されるものです。

指定時間に closure を呼ぶ API

init(fire: Date, interval: TimeInterval, repeats: Bool, block: (Timer) -> Void)

1番めの引数 Date に、通知が必要な時間をセットします。以降の引数は先ほどと同じです。繰り返し通知する際の間隔と繰り返しするかどうかを指定します。

タイマーを使うときの注意点

タイマーを作成した後は、RunLoop に add しないとタイマーは動作しません。

Timer add 例

if let timer = Timer.init(timeInterval: Double(60*60), repeats: true, block: { _ in }) {
    RunLoop.current.add(timer, forMode: .common)
}

指定時間を作成する

毎正時の通知をタイマーで実現するには、最初の通知時間を正しく指定できれば、あとは間隔を指定することで繰り返し通知ができます。

正時を Date で作成する

正時という指定時間を Date で指定することが 標準の Date ではすこし手間です。

SwiftDate という GitHub で公開されているライブラリを使うと非常に便利なメソッドが使えるようになります。

現在から見て次にくる正時を計算するには、以下のような計算で可能となります。

次に来る正時ピッタリ

let nextFireDate = DateInRegion(Date(), region: Region.current).dateRoundedAt(.toCeilMins(60)).date

上のコードでは、現在のリージョン時間を取得してから、それを60分単位で切り上げています。(切り下げると過去になってしまいます。)

この Date 算出方法と先ほどの API を組み合わせることで毎正時に通知(fire)するタイマーを設定することが可能となります。

DateInRegion
大抵の国ではUTCからの時差は、1時間単位ですので、DateInRegionを使わずに時間を生成してもあまり問題ありません。例えば、日本は、UTC+9ですので、ローカル時間を使っても、UTC時間を使っても不都合はありません。
ただ、Regionによっては、.5の単位で時差があるケースがあります。例えば、インドは、UTC+5:30なので、ローカル時間を使うときちんと「X時ちょうど」にできますが、UTCを使ってしまうと、「X時30分ちょうど」になってしまいます。ですので、ここでは、DateInRegionを使って、Regionの時間をRegionの単位で切り上げることが必要になっています。
# アプリがどのRegionで使われるかは予想できませんので、このようなベースの部分は、最初からある程度検討しておいた方が、後々のメンテナンスの手間が減ります。

毎正時になるタイマーを作成する

次に来る正時を指定してタイマーを作成し、RunLoop に add することで 毎正時に通知(fire) されるタイマーが完成します。

次に来る正時ピッタリを開始時刻にして1時間毎に繰り返すタイマー

let nextFireDate = DateInRegion(Date(), region: Region.current).dateRoundedAt(.toCeilMins(60)).date
if let timer = Timer.init(fire: nextFireDate, interval: Double(60*60), repeats: true, block: { _ in }) {
    RunLoop.current.add(timer, forMode: .common)
}
毎正時に通知(fire)するタイマーの作り方
  • Timer を使用する
  • init(fire: Date, interval: TimeInterval, repeats: Bool, block: (Timer) -> Void) を使って、指定時間から指定周期で通知するタイマーを作る
  • SwiftDate を使用すると Region を考慮した正時を算出するのが簡単になります
  • interval は、秒指定なので 60 * 60 が1時間を表す
  • Tips: 作成したタイマーを RunLoop に add するのを忘れない

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です