概要
iPhoneアプリ開発の学習として、アプリを開くと技術用語がランダムに表示され、その説明も見られる——そんなシンプルなアプリを作ってみました
作っていく中で「電光掲示板のように単語が自動でローテーション表示される」機能まで発展したので、実装の流れとポイントをまとめておこうと思います
本題
環境
- 言語: Swift
- UI: SwiftUI
- 対応: iOS 17.0〜
- データ: アプリに同梱したJSON(外部API・バックエンドなし)
最終的にできたもの
- アプリ起動時にスプラッシュ画面を表示
- その後、プログラミングやITに関連する用語がランダムに1つ表示される
- 単語→説明の順にフェードイン
- 10秒間表示を保持
- 保持時間が終わると自動でフェードアウトし、別のランダムな単語に切り替わる(直前と同じ単語は選ばれない)
- 画面下部に「次の単語に切り替わるまでの残り時間」を示すカウントダウンバーを表示
イメージとしては、電光掲示板の広告のようなものをイメージしてもらえればわかりやすいと思います
データモデルとJSON
用語データはシンプルなCodable構造体にし、アプリにJSONファイルとして同梱しました
struct TechTerm: Codable, Identifiable {
let id: String
let term: String
let explanation: String
}
[
{
"id": "api",
"term": "API",
"explanation": "アプリケーションやサービス同士がやり取りするための窓口となる仕組み機能を外部に公開し、開発者は中身を知らなくても呼び出せる"
}
]
読み込みとランダム選出はリポジトリ層に切り出しています
直前に表示した単語のIDを除外することで、同じ単語が連続で出るのを防いでいます
enum TechTermRepository {
private static let allTerms: [TechTerm] = {
// Bundle からJSONを読み込みデコード(省略)
}()
static func randomTerm(excluding excludedID: String? = nil) -> TechTerm {
guard let excludedID, allTerms.count > 1 else {
return allTerms.randomElement()!
}
let candidates = allTerms.filter { $0.id != excludedID }
return candidates.randomElement() ?? allTerms.randomElement()!
}
}
アニメーションの仕組み
見た目の変化はすべて「不透明度(opacity)の値をwithAnimationで滑らかに変化させる」ことで実現しています
画像やビューを差し替えているのではなく、同じテキストの透明度を動かしているだけです
タイミング制御にはTimerではなく、Swift ConcurrencyのTaskとTask.sleepを使いました
上から順に読むだけで「表示→待つ→説明表示→待つ→消す→待つ」という流れがそのままコードになるのが利点です
private func runSingleCycle() async {
term = TechTermRepository.randomTerm(excluding: term.id)
wordOpacity = 0
explanationOpacity = 0
barProgress = 1
withAnimation(.easeOut(duration: wordFadeDuration)) {
wordOpacity = 1
}
withAnimation(.linear(duration: barCountdownDuration)) {
barProgress = 0
}
try? await Task.sleep(for: .seconds(explanationDelay))
guard !Task.isCancelled else { return }
withAnimation(.easeOut(duration: explanationFadeDuration)) {
explanationOpacity = 1
}
try? await Task.sleep(for: .seconds(explanationFadeDuration + holdDuration))
guard !Task.isCancelled else { return }
withAnimation(.easeIn(duration: fadeOutDuration)) {
wordOpacity = 0
explanationOpacity = 0
}
try? await Task.sleep(for: .seconds(fadeOutDuration + pauseBetweenCycles))
}
これをwhileループで無限に繰り返すことで、ループ表示を実現しています
private func startLoop() {
loopTask?.cancel()
loopTask = Task {
while !Task.isCancelled {
await runSingleCycle()
}
}
}
画面が消えたらonDisappearでTaskをキャンセルするのを忘れずに放置するとバックグラウンドで無限ループが回り続け、リソースを無駄に消費してしまいます
.onAppear { startLoop() }
.onDisappear { loopTask?.cancel() }
カウントダウンバー
「あとどれくらいで次の単語に切り替わるか」を視覚化するため、画面下部にバーを追加しました仕組みはシンプルで、barProgress(1→0)を単語の保持時間が終わるタイミングにちょうど0になるようlinearアニメーションで減少させているだけです
private var countdownBar: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Capsule().fill(Color.secondary.opacity(0.2))
Capsule()
.fill(Color.accentColor)
.frame(width: geometry.size.width * barProgress)
}
}
.frame(height: 4)
}
サイクル開始時にbarProgress = 1へアニメーションなしで即座にリセットしてから減少アニメーションを再スタートすることで、「単語が切り替わった瞬間にバーも満タンに戻る」という挙動になります
バーの表示・非表示は単語本文と同じopacity変数に連動させているので、テキストのフェードと自然に同期します
アウトプット
おわりに
発展形として「話題のニュース記事をローテーション表示する」というアイデアも検討しました
この場合はアプリに同梱した静的JSONではなく、RSSフィードなどから都度データを取得する必要があります
個人開発の範囲であれば、秘密のAPIキーが不要なRSSフィードを使い、アプリから直接取得する構成で十分だと考えています
また、複数ソースの集約や学習型のレコメンドを行いたくなった場合は、その時点でサーバーサイドの導入を検討するのが現実的かと思っています
SwiftUIのwithAnimationと、Swift ConcurrencyのTask/Task.sleepを組み合わせるだけで、電光掲示板のような自動ローテーション表示は意外とシンプルに実装できました
オフラインで完結するミニマムな構成から始めて、今後は表示するコンテンツの幅や取得方法を広げていきたいと思います

