Skip to main content

Your First Freeze

This tutorial walks you through freezing your first SwiftData schema with FreezeRay, from installation to verifying drift detection works. Time required: 10-15 minutes What you’ll learn:
  • Install FreezeRay CLI and package
  • Add @FreezeSchema to your schema
  • Freeze a schema version
  • Run drift detection tests
  • Commit fixtures to git

Prerequisites

Before starting, ensure you have:
  • Xcode 15.0+ installed
  • macOS 14+
  • An iOS 17+ app with SwiftData
  • Git for version control
  • npm (for CLI installation)
  • Your project uses VersionedSchema (not just plain Schema)
If your app doesn’t use VersionedSchema yet, you’ll need to migrate to it first. See Apple’s SwiftData migration documentation.

Step 1: Install FreezeRay CLI

Install the CLI via npm:
npm install -g @trinsicventures/freezeray
Verify installation:
freezeray --version
# Should print: 0.4.3 (or later)
Apple Silicon users: The CLI includes pre-built binaries.Intel Mac users: The CLI will build from source automatically (requires Swift toolchain).

Step 2: Initialize FreezeRay in Your Project

Navigate to your Xcode project directory:
cd /path/to/MyApp
Run the init command:
freezeray init
What this does:
  • Creates FreezeRay/Fixtures/ and FreezeRay/Tests/ directories
  • Adds FreezeRay Swift package to your project
  • Updates your Xcode project or Package.swift
Expected output:
🔹 FreezeRay Init

🔹 Detecting project type...
   Type: Xcode Project

🔹 Creating directory structure...
   ✅ Created FreezeRay/Fixtures/
   ✅ Created FreezeRay/Tests/

🔹 Adding FreezeRay dependency...
   ✅ Added FreezeRay package to Xcode project
   ✅ Added FreezeRay/ folder to project navigator

✅ FreezeRay initialized successfully!

Step 3: Update Your Schema Definition

Open your schema file (e.g., Schemas.swift or DataModel.swift) and import FreezeRay:
import SwiftData
import FreezeRay  // Add this
Add @FreezeSchema to your schema:
@FreezeSchema(version: "1.0.0")  // Add this annotation
enum AppSchemaV1: VersionedSchema {
    static let versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [User.self]
    }

    @Model
    final class User {
        var name: String
        var email: String

        init(name: String, email: String) {
            self.name = name
            self.email = email
        }
    }
}
The @FreezeSchema(version:) string (“1.0.0”) is separate from SwiftData’s versionIdentifier. Use it to organize your fixtures.
If you have multiple schema versions, annotate each:
@FreezeSchema(version: "1.0.0")
enum AppSchemaV1: VersionedSchema {
    // ...
}

@FreezeSchema(version: "2.0.0")
enum AppSchemaV2: VersionedSchema {
    // ...
}

Step 4: Build Your Project

Before freezing, ensure your project builds:
# Open in Xcode
open MyApp.xcodeproj

# Build: ⌘B
# Or from command line:
xcodebuild -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17'
If your project doesn’t build, the freeze command will fail. Fix any build errors before continuing.

Step 5: Freeze Your Schema

Now freeze your schema version:
freezeray freeze 1.0.0
What happens:
  1. Auto-detection - Discovers project, scheme, and test target
  2. Source parsing - Finds @FreezeSchema(version: "1.0.0") in your code
  3. Test generation - Creates temporary test file
  4. Simulator run - Builds and runs in iOS Simulator
  5. Fixture extraction - Copies SQLite database from simulator
  6. Test scaffolding - Generates drift detection test
Expected output:
🔹 FreezeRay v0.4.3
🔹 Freezing schema version: 1.0.0

🔹 Auto-detecting project configuration...
   Found: MyApp.xcodeproj
   Scheme: MyApp (auto-detected)
   Test target: MyAppTests (inferred)

🔹 Parsing source files for @Freeze(version: "1.0.0")...
   Found: AppSchemaV1 in Schemas.swift

🔹 Generating freeze test...
   Generated: FreezeSchemaV1_0_0_Test.swift

🔹 Building and running tests in simulator...
   Simulator: iPhone 17
   Running xcodebuild...
   [Build logs...]
   Test Suite 'FreezeSchemaV1_0_0_Test' passed

