0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KiroのSpecモードでダッシュボードアプリを作成してみた

Posted at

はじめに

こんにちは!
某SI会社2グループのかとうです。

前回に引き続き、今回も、KiroのSpecモードを使ってiOSのモバイルアプリを作成しました。

前回は、Kiroが作成してくれた要件定義書・設計書を修正せず実装に進んだところ、
完成後に、ロジックに問題があったことが発覚してしまいました。
(最終的には修正し、アプリを完成させることができましたが…)

(前回の記事 ↓)

そこで今回は、要件定義・設計書でしっかり仕様を詰めてから、
実装してみようと思います。

これまで同様、「IT初心者が、AIエージェントを使ってどこまでのものを作れるのか?」という視点から、検証していきます。

よろしくお願いします!

今回の検証

今回も、Swiftを使用したモバイルアプリを作成します。

前回は、画面遷移もほとんどなかったので、
今回はもう少し機能の多いアプリを作ってみようと思います!

今回作成するアプリですが……

朝起きたとき、確認することがたくさんある!
でも、カレンダーとか時刻表アプリとか、いちいち開くのがめんどくさい!
一つのアプリで、全部まとめて確認したい!!

という人向けのアプリを作ってみました。

前回より画面数を増やし、あわよくば、他アプリとの連携なんかもできればいいな…という目論見で、検証開始です。

検証スタート!

前回は、最初からかなり詳細なプロンプトを投げましが、
今回は、ふわっとしたプロンプトを投げてみました。
細かい仕様は、Kiroに一旦お任せしてみます。

(プロンプト)
「朝起きたときに、いろんなアプリを開くのが面倒くさい。
天気も予定も電車の時間も、全部一つのアプリで確認したい。」

たったこれだけで、要件定義書が出力されます。

最初の要件定義書がこちら↓

要件定義書
# 要件定義書


## はじめに


モーニングダッシュボードは、ユーザーが朝起きたときに必要な情報(天気、予定、電車の時間)を一つの画面で確認できるアプリケーションです。複数のアプリを開く手間を省き、朝の準備を効率化することを目的としています。


## 用語集


- **ダッシュボード**: ユーザーに情報を表示するメイン画面
- **天気情報**: 現在地の天気予報データ
- **予定情報**: ユーザーのカレンダーイベント
- **電車情報**: 指定された路線の運行状況と時刻表
- **システム**: モーニングダッシュボードアプリケーション


## 要件


### 要件 1


**ユーザーストーリー:** ユーザーとして、朝起きたときに今日の天気を確認したい。そうすることで、適切な服装を選べるようになる。


#### 受け入れ基準


1. WHEN ユーザーがダッシュボードを開くとき、THEN システムは現在地の天気情報を表示しなければならない
2. WHEN 天気情報を表示するとき、THEN システムは気温、天気の状態(晴れ、曇り、雨など)、降水確率を含めなければならない
3. WHEN 天気APIからデータを取得できないとき、THEN システムはエラーメッセージを表示し、前回取得した天気情報をキャッシュから表示しなければならない
4. WHEN 天気情報が6時間以上古いとき、THEN システムは自動的に最新の天気情報を取得しなければならない


### 要件 2


**ユーザーストーリー:** ユーザーとして、今日の予定を一目で確認したい。そうすることで、朝の準備時間を調整できるようになる。

#### 受け入れ基準


1. WHEN ユーザーがダッシュボードを開くとき、THEN システムは今日の予定リストを時系列順に表示しなければならない
2. WHEN 予定を表示するとき、THEN システムは各予定の開始時刻、タイトル、場所を含めなければならない
3. WHEN 予定がないとき、THEN システムは「今日の予定はありません」というメッセージを表示しなければならない
4. WHEN カレンダーAPIからデータを取得できないとき、THEN システムはエラーメッセージを表示し、前回取得した予定情報をキャッシュから表示しなければならない


### 要件 3


**ユーザーストーリー:** ユーザーとして、よく使う路線の電車の時刻を確認したい。そうすることで、家を出る時間を決められるようになる。


#### 受け入れ基準


1. WHEN ユーザーがダッシュボードを開くとき、THEN システムは登録された路線の次の3本の電車の発車時刻を表示しなければならない
2. WHEN 電車情報を表示するとき、THEN システムは発車時刻、行き先、現在時刻からの残り時間を含めなければならない
3. WHEN 遅延や運休が発生しているとき、THEN システムは運行状況の警告を目立つ色で表示しなければならない
4. WHEN 電車APIからデータを取得できないとき、THEN システムはエラーメッセージを表示し、通常の時刻表データを表示しなければならない


### 要件 4


**ユーザーストーリー:** ユーザーとして、初回起動時に自分の設定(場所、路線など)を登録したい。そうすることで、自分に合った情報を表示できるようになる。


#### 受け入れ基準


1. WHEN ユーザーが初めてアプリを起動するとき、THEN システムは設定画面を表示しなければならない
2. WHEN 設定画面で入力するとき、THEN システムは現在地、よく使う駅、よく使う路線の入力フィールドを提供しなければならない
3. WHEN ユーザーが設定を保存するとき、THEN システムは入力内容をローカルストレージに永続化しなければならない
4. WHEN 必須項目が未入力のとき、THEN システムは保存を拒否し、エラーメッセージを表示しなければならない


### 要件 5


**ユーザーストーリー:** ユーザーとして、ダッシュボードの情報を手動で更新したい。そうすることで、最新の情報を確認できるようになる。

