本アプリは SwiftData/Realm を使わず、UserDefaults(AppGroup)+ Bundle JSON の構成。
豆知識・ジャンルクイズの問題はBundle内JSON(read-only)、ユーザーの状態(コイン残高・お気に入り・テーマ・履歴)はAppGroup UserDefaultsに保存。Widget拡張も同じAppGroupを参照。
データソース構成
flowchart LR
Bundle[Bundle JSON
豆知識・ジャンルクイズ] --> Loader[TipLoaderService
GenreQuizService]
Loader --> ViewModel[AppViewModel]
AppGroup[AppGroup UserDefaults
group.com.happyboy1002.DailyTips]
AppGroup --> Coin[CoinService]
AppGroup --> Theme[ThemeService]
AppGroup --> Fav[FavoriteService]
AppGroup --> Hist[HistoryService]
AppGroup --> Genre[GenreProgressService]
ViewModel --> Sync[syncWidgetData]
Sync --> WTip[WidgetTipData]
Sync --> WCal[WidgetCalendarData]
WTip -. AppGroup経由 .-> Widget[Widget拡張]
WCal -. AppGroup経由 .-> Widget
モデル一覧
DailyTip / TipQuiz
struct DailyTip: Identifiable, Equatable {
let id: String
let title: String
let content: String
let category: String
let quiz: TipQuiz?
}
struct TipQuiz: Codable, Sendable, Equatable {
let question: String
let choices: [String]
let correct: Int
}
GenreQuizQuestion / GenreDefinition
struct GenreQuizQuestion: Identifiable, Equatable {
let id: String
let question: String
let choices: [String]
let correct: Int
let explanation: String?
}
struct GenreDefinition: Identifiable, Equatable {
let id: String
let name: String
let icon: String // SF Symbol
let cost: Int // 100pt
}
enum GenrePlayMode {
case random
case sequential
case continued
}
AppThemeItem
struct AppThemeItem: Identifiable, Equatable {
let id: String
let name: String
let cost: Int // 0(無料)or 200
let colors: [Color]
let begin: UnitPoint
let end: UnitPoint
}
WidgetSharedData(AppGroup共有)
struct WidgetTipData: Codable, Sendable {
let title: String
let category: String
let dateKey: String
let updatedAt: Date
static func load() -> WidgetTipData? { /* AppGroup UserDefaultsから読込 */ }
}
struct WidgetCalendarData: Codable, Sendable {
let loginDaysThisWeek: [Bool] // 7要素
let loginStreak: Int
let updatedAt: Date
static func load() -> WidgetCalendarData? { /* 同上 */ }
}
enum WidgetAppGroup {
static let suiteName = "group.com.happyboy1002.DailyTips"
static let tipDataKey = "widgetTipData"
static let calendarDataKey = "widgetCalendarData"
}
CoinResult(戻り値DTO)
struct CoinResult {
let earned: Int
let streak: Int
let balance: Int
let alreadyReceived: Bool
}
UserDefaults キー一覧(AppGroup)
| キー | 型 | 用途 |
coin_balance | Int | コイン残高 |
last_login_date | String | 最終ログイン日(連続日数判定) |
login_streak | Int | 連続ログイン日数 |
video_bonus_count_* | Int | 当日の動画ボーナス取得回数(最大5) |
favorite_tips | [String] | お気に入りtipのID配列 |
selected_theme | String | 選択中テーマID |
unlocked_themes | [String] | 購入済テーマID配列 |
unlocked_genres | [String] | アンロック済ジャンルID配列 |
genre_progress_* | Codable | ジャンル別の回答数・正解数・位置 |
quiz_streak | Int | クイズ正解連続数 |
login_dates | [String] | カレンダー表示用ログイン日 |
ad_viewed_dates | [String] | カレンダー表示用広告視聴日 |
launch_count | Int | 起動回数(レビュー誘導判定) |
widgetTipData | JSON | Widget用Smallデータ |
widgetCalendarData | JSON | Widget用Mediumデータ |
Bundle JSONリソース
| ファイル | 内容 |
daily_tips.json | 日付キー("MM-DD" / "MM-DD_v2")→ DailyTip 配列 |
genre_quiz_*.json | ジャンル別の問題セット(13ジャンル) |
豆知識ローテーション
2026/4/1 を境にキー命名規則が切り替わる:
2026/4/1 以前 → キー "MM-DD"(年1)
2026/4/1 以降 → キー "MM-DD_v2"(年2)
TipLoaderService.keyFor(date:) が日付から自動判定。
コイン経済の収支
| 区分 | イベント | 金額 |
| 獲得 | デイリーログインボーナス | +1pt/日 |
| 獲得 | プレミアム月次ボーナス | +100pt(毎月1日) |
| 獲得 | 動画視聴ボーナス | +3pt(最大5回/日) |
| 消費 | テーマ購入 | -200pt |
| 消費 | ジャンルアンロック | -100pt(13ジャンル) |
関連ファイル
DailyTips/Models/DailyTip.swift
DailyTips/Models/GenreQuizItem.swift
DailyTips/Models/GenrePlayMode.swift
DailyTips/Models/AppThemeItem.swift
DailyTips/Models/WidgetSharedData.swift
DailyTips/Services/CoinService.swift
DailyTips/Services/TipLoaderService.swift
変更履歴
| バージョン | 日付 | 変更内容 |
| 1.0 | 2026-05-09 | 初版作成(ソースコードからリバース) |