[SC]()

iOS. Apple. Indies. Plus Things.

Using PreviewModifier for Quick Xcode Previews

// Written by Jordan Morgan // Sep 30th, 2024 // Read it in about 2 minutes // RE: SwiftUI

This post is brought to you by Emerge Tools, the best way to build on mobile.

Ah, Xcode Previews. The absolute best, worst thing Apple has ever shipped for developers. When it works, which for me is around 90% of the time, it’s absolutely essential to my workflow.

Other times? It crashes, spins, indexes and loads for an eternity — and I end up sitting there and waiting for it to work again out of nothing else but pure, unadulterated spite. And, as I launch my fusillade of negative thoughts and words towards Xcode, I couldn’t help but wonder if the problem was me.

Foolishly, I was passing in an expensive object around my previews, a holdover of a //TODO: Don't do this I had while prototyping stuff. In the process, though, I came to love having actual data from the server that wasn’t mocked. This was the good stuff, along with the real-world connectivity loading times.

In short, I wanted my cake. And I wanted to eat it, too.

Recently, I found a tip that’s helped out with the long loads like this, PreviewModifier (docs). It helps make complex, heavy-load type of objects more performant to use across previews by basically doing all the expensive work once.

Sharing is Caring

Consider the purpose of Observable - it’s literally to reuse and share the same chunk of data, managers, data access objects, networking layers — you name it — across your codebase:

@Observable
class DAO {
    var accountData: AccountData = .empty 
    init() {
        veryExpensiveSetup()
    }

    func veryExpensiveSetup() { 
        // Hit API, or DB, or both...
        accountData = fetchedData
    }
}


@main
struct TheBestApp: App {
    // Expensive work happens here
    @State private var dataAccess = DAO()


    var body: some Scene {
        WindowGroup {
            MyViewHierarchy()
                // But at least the hit happens only once
                .environment(dataAccess) 
        }
    }
}

And so it goes, dataAccess can do the time-consuming stuff with an upfront cost, and let the rest of the app use it without incurring the same hit:

struct MyViewHierarchy: View {
    @Environment(DAO.self) private var dataAccess


    var body: some View {
        Form {
            // Everything has access to dataAccess
            Text(dataAccess.accountData.name)
            UserAccountView()
            NotificationsView()
            LegalView()
        }
    }
}

This is SwiftUI 101 stuff. So, given the wins we have here — why don’t we extend the same logic to Xcode Previews? Instead of doing something like this:

#Preview {
    MyViewHierarchy()
        // Snapple! We're running `init()` every
        // time this thing reloads!
        .environment(DAO())
}

…or this1:

extension DAO {
    var mock: DAO { // Setup mocked DAO... }
}

extension AccountData {
    var mock: AcccountData { // Setup mocked account data... }
}

…maybe you want the real, bonafide data that comes with actually hitting your API, or setting up your data layer. But, without doing that over and over.

Caching Preview Objects

It turns out, you can. And I had no idea this was possible until a few days ago. It’s eerily similar to how ViewModifier is handled. Here’s what it looks like:

// Adopt `PreviewModifier`
struct CachedPreviewData: PreviewModifier {

    static func makeSharedContext() async throws -> DAO {
        let dao = DAO()
        await dao.makeSomeOtherNetworkCalls()
        await dao.noSeriouslyGoCrazyWithIt()

        // Because we're only doing this once...
        return dao
    }


    // And now it's reused
    func body(content: Content, context: DAO) -> some View {
        content
            .environment(context)
    }
}


// Add the modifier to the preview.
#Preview(traits: .modifier(CachedPreviewData())) {
    // Now this, or any other preview using the same setup
    // Has access to `DAO`, and it only ran `init()` once
    MyViewHierarchy()
}

When you do this, Xcode Previews will cache the expensive object, and then refer to it over and over. Now, you’re free to have real data, in real previews, without it being a real %!@*&%$ pain in the @$!.

And that’s it! Print out some logs, and you’ll see that crazy work only happens once. Plus, you can get as creative as you want here - again, it works just like ViewModifier does. So, pass in some flags or data in the initializer, make a few of them, house core logic in only one — it all works:

struct CachedPreviewData: PreviewModifier {
    // Add in a flag to access different stuff
    enum UserRole: Int { case user, admin, temp }
    var role: UserRole 

    // Same code as we had before that uses that enum
}

#Preview(traits: .modifier(CachedPreviewData(role: .admin))) {
    // Admin role will be used
    MyViewHierarchy()
}

Until next time ✌️

  1. Which, by the way, is not a bad approach at all. I use this method all the time. But there are situations where you need to hit the actual server for a variety of testing reasons. 

···

Spot an issue, anything to add?

Reach Out.