SwiftUI でアプリを作っていると、実装の方針を決めるだけで30分が過ぎることがあります。「このアニメーション、.animation と .withAnimation どちらで書くべきだろう」「SwiftData の @Relationship の deleteRule、ここは .cascade にすべきか .nullify にすべきか」——そういった判断の積み重ねが、1日の終わりに妙な疲れを生むことはありませんか。
Antigravity をSwiftUI プロジェクトに組み込んでから、この種の「判断コスト」がかなり下がりました。ただ、どんな問いを持ち込んでも解決するわけではありません。効果が高い場面と、そうでない場面の差が意外とはっきりあります。ここでは個人で iOS アプリを開発している視点から、具体的なパターンをお伝えします。
プロジェクト設定ファイルに「制約」を先に書いておく
Antigravity が生成するコードの品質は、どれだけプロジェクトの文脈を伝えられているかに大きく左右されます。新しい iOS プロジェクトで最初にやっておきたいのが、.antigravity/settings.json へのプロジェクト方針の記述です。
{
"instructions": "This is an iOS app using SwiftUI and SwiftData. Minimum deployment target is iOS 17. Use Swift 5.10 features where appropriate. Prefer @Observable macro over ObservableObject. Always add proper error handling with Result type or throws. Do not use deprecated APIs.",
"exclude": [
".build/",
"*.xcuserdata",
"*.xcworkspace/xcuserdata"
]
}「@Observable マクロを優先する」「非推奨 API は使わない」この2行を書いておくだけで、古い ObservableObject パターンではなく iOS 17 以降のコードが出てくるようになります。毎回「iOS 17 の書き方で」と伝えなくて済むようになるのは、地味に効いてきます。
カスタムルールを活用する場合はAntigravity カスタムルール&プロジェクト設定 完全実践ガイドも参考になります。チーム開発ではなく個人開発でも、プロジェクトをまたいで設定を統一するのに使えます。
UI プロトタイピング:見た目より「制約」を先に伝える
SwiftUI の View 設計で Antigravity が最も力を発揮するのは、「どの修飾子の組み合わせが正解かわからない」という場面です。このとき、最終的な見た目をそのまま言葉で説明するより、制約条件と目的を先に提示する方が精度の高いコードが返ってきます。
// プロンプト例:
// 「カード型リストアイテムを作りたい。条件:
// - タップ時にスケールアニメーション(バウンス感あり)
// - SF Symbols アイコン + タイトル / サブタイトルの2行テキスト
// - ダークモード対応(.secondary カラー使用)
// - iOS 17 の @Observable モデルからデータを受け取る」
struct ItemCardView: View {
let item: Item // @Observable Item モデル
@State private var isPressed = false
var body: some View {
HStack(spacing: 12) {
Image(systemName: item.iconName)
.font(.title2)
.foregroundStyle(.accent)
.frame(width: 44, height: 44)
.background(
.accent.opacity(0.1),
in: RoundedRectangle(cornerRadius: 10)
)
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.tertiary)
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 14))
.scaleEffect(isPressed ? 0.97 : 1.0)
.animation(.spring(duration: 0.2), value: isPressed)
.onTapGesture {
withAnimation { isPressed = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
isPressed = false
}
}
}
}このコードをそのまま使うより、「なぜ .spring(duration: 0.2) を使ったのか」「isPressed を @State にした理由は」といった点を読み解くことに価値があります。生成されたコードが理解の素材になることで、次からは自分でアレンジできるようになります。
SwiftData モデル設計:関係性の設計ミスを防ぐ
SwiftData の @Relationship は、設計を間違えると後からスキーママイグレーションが必要になります。個人開発では特に厄介な問題で、リリース後のユーザーデータに影響することもあります。
Antigravity へのアプローチとして効果的なのは、ER 図ではなく日本語で関係性を説明する方法です。「プロジェクトを削除したらタスクも一緒に消す」「タグは複数タスクから参照できる多対多」と伝えると、deleteRule の使い分けを含めたコードが出てきます。
// プロンプト: 「タスク管理アプリ。プロジェクト削除→タスクも削除(cascade)。
// タグは複数タスクに紐づく多対多。Task は Codable も必要」
import SwiftData
@Model
final class Project {
var name: String
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Task.project)
var tasks: [Task] = []
init(name: String) {
self.name = name
self.createdAt = .now
}
}
@Model
final class Task: Codable {
var title: String
var isCompleted: Bool
var dueDate: Date?
var project: Project?
@Relationship(deleteRule: .nullify, inverse: \Tag.tasks)
var tags: [Tag] = []
enum CodingKeys: String, CodingKey {
case title, isCompleted, dueDate
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
title = try c.decode(String.self, forKey: .title)
isCompleted = try c.decode(Bool.self, forKey: .isCompleted)
dueDate = try c.decodeIfPresent(Date.self, forKey: .dueDate)
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(title, forKey: .title)
try c.encode(isCompleted, forKey: .isCompleted)
try c.encodeIfPresent(dueDate, forKey: .dueDate)
}
init(title: String) {
self.title = title
self.isCompleted = false
}
}
@Model
final class Tag {
var name: String
var colorHex: String
var tasks: [Task] = []
init(name: String, colorHex: String) {
self.name = name
self.colorHex = colorHex
}
}deleteRule: .cascade(タスクをプロジェクトと一緒に削除)と .nullify(タグ削除時にタスクは残す)の使い分けが一度で正しく出てくることが多く、「後から気づいてマイグレーション」という事故を防げます。
テストコード生成:実装と同時に積み上げる
Unit Test は「後で書く」と決めてしまうと、ほぼ書かずにリリースになります。私もそのパターンを繰り返してきました。Antigravity を使うと、実装したコードを選択して「このビジネスロジック用のテストを生成して」と投げるだけで XCTest ベースのコードが出てきます。
テスト対象はビジネスロジックに絞るのが現実的です。View のレイアウトをテストするより、@Observable モデルのメソッドをテストする方が長く使えます。
// テスト対象モデル
@Observable
final class TaskFilter {
var showCompleted: Bool = false
var selectedTag: Tag? = nil
func filter(_ tasks: [Task]) -> [Task] {
tasks.filter { task in
let completedMatch = showCompleted ? true : \!task.isCompleted
let tagMatch: Bool
if let tag = selectedTag {
tagMatch = task.tags.contains(where: { $0.name == tag.name })
} else {
tagMatch = true
}
return completedMatch && tagMatch
}
}
}
// Antigravity が生成したテストコード
final class TaskFilterTests: XCTestCase {
var filter: TaskFilter\!
var pendingTask: Task\!
var completedTask: Task\!
override func setUp() {
filter = TaskFilter()
pendingTask = Task(title: "未完了タスク")
completedTask = Task(title: "完了タスク")
completedTask.isCompleted = true
}
func testHidesCompletedTasksByDefault() {
let result = filter.filter([pendingTask, completedTask])
XCTAssertEqual(result.count, 1)
XCTAssertFalse(result.first\!.isCompleted)
}
func testShowsAllTasksWhenEnabled() {
filter.showCompleted = true
let result = filter.filter([pendingTask, completedTask])
XCTAssertEqual(result.count, 2)
}
func testFiltersByTag() {
let tag = Tag(name: "仕事", colorHex: "#3B82F6")
pendingTask.tags = [tag]
filter.selectedTag = tag
let result = filter.filter([pendingTask, completedTask])
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result.first?.title, "未完了タスク")
}
}テストが機能ごとに積み上がると、リファクタリングへの心理的なハードルが下がります。これが個人開発の長期メンテナンスに思いのほか効いてきます。
テスト生成の詳細な戦略についてはAntigravity AI でテストコードを自動生成にまとめています。
Antigravity が苦手な場面も知っておく
SwiftUI 開発で使っていて気づいた、Antigravity の精度が落ちやすいパターンを共有します。
大きなファイルをそのまま貼り付けた場合
200行を超える View ファイルをコンテキストとして渡すと、応答の精度が落ちることがあります。View を適切にコンポーネント分割して1ファイルを小さく保つことが、AI のためにも SwiftUI の設計原則としても正しい方向です。
#Preview 周り
Antigravity が生成した View コードで #Preview が動かないことがあります。@Environment や @Query を使う View は、プレビュー用のインメモリ ModelContainer を設定する必要があります。「このViewのPreviewコードも書いて」と追加で依頼すると、ModelContainer(for: [Task.self], configurations: ModelConfiguration(isStoredInMemoryOnly: true)) を含むコードが出てきます。
Xcode 環境との連携を深めたい場合はXcode × Antigravity iOS 開発ガイドも参考になります。
今日から試せる一歩
今取り組んでいる View の中で「この書き方で合っているか自信がない」と感じている部分を1つ選んでみてください。Antigravity への問いかけを「どう書けばいいですか」ではなく、「今こう書いているのですが、iOS 17 のベストプラクティスとどこが違いますか」にしてみると、修正点だけでなく判断の根拠まで返ってくることが多いです。
コードを生成させることより、判断の理由を引き出すことに使うと、Antigravity は SwiftUI 開発の学習加速装置としても機能します。