Why doesn't code in a body property of a View run each time an #State variable of its parent View changes? - swift

I wish to run the function calculateBedtime() when the app first loads, and each time any of the #State variables of ContentView change, so that an updated bedtime is displayed constantly at the bottom of the app in the lowermost Section. However, the app acts as if variable bedtime just keeps its initial value all the time and never changes.
What I am expecting to happen is that when I change any #State variable, say using the DatePicker to change wakeUp, the body property is reinvoked, the first line of which is a call to calculateBedtime(), and so this function runs and updates bedtime as frequently as I want it to.
import SwiftUI
struct ContentView: View {
#State private var wakeUp = defaultWakeTime
#State private var bedtime = ""
#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 {
bedtime = calculateBedtime()
return NavigationView
{
Form
{
Section(header: Text("When do you want to wake up?").font(.headline))
{
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")
.font(.headline))
{
Stepper(value: $sleepAmount, in: 4...12, step: 0.25)
{
Text("\(sleepAmount, specifier: "%g") hours")
}
}
Section(header: Text("Daily coffee intake")
.font(.headline))
{
Picker("\(coffeeAmount+1) cup(s)", selection: $coffeeAmount)
{
ForEach(1..<21)
{ num in
if num==1
{
Text("\(num) cup")
}
else
{
Text("\(num) cups")
}
}
}
.pickerStyle(MenuPickerStyle())
}
Section(header: Text("Your Ideal Bedtime")
.font(.headline))
{
Text("\(bedtime)")
.font(.largeTitle)
}
}
.navigationBarTitle("BetterRest")
}
/*.onAppear(perform: {
calculateBedtime()
})
.onChange(of: wakeUp, perform: { value in
calculateBedtime()
})
.onChange(of: sleepAmount, perform: { value in
calculateBedtime()
})
.onChange(of: coffeeAmount, perform: { value in
calculateBedtime()
})*/
}
static var defaultWakeTime: Date
{
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date()
}
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
var sleepTime = ContentView.defaultWakeTime
do
{
let prediction = try
model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
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
return alertMessage
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
What is the problem here? I am new to SwiftUI and feel that I must have a crucial misunderstanding of how the #State wrapper works. And what would be a good way to get the behavior I desire?

#State variables can only be mutated from within the body of your view and methods invoked by it; for anything else, you need to use ObservableObject which I think will solve your problem here.
You should only access a state property from inside the view’s body, or from methods called by it. For this reason, declare your state properties as private, to prevent clients of your view from accessing them. It is safe to mutate state properties from any thread.
More or less the scaffolding of the code below should achieve the results you want:
class SleepTimerViewModel: ObservableObject {
#Published public var bedTimeMessage: String?
}
struct ContentView: View {
#ObservedObject public var sleepTimerViewModel: SleepTimerViewModel
var body: some View {
Text(sleepTimerViewModel.bedTimeMessage)
}
public func updateBedTimeMessage() {
sleepTimerViewModel.bedTimeMessage = "Hello World"
}
}
I do think it's kind of annoying that Swift just don't care to let you know that you're updating a #State variable incorrectly. It just silently ignores the value you're trying to set, which is super annoying!

Related

Display function output live without Button press

My Swift UI code currently calls a function to display calculations upon a button call. I'd like to display the function's output without the button call (in other words, the function is "live" and constantly calculating anytime a necessary variable is changed). Basically, I'm looking to get rid of the button that triggers this function call calculation, and always have the function's display shown. It has default values so it should have info even before the user inputs or something is changed.
The first screenshot shows the code currently, and the second shows where I'd like the time calculation string to always be. Note: this uses a Create ML file, so if you're inputting this code into your editor, it's not necessary to have the model use to calculate. Any use and output of the variables will do and I've left some commented code that might help.
I'm thinking there might be a calculate on change of X, Y, Z variable needed here. I'm not sure the best way to approach this and would love any ideas. Thanks!
import CoreML
import SwiftUI
struct ContentView: View {
#State var wakeUpTime = defaultWakeTime
#State var coffeeAmount = 1.0
#State var sleepAmount = 8.0
#State var alertTitle = ""
#State var alertMessage = ""
#State var showAlert = 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 {
Section {
DatePicker("Please enter a time", selection: $wakeUpTime, displayedComponents: .hourAndMinute)
.labelsHidden()
} header: {
Text("When do you want to wake up?")
.font(.headline)
}
VStack(alignment: .leading, spacing: 0) {
Text("Hours of sleep?")
.font(.headline)
Stepper(sleepAmount == 1 ? "1 hour" : "\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 1...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Cups of coffee?")
.font(.headline)
Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount.formatted()) cups", value: $coffeeAmount, in: 1...12, step: 0.25)
}
Section {
Text("Head to bed at: IDEAL TIME HERE")
}
}
.navigationTitle("BetterRest")
.toolbar {
Button("Calculate", action: calculateBedtime)
}
.alert(alertTitle, isPresented: $showAlert) {
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: wakeUpTime)
let hour = (components.hour ?? 0) * 60 * 60
let minute = (components.minute ?? 0) * 60
let predicition = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
let sleepTime = wakeUpTime - predicition.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."
}
showAlert = true
// IF TRYING WITHOUT CREATE ML MODEL, comment out all of above^
// let alertTitle = "Showing calculated title"
// let alertMessage = "7:15 am"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
you could try this approach, where you create a class BedTimeModel: ObservableObject to
monitor changes in the various variables that is used to calculate (dynamically)
your sleepTime using func calculateBedtime().
EDIT-1: using Optional sleepTime
class BedTimeModel: ObservableObject {
#Published var sleepTime: Date? = Date() // <-- here optional
#Published var wakeUpTime = defaultWakeTime {
didSet { calculateBedtime() }
}
#Published var coffeeAmount = 1.0 {
didSet { calculateBedtime() }
}
#Published var sleepAmount = 8.0 {
didSet { calculateBedtime() }
}
// can also change this to return the calculated value and use it to update the `sleepTime`
func calculateBedtime() {
// do {
// let config = MLModelConfiguration()
// let model = try SleepCalculator(configuration: config)
// let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUpTime)
// let hour = (components.hour ?? 0) * 60 * 60
// let minute = (components.minute ?? 0) * 60
// let predicition = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
//
// sleepTime = wakeUpTime - predicition.actualSleep // <-- here
// }
// catch {
// sleepTime = nil // <-- here could not be calculated
// }
// for testing, adjust the real calculation to update sleepTime
sleepTime = wakeUpTime.addingTimeInterval(36000 * (sleepAmount + coffeeAmount))
}
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
}
struct ContentView: View {
#StateObject private var vm = BedTimeModel() // <-- here
var body: some View {
NavigationView {
Form {
Section {
DatePicker("Please enter a time", selection: $vm.wakeUpTime, displayedComponents: .hourAndMinute)
.labelsHidden()
} header: {
Text("When do you want to wake up?").font(.headline)
}
VStack(alignment: .leading, spacing: 0) {
Text("Hours of sleep?").font(.headline)
Stepper(vm.sleepAmount == 1 ? "1 hour" : "\(vm.sleepAmount.formatted()) hours", value: $vm.sleepAmount, in: 1...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Cups of coffee?").font(.headline)
Stepper(vm.coffeeAmount == 1 ? "1 cup" : "\(vm.coffeeAmount.formatted()) cups", value: $vm.coffeeAmount, in: 1...12, step: 0.25)
}
Section {
// -- here
if let stime = vm.sleepTime {
Text("Head to bed at: \(stime.formatted(date: .omitted, time: .shortened))")
} else {
Text("There was a problem calculating your bedtime.")
}
}
}
.navigationTitle("BetterRest")
}
}
}

SwiftUI: Preventing binding value from passing back up?

in the Secondary struct, the #Binding property is secondTime and I want it to initially have the value from the "parent".
But when I change the value in this struct, the time property in the parent also changes. Is there a way to get the value from the parent but prevent any changes to the value from going back up to the parent?
struct ContentView: View {
#State var time: String = "";
var body: some View {
VStack {
Text("it is: \(time)")
Secondary(secondTime: $time)
Button("Change time") {
time = "2 poclock"
}
}
}
}
struct Secondary: View {
#Binding var secondTime: String;
var body: some View {
Text("secondary time is \(secondTime)")
Button("Change time again from Secondary View") {
secondTime = "3 oclock"
}
}
}
In Secondary use:
#State var secondTime: String
and in ContentView use:
Secondary(secondTime: time)
not $time
EDIT1:
If you want to click the button in ContentView to change both views,
but Secondary only changes itself, then try this approach:
struct Secondary: View {
#Binding var secondTime: String
#State var localTime: String = ""
var body: some View {
Text("Secondary time is \(localTime)") // <--- here
.onChange(of: secondTime) { newval in // <--- here
localTime = newval // <--- here
}
Button("Change time again from Secondary View") {
localTime = "3 oclock " + String(Int.random(in: 1..<100)) // <-- to show changes
}
}
}
struct ContentView: View {
#State var time: String = ""
var body: some View {
VStack (spacing: 55) {
Text("ContentView it is: \(time)")
Secondary(secondTime: $time)
Button("Change time") {
time = "2 oclock " + String(Int.random(in: 1..<100)) // <-- to show changes
}
}
}
}

SwiftUI: EnvironmentObject loads only the first time

in my current project I work with an EnvironmentObject but I have a very strange bug, maybe someone can help.
First the code:
Model:
class Daten: ObservableObject{
#Published var intervallInsgesamt: Double = UserDefaults.standard.double(forKey: Keys.intervallInsgesamt){
didSet{
UserDefaults.standard.set(self.intervallInsgesamt, forKey: Keys.intervallInsgesamt)
}
}
#Published var pauseInsgesamt: Double = UserDefaults.standard.double(forKey: Keys.pauseInsgesamt ){
didSet{
UserDefaults.standard.set(self.pauseInsgesamt, forKey: Keys.pauseInsgesamt)
}
}
...
}
First View:
#EnvironmentObject var daten: Daten
var body: some View {
NavigationView{
GeometryReader { geometry in
VStack{
ScrollView(showsIndicators: false){
...
}
//IMPORTANT START
NavigationLink(destination: TrainingView().environmentObject(daten)){ //!!!
Text("Start")
.styleButton()
}
//IMPORTANT END
}
}
}
}
Second View (TraingsView):
struct TrainingView: View {
#EnvironmentObject var daten: Daten
#State private var isRunning = false
#State private var to: CGFloat = 0
#State private var insgesamt = 0.0
#State private var minuten = 0
#State private var sekunden = 0
#State private var durchgang = 1
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var zähl = 0
#State private var color = Color.red
#State private var übung = 0
#State private var momentaneÜbung = ""
#State private var nächsteÜbung = ""
#State private var teilerTo = 0
var body: some View {
ZStack{
VStack{
...
}
HStack(spacing: 50){
Button(action:{
isRunning = true
Sounds.playSounds(soundfile: "sound.wav")
}) {
Text("Start")
}
Button(action:{
isRunning = false
Sounds.playSounds(soundfile: "sound.wav")
}) {
Text("Stop")
}
}
}
}
.onReceive(timer) { _ in
if isRunning{
if insgesamt > 0{
print("1. Durchgang: \(durchgang), Übung: \(übung), momenaten: \(momentaneÜbung), nächste: \(nächsteÜbung)")
insgesamt -= 1.0
minuten = Int(insgesamt/60)
sekunden = Int(insgesamt.truncatingRemainder(dividingBy: 60))
zähl += 1
withAnimation(.default) {
to = CGFloat(Double(zähl) / Double(teilerTo))
}
}else{
... stuff that should run when insgesamt <= 0 but is not important for my problem
}
}
}
.onAppear {
teilerTo = Int(daten.intervallInsgesamt)
print("Teiler: \(teilerTo)")
insgesamt = daten.intervallInsgesamt
minuten = Int(insgesamt/60)
sekunden = Int(insgesamt.truncatingRemainder(dividingBy: 60))
if !daten.übungen.isEmpty{
momentaneÜbung = daten.übungen[übung].name
if übung+1 < daten.übungen.count{
nächsteÜbung = "Pause"
}else{
nächsteÜbung = "Nächster Durchgang"
}
}
}
}
}
NOW THE PROBLEM:
When I start the app, set the different times, go to the TrainingsView, everything works fine. Then I go back to the first View(change the times or not, it doesn't matter) and go back to the TraingsView. Now nothing works! It doesn't find the EnvironmentObject anymore. I don't know why because the first time it did. Does somebody know why?
(An other problem I is, that print() doesn't work. I can't see the things that should print out. I don't know if that is important...)
Thanks in advance!
In your root view, you should declare your object as an observable object:
#ObservableObject var daten = Daten()
then you should use a .environmentObject() modifier on top of your first View:
// pass the daten object that was declared as observable object here
.environmentObject(daten)
from now on, on other descendant views, you can get your model by declaring it as an environment object:
#EnvironmentObject var daten: Daten
// gives you the same old daten model

updating a view with changing variables

I have this program with SwiftUI. The program is for calculating the bedtime using machine learning based on 3 user inputs. I have a Text("") showing users their updated bedtime.
I want the program to update the bedtime automatically and display it on my Text(""). I tried many methods and none seems to work. What I tried so far
onAppear - only updates once bedtime when the program first runs
onTapGesture - only updates the bedtime when tapping on the picker (scrolling the picker doesn't work), and it somehow hinders updating the stepper (clicking +/- doesn't change the hours)
using didSet with class conforming to observableObject, #Pulished vars in the class and #ObservedObject in the view struct. Didn't work as well but I tried it only when the class has default values
using didSet in the struct - it didn't update bedtime
Does anyone know if there's an easier way to have the bedtime updated however the user scrolls the picker and whenever a variable changes?
UI looks for detail
struct ContentView: View {
static var defaultWakeUpTime : Date {
var defaultTime = DateComponents()
defaultTime.hour = 7
defaultTime.minute = 0
return Calendar.current.date(from: defaultTime) ?? Date()
}
#State private var wakeUp = defaultWakeUpTime
#State private var sleepAmount = 8.0
#State private var coffeeAmount = 0 {
didSet {
calculateSleepTime()
}
}
#State private var showTime : String = " "
func calculateSleepTime() {**CONTENT**}
var body: some View {
NavigationView {
VStack {
Spacer(minLength: 20)
Text("Your optimum sleep time is \(showTime)")
Spacer(minLength: 10)
Section {
Text("When do you want to wake up?")
.font(.headline)
DatePicker("Please choose a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
}
Spacer()
Form {
Text("How many hours would you like to sleep?")
.font(.headline)
Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
Text("\(sleepAmount, specifier: "%g" ) hours")
}
}
Spacer()
Section {
Text("How many cups of coffee do you drink?")
.font(.headline)
Picker("Coffee Selector", selection: $coffeeAmount) {
ForEach (1..<21) {
Text("\($0) " + "Cup")
}
}
.labelsHidden()
}
}
.navigationBarTitle(Text("BetterSleep"))
.onAppear(perform: calculateSleepTime)
}
}
}
I would use a viewModel and use subscriptions to track values and calculate sleep time.
Change your ContentView at the top to this
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
Now precede any variables with viewModel.
Create a new .swift file I just called it ViewModel but you don't have to.
import Combine
final class ViewModel: ObservableObject {
#Published private(set) var bedTime: String = ""
#Published var wakeUp: Date = Date()
#Published var sleepAmount: Double = 8.0
#Published var coffeeAmount = 0
private var cancellables = Set<AnyCancellable>()
init() {
$wakeUp
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.calculateSleepTime()
}.store(in: &cancellables)
$sleepAmount
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.calculateSleepTime()
}.store(in: &cancellables)
$coffeeAmount
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.calculateSleepTime()
}.store(in: &cancellables)
}
private func calculateSleepTime() {
// Your Logic
self.bedTime =
}
}
Now anytime one of the values changes the suggested bedtime will update. Remember to add one to the coffeeAmount as it starts at 0.