#### 受け入れ基準

1. WHEN ユーザーが更新ボタンをクリックするとき、THEN システムはすべての情報源から最新データを取得しなければならない
2. WHEN データ更新中のとき、THEN システムはローディングインジケーターを表示しなければならない
3. WHEN データ更新が完了したとき、THEN システムは更新された情報を表示し、最終更新時刻を記録しなければならない
4. WHEN データ更新が失敗したとき、THEN システムはエラーメッセージを表示し、既存のデータを保持しなければならない


### 要件 6


**ユーザーストーリー:** ユーザーとして、ダッシュボードを素早く開きたい。そうすることで、朝の忙しい時間を節約できるようになる。


#### 受け入れ基準


1. WHEN ユーザーがアプリを起動するとき、THEN システムは3秒以内にダッシュボードを表示しなければならない
2. WHEN キャッシュデータが利用可能なとき、THEN システムはキャッシュデータを即座に表示し、バックグラウンドで最新データを取得しなければならない
3. WHEN 初回起動でキャッシュがないとき、THEN システムはローディング画面を表示しながらデータを取得しなければならない




プロンプトで、アプリを開く早さには特に言及していませんが、
よく見ると、「早く開けること」にフォーカスしてくれてます。

### 要件 6


**ユーザーストーリー:** ユーザーとして、ダッシュボードを素早く開きたい。そうすることで、朝の忙しい時間を節約できるようになる。


#### 受け入れ基準


1. WHEN ユーザーがアプリを起動するとき、THEN システムは3秒以内にダッシュボードを表示しなければならない
2. WHEN キャッシュデータが利用可能なとき、THEN システムはキャッシュデータを即座に表示し、バックグラウンドで最新データを取得しなければならない
3. WHEN 初回起動でキャッシュがないとき、THEN システムはローディング画面を表示しながらデータを取得しなければならない

「朝は忙しいから、早く確認したいはず」という、ユーザーの要望を予測してくれたようです。

要件定義書のチェック

前回は要件定義に一切突っ込まず、実装に入りましたが、
今回は、気になるポイントや、要望を指摘していきます。

全てのやりとりを取り上げると長くなるので省略しますが、
修正・確認したポイントは、ざっくり以下の通りです。

・予定は、端末のカレンダーアプリから取得してもらう
・予定について、場所は必須項目としない
・アプリ内で新規予定を追加できるようにする
・「よく使う駅」「よく使う路線」の登録ではなく、「目的地」を登録し、そこにに行くルートを提示する
・アプリを開いた時、まずは予定や天気を一覧表示し、タップで詳細情報を開く
・SwiftUIで実装する!(最初に言っておくのを忘れてた!)

修正された要件定義書がこちら↓

要件定義書(修正後)
# 要件定義書


## はじめに


モーニングダッシュボードは、ユーザーが朝起きたときに必要な情報(天気、予定、電車の時間)を一つの画面で確認できるiOSアプリケーションです。複数のアプリを開く手間を省き、朝の準備を効率化することを目的としています。SwiftUIを使用して開発されます。


## 用語集


- **ダッシュボード**: ユーザーに情報を表示するメイン画面
- **天気情報**: 現在地の天気予報データ
- **予定情報**: ユーザーのカレンダーイベント
- **電車情報**: 指定された路線の運行状況と時刻表
- **システム**: モーニングダッシュボードアプリケーション
- **SwiftUI**: Appleが提供するiOSアプリ開発用のUIフレームワーク
- **EventKit**: iOSのカレンダーデータにアクセスするためのフレームワーク


## 要件


### 要件 1


**ユーザーストーリー:** ユーザーとして、朝起きたときに今日の天気を確認したい。そうすることで、適切な服装を選べるようになる。

#### 受け入れ基準

1. WHEN ユーザーがダッシュボードを開くとき、THEN ダッシュボードは現在地の天気情報を表示しなければならない
2. WHEN 天気情報を表示するとき、THEN ダッシュボードは気温、天気の状態(晴れ、曇り、雨など)、降水確率を含めなければならない
3. WHEN 天気APIからデータを取得できないとき、THEN ダッシュボードはエラーメッセージを表示し、前回取得した天気情報をキャッシュから表示しなければならない
4. WHEN 天気情報が6時間以上古いとき、THEN ダッシュボードは自動的に最新の天気情報を取得しなければならない
5. WHEN 天気情報を取得するとき、THEN ダッシュボードは取得時刻をタイムスタンプとして記録しなければならない


### 要件 2


**ユーザーストーリー:** ユーザーとして、今日の予定を一目で確認したい。そうすることで、朝の準備時間を調整できるようになる。


#### 受け入れ基準


1. WHEN ユーザーがダッシュボードを開くとき、THEN ダッシュボードは端末のカレンダーAPIを使用して今日の予定リストを時系列順に表示しなければならない
2. WHEN 予定を表示するとき、THEN ダッシュボードは各予定の開始時刻とタイトルを含めなければならない
3. WHERE 予定に場所が登録されているとき、THEN ダッシュボードはその場所も表示しなければならない
4. WHEN 予定がないとき、THEN ダッシュボードは「今日の予定はありません」というメッセージを表示しなければならない
5. WHEN カレンダーAPIからデータを取得できないとき、THEN ダッシュボードはエラーメッセージを表示し、前回取得した予定情報をキャッシュから表示しなければならない
6. WHEN 初回起動時にカレンダーへのアクセス権限がないとき、THEN ダッシュボードはユーザーに権限を要求しなければならない

