diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4f91376 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +# CLAUDE.md + +AI アシスタント向けのリファレンスです。詳細は各ドキュメントを参照してください。 + +## ドキュメント + +| ドキュメント | 内容 | +|------------|------| +| [README.md](README.md) | プロジェクト概要・動作環境 | +| [doc/architecture.md](doc/architecture.md) | アーキテクチャ・設計・主要ファイル・規約 | +| [doc/development.md](doc/development.md) | 開発環境セットアップ・ブランチ戦略・ビルド方法 | +| [doc/testing.md](doc/testing.md) | テストの実行方法・テストの書き方 | +| [doc/deployment.md](doc/deployment.md) | fastlane レーン・CI/CD パイプライン・環境変数 | + +## クイックリファレンス + +```bash +# テスト実行 +bundle exec fastlane ios test + +# TestFlight 配信 +bundle exec fastlane ios beta + +# App Store リリース +bundle exec fastlane ios release +``` + +- プロジェクトは必ず `TimerWatcherWorkspace.xcworkspace` で開く +- コミットメッセージは日本語で書く +- ビルド番号は手動で変更しない(fastlane が自動管理) diff --git a/README.md b/README.md index 0380b17..10f5ba7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # TimeWatcher -TimeWatchのiOSアプリ + +iOSのタイマーアプリです。 + +## 概要 + +Swift / SwiftUI で構築されたネイティブ iOS タイマーアプリです。 + +- アニメーション付きタイマー表示 +- ホーム画面・ロック画面ウィジェット(iOS 16+) +- ライブアクティビティ(Dynamic Island / ロック画面)対応 +- ディープリンク対応 +- 最大表示時間:99時間 + +## 動作環境 + +| 項目 | バージョン | +|------|-----------| +| iOS | 17.0+ | +| Swift | 5.9+ | +| Xcode | 15(CI)/ 16(CD) | + +## ドキュメント + +詳細は [`doc/`](doc/) を参照してください。 + +- [アーキテクチャ](doc/architecture.md) +- [開発ワークフロー](doc/development.md) +- [テスト](doc/testing.md) +- [デプロイ](doc/deployment.md) diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 0000000..6400a67 --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,62 @@ +# アーキテクチャ + +## パターン:MVVM + Combine + +| 層 | 役割 | 主なファイル | +|----|------|------------| +| Model | タイマーのコアロジック・状態管理 | `TimeWatch.swift` | +| ViewModel | UIに公開する状態・イベント処理 | `MainTimerViewModel.swift` | +| View | SwiftUI による宣言的 UI | `MainTimerView.swift` など | + +- ViewModel は `ObservableObject` + `@Published` で状態を公開する +- View はモデルの状態を直接変更しない + +## ディレクトリ構成 + +``` +TimeWatcherPrj/ +├── TimeWatcher/ # メインアプリターゲット +│ ├── AppModel/ # ビジネスロジック +│ │ ├── TimeWatch.swift # タイマーエンジン(シングルトン) +│ │ └── LiviActivity/ # ライブアクティビティ管理 +│ ├── View/ # SwiftUI ビュー +│ │ ├── RootView/ # アプリエントリ・ディープリンク +│ │ ├── MainTimerView/ # タイマー画面 + ViewModel +│ │ └── ViewParts/ # 共通UIコンポーネント +│ ├── Utility/ # 汎用ユーティリティ +│ │ ├── Extension/ # Date / Calendar / TimeInterval 拡張 +│ │ ├── Logger/ # アプリ共通ロガー +│ │ └── Constant/ # AppConstants +│ └── Dependency/ # 依存性注入 +├── TimeWatcherWidget/ # ウィジェット拡張ターゲット +│ └── Intent/ # App Intents(タイマー操作) +├── TimeWatcherTests/ # ユニットテスト +└── TimeWatcherExternalResouce/ # SwiftPackage:デザイントークン +``` + +## 主要ファイル + +| ファイル | 内容 | +|---------|------| +| `TimeWatcher/AppModel/TimeWatch.swift` | タイマーエンジン。Combine Publisher で 0.001秒刻みに状態を配信 | +| `TimeWatcher/View/MainTimerView/MainTimerViewModel.swift` | タイマー状態を UI 向けに変換。最大表示 99時間 | +| `TimeWatcher/AppModel/LiviActivity/LiveActivityManager.swift` | ActivityKit ラッパー。テスト用モックあり | +| `TimeWatcher/Dependency/DateDependency.swift` | 現在時刻の DI。テストで時刻をモック可能にする | +| `TimeWatcherWidget/Intent/TimerStartIntent.swift` | ウィジェットからタイマー開始 | +| `TimeWatcherWidget/Intent/TimerStopIntent.swift` | ウィジェットからタイマー停止 | +| `TimeWatcherExternalResouce/swiftgen.yml` | SwiftGen 設定。カラー・画像をタイプセーフに生成 | + +## 設計上の規約 + +- **シングルトン**:`TimeWatch.shared` のみ。新たな共有状態を増やさない +- **時刻取得**:`Date.now` を直接呼ばず `DateDependency` 経由で取得する +- **リソース参照**:`Asset.Colors.*` / `Asset.Images.*`(SwiftGen 生成)を使い、文字列リテラルは使わない +- **ライブアクティビティ**:タイマー状態が変わるときは必ずライブアクティビティも更新する +- **ウィジェット Intent**:Intent は薄く保ち、ビジネスロジックはモデル層に委譲する +- **ロギング**:`print()` は使わず `logger`(`Logger.swift`)を使う +- **定数**:マジックリテラルを避け `AppConstants.swift` に定義する + +## 注意点 + +- iOS 18 でライブアクティビティの App Intent が動作しない問題の修正が `da0eb9c` に含まれる。この変更を誤って戻さないこと +- SwiftGen で生成されたコードが見つからない場合は `TimeWatcherExternalResouce` ターゲットを先にビルドする diff --git a/doc/deployment.md b/doc/deployment.md new file mode 100644 index 0000000..93c4d08 --- /dev/null +++ b/doc/deployment.md @@ -0,0 +1,40 @@ +# デプロイ + +## 環境変数のセットアップ + +`.env.skel` をコピーして `.env` を作成し、各値を設定する。 + +``` +ASC_KEY_ID= +ASC_ISSUER_ID= +ASC_KEY_CONTENT= +MATCH_PASSWORD= +MATCH_KEYCHAIN_PASSWORD=<キーチェーンパスワード> +FASTLANE_USER= +MATCH_GIT_BASIC_AUTHORIZATION= +ENVIRONMENT=CI +LANG=en_US.UTF-8 +LC_ALL=en_US.UTF-8 +``` + +## fastlane レーン + +| コマンド | 内容 | +|---------|------| +| `fastlane ios test` | ユニットテスト実行 | +| `fastlane ios beta` | TestFlight へビルド&アップロード(ビルド番号 +0.1) | +| `fastlane ios release` | App Store へビルド&提出(ビルド番号 +0.01) | +| `fastlane ios upload_beta` | 既存 IPA を TestFlight にアップロード | +| `fastlane ios upload_release` | 既存 IPA を App Store にアップロード | +| `fastlane ios match_development` | 開発用プロビジョニングプロファイル同期 | +| `fastlane ios match_appstore` | App Store 用プロビジョニングプロファイル同期 | + +**ビルド番号は手動で編集しない。** fastlane が自動でインクリメントする。 + +## CI/CD パイプライン(GitHub Actions) + +| ファイル | トリガー | 内容 | +|---------|---------|------| +| `ci.yml` | 全ブランチへの PR | ビルド+テスト(macOS 13 / Xcode 15) | +| `cdBeta.yml` | `release/beta/*` へのプッシュ | TestFlight 配信(Xcode 16) | +| `cdRelease.yml` | `release/store/*` へのプッシュ | App Store リリース(Xcode 16) | diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 0000000..c4d331b --- /dev/null +++ b/doc/development.md @@ -0,0 +1,47 @@ +# 開発ワークフロー + +## 環境要件 + +| ツール | バージョン | +|-------|-----------| +| Xcode | 15(CI) / 16(CD) | +| iOS SDK | 17.0+ | +| Ruby | Gemfile で管理 | +| Swift | 5.9+ | + +## プロジェクトの開き方 + +**必ずワークスペースを開くこと。** `.xcodeproj` を直接開くと `TimeWatcherExternalResouce` パッケージが認識されない。 + +``` +TimerWatcherWorkspace.xcworkspace +``` + +## Ruby 依存パッケージのインストール + +```bash +bundle install +``` + +## ブランチ戦略 + +| ブランチパターン | 用途 | +|--------------|------| +| `master` | メイン開発ブランチ | +| `release/beta/*` | TestFlight 配信トリガー | +| `release/store/*` | App Store リリーストリガー | +| `claude/*` | AI 支援作業ブランチ | + +`main` / `master` への直プッシュは禁止。PR 経由でマージする。 + +## ビルド(CLI) + +```bash +xcodebuild \ + -workspace TimerWatcherWorkspace.xcworkspace \ + -scheme TimeWatcher \ + -sdk iphonesimulator \ + -configuration Debug \ + -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro" \ + clean build +``` diff --git a/doc/testing.md b/doc/testing.md new file mode 100644 index 0000000..d0cc0fa --- /dev/null +++ b/doc/testing.md @@ -0,0 +1,49 @@ +# テスト + +## テストの実行 + +### fastlane(推奨) + +```bash +bundle exec fastlane ios test +``` + +### xcodebuild + +```bash +xcodebuild \ + -workspace TimerWatcherWorkspace.xcworkspace \ + -scheme TimeWatcher \ + -testPlan TimeWatcher \ + -sdk iphonesimulator \ + -configuration Debug \ + -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro" \ + clean test | xcpretty +``` + +## テスト構成 + +| ターゲット | 状態 | 内容 | +|-----------|------|------| +| `TimeWatcherTests` | 有効 | ユニットテスト(並列実行可) | +| `TimeWatcherUITests` | 無効 | スナップショットテスト(手動実行時のみ有効化) | + +テストプラン:`TimeWatcher.xctestplan` + +## テストの書き方 + +- テストは `TimeWatcherPrj/TimeWatcherTests/` 配下に配置する +- ソースのディレクトリ構造をテスト側でも踏襲する +- 日付・時刻のモックは `DateDependency` を使う +- テストユーティリティは `Utilities/TestUtilities.swift` を活用する +- CI では `parallel_testing: false`(シミュレータのリソース競合防止) + +## テストファイル一覧 + +| ファイル | 内容 | +|---------|------| +| `MainTimerView/MainTimerViewTest.swift` | タイマー状態・ViewModel のテスト | +| `RootView/OpenUrlViewModelTest.swift` | ディープリンク URL 解析のテスト | +| `TimerWatcherWidgetIntent/TimerStartIntentTest.swift` | ウィジェット開始 Intent のテスト | +| `TimerWatcherWidgetIntent/TimerStopIntentTest.swift` | ウィジェット停止 Intent のテスト | +| `Utilities/TestUtilities.swift` | 日付操作などのテストヘルパー |