Day 26
Day 26 êŽë š
Project 4, part 1
Weâre starting a new project today, and weâre going to add some more SwiftUI skills to your collection while also tackling a fascinating area of programming: machine learning.
Nick Bostrom, a Swedish professor teaching at Oxford University, once said âmachine intelligence is the last invention that humanity will ever need to make.â Is that true? Well, itâs certainly true that if we can teach computers to think competently enough it would remove the need for humans to do that thinking, but on the other hand some might argue most of humanity doesnât do much thinking in the first place.
Still, I think youâll be impressed by how easy it is to get started, and how itâs a really natural fit inside SwiftUI.
As a side note, I want to add that by now youâre starting to settling into the frequency of this course: some studying, some app building, a technique project, then consolidation â all repeated several times over.
However, I want to say that if at any point youâre feeling tired, or if life just gets in the way, take a break! If you come back to your code in a day or two, youâll be more relaxed and ready to learn. Like I said at the beginning, this is a marathon not a sprint, and you wonât learn effectively if youâre stressed.
Today you have five topics to work through, and youâll meet Stepper
, DatePicker
, DateFormatter
, and more.
BetterRest: Introduction
BetterRest: Introduction
This SwiftUI project is another forms-based app that will ask the user to enter information and convert that all into an alert, which might sound dull â youâve done this already, right?
Well, yes, but practice is never a bad thing. However, the reason we have a fairly simple project is because I want to introduce you to one of the true power features of iOS development: machine learning (ML).
All iPhones come with a technology called Core ML built right in, which allows us to write code that makes predictions about new data based on previous data it has seen. Weâll start with some raw data, give that to our Mac as training data, then use the results to build an app able to make accurate estimates about new data â all on device, and with complete privacy for users.
The actual app weâre building is called BetterRest, and itâs designed to help coffee drinkers get a good nightâs sleep by asking them three questions:
- When do they want to wake up?
- Roughly how many hours of sleep do they want?
- How many cups of coffee do they drink per day?
Once we have those three values, weâll feed them into Core ML to get a result telling us when they ought to go to bed. If you think about it, there are billions of possible answers â all the various wake times multiplied by all the number of sleep hours, multiplied again by the full range of coffee amounts.
Thatâs where machine learning comes in: using a technique called regression analysis we can ask the computer to come up with an algorithm able to represent all our data. This in turn allows it to apply the algorithm to fresh data it hasnât seen before, and get accurate results.
Youâre going to need to download some files for this project, which you can do from GitHub: twostraws/HackingWithSwift â make sure you look in the SwiftUI section of the files.
Once you have those, go ahead and create a new App project in Xcode called BetterRest. As before weâre going to be starting with an overview of the various technologies required to build the app, so letâs get into itâŠ
Entering numbers with Stepper
Entering numbers with Stepper
SwiftUI has two ways of letting users enter numbers, and the one weâll be using here is Stepper
: a simple - and + button that can be tapped to select a precise number. The other option is Slider
, which weâll be using later on â it also lets us select from a range of values, but less precisely.
Steppers are smart enough to work with any kind of number type you like, so you can bind them to Int
, Double
, and more, and it will automatically adapt. For example, we might create a property like this:
@State private var sleepAmount = 8.0
We could then bind that to a stepper so that it showed the current value, like this:
Stepper("\(sleepAmount) hours", value: $sleepAmount)
When that code runs youâll see 8.000000 hours, and you can tap the - and + to step downwards to 7, 6, 5 and into negative numbers, or step upwards to 9, 10, 11, and so on.
By default steppers are limited only by the range of their storage. Weâre using a Double
in this example, which means the maximum value of the slider will be absolutely massive.
Now, as a father of two kids I canât tell you how much I love to sleep, but even I canât sleep that much. Fortunately, Stepper
lets us limit the values we want to accept by providing an in
range, like this:
Stepper("\(sleepAmount) hours", value: $sleepAmount, in: 4...12)
With that change, the stepper will start at 8, then allow the user to move between 4 and 12 inclusive, but not beyond. This allows us to control the sleep range so that users canât try to sleep for 24 hours, but it also lets us reject impossible values â you canât sleep for -1 hours, for example.
Thereâs a fourth useful parameter for Stepper
, which is a step
value â how far to move the value each time - or + is tapped. Again, this can be any sort of number, but it does need to match the type used for the binding. So, if you are binding to an integer you canât then use a Double
for the step value.
In this instance, we might say that users can select any sleep value between 4 and 12, moving in 15 minute increments:
Stepper("\(sleepAmount) hours", value: $sleepAmount, in: 4...12, step: 0.25)
Thatâs starting to look useful â we have a precise range of reasonable values, a sensible step increment, and users can see exactly what they have chosen each time.
Before we move on, though, letâs fix that text: it says 8.000000 right now, which is accurate but a little too accurate. To fix this, we can just ask Swift to format the Double
using formatted()
:
Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
Perfect!
Selecting dates and times with DatePicker
Selecting dates and times with DatePicker
SwiftUI gives us a dedicated picker type called DatePicker
that can be bound to a date property. Yes, Swift has a dedicated type for working with dates, and itâs called â unsurprisingly â Date
.
So, to use it youâd start with an @State
property such as this:
@State private var wakeUp = Date.now
You could then bind that to a date picker like this:
DatePicker("Please enter a date", selection: $wakeUp)
Try running that in the simulator so you can see how it looks. You should see a tappable options to control days and times, plus the âPlease enter a dateâ label on the left.
Now, you might think that label looks ugly, and try replacing it with this:
DatePicker("", selection: $wakeUp)
But if you do that you now have two problems: the date picker still makes space for a label even though itâs empty, and now users with the screen reader active (more familiar to us as VoiceOver) wonât have any idea what the date picker is for.
A better alternative is to use the labelsHidden()
modifier, like this:
DatePicker("Please enter a date", selection: $wakeUp)
.labelsHidden()
That still includes the original label so screen readers can use it for VoiceOver, but now they arenât visible onscreen any more â the date picker wonât be pushed to one side by some empty text.
Date pickers provide us with a couple of configuration options that control how they work. First, we can use displayedComponents
to decide what kind of options users should see:
- If you donât provide this parameter, users see a day, hour, and minute.
- If you use
.date
users see month, day, and year. - If you use
.hourAndMinute
users see just the hour and minute components.
So, we can select a precise time like this:
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
Finally, thereâs an in
parameter that works just the same as with Stepper
: we can provide it with a date range, and the date picker will ensure the user canât select beyond it.
Now, weâve been using ranges for a while now, and youâre used to seeing things like 1...5
or 0..<10
, but we can also use Swift dates with ranges. For example:
func exampleDates() {
// create a second Date instance set to one day in seconds from now
let tomorrow = Date.now.addingTimeInterval(86400)
// create a range from those two
let range = Date.now...tomorrow
}
Thatâs really useful with DatePicker
, but thereâs something even better: Swift lets us form one-sided ranges â ranges where we specify either the start or end but not both, leaving Swift to infer the other side.
For example, we could create a date picker like this:
DatePicker("Please enter a date", selection: $wakeUp, in: Date.now...)
That will allow all dates in the future, but none in the past â read it as âfrom the current date up to anything.â
Working with dates
Working with dates
Having users enter dates is as easy as binding an @State
property of type Date
to a DatePicker
SwiftUI control, but things get a little woolier afterwards.
You see, working with dates is hard. Like, really hard â way harder than you think. Way harder than I think, and Iâve been working with dates for years.
Take a look at this trivial example:
let now = Date.now
let tomorrow = Date.now.addingTimeInterval(86400)
let range = now...tomorrow
That creates a range from now to the same time tomorrow (86400 is the number of seconds in a day).
That might seem easy enough, but do all days have 86,400 seconds? If they did, a lot of people would be out of jobs! Think about daylight savings time: sometimes clocks go forward (losing an hour) and sometimes go backwards (gaining an hour), meaning that we might have 23 or 25 hours in those days. Then there are leap seconds: times that get added to the clocks in order to adjust for the Earthâs slowing rotation.
If you think thatâs hard, try running this from your Macâs terminal: cal
. This prints a simple calendar for the current month, showing you the days of the week. Now try running cal 9 1752
, which shows you the calendar for September 1752 â youâll notice 12 whole days are missing, thanks to the calendar moving from Julian to Gregorian.
Now, the reason Iâm saying all this isnât to scare you off â dates are inevitable in our programs, after all. Instead, I want you to understand that for anything significant â any usage of dates that actually matters in our code â we should rely on Appleâs frameworks for calculations and formatting.
In the project weâre making weâll be using dates in three ways:
- Choosing a sensible default âwake upâ time.
- Reading the hour and minute they want to wake up.
- Showing their suggested bedtime neatly formatted.
We could, if we wanted, do all that by hand, but then youâre into the realm of daylight savings, leap seconds, and Gregorian calendars.
Much better is to have iOS do all that hard work for us: itâs much less work, and itâs guaranteed to be correct regardless of the userâs region settings.
Letâs tackle each of those individually, starting with choosing a sensible wake up time.
As youâve seen, Swift gives us Date
for working with dates, and that encapsulates the year, month, date, hour, minute, second, timezone, and more. However, we donât want to think about most of that â we want to say âgive me an 8am wake up time, regardless of what day it is today.â
Swift has a slightly different type for that purpose, called DateComponents
, which lets us read or write specific parts of a date rather than the whole thing.
So, if we wanted a date that represented 8am today, we could write code like this:
var components = DateComponents()
components.hour = 8
components.minute = 0
let date = Calendar.current.date(from: components)
Now, because of difficulties around date validation, that date(from:)
method actually returns an optional date, so itâs a good idea to use nil coalescing to say âif that fails, just give me back the current dateâ, like this:
let date = Calendar.current.date(from: components) ?? Date.now
The second challenge is how we could read the hour they want to wake up. Remember, DatePicker
is bound to a Date
giving us lots of information, so we need to find a way to pull out just the hour and minute components.
Again, DateComponents
comes to the rescue: we can ask iOS to provide specific components from a date, then read those back out. One hiccup is that thereâs a disconnect between the values we request and the values we get thanks to the way DateComponents
works: we can ask for the hour and minute, but weâll be handed back a DateComponents
instance with optional values for all its properties. Yes, we know hour and minute will be there because those are the ones we asked for, but we still need to unwrap the optionals or provide default values.
So, we might write code like this:
let components = Calendar.current.dateComponents([.hour, .minute], from: someDate)
let hour = components.hour ?? 0
let minute = components.minute ?? 0
The last challenge is how we can format dates and times, and here we have two options.
First is to rely on the format
parameter that has worked so well for us in the past, and here we can ask for whichever parts of the date we want to show.
For example, if we just wanted the time from a date we would write this:
Text(Date.now, format: .dateTime.hour().minute())
Or if we wanted the day, month, and year, we would write this:
Text(Date.now, format: .dateTime.day().month().year())
You might wonder how that adapts to handling different date formats â for example, here in the UK we use day/month/year, but in some other countries they use month/day/year. Well, the magic is that we donât need to worry about this: when we write day().month().year()
weâre asking for that data, not arranging it, and iOS will automatically format that data using the userâs preferences.
As an alternative, we can use the formatted()
method directly on dates, passing in configuration options for how we want both the date and the time to be formatted, like this:
Text(Date.now.formatted(date: .long, time: .shortened))
The point is that dates are hard, but Apple has provided us with stacks of helpers to make them less hard. If you learn to use them well youâll write less code, and write better code too!
Training a model with Create ML
Training a model with Create ML
On-device machine learning went from âextremely hard to doâ to âquite possible, and surprisingly powerfulâ in iOS 11, all thanks to one Apple framework: Core ML. A year later, Apple introduced a second framework called Create ML, which added âeasy to doâ to the list, and then a second year later Apple introduced a Create ML app that made the whole process drag and drop. As a result of all this work, itâs now within the reach of anyone to add machine learning to their app.
Core ML is capable of handling a variety of training tasks, such as recognizing images, sounds, and even motion, but in this instance weâre going to look at tabular regression. Thatâs a fancy name, which is common in machine learning, but all it really means is that we can throw a load of spreadsheet-like data at Create ML and ask it to figure out the relationship between various values.
Machine learning is done in two steps: we train the model, then we ask the model to make predictions. Training is the process of the computer looking at all our data to figure out the relationship between all the values we have, and in large data sets it can take a long time â easily hours, potentially much longer. Prediction is done on device: we feed it the trained model, and it will use previous results to make estimates about new data.
Letâs start the training process now: please open the Create ML app on your Mac. If you donât know where this is, you can launch it from Xcode by going to the Xcode menu and choosing Open Developer Tool > Create ML.
The first thing the Create ML app will do is ask you to create a project or open a previous one â please click New Document to get started. Youâll see there are lots of templates to choose from, but if you scroll down to the bottom youâll see Tabular Regression; please choose that and press Next. For the project name please enter BetterRest, then press Next, select your desktop, then press Create.
This is where Create ML can seem a little tricky at first, because youâll see a screen with quite a few options. Donât worry, though â once I walk you through it isnât so hard.
The first step is to provide Create ML with some training data. This is the raw statistics for it to look at, which in our case consists of four values: when someone wanted to wake up, how much sleep they thought they liked to have, how much coffee they drink per day, and how much sleep they actually need.
Iâve provided this data for you in BetterRest.csv
, which is in the project files for this project. This is a comma-separated values data set that Create ML can work with, and our first job is to import that.
So, in Create ML look under Data and select âSelectâŠâ under the Training Data title. When you press âSelectâŠâ again it will open a file selection window, and you should choose BetterRest.csv
.
Important: This CSV file contains sample data for the purpose of this project, and should not be used for actual health-related work.
The next job is to decide the target, which is the value we want the computer to learn to predict, and the features, which are the values we want the computer to inspect in order to predict the target. For example, if we chose how much sleep someone thought they needed and how much sleep they actually needed as features, we could train the computer to predict how much coffee they drink.
In this instance, Iâd like you to choose âactualSleepâ for the target, which means we want the computer to learn how to predict how much sleep they actually need. Now press Choose Features, and select all three options: wake, estimatedSleep, and coffee â we want the computer to take all three of those into account when producing its predictions.
Below the Select Features button is a dropdown button for the algorithm, and there are five options: Automatic, Random Forest, Boosted Tree, Decision Tree, and Linear Regression. Each takes a different approach to analyzing data, but helpfully there is an Automatic option that attempts to choose the best algorithm automatically. Itâs not always correct, and in fact it does limit the options we have quite dramatically, but for this project itâs more than good enough.
Tip: If you want an overview of what the various algorithms do, I have a talk just for you called Create ML for Everyone â itâs on YouTube at
When youâre ready, click the Train button in the window title bar. After a couple of seconds â our data is pretty small! â it will complete, and youâll see a big checkmark telling you that everything went to plan.
To see how the training went, select the Evaluation tab then choose Validation to see some result metrics. The value we care about is called Root Mean Squared Error, and you should get a value around about 170. This means on average the model was able to predict suggested accurate sleep time with an error of only 170 seconds, or three minutes.
Tip: Create ML provides us with both Training and Validation statistics, and both are important. When we asked it to train using our data, it automatically split the data up: some to use for training its machine learning model, but then it held back a chunk for validation. This validation data is then used to check its model: it makes a prediction based on the input, then checks how far that prediction was off the real value that came from the data.
Even better, if you go to the Output tab youâll see an our finished model has a file size of 544 bytes or so. Create ML has taken 180KB of data, and condensed it down to just 544 bytes â almost nothing.
Now, 544 bytes sounds tiny, I know, but itâs worth adding that almost all of those bytes are metadata: the author name is in there, along with the names of all the fields: wake, estimatedSleep, coffee, and actualSleep.
The actual amount of space taken up by the hard data â how to predict the amount of required sleep based on our three variables â is well under 100 bytes. This is possible because Create ML doesnât actually care what the values are, it only cares what the relationships are. So, it spent a couple of billion CPU cycles trying out various combinations of weights for each of the features to see which ones produce the closest value to the actual target, and once it knows the best algorithm it simply stores that.
Now that our model is trained, Iâd like you to press the Get button to export it to your desktop, so we can use it in code.
Tip: If you want to try training again â perhaps to experiment with the various algorithms available to us â right-click on your model source in the left-hand window, then select Duplicate.
Once youâve made it through those topics, make sure and post your progress somewhere online â youâve taken the first steps towards understanding machine learning!