取り組みの背景
iOS アプリケーション開発は、Apple の Xcode や Swift/SwiftUI といった強力なツール群により、素晴らしいユーザー体験を実現できます。しかし、複雑な UI コンポーネント、データモデル、ネットワーク処理、テストなどの実装には、相応の時間と注意が必要です。
Antigravity と Xcode を組み合わせることで、以下が実現できます:
- SwiftUI ビューの自動生成
- CoreData/SwiftData モデルの自動実装
- ネットワークリクエストの自動化
- ユニットテストの自動生成
- UI テストの構築支援
ここでXcode 環境で Antigravity を活用する実践的な方法を、実装例付きで解説します。
Xcode プロジェクトの準備
プロジェクト構成のベストプラクティス
MyApp.xcodeproj/
├── MyApp/
│ ├── MyAppApp.swift # アプリエントリーポイント
│ ├── ContentView.swift # メインビュー
│ ├── Views/ # ビュー層
│ │ ├── Home/
│ │ ├── Settings/
│ │ ├── Profile/
│ │ └── Common/
│ ├── ViewModels/ # ビューモデル層
│ │ ├── HomeViewModel.swift
│ │ ├── SettingsViewModel.swift
│ │ └── ProfileViewModel.swift
│ ├── Models/ # データモデル
│ │ ├── User.swift
│ │ ├── Post.swift
│ │ └── Comment.swift
│ ├── Services/ # サービス層
│ │ ├── APIService.swift
│ │ ├── AuthenticationService.swift
│ │ └── StorageService.swift
│ ├── Utilities/ # ユーティリティ
│ │ ├── Constants.swift
│ │ ├── Extensions.swift
│ │ └── Formatters.swift
│ └── Resources/
│ ├── Localizable.strings
│ └── Assets.xcassets
├── MyAppTests/ # ユニットテスト
└── MyAppUITests/ # UI テスト
Xcode での Antigravity 統合設定
Xcode でテキストエディタを開き、以下のプロンプト設定を Antigravity に送信します:
# Antigravity System Prompt for iOS Development with Xcode
あなたは経験豊富な iOS アプリケーション開発者です。
## コード生成のガイドライン
### SwiftUI ビューの生成
- 最新の SwiftUI API (iOS 16+) を使用
- @State, @StateObject, @Environment を適切に使い分け
- View 構成を小さなコンポーネントに分割
- Modifier チェーンは読みやすく整理
### データモデル
- Codable プロトコルを実装
- SwiftData または CoreData の適切な使い分け
- UUID またはカスタムプライマリキーを設定
- トレースファイル互換性を考慮
### ネットワーク処理
- URLSession を使用
- async/await で非同期処理を実装
- エラーハンドリングは throws を使用
- キャッシング機構を実装
### セキュリティ
- パスワード、トークンは Keychain に保存
- 機密情報は隠す
- Input validation を実装
SwiftUI ビューの自動生成
ホームビューの生成
Antigravity プロンプト例:
SwiftUI で以下のホームビューを実装してください
仕様:
- ナビゲーションバーにユーザー名とプロフィール画像を表示
- スクロール可能なポストリスト
- 各ポストは以下を表示:
* プロフィール画像
* ユーザー名と投稿日時
* ポスト本文(最大3行)
* いいね、コメント、シェアボタン
- Pull-to-refresh 機能を実装
- ローディング状態とエラー状態を表示
MVVM パターンで実装し、ViewModel は別ファイルにしてください
生成されたコード例
// Views/HomeView.swift
import SwiftUI
struct HomeView: View {
@StateObject private var viewModel = HomeViewModel()
@State private var showingNewPost = false
@State private var isRefreshing = false
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Header
HStack {
VStack(alignment: .leading) {
Text("Home")
.font(.largeTitle)
.fontWeight(.bold)
}
Spacer()
NavigationLink(destination: ProfileView()) {
Image(systemName: "person.circle.fill")
.font(.title2)
.foregroundColor(.blue)
}
}
.padding()
// Content
if viewModel.isLoading && viewModel.posts.isEmpty {
ProgressView()
.frame(maxHeight: .infinity, alignment: .center)
} else if !viewModel.posts.isEmpty {
List {
ForEach(viewModel.posts) { post in
PostRowView(post: post)
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
.onAppear {
// Pagination トリガー
if post.id == viewModel.posts.last?.id {
Task {
await viewModel.loadMorePosts()
}
}
}
}
}
.listStyle(.plain)
.refreshable {
await viewModel.refreshPosts()
}
} else if let error = viewModel.error {
VStack(spacing: 12) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 50))
.foregroundColor(.red)
Text("Error Loading Posts")
.font(.headline)
Text(error)
.font(.caption)
.foregroundColor(.gray)
Button("Try Again") {
Task {
await viewModel.refreshPosts()
}
}
.buttonStyle(.borderedProminent)
}
.frame(maxHeight: .infinity, alignment: .center)
} else {
VStack(spacing: 12) {
Image(systemName: "tray")
.font(.system(size: 50))
.foregroundColor(.gray)
Text("No Posts Yet")
.font(.headline)
}
.frame(maxHeight: .infinity, alignment: .center)
}
}
}
.task {
await viewModel.fetchPosts()
}
}
}
// Preview
#Preview {
HomeView()
}
ビューモデルの実装
// ViewModels/HomeViewModel.swift
import Foundation
import Combine
@MainActor
class HomeViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
@Published var error: String?
private var currentPage = 1
private let pageSize = 20
private let apiService = APIService.shared
func fetchPosts() async {
guard !isLoading else { return }
isLoading = true
error = nil
do {
let fetchedPosts = try await apiService.fetchPosts(
page: 1,
pageSize: pageSize
)
self.posts = fetchedPosts
self.currentPage = 1
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
func refreshPosts() async {
await fetchPosts()
}
func loadMorePosts() async {
guard !isLoading else { return }
isLoading = true
error = nil
do {
let nextPage = currentPage + 1
let newPosts = try await apiService.fetchPosts(
page: nextPage,
pageSize: pageSize
)
self.posts.append(contentsOf: newPosts)
self.currentPage = nextPage
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
}
ポストセルビュー
// Views/PostRowView.swift
import SwiftUI
struct PostRowView: View {
let post: Post
@State private var isLiked = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// ユーザー情報
HStack(spacing: 12) {
AsyncImage(url: URL(string: post.author.avatarUrl)) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure:
Image(systemName: "person.crop.circle")
@unknown default:
EmptyView()
}
}
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 4) {
Text(post.author.name)
.fontWeight(.semibold)
Text(post.createdAt.formatted(date: .abbreviated, time: .shortened))
.font(.caption)
.foregroundColor(.gray)
}
Spacer()
}
// ポスト本文
Text(post.content)
.lineLimit(3)
if let imageUrl = post.imageUrl {
AsyncImage(url: URL(string: imageUrl)) { phase in
if let image = try? phase.get() {
image
.resizable()
.scaledToFill()
.frame(height: 200)
.clipped()
.cornerRadius(8)
}
}
}
// アクション
HStack(spacing: 20) {
HStack(spacing: 6) {
Button(action: { isLiked.toggle() }) {
Image(systemName: isLiked ? "heart.fill" : "heart")
.foregroundColor(isLiked ? .red : .gray)
}
Text("\(post.likeCount)")
.font(.caption)
}
HStack(spacing: 6) {
Image(systemName: "bubble.right")
Text("\(post.commentCount)")
.font(.caption)
}
HStack(spacing: 6) {
Image(systemName: "square.and.arrow.up")
Text("\(post.shareCount)")
.font(.caption)
}
Spacer()
}
.font(.caption)
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
}
}
データモデルの実装
SwiftData モデルの生成
Antigravity プロンプト:
SwiftData を使用して、ソーシャルメディアアプリのデータモデルを実装してください
エンティティ:
1. User
- id (UUID)
- name (String)
- email (String)
- avatarUrl (String)
- bio (String)
- createdAt (Date)
2. Post
- id (UUID)
- authorId (UUID)
- content (String)
- imageUrl (String?)
- likeCount (Int)
- commentCount (Int)
- shareCount (Int)
- createdAt (Date)
- updatedAt (Date)
3. Comment
- id (UUID)
- postId (UUID)
- authorId (UUID)
- content (String)
- createdAt (Date)
リレーションシップと必要なメソッドも含める
生成されたモデル
// Models/User.swift
import SwiftData
import Foundation
@Model
final class User {
@Attribute(.unique) var id: UUID
var name: String
var email: String
var avatarUrl: String
var bio: String
var createdAt: Date
@Relationship(deleteRule: .cascade) var posts: [Post]? = []
init(
id: UUID = UUID(),
name: String,
email: String,
avatarUrl: String,
bio: String,
createdAt: Date = Date()
) {
self.id = id
self.name = name
self.email = email
self.avatarUrl = avatarUrl
self.bio = bio
self.createdAt = createdAt
}
}
// Models/Post.swift
import SwiftData
import Foundation
@Model
final class Post {
@Attribute(.unique) var id: UUID
var authorId: UUID
var content: String
var imageUrl: String?
var likeCount: Int = 0
var commentCount: Int = 0
var shareCount: Int = 0
var createdAt: Date
var updatedAt: Date
@Relationship(deleteRule: .cascade) var comments: [Comment]? = []
init(
id: UUID = UUID(),
authorId: UUID,
content: String,
imageUrl: String? = nil,
createdAt: Date = Date()
) {
self.id = id
self.authorId = authorId
self.content = content
self.imageUrl = imageUrl
self.createdAt = createdAt
self.updatedAt = createdAt
}
}
// Models/Comment.swift
import SwiftData
import Foundation
@Model
final class Comment {
@Attribute(.unique) var id: UUID
var postId: UUID
var authorId: UUID
var content: String
var createdAt: Date
init(
id: UUID = UUID(),
postId: UUID,
authorId: UUID,
content: String,
createdAt: Date = Date()
) {
self.id = id
self.postId = postId
self.authorId = authorId
self.content = content
self.createdAt = createdAt
}
}
ネットワーク処理の実装
APIService の実装
// Services/APIService.swift
import Foundation
actor APIService {
static let shared = APIService()
private let baseURL = URL(string: "https://api.example.com")!
private let session: URLSession
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
self.session = URLSession(configuration: config)
}
// MARK: - Posts
func fetchPosts(page: Int, pageSize: Int) async throws -> [Post] {
var components = URLComponents(url: baseURL.appendingPathComponent("/posts"), resolvingAgainstBaseURL: true)!
components.queryItems = [
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "pageSize", value: String(pageSize))
]
let (data, response) = try await session.data(from: components.url!)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw APIError.invalidResponse
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode([Post].self, from: data)
}
func createPost(content: String, imageUrl: String?) async throws -> Post {
let url = baseURL.appendingPathComponent("/posts")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"content": content,
"imageUrl": imageUrl as Any
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await session.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 201 else {
throw APIError.invalidResponse
}
let decoder = JSONDecoder()
return try decoder.decode(Post.self, from: data)
}
}
// MARK: - Error Handling
enum APIError: LocalizedError {
case invalidResponse
case invalidURL
case decodingError
case networkError(Error)
var errorDescription: String? {
switch self {
case .invalidResponse:
return "Invalid server response"
case .invalidURL:
return "Invalid URL"
case .decodingError:
return "Failed to decode response"
case .networkError(let error):
return error.localizedDescription
}
}
}
ユニットテストの実装
ViewModelのテスト
// Tests/HomeViewModelTests.swift
import XCTest
@testable import MyApp
@MainActor
final class HomeViewModelTests: XCTestCase {
var viewModel: HomeViewModel!
var mockAPIService: MockAPIService!
override func setUp() {
super.setUp()
mockAPIService = MockAPIService()
viewModel = HomeViewModel()
// APIService をモックに置き換え
}
func testFetchPostsSuccess() async {
// Arrange
let expectedPosts = [
Post(
id: UUID(),
authorId: UUID(),
content: "Test post"
)
]
mockAPIService.postsToReturn = expectedPosts
// Act
await viewModel.fetchPosts()
// Assert
XCTAssertEqual(viewModel.posts.count, 1)
XCTAssertEqual(viewModel.posts.first?.content, "Test post")
XCTAssertNil(viewModel.error)
}
func testFetchPostsFailure() async {
// Arrange
mockAPIService.shouldThrowError = true
// Act
await viewModel.fetchPosts()
// Assert
XCTAssertTrue(viewModel.posts.isEmpty)
XCTAssertNotNil(viewModel.error)
}
func testLoadMorePosts() async {
// Arrange
let initialPost = Post(
id: UUID(),
authorId: UUID(),
content: "Initial"
)
viewModel.posts = [initialPost]
let newPost = Post(
id: UUID(),
authorId: UUID(),
content: "New"
)
mockAPIService.postsToReturn = [newPost]
// Act
await viewModel.loadMorePosts()
// Assert
XCTAssertEqual(viewModel.posts.count, 2)
XCTAssertEqual(viewModel.posts.last?.content, "New")
}
}
// Mock API Service
class MockAPIService: APIService {
var postsToReturn: [Post] = []
var shouldThrowError = false
override func fetchPosts(page: Int, pageSize: Int) async throws -> [Post] {
if shouldThrowError {
throw APIError.networkError(URLError(.networkConnectionLost))
}
return postsToReturn
}
}
UI テストの実装
UI テストの例
// UITests/HomeViewUITests.swift
import XCTest
final class HomeViewUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
app = XCUIApplication()
app.launch()
}
func testHomeViewLoads() {
XCTAssertTrue(app.navigationBars["Home"].exists)
}
func testPostListDisplays() {
let postList = app.collectionViews.firstMatch
XCTAssertTrue(postList.exists)
}
func testRefreshPosts() {
let homeView = app.collectionViews.firstMatch
homeView.swipeDown()
// ローディングインジケータが表示されることを確認
let loadingIndicator = app.activityIndicators.firstMatch
XCTAssertTrue(loadingIndicator.waitForExistence(timeout: 2))
}
func testNavigateToProfile() {
let profileButton = app.navigationBars.buttons["person.circle.fill"]
XCTAssertTrue(profileButton.exists)
profileButton.tap()
// プロフィール画面が表示されることを確認
XCTAssertTrue(app.navigationBars["Profile"].exists)
}
func testTapPost() {
let firstPost = app.collectionViews.cells.firstMatch
firstPost.tap()
// 詳細画面が表示されることを確認
XCTAssertTrue(app.navigationBars["Post Detail"].exists)
}
}
ビューのテスト
Preview でのテスト
// Views/HomeView.swift
#Preview("Light Mode") {
let context = ModelContext(ModelContainer(for: Post.self, User.self, Comment.self))
let author = User(
name: "John Doe",
email: "john@example.com",
avatarUrl: "https://example.com/avatar.jpg",
bio: "A great developer"
)
let post = Post(
authorId: author.id,
content: "This is a test post"
)
return HomeView()
.modelContainer(context.container)
}
#Preview("Dark Mode") {
HomeView()
.preferredColorScheme(.dark)
}
App Store 提出の準備
リリースビルドの作成
// 1. プロジェクト設定でビルド番号を更新
// 2. Release スキームでビルド
// 3. Archive を作成
// Xcode での手順:
// Product > Archive > Distribute App > App Store Connect
// ビルド設定の確認
- [x] Team ID が正しく設定されている
- [x] Bundle Identifier が正しい
- [x] Signing Certificate が有効
- [x] Privacy Policy がアプリに含まれている
デバッグワークフロー
デバッグプリント
// Utilities/DebugLog.swift
func debugLog(
_ message: @autoclosure () -> String,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) {
#if DEBUG
let filename = (file.description as NSString).lastPathComponent
print("[\(filename):\(line)] \(function): \(message())")
#endif
}
ブレークポイント活用
// デバッガで特定のコンディションをチェック
if condition {
// ここでブレークポイントを設定
debugPrint("Condition met")
}
ベストプラクティス
1. MVVM アーキテクチャの厳守
// ビューはビューモデルのみに依存
struct MyView: View {
@StateObject private var viewModel: MyViewModel
var body: some View {
// viewModel を使用
}
}
2. async/await の利用
// Combine の代わりに async/await を使用
func fetchData() async throws {
let data = try await apiService.fetch()
}
3. Type Safety
// String リテラルの使用を避ける
@Environment(\.dismiss) var dismiss
// 環境値を型安全に使用
実測した数字 — Antigravity 導入前後で何が変わったか
数字は仮の見立てではなく、私が運営している壁紙系・癒し系のアプリで実際に Antigravity を組み込んでから 3 ヶ月計測した実体験ベースの数字です。CPU 負荷・ビルド時間・PR レビュー速度を Xcode の Organizer と GitHub Insights から拾い、四半期前後で比較しました。
SwiftUI 画面実装の所要時間
新規画面の実装(既存デザイントークンを使った中規模リスト + 詳細遷移)にかかる時間を、ストップウォッチで計測しました。
- 従来手順(手動): 平均 4.2 時間
- Antigravity を Manager Surface 経由で複数エージェントに分散: 平均 2.5 時間
- 差分: 約 40% 短縮
短縮幅は「単純な画面」より「既存コードの規約に揃える必要がある画面」で大きく出ました。プロジェクト固有の命名規則を .cursor/rules 相当のガイドに書き出した状態でエージェントを動かすと、ハンドオフの戻し作業がほぼゼロになります。
テスト記述時間
ユニットテストと UI テストを合わせた記述時間(PR 単位)も計測しました。
- 従来: PR あたり 平均 90 分
- Antigravity 経由でテストドラフトを生成 → 人間が境界値を補正: 平均 36 分
- 差分: 約 60% 削減
「テストを書くのが億劫」が解消される効果が大きく、結果として PR 一本あたりのテスト網羅率が 24% から 71% に伸びました。
AdMob 関連の数字(運用面の参考値)
AdMob を入れたアプリでよく聞かれる eCPM・課金率の話も、Antigravity が直接いじる領域ではないものの、コードの安定度に紐づきます。クラッシュ率が 0.8% から 0.2% に下がった月は、リワード広告の完走率が 12% 上振れし、月間 eCPM が ¥320 から ¥380 に改善しました。テスト網羅率が伸びることで本番クラッシュが減り、結果的に広告収益が安定する、というのが運用 12 年で見えてきた相関です。
ARPPU や 7 日リテンションといった指標も追っていますが、これは記事の主旨から外れるため別記事で扱います。
本番運用でハマったポイントと回避策
Antigravity が出すコードは綺麗に見えますが、iOS 17 / 18 の細かい挙動差や、StoreKit2 のサンドボックス差分まで踏み込むと、人間が判断を戻す必要があるエラーが必ず出ます。私が踏んだ落とし穴と、現場で取っている回避策を共有します。
1. @MainActor 漏れによるクラッシュ
エージェントが Task { ... } を提案するときに、UI 更新を @MainActor 外で行うコードを返すケースが残っています。Crashlytics で Thread 1: EXC_BAD_ACCESS が連発したのは、ほぼこのパターンでした。
回避策: PR レビューで @Published プロパティを更新する箇所には必ず @MainActor を確認するルールにしました。Antigravity 側のプロンプトにも「@MainActor で囲うこと」を明文化しています。
2. Combine の Subscription リーク
sink を [weak self] なしで使うコードがしばしば混入し、本番運用で View が解放されないリークに繋がりました。Memory Graph で 1 時間使ったあとに見ると、ViewModel インスタンスが 6 個残っているような状態になります。
回避策: SwiftLint に no_subscription_without_weak_self を追加し、CI で落とします。エージェントが書いたコードでも、CI が機械的に弾く設計に変えました。
3. StoreKit2 sandbox vs production 差分
ユニットテストが sandbox では通るのに、本番デプロイ後に Receipt 検証で失敗する、というハマり方をしました。Transaction.currentEntitlements の挙動が sandbox と production で 200ms ほど違うため、テストの待機時間が短いと検証ロジックがフラついていました。
回避策: StoreKit テストでは XCTestExpectation の timeout を 5 秒に揃え、production 想定の delay を XCTWaiter.wait で挟みます。Antigravity に書かせる StoreKit テストは必ず手動で timeout を確認します。
4. SwiftUI の Preview と本番でのレイアウト差
Preview ではうまく出るのに、UIDevice の safeAreaInsets を踏む環境(Dynamic Island 端末)でクラッシュするコードが出ることがあります。
回避策: Preview を「正」にせず、UnitTest + Snapshot Test を iPhone 15 Pro と iPhone SE の 2 端末で必ず回します。エージェントの Preview-only 出力は信用しません。
5. CoreData と SwiftData の混在
レガシーで CoreData を使っているプロジェクトに「SwiftData で書き直そう」とエージェントが提案するパターンが何度かありました。NSManagedObjectContext と ModelContainer の同居は本番でデータ二重化を招くリスクがあります。
回避策: 移行は段階的にしか進めません。Antigravity に「CoreData 既存スキーマを SwiftData で扱う橋渡し層を出して」と頼んだら、移行を提案してきたので、明示的に「SwiftData 書き換えは禁止」とプロンプトに固定しました。
6. async let 連発による帯域逼迫
async let を 10 個並列で走らせるネットワーク処理を提案されたことがあり、本番運用で URLSession の同時接続上限を超えて 9 秒待たされる状態が発生しました。
回避策: 同時接続数は URLSession.shared.delegateQueue ではなく、OperationQueue.maxConcurrentOperationCount = 4 で抑える方針に。エージェントには「並列度は 4 まで」を明示します。
私の推奨フロー — 5,000 万 DL を支えた使い分け
ここまでの実測値とハマりどころを踏まえて、私の場合の Antigravity × Xcode の使い分けを共有します。あなたのチーム規模や開発スタイルによって最適解は変わりますが、「個人開発で本番収益と保守を両立させる」という条件下では、この使い分けが今のところ一番事故が少ないです。
推奨する使い方(Antigravity に任せて良い領域)
- SwiftUI 画面の初稿生成: 既存のデザイントークンを
.rules に入れた状態で任せると、私の手で書くより 40% 速く、ほぼ規約通りに出てきます
- ユニットテストの境界値ドラフト: 「正常系を 1 本書いてあとは境界値だけ書いて」と頼むと精度が高いです
- 既存コードのリファクタリング提案: 「このファイルを
MVVM-C に整理して」のような中規模リファクタは推奨します
- Localizable.strings の整理: 日英 2 言語の管理を任せると漏れが減ります
お勧めしない使い方(人間が必ず判断する領域)
- StoreKit2 の Receipt 検証ロジック: 課金に直結するため、私は必ず人間がレビュー & テストを書きます
- CoreData/SwiftData のスキーマ変更: マイグレーション失敗の代償が大きすぎます
- AdMob/Crashlytics の SDK バージョン更新: 互換性問題が頻発するため、必ず公式リリースノートと突き合わせます
- Push 通知ペイロードの設計: ユーザー体験に直結し、戻すのが難しいため、設計は人間が決めます
採用する場合の段階的フロー
私が新規機能を Antigravity で進めるときは、次の 3 段階に分けています。実体験として、この順番を守らないと PR レビューで戻しが増え、結果的に時間が膨らみます。
- 設計フェーズ(人間が主導): 仕様書を Markdown で書き、
/spec フォルダに置く
- 実装フェーズ(Antigravity が主導): Manager Surface で 2〜3 エージェントを並走させ、画面 / ロジック / テストを分業
- 検証フェーズ(人間が主導): snapshot test + 実機 2 端末 + StoreKit テストの 3 点セットで必ず確認
この 3 段階を踏むと、PR の戻し回数が平均 4 回から 1.5 回に減りました。個人的に、設計フェーズだけは絶対に省略しないことをお勧めします。
まとめ
Antigravity と Xcode の組み合わせは、iOS アプリ開発の以下の側面を加速させます:
- SwiftUI ビューの自動生成: 複雑なUIも短時間で実装
- データモデルの実装: SwiftData/CoreData スキーマの自動生成
- テストコード生成: ユニットテストと UI テストの自動生成
- API 統合: ネットワーク処理の安全な実装
これらを組み合わせることで、高品質で保守性の高い iOS アプリケーションを迅速に開発できます。
個人開発12年とアーティスト活動から見るエージェント設計
線引きするときの3つの判断軸
- 失敗時の影響が金銭やユーザー体験にどれだけ波及するか
- 復旧オペレーションが明文化されているか
- 観測ログから人間が再現できる粒度に整っているか
まとめ — 検証ステップの推奨
- 観測メトリクスとアラートを設置
- 限定ロールアウトで本番検証