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)
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:
Run the init command:
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:
What happens:
- Auto-detection - Discovers project, scheme, and test target
- Source parsing - Finds
@FreezeSchema(version: "1.0.0") in your code
- Test generation - Creates temporary test file
- Simulator run - Builds and runs in iOS Simulator
- Fixture extraction - Copies SQLite database from simulator
- 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:
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:
-
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 }
}
}
-
Run tests again:
xcodebuild test -scheme MyApp
-
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
-
Revert the change:
@FreezeSchema(version: "1.0.0")
enum AppSchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] {
[User.self] // ✅ Back to original
}
}
-
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:
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:
-
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 ...
}
-
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.
-
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
)
}
-
Develop and test: Run tests (⌘U) as you implement the schema and migration.
-
Freeze the new version:
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"
Problem: Something went wrong during the freeze.
Solution:
- Delete fixtures:
rm -rf FreezeRay/Fixtures/1.0.0/
- Freeze again:
freezeray freeze 1.0.0
- 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