🔹 Extracting fixtures from simulator...
   Copied: App-1_0_0.sqlite → FreezeRay/Fixtures/1.0.0/
   Copied: schema-1_0_0.json → FreezeRay/Fixtures/1.0.0/
   Copied: schema-1_0_0.sql → FreezeRay/Fixtures/1.0.0/
   Copied: schema-1_0_0.sha256 → FreezeRay/Fixtures/1.0.0/
   Copied: export_metadata.txt → FreezeRay/Fixtures/1.0.0/

🔹 Scaffolding drift test...
   Created: AppSchemaV1_1_0_0_DriftTests.swift
   ✅ Added test files to MyAppTests target
   ✅ Added FreezeRay/Fixtures to test bundle resources

✅ Schema v1.0.0 frozen successfully!

📝 Next steps:
   1. Review fixtures: FreezeRay/Fixtures/1.0.0
   2. Customize drift test: FreezeRay/Tests/AppSchemaV1_1_0_0_DriftTests.swift
   3. Run tests: xcodebuild test -scheme MyApp
   4. Commit to git: git add FreezeRay/
The freeze process takes 30-60 seconds depending on your project’s build time.

Step 6: Inspect the Fixtures

Check what was created:
tree FreezeRay/
Output:
FreezeRay/
├── Fixtures/
│   └── 1.0.0/
│       ├── App-1_0_0.sqlite         # Real SQLite database
│       ├── schema-1_0_0.json        # Human-readable schema
│       ├── schema-1_0_0.sql         # SQL DDL statements
│       ├── schema-1_0_0.sha256      # Checksum
│       └── export_metadata.txt      # Metadata
└── Tests/
    └── AppSchemaV1_1_0_0_DriftTests.swift  # Drift test

Examine the SQLite database

sqlite3 FreezeRay/Fixtures/1.0.0/App-1_0_0.sqlite ".schema"
You’ll see SwiftData’s internal schema:
CREATE TABLE ZUSER (
    Z_PK INTEGER PRIMARY KEY,
    Z_ENT INTEGER,
    Z_OPT INTEGER,
    ZNAME VARCHAR,
    ZEMAIL VARCHAR
);
CREATE TABLE Z_PRIMARYKEY (...);
CREATE TABLE Z_METADATA (...);
-- ... more SwiftData tables
This is the actual schema SwiftData created - not a theoretical representation.

Check the drift test

Open FreezeRay/Tests/AppSchemaV1_1_0_0_DriftTests.swift:
// AUTO-GENERATED by FreezeRay CLI
// This file is scaffolded once and owned by you. Customize as needed.

import Testing
import FreezeRay
@testable import MyApp

@Test("AppSchemaV1 v1.0.0 has not drifted")
func testAppSchemaV1_1_0_0_Drift() throws {
    // Verify frozen schema matches current definition
    try AppSchemaV1.__freezeray_check_1_0_0()
}
This test runs on every test execution and verifies your schema hasn’t changed.

Step 7: Run Tests

Run your test suite to verify drift detection works:
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17'
Or in Xcode: ⌘U Expected result: All tests pass, including the new drift test.
The drift test should always pass immediately after freezing. If it fails, something went wrong during the freeze process.

Step 8: Verify Drift Detection Works

Let’s test that drift detection catches schema changes:
  1. Modify your frozen schema (temporarily):
    @FreezeSchema(version: "1.0.0")
    enum AppSchemaV1: VersionedSchema {
        static let versionIdentifier = Schema.Version(1, 0, 0)
    
        static var models: [any PersistentModel.Type] {
            [User.self, Post.self]  // ❌ Added Post (drift!)
        }
    
        // ... User definition unchanged ...
    
        @Model
        final class Post {  // New model
            var title: String
            init(title: String) { self.title = title }
        }
    }
    
  2. Run tests again:
    xcodebuild test -scheme MyApp
    
  3. Observe the error:
    ❌ Schema drift detected in sealed version 1.0.0
    
    The sealed schema has changed since it was frozen.
    Sealed schemas are immutable - create a new schema version instead.
    
    Expected checksum: 0cc298858e409d8b9c7e37a7779af0e2
    Actual checksum:   26d70c10e9febf7f8c5a9e3d4b2a1f0e
    
  4. Revert the change:
    @FreezeSchema(version: "1.0.0")
    enum AppSchemaV1: VersionedSchema {
        static var models: [any PersistentModel.Type] {
            [User.self]  // ✅ Back to original
        }
    }
    
  5. Tests pass again:
    xcodebuild test -scheme MyApp
    # ✅ All tests pass
    
Success! Drift detection is working.

Step 9: Commit to Git

