UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: BetterRest (Hacking with iOS: SwiftUI Edition project 4) - help with Challenge 3

Forums > SwiftUI

Hi all. I've been making good progress in learning SwiftUI, having completed the first 3 projects in Hacking with iOS, including all the challenges so far, but am completely stumped on the 3rd challenge of project 4 (BetterRest): https://www.hackingwithswift.com/books/ios-swiftui/betterrest-wrap-up

The challenge is: "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."

Removing the calculate button and displaying the text is easy, but I don't know how to have it always show the result in the text when you adjust the other controls (instead of clicking the calculate button to show the result in an alert)

I have searched the internet and it seems like Binding could solve the problem, but am failing to implement this into the project - because I'm not sure if this is the solution, and also struggling to understand how Binding works (and even where I'd put the code).

Any help would be much appreciated! Here is the completed project, without any alterations:

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()
    }

    @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

    var body: some View {

        NavigationView {
            Form {
                Text("When do you want to wake up?")
                    .font(.headline)

                DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
                    .labelsHidden()
                    .datePickerStyle(WheelDatePickerStyle())

                Section(header: Text("Desired amount of sleep")) {

                    Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
                        Text("\(sleepAmount, specifier: "%g") hours")
                    }
                }

                Section() {
                    Picker(selection: $coffeeAmount, label: Text("Daily coffee intake")) {
                        ForEach(0...20, id: \.self) {
                            Text("\($0)")
                        }
                    }
                }
            }
            .navigationBarTitle("BetterRest")
            .navigationBarItems(trailing:
                Button(action: calculateBedtime) {
                    Text("Calculate")
                })
            .alert(isPresented: $showingAlert) {
                Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
            }
        }
    }

    func calculateBedtime() {
        let model = SleepCalculator()
        let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
        let hour = (components.hour ?? 0) * 60 * 60
        let minute = (components.minute ?? 0) * 60

        do {
            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
            let sleepTime = wakeUp - prediction.actualSleep

            let formatter = DateFormatter()
            formatter.timeStyle = .short

            alertMessage = formatter.string(from: sleepTime)
            alertTitle = "Your ideal bedtime is…"
        } catch {
            alertTitle = "Error"
            alertMessage = "Sorry, there was a problem calculating your bedtime."
        }
        showingAlert = true
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

4      

You can use onAppear(perform:) method to call calculateBedtime() when the view appears.

3      

@Sangsom
You can use onAppear(perform:) method to call calculateBedtime() when the view appears.

Thanks for your reply. I had already tried this, but found that although it helps with the Picker (as the view changes, so onAppear gets called when you return to the original view), it doesn't help when you change the Stepper or DatePicker values.

4      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

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

Yeah correct, ok I've checked again my solution and remembered that I could get it working with onAppear method, so I've refactored calculateBedTime() to return String and used it so.

func calculateBedTime() -> String {
        let model = SleepCalculator()

        let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
        let hour = (components.hour ?? 0) * 60 * 60
        let minute = (components.minute ?? 0) * 60

        do {
            let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
            let sleepTime = wakeUp - prediction.actualSleep

            let formatter = DateFormatter()
            formatter.timeStyle = .short

            return formatter.string(from: sleepTime)
        } catch {
            return "Sorry, there was a problem calculating your bedtime."
        }
  }

And I'm using it in

Section(header: Text("Your recommended bed time is...")) {
    Text(calculateBedTime())
        .font(.largeTitle)
        .foregroundColor(Color(UIColor.systemOrange))
}

Whenever I update desired amount of sleep, wake up time, or daily coffee intake it get updated automatically.

8      

Thank you so much, that works beautifully. I think I was overthinking it and going down a far more complicated route than I needed to!

3      

@Sangsom is there way to center align this section ?

Section(header: Text("Your recommended bed time is...")) {
    Text(calculateBedTime())
        .font(.largeTitle)
        .foregroundColor(Color(UIColor.systemOrange))
}

I used HStack to be able to do that ... couldnt do with Section

HStack{
                    Spacer()
                    Text("Best sleep Time: \(calculateBedtime())")
                        .font(.headline)
                    Spacer()
                }

to center align it.

3      

@ioprojecton

If it helps, I got to center align Text( ) inside a Section, using .frame modifier like this:

Section(header: Text("Your ideal bedtime is ...")){
              Text(calculateBedTime())
                      .font(.title)
                      .frame(maxWidth: .infinity, alignment: .center)
}

3      

This may be a totally different approach but I wanted to ask anyway.. could we possibly implement this using computed properties.. ie. switch the calculateBedTime function into a computed property >.>

3      

I moved the calculateBedTime calculations to a computed property of type String and used that to display to the user. Seems to work

3      

Thank you, I also not sure how to solve challange number 3

3      

Hello, here is one solution for Stepper, I don't know is it the best but it works just fine

 Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
        .onChange(of: sleepAmount, perform: { newValue in
              calculateBedTime()
        })

3      

I can't get my bedtime to update automatically. I use the instructor's code and made slight adjustments and cannot get the bedtime value to appear. I get the following error: Modifying state during view update, this will cause undefined behavior.

Here is my code:

//
//  ContentView.swift
//  BetterRest
//
//

import CoreML
import SwiftUI

struct ContentView: View, Identifiable
{
    @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 bedTime = ""
    let numberOfCoffeeCups = Array(1...20)
    let id = UUID()

    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
            {
                Section
                {
                    Text("When do you want to wake up")
                        .font(.headline)
                    DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
                        .labelsHidden()
                        /*.onAppear(perform: {
                            calculateBedtime()
                        })*/
                }

                Section
                {
                    Text("Desired amout of sleep")
                        .font(.headline)

                    Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
                        /*.onAppear(perform: {
                            calculateBedtime()
                        })*/
                }

                Section
                {

                    Picker("Daily coffee intake", selection: $coffeeAmount)
                    {
                        ForEach(numberOfCoffeeCups, id: \.self)
                        {
                            Text($0 == 1 ? "1 cup" : "\($0) cups")
                        }
                    }
                    /*.onAppear(perform: {
                        calculateBedtime()
                    })*/
                    .pickerStyle(.automatic)
                    .font(.headline)

                    Text("Bedtime is \(calculateBedtime())")

                }
            }
            .navigationTitle("BetterRest")
            .toolbar
            {
                //Button("Calculate", action: calculateBedtime)
            }
            /*.alert(alertTitle, isPresented: $showingAlert)
            {
                Button("OK") { }
            } message: {
                Text(alertMessage)
            }*/
        }
    }

    func calculateBedtime() -> String
    {
       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: Int64(hour + minute), estimatedSleep: sleepAmount, coffee: Int64(coffeeAmount))

           let sleepTime = wakeUp - prediction.actualSleep
           //alertTitle = "Your ideal bedtime is..."
           alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)

           return alertMessage

       } catch {
           //alertTitle = "Error"
           alertMessage = "Error."
           return alertMessage
       }
        //showingAlert = true
    }
}

struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView()
    }
}

2      

Making a computed property called bedTime call calculateBedtime() resolve my issue. Now it automatically updates.

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

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.