SwiftUI TimelineView でリアルタイム更新
TimelineView は、時間の経過に応じてビューを更新する仕組みだ。一定間隔での更新やアニメーションフレームごとの更新が可能で、リアルタイムの時計表示やゲームのような滑らかな描画に活用できる。
TimelineView の基本
TimelineView は、スケジュールに従ってコンテンツを再描画する。
struct ClockView: View {
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0)) { context in
Text(context.date.formatted(date: .omitted, time: .standard))
.font(.largeTitle.monospacedDigit())
}
}
}.periodic(from:by:) で 1 秒ごとに更新するスケジュールを指定している。context.date で現在の時刻が取得でき、毎秒ビューが再描画される。
スケジュールの種類
TimelineView には複数のスケジュールオプションがある。
// 一定間隔で更新
TimelineView(.periodic(from: .now, by: 0.1)) { ... }
// アニメーションフレームごとに更新(最大 60fps 程度)
TimelineView(.animation) { ... }
// 最小限の更新頻度(低電力)
TimelineView(.animation(minimumInterval: 1/30)) { ... }
// 指定した時刻のリストで更新
TimelineView(.explicit([date1, date2, date3])) { ... }
// 毎分0秒に更新
TimelineView(.everyMinute) { ... }| .periodic | 指定した間隔で定期的に更新 |
| .animation | ディスプレイのリフレッシュレートに合わせて更新 |
| .explicit | 指定した時刻の配列に従って更新 |
| .everyMinute | 毎分0秒に更新 |
TimelineViewDefaultContext
context パラメータには、更新のタイミングに関する情報が含まれる。
TimelineView(.animation) { context in
// 現在の日時
let date = context.date
// ビューが表示されているか
// .lowFrequency: バックグラウンドなど
// .normal: 通常表示
let cadence = context.cadence
}cadence を確認することで、バックグラウンド時に処理を軽減するといった最適化ができる。
アニメーションへの活用
.animation スケジュールを使えば、フレーム単位の滑らかなアニメーションが可能になる。
struct SmoothRotation: View {
var body: some View {
TimelineView(.animation) { context in
let seconds = context.date.timeIntervalSinceReferenceDate
let rotation = seconds.truncatingRemainder(dividingBy: 2) * 180
Image(systemName: "gear")
.font(.system(size: 80))
.rotationEffect(.degrees(rotation))
}
}
}時刻から回転角度を計算し、毎フレーム描画することで滑らかな回転を実現している。
Canvas と組み合わせる
TimelineView と Canvas を組み合わせると、カスタム描画のアニメーションが作れる。
struct ParticleAnimation: View {
var body: some View {
TimelineView(.animation) { context in
let time = context.date.timeIntervalSinceReferenceDate
Canvas { graphicsContext, size in
for i in 0..<20 {
let angle = Double(i) / 20 * .pi * 2 + time
let radius = 100 + sin(time * 2 + Double(i)) * 30
let x = size.width / 2 + cos(angle) * radius
let y = size.height / 2 + sin(angle) * radius
let circle = Path(ellipseIn: CGRect(
x: x - 10, y: y - 10,
width: 20, height: 20
))
graphicsContext.fill(
circle,
with: .color(.blue.opacity(0.7))
)
}
}
}
.frame(width: 300, height: 300)
}
}20 個の円が波打ちながら回転する、複雑なアニメーションが実現できる。
パフォーマンスの考慮
TimelineView は継続的に再描画を行うため、パフォーマンスへの影響が大きい。
常時表示が不要な場合は、@State で表示状態を管理し、非表示時は TimelineView を使わないようにする。
60fps が不要な場面では minimumInterval を指定するか、.periodic で更新頻度を下げる。
Canvas 内の描画処理を軽量に保つ。複雑な計算は可能な限り事前に行う。
struct OptimizedTimelineView: View {
@State private var isActive = true
var body: some View {
Group {
if isActive {
TimelineView(.animation(minimumInterval: 1/30)) { context in
// 30fps で十分な場合
AnimatedContent(time: context.date.timeIntervalSinceReferenceDate)
}
} else {
StaticContent()
}
}
}
}実践例:アナログ時計
struct AnalogClock: View {
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0)) { context in
let components = Calendar.current.dateComponents(
[.hour, .minute, .second],
from: context.date
)
let hour = Double(components.hour ?? 0)
let minute = Double(components.minute ?? 0)
let second = Double(components.second ?? 0)
ZStack {
Circle().stroke(lineWidth: 4)
// 時針
ClockHand(length: 50, width: 4)
.rotationEffect(.degrees((hour + minute / 60) * 30 - 90))
// 分針
ClockHand(length: 70, width: 3)
.rotationEffect(.degrees(minute * 6 - 90))
// 秒針
ClockHand(length: 80, width: 1)
.foregroundColor(.red)
.rotationEffect(.degrees(second * 6 - 90))
}
.frame(width: 200, height: 200)
}
}
}毎秒更新で十分なアナログ時計は .periodic(by: 1.0) を使い、不要な再描画を避ける。用途に応じた適切なスケジュール選択が、バッテリー消費を抑える鍵となる。