Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 が自動管理)
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)
62 changes: 62 additions & 0 deletions doc/architecture.md
Original file line number Diff line number Diff line change
@@ -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` ターゲットを先にビルドする
40 changes: 40 additions & 0 deletions doc/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# デプロイ

## 環境変数のセットアップ

`.env.skel` をコピーして `.env` を作成し、各値を設定する。

```
ASC_KEY_ID=<App Store Connect API キー ID>
ASC_ISSUER_ID=<App Store Connect 発行者 ID>
ASC_KEY_CONTENT=<Base64 エンコードされた .p8 キー>
MATCH_PASSWORD=<Match 暗号化パスワード>
MATCH_KEYCHAIN_PASSWORD=<キーチェーンパスワード>
FASTLANE_USER=<Apple ID メールアドレス>
MATCH_GIT_BASIC_AUTHORIZATION=<Base64 git 認証情報>
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) |
47 changes: 47 additions & 0 deletions doc/development.md
Original file line number Diff line number Diff line change
@@ -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
```
49 changes: 49 additions & 0 deletions doc/testing.md
Original file line number Diff line number Diff line change
@@ -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` | 日付操作などのテストヘルパー |
Loading