1. WHEN ユーザーがダッシュボードを開くとき、THEN ダッシュボードは登録された路線の次の3本の電車の発車時刻を表示しなければならない
2. WHEN 電車情報を表示するとき、THEN ダッシュボードは発車時刻、行き先、現在時刻からの残り時間を含めなければならない
3. WHEN 遅延や運休が発生しているとき、THEN ダッシュボードは運行状況の警告を目立つ色で表示しなければならない
4. WHEN 電車APIからデータを取得できないとき、THEN ダッシュボードはエラーメッセージを表示し、通常の時刻表データを表示しなければならない

### 要件 3


**ユーザーストーリー:** ユーザーとして、よく使う路線の電車の時刻を確認したい。そうすることで、家を出る時間を決められるようになる。

1. WHEN ユーザーがダッシュボードを開くとき、THEN ダッシュボードは登録された路線の次の3本の電車の発車時刻を表示しなければならない
2. WHEN 電車情報を表示するとき、THEN ダッシュボードは発車時刻、行き先、現在時刻からの残り時間を含めなければならない
3. WHEN 遅延や運休が発生しているとき、THEN ダッシュボードは運行状況の警告を目立つ色で表示しなければならない
4. WHEN 電車APIからデータを取得できないとき、THEN ダッシュボードはエラーメッセージを表示し、通常の時刻表データを表示しなければならない


#### 受け入れ基準


### 要件 4


**ユーザーストーリー:** ユーザーとして、初回起動時に自分の設定(場所、路線など)を登録したい。そうすることで、自分に合った情報を表示できるようになる。


#### 受け入れ基準


1. WHEN ユーザーが初めてアプリを起動するとき、THEN ダッシュボードは設定画面を表示しなければならない
2. WHEN 設定画面で入力するとき、THEN ダッシュボードは現在地、よく使う駅、よく使う路線の入力フィールドを提供しなければならない
3. WHEN ユーザーが設定を保存するとき、THEN ダッシュボードは入力内容をローカルストレージに永続化しなければならない
4. WHEN 必須項目が未入力のとき、THEN ダッシュボードは保存を拒否し、エラーメッセージを表示しなければならない
5. WHEN 設定を読み込むとき、THEN ダッシュボードはローカルストレージから設定を取得し、アプリケーション状態に復元しなければならない


### 要件 6


**ユーザーストーリー:** ユーザーとして、ダッシュボードの情報を手動で更新したい。そうすることで、最新の情報を確認できるようになる。


#### 受け入れ基準


1. WHEN ユーザーが更新ボタンをクリックするとき、THEN ダッシュボードはすべての情報源から最新データを取得しなければならない
2. WHEN データ更新中のとき、THEN ダッシュボードはローディングインジケーターを表示しなければならない
3. WHEN データ更新が完了したとき、THEN ダッシュボードは更新された情報を表示し、最終更新時刻を記録しなければならない
4. WHEN データ更新が失敗したとき、THEN ダッシュボードはエラーメッセージを表示し、既存のデータを保持しなければならない


### 要件 7


**ユーザーストーリー:** ユーザーとして、ダッシュボードで情報のプレビューを確認し、必要に応じて詳細を見たい。そうすることで、素早く情報を把握しつつ、必要な時だけ詳しく確認できる。


#### 受け入れ基準


1. WHEN ユーザーがアプリを起動するとき、THEN ダッシュボードは今日の予定、今日の天気、電車の時刻のプレビューを一覧表示しなければならない
2. WHEN プレビューを表示するとき、THEN ダッシュボードは各情報カードに最も重要な情報のみを表示しなければならない
3. WHEN ユーザーが情報カードをタップするとき、THEN ダッシュボードはその情報の詳細画面を表示しなければならない
4. WHEN 詳細画面を表示するとき、THEN ダッシュボードはプレビューよりも詳しい情報を表示しなければならない
5. WHEN ユーザーが詳細画面を閉じるとき、THEN ダッシュボードはダッシュボードのプレビュー画面に戻らなければならない


### 要件 8


**ユーザーストーリー:** ユーザーとして、予定の詳細画面で新しい予定を追加したり既存の予定を編集したい。そうすることで、予定の管理をこのアプリ内で完結できる。


#### 受け入れ基準


1. WHEN ユーザーが予定の詳細画面を開くとき、THEN ダッシュボードは今日の予定だけでなく、将来の予定も時系列順に表示しなければならない


2. WHEN 予定一覧を表示するとき、THEN ダッシュボードは少なくとも今後30日間の予定を表示しなければならない
3. WHEN ユーザーが予定の詳細画面で追加ボタンをタップするとき、THEN ダッシュボードは空の予定入力フォームを表示しなければならない
4. WHEN ユーザーが既存の予定をタップして詳細画面で編集ボタンをタップするとき、THEN ダッシュボードは現在の予定情報が入力された予定編集フォームを表示しなければならない
5. WHEN 予定入力フォームを表示するとき、THEN ダッシュボードはタイトル、日付、開始時刻、終了時刻、場所の入力欄を提供しなければならない
6. WHEN ユーザーが新しい予定を保存するとき、THEN ダッシュボードはその予定を端末のカレンダーAPIを使用して登録しなければならない
7. WHEN ユーザーが既存の予定を更新するとき、THEN ダッシュボードはその変更を端末のカレンダーAPIを使用して反映しなければならない
8. WHEN ユーザーが既存の予定を削除するとき、THEN ダッシュボードはその予定を端末のカレンダーAPIを使用して削除しなければならない
9. WHEN 予定が正常に登録、更新、または削除されたとき、THEN ダッシュボードは予定一覧を更新して変更を表示しなければならない
10. WHEN 必須項目(タイトル、日付、開始時刻)が入力されていないとき、THEN ダッシュボードは保存を拒否し、エラーメッセージを表示しなければならない
11. WHEN 開始時刻が終了時刻より後のとき、THEN ダッシュボードは保存を拒否し、エラーメッセージを表示しなければならない
12. WHEN カレンダーAPIへの書き込みが失敗したとき、THEN ダッシュボードはエラーメッセージを表示し、ユーザーに再試行を促さなければならない


