Previewing SwiftUI views in both dark and light mode

22 Apr 2022

The light and dark mode feature in iOS was a very welcome addition when it was first introduced. These days it's more or less expected that a high-quality app has a good implementation of both light and dark color schemes. The iOS frameworks have very good support for implementing this, so this post won't go into any details there. Instead, we'll take a look at how you can easily preview both cases (and any potential future cases) using a very simple view struct in SwiftUI.

The first thing to note is how to force the system to use one of the color schemes. The old (and now deprecated) way is by using the view modifier

@inlinable public func colorScheme(_ colorScheme: ColorScheme) -> some View

This view modifier also has a key problem, it changes the color scheme of the entire screen, not only the view it is applied to. This can lead to some undefined situations if several views in the view hierarchy use this modifier at the same time.

Instead, we'll use the new view modifier

@inlinable public func preferredColorScheme(_ colorScheme: ColorScheme?) -> some View

To make this even more useful we'll create a reusable View component that can be used across our app to preview views we create in all possible color schemes. We can use the view builder functionality that SwiftUI is built on so that the call site can supply more than one view, and do it with the same syntax that the views are built. The ColorScheme enum that specifies the active scheme implements the CaseIterable protocol, making it very easy for us to iterate over all cases in the enum. Putting this all together we can get the following View type:

struct ColorSchemesPreview<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
            content.preferredColorScheme(colorScheme)
        }
    }
}

We can also set up a simple preview to demonstrate how this component works:

struct ColorSchemesPreview_Previews: PreviewProvider {
    static var previews: some View {
        ColorSchemesPreview {
            Text("Hello")
            Text("world!")
        }
        .padding()
        .previewLayout(.sizeThatFits)
    }
}

Note how we're using both padding() and previewLayout(.sizeThatFits) directly on the ColorSchemesPreview type. This works so well because of the view builder mechanism, allowing us to specify views like we would in a system-provided SwiftUI component.

One step further

We can also expand on this by using view modifiers. The view modifier can be set up using the same mechanism as the preview View:

struct ColorSchemesViewModifier: ViewModifier {
    func body(content: Content) -> some View {
        Group {
            ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
                content.preferredColorScheme(colorScheme)
            }
        }
    }
}

We'll add an extension on View to allow for easy use.

extension View {
    func colorSchemesPreview() -> some View {
        self.modifier(ColorSchemesViewModifier())
    }
}

Then we can change the view to use the view modifier instead:

struct ColorSchemesPreview<Content: View>: View {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        content.colorSchemesPreview()
    }
}

Finally, we'll update our preview code to show off how to use the view modifier:

struct ColorSchemesPreview_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            Text("Hello")
            Text("world!")
        }
        .colorSchemesPreview()
        
        Text("Foo")
            .colorSchemesPreview()
    }
}

Happy previewing! ✌️