Enhance your SwiftUI previews with 🎭 Gala

I switched to dark mode on my Mac recently and after doing so it occurred to me that I should really add previews for both colour schemes to my SwiftUI previews.

So off I went and wrote the following:

    static var previews: some View {
        Group {
            ContentView()
                .environment(\.colorScheme, .light)

            ContentView()
                .environment(\.colorScheme, .dark)
        }
    }

That worked well enough but my left eye started twitching almost immediately at the thought that I’m going to have to duplicate this for every preview.

On top of that, most of my views take setup parameters or other configuration to make them look useful, like for instance this first one I was going to instrument:

    static var previews: some View {
        SearchResultList(query: "",
                         results: .constant(["foo", "bar", "bar"]),
                         selected: .constant(nil),
                         show: true)
            .padding()
            .frame(width: 400, height: 300)
    }

If during refactoring I change any of the properties in SearchResultList I’m going to have to change it for both the dark and the light variant. Plus, I’m going to want to add further variants with other configurations, to see how it lays out in different situations – that’s all going to have to happen twice.

Clearly, that loop cannot remain unrolled! Because that’s what it really is, an unrolled loop.

After a bit of poking around I discovered* that you can actually use ForEach to loop over ColorScheme attributes:

    static var previews: some View {
        ForEach([ColorScheme.light, .dark], id: \.self) { scheme in
            SearchResultList(query: "",
                             results: .constant(["foo", "bar", "bar"]),
                             selected: .constant(nil), show: true)
                .padding()
                .frame(width: 400, height: 300)
                .environment(\.colorScheme, scheme)
        }
    }

My tweet about this discovery was surprisingly popular and led me to explore this further, because while the ForEach loop is certainly a big improvement it still irked me that I’d have to spell out the list of colour schemes each time. (Yes, I’ll happily spend a day saving the work of an hour 😳.)

Anyway, this should be fixable somehow – and indeed it is. Thanks to Function Builder’s ViewBuilder, we can create the following wrapper:

public func NightAndDay<A: View>(_ name: String? = nil,
                                 @ViewBuilder items: @escaping () -> A) -> some View {
    ForEach([ColorScheme.light, .dark], id: \.self) { scheme in
        items()
            .previewDisplayName(name.map { "\($0) \(scheme)" } ?? "\(scheme)")
            .environment(\.colorScheme, scheme)
    }
}

and then use it as follows:

   static var previews: some View {
       NightAndDay {
           ContentView()
       }
   }

It’s like night and … 😶

Of course, once I’m down a rabbit hole I’m going to make myself really comfortable and explore the corners.

Wouldn’t it be nice to have little wrappers for other attributes as well? And wouldn’t it be nice to have that in a package rather than copy functions around?

So from the rabbit hole I emerge with Gala, a little swift package manager library that brings you NightAndDay and some other helpers, like for instance Layouts:

    static var previews: some View {
        Layouts([.fixed(width: 200, height: 150),
                 .sizeThatFits,
                 .device], "Home") 
                
        
    }

Of course, if you really like scrolling your preview screen you’ll nest these

    static var previews: some View {
        NightAndDay {
            Layouts([.fixed(width: 200, height: 150),
                     .sizeThatFits,
                     .device], "Home") {
                    ContentView()
            }
        }
    }

Gala also brings a Device iterator that allows you to render to different device screens without having to guess the stringly initialiser argument of PreviewDevice:

    static var previews: some View {
        Devices([ .iPhoneX, .iPhone11, .iPhone11Pro ]) {
                ContentView()
        }
    }

Using autocompletion you don't have to remember the precise names. Please note that I’ve taken particular care to transform Apple's fantastic product names into identifiers. For instance:

  • iPadPro9·7inch
  • iPhoneXʀ
  • appleWatchSeries5﹘40mm

Thanks to Swift's support of unicode in identifiers you can use these free of ugly underscores (and again thanks to autocomplete you can actually type them 😅).

You can also use Devices(.iPhones) (.iPads, .watches, .tvs) to preview all of them:

    static var previews: some View {
        Devices(.iPhones) {
            ContentView()
        }
    }

You can find Gala on Github. Simply import it as a Swift Package into your Xcode project and off you go.

I hope you find it useful. Please get in touch via Twitter or email if you have questions or via Github if you have fixes or can think of improvements.

* I’ve since found while researching other attributes to loop over for this blog post that the ForEach loop trick has been mentioned in WWDC19 session 233 “Mastering Xcode Previews”.