### 要件 9


**ユーザーストーリー:** ユーザーとして、天気の詳細画面で現在地を変更したい。そうすることで、別の場所の天気も確認できる。


#### 受け入れ基準


1. WHEN ユーザーが天気の詳細画面で場所編集ボタンをタップするとき、THEN ダッシュボードは場所入力フォームを表示しなければならない
2. WHEN 場所入力フォームを表示するとき、THEN ダッシュボードは都市名または住所を入力できる欄を提供しなければならない
3. WHEN ユーザーが新しい場所を指定するとき、THEN ダッシュボードはその場所の天気情報を取得して表示しなければならない
4. WHEN 場所の変更が成功したとき、THEN ダッシュボードはその場所をデフォルト地域として保存しなければならない
5. WHEN 指定された場所が見つからないとき、THEN ダッシュボードはエラーメッセージを表示し、現在の場所を保持しなければならない

### 要件 10


**ユーザーストーリー:** ユーザーとして、ダッシュボードを素早く開きたい。そうすることで、朝の忙しい時間を節約できるようになる。


#### 受け入れ基準


1. WHEN ユーザーがアプリを起動するとき、THEN ダッシュボードは3秒以内にダッシュボード画面を表示しなければならない
2. WHEN キャッシュデータが利用可能なとき、THEN ダッシュボードはキャッシュデータを即座に表示し、バックグラウンドで最新データを取得しなければならない
3. WHEN 初回起動でキャッシュがないとき、THEN ダッシュボードはローディング画面を表示しながらデータを取得しなければならない


次に、設計書を出力してもらいましたが、

設計書には、特にツッコミ所を見つけることができなかったので、
そのまま、実装に入ってもらいました。

設計書がこちら ↓

設計書
# 設計書


## 概要


モーニングダッシュボードは、ユーザーが朝起きたときに必要な情報(天気、予定、電車の時刻)を一つの画面で確認できるiOSアプリケーションです。SwiftUIを使用して開発し、外部APIとの統合、EventKitを使用したカレンダー連携、オフライン対応を実現します。


主な機能:
- 天気、予定、電車情報の統合ダッシュボード表示
- 各情報の詳細画面への遷移
- 予定の追加・編集・削除機能(EventKit連携)
- 天気情報の地域変更機能
- オフライン時のキャッシュデータ表示
- ユーザー設定の永続化(UserDefaults)


## アーキテクチャ


### 全体構成


┌─────────────────────────────────────────┐
│         Presentation Layer              │
│        (SwiftUI Views)                  │
└─────────────────────────────────────────┘

                  ↓
┌─────────────────────────────────────────┐
│          Application Layer              │
│    (ViewModels - ObservableObject)      │
└─────────────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────┐
│            Service Layer                │
│  (Business Logic & API Integration)     │
└─────────────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────┐
│          Infrastructure Layer           │
│  (External APIs, EventKit,              │
│   UserDefaults, Cache)                  │
└─────────────────────────────────────────┘



### レイヤー構成


1. **Presentation Layer(プレゼンテーション層)**
  - SwiftUI Views
  - 画面遷移管理(NavigationStack)
  - UIロジック


2. **Application Layer(アプリケーション層)**
  - ViewModels(ObservableObject)
  - アプリケーション全体の状態管理
  - ビジネスロジックの調整


3. **Service Layer(サービス層)**
  - WeatherService
  - CalendarService(EventKit)
  - TransitService
  - SettingsService
  - CacheService


4. **Infrastructure Layer(インフラストラクチャ層)**
  - 外部API通信(URLSession)
  - EventKit(カレンダーアクセス)
  - UserDefaults(設定保存)
  - FileManager(キャッシュ保存)


## コンポーネントとインターフェース


### 主要ビュー


#### 1. DashboardView
ダッシュボードのメイン画面。天気、予定、電車情報のプレビューカードを表示。


swift
struct DashboardView: View {
   @StateObject private var viewModel: DashboardViewModel
}



#### 2. WeatherCardView
天気情報のプレビューカード。


