Building a SwiftUI app means making a constant stream of small decisions: .animation vs withAnimation, which deleteRule to use in SwiftData, whether to reach for @Observable or stick with ObservableObject. These decisions are individually minor, but they accumulate into a surprising amount of cognitive load by the end of the day.
Since integrating Antigravity into my SwiftUI workflow, that decision overhead has noticeably decreased. But not every problem benefits equally — there are situations where it genuinely helps, and situations where the results are mediocre. This article covers the specific patterns that work, from the perspective of an indie iOS developer.
Start with Constraints in Your Project Settings
Antigravity's output quality depends heavily on how well it understands your project context. The first thing worth doing in any new iOS project is writing your project constraints into .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"
]
}Two lines matter most here: "prefer @Observable macro" and "do not use deprecated APIs." Without them, you'll occasionally get older ObservableObject patterns or APIs that were deprecated in iOS 17. With them, you stop needing to repeat those instructions on every request.
For teams or multi-project setups, Antigravity Custom Rules & Project Configuration Mastery covers how to standardize these settings across projects and team members.
UI Prototyping: Lead with Constraints, Not Appearance
The place where Antigravity helps most in SwiftUI is when you're unsure which modifier combination is correct. Describing the final visual appearance tends to produce generic code. Describing constraints and goals produces code you can actually reason about.
// Prompt:
// "Card-style list item with these requirements:
// - Tap triggers spring-feel scale animation
// - SF Symbols icon + two-line text (title / subtitle)
// - Dark mode aware (using .secondary label color)
// - Receives data from an @Observable model (iOS 17+)"
struct ItemCardView: View {
let item: Item // @Observable Item model
@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
}
}
}
}The real value isn't the code itself — it's reading it to understand why .spring(duration: 0.2) was chosen over a linear animation, or why isPressed is @State rather than computed. Generated code that you understand becomes a template you can adapt. Generated code you don't understand becomes technical debt.
SwiftData Models: Preventing Relationship Mistakes
SwiftData @Relationship design mistakes often require schema migrations — a genuine problem once users have real data on their devices. Describing relationships in plain language to Antigravity consistently produces code with the right deleteRule settings the first time.
// Prompt: "Task management app. Deleting a project should also delete its tasks (cascade).
// Tags can belong to multiple tasks (many-to-many). Task also needs Codable conformance."
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
}
}The key detail here is having both .cascade (delete tasks with their project) and .nullify (preserve tasks when a tag is deleted) appear correctly in the same output. Getting this wrong means discovering the issue after users have lost data.
Test Generation: Build and Test at the Same Time
Unit tests in SwiftUI projects tend to be "I'll write them later" items that never get written. With Antigravity, selecting an @Observable model and asking for tests produces working XCTest code immediately.
The practical insight is to test business logic, not View layout. Testing @Observable model methods is realistic, maintainable, and actually tells you something useful.
// Target: TaskFilter model's filtering logic
@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-generated test code
final class TaskFilterTests: XCTestCase {
var filter: TaskFilter\!
var pendingTask: Task\!
var completedTask: Task\!
override func setUp() {
filter = TaskFilter()
pendingTask = Task(title: "Pending task")
completedTask = Task(title: "Completed task")
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: "Work", colorHex: "#3B82F6")
pendingTask.tags = [tag]
filter.selectedTag = tag
let result = filter.filter([pendingTask, completedTask])
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result.first?.title, "Pending task")
}
}When tests accumulate feature by feature, refactoring stops feeling risky. For a broader look at test generation strategies, Antigravity AI Test Code Auto-Generation covers unit, integration, and E2E approaches in detail.
Where Antigravity Falls Short
Two patterns where I've seen accuracy drop in SwiftUI projects:
Large files as context
Feeding a 300-line View file into Antigravity as context noticeably reduces response quality. Keeping Views split into focused components under ~200 lines each improves both AI accuracy and SwiftUI architecture in general.
#Preview compatibility
Views using @Environment or @Query often produce #Preview blocks that don't actually compile. Adding "also write the Preview code" to your prompt usually gets Antigravity to include an in-memory ModelContainer setup. Without that prompt, it tends to produce previews that work in simple cases but fail with SwiftData.
For a complete Xcode integration setup, Xcode × Antigravity iOS Dev Guide walks through the environment configuration in detail.
One Thing to Try Today
Pick a View in your current project where you're unsure if your approach is right. Instead of asking "how should I do this?", try: "I wrote it this way — where does this diverge from iOS 17 best practices?" That framing consistently produces more actionable feedback, because Antigravity has something specific to evaluate rather than an open-ended prompt to fill.
Using Antigravity to evaluate your code, rather than just generate it, tends to be where the real learning happens.