iOS アプリ¶
ios/Rikako の SwiftUI アプリ。1 つのコードベースを flavor で出し分けて 2 アプリを配信
している(化学版 jp.conol.chemist / 4択IT org.rikako.it-passport)。
レイヤ構成とディレクトリ責務は iOS Architecture、オンボーディングの 画面仕様は iOS Onboarding を参照。
起動フロー¶
RootView が起動時にアプリ状態を取得し、強制アップデート・メンテナンス・通常を出し分ける。
flowchart TD
A[RootView] --> B[スプラッシュ<br/>初期化]
B --> C{アプリ状態}
C -->|要アップデート| D[UpdateRequiredView]
C -->|メンテナンス| E[MaintenanceView]
C -->|通常| F{オンボーディング完了?}
F -->|No| G[OnboardingView]
F -->|Yes| H[MainView]
G -->|完了| H
- アプリ状態は公開 API の
GET /statusから取得する(app_statusテーブル) - オンボーディング完了フラグは
AppStateが持つ
メイン画面(3 タブ)¶
MainView は TabView。ログインは起動時には求めず、マイページからの任意操作。
flowchart TD
Main[MainView<br/>TabView] --> Study[学習<br/>StudyHomeView]
Main --> Record[学習記録<br/>StudyRecordView]
Main --> My[マイページ<br/>MyPageView]
Study -->|問題集を変更(sheet)| Picker[問題集ピッカー]
Study -->|解く| Quiz[QuizView]
Quiz --> Result[ResultView]
Quiz -->|質問する| AIChat[AIChatView]
Record --> Wrong[WrongAnswersView]
My --> Profile[ProfileView]
My --> Notifications[NotificationsView]
My --> Help[HelpAndSupportView]
My --> Settings[SettingsView]
Profile --> Transfer[TransferView<br/>引き継ぎコード]
Settings --> Login[LoginView]
Settings --> Debug[DebugView<br/>DEBUG ビルドのみ]
Login --> SignUp[SignUpView]
Login --> Confirm[ConfirmCodeView]
Login --> Forgot[ForgotPasswordView]
SignUp --> Confirm
Notifications --> NotifDetail[NotificationDetailView]
画面一覧¶
| 画面 | 到達経路 | 説明 |
|---|---|---|
RootView |
起動 | 状態に応じて出し分ける入口 |
UpdateRequiredView |
Root | 強制アップデート |
MaintenanceView |
Root | メンテナンス表示 |
OnboardingView |
Root(初回) | 問題集選択を含むオンボーディング |
MainView |
Root | 3 タブのコンテナ |
StudyHomeView |
タブ | 選択中の問題集と進捗、出題開始 |
QuizView |
学習タブ | 出題・解答・解説 |
AIChatView |
Quiz | 問題について質問する(POST /questions/{id}/chat) |
ResultView |
Quiz | スコアと正誤一覧 |
StudyRecordView |
タブ | 学習記録(週次・累計) |
WrongAnswersView |
学習記録 | 間違えた問題の一覧 |
MyPageView |
タブ | 各種メニューの入口 |
ProfileView |
マイページ | プロフィール |
TransferView |
プロフィール | 引き継ぎコードの発行・適用 |
NotificationsView / NotificationDetailView |
マイページ | お知らせ |
HelpAndSupportView |
マイページ | ヘルプ・問い合わせ |
SettingsView |
マイページ | 設定 |
LoginView / SignUpView / ConfirmCodeView / ForgotPasswordView |
設定 | メールログイン(#283) |
DebugView / DebugWorkbooksView / DebugLearningLogView |
設定 | DEBUG ビルドのみ |
認証の考え方¶
普段は Cognito Identity Pool の匿名認証のまま使い、機種変更などでデータを引き継ぎたい ときだけ設定からメールログインする。詳細は メールログイン設計 と アーキテクチャ。
引き継ぎ手段は 2 つある。
- メールログイン(
POST /account/link): アカウントに紐づけて引き継ぐ - 引き継ぎコード(
TransferView、/transfer/tokenと/transfer/apply): アカウント無しで移す
データの取得元¶
問題データは公開 API から取らない。 コンテンツ CDN(content.rikako.org)の静的 JSON を
読む。公開 API は回答送信・学習記録・お知らせ・AIチャットなど動的なものを担当する。
詳細は アーキテクチャ。
利用イベント計測(Analytics)¶
リリース後にオンボーディング離脱や主要機能の成功/失敗率をバージョン別に把握するための計測基盤(#261)。計測基盤は Firebase Analytics を採用(クラッシュ収集 #235 の Crashlytics と統合する方針)。
アーキテクチャ¶
計測は差し替え可能な抽象に載せている(LearningRepository 等と同じ protocol-first)。
| 種別 | ファイル | 役割 |
|---|---|---|
| protocol | Domain/Analytics/AnalyticsClient.swift |
計測の抽象。log(_:) / setCommonProperties(_:) |
| イベント | Domain/Analytics/AnalyticsEvent.swift |
最小イベントの enum + 失敗理由カテゴリ |
| 実装(Noop) | Infrastructure/Analytics/NoopAnalyticsClient.swift |
preview / UIテスト用 |
| 実装(Console) | Infrastructure/Analytics/ConsoleAnalyticsClient.swift |
DEBUG でイベントをコンソール出力 |
| 共通プロパティ | Infrastructure/Analytics/AnalyticsCommonProperties+Current.swift |
app version/build・app slug・OS version |
AppContainer が構築して各 ViewModel に注入(DIしている OnboardingViewModel)または AppContainer.shared.analytics 経由で発火(service-locator の Quiz/AIChat/Transfer 等)する。
イベント一覧¶
app_open / onboarding_started / onboarding_step_viewed(step) / onboarding_completed / workbook_started(workbook_id) / workbook_completed(workbook_id) / answers_submitted(workbook_id, count) / answers_submission_failed(reason) / ai_chat_started / ai_chat_succeeded / ai_chat_failed(reason) / transfer_started / transfer_completed / transfer_failed(reason)
プライバシー方針(重要)¶
- イベントに載せるのは識別子・件数・カテゴリの非 PII スカラーのみ。ユーザー入力本文・問題文・メールアドレス・Cognito Identity ID は絶対に含めない。
- 失敗は生のエラーメッセージではなく
AnalyticsFailureReason(network/server/decoding/cancelled/unauthorized/unknown)の有限カテゴリに畳み込む。 - 非 PII であることは
RikakoTests/AnalyticsEventTests.swiftで検証(キーの allowlist・値の型・reason の有限集合)。PrivacyInfo.xcprivacyはProductInteractionを宣言済み。
実装フェーズ¶
- Phase 1(実装済み): Firebase 非依存の計測レイヤー+全イベント発火+PIIテスト。
- Phase 2(実装済み・結線):
firebase-ios-sdkを SPM 追加(FirebaseAnalytics単体)、FirebaseAnalyticsClient実装、AppContainerで環境別に選択。
計測クライアントの選択(AppContainer):
| ビルド | フレーバー | クライアント | 送信先 |
|---|---|---|---|
| DEBUG | dev | CompositeAnalyticsClient(ConsoleAnalyticsClient + FirebaseAnalyticsClient) |
コンソール出力 + Firebase sandbox-492513(共有 sandbox、GA4 未リンク) |
| Release | prod | FirebaseAnalyticsClient |
Firebase rikako-prd |
FirebaseAnalyticsClient.configured(slug:environment:) が GoogleService-Info-<slug>-<env>.plist で FirebaseApp.configure する。plist が見つからない場合は Firebase 分をスキップ(dev は Console のみ / prod は NoopAnalyticsClient)=クラッシュしない。
Firebase プロジェクトは dev/prod で分離(dev データが prod GA4 を汚さないため):
| 環境 | プロジェクト | 登録アプリ(bundle ID) |
|---|---|---|
| prod | rikako-prd |
jp.conol.chemist / org.rikako.it-passport |
| dev | sandbox-492513(共有 sandbox。GA4 未リンクなので DebugView は見えない) |
org.rikako.chemist.dev / org.rikako.it-passport.dev |
各プロジェクト内は GA4 プロパティ共有だが、共通プロパティ app_slug でアプリ別にセグメント可能。
IDFA/プライバシー: firebase-ios-sdk 12.x は FirebaseAnalytics 単体で IDFA を収集しない(旧 FirebaseAnalyticsWithoutAdIdSupport 相当がデフォルト)。IDFA を有効化する FirebaseAnalyticsIdentitySupport は追加しないこと(NSPrivacyTracking=false 維持のため)。#235 で Crashlytics を足す時も同様。
GoogleService-Info.plist の扱い(git に載せない)¶
API_KEY がシークレットスキャナに検知され通知ノイズになるため git 管理外(.gitignore で **/GoogleService-Info*.plist を除外)。
- 配置先:
ios/Rikako/Firebase/GoogleService-Info-<app_slug>-<env>.plist(<app_slug>=high-school-chemistry/it-passport、<env>=prod/dev、計4ファイル)。folder sync でアプリバンドルに含まれる。 - 取得: SSM Parameter Store(
/rikako/<development|production>/firebase/ios/<app_slug>、SecureString)に置いてあり、scripts/firebase-config.sh pull dev ios/pull prod iosで配置できる(AWS_PROFILE設定 +aws sso login済みであること。AWS CLI セットアップ 参照。dev と prod は別アカウントなのでスクリプトが STS でアカウント ID を検証する。1 回で両方取るならAWS_PROFILE_DEV=... AWS_PROFILE_PROD=... scripts/firebase-config.sh pull all ios)。Firebase コンソールで plist を作り直したら、この名前で置いてからscripts/firebase-config.sh push <env> iosで SSM を更新する。パラメータは手動 put で Terraform 管理外。 - CI: 現状の iOS CI は dev(Debug)を build / build-for-testing するのみ。dev plist が無くても Console フォールバックでビルド・実行できる(Firebase 送信なし。Crashlytics の dSYM アップロードもスキップ)。Release ビルドを CI で行う場合は OIDC で assume したロールから同じスクリプトで pull する(Android の
deploy-android-prod.ymlと同じ方式)。 - Firebase DebugView での PII 非送信の実機確認は未実施。
クラッシュ収集(Crashlytics)¶
#235。Analytics と同じ firebase-ios-sdk の FirebaseCrashlytics を SPM で追加している。
| 項目 | ファイル | 役割 |
|---|---|---|
| 設定 | Infrastructure/Crash/CrashReporter.swift |
Firebase configure 済みなら Crashlytics を有効化し app_slug をカスタムキーに載せる |
| 結線 | AppContainer |
FirebaseAnalyticsClient.configured の直後に CrashReporter.configure(flavor:) を呼ぶ |
| dSYM | Xcode Build Phase「Upload dSYMs to Crashlytics」 | ビルド末尾で firebase-ios-sdk 同梱の Crashlytics/run を実行 |
- Firebase の
configureは Analytics 側が行うため、Crashlytics は plist(GoogleService-Info-<slug>-<env>.plist)があるときだけ動く。無ければ何もしない(クラッシュしない)。 - Debug ビルドでも収集する。dev(Debug)は
sandbox-492513、prod(Release)はrikako-prdと Firebase プロジェクトが分かれているので prod のデータは汚れない。 - クラッシュレポートに載せるのは非 PII のキーのみ(Analytics と同じ方針)。
setUserIDは使わない。 - IDFA: Crashlytics を足しても
FirebaseAnalyticsIdentitySupportは追加しない(NSPrivacyTracking=false維持)。
dSYM アップロード¶
Build Phase のスクリプトは CONFIGURATION 名が *Release なら prod、それ以外は dev の plist を -gsp で渡す。plist が無い(CI 等)場合は警告を出して exit 0 するので、plist 無しでもビルドは通る。
DEBUG_INFORMATION_FORMATは Debug / Release ともdwarf-with-dsym。- アプリターゲットの
OTHER_LDFLAGSに-ObjC(firebase-ios-sdk の SwiftPackageManager.md が FirebaseAnalytics に要求している。Analytics 導入時に漏れていたものを #235 で追加)。 - アプリターゲットは
ENABLE_USER_SCRIPT_SANDBOXING = NO(upload-symbolsが宣言外のパスへ書き込むため)。テストターゲットはYESのまま。 - App Store Connect へ上げたビルドは Xcode が dSYM を生成するので、通常の Archive → Upload でこのスクリプトが走れば十分。「Missing dSYM」が出たら Organizer から dSYM を DL して
Crashlytics/upload-symbols -gsp <plist> -p ios <dSYM のパス>で手動アップロードできる。
到達確認(強制クラッシュ)¶
DEBUG ビルドで起動引数に -crashlytics-test-crash を付けると起動 1 秒後に fatalError する。Crashlytics はデバッガ接続中のクラッシュを拾わないので、デバッガを付けずに起動引数を渡す必要がある(Xcode で Run → 停止 → ホーム画面から起動、では引数が引き継がれず発火しない)。方法はどちらか:
- Xcode: Edit Scheme → Run → Info で「Debug executable」のチェックを外し、Arguments に
-crashlytics-test-crashを入れて Run(デバッガ無しで引数付き起動になる) - CLI: Xcode でインストールだけしてから
# シミュレータ xcrun simctl launch --terminate-running-process booted org.rikako.chemist.dev -crashlytics-test-crash # 実機(UDID は xcrun devicectl list devices) xcrun devicectl device process launch --device <UDID> org.rikako.chemist.dev -crashlytics-test-crash
クラッシュ後にもう一度アプリを起動する(Crashlytics は次回起動時にレポートを送る)と、Firebase コンソール(sandbox-492513)の Crashlytics に数分で表示される。
プライバシー¶
PrivacyInfo.xcprivacyにCrashData/PerformanceData/OtherDiagnosticData(Linked=false, Tracking=false, AppFunctionality)を追加済み。- App Store Connect の「アプリのプライバシー」も更新が必要(クラッシュデータ・パフォーマンスデータ・その他の診断データを「ユーザーに紐付かない」「トラッキングに使用しない」で申告)。Crashlytics 入りの版を提出する前に済ませること。
- 文面は docs/privacy.md の「外部サービスの利用」を参照。