runtime error when try to read fetchrequest<>

I want to get data form coredata ,and show it in Charts,
there is a runtime error at the code in init func when app is loaded:
let x=self.marks.wrappedValue.count
the error message is :Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
the whole code is following:
import SwiftUI
import Charts
import CoreData
struct CareDetails: View {
#Environment(\.managedObjectContext) var context
let timeSpan=["day","week","month","year"]
let care:Care
var marks:FetchRequest<Mark>
#State private var timeSpanIndex = 0
#State private var showSheet = false
private var sessions:[Int]=[]
private var accuracy:[Double]=[]
init(care:Care){
self.care=care
self.marks = FetchRequest(entity:Mark.entity(),
sortDescriptors: [],
predicate: NSPredicate(format:"care_id == %#",care.id!.uuidString))
let x=self.marks.wrappedValue.count
}
var body: some View {
VStack{
Picker(selection: $timeSpanIndex, label: Text("timeSpan")) {
ForEach(0..<timeSpan.count,id:\.self){ idx in
Text(self.timeSpan[idx])
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
LineChartViewSwiftUI(dataLabel: self.care.unit!,sessions: self.sessions,accuracy: self.accuracy)
Form{
Section{
List{
NavigationLink(destination:
CareDetailsList(marks: self.marks)
.environment(\.managedObjectContext, self.context)
){
Text("显示所有数据")
}
}
}
.multilineTextAlignment(.leading)
}
.padding()
}
}
}
It hasn't been evaluated yet. It resolves when update() gets called by your view's body function. Try structuring it slightly differently and using it directly in body.
struct CareView: View {
let care: Care
#FetchRequest var marks: FetchedResult<Mark>
#State private var timeSpanIndex = 0
#State private var showSheet = false
init(care: Care) {
self.care = care
self._marks = FetchRequest(...) // set up as before now. note the `_` though!
}
var body: some View {
// make sure you can ID each `Mark` by something
ForEach(marks, id: \.id) { mark in
// create your `MarkView` here
}
}
}