swift
struct WeatherCardView: View {
   let weather: WeatherPreview
   let onTap: () -> Void



#### 3. CalendarCardView
予定情報のプレビューカード。

swift
struct CalendarCardView: View {
   let events: [CalendarEvent]
   let onTap: () -> Void
}


#### 4. TransitCardView
電車情報のプレビューカード。


swift
struct TransitCardView: View {
   let trains: [TrainInfo]
   let onTap: () -> Void
}



#### 5. WeatherDetailView
天気の詳細画面。時間帯別の天気予報と地域変更機能。


swift
struct WeatherDetailView: View {
   @StateObject private var viewModel: WeatherDetailViewModel
}


#### 6. CalendarDetailView
予定の詳細画面。予定一覧と追加・編集・削除機能。


swift
struct CalendarDetailView: View {
   @StateObject private var viewModel: CalendarDetailViewModel
}



#### 7. EventFormView
予定の追加・編集フォーム画面。


swift
struct EventFormView: View {
   @StateObject private var viewModel: EventFormViewModel
   let mode: EventFormMode
   let event: CalendarEvent?
}


enum EventFormMode {
   case create
   case edit
}


#### 8. SettingsView
初回設定および設定変更画面。


swift
struct SettingsView: View {
   @StateObject private var viewModel: SettingsViewModel
}



### ViewModels


#### DashboardViewModel
swift
@MainActor
class DashboardViewModel: ObservableObject {
@Published var weatherPreview: WeatherPreview?
   @Published var todayEvents: [CalendarEvent] = []
   @Published var upcomingTrains: [TrainInfo] = []
   @Published var isLoading: Bool = false
   @Published var lastUpdateTime: Date?
   @Published var errorMessage: String?
  
   private let weatherService: WeatherService
   private let calendarService: CalendarService
   private let transitService: TransitService
  
   func loadAllData() async
   func refreshData() async
}



#### WeatherDetailViewModel
swift
@MainActor
class WeatherDetailViewModel: ObservableObject {
   @Published var currentWeather: WeatherData?
   @Published var hourlyForecast: [HourlyForecast] = []
   @Published var isLoading: Bool = false
   @Published var errorMessage: String?
  
   private let weatherService: WeatherService
  
   func loadWeatherData() async
   func changeLocation(_ location: Location) async
}



#### CalendarDetailViewModel
swift
@MainActor
class CalendarDetailViewModel: ObservableObject {
   @Published var events: [CalendarEvent] = []
   @Published var isLoading: Bool = false
   @Published var errorMessage: String?
  
   private let calendarService: CalendarService
  
   func loadEvents() async
   func deleteEvent(_ event: CalendarEvent) async
}

#### EventFormViewModel
swift
@MainActor
class EventFormViewModel: ObservableObject {
   @Published var title: String = ""
   @Published var date: Date = Date()
   @Published var startTime: Date = Date()
   @Published var endTime: Date = Date()
   @Published var location: String = ""
   @Published var errorMessage: String?
  
   private let calendarService: CalendarService
  
   func saveEvent() async -> Bool
   func validateInput() -> Bool
}



#### SettingsViewModel
swift
@MainActor
class SettingsViewModel: ObservableObject {
   @Published var defaultLocation: String = ""
   @Published var fromStation: String = ""
   @Published var toStation: String = ""
   @Published var lineName: String = ""
   @Published var errorMessage: String?
  
   private let settingsService: SettingsService
  
