ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-04-16Intermediate

Where Antigravity Actually Helps in SwiftUI Development — and Where It Doesn't

Practical patterns for integrating Antigravity into SwiftUI development — UI prototyping, SwiftData model design, and test generation. Actionable techniques for indie iOS developers.

swiftui2ios36app-dev49antigravity435xcode4swiftdata

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.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-04-05
How to Auto-Generate and Manage iOS Privacy Manifests with Antigravity
Auto-generate iOS Privacy Manifests with Antigravity. Scan Required Reason APIs, create PrivacyInfo.xcprivacy, and pass App Store review — step by step.
App Dev2026-07-03
Catching Download Size Regressions Before Submission Day — A Weekly Agent Gate for AAB/IPA Size Budgets
Mediation SDKs and bundled assets quietly inflate download size. A design for size ledgers, budget gates, and agent-driven delta attribution using bundletool and App Thinning reports.
App Dev2026-06-28
Adding Mediation Partners Quietly Starved My iOS Attribution — Reconciling SKAdNetwork IDs Across Four Apps
I added mediation partners but iOS revenue barely moved — the cause was missing SKAdNetwork IDs in Info.plist. Here is how I reconciled SKAdNetworkItems across four apps, using an Antigravity agent as the matcher while keeping the revenue decisions by hand.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →