Day 21
Day 21 êŽë š
Project 2, part 2
One of the things people often complain about while learning to program is that they really want to get busy making The Big App Idea they had, but instead they need to follow tutorials making different apps entirely.
I know this can be annoying, but trust me: nothing youâre learning will be wasted. Sure, you might not ever make a flag guessing game, but the concepts you learn here â building layouts, tracking state, randomizing arrays, and more â will last you for years.
Oprah Winfrey once said, âdo what you have to do until you can do what you want to do.â By the end of this 100 days course I hope youâll be able to do exactly what you want, but in the meantime stick with it â youâre learning key skills here!
Today you have three topics to work through, in which youâll apply your knowledge of VStack
, LinearGradient
, alerts, and more.
Stacking up buttons
Stacking up buttons
Weâre going to start our app by building the basic UI structure, which will be two labels telling the user what to do, then three image buttons showing three world flags.
First, find the assets for this project and drag them into your asset catalog. That means opening Assets.xcassets in Xcode, then dragging in the flag images from the project2-files folder. Youâll notice that the images are named after their country, along with either @2x or @3x â these are images at double resolution and triple resolution to handle different types of iPhone screen.
Next, we need two properties to store our game data: an array of all the country images we want to show in the game, plus an integer storing which country image is correct.
var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"]
var correctAnswer = Int.random(in: 0...2)
The Int.random(in:)
method automatically picks a random number, which is perfect here â weâll be using that to decide which country flag should be tapped.
Inside our body, we need to lay out our game prompt in a vertical stack, so letâs start with that:
var body: some View {
VStack {
Text("Tap the flag of")
Text(countries[correctAnswer])
}
}
Below there we want to have our tappable flag buttons, and while we could just add them to the same VStack
we can actually create a second VStack
so that we have more control over the spacing.
The VStack
we just created above holds two text views and has no spacing, but the flags are going to have 30 points of spacing between them so it looks better.
So, start by adding this ForEach
loop directly below the end of the VStack
we just created:
ForEach(0..<3) { number in
Button {
// flag was tapped
} label: {
Image(countries[number])
.renderingMode(.original)
}
}
The renderingMode(.original)
modifier tells SwiftUI to render the original image pixels rather than trying to recolor them as a button.
And now we have a problem: our body
property is trying to send back two views, a VStack
and a ForEach
, but that wonât work correctly. This is where our second VStack
will come in: Iâd like you to wrap the original VStack
and the ForEach
below in a new VStack
, this time with a spacing of 30 points.
So your code should look like this:
var body: some View {
VStack(spacing: 30) {
VStack {
Text("Tap the flag of")
// etc
}
ForEach(0..<3) { number in
// etc
}
}
}
Having two vertical stacks like this allows us to position things more precisely: the outer stack will space its views out by 30 points each, whereas the inner stack has no spacing.
Thatâs enough to give you a basic idea of our user interface, and already youâll see it doesnât look great â some flags have white in them, which blends into the background, and all the flags are centered vertically on the screen.
Weâll come back to polish the UI later, but for now letâs put in a blue background color to make the flags easier to see. Because this means putting something behind our outer VStack
, we need to use a ZStack
as well. Yes, weâll have a VStack
inside another VStack
inside a ZStack
, and that is perfectly normal.
Start by putting a ZStack
around your outer VStack
, like this:
var body: some View {
ZStack {
// previous VStack code
}
}
Now put this just inside the ZStack
, so it goes behind the outer VStack
:
Color.blue
.ignoresSafeArea()
That .ignoresSafeArea()
modifier ensures the color goes right to the edge of the screen.
Now that we have a darker background color, we should give the text something brighter so that it stands out better:
Text("Tap the flag of")
.foregroundColor(.white)
Text(countries[correctAnswer])
.foregroundColor(.white)
This design is not going to set the world alight, but itâs a solid start!
Showing the playerâs score with an alert
Showing the playerâs score with an alert
In order for this game to be fun, we need to randomize the order in which flags are shown, trigger an alert telling them whether they were right or wrong whenever they tap a flag, then reshuffle the flags.
We already set correctAnswer
to a random integer, but the flags always start in the same order. To fix that we need to shuffle the countries
array when the game starts, so modify the property to this:
var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()
As you can see, the shuffled()
method automatically takes care of randomizing the array order for us.
Now for the more interesting part: when a flag has been tapped, what should we do? We need to replace the // flag was tapped
comment with some code that determines whether they tapped the correct flag or not, and the best way of doing that is with a new method that accepts the integer of the button and checks whether that matches our correctAnswer
property.
Regardless of whether they were correct, we want to show the user an alert saying what happened so they can track their progress. So, add this property to store whether the alert is showing or not:
@State private var showingScore = false
And add this property to store the title that will be shown inside the alert:
@State private var scoreTitle = ""
So, whatever method we write will accept the number of the button that was tapped, compare that against the correct answer, then set those two new properties so we can show a meaningful alert.
Add this directly after the body
property:
func flagTapped(_ number: Int) {
if number == correctAnswer {
scoreTitle = "Correct"
} else {
scoreTitle = "Wrong"
}
showingScore = true
}
We can now call that by replacing the // flag was tapped
comment with this:
flagTapped(number)
We already have number
because itâs given to us by ForEach
, so itâs just a matter of passing that on to flagTapped()
.
Before we show the alert, we need to think about what happens when the alert is dismissed. Obviously the game shouldnât be over, otherwise the whole thing would be over immediately.
Instead weâre going to write an askQuestion()
method that resets the game by shuffling up the countries and picking a new correct answer:
func askQuestion() {
countries.shuffle()
correctAnswer = Int.random(in: 0...2)
}
That code wonât compile, and hopefully youâll see why pretty quickly: weâre trying to change properties of our view that havenât been marked with @State
, which isnât allowed. So, go to where countries
and correctAnswer
are declared, and put @State private
before them, like this:
@State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()
@State private var correctAnswer = Int.random(in: 0...2)
And now weâre ready to show the alert. This needs to:
- Use the
alert()
modifier so the alert gets presented whenshowingScore
is true. - Show the title we set in
scoreTitle
. - Have a dismiss button that calls
askQuestion()
when tapped.
So, put this at the end of the ZStack
in the body
property:
.alert(scoreTitle, isPresented: $showingScore) {
Button("Continue", action: askQuestion)
} message: {
Text("Your score is ???")
}
Yes, there are three question marks that should hold a score value â youâll be completing that part soon!
Styling our flags
Styling our flags
Our game now works, although it doesnât look great. Fortunately, we can make a few small tweaks to our design to make the whole thing look better.
First, letâs replace the solid blue background color with a linear gradient from blue to black, which ensures that even if a flag has a similar blue stripe it will still stand out against the background.
So, find this line:
Color.blue
.ignoresSafeArea()
And replace it with this:
LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
It still ignores the safe area, ensuring that the background goes edge to edge.
Now letâs adjust the fonts weâre using just a little, so that the country name â the part they need to guess â is the most prominent piece of text on the screen, while the âTap the flag ofâ text is smaller and bold.
We can control the size and style of text using the font()
modifier, which lets us select from one of the built-in font sizes on iOS. As for adjusting the weight of fonts â whether we want super-thin text, slightly bold text, etc â we can get fine-grained control over that by adding a weight()
modifier to whatever font we ask for.
Letâs use both of these here, so you can see them in action. Add this directly after the âTap the flag ofâ text:
.font(.subheadline.weight(.heavy))
And put this modifiers directly after the Text(countries[correctAnswer])
view:
.font(.largeTitle.weight(.semibold))
âLarge titleâ is the largest built-in font size iOS offers us, and automatically scales up or down depending on what setting the user has for their fonts â a feature known as Dynamic Type. Weâre overriding the weight of the font so itâs a little bolder, but it will still scale up or down as needed.
Finally, letâs jazz up those flag images a little. SwiftUI gives us a number of modifiers to affect the way views are presented, and weâre going to use two here: one to change the shape of flags, and one to add a shadow.
There are four built-in shapes in Swift: rectangle, rounded rectangle, circle, and capsule. Weâll be using capsule here: it ensures the corners of the shortest edges are fully rounded, while the longest edges remain straight â it looks great for buttons. Making our image capsule shaped is as easy as adding the .clipShape(Capsule())
modifier, like this:
.clipShape(Capsule())
And finally we want to apply a shadow effect around each flag, making them really stand out from the background. This is done using shadow()
, which takes the color, radius, X, and Y offset of the shadow, but if you skip the color we get a translucent black, and if we skip X and Y it assumes 0 for them â all sensible defaults.
So, add this last modifier below the previous two:
.shadow(radius: 5)
So, our finished flag image looks like this:
Image(countries[number])
.renderingMode(.original)
.clipShape(Capsule())
.shadow(radius: 5)
SwiftUI has so many modifiers that help us adjust the way fonts and images are rendered. They all do exactly one thing, so itâs common to stack them up as you can see above.
Upgrading our design
Upgrading our design
At this point weâve built the app and it works well, but with all the SwiftUI skills youâve learned so far we can actually take what weâve built and re-skin it â produce a different UI for the project weâve currently built. This wonât affect the logic at all; weâre just trying out some different UI to see what you can do with your current knowledge.
Experimenting with designs like this is a lot of fun, but I do want to add one word of caution: at the very least, make sure you run your code on all sizes of iOS device, from the tiny iPod touch up to an iPhone 13 Pro Max. Finding something that works well on that wide range of screen sizes takes some thinking!
Letâs start off with the blue-black gradient we have behind our flags. It was okay to get us going, but now I want to try something a little fancier: a radial gradient with custom stops.
Previously I showed you how we can use very precise gradient stop locations to adjust the way our gradient is drawn. Well, if we create two stops that are identical to each other then the gradient goes away entirely â the color just switches from one to the other directly. Letâs try it out with our current design:
RadialGradient(stops: [
.init(color: .blue, location: 0.3),
.init(color: .red, location: 0.3),
], center: .top, startRadius: 200, endRadius: 700)
.ignoresSafeArea()
Thatâs an interesting effect, I think â like we have a blue circle overlaid on top of a red background. That said, itâs also ugly: those red and blue colors together are much too bright.
So, we can send in toned-down versions of those same colors to get something looking more harmonious â shades that are much more common in flags:
RadialGradient(stops: [
.init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
.init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3),
], center: .top, startRadius: 200, endRadius: 400)
.ignoresSafeArea()
Next, right now we have a VStack
with spacing of 30 to place the question area and the flags, but Iâd like to reduce that down to 15:
VStack(spacing: 15) {
Why? Well, because weâre going to make that whole area into a visual element in our UI, making it a colored rounded rectangle so that part of the game stands out on the screen.
To do that, add these modifiers to the end of the same VStack
:
.frame(maxWidth: .infinity)
.padding(.vertical, 20)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
That lets it resize to take up all the horizontal space it needs, adds a little vertical padding, applies a background material so that it stands out from the red-blue gradient the background, and finally clips the whole thing into the shape of a rounded rectangle.
I think thatâs already looking a lot better, but letâs keep pressing on!
Our next step is to add a title before our main box, and a score placeholder after. This means another VStack
around what we have so far, because the existing VStack(spacing: 15)
we have is where we apply the material effect.
So, wrap your current VStack
in a new one with a title at the top, like this:
VStack {
Text("Guess the Flag")
.font(.largeTitle.weight(.bold))
.foregroundColor(.white)
// current VStack(spacing: 15) code
}
Tip: Asking for bold fonts is so common thereâs actually a small shortcut: .font(.largeTitle.bold())
.
That adds a new title at the top, but we can also slot in a score label at the bottom of that new VStack
, like this:
Text("Score: ???")
.foregroundColor(.white)
.font(.title.bold())
Both the âGuess the Flagâ title and score label look great with white text, but the text inside our box doesnât â we made it white because it was sitting on top of a dark background originally, but now itâs really hard to read.
To fix this, we can delete the foregroundColor()
modifier for Text(countries[correctAnswer])
so that it defaults to using the primary color for the system â black in light mode, and white in dark mode.
As for the white âTap the flag ofâ, we can have that use the iOS vibrancy effect to let a little of the background color shine through. Change its foregroundColor()
modifier to this:
.foregroundStyle(.secondary)
At this point our UI more or less works, but I think itâs a little too squished up â if youâre on a larger device youâll see the content all sits in the center of the screen with lots of space above and below, and the white box in the middle runs right to the edges of the screen.
To fix this weâre going to do two things: add a little padding to our outermost VStack
, then add some Spacer()
views to force the UI elements apart. On larger devices these spacers will split up the available space between them, but on small devices theyâll practically disappear â itâs a great way to make our UI work well on all screen sizes.
There are four spacers Iâd like you to add:
- One directly before the âGuess the Flagâ title.
- Two (yes, two) directly before the âScore: ???â text.
- And one directly after the âScore: ???â text.
Remember, when you have multiple spacers like this they will automatically divide the available space equally â having two spacers together will make them take up twice as much space as a single spacer.
And now all that remains is to add a little padding around the outermost VStack
, with this:
.padding()
And thatâs our refreshed design complete! Having all those spacers means that on small devices such as the iPod touch, while also scaling up smoothly to look good even on Pro Max iPhones.
However, this is only one possible design for our app â maybe you prefer the old design over this one, or maybe you want to try something else. The point is, youâve seen how even with the handful of SwiftUI skills you already have itâs possible to build very different designs, and if you have the time I would encourage you to have a play around and see where you end up!
Admit it: building a SwiftUI app is fast, isnât it? Once you know the tools youâre working with, you can turn around a complete game in under 15 minutes, and then just like we did try playing around with the design until you find something you like.