Mock data in #Preview, using @Environment with preset properties

I wanted to fix the UI glitch in my game Pluttifikation today, and realised I’d need to populate the Xcode preview with some mock data, unless I wanted to recompile the app for every change I made.

However, the problem is that I created a Player-class where all the properties that keep track of the players progress have a pre-configured value. On top of that, the PlayerView access’ Player as a subclass of the @Observable GameController-class that is injected in Environment.

The player’s progress – and name, if they choose to change it – is then saved to, and loaded from, UserDefaults. Simple and effective. The only thing I had to do to make the preview work was injecting the GameController like this:

#Preview {
	PlayerView()
		.environment(GameController())
}

And then I got this lovely preview in Xcode, showing the apps state when it’s opened for the first time. No results, no progress and no player-name:

I needed to add some data, but how?

I knew that you can expand the #Preview macro by creating and returning custom structs and hoped that an init could be used to override the presets. That at least got the compiler to stop complaining, but instead the preview crashed, so not really a win.

I was almost ready to give up when I realised that if the preview is a simple View, the .onAppear-modifier should work just fine.

I tried this:

#Preview {
	struct PreviewWrapper: View {
		@Environment(GameController.self) var gc

		var body: some View {
			PlayerView(gameController: _gc)
				.onAppear {
					previewValues()
				}
		}

		func previewValues() {
			gc.player.name = "John Appleseed"
			gc.player.scorePercentage = 0.75
			gc.player.totalRounds = 330
      ...
		}
	}
	return PreviewWrapper()
		.environment(GameController())
}

And got this:

In hindsight, recompiling the app for every change – or better yet, refactoring the Player-class to allow passing values on creation – would have saved me a lot of time. Especially since the glitch turned out to be caused by applying a shadow to a Gauge with a gradient.

But at least I’ve learned something. And if you’ve painted yourself in the same corner as I did and found this post trying to find a solution, you may have as well.

PS. After writing this post I figured I’d probably benefit from having some mock data for simulator as well, to take away some of the pain that comes with creating screen shots for App Store.

So I just added an #if DEBUG to the method that loads the user default, that simply overrides everything with default values. And of course, with that in place I can go back to using the default #Preview-macro with .environment-injection.

No refactoring required.

func load() {
		self.name = defaults.string(forKey: Settings.name.rawValue) ?? "Spelare"
		self.totalRounds = defaults.double(forKey: Settings.totalRounds.rawValue)
		self.totalScore = defaults.double(forKey: Settings.totalScore.rawValue)
    ...

		// This is just for AppStore-previews.
		#if DEBUG
		name = "John Appleseed"
		scorePercentage = 0.75
		totalRounds = 330
		totalScore = 248
		...
}

Reusable gradient view

The first code snippet I’d like to share comes from a simple game I created for my daughter. The origin is a challenge from 100 days of SwiftUI, where you are supposed to create an app for training the times tables. Back then I only got the logic working, but when she started with multiplications in school this year, she asked if could finish it for her.

At first I planned to get it on her phone with Test Flight, but it turns out that you have to be sixteen to install Test Flight. So I realised the only viable option was to go through App Store, and since that meant the app would be available to anyone with an iPhone I’d better put some effort in the interface.

With some input from my target audience I ended up with a gradient based theme, and now we’ve arrived to the reason for this post, the progress tracking view.

It’s a pretty simple table layout (or a LazyVgrid to be accurate) that shows how many times you’ve played – or practiced – each table, that is represented by different gradients, looking a bit like candy. Maybe that’s why my daughter likes them…

The code is pretty straightforward. Each table is created by calling a GradientView that takes a few arguments.

struct GradientView: View {
	let gradient: Gradient
	let width: CGFloat
	let height: CGFloat
	let radius: CGFloat
	let tabell: LocalizedStringResource
	let spel: Int

    var body: some View {
		LinearGradient(gradient: gradient, startPoint: .topLeading, endPoint: .bottomTrailing)
			.clipShape(RoundedRectangle(cornerRadius: radius))
			.frame(width: width, height: height, alignment: .center)
			.overlay {
				RoundedRectangle(cornerRadius: 5)
					.strokeBorder(.ultraThinMaterial, lineWidth: 0.35)
				VStack {
					Text(tabell)
						.font(Font.custom(Fonts.primary, size: 22))
					Text("\(spel)")
						.font(Font.custom(Fonts.primary, size: 28))
				}
				.shadow(color: .black, radius: 1)
			}
			.shadow(radius: 1, x: 1, y: 2)
			.foregroundStyle(.white)
    }
}

The only important properties to create the View are gradient, width, height and radius. The last two are used to pass in the text that is displayed and can be replaced with whatever fits your needs.

Then I simply call the view in a ForEach, inside my LazyVGrid.

LazyVGrid(columns: gridItems, spacing: width / 32) {
    ForEach(gameController.player.timesEachTablePlayed.indices.dropFirst(), id: \.self) { i in
		GradientView(
			gradient: Gradients.gradients[i],
			width: height / 8,
			height: height / 8,
			radius: 8,
			tabell: globals.tabellerna[i],
			spel: gameController.player.timesEachTablePlayed[i]
		)
	}
}

There are a few things worth mentioning. First, I’ve created an array with predefined gradients to match the twelve playable times tables.

struct Gradients {
	static let gradients: [Gradient] = [
		Gradient(colors: [Color(.blue), Color(.yellow), Color(.red)]), 
		Gradient(colors: [Color(.white), Color(.red), Color(.pink)]),
		Gradient(colors: [Color(.white), Color(.pink), Color(.orange)]),
    ... and so on ...
	]
}

Second, the main view is wrapped in a Geometry Reader to make the layout automatically adjust to the current screen size, hence height/8, being passed to the GradientView. Since I want my buttons to be squares, I don’t bother using height and instead pass width for both properties.

I admit that this solution has a serious problem. If you can’t be sure that your ForEach will return the same number of indices on each run, using a fixed set of pre defined gradients is probably not good idea, unless you want your app to crash. But an easy fix could be to use the modulo operator and have the gradients repeat when you run out of them.

I admit that you could probably find a smarter solution, but if want to become a contributor to the swift community, I’ll have to start somewhere.

This is somewhere.