5.
Beginning SwiftUI
Written by Sarah Reichelt
In Section 1, you installed Xcode and used various tools to run Swift code on your Mac. In this section, you’ll apply that knowledge to create an entire app for your Mac using Swift and SwiftUI.
Chapter 1 gave a tour of Xcode, explained the basic structure of a Mac app project and showed you how SwiftUI views and previews work together. This chapter builds on that.
The app you’re about to create is a word guessing game called Snowman. The computer picks a word and you enter letters to guess the word. Every time you choose an incorrect letter, part of the snowman disappears. If the snowman vanishes completely before you’ve guessed the word, you lose. :[
Setting Up Your App
Start Xcode as you’ve done many times now. Create a new project from the Welcome window or by selecting File ▸ New ▸ Project…. When you get to the template chooser, select macOS ▸ App. Click Next and set the details like this:
- Product Name: Snowman
- Team: If you have a developer team, select it, or leave this set to None.
- Organization Identifier: Enter your reverse domain name as you did in Chapter 1.
- Bundle Identifier: Xcode fills this in based on your previous entries.
- Interface: SwiftUI
- Language: Swift
- Leave the checkboxes unchecked.
When your settings look like this, click Next:
Select where you want to save your project and click Create.
The project window appears and you’re ready to code.
What is SwiftUI?
When developing apps for the Mac, you have a choice of two layout frameworks. AppKit is the older one, and you’ll learn about it later. If you’ve done any iOS programming, it’s similar to UIKit. SwiftUI is the new framework, which Apple describes as “a modern way to declare user interfaces for any Apple platform”.
SwiftUI is a declarative framework. You don’t micro-manage all the details of the user interface. You describe what you want and SwiftUI generates appropriate interface elements for the selected platform.
SwiftUI is also reactive: You connect data to elements in the interface and SwiftUI automatically keeps them in sync. Due to this behavior, the data and the user interface are tightly connected in a SwiftUI app. If you’re getting your data from an external source, it works better to design your data structures first and then make the user interface match them. In this app you have total control, so you’ll lay out the interface first and then see what data you need to make it work.
Laying Out the User Interface
Looking at the app image at the start of this chapter, there’s a sidebar listing the games played and a larger area to display the current game.
This is a common layout for Mac apps, and you’ll use a NavigationSplitView to create this.
Open ContentView.swift and delete everything inside body.
Replace it with this:
// 1
NavigationSplitView {
// 2
Text("Sidebar")
} detail: {
// 3
Text("Game view")
}
What does this do?
- A
NavigationSplitViewhas several different initializers. This one sets it up to have a sidebar and a detail view, which is exactly what this app requires. - The first set of curly braces contains the code to layout the sidebar view. For now, this is a
Textview. - The detail section contains the layout code for the main part of the window. Again, you’re using a
Textplaceholder for now.
The preview will update automatically to show your new design. Hide the inspectors pane on the right to give yourself more space, and use the buttons at the bottom right of the preview pane to adjust the zoom until you see this:
Note: If you don’t see the preview, choose Editor ▸ Canvas or press Option-Command-Return. If the preview pane is open but not active, click the circle arrow beside Preview paused or press Option-Command-P to start it.
Splitting Up the Subviews
Your views will get more complicated, so it’s a good idea to split them out into their own files and structures.
Go to File ▸ New ▸ File… or press Command-N to open the new file selector. Choose macOS ▸ User Interface ▸ SwiftUI View and click Next. Set the file name to SidebarView.swift and click Create.
Repeat this process to make a second SwiftUI view file called GameView.swift.
You now have three View files and there will be more to come, so to make the Project navigator easier to work with, you’ll put them into a separate group.
In the navigator, select ContentView.swift, SidebarView.swift and GameView.swift. With the three files selected, right-click and choose New Group from Selection from the popup menu. Edit the name of the new group to Views and press Return to set it.
Your Project navigator now looks like this:
Large projects can accumulate a lot of files, so this sort of organization is really helpful. You can even make sub-groups inside other groups.
You’ve made two new files, so now it’s time to use them.
Open SidebarView.swift and look at the default code:
// 1
struct SidebarView: View {
// 2
var body: some View {
// 3
Text("Hello, World!")
}
}
This is a standard SwiftUI view:
- Every SwiftUI view is a
structurethat conforms to theViewprotocol. - The
Viewprotocol requires a property calledbodythat has a type ofsome View. This means that it doesn’t matter what sort of a view it is, but it must be something that conforms toView. - The
bodyproperty returns a singleTextview containing some starter text. Asbodyreturns a single object, thereturnkeyword in unnecessary.
You’re not going to work on the sidebar yet, so edit the default “Hello, World!” to say “Games will be listed here”.
Over in GameView.swift, change its Text to say “Game view here”. This is temporary as you’ll start building out the game view very soon.
To use your two new views, go back to ContentView.swift and replace the two Text placeholders so that body looks like this:
var body: some View {
// 1
NavigationSplitView {
// 2
SidebarView()
} detail: {
// 3
GameView()
}
}
Going through this:
- The body property returns a
NavigationSplitView. - The sidebar section of the
NavigationSplitViewdisplaysSidebarView. - The detail section shows
GameView.
You’ve extracted the sidebar and detail views out into their own files and told ContentView to display them. The preview shows the placeholder text you added:
With this in place, you’re ready to design the game view.
Designing the Game View
One of the fundamentals of SwiftUI is that you can build complex views from a set of component views. It looks like the game view has a lot of parts, but if you break it down into components, you can add them one at a time. That way you don’t lose yourself in complexity.
Look at this view of the game area with the parts numbered:
Listing these components:
- The snowman image which changes when you choose an incorrect letter.
- Some status text.
- The word displayed as empty boxes for letters the player hasn’t guessed yet and blue boxes for correctly guessed letters. After the game is over, this draws a red box around letters the player never guessed.
- The New Game button that appears when a game has ended.
- The letters the player guessed already and a field for entering further guesses.
The Snowman Images
SwiftUI has an Image view that can display a picture, but first, you need the images to show. Open the downloaded materials for this chapter and look in the assets folder. There’s a folder called Snowmen with images for both light and dark mode.
To get them into your project, select Assets.xcassets in the Project navigator. It already has entries for AccentColor and AppIcon. Drag the Snowmen folder from the assets folder into the sidebar underneath AppIcon:
Click the > beside the Snowmen folder to expand the folder and see all the imported images:
There are eight images named using the numbers 0 to 7. This naming allows you to use the number of incorrect guesses to select the correct image.
Each image has two versions: one “1x Any Appearance” and the other “1x Dark”. This accommodates both of the Mac’s display modes.
To display an image, open GameView.swift and replace the Text view with:
// 1
Image("0")
// 2
.resizable()
// 3
.aspectRatio(contentMode: .fit)
// 4
.frame(width: 230)
What’s happening here?
-
Imageis a view type likeText, and it takes the name of an image to use. For now, you use “0”, which shows the complete snowman. - By default,
Imageshows the image at its full size. To control this, you add a modifier to make theImageresizable. - The aspect ratio of an image is the ratio between its width and its height. If you change this, the image appears distorted. Applying this
aspectRatiomodifier tells the image to use its original ratio. ThecontentModecan be either.fillwhich expands the image to fill the space chopping off extra bits, or.fitwhich makes sure you can see the entire image. - Finally, a
framemodifier sets the width of the image so it doesn’t jump around as the window resizes.
But what is a modifier? A modifier is a method that takes a view, changes it and returns the edited view. SwiftUI does this very efficiently, so you can add multiple modifiers to a view without affecting performance.
Note:
.fitand.fillare the two cases of theContentModeenumeration. As thecontentModeargument must receive one of these cases, there’s no need to specify the type before the period. You’ll see this pattern a lot in SwiftUI. If you’re curious about the type of any data, Option-click it to show the Quick Help.
The preview shows the complete snowman. Change the number in the Image initializer to see different versions. Click the Variants button at the bottom of the Preview pane and select Color Scheme Variants to see both light and dark modes:
Click the Selectable button to the left of the Variants button to turn off the color variants. This gives more room in the preview for the next part.
That covers the image part of the game view. Now, you’ll add the next component.
Stacking
SwiftUI offers various stack views for arranging components in your layout. HStack arranges them horizontally, VStack arranges them vertically and ZStack piles them on top of each other.
Looking back at the design, the image is to the left and everything else is to the right, so this looks like a good place for an HStack.
Still in GameView.swift, Command-click Image and select Embed in HStack. Nothing changes yet in the preview but add a blank line after the .frame line and then enter this:
Text("Enter a letter to guess the word.")
.font(.title2)
This inserts a Text view to hold the status text and applies a font modifier. There are lots of preset font options you can select. To see what’s available, delete .title2 and type a period — auto-complete now shows you a list of possibilities. Test them out and see how they appear in the preview. Revert to .title2 when you’re done.
You’ve added one more component, but the next one goes below the status text, so this needs a VStack. Command-click Text and choose Embed in VStack.
Again, nothing changes in the preview because there’s only one view in the VStack, but now you’ll add the view that shows the word’s letters.
Looping Through Views
To create the letters view, you’ll loop through the letters in the word, using Text views to show each letter and overlaying this with a rounded rectangle to draw the box.
To start with, you need a test array of letters to work with.
At the top of GameView, before body, define this property:
let word = ["S", "N", "O", "W", "M", "A", "N"]
Next, scroll back down to the VStack and add a blank line after the font modifier before entering this:
// 1
HStack {
// 2
ForEach(word, id: \.self) { letter in
// 3
Text(letter)
.font(.title)
.bold()
.frame(width: 20, height: 20)
.padding()
// 4
.overlay(
// 5
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 2)
// 6
.foregroundColor(.accentColor)
// 7
.padding(2))
}
}
This is the most complicated view you’ve used so far, but taking it bit by bit:
-
Since you want the letters arranged horizontally, start with an
HStack. -
SwiftUI uses
ForEachto create views in a loop. The first argument is the array to loop through, and theidargument allocates an identifier to each element so that SwiftUI can track it. In this case, you’re using each letter as its own identifier. Each time through the loop, the current element has the nameletter. -
Next, display the letter in a
Textview. This has several modifiers to set the font, turn on bold text, specify width and height and add some padding around the letter to space out the border. -
Any view can have an
overlaymodifier which puts another view on top. In this case, you’re adding one of the graphic views. The argument for theoverlayinitializer is the view to lay on top. -
A
RoundedRectangletakes an argument to set the corner radius. Thestrokedictates how thick the line is. -
Use a
foregroundColormodifier to set the color. This usesaccentColor, which is a special color that changes depending on the system color scheme and the accent color selected in System Settings. -
Finally, another
paddingmodifier spaces out the letters. This padding specifies an amount of padding instead of using the platform’s default.
Now the preview looks like this:
The last letter is at the edge of the view, so to make it look better, add a padding modifier to the VStack.
To find the bottom of the VStack, double-click the curly brace on its starting line. This selects all the code inside, including the closing curly brace. Add a new line after that closing brace and type in .padding().
An alternative way to locate the end is to collapse the VStack completely using the code folding ribbon, as you saw in Chapter 1, “Introducing Xcode”.
Buttons
Now, you’ll add the New Game button. It goes in the VStack underneath the letters view. Collapse the HStack that contains the ForEach loop and add a blank line after it.
Next, type in this:
// 1
Button("New Game") {
// 2
print("Starting new game.")
}
// 3
.keyboardShortcut(.defaultAction)
Another new view type:
- This
Buttonview initializer takes the title for the button as its argument. - In curly braces after that, add the code to call when the user clicks the button. This prints to the Xcode console for now.
- A keyboard shortcut allows users to operate without a mouse or trackpad if they want. The
defaultActionkeyboard shortcut means the Return key “clicks” the button.
The preview doesn’t show the default style for the button, so for the first time, press Command-R to build and run the game:
There are some interesting features here that you don’t see in the preview.
First, you can see that the button now looks like a default button. Click it and then press Return. Check the Xcode console to see “Starting new game.” printed twice.
At the top of the window, you see the standard window controls so you can close, minimize or maximize the window. If you close the window, use File ▸ New Window or Command-N to open another one. You can drag the corners or edges to resize the window, but it’s easy to make it too small to be usable. You’ll see how to fix that later in this chapter.
Notice that your window has a toolbar with a button for toggling the sidebar — NavigationSplitView gives you this automatically. You can resize or hide the sidebar by dragging its divider, but once you’ve hidden it, this is the way to get it back.
And while you’re in the app, check out the menus, which are what you would expect in any standard Mac app.
Now, there’s only one more section of the game view to add, so quit the app and get back to Xcode. Press Option-Command-P to resume the preview which pauses whenever you run.
Note: If you’re using Stage Manager, Xcode disappears every time you run the app. This is inconvenient during development, so turn off Stage Manager while you’re coding.
Adding the Final Components
The last section is the area that shows the letters guessed and allows you to guess new ones. First, you’ll need some test data to hold the guesses.
Open GameView.swift and scroll up where you defined word. Add this new property on the next line:
let guesses = [ "E", "S", "R", "X"]
Next, scroll down to where you defined the New Game button. Add a blank line after its keyboardShortcut modifier and insert:
// 1
HStack {
// 2
Text("Letters used:")
// 3
Text(guesses.joined(separator: ", "))
}
This shows the guesses made so far:
- You want a header before the list of letters, so start with with an
HStack. - The header is a
Textview with fixed content. - The second
Textview uses anArraymethod to join theStringelements in the array, separated by comma and space. Thejoinedmethod inserts theseparatorargument between each pair of elements.
The final component is the text field to receive input. This requires some data management, which you’ll learn how to do later. For now, add this placeholder after the end of the last HStack:
// 1
LabeledContent("Guess a letter:") {
// 2
Text("Q")
}
Here’s another new view:
-
LabeledContenttakes aStringto use as the label. - The view inside the curly braces show what comes after the label — in this case, a
Textplaceholder.
With all this in place, your GameView preview looks like this:
Spacing
When you look back at the original design, you’ll see that you have all the parts, but SwiftUI has clustered them into the center.
Some SwiftUI views are greedy and take up as much space as they can. Others only take as much space as they need.
With the preview active, click the different views in your layout code. Xcode draws a box around the active element in the preview, so you can see the space it occupies. The VStack is the one that needs to spread out.
You have two tools for this. First, you can set a spacing for any VStack or HStack.
Command-click VStack and select Show SwiftUI Inspector…. The first option is Spacing , which is set to Inherited by default. This means that it uses whatever is best suited to the platform.
Change Spacing to 30 and click anywhere outside the popup to apply it.
First, you’ll see your new setting appear in the code as an argument to VStack. Then, the preview updates to show your components with more space between them.
This is definite progress, but it spaces the five parts evenly and that’s not what you want. You want a gap at the top, the top two components, another gap, the button and then the remaining two components together at the bottom.
This is where you’ll use Spacer views. A Spacer takes up as much room as it can to push the neighboring elements apart. It works in an HStack or a VStack.
First, insert a Spacer as the top view in the VStack, like this:
VStack(spacing: 30.0) {
Spacer() // NEW
Text("Enter a letter to guess the word.")
Next, find your Button and add two Spacer views, so that the code looks like this:
Spacer() // NEW
Button("New Game") {
print("Starting new game.")
}
.keyboardShortcut(.defaultAction)
Spacer() // NEW
This looks a lot closer, but there’s one more thing… The bottom two views should be closer together. To make this happen, you’ll embed them in another VStack inside the first one. This doesn’t change the direction of layout, but does change the spacing between the parts.
Select the two parts: HStack and LabeledContent. With those lines selected, type an opening curly brace. This wraps them in a pair of braces, indents them and places the cursor before the opening brace. Type VStack and a space to finish the job.
Now check the preview:
Doesn’t that look good? With stack views, padding and spacers, you’ve constructed a great looking view from standard components.
Framing the Window
Time for another run, so press Command-R to build and run the app:
It looks like the preview when it starts up, but what happens when you resize the window?
It doesn’t look so good when the window is very small or very large. When you’re building a Mac app, you have to make it flexible. Some people will run your app on a huge external display and some in a corner of a small monitor. You have to arrange it so that it works no matter what.
First, you’ll fix the large window. The problem here is that the snowman comes towards the center, but you want it to stick to the left.
Looks like a job for another Spacer. :]
Insert a Spacer between the Image and the start of the first VStack, like this:
Image("2")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 230)
Spacer() // NEW
VStack(spacing: 30.0) {
That pushes the snowman to the left, but it pushes the rest of the layout to the right.
Fix this by adding yet another Spacer after the outer VStack’s padding modifier:
} // end of VStack
.padding()
Spacer() // NEW
Run the app again and check out a large window:
That looks good, so now you need to fix the small window case. As you shrink the window’s width, the sidebar hides itself automatically, which is useful. But as you shrink the window further, most of the UI disappears.
To fix this, you’ll use a frame modifier. You used one earlier to set the width of the snowman image. As an experiment, test the app without it. Place the cursor anywhere in the Image’s frame line and press Command-/ to comment it out.
Run the app now and resize the window up and down:
The snowman can get so small it almost disappears and so big it takes over the window.
Quit the app and press Command-/ on the frame line again to fix the snowman size.
Run the app once more so you can work out the best minimum size. Adjust the window until it is just big enough to show all the components and the sidebar.
To work out what size that is, take a screenshot. Press Shift-Command-4 to get the screenshot cursor. Move the mouse pointer over the Snowman window and press Space. This highlights the entire window. Finally, Option-click inside the window.
By default, the screenshot includes a shadow around the window, but you want only the window content. Option-clicking turns off the shadow.
Find the screenshot file on your Desktop and press Command-I to open its info panel. Expand the More Info section to see the dimensions:
Mine shows a width of 1682 and a height of 1002 but on a Retina screen, you divide the numbers by two to convert to SwiftUI units. Dividing and rounding gives a width of 850 and a height of 500 as a good minimum.
To apply these dimensions, open ContentView.swift where you configured the NavigationSplitView.
Replace the contents of body with:
NavigationSplitView {
SidebarView()
} detail: {
GameView()
}
// 1
.frame(minWidth: 850, minHeight: 500)
There’s only one new line here.
- The
framemodifier can take a lot of different arguments. You usedwidthearlier to set a fixed size. Now you’re usingminWidthandminHeightto set lower limits. The window can still get as big as the user wants, but it can’t get any smaller than this.
Run the app again and try resizing the window. Make it as small as possible to confirm that your numbers are correct:
Great work! You’ve laid out the interface, made sure it works with various window sizes and used a lot of different SwiftUI view types.
Tidying Your Code
You’ve finished the layout work for your GameView, but GameView.swift has become long and complicated. Now is a good time to separate out some of this code into subviews.
The first candidate is the HStack that displays the letters in their boxes.
Start by selecting GameView.swift in the Project navigator and then press Command-N to bring up the new file dialog. Having GameView.swift selected means that the new file appears in the Views group, under that file. Choose macOS ▸ SwiftUI View, click Next and name your file LettersView.swift.
Go back to GameView.swift and find the HStack with the ForEach inside.
Before the HStack, add:
LettersView()
This tell GameView to use your new LettersView here.
Select the entire HStack block and press Command-X to cut it. Over in LettersView.swift, delete the Text placeholder and press Command-V to paste your HStack into body.
This gives an error because it can’t find the word property, so cut that property out of GameView and paste it into LettersView.
Using the same technique, make another new SwiftUI View file called GuessesView.swift.
In GameView.swift, find the second VStack and insert this line above it:
GuessesView()
Cut the VStack and paste it to replace the Text in GuessesView.swift. Move the guesses property too, so GuessesView looks like:
struct GuessesView: View {
let guesses = [ "E", "S", "R", "X"]
var body: some View {
VStack {
HStack {
Text("Letters used:")
Text(guesses.joined(separator: ", "))
}
LabeledContent("Guess a letter:") {
Text("Q")
}
}
}
}
Build and run once more to check that everything looks the same as before. You haven’t changed any code, only moved it around, so there are no visible differences. But, you’ve organized your project in a neater and more maintainable way.
Key Points
- SwiftUI is a framework that allows you to layout your user interface programmatically. You tell SwiftUI what you want and it decides how to do it.
- You build your interface by assembling components and grouping them into stacks.
- Modifiers change the views and you can chain multiple modifiers together.
- With a Mac app, it’s important to set a minimum size for your window.
Where to Go From Here
Your game view interface is complete. In the next chapter, you’ll start making it live with real data instead of placeholders.
For more details on SwiftUI, check out SwiftUI Apprentice. It’s written for iOS but the fundamentals of SwiftUI are the same on any Apple platform.