View not refreshed when .onAppear runs SwiftUI

Viewed 1374

I have some code inside .onAppear and it runs, the problem is that I have to go to another view on the menu and then come back for the UI view to refresh. The code is kind of lengthy, but the main components are below, where mealData comes from CoreData and has some objects:

Go to the updated comment at the bottom for a simpler code example

VStack(alignment: .leading) {

            ScrollView(.vertical, showsIndicators: false) {
                VStack(spacing: 2) {
                    ForEach(mealData, id: \.self) { meal in
                        VStack(alignment: .leading) { ... }
                        .onAppear {

                            // If not first button and count < total amt of objects
                            if((self.settingsData.count != 0) && (self.settingsData.count < self.mealData.count)){
                                let updateSettings = Settings(context: self.managedObjectContext)

                                // If will_eat time isn't nill and it's time is overdue and meal status isn't done
                                if ((meal.will_eat != nil) && (IsItOverDue(date: meal.will_eat!) == true) && (meal.status! != "done")){
                                    self.mealData[self.settingsData.count].status = "overdue"

                                    print(self.mealData[self.settingsData.count])

                                        if(self.settingsData.count != self.mealData.count-1) {
                                            // "Breakfast": "done" = active - Add active to next meal
                                            self.mealData[self.settingsData.count+1].status = "active"
                                        }

                                        updateSettings.count += 1

                                    if self.managedObjectContext.hasChanges {
                                        // Save the context whenever is appropriate
                                        do {
                                            try self.managedObjectContext.save()
                                        } catch let error as NSError {
                                            print("Error loading: \(error.localizedDescription), \(error.userInfo)")
                                        }
                                    }
                                }
                            }
                        }
                    }
                 }
             }
         }

Most likely since the UI is not refreshing automatically I'm doing something wrong, but what?

UPDATE:

I made a little example replicating what's going on, if you run it, and click on set future date, and wit 5 seconds, you'll see that the box hasn't changed color, after that, click on Go to view 2 and go back to view 1 and you'll see how the box color changes... that's what's happening above too:

import SwiftUI

struct ContentView: View {

@State var past = Date()
@State var futuredate = Date()


var body: some View {

    NavigationView {
        VStack {
            NavigationLink(destination: DetailView())
            { Text("Go to view 2") }

            Button("set future date") {
                self.futuredate = self.past.addingTimeInterval(5)
            }

            VStack {
                if (past < futuredate) {
                    Button(action: {
                    }) {
                        Text("")
                    }
                    .padding()
                    .background(Color.blue)
                } else {
                    Button(action: {
                    }) {
                        Text("")
                    }
                    .padding()
                    .background(Color.black)
                }
            }
        }
        .onAppear {
            self.past = Date()
        }
    }
}

}


struct DetailView: View {

@Environment(\.presentationMode) var presentationMode: Binding

var body: some View {
   Text("View 2")
}
}
1 Answers

You need to understand that all you put in body{...} is just an instruction how to display this view. In runtime system creates this View using body closure, gets it in memory like picture and pin this picture to struct with all the stored properties you define in it. Body is not a stored property. Its only instruction how to create that picture.

In your code, there goes init first. You didn't write it but it runs and sets all the structs property to default values = Date(). After that runs .onAppear closure which changes past value. And only then system runs body closure to make an image to display with new past value. And that's all. It is not recreating itself every second. You need to trigger it to check if the condition past < futuredate changed.

When you go to another view and back you do exactly that - force system to recreate view and that's why it checks condition again.

If you want View to change automatically after some fixed time spend, use

DispatchQueue.main.asyncAfter(deadline: .now() + Double(2)) {
    //change @State variable to force View to update
}

for more complex cases you can use Timer like this:

class MyTimer: ObservableObject {
    @Published var currentTimePublisher: Timer.TimerPublisher
    var cancellable: Cancellable?
    let interval: Double
    var isOn: Bool{
        get{if self.cancellable == nil{
                return false
            }else{
                return true
            }
        }
        set{if newValue == false{
                self.stop()
            }else{
                self.start()
            }
        }
    }

    init(interval: Double, autoStart: Bool = true) {
        self.interval = interval
        let publisher = Timer.TimerPublisher(interval: interval, runLoop: .main, mode: .default)
        self.currentTimePublisher = publisher
        if autoStart{
            self.start()
        }
    }
    func start(){
        if self.cancellable == nil{
            self.currentTimePublisher = Timer.TimerPublisher(interval: self.interval, runLoop: .main, mode: .default)
            self.cancellable = self.currentTimePublisher.connect()
        }else{
            print("timer is already started")
        }
    }
    func stop(){
        if self.cancellable != nil{
            self.cancellable!.cancel()
            self.cancellable = nil
        }else{
            print("timer is not started (tried to stop)")
        }
    }
    deinit {
        if self.cancellable != nil{
            self.cancellable!.cancel()
            self.cancellable = nil
        }
    }
}
struct TimerView: View {
    @State var counter: Double
    let timerStep: Double
    @ObservedObject var timer: TypeTimer
    init(timerStep: Double = 0.1, counter: Double = 10.0){
        self.timerStep = timerStep
        self._counter = State<Double>(initialValue: counter)
        self.timer = MyTimer(interval: timerStep, autoStart: false)
    }
    var body: some View {
        Text("\(counter)")
            .onReceive(timer.currentTimePublisher) {newTime in
                print(newTime.description)
                if self.counter > self.timerStep {
                    self.counter -=  self.timerStep
                }else{
                    self.timer.stop()
                }
            }
        .onTapGesture {
            if self.timer.isOn {
                self.timer.stop()
            }else{
                self.timer.start()
            }
         }
    }
}

See the idea? You start timer and it will force View to update by sending notification with the Publisher. View gets that notifications with .onReceive and changes @State variable counter and that cause View to update itself. You need to do something like that.

Related