Day 16
Day 16 êŽë š
Project 1, part one
Now that youâve mastered the basics of the Swift language, itâs time to start applying your skills to some real code in our first project.
This project is a check-sharing app that calculates how to split a check based on the number of people and how much tip you want to leave. The project in itself isnât complicated, but weâll be taking it slow so you can see exactly how these fundamentals fit together.
In some respects going back to the basics like this will feel odd â youâve learned about closures, optionals, and throwing functions, and now thereâs a bit of a reset as we look at the basics of SwiftUI. But take hope: thereâs a lot of value in being able to approach new topics with a fresh mind. As Meister Eckhart said, âbe willing to be a beginner every single morningâ â do that, and youâll learn much faster.
Today is the project overview day, which is where weâll be looking at the isolated pieces of code that you need to understand in order to build our project. Tomorrow weâll move on to the implementation day, where youâll put those new techniques into practice with our app.
Today you have seven topics to work through, and youâll meet Form
, NavigationView
, @State
, and more.
WeSplit: Introduction
WeSplit: Introduction
In this project weâre going to be building a check-splitting app that you might use after eating at a restaurant â you enter the cost of your food, select how much of a tip you want to leave, and how many people youâre with, and it will tell you how much each person needs to pay.
This project isnât trying to build anything complicated, because its real purpose is to teach you the basics of SwiftUI in a useful way while also giving you a real-world project you can expand on further if you want.
Youâll learn the basics of UI design, how to let users enter values and select from options, and how to track program state. As this is the first project, weâll be going nice and slow and explaining everything along the way â subsequent projects will slowly increase the speed, but for now weâre taking it easy.
This project â like all the projects that involve building a complete app â is broken down into three stages:
- A hands-on introduction to all the techniques youâll be learning.
- A step-by-step guide to build the project.
- Challenges for you to complete on your own, to take the project further.
Each of those are important, so donât try to rush past any of them.
In the first step Iâll be teaching you each of the individual new components in isolation, so you can understand how they work individually. There will be lots of code, but also some explanation so you can see how everything works just by itself. This step is an overview: here are the things weâre going to be using, here is how they work, and here is how you use them.
In the second step weâll be taking those concepts and applying them in a real project. This is where youâll see how things work in practice, but youâll also get more context â hereâs why we want to use them, and hereâs how they fit together with other components.
In the final step weâll be summarizing what you learned, and then youâll be given a short test to make sure youâve really understood what was covered. Youâll also be given three challenges: three wholly new tasks that you need to complete yourself, to be sure youâre able to apply the skills youâve learned. We donât provide solutions for these challenges (so please donât write an email asking for them!), because they are there to test you rather than following along with a solution.
Anyway, enough chat: itâs time to begin the first project. Weâre going to look at the techniques required to build our check-sharing app, then use those in a real project.
So, launch Xcode now, and choose Create A New Xcode Project. Youâll be shown a list of options, and Iâd like you to choose iOS and App, then press Next. On the subsequent screen you need to do the following:
- For Product Name please enter âWeSplitâ.
- For Organization Identifier you can enter whatever you want, but if you have a website you should enter it with the components reversed: âhackingwithswift.comâ would be âcom.hackingwithswiftâ. If you donât have a domain, make one up â âme.yourlastname.yourfirstnameâ is perfectly fine.
- For Interface please select SwiftUI.
- For Language please make sure you have Swift selected.
- Make sure all the checkboxes at the bottom are not checked.
In case you were curious about the organization identifier, you should look at the text just below: âBundle Identifierâ. Apple needs to make sure all apps can be identified uniquely, and so it combines the organization identifier â your website domain name in reverse â with the name of the project. So, Apple might have the organization identifier of âcom.appleâ, so Appleâs Keynote app might have the bundle identifier âcom.apple.keynoteâ.
When youâre ready, click Next, then choose somewhere to save your project and click Create. Xcode will think for a second or two, then create your project and open some code ready for you to edit.
Later on weâre going to be using this project to build our check-splitting app, but for now weâre going to use it as a sandbox where we can try out some code.
Okay, letâs get to it!
Understanding the basic structure of a SwiftUI app
Understanding the basic structure of a SwiftUI app
When you create a new SwiftUI app, youâll get a selection of files and maybe 20 lines of code in total.
Inside Xcode you should see the following files in the space on the left, which is called the project navigator:
- .
WeSplitApp.swift
contains code for launching your app. If you create something when the app launches and keep it alive the entire time, youâll put it here. - .
ContentView.swift
contains the initial user interface (UI) for your program, and is where weâll be doing all the work in this project. - .
Assets.xcassets
is an asset catalog â a collection of pictures that you want to use in your app. You can also add colors here, along with app icons, iMessage stickers, and more. Preview Content
is a group, with PreviewAssets.xcassets
inside â this is another asset catalog, this time specifically for example images you want to use when youâre designing your user interfaces, to give you an idea of how they might look when the program is running.
Tip: Depending on Xcodeâs configuration, you may or may not see file extensions in your project navigator. You can control this by going to Xcodeâs preferences, choosing the General tab, then adjusting the File Extensions option.
All our work for this project will take place in ContentView.swift, which Xcode will already have opened for you. It has some comments at the top â those things marked with two slashes at the start â and they are ignored by Swift, so you can use them to add explanations about how your code works.
Below the comments are ten or so lines of code:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Before we start writing our own code, itâs worth going over what all that does, because a couple of things will be new.
First, import SwiftUI
tells Swift that we want to use all the functionality given to us by the SwiftUI framework. Apple provides us with many frameworks for things like machine learning, audio playback, image processing, and more, so rather than assume our program wants to use everything ever we instead say which parts we want to use so they can be loaded.
Second, struct ContentView: View
creates a new struct called ContentView
, saying that it conforms to the View
protocol. View
comes from SwiftUI, and is the basic protocol that must be adopted by anything you want to draw on the screen â all text, buttons, images, and more are all views, including your own layouts that combine other views.
Third, var body: some View
defines a new computed property called body
, which has an interesting type: some View
. This means it will return something that conforms to the View
protocol, which is our layout. Behind the scenes this will actually result in a very complicated data type being returned based on all the things in our layout, but some View
means we donât need to worry about that.
The View
protocol has only one requirement, which is that you have a computed property called body
that returns some View
. You can (and will) add more properties and methods to your view structs, but body
is the only thing thatâs required.
Fourth, Text("Hello, world!")
creates a text view using the string âHello, world!â Text views are simple pieces of static text that get drawn onto the screen, and will automatically wrap across multiple lines as needed.
Fifth, youâll see the padding()
method is being called on the text view. This is what Swift calls a modifier, which are regular methods with one small difference: they always return a new view that contains both your original data, plus the extra modification you asked for. In our case that means body
will return a padded text view, not just a regular text view.
Below the ContentView
struct youâll see a ContentView_Previews
struct, which conforms to the PreviewProvider
protocol. This piece of code wonât actually form part of your final app that goes to the App Store, but is instead specifically for Xcode to use so it can show a preview of your UI design alongside your code.
These previews use an Xcode feature called the canvas, which is usually visible directly to the right of your code. You can customize the preview code if you want, and they will only affect the way the canvas shows your layouts â it wonât change the actual app that gets run.
The canvas will automatically preview using one specific Apple device, such as the iPhone 13 Pro or an iPod touch. To change this, look at the top center of your Xcode window for the current device, then click on it and select an alternative. This will also affect how your code is run in the virtual iOS simulator later on.
Important: If you donât see the canvas in your Xcode window, go to the Editor menu and select Canvas.
Very often youâll find that an error in your code stops Xcodeâs canvas from updating â youâll see something like âAutomatic preview updating pausedâ, and can press Resume to fix it. As youâll be doing this a lot, let me recommend an important shortcut: Option+Cmd+P does the same as clicking Resume.
Creating a form
Creating a form
Many apps require users to enter some sort of input â it might be asking them to set some preferences, it might be asking them to confirm where they want a car to pick them up, it might be to order food from a menu, or anything similar.
SwiftUI gives us a dedicated view type for this purpose, called Form. Forms
are scrolling lists of static controls like text and images, but can also include user interactive controls like text fields, toggle switches, buttons, and more.
You can create a basic form just by wrapping the default text view inside Form
, like this:
var body: some View {
Form {
Text("Hello, world!")
.padding()
}
}
If youâre using Xcodeâs canvas, youâll see it change quite dramatically: before Hello World was centered on a white screen, but now the screen is a light gray, and Hello World appears in the top left in white.
You can remove the padding too â weâll come back to that later:
Form {
Text("Hello, world!")
}
What youâre seeing here is the beginnings of a list of data, just like youâd see in the Settings app. We have one row in our data, which is the Hello World text, but we can add more freely and have them appear in our form immediately:
Form {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
In fact, you can have as many things inside a form as you want, although if you intend to add more than 10 SwiftUI requires that you place things in groups to avoid problems.
For example, this code shows ten rows of text just fine:
Form {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
But this attempts to show 11, which is not allowed:
Form {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
Tip: In case you were curious why 10 rows are allowed but 11 is not, this is a limitation in SwiftUI: it was coded to understand how to add one thing to a form, how to add two things to a form, how to add three things, four things, five things, and more, all the way up to 10, but not beyond â they needed to draw a line somewhere. This limit of 10 children inside a parent actually applies everywhere in SwiftUI.
If you wanted to have 11 things inside the form you should put some rows inside a Group
:
Form {
Group {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
Group {
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
}
Groups donât actually change the way your user interface looks, they just let us work around SwiftUIâs limitation of ten child views inside a parent â thatâs text views inside a form, in this instance.
If you want your form to look different when splitting items into chunks, you should use the Section
view instead. This splits your form into discrete visual groups, just like the Settings app does:
Form {
Section {
Text("Hello, world!")
}
Section {
Text("Hello, world!")
Text("Hello, world!")
}
}
Thereâs no hard and fast rule when you should split a form into sections â itâs just there to group related items visually.
Adding a navigation bar
Adding a navigation bar
If we ask for it, iOS allows us to place content anywhere on the screen, including under the system clock at the top and the home indicator at the bottom. This doesnât look great, which is why by default SwiftUI ensures components are placed in an area where they canât be covered up by system UI or device rounded corners â an area known as the safe area.
On an iPhone 13, the safe area spans the space from just below the notch down to just above the home indicator. You can see it in action with a user interface like this one:
struct ContentView: View {
var body: some View {
Form {
Section {
Text("Hello, world!")
}
}
}
}
Try running that in the iOS simulator â press the Play button in the top-left corner of Xcodeâs window, or press Cmd+R.
Youâll see that the form starts below the notch, so by default the row in our form is fully visible. However, forms can also scroll, so if you swipe around in the simulator youâll find you can move the row up so it goes under the clock, making them both hard to read.
A common way of fixing this is by placing a navigation bar at the top of the screen. Navigation bars can have titles and buttons, and in SwiftUI they also give us the ability to display new views when the user performs an action.
Weâll get to buttons and new views in a later project, but I do at least want to show you how to add a navigation bar and give it a title, because it makes our form look better when it scrolls.
Youâve seen that we can place a text view inside a section by adding Section
around the text view, and that we can place the section inside a Form
in a similar way. Well, we add a navigation bar in just the same way, except here itâs called NavigationView
.
var body: some View {
NavigationView {
Form {
Section {
Text("Hello, world!")
}
}
}
}
When you see that code in Xcodeâs canvas, youâll notice thereâs a large gray space at the top of your UI. Well, thatâs our navigation bar in action, and if you run your code in the simulator youâll see the form slides under the bar as it moves to the top of the screen.
Youâll usually want to put some sort of title in the navigation bar, and you can do that by attaching a modifier to whatever youâve placed inside.
Letâs try adding a modifier to set the navigation title for our form:
NavigationView {
Form {
Section {
Text("Hello, world!")
}
}
.navigationTitle("SwiftUI")
}
When we attach the .navigationTitle()
modifier to our form, Swift actually creates a new form that has a navigation title plus all the existing contents you provided.
When you add a title to a navigation bar, youâll notice it uses a large font for that title. You can get a small font by adding another modifier:
.navigationBarTitleDisplayMode(.inline)
You can see how Apple uses these large and small titles in the Settings app: the first screen says âSettingsâ in large text, and subsequent screens show their titles in small text.
Modifying program state
Modifying program state
Thereâs a saying among SwiftUI developers that our âviews are a function of their state,â but while thatâs only a handful of words it might be quite meaningless to you at first.
If you were playing a fighting game, you might have lost a few lives, scored some points, collected some treasure, and perhaps picked up some powerful weapons. In programming, we call these things state â the active collection of settings that describe how the game is right now.
When we say SwiftUIâs views are a function of their state, we mean that the way your user interface looks â the things people can see and what they can interact with â are determined by the state of your program. For example, they canât tap Continue until they have entered their name in a text field.
Letâs put this into practice with a button, which in SwiftUI can be created with a title string and an action closure that gets run when the button is tapped:
struct ContentView: View {
var tapCount = 0
var body: some View {
Button("Tap Count: \(tapCount)") {
tapCount += 1
}
}
}
That code looks reasonable enough: create a button that says âTap Countâ plus the number of times the button has been tapped, then add 1 to tapCount
whenever the button is tapped.
However, it wonât build; thatâs not valid Swift code. You see, ContentView
is a struct, which might be created as a constant. If you think back to when you learned about structs, that means itâs immutable â we canât change its values freely.
When creating struct methods that want to change properties, we need to add the mutating
keyword: mutating func doSomeWork()
, for example. However, Swift doesnât let us make mutating computed properties, which means we canât write mutating var body: some View
â it just isnât allowed.
This might seem like weâre stuck at an impasse: we want to be able to change values while our program runs, but Swift wonât let us because our views are structs.
Fortunately, Swift gives us a special solution called a property wrapper: a special attribute we can place before our properties that effectively gives them super-powers. In the case of storing simple program state like the number of times a button was tapped, we can use a property wrapper from SwiftUI called @State
, like this:
struct ContentView: View {
@State var tapCount = 0
var body: some View {
Button("Tap Count: \(tapCount)") {
self.tapCount += 1
}
}
}
That small change is enough to make our program work, so you can now build it and try it out.
@State
allows us to work around the limitation of structs: we know we canât change their properties because structs are fixed, but @State
allows that value to be stored separately by SwiftUI in a place that can be modified.
Yes, it feels a bit like a cheat, and you might wonder why we donât use classes instead â they can be modified freely. But trust me, itâs worthwhile: as you progress youâll learn that SwiftUI destroys and recreates your structs frequently, so keeping them small and simple is important for performance.
Tip: There are several ways of storing program state in SwiftUI, and youâll learn all of them. @State
is specifically designed for simple properties that are stored in one view. As a result, Apple recommends we add private
access control to those properties, like this: @State private var tapCount = 0
.
Binding state to user interface controls
Binding state to user interface controls
SwiftUIâs @State
property wrapper lets us modify our view structs freely, which means as our program changes we can update our view properties to match.
However, things are a little more complex with user interface controls. For example, if you wanted to create an editable text box that users can type into, you might create a SwiftUI view like this one:
struct ContentView: View {
var body: some View {
Form {
TextField("Enter your name")
Text("Hello, world!")
}
}
}
That tries to create a form containing a text field and a text view. However, that code wonât compile because SwiftUI wants to know where to store the text in the text field.
Remember, views are a function of their state â that text field can only show something if it reflects a value stored in your program. What SwiftUI wants is a string property in our struct that can be shown inside the text field, and will also store whatever the user types in the text field.
So, we could change the code to this:
struct ContentView: View {
var name = ""
var body: some View {
Form {
TextField("Enter your name", text: name)
Text("Hello, world!")
}
}
}
That adds a name
property, then uses it to create the text field. However, that code still wonât work because Swift needs to be able to update the name
property to match whatever the user types into the text field, so you might use @State
like this:
@State private var name = ""
But that still isnât enough, and our code still wonât compile.
The problem is that Swift differentiates between âshow the value of this property hereâ and âshow the value of this property here, but write any changes back to the property.â
In the case of our text field, Swift needs to make sure whatever is in the text is also in the name
property, so that it can fulfill its promise that our views are a function of their state â that everything the user can see is just the visible representation of the structs and properties in our code.
This is whatâs called a two-way binding: we bind the text field so that it shows the value of our property, but we also bind it so that any changes to the text field also update the property.
In Swift, we mark these two-way bindings with a special symbol so they stand out: we write a dollar sign before them. This tells Swift that it should read the value of the property but also write it back as any changes happen.
So, the correct version of our struct is this:
struct ContentView: View {
@State private var name = ""
var body: some View {
Form {
TextField("Enter your name", text: $name)
Text("Hello, world!")
}
}
}
Try running that code now â you should find you can tap on the text field and enter your name, as expected.
Before we move on, letâs modify the text view so that it shows the userâs name directly below their text field:
Text("Your name is \(name)")
Notice how that uses name
rather than $name
? Thatâs because we donât want a two-way binding here â we want to read the value, yes, but we donât want to write it back somehow, because that text view wonât change.
So, when you see a dollar sign before a property name, remember that it creates a two-way binding: the value of the property is read, but also written.
Creating views in a loop
Creating views in a loop
Itâs common to want to create several SwiftUI views inside a loop. For example, we might want to loop over an array of names and have each one be a text view, or loop over an array of menu items and have each one be shown as an image.
SwiftUI gives us a dedicated view type for this purpose, called ForEach
. This can loop over arrays and ranges, creating as many views as needed. Even better, ForEach
doesnât get hit by the 10-view limit that would affect us if we had typed the views by hand.
ForEach
will run a closure once for every item it loops over, passing in the current loop item. For example, if we looped from 0 to 100 it would pass in 0, then 1, then 2, and so on.
For example, this creates a form with 100 rows:
Form {
ForEach(0..<100) { number in
Text("Row \(number)")
}
}
Because ForEach
passes in a closure, we can use shorthand syntax for the parameter name, like this:
Form {
ForEach(0 ..< 100) {
Text("Row \($0)")
}
}
ForEach
is particularly useful when working with SwiftUIâs Picker
view, which lets us show various options for users to select from.
To demonstrate this, weâre going to define a view that:
- Has an array of possible student names.
- Has an
@State
property storing the currently selected student. - Creates a
Picker
view asking users to select their favorite, using a two-way binding to the@State
property. - Uses
ForEach
to loop over all possible student names, turning them into a text view.
Hereâs the code for that:
struct ContentView: View {
let students = ["Harry", "Hermione", "Ron"]
@State private var selectedStudent = "Harry"
var body: some View {
NavigationView {
Form {
Picker("Select your student", selection: $selectedStudent) {
ForEach(students, id: \.self) {
Text($0)
}
}
}
}
}
}
Thereâs not a lot of code in there, but itâs worth clarifying a few things:
- The
students
array doesnât need to be marked with@State
because itâs a constant; it isnât going to change. - The
selectedStudent
property starts with the value âHarryâ but can change, which is why itâs marked with@State
. - The
Picker
has a label, âSelect your studentâ, which tells users what it does and also provides something descriptive for screen readers to read aloud. - The
Picker
has a two-way binding toselectedStudent
, which means it will start showing a selection of âHarryâ but update the property when the user selects something else. - Inside the
ForEach
we loop over all the students. - For each student we create one text view, showing that studentâs name.
The only confusing part in there is this: ForEach(students, id: \.self)
. That loops over the students
array so we can create a text view for each one, but the id: \.self
part is important. This exists because SwiftUI needs to be able to identify every view on the screen uniquely, so it can detect when things change.
For example, if we rearranged our array so that Ron came first, SwiftUI would move its text view at the same time. So, we need to tell SwiftUI how it can identify each item in our string array uniquely â what about each string makes it unique? If we had an array of structs we might say âoh, my struct has a title
string that is always unique,â or âmy struct has an id
integer that is always unique.â Here, though, we just have an array of simple strings, and the only thing unique about the string is the string itself: each string in our array is different, so the strings are naturally unique.
So, when weâre using ForEach
to create many views and SwiftUI asks us what identifier makes each item in our string array unique, our answer is \.self
, which means âthe strings themselves are unique.â This does of course mean that if you added duplicate strings to the students
array you might hit problems, but here itâs just fine.
Anyway, weâll look at other ways to use ForEach
in the future, but thatâs enough for this project.
This is the final part of the overview for this project, so itâs almost time to get started with the real code. If you want to save the examples youâve programmed you should copy your project directory somewhere else.
When youâre ready, put ContentView.swift
back to the way it started when you first made the project, so we have a clean slate to work from:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Once youâve made it through those topics, make sure and post your progress somewhere online â itâs such an easy way to keep yourself motivated and accountable!