From 1dbbe15b42f989f11818bf8d30789b64163b0572 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 12:56:43 +0000 Subject: [PATCH 1/2] Add CLAUDE.md with comprehensive codebase documentation Documents architecture (MVVM + Combine), directory structure, testing workflows, deployment pipelines, key conventions, and common gotchas for AI assistants. https://claude.ai/code/session_01KsfWvCyMTnkteSWe2gd2nw --- CLAUDE.md | 285 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f567700 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,285 @@ +# CLAUDE.md — TimeWatcher Codebase Guide + +This file provides a comprehensive guide for AI assistants working in this repository. It covers architecture, development workflows, testing, and key conventions. + +--- + +## Project Overview + +**TimeWatcher** is a native iOS timer application built with Swift and SwiftUI. It targets iOS 17.0+ and includes: +- A main iOS app with animated timer display +- An iOS Widget extension for home/lock screen timers +- Live Activity support (Dynamic Island / Lock Screen) +- Deep linking support +- Comprehensive unit tests + +The app is managed using an Xcode workspace and deployed via fastlane with GitHub Actions CI/CD. + +--- + +## Repository Structure + +``` +TimeWatcher/ +├── .github/workflows/ # CI/CD GitHub Actions (ci.yml, cdBeta.yml, cdRelease.yml) +├── TimeWatcherPrj/ # Xcode project root +│ ├── TimeWatcher/ # Main iOS app target +│ │ ├── AppModel/ # Core business logic +│ │ │ ├── TimeWatch.swift # Timer engine (singleton) +│ │ │ └── LiviActivity/ # Live Activity management +│ │ ├── View/ # SwiftUI views (MVVM pattern) +│ │ │ ├── RootView/ # App entry, deep linking +│ │ │ ├── MainTimerView/ # Primary timer UI + ViewModel +│ │ │ └── ViewParts/ # Reusable view components +│ │ ├── Utility/ # Helpers and extensions +│ │ │ ├── Extension/ # Date, Calendar, TimeInterval extensions +│ │ │ ├── Logger/ # App-wide logger +│ │ │ ├── Constant/ # AppConstants +│ │ │ ├── Resource/ # Resource adapters +│ │ │ └── ViewUtility/ # UI helper utilities +│ │ └── Dependency/ # Dependency injection (DateDependency) +│ ├── TimeWatcherWidget/ # Widget extension target +│ │ ├── Intent/ # App Intents for widget actions +│ │ └── Definition/ # Widget configuration +│ ├── TimeWatcherTests/ # Unit tests +│ │ ├── MainTimerView/ # ViewModel tests +│ │ ├── RootView/ # Deep link URL parsing tests +│ │ ├── TimerWatcherWidgetIntent/ # Widget intent tests +│ │ └── Utilities/ # TestUtilities.swift +│ └── TimeWatcherUITests/ # UI tests (currently disabled) +├── TimeWatcherExternalResouce/ # Swift Package: design tokens (SwiftGen) +├── TimerWatcherWorkspace.xcworkspace/ # Xcode workspace +├── fastlane/ # Deployment automation +│ ├── Fastfile # Lane definitions (test, beta, release) +│ ├── Appfile # Bundle ID and Apple account settings +│ ├── Gymfile # Build configuration (Release, app-store) +│ ├── Scanfile # Test scan configuration +│ ├── Matchfile # Code signing management +│ └── README.md # Auto-generated fastlane docs +├── Gemfile / Gemfile.lock # Ruby gem dependencies +├── TimeWatcher.xctestplan # Test plan (unit tests enabled, UI tests disabled) +├── .env.skel # Template for required environment variables +└── README.md # Minimal project description (Japanese) +``` + +--- + +## Architecture & Key Conventions + +### MVVM Pattern + +The app follows **Model-View-ViewModel** consistently: + +- **Model** — `TimeWatch.swift` (singleton timer engine using Combine) +- **ViewModel** — `MainTimerViewModel.swift` (ObservableObject, @Published properties) +- **View** — SwiftUI views that observe the ViewModel + +### Dependency Injection for Testability + +`DateDependency.swift` injects the current date/time to allow mocking in tests. All code that needs `Date.now` or `Calendar` should use this rather than calling system APIs directly. + +### Singleton Usage + +`TimeWatch` is a singleton (`TimeWatch.shared`). Avoid creating additional shared state. Prefer injecting dependencies via initializers in new code. + +### Reactive Programming + +The app uses **Combine** extensively. Timer ticks are published as a `Publisher` at 0.001-second resolution. ViewModels subscribe using `.sink` and cancel subscriptions in `deinit` or with `AnyCancellable`. + +### Live Activities + +`LiveActivityManager.swift` wraps `ActivityKit`. A mock version is provided for tests. Always update live activity state when timer state changes. + +### Widget Integration + +Widget App Intents (`TimerStartIntent`, `TimerStopIntent`) directly manipulate the shared `TimeWatch` singleton. Keep widget intents thin — delegate business logic to the model layer. + +### Resource / Design Tokens + +Colors and images are defined in the `TimeWatcherExternalResouce` Swift Package and generated via **SwiftGen** (`swiftgen.yml`). Always use generated type-safe accessors (`Asset.Colors.*`, `Asset.Images.*`) rather than string literals. + +--- + +## Development Workflow + +### Opening the Project + +Always open the **workspace**, not the `.xcodeproj`: + +``` +TimerWatcherWorkspace.xcworkspace +``` + +### Branch Strategy + +| Branch Pattern | Purpose | +|------------------------|------------------------------| +| `master` | Primary development branch | +| `release/beta/*` | Triggers TestFlight deployment | +| `release/store/*` | Triggers App Store release | +| `claude/*` | AI-assisted feature branches | + +**Never push directly to `main` or `master`** without a PR. + +### Building + +Use Xcode or `xcodebuild`. For CI-style builds: + +```bash +xcodebuild \ + -workspace TimerWatcherWorkspace.xcworkspace \ + -scheme TimeWatcher \ + -sdk iphonesimulator \ + -configuration Debug \ + -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro" \ + clean build +``` + +--- + +## Running Tests + +### Via fastlane (recommended) + +```bash +bundle exec fastlane ios test +``` + +### Via 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 +``` + +### Test Plan + +`TimeWatcher.xctestplan` defines: +- **TimeWatcherTests** — enabled, parallelizable +- **TimeWatcherUITests** — disabled (snapshot tests, require manual setup) + +### Writing Tests + +- Place unit tests under `TimeWatcherPrj/TimeWatcherTests/` +- Use `TestUtilities.swift` for date manipulation helpers +- Inject mock dependencies via `DateDependency` to control time in tests +- Mirror the source directory structure in test directories + +--- + +## Deployment + +### Environment Setup + +Copy `.env.skel` to `.env` and fill in credentials: + +``` +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 Lanes + +| Command | Action | +|----------------------------------|---------------------------------------------| +| `fastlane ios test` | Run unit tests via scan | +| `fastlane ios beta` | Build + upload to TestFlight (build +0.1) | +| `fastlane ios release` | Build + submit to App Store (build +0.01) | +| `fastlane ios upload_beta` | Upload pre-built IPA to TestFlight | +| `fastlane ios upload_release` | Upload pre-built IPA to App Store | +| `fastlane ios match_development` | Sync development provisioning profiles | +| `fastlane ios match_appstore` | Sync App Store provisioning profiles | + +### CI/CD Pipelines + +| Workflow file | Trigger | Action | +|-------------------|-----------------------------|-------------------------------| +| `ci.yml` | PR (all branches) | Build + test on macOS/Xcode 15 | +| `cdBeta.yml` | Push to `release/beta/*` | Deploy to TestFlight (Xcode 16)| +| `cdRelease.yml` | Push to `release/store/*` | Deploy to App Store (Xcode 16) | + +--- + +## Key Files Reference + +| File | Purpose | +|------|---------| +| `TimeWatcher/AppModel/TimeWatch.swift` | Core timer engine, Combine publishers, state machine | +| `TimeWatcher/View/MainTimerView/MainTimerViewModel.swift` | Timer ViewModel, state exposed to UI | +| `TimeWatcher/View/MainTimerView/MainTimerView.swift` | Primary SwiftUI timer screen | +| `TimeWatcher/View/RootView/` | App root, URL/deep link handling | +| `TimeWatcher/AppModel/LiviActivity/LiveActivityManager.swift` | Live Activity lifecycle | +| `TimeWatcher/Dependency/DateDependency.swift` | Injectable date/time for testability | +| `TimeWatcherWidget/Intent/TimerStartIntent.swift` | Widget: start timer App Intent | +| `TimeWatcherWidget/Intent/TimerStopIntent.swift` | Widget: stop timer App Intent | +| `TimeWatcherTests/Utilities/TestUtilities.swift` | Shared test helpers | +| `TimeWatcherExternalResouce/swiftgen.yml` | SwiftGen config for asset code generation | +| `fastlane/Fastfile` | All deployment lane definitions | + +--- + +## Coding Conventions + +- **Swift 5.9+** syntax throughout; use structured concurrency (`async/await`) for new async code where possible +- **SwiftUI** for all UI — no UIKit unless integrating a framework that requires it +- **@Published** properties in ViewModels; views must not mutate model state directly +- **Logging** — use the global `logger` instance from `Logger.swift`, not `print()` +- **Constants** — define in `AppConstants.swift`, not as magic literals +- **Timer max display** — capped at 99 hours in the ViewModel; enforce this limit in business logic +- **Commit messages** — historically in Japanese; match the existing style when committing + +--- + +## Common Gotchas + +1. **Workspace vs. project** — Always open `TimerWatcherWorkspace.xcworkspace`. Opening `TimeWatcherPrj.xcodeproj` directly will miss the `TimeWatcherExternalResouce` Swift package. +2. **SwiftGen** — If colors/images are unavailable, run the SwiftGen plugin by building the `TimeWatcherExternalResouce` package target first. +3. **Live Activity on iOS 18** — A fix for App Intents not working in Live Activities on iOS 18 was applied (commit `da0eb9c`). Be careful not to revert that logic. +4. **UI Tests disabled** — `TimeWatcherUITests` are intentionally disabled in the test plan. Enable them only when running fastlane snapshot testing locally. +5. **Build number** — Do not manually edit build numbers. They are incremented automatically by fastlane lanes (`+0.1` for beta, `+0.01` for release). +6. **Parallelized tests** — `parallel_testing: false` is set in CI builds (`ci.yml`) to prevent simulator resource conflicts. Keep this setting in CI. + +--- + +## Dependencies + +### Swift Package Manager + +| Package | Version | Purpose | +|---------|---------|---------| +| SwiftGenPlugin | ≥ 6.6.2 | Type-safe asset code generation | + +### Ruby (Fastlane ecosystem) + +Managed via `Gemfile`. Install with: + +```bash +bundle install +``` + +Key gems: `fastlane`, `xcpretty`, `aws-sdk-s3` (for match), `google-cloud-storage`. + +--- + +## Environment Requirements + +- **Xcode 15** for CI (unit tests) +- **Xcode 16** for CD (deployment builds) +- **iOS 17.0** minimum deployment target +- **macOS 13+** for local development (macOS latest for CD) +- **Ruby** (version managed by Gemfile) for fastlane +- **Swift 5.9+** From 62d1e43387aeba81c51e55fb039dac88a89ec86b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 13:13:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92doc=E3=83=87=E3=82=A3=E3=83=AC?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=83=AA=E3=81=AB=E5=88=86=E5=89=B2=E3=81=97?= =?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.mdをリファレンスのみに縮小し、詳細情報をdoc/配下に整理した。 - README.md: プロジェクト概要を追記 - doc/architecture.md: アーキテクチャ・設計・規約 - doc/development.md: 開発環境・ブランチ戦略・ビルド方法 - doc/testing.md: テスト実行方法・テストの書き方 - doc/deployment.md: fastlaneレーン・CI/CDパイプライン https://claude.ai/code/session_01KsfWvCyMTnkteSWe2gd2nw --- CLAUDE.md | 293 +++----------------------------------------- README.md | 30 ++++- doc/architecture.md | 62 ++++++++++ doc/deployment.md | 40 ++++++ doc/development.md | 47 +++++++ doc/testing.md | 49 ++++++++ 6 files changed, 246 insertions(+), 275 deletions(-) create mode 100644 doc/architecture.md create mode 100644 doc/deployment.md create mode 100644 doc/development.md create mode 100644 doc/testing.md diff --git a/CLAUDE.md b/CLAUDE.md index f567700..4f91376 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,285 +1,30 @@ -# CLAUDE.md — TimeWatcher Codebase Guide +# CLAUDE.md -This file provides a comprehensive guide for AI assistants working in this repository. It covers architecture, development workflows, testing, and key conventions. +AI アシスタント向けのリファレンスです。詳細は各ドキュメントを参照してください。 ---- +## ドキュメント -## Project Overview +| ドキュメント | 内容 | +|------------|------| +| [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 パイプライン・環境変数 | -**TimeWatcher** is a native iOS timer application built with Swift and SwiftUI. It targets iOS 17.0+ and includes: -- A main iOS app with animated timer display -- An iOS Widget extension for home/lock screen timers -- Live Activity support (Dynamic Island / Lock Screen) -- Deep linking support -- Comprehensive unit tests - -The app is managed using an Xcode workspace and deployed via fastlane with GitHub Actions CI/CD. - ---- - -## Repository Structure - -``` -TimeWatcher/ -├── .github/workflows/ # CI/CD GitHub Actions (ci.yml, cdBeta.yml, cdRelease.yml) -├── TimeWatcherPrj/ # Xcode project root -│ ├── TimeWatcher/ # Main iOS app target -│ │ ├── AppModel/ # Core business logic -│ │ │ ├── TimeWatch.swift # Timer engine (singleton) -│ │ │ └── LiviActivity/ # Live Activity management -│ │ ├── View/ # SwiftUI views (MVVM pattern) -│ │ │ ├── RootView/ # App entry, deep linking -│ │ │ ├── MainTimerView/ # Primary timer UI + ViewModel -│ │ │ └── ViewParts/ # Reusable view components -│ │ ├── Utility/ # Helpers and extensions -│ │ │ ├── Extension/ # Date, Calendar, TimeInterval extensions -│ │ │ ├── Logger/ # App-wide logger -│ │ │ ├── Constant/ # AppConstants -│ │ │ ├── Resource/ # Resource adapters -│ │ │ └── ViewUtility/ # UI helper utilities -│ │ └── Dependency/ # Dependency injection (DateDependency) -│ ├── TimeWatcherWidget/ # Widget extension target -│ │ ├── Intent/ # App Intents for widget actions -│ │ └── Definition/ # Widget configuration -│ ├── TimeWatcherTests/ # Unit tests -│ │ ├── MainTimerView/ # ViewModel tests -│ │ ├── RootView/ # Deep link URL parsing tests -│ │ ├── TimerWatcherWidgetIntent/ # Widget intent tests -│ │ └── Utilities/ # TestUtilities.swift -│ └── TimeWatcherUITests/ # UI tests (currently disabled) -├── TimeWatcherExternalResouce/ # Swift Package: design tokens (SwiftGen) -├── TimerWatcherWorkspace.xcworkspace/ # Xcode workspace -├── fastlane/ # Deployment automation -│ ├── Fastfile # Lane definitions (test, beta, release) -│ ├── Appfile # Bundle ID and Apple account settings -│ ├── Gymfile # Build configuration (Release, app-store) -│ ├── Scanfile # Test scan configuration -│ ├── Matchfile # Code signing management -│ └── README.md # Auto-generated fastlane docs -├── Gemfile / Gemfile.lock # Ruby gem dependencies -├── TimeWatcher.xctestplan # Test plan (unit tests enabled, UI tests disabled) -├── .env.skel # Template for required environment variables -└── README.md # Minimal project description (Japanese) -``` - ---- - -## Architecture & Key Conventions - -### MVVM Pattern - -The app follows **Model-View-ViewModel** consistently: - -- **Model** — `TimeWatch.swift` (singleton timer engine using Combine) -- **ViewModel** — `MainTimerViewModel.swift` (ObservableObject, @Published properties) -- **View** — SwiftUI views that observe the ViewModel - -### Dependency Injection for Testability - -`DateDependency.swift` injects the current date/time to allow mocking in tests. All code that needs `Date.now` or `Calendar` should use this rather than calling system APIs directly. - -### Singleton Usage - -`TimeWatch` is a singleton (`TimeWatch.shared`). Avoid creating additional shared state. Prefer injecting dependencies via initializers in new code. - -### Reactive Programming - -The app uses **Combine** extensively. Timer ticks are published as a `Publisher` at 0.001-second resolution. ViewModels subscribe using `.sink` and cancel subscriptions in `deinit` or with `AnyCancellable`. - -### Live Activities - -`LiveActivityManager.swift` wraps `ActivityKit`. A mock version is provided for tests. Always update live activity state when timer state changes. - -### Widget Integration - -Widget App Intents (`TimerStartIntent`, `TimerStopIntent`) directly manipulate the shared `TimeWatch` singleton. Keep widget intents thin — delegate business logic to the model layer. - -### Resource / Design Tokens - -Colors and images are defined in the `TimeWatcherExternalResouce` Swift Package and generated via **SwiftGen** (`swiftgen.yml`). Always use generated type-safe accessors (`Asset.Colors.*`, `Asset.Images.*`) rather than string literals. - ---- - -## Development Workflow - -### Opening the Project - -Always open the **workspace**, not the `.xcodeproj`: - -``` -TimerWatcherWorkspace.xcworkspace -``` - -### Branch Strategy - -| Branch Pattern | Purpose | -|------------------------|------------------------------| -| `master` | Primary development branch | -| `release/beta/*` | Triggers TestFlight deployment | -| `release/store/*` | Triggers App Store release | -| `claude/*` | AI-assisted feature branches | - -**Never push directly to `main` or `master`** without a PR. - -### Building - -Use Xcode or `xcodebuild`. For CI-style builds: - -```bash -xcodebuild \ - -workspace TimerWatcherWorkspace.xcworkspace \ - -scheme TimeWatcher \ - -sdk iphonesimulator \ - -configuration Debug \ - -destination "platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro" \ - clean build -``` - ---- - -## Running Tests - -### Via fastlane (recommended) +## クイックリファレンス ```bash +# テスト実行 bundle exec fastlane ios test -``` - -### Via 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 -``` - -### Test Plan - -`TimeWatcher.xctestplan` defines: -- **TimeWatcherTests** — enabled, parallelizable -- **TimeWatcherUITests** — disabled (snapshot tests, require manual setup) - -### Writing Tests - -- Place unit tests under `TimeWatcherPrj/TimeWatcherTests/` -- Use `TestUtilities.swift` for date manipulation helpers -- Inject mock dependencies via `DateDependency` to control time in tests -- Mirror the source directory structure in test directories ---- - -## Deployment - -### Environment Setup - -Copy `.env.skel` to `.env` and fill in credentials: +# TestFlight 配信 +bundle exec fastlane ios beta +# App Store リリース +bundle exec fastlane ios release ``` -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 Lanes - -| Command | Action | -|----------------------------------|---------------------------------------------| -| `fastlane ios test` | Run unit tests via scan | -| `fastlane ios beta` | Build + upload to TestFlight (build +0.1) | -| `fastlane ios release` | Build + submit to App Store (build +0.01) | -| `fastlane ios upload_beta` | Upload pre-built IPA to TestFlight | -| `fastlane ios upload_release` | Upload pre-built IPA to App Store | -| `fastlane ios match_development` | Sync development provisioning profiles | -| `fastlane ios match_appstore` | Sync App Store provisioning profiles | - -### CI/CD Pipelines - -| Workflow file | Trigger | Action | -|-------------------|-----------------------------|-------------------------------| -| `ci.yml` | PR (all branches) | Build + test on macOS/Xcode 15 | -| `cdBeta.yml` | Push to `release/beta/*` | Deploy to TestFlight (Xcode 16)| -| `cdRelease.yml` | Push to `release/store/*` | Deploy to App Store (Xcode 16) | - ---- - -## Key Files Reference - -| File | Purpose | -|------|---------| -| `TimeWatcher/AppModel/TimeWatch.swift` | Core timer engine, Combine publishers, state machine | -| `TimeWatcher/View/MainTimerView/MainTimerViewModel.swift` | Timer ViewModel, state exposed to UI | -| `TimeWatcher/View/MainTimerView/MainTimerView.swift` | Primary SwiftUI timer screen | -| `TimeWatcher/View/RootView/` | App root, URL/deep link handling | -| `TimeWatcher/AppModel/LiviActivity/LiveActivityManager.swift` | Live Activity lifecycle | -| `TimeWatcher/Dependency/DateDependency.swift` | Injectable date/time for testability | -| `TimeWatcherWidget/Intent/TimerStartIntent.swift` | Widget: start timer App Intent | -| `TimeWatcherWidget/Intent/TimerStopIntent.swift` | Widget: stop timer App Intent | -| `TimeWatcherTests/Utilities/TestUtilities.swift` | Shared test helpers | -| `TimeWatcherExternalResouce/swiftgen.yml` | SwiftGen config for asset code generation | -| `fastlane/Fastfile` | All deployment lane definitions | - ---- - -## Coding Conventions - -- **Swift 5.9+** syntax throughout; use structured concurrency (`async/await`) for new async code where possible -- **SwiftUI** for all UI — no UIKit unless integrating a framework that requires it -- **@Published** properties in ViewModels; views must not mutate model state directly -- **Logging** — use the global `logger` instance from `Logger.swift`, not `print()` -- **Constants** — define in `AppConstants.swift`, not as magic literals -- **Timer max display** — capped at 99 hours in the ViewModel; enforce this limit in business logic -- **Commit messages** — historically in Japanese; match the existing style when committing - ---- - -## Common Gotchas - -1. **Workspace vs. project** — Always open `TimerWatcherWorkspace.xcworkspace`. Opening `TimeWatcherPrj.xcodeproj` directly will miss the `TimeWatcherExternalResouce` Swift package. -2. **SwiftGen** — If colors/images are unavailable, run the SwiftGen plugin by building the `TimeWatcherExternalResouce` package target first. -3. **Live Activity on iOS 18** — A fix for App Intents not working in Live Activities on iOS 18 was applied (commit `da0eb9c`). Be careful not to revert that logic. -4. **UI Tests disabled** — `TimeWatcherUITests` are intentionally disabled in the test plan. Enable them only when running fastlane snapshot testing locally. -5. **Build number** — Do not manually edit build numbers. They are incremented automatically by fastlane lanes (`+0.1` for beta, `+0.01` for release). -6. **Parallelized tests** — `parallel_testing: false` is set in CI builds (`ci.yml`) to prevent simulator resource conflicts. Keep this setting in CI. - ---- - -## Dependencies - -### Swift Package Manager - -| Package | Version | Purpose | -|---------|---------|---------| -| SwiftGenPlugin | ≥ 6.6.2 | Type-safe asset code generation | - -### Ruby (Fastlane ecosystem) - -Managed via `Gemfile`. Install with: - -```bash -bundle install -``` - -Key gems: `fastlane`, `xcpretty`, `aws-sdk-s3` (for match), `google-cloud-storage`. - ---- - -## Environment Requirements -- **Xcode 15** for CI (unit tests) -- **Xcode 16** for CD (deployment builds) -- **iOS 17.0** minimum deployment target -- **macOS 13+** for local development (macOS latest for CD) -- **Ruby** (version managed by Gemfile) for fastlane -- **Swift 5.9+** +- プロジェクトは必ず `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` | 日付操作などのテストヘルパー |