WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: BetterRest Challenge #3

Forums > 100 Days of SwiftUI

I'm trying to figure out BetterRest app challenge #3.

Goal is to do away with the calculate button and have the app always display the recommended bedtime.

My solution was to add an action to the cups per day Picker that calls the calculate bedtime function.

When I run the app and change the cups of coffee from the default the action to call the calculate bedtime function doesn't work.

Any assistance would be appreciated.

import CoreML
import SwiftUI

struct ContentView: View {
    static var defaultWakeTime: Date {
        var components = DateComponents()
        components.hour = 7
        components.minute = 0
        return Calendar.current.date(from: components) ?? Date.now
    }
    static var defaultBedTime: Date {
        var components = DateComponents()
        components.hour = 19
        components.minute = 0
        return Calendar.current.date(from: components) ?? Date.now
    }
    @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
    @State private var recommendedBedTime = defaultBedTime

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Text("When do you want to wake up?")
                        .font(.headline)

                    DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
                                .labelsHidden()
                }
                Section {
                    Text("Desired amount of sleep")
                        .font(.headline)
                    Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
                }
                Section {
                    Text("Daily coffee intake")
                        .font(.headline)
                    Picker("Cups per day", selection: $coffeeAmount) {
                        ForEach(1..<21) { number in
                            Button("\(number)", action: calculateBedTime)
                        }
                    }
                }
                Section {
                    Text("Your ideal bedtime is: \(recommendedBedTime.formatted(date: .omitted, time: .shortened))")
                }
                }.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 hour = (components.hour ?? 0) * 60 * 60
            let minute = (components.minute ?? 0) * 60
            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
            let sleepTime = wakeUp - prediction.actualSleep
            recommendedBedTime = sleepTime
            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()
    }
}

   

You might want to look at

.onChange(of: /* Property */ ) { _ in
    calculateBedTime()
}

but you have to have one for each different property that you want to re-calculate

   

Thanks for the tip.

I looked up the method in the Swift documentation and reviewed example there. I thought I could attach the method to the app Form but get an error message that value of type Form has no member 'onChange(of)'. I tried attaching to all the views, i.e., Section, Form and Navigation with the same result.

Surely I'm doing something wrong but not sure what...

   

Try to move most of the calculateBedTime function into a computed property and then pass that along into a Text view somewhere in your app.

Think about it this way... You have @State variables. Whenever ANY @State variable is changed in your view, the view recognizes that change and as a result it destroys the view and re-creates it. This means that any computed properties inside the view also get re-calcuted!

   

I have attached it to a Form the .onChange(ofThis need to be Equatable)

What error message do you get?

   

Here is the error on all attempts to attach...type Form has no member 'onChange(of)'

   

So...I attached the method again to my new Section with the text required by the challenge and got no error this time.

The onChange did the trick. Not sure why I was getting the errors before exactly. Will have to look into that.

Thanks for your help.

   

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.