Commit the fixtures and tests to version control:
git add FreezeRay/
git add Schemas.swift  # (or wherever you added @FreezeSchema)
git commit -m "Freeze schema v1.0.0"
git push
Always commit fixtures immediately after freezing. They must be in git for:
  • CI/CD to run drift tests
  • Team members to have the same fixtures
  • Traceability (which commit introduced the schema)

Step 10: Team Workflow

When other developers pull your changes:
git pull origin main
They automatically get:
  • The frozen fixtures (FreezeRay/Fixtures/1.0.0/)
  • The drift test (FreezeRay/Tests/AppSchemaV1_1_0_0_DriftTests.swift)
When they run tests (⌘U), drift detection verifies they haven’t accidentally modified the frozen schema.

What’s Next?

You’ve successfully frozen your first schema! Here’s what to do next:

Add a Second Schema Version

When you need to make schema changes:
  1. Create a new schema version:
    @FreezeSchema(version: "2.0.0")
    enum AppSchemaV2: VersionedSchema {
        static let versionIdentifier = Schema.Version(2, 0, 0)
    
        static var models: [any PersistentModel.Type] {
            [User.self, Post.self]  // Now includes Post
        }
    
        // ... models ...
    }
    
  2. Generate migration test:
    freezeray generate migration-tests --from-schema 1.0.0 --to-schema 2.0.0
    
    This creates MigrateV1_0_0toV2_0_0_Tests.swift. Edit it to add custom data validation.
  3. Add migration:
    enum AppMigrations: SchemaMigrationPlan {
        static var schemas: [any VersionedSchema.Type] {
            [AppSchemaV1.self, AppSchemaV2.self]
        }
    
        static var stages: [MigrationStage] {
            [migrateV1toV2]
        }
    
        static let migrateV1toV2 = MigrationStage.lightweight(
            fromVersion: AppSchemaV1.self,
            toVersion: AppSchemaV2.self
        )
    }
    
  4. Develop and test: Run tests (⌘U) as you implement the schema and migration.
  5. Freeze the new version:
    freezeray freeze 2.0.0
    
    This generates fixtures and drift test only. The migration test already exists from step 2.

Set Up CI Integration

Add FreezeRay to your CI pipeline to catch drift on every PR:
# .github/workflows/test.yml
- name: Run Tests
  run: |
    xcodebuild test \
      -scheme MyApp \
      -destination 'platform=iOS Simulator,name=iPhone 17'
See the CI Integration guide for full setup.

Customize Tests

Add custom validation to drift and migration tests:
@Test("AppSchemaV1 v1.0.0 has not drifted")
func testAppSchemaV1_1_0_0_Drift() throws {
    try AppSchemaV1.__freezeray_check_1_0_0()

    // ✅ Add custom checks
    // For example: verify certain models exist
}
See the Testing Migrations guide for examples.

Troubleshooting

”No @Freeze(version: “1.0.0”) annotation found”

Problem: You forgot to add @FreezeSchema to your schema. Solution:
@FreezeSchema(version: "1.0.0")  // ← Add this
enum AppSchemaV1: VersionedSchema {
    // ...
}

“Build failed”

Problem: Your project doesn’t compile. Solution: Fix build errors in Xcode (⌘B) before running freezeray freeze.

”Test failed”

Problem: Your existing tests are failing. Solution: Fix failing tests before freezing. The freeze command requires all tests to pass.

”Simulator not found: iPhone 17”

Problem: You don’t have iPhone 17 simulator installed. Solution: Option 1 - Install iPhone 17 simulator:
xcrun simctl list devices
# If iPhone 17 is not listed, install it via Xcode settings
Option 2 - Use a different simulator:
freezeray freeze 1.0.0 --simulator "iPhone 15"

Drift test fails immediately after freezing

Problem: Something went wrong during the freeze. Solution:
  1. Delete fixtures: rm -rf FreezeRay/Fixtures/1.0.0/
  2. Freeze again: freezeray freeze 1.0.0
  3. If it still fails, file a bug report at github.com/TrinsicVentures/FreezeRayCLI/issues

Summary

You’ve learned how to:
  • ✅ Install FreezeRay CLI and initialize your project
  • ✅ Annotate schemas with @FreezeSchema(version:)
  • ✅ Freeze a schema version to create immutable fixtures
  • ✅ Run drift detection tests
  • ✅ Commit fixtures to version control
Your schema is now frozen and protected from accidental changes!

Next Steps