It’s day 28 of the 100 Days of SwiftUI! Yesterday, we finished our BetterRest app by implementing our machine learning model and creating a simple layout. Today, we’re wrapping up our project by reviewing what we learned with a short test by Paul, as well as a few challenges. Let’s dive in!
BetterRest SwiftUI challenges
Learning about machine learning and implementing a machine learning model this early on was a lot of fun and very interesting. That being said, we have much more to learn on this subject, so the challenges today won’t be focused on using Core ML
. The idea was to introduce us to the subject and show how easy it can be to implement in our code.
As for our actual challenges, let’s take a look:
- Replace each
VStack
in our form with aSection
, where the text view is the title of the section. Do you prefer this layout or theVStack
layout? It’s your app – you choose! - Replace the “Number of cups” stepper with a
Picker
showing the same range of values. - Change the user interface so that it always shows their recommended bedtime using a nice and large font. You should be able to remove the “Calculate” button entirely.
For reference, I’ll start with the original code before modifications were made:
import CoreML
import SwiftUI
struct ContentView: View {
@State private var wakeUp = defaultWakeTime
@State private var sleepAmount = 8.0
@State private var coffeeAmount = 1
@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var showingAlert = false
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
var body: some View {
NavigationView {
Form {
VStack(alignment: .leading, spacing: 0) {
Text("When do you want to wake up?")
.font(.headline)
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
.labelsHidden()
}
VStack(alignment: .leading, spacing: 0) {
Text("Desired amount of sleep")
.font(.headline)
Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0){
Text("Daily coffee intake")
.font(.headline)
Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount) cups", value: $coffeeAmount, in: 1...20)
}
}
.navigationTitle("BetterRest")
.toolbar {
Button("Calculate", action: calculateBedtime)
}
.alert(alertTitle, isPresented: $showingAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
}
}
func calculateBedtime() {
do {
let config = MLModelConfiguration()
let model = try SleepCalculator(configuration: config)
let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
let hours = (components.hour ?? 0) * 60 * 60
let minutes = (components.minute ?? 0) * 60
let prediction = try model.prediction(wake: Double(hours + minutes), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
let sleepTime = wakeUp - prediction.actualSleep
alertTitle = "Your ideal bedtime is..."
alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
} catch {
alertTitle = "Error"
alertMessage = "Sorry, there was a problem calculating your bedtime."
}
showingAlert = true
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Challenge #1
Replacing each VStack
with a Section
was done within minutes and I have to say that I find it looks better. It’s cleaner and more cohesive.
var body: some View {
NavigationView {
Form {
Section(header: Text("When do you want to wake up?")) {
DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
.labelsHidden()
}
Section(header: Text("Desired amount of sleep")) {
Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
}
Section(header: Text("Daily coffee intake")) {
Picker("Daily coffee intake", selection: $coffeeAmount) {
ForEach(1..<21) {
Text("\($0) cup(s) of coffee")
}
}
}
Section(header: Text("Recommended bedtime")) {
Text("\(calculateBedtime())")
.font(.title)
}
}
.navigationTitle("BetterRest")
}
}
Challenge #2
Next, I replaced the number of cups Stepper
with a Picker
. In this case, I preferred the Stepper
.
Picker("Daily coffee intake", selection: $coffeeAmount) {
ForEach(1..<21) {
Text("\($0) cup(s) of coffee")
}
Challenge #3
Last but not least, we had to change the user interface so that we didn’t have to press the calculate button. Instead, the app should display the recommended bedtime in real time, using a nice large font.
I started by removing the toolbar and the accompanying button. Then, I modified the calculateBedtime
function to return a string and returned the formatted Date
in a constant called bedTime
. Then, I called the function in a new Section
using string interpolation and added a few modifiers.
Section(header: Text("Recommended bedtime")) {
Text("\(calculateBedtime())")
.font(.title)
}
func calculateBedtime() -> String {
do {
let config = MLModelConfiguration()
let model = try SleepCalculator(configuration: config)
let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
let hours = (components.hour ?? 0) * 60 * 60
let minutes = (components.minute ?? 0) * 60
let prediction = try model.prediction(wake: Double(hours + minutes), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
let sleepTime = wakeUp - prediction.actualSleep
let bedTime = sleepTime.formatted(date: .omitted, time: .shortened)
return bedTime
} catch {
alertTitle = "Error"
alertMessage = "Sorry, there was a problem calculating your bedtime."
showingAlert = true
}
return ""
}
And that’s it for day 28! This project was a lot of fun and I look forward to using Core ML
again in the future. For now, it’s time to recharge as we look ahead to our fifth project where we’ll develop an app called Word Scramble. See you tomorrow!
100 Days of SwiftUI – Day 28 – BetterRest Wrap Up