Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to swiftui preview engine, best practices for implementation, and data security standards.
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.
// 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"))
}
// 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
}
}
// 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") }
}
}
}
// 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)
}
}
Debug target membership so they don't increase production app bundle size.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..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.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.
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.