Swiftで学習アプリの「学習履歴」と「継続」を作る:ストリーク・週間アクティビティ・トラック分析
こんにちは。
今回は、**学習アプリを作るなら避けて通れない「学習履歴」**について書いてみます。
音声学習アプリや語学学習アプリを作っていると、
「今日ちゃんと勉強したかな?」
とか、
「この曲、何回くらい練習したんだろう?」
とか、
「最近ちゃんと続いてるな!」
みたいな情報を、ユーザーに見せたくなってきます。
そして、ここで出てくるのが、
学習履歴
ストリーク(連続学習日数)
週間アクティビティ
月ごとの学習回数
トラックごとの学習状況
あたりです。
今回はまず、UserDefaults + JSONだけで作る最小構成から始めます。
小〜中規模のアプリなら、最初はこれくらいでも十分です。
そのうえで後半では、
トラック別の分析
SwiftUIダッシュボード
CloudKit同期
JSON / CSVエクスポート
マイグレーション
XCTest
実運用でハマりやすいポイント
まで、一気に発展させていきます。
1. まずは「学習した」という記録を残す
最初に必要なのは、ものすごくシンプルです。
「いつ、何を、どんな練習をしたか」
これを記録できればOK。
今回は、例えばこんなデータを持たせます。
学習した日時
学習タイプ
トラック名
アーティスト名
正解数
問題数
学習タイプは、今回は3種類にしてみます。
enum PracticeType: String, Codable {
case dictation
case sectionRepeat
case flashCard
}そして、学習履歴そのものはこんな感じ。
struct PracticeRecord: Codable, Equatable {
let id: UUID
let date: Date
let type: PracticeType
let trackTitle: String
let trackArtist: String
let correctCount: Int
let totalCount: Int
var scorePercent: Int? {
guard totalCount > 0 else { return nil }
return Int(
(Double(correctCount) / Double(totalCount) * 100).rounded()
)
}
static func == (a: PracticeRecord, b: PracticeRecord) -> Bool {
return a.id == b.id
}
}ここで scorePercent を計算できるようにしておくと、あとで、
「このトラック、前より正解率上がったな」
みたいな表示も簡単にできます。
2. 保存先は、とりあえず UserDefaults でOK
「学習履歴」と聞くと、
Core Dataを使った方がいい?
とか、
CloudKitまで入れないとダメ?
と思うかもしれません。
でも、最初からそこまでやらなくても大丈夫です。
今回のような小〜中規模のデータなら、まずは
UserDefaults + JSON
くらいでも十分扱えます。
final class PracticeHistoryService {
static let shared = PracticeHistoryService()
private init() {}
private let storageKey = "practice_history_v1"
private var cache: [PracticeRecord]?
func allRecords() -> [PracticeRecord] {
if let c = cache {
return c
}
let loaded = loadFromDisk()
cache = loaded
return loaded
}
func add(_ record: PracticeRecord) {
var updated = allRecords()
// 新しいものを先頭に追加
updated.insert(record, at: 0)
// とりあえず200件まで
if updated.count > 200 {
updated = Array(updated.prefix(200))
}
cache = updated
saveToDisk(updated)
}
private func loadFromDisk() -> [PracticeRecord] {
guard let data = UserDefaults.standard.data(forKey: storageKey) else {
return []
}
return (try? JSONDecoder().decode(
[PracticeRecord].self,
from: data
)) ?? []
}
private func saveToDisk(_ records: [PracticeRecord]) {
guard let data = try? JSONEncoder().encode(records) else {
return
}
UserDefaults.standard.set(data, forKey: storageKey)
}
}このくらいなら、かなりシンプルですね。
3. 学習アプリで重要な「ストリーク」
ここからちょっと面白くなります。
学習アプリでは、
「何日連続で学習しているか」
が結構重要です。
いわゆる、
ストリーク(Streak)
ですね。
例えば、
🔥 7日連続学習中!
みたいな表示です。
実装としては、
過去の学習記録から「学習した日」を取り出す
今日から昨日、2日前……と確認する
連続していればカウントする
という考え方で作れます。
func currentStreak() -> Int {
let calendar = Calendar.current
let practicedDays = Set(
allRecords().map {
calendar.startOfDay(for: $0.date)
}
)
var streak = 0
var check = calendar.startOfDay(for: Date())
while practicedDays.contains(check) {
streak += 1
guard let prev = calendar.date(
byAdding: .day,
value: -1,
to: check
) else {
break
}
check = prev
}
return streak
}これだけでも、
let streak = PracticeHistoryService.shared.currentStreak()で現在のストリークを取得できます。
4. 「今週どれくらい勉強した?」も欲しい
ストリークとセットで欲しくなるのが、
週間アクティビティ
です。
例えば、
● ● ○ ● ● ○ ●
みたいに、過去7日間で学習した日を表示します。
これも、実装自体はそれほど難しくありません。
func weekActivity() -> [Bool] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let practicedDays = Set(
allRecords().map {
calendar.startOfDay(for: $0.date)
}
)
return (0..<7).map { offset in
guard let d = calendar.date(
byAdding: .day,
value: -(6 - offset),
to: today
) else {
return false
}
return practicedDays.contains(d)
}
}これで、
let week = PracticeHistoryService.shared.weekActivity()とすれば、
[true, true, false, true, true, false, true]のようなデータが取れます。
あとはSwiftUI側で丸や四角を並べれば、簡単な学習カレンダーになります。
5. 月ごとの学習回数も集計してみる
もう一つ便利なのが、
「今月は何回勉強した?」
という情報です。
例えば、
1月:23回
2月:31回
3月:18回
みたいな感じですね。
func monthlyCount(year: Int)
-> [(month: Int, count: Int)] {
let calendar = Calendar.current
var counts = [Int: Int]()
for r in allRecords() {
let comps = calendar.dateComponents(
[.year, .month],
from: r.date
)
guard comps.year == year,
let month = comps.month else {
continue
}
counts[month, default: 0] += 1
}
return (1...12).map {
(month: $0, count: counts[$0] ?? 0)
}
}ここまで来ると、かなり「学習アプリっぽく」なってきます。
6. 実際に学習が終わったときはこう記録する
例えばディクテーションの問題を10問やって、8問正解したとします。
その場合は、
let record = PracticeRecord(
id: UUID(),
date: Date(),
type: .dictation,
trackTitle: "Song A",
trackArtist: "Artist",
correctCount: 8,
totalCount: 10
)
PracticeHistoryService.shared.add(record)これだけ。
あとは保存したデータから、
ストリーク
週間アクティビティ
月間学習回数
正解率
などを計算できます。
ここまででも、実はかなり使える
ここまでの実装だけでも、
「学習履歴を保存する」
という目的は達成できます。
しかも、
UserDefaults
Codable
JSON
Calendar
くらいしか使っていません。
最初から大げさなデータベースを導入しなくても、まずはこのくらいから始めるのは全然アリだと思います。
ただ、実際にアプリとして育てていくと、ここから問題が出てきます。
例えば、
「この曲、今まで何回練習した?」
「前回より正解率は上がった?」
「今年と去年で学習量はどう変わった?」
「別端末でも履歴を見たい」
「学習履歴をバックアップしたい」
……となってきます。
ここから先は、単純な履歴保存だけでは足りなくなってきます。
🔒 ここから先は有料パートです
ここからは、実際のアプリとして一段上の学習履歴機能を作る部分に入ります。
有料パートでは、以下をまとめて実装します。
A. 拡張モデル
updatedAt の追加
同期を前提にしたデータ構造
後から拡張しやすいモデル設計
ここから先は
¥ 500
この記事が気に入ったらチップで応援してみませんか?