   func loadSettings() async
   func saveSettings() async -> Bool
   func validateSettings() -> Bool
}



### サービスプロトコル


#### WeatherService
swift
protocol WeatherService {
   func getCurrentWeather(location: Location) async throws -> WeatherData
   func getHourlyForecast(location: Location) async throws -> [HourlyForecast]
 func getCachedWeather() async -> WeatherData?
   func cacheWeather(_ data: WeatherData) async throws
}



#### CalendarService
swift
protocol CalendarService {
   func requestAccess() async -> Bool
   func getTodayEvents() async throws -> [CalendarEvent]
   func getEventsInRange(startDate: Date, endDate: Date) async throws -> [CalendarEvent]
   func createEvent(_ event: CalendarEventInput) async throws -> CalendarEvent
   func updateEvent(eventId: String, event: CalendarEventInput) async throws -> CalendarEvent
   func deleteEvent(eventId: String) async throws
   func getCachedEvents() async -> [CalendarEvent]
   func cacheEvents(_ events: [CalendarEvent]) async throws
}



#### TransitService
swift
protocol TransitService {
   func getNextTrains(route: TransitRoute) async throws -> [TrainInfo]
   func getServiceStatus(route: TransitRoute) async throws -> ServiceStatus
   func getCachedTrains() async -> [TrainInfo]
   func cacheTrains(_ trains: [TrainInfo]) async throws
}



#### SettingsService
swift
protocol SettingsService {
   func getSettings() async -> UserSettings
   func saveSettings(_ settings: UserSettings) async throws
   func isFirstLaunch() async -> Bool
}



## データモデル


### WeatherData
swift
struct WeatherData: Codable, Equatable {
let location: Location
   let temperature: Double
   let condition: WeatherCondition
   let precipitationProbability: Int
   let timestamp: Date
}


struct Location: Codable, Equatable {
   let name: String
   let latitude: Double
   let longitude: Double
}


enum WeatherCondition: String, Codable {
   case sunny
   case cloudy
   case rainy
   case snowy
   case stormy
}



### WeatherPreview
```swift
struct WeatherPreview: Equatable {
   let temperature: Double
   let condition: WeatherCondition
   let precipitationProbability: Int
}



### HourlyForecast
swift
struct HourlyForecast: Codable, Equatable, Identifiable {
   let id: UUID
   let time: Date
   let temperature: Double
   let condition: WeatherCondition
   let precipitationProbability: Int
}


swift
struct CalendarEvent: Identifiable, Equatable {
 let id: String
   let title: String
   let startTime: Date
   let endTime: Date
   let location: String?
   let description: String?
}


struct CalendarEventInput {
   let title: String
   let startTime: Date
   let endTime: Date
   let location: String?
   let description: String?
}



### TrainInfo
swift
struct TrainInfo: Identifiable, Equatable {
   let id: UUID
   let departureTime: Date
   let destination: String
   let minutesUntilDeparture: Int
   let platform: String?
   let status: TrainStatus
}


enum TrainStatus: String, Codable {
   case onTime
   case delayed
   case cancelled
}



### ServiceStatus
swift
struct ServiceStatus: Equatable {
   let route: TransitRoute
   let status: ServiceStatusType
   let message: String?
}


enum ServiceStatusType: String {
   case normal
 case delayed
   case disrupted
}



### TransitRoute
```swift
struct TransitRoute: Codable, Equatable {
   let lineName: String
   let fromStation: String
   let toStation: String
}



### UserSettings
```swift
struct UserSettings: Codable, Equatable {
   let defaultLocation: Location
   let transitRoute: TransitRoute
   let isFirstLaunch: Bool
}



### CacheEntry
swift
struct CacheEntry<T: Codable>: Codable {
   let data: T
   let timestamp: Date
   let expiresAt: Date
}





## 正確性プロパティ


*プロパティとは、システムの全ての有効な実行において真であるべき特性や振る舞いのことです。本質的には、システムが何をすべきかについての形式的な記述です。プロパティは、人間が読める仕様と機械で検証可能な正確性の保証との橋渡しをします。*


### プロパティ 1: 天気情報の必須フィールド表示
*任意の*天気データに対して、レンダリングされた天気カードには気温、天気の状態、降水確率が含まれていなければならない
**検証: 要件 1.2**


### プロパティ 2: 古い天気情報の自動更新
*任意の*キャッシュされた天気データに対して、タイムスタンプが6時間以上前の場合、ダッシュボードは最新の天気情報を取得しなければならない
**検証: 要件 1.4**


### プロパティ 3: 天気情報取得時のタイムスタンプ記録
*任意の*天気情報取得に対して、取得後のデータにはタイムスタンプが記録されていなければならない
**検証: 要件 1.5**


### プロパティ 4: 予定の時系列順表示
*任意の*予定リストに対して、表示される予定は開始時刻の昇順でソートされていなければならない
**検証: 要件 2.1, 8.1**


### プロパティ 5: 予定の必須フィールド表示
*任意の*予定に対して、レンダリングされた予定カードには開始時刻とタイトルが含まれていなければならない
**検証: 要件 2.2**


### プロパティ 6: 予定の場所の条件付き表示
*任意の*予定に対して、場所が設定されている場合はレンダリング結果に場所が含まれ、設定されていない場合は含まれないこと
**検証: 要件 2.3**


### プロパティ 7: 電車情報の表示数制限
*任意の*電車データリストに対して、表示される電車は最大3本までであること
**検証: 要件 3.1**


### プロパティ 8: 電車情報の必須フィールド表示
*任意の*電車データに対して、レンダリングされた電車カードには発車時刻、行き先、残り時間が含まれていなければならない
**検証: 要件 3.2**


### プロパティ 9: 遅延・運休時の警告表示
*任意の*遅延または運休状態の電車データに対して、レンダリング結果には警告要素が含まれていなければならない
**検証: 要件 3.3**


### プロパティ 10: 設定のラウンドトリップ
*任意の*有効な設定データに対して、保存してから読み込んだ設定は元の設定と等しくなければならない
**検証: 要件 4.3, 4.5**


### プロパティ 11: 不完全な設定の保存拒否
*任意の*必須項目が欠けている設定データに対して、保存操作は失敗し、エラーメッセージが表示されなければならない
**検証: 要件 4.4**
### プロパティ 12: 更新時の全サービス呼び出し
*任意の*更新操作に対して、天気サービス、カレンダーサービス、電車サービスの全てが呼び出されなければならない
**検証: 要件 6.1**


### プロパティ 13: データ更新中のローディング表示
*任意の*データ更新操作中、ローディングインジケーターが表示されていなければならない
**検証: 要件 6.2**


### プロパティ 14: データ更新完了時のタイムスタンプ更新
*任意の*データ更新完了後、最終更新時刻が更新されていなければならない
**検証: 要件 6.3**


### プロパティ 15: カードタップ時の詳細画面遷移
*任意の*情報カード(天気、予定、電車)に対して、タップすると対応する詳細画面に遷移しなければならない
**検証: 要件 7.3**


### プロパティ 16: 詳細画面からの戻り操作
*任意の*詳細画面に対して、戻り操作を実行するとダッシュボード画面に戻らなければならない
**検証: 要件 7.5**


### プロパティ 17: 予定詳細画面の表示範囲
*任意の*予定リストに対して、予定詳細画面には少なくとも今後30日間の予定が表示されなければならない
**検証: 要件 8.2**


### プロパティ 18: 予定編集フォームの初期値
*任意の*既存の予定に対して、編集フォームには現在の予定情報が初期値として設定されていなければならない
**検証: 要件 8.4**


### プロパティ 19: 予定の作成と登録
*任意の*有効な予定データに対して、保存後にEventKitを通じて登録され、予定一覧に表示されなければならない
**検証: 要件 8.6, 8.9**


### プロパティ 20: 予定の更新と反映
*任意の*既存の予定の変更に対して、保存後にEventKitを通じて更新され、予定一覧に反映されなければならない
**検証: 要件 8.7, 8.9**


### プロパティ 21: 予定の削除
*任意の*既存の予定に対して、削除後にEventKitから削除され、予定一覧から消えなければならない
**検証: 要件 8.8, 8.9**


### プロパティ 22: 予定の必須項目検証
*任意の*タイトル、日付、または開始時刻が欠けている予定データに対して、保存操作は失敗し、エラーメッセージが表示されなければならない
**検証: 要件 8.10**


### プロパティ 23: 予定の時刻検証
*任意の*開始時刻が終了時刻より後の予定データに対して、保存操作は失敗し、エラーメッセージが表示されなければならない
**検証: 要件 8.11**


### プロパティ 24: 場所変更時の天気情報取得
*任意の*有効な場所に対して、場所を変更するとその場所の天気情報が取得されて表示されなければならない
**検証: 要件 9.3**


### プロパティ 25: 場所変更の永続化
*任意の*場所変更に対して、変更後の場所がデフォルト地域として保存されなければならない
**検証: 要件 9.4**


### プロパティ 26: キャッシュデータの即座表示
*任意の*有効なキャッシュデータが存在する場合、ダッシュボードはキャッシュデータを即座に表示しなければならない
**検証: 要件 10.2**


## エラーハンドリング


### エラーの種類


1. **ネットワークエラー**
  - 天気API、電車APIへの接続失敗
  - タイムアウト
  - 対応: キャッシュデータの表示、エラーメッセージの表示


2. **権限エラー**
  - カレンダーへのアクセス権限がない
  - 位置情報へのアクセス権限がない
  - 対応: 権限要求ダイアログの表示


3. **データ検証エラー**
  - 不完全な設定データ
  - 無効な予定データ(必須項目欠如、時刻の矛盾)
  - 対応: エラーメッセージの表示、保存の拒否


4. **外部APIエラー**
  - EventKitの書き込み失敗
  - 天気APIのレスポンスエラー
 - 対応: エラーメッセージの表示、再試行の促し


5. **データ不在エラー**
  - 予定がない
  - 路線が設定されていない
  - 対応: 適切なメッセージの表示、設定への誘導


### エラーハンドリング戦略


1. **グレースフルデグラデーション**
  - 一部の情報取得に失敗しても、他の情報は正常に表示
  - キャッシュデータを活用してオフライン時も基本機能を提供


2. **ユーザーフィードバック**
  - エラー発生時は明確なメッセージを表示
  - 再試行や設定変更など、次のアクションを提示


3. **データの整合性**
  - 保存前にデータを検証
  - 不正なデータは保存せず、エラーを通知


4. **リトライメカニズム**
  - ネットワークエラー時は自動リトライ(最大3回)
  - ユーザーによる手動更新も可能


## テスト戦略


### ユニットテスト


各サービス、ViewModel、ユーティリティ関数に対してユニットテストを実施します。


**テスト対象:**
- WeatherService: API呼び出し、キャッシュ操作
- CalendarService: CRUD操作、権限チェック
- TransitService: API呼び出し、キャッシュ操作
- SettingsService: 設定の保存・読み込み
- データ検証関数: 予定データ、設定データの検証
- 日付・時刻ユーティリティ: 時刻計算、フォーマット


**テストフレームワーク:** XCTest


### プロパティベーステスト


正確性プロパティに基づいてプロパティベーステストを実施します。


**テストライブラリ:** SwiftCheck (Swift用のプロパティベーステストライブラリ)


**設定:**
- 各プロパティテストは最低100回の反復を実行
- 各テストには対応する正確性プロパティへの参照をコメントで明記
- フォーマット: `// Feature: morning-dashboard, Property {number}: {property_text}`


**テスト対象:**
- プロパティ1〜26の全ての正確性プロパティ
- ランダムなデータ生成による広範な入力のテスト
- エッジケース(空のリスト、境界値など)の自動検出


### UIテスト


画面遷移、ユーザーインタラクション、エンドツーエンドのフローをテストします。


**テスト対象:**
- ダッシュボードから詳細画面への遷移
- 予定の作成・編集・削除フロー
- 設定変更の反映
- データ更新フロー


**テストフレームワーク:** XCTest UI Testing


### テストデータ生成


プロパティベーステストのために、以下のジェネレーターを実装します:


- `Gen.weatherData()`: ランダムな天気データ
- `Gen.calendarEvent()`: ランダムな予定データ
- `Gen.trainInfo()`: ランダムな電車データ
- `Gen.userSettings()`: ランダムな設定データ
- `Gen.location()`: ランダムな位置情報


各ジェネレーターは有効なデータと無効なデータの両方を生成できるようにします。


## 技術スタック


### フロントエンド
- **フレームワーク:** SwiftUI
- **言語:** Swift 5.9+
- **最小対応バージョン:** iOS 16.0+
- **ナビゲーション:** NavigationStack


### データ永続化
- **ローカルストレージ:** UserDefaults(設定)
- **キャッシュ:** FileManager + Codable(天気、電車、予定のキャッシュ)


### 外部API統合
- **天気API:** OpenWeatherMap API
- **電車API:** 各国の公共交通APIまたはサードパーティAPI
- **カレンダー:** EventKit
- **位置情報:** CoreLocation
- **ネットワーク:** URLSession + async/await


### テスト
- **ユニットテスト:** XCTest
- **プロパティベーステスト:** SwiftCheck
- **UIテスト:** XCTest UI Testing


### 開発ツール
- **IDE:** Xcode 15.0+
- **パッケージマネージャー:** Swift Package Manager
- **リンター:** SwiftLint
- **フォーマッター:** swift-format


## 実装の考慮事項


### パフォーマンス
- 初回起動時はキャッシュデータを優先表示し、バックグラウンドで最新データを取得
- 画像やアイコンの最適化
- LazyVStackを使用したリストの効率的なレンダリング


### セキュリティ
- APIキーは環境変数またはInfo.plistで管理
- ユーザーデータはローカルに保存(外部サーバーには送信しない)
- HTTPS通信の使用


### アクセシビリティ
- VoiceOver対応
- Dynamic Type対応
- 適切なコントラスト比
- タッチターゲットのサイズ確保


### ローカライゼーション
- 日本語UIをデフォルトとする
- 日付・時刻のローカライズ
- 将来的な多言語対応の準備


### オフライン対応
- キャッシュデータの活用
- オフライン時の明確な表示
- オンライン復帰時の自動更新


### プライバシー
- Info.plistに適切な権限説明を記載
 - NSCalendarsUsageDescription
 - NSLocationWhenInUseUsageDescription
- ユーザーが権限を拒否した場合の適切な処理

実装

設計書を元にタスクリストを作成してもらい、それに沿って実装を進めていたのですが、
その中で、一つ予想外だったことがありました。

Kiroが提案してくれた天気APIが、会員登録必須のものだったのです😵
(登録できないこともないけど、できればしたくない…)

モック実装や、APIの変更を頼むこともできましたが、
ここは試しに、Kiroに代替案を提案してもらうことにしました。

スクリーンショット 2025-12-24 12.51.25.png

好きなものを選べるように、たくさん提案してくれました🎉
せっかくなので、Kiroのおすすめ三つを、実装してもらうことにします。

★ 修正のポイント ★

修正内容が決まったらKIroに直してもらうのですが、
ここで一つ、注意点があります。

「修正して!」とだけお願いしてしまうと、
要件定義書や設計書が修正されず、実装だけ変わってしまうので、
せっかくのドキュメントが、実装と乖離してしまいます。

前回は、微々たる修正だったので、実装だけ直しても問題なかったのですが、
基本は、ドキュメントごと修正するのがいいと思います。

チャットで、「ドキュメントも一緒に変えてね」と一言付け加えておけば、
ちゃんとその通りやってくれます。

アプリ完成!

要件定義書の修正を依頼し、
新たに作成されたタスクを実行、あとはいくつか出ていたエラーを解消して、
無事、アプリ完成です!

ここまでの作業で、約4時間かかりました。

完成したアプリがこちら。
完成直後アプリ画面.gif

UIも整っており、基本的な機能が、ちゃんと要件通りに実装されていました。
(なお、電車の時刻表だけ、モック実装になっています。)

ただ、ドキュメントに明記されていない細かい部分で、イメージと違うところがいくつかありました。

例えば、予定の登録画面で、日付が「12月13日」ではなく、
「Dec,13」のような形になっていたり…。

今回のアプリは、機能を欲張ったためか、修正したい箇所が多く、
納得のいくところまで修正するのに、追加で4時間必要でした。

仕様通りにできてない!というよりは、
ドキュメントに明記されていない、細かい部分でイメージが違った感じです。

修正後

細部を修正して、ようやく、納得のいくアプリになりました!

スクリーンショット 2025-12-04 16.51.36.jpeg

UIは、朝らしい、爽やかなテイストにしてもらいました☺️
(100%Kiroお任せです)

こんな風に、iOS標準のリマインダー、カレンダーと同期できます。

↓ リマインダーをアプリから追加したり…
リマインダー同期.gif

↓ カレンダーの予定をアプリから削除したり…

カレンダー連携.gif

なかなか、良いアプリになったのではないでしょうか。

おわりに

今回の感想

ここまで作成してみて思いましたが、
これなら、要件定義には、そんなに時間かけなくてもよかったかも??

どちらにしても、実装終了後に手直しは必要になりそうです。
Kiroの出力してくれるドキュメントはかなり細かいですが、どうしても明記されていない部分でイメージと違う……は出てきますし、
そもそも、詳細に読むのは結構手間です💦

実装が一通り終わっても、要件定義書の修正はできるので、
ある程度イメージに近い要件ができたら、まず実装してみて、仕様を見てから修正、の方が効率いいように感じました。

後から大きく修正するのは大変なので、
明らかにおかしいところだけ、直しておけばいいのかなという気がします。

ちゃんとこれまでの仕様がドキュメントに明記されているので、
安心して修正をお願いすることができます。

Kiro Specモードを使ってみて

11月に引き続き、12月もKiroのSpecモードを使いましたが、
初心者の目線からの感想として、
「AIとの連携が取れること」が嬉しいなと思いました。

例えば……他のAIでVibeコーディングをしているとき、
やりたいことをAIにどう説明するか、プロンプト作成で迷うことがありますが、
KIroの場合は、要件定義書の用語集なども使い、楽に指示を出すことができました。

また、タスクリストを順番に実行していくので、
全体のタスクと、それが今どこまで進んでいるのかを、常に把握できるのがありがたかったです。

他のAIでVibeコーディングしている時に比べて、
やりたいことを、Kiroと共有できている安心感がありました☺️

実際の業務でどうやって使えるかも、今後、検証してみたい……

それでは、今回の記事はここまでです。
お読みいただきありがとうございました!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?