本アプリは 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_balanceIntコイン残高
last_login_dateString最終ログイン日(連続日数判定)
login_streakInt連続ログイン日数
video_bonus_count_*Int当日の動画ボーナス取得回数(最大5)
favorite_tips[String]お気に入りtipのID配列
selected_themeString選択中テーマID
unlocked_themes[String]購入済テーマID配列
unlocked_genres[String]アンロック済ジャンルID配列
genre_progress_*Codableジャンル別の回答数・正解数・位置
quiz_streakIntクイズ正解連続数
login_dates[String]カレンダー表示用ログイン日
ad_viewed_dates[String]カレンダー表示用広告視聴日
launch_countInt起動回数(レビュー誘導判定)
widgetTipDataJSONWidget用Smallデータ
widgetCalendarDataJSONWidget用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ジャンル)

関連ファイル

変更履歴

バージョン日付変更内容
1.02026-05-09初版作成(ソースコードからリバース)