Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

SwiftUI Mastery: Automating Preview Data

This technical guide provides an in-depth analysis of the json to swiftui preview engine, best practices for implementation, and data security standards.

JSON to SwiftUI Previews: #Preview Macro, Multi-State Fixtures, and Codable Loading

SwiftUI Previews let you see UI changes instantly in Xcode without running the simulator. But previews that use hardcoded strings like User(name: "Test User", bio: "Lorem ipsum") diverge from real API shapes within days. Loading JSON fixtures — actual or representative API responses — makes previews realistic: they catch layout issues from long strings, missing fields, and nested structures that minimal hardcoded mocks miss. Swift 5.9 (iOS 17+) replaced PreviewProvider with the #Preview macro, which is simpler and supports closures directly.

Live Example: JSON Fixture Loading with #Preview

// Models/User.swift — Codable struct matching your API
struct User: Codable, Identifiable {
    let id:            String
    let username:      String
    let bio:           String?
    let followerCount: Int
    let isVerified:    Bool
    let joinedAt:      Date

    enum CodingKeys: String, CodingKey {
        case id, username, bio
        case followerCount = "follower_count"
        case isVerified    = "is_verified"
        case joinedAt      = "joined_at"
    }
}

// Preview/Fixtures/user-success.json
{
  "id": "usr_001",
  "username": "alice_dev",
  "bio": "iOS engineer. Building things with SwiftUI and Combine. Open source contributor.",
  "follower_count": 1250,
  "is_verified": true,
  "joined_at": "2022-03-15T08:00:00Z"
}

// Preview/Fixtures/user-minimal.json
{
  "id": "usr_002",
  "username": "bob",
  "follower_count": 3,
  "is_verified": false,
  "joined_at": "2024-11-01T08:00:00Z"
}

// Utilities/JSONFixture.swift (DEBUG only)
#if DEBUG
struct JSONFixture {
    static func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy  = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .iso8601

        guard let url  = Bundle.main.url(forResource: filename, withExtension: "json"),
              let data = try? Data(contentsOf: url),
              let result = try? decoder.decode(T.self, from: data)
        else {
            fatalError("Failed to load fixture: \(filename).json")
        }
        return result
    }
}
#endif

// Views/UserProfileView.swift
struct UserProfileView: View {
    let user: User

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Image(systemName: user.isVerified ? "checkmark.seal.fill" : "person.circle")
                    .foregroundStyle(user.isVerified ? .blue : .gray)
                Text(user.username).font(.headline)
            }
            if let bio = user.bio {
                Text(bio).font(.subheadline).foregroundStyle(.secondary)
            }
            Label("\(user.followerCount) followers", systemImage: "person.2")
                .font(.caption)
        }
        .padding()
    }
}

// Previews using the new #Preview macro (iOS 17 / Xcode 15+)
#Preview("Success State") {
    UserProfileView(user: JSONFixture.load("user-success"))
}

#Preview("Minimal (No Bio)") {
    UserProfileView(user: JSONFixture.load("user-minimal"))
}

#Preview("Dark Mode") {
    UserProfileView(user: JSONFixture.load("user-success"))
        .preferredColorScheme(.dark)
}

#Preview("Localization — German") {
    UserProfileView(user: JSONFixture.load("user-success"))
        .environment(\.locale, Locale(identifier: "de_DE"))
}

PreviewProvider Pattern (iOS 16 and Earlier)

// For projects supporting iOS 16 or earlier, use PreviewProvider
struct UserProfileView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            UserProfileView(user: JSONFixture.load("user-success"))
                .previewDisplayName("Success")

            UserProfileView(user: JSONFixture.load("user-minimal"))
                .previewDisplayName("Minimal")

            UserProfileView(user: JSONFixture.load("user-success"))
                .preferredColorScheme(.dark)
                .previewDisplayName("Dark Mode")
        }
        .previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro"))
        .previewLayout(.sizeThatFits)    // size to content, not full screen
    }
}

Multiple State Previews from JSON Array

// Preview fixtures for all UI states
// Fixtures/feed-states.json
{
  "states": [
    {
      "id": "loading",
      "items": [],
      "isLoading": true
    },
    {
      "id": "empty",
      "items": [],
      "isLoading": false
    },
    {
      "id": "success",
      "items": [
        { "id": "p1", "title": "Widget Pro", "price": 49.99 },
        { "id": "p2", "title": "Cable USB-C", "price": 12.99 }
      ],
      "isLoading": false
    }
  ]
}

struct FeedState: Codable {
    let id:        String
    let items:     [Product]
    let isLoading: Bool
}

// Preview each state from the JSON fixture
#Preview("All Feed States") {
    let fixture: [FeedState] = JSONFixture.load("feed-states", as: [String: [FeedState]].self)["states"] ?? []

    TabView {
        ForEach(fixture, id: \.id) { state in
            FeedView(products: state.items, isLoading: state.isLoading)
                .tabItem { Label(state.id, systemImage: "circle") }
        }
    }
}

Testing Codable Conformance in Previews

// Previews that fail to decode expose Codable mismatches before production
// Use fatalError in fixtures so mismatches show as Xcode preview errors

// BUG CATCH EXAMPLE:
// Your API returns "follower_count": "1250" (string)
// Your model has: let followerCount: Int
// JSONFixture.load will fatalError in the preview → you catch it before shipping

// Safer: use try/catch in previews to show error state
#if DEBUG
extension JSONFixture {
    static func safeLoad<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> Result<T, Error> {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy  = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .iso8601

        guard let url  = Bundle.main.url(forResource: filename, withExtension: "json"),
              let data = try? Data(contentsOf: url)
        else {
            return .failure(NSError(domain: "Fixture", code: -1, userInfo: [NSLocalizedDescriptionKey: "\(filename).json not found"]))
        }
        do {
            let result = try decoder.decode(T.self, from: data)
            return .success(result)
        } catch {
            return .failure(error)
        }
    }
}
#endif

#Preview("With Error Handling") {
    switch JSONFixture.safeLoad("user-success", as: User.self) {
    case .success(let user):
        UserProfileView(user: user)
    case .failure(let error):
        Text("Fixture error: \(error.localizedDescription)")
            .foregroundStyle(.red)
    }
}

Best Practices for Production

  • Put fixture files in a "Preview Content" group marked debug-only: In Xcode, Preview Assets groups are automatically excluded from production builds. Alternatively, put JSON fixtures in a Debug target membership so they don't increase production app bundle size.
  • Use real truncated API responses as fixtures: Copy-paste actual API responses and trim to a reasonable size. This catches real-world issues like unexpectedly long strings, unusual characters, and nested structures that synthetic mocks miss.
  • Name fixtures by scenario, not by number: user-verified-no-bio.json is self-documenting; user2.json requires reading the content to understand. Good names make it obvious which state a failing preview was testing.
  • Test accessibility in previews: Add .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) to preview dynamic type scaling. JSON-driven previews make this easy — the same fixture tests multiple accessibility sizes without extra mock data.

FAQ

Q: What's the difference between #Preview and PreviewProvider?
A: #Preview (Swift 5.9, Xcode 15+) is a macro that generates less boilerplate — just a name string and a closure. PreviewProvider is a protocol requiring a static previews property. Both produce the same result; #Preview is cleaner for iOS 17+ projects. Use PreviewProvider when supporting iOS 16 or earlier.

Q: Can I load fixtures from a test bundle?
A: Not directly in preview code — previews run in the main bundle context. Use Bundle.main.url(forResource:). If you want the same fixtures in both unit tests and previews, put them in the main target and mark them as non-production in the build settings (Exclude from Build: Release).

Q: How do I preview a view that requires a ViewModel?
A: Create a preview-specific ViewModel state: #Preview { ContentView(viewModel: ContentViewModel.preview(json: JSONFixture.load("content-state"))) }. Add a static preview(json:) factory on your ViewModel for debug builds that takes decoded JSON and bypasses network calls.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.