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.

Reboot: A new journey with Swift

It’s been more than thirteen years since my last post, and I think it’s time for a reboot.

The reason is that a few years ago I discovered Hacking With Swift and decided to give coding another try (last time was back in the days of the C64) and finally have reached the stage were I may have something to give back to the community.

You shouldn’t expect in-depth tutorials in this blog, at least not for now, but more of a code journal where I document things I’ve learned along the way – especially if there’s a problem where the solution wasn’t easily found on the we.

Testar en grej – och ger ett livstecken

Det var ju ett tag sedan kom mig för att skriva något, eller lägga upp en bild. Och då jag behöver testa en sak så kan jag ju göra både och.

Sagt och gjort.

2012-10-07 16.09 1dhzcf7.40

Hushållsnära tjänster

Efter tre dagar och ett par pilsner är det flagnande och grå räcket vitt och blankt. I morgon ska vi snickra i källaren.

Nymålat räcke.

Semester är bara ett finare ord för hushållsarbete. I alla fall om man inte längre bor i hyresrätt.

Fast bonnabrännan blir förstås fin.

Bättre än väntat, men bra segt på slutet

Då har man alltså klarat sin första halvmara och det gick helt klart över förväntan. Visst, det var lite trist att försöka hitta skydd mot de många regnskurarna som hemsökte oss under de fyra timmar vi fick vänta innan Broloppet drog igång. Men gott sällskap gjorde själva väntan mer än uthärdlig.

Till slut var det i alla fall dags att springa och drygt en minut efter att starten gått passerade jag startlinjen och gav mig in i tunneln.

Jag hade blivit varnad att det skulle kunna bli rejält kallt i tunneln, men det var precis tvärtom. Rena bastun. Ett tag undrade jag om det var förkylningen som inte alls hade gått över utan slagit ut i full blom, men när jag såg de andra löparnas svettiga ryggar lugnade jag mig lite och njöt av ljudet från tusentals trampande fötter.

Väl ute på bron blev det lite svalare och medvinden gjorde det svårt att hålla det planerade tempot. Det gick lite för fort helt enkelt. Jag trodde faktiskt inte jag skulle behöva sota för det, eftersom de kraftiga byarna gjorde att jag kunde slappna av och ändå komma ner i 4.20-tempo utan ansträngning. I alla fall kändes det inte ansträngande.

Inte så länge jag var på bron i alla fall.

Men som på beställning, så snart vi kom fram till betalstationen var det som om kroppen sa stopp. Förkylning och skadeuppehåll tog ut sin rätt och jag rasade till 5.20-5.25. Väl inne i Limhamn kom äntligen sportdrycken, som borde ha serverats redan ute på bron, och den, tillsammans med publiken, gav en liten kick som gjorde att jag orkade få upp farten lite.

De sista kilometrarna gick på ren vilja och grinighet. Grinigheten kom framför allt från vetskapen att jag inte borde ha haft det minsta problem att hålla brotempot hela vägen in i mål, gärna med en rejäl spurt på slutet. Nu blev det bara en liten fartökning de sista hundra metrarna som gjorde slut på alla krafter.

Men det är ju precis så det ska vara. Har du ork att fortsätta när du gått i mål efter ett lopp har du inte gett tillräckligt. Nu var det precis så jag orkade hålla mig på benen, men inte mycket mer.

Och tiden?

1:40:56 enligt de uppdaterade resultatlistorna (1:41:32 sa min Garminklocka, men jag var nog inte så snabb att stänga av den i mål). Långt bättre än förhoppningen att klara 1:45, för att inte tala om de 1:50 jag faktiskt trodde att det skulle bli. Det bådar verkligen gott inför Berlin, så länge jag får träna som planerat.

Amager Strandvej tur och retur

Det är mindre än en timme kvar innan bussen över till Danmark lämnar Limhamnsfältet. Jag är allt lite smånervös, det är trots allt min första halvmara och uppladdningen kunde ha varit bättre.

Efter Lundaloppet tog det tre och en halv vecka innan jag kunde börja springa igen. Och då först efter ett besök hos Raffi på GGG Physical Fitness som konstaterade att mitt höftproblem orsakades av att höften inte rörde sig som den skulle, vilket i sin tur berodde på två kotor som satt lite snett. Lite drag, slit och ett par akupunkturnålar senare var det åtgärdat och jag hade fått en helt annan rörlighet i höften.

Det blev fyra rätt besvärliga rundor när jag väl kom igång. Jag har ju tränat teknik med Elins löparvänner och fått ett rätt bra löpsteg allt eftersom. Men allt jag lärt mig var som bortblåst. Eller rättare sagt, det fanns kvar men till ingen nytta. Musklerna gjorde vad de drillats till, men eftersom höften nu rör sig på ett helt annat sätt än under inlärningen har jag känt mig alldeles sladdrig i kroppen.

Och vi ska inte tala om vad nästan fyra veckors uppehåll gör med uthållighet. Eller vad en förkylning fyra dagar innan första halvmaran gör med psyket.

Just det, jag fick alltså göra ett nytt uppehåll efter onsdagens morgonrunda och har ägnat resten av veckan till att kurera mig, i stället för att som planerat köra 5-7 kilometers lätt löpning varje dag den här veckan för att hålla kroppen i gång.

Jag är i alla fall rätt så återställd. Inget halsont, ingen feber, bara lite lätt nästäppa. Tillräckligt bra för att jag åtminstone ska starta. Målet är att komma över bron, gärna under 1.45 minuter – vilket inte borde vara något problem eftersom mitt ordinarie slöjoggtempo ska vara tillräckligt för att klara det – och att njuta av ett långpass tillsammans med 30.000 andra löparidioter.

Att komma under 1.30, vilket inte borde ha varit orimligt om jag kunnat träna som vanligt, får vi sikta på till nästa halvmara. Och oavsett hur det går då så vet jag att jag kommer slå ett nytt personbästa. Alltid något.

Ofrivillig vila

Lundaloppet gick ju bra, men sedan dess har jag inte sprungit en meter. Kvalitetsträning och lopp i full fräs gjorde att en inflammation i höften kom som ett brev på posten.

Ja, ska man vara ärlig så hade jag känningar redan när jag sprang i Lund, men har man betalat för att göra något man tycker är vansinnigt kul så …

Det onda har dock gett med sig hyfsat, även om ett felaktigt skoval i fredags innebar en liten försämring. Frågan är dock om jag fixar blodomloppet på tisdag?

Om allt går som det ska gör jag ett försiktigt pass i morgon eller på söndag för att testa hållfastheten. Gör det ont så har jag en plats till salu – inklusive picknick och grymt trevligt sällskap i Elins löparvänner – eftersom jag hellre passar på Blodomloppet för att inte riskera Broloppet.

… och fort sprang jag

Mitt mål och min förhoppning var att klara Lundaloppet – en mil – på under 45 minuter. Jag var lite orolig att jag satt ribban lite för högt eftersom jag inte sprungit så fort i år och dessutom riskerade att ryckas med av stämningen och gå ut för hårt i början, alternativt hindras i min framfart av andra löpare.

Den oron var dock helt obefogad, om man bortser från en del ouppmärksamma löpare som blockerade mig när jag försökte gå om.

Jag gick ut lugnt, tyckte jag, eftersom uppförsbacken från Södra esplanaden till Tornavägen är rätt lång och jag ville inte krokna där. Särskilt inte som jag skulle springa den två gånger. Farten skulle komma lagom vid Tomegapsgatan och då var det dags att börja kolla GPS-klockan för att se hur mycket jag halkat efter.

Men ända upp till Tornavägen låg jag faktiskt före min tänkta tid, och sen blev det bara bättre. Att jag sprang om säkert hundra andra löpare och endast passerades av ett tiotal gjorde inte saken sämre. Men den riktiga egokicken kom på upploppet inne på Lunds IP där jag hamnat i en grupp på ungefär tio löpare.

Efter sista kurvan ökade vi alla farten ungefär lika mycket, så den inbördes ställningen var oförändrad – en liten stund. Med drygt femtio meter kvar av loppet kopplade jag in sprintformen som Elin tränat oss på under intervallpassen och jag gick i mål med tio meter till godo på killen som legat jämsides med mig den sista kilometern.

Min officiella tid blev 43.33 (GPS-klockan hävdade att jag var elva sekunder snabbare), vilket gav mig en hedrande tvåhundratrettiotredjeplats bland nära tolvhundra deltagare. Och med tanke på att flera av dem som kom i mål efter mig tävlade för en riktig klubb så är jag långt mer än nöjd med dagens lopp.

I morgon ska jag springa riktigt fort

… eller åtminstone så fort jag kan. Det är dags för Lundaloppet och i år är jag anmäld. Veckans träningspass har därför varit lite kortare och gått i något högre tempo än vanligt. En åttakilometersrunda i 4:38-tempo i tisdags och ett intervallpass knappt fyra kilomters lätt uppvärmningsjogg följt av fem 1000-metersintervaller med en minuts joggvila mellan.

Tanken var att hålla som lägst 4.30-tempo på tusingarna, vilket gick utan problem, nästan utan ansträngning. Och det innebär att milen under 45 minuter inte är en orimlig målsättning i morgon. Fast det är klart, Lund är ju en enda jävla uppförsbacke – åtminstone åt ena hållet – och det kan naturligtvis påverka utgången.