Search code examples
iosxcodeforeachswiftuiuitabview

How to make a timer in SwiftUI keep firing when changing tab with tabview


I have a timer that fires every half second and that leads to the calling of a function that outputs a set of strings that are used to display a countdown to a specific date. It works when I create a new event and then switch over to the tab that contains the information for the countdown, but when I switch back to the add event tab and then back it stops counting down.

The timer is made using this:

    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()

It runs later using this

ForEach(eventNames.indices, id: \.self) { index in


                    VStack{
                        Text("Your event " + "\(self.eventNames[index])" + " is in " + "\(self.string[index])")
                        .onReceive(self.timer) { input in
                        self.differenceDate(numbers: index)

                        }
                    }


                }

And finally, it calls this function

func differenceDate(numbers: Int) {
        self.formatter.unitsStyle = .full
        self.formatter.allowedUnits = [.day, .hour, .minute, .second]
        //self.formatter.maximumUnitCount = 2
        self.now = Date();
        if self.now > self.eventDates[numbers] {
            self.eventNames[numbers] = "";
        }
        else {
        self.string[numbers] = self.formatter.string(from: self.now, to: self.eventDates[numbers]) ?? ""
        }

        }

This is the full code


import SwiftUI

struct ContentView: View {
    @State private var selection = 0
    @State private var eventDates = [Date]()
    @State private var eventNames = [String]()
    @State private var currentName = "";
    @State private var counter = 0;
    @State private var placeholderText = "Event Name";
    @State private var selectedDate = Date();
    var numbers = 0;
    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
    @State var now = Date();
    @State var string = [String]();
    var formatter = DateComponentsFormatter();
    func differenceDate(numbers: Int) {
        self.formatter.unitsStyle = .full
        self.formatter.allowedUnits = [.day, .hour, .minute, .second]
        //self.formatter.maximumUnitCount = 2
        self.now = Date();
        if self.now > self.eventDates[numbers] {
            self.eventNames[numbers] = "";
        }
        else {
        self.string[numbers] = self.formatter.string(from: self.now, to: self.eventDates[numbers]) ?? ""
        }

        }
    var body: some View {
        TabView(selection: $selection){
            //Page 1
            VStack{
                Text("Add New Event")
                .underline()
                .font(.title)
                .padding(15)


//                        .onReceive(self.timer) { input in
//                        self.differenceDate(numbers: index)
//                        //}
//                        }
                  //  .minimumScaleFactor(0.1)




                TextField("\(placeholderText)", text: $currentName)
                .padding(10)
                .overlay(
                    RoundedRectangle(cornerRadius: 5)
                        .stroke(Color.gray, lineWidth: 1)
                    .padding(5)
                )



               Text("When is your event?")
                DatePicker("Please enter a date", selection: $selectedDate, displayedComponents: .date)
                    .labelsHidden()
                    .scaledToFill()
                Button(action: {
                    if self.currentName != "" {
                    self.eventNames.append(self.currentName)
                    self.eventDates.append(self.selectedDate)
                    self.string.append("")

                    self.currentName = "";
                    }

                })

                {
                    Text("Add Event")
                        .font(.headline)
                        .foregroundColor(.black)
                }
                .padding(25)

                .overlay(
                    RoundedRectangle(cornerRadius: 5)
                    .stroke(Color.gray, lineWidth: 3)
                    .padding(5)
                )



            }
                //Tab 1
                .tabItem {
                    VStack {
                        Image(systemName: "calendar")
                        Text("Add Event")
                    }
                }
                .tag(1)

            //Page 2
            VStack{

                Text("Your Events").underline()
                .font(.title)
                .padding(15)


                ForEach(eventNames.indices, id: \.self) { index in


                    VStack{
                        Text("Your event " + "\(self.eventNames[index])" + " is in " + "\(self.string[index])")
                        .onReceive(self.timer) { input in
                        self.differenceDate(numbers: index)

                        }
                    }


                }


            }

                //Tab 2
                .font(.title)
                .tabItem {
                    VStack {
                        Image(systemName: "flame.fill")
                        Text("Countdowns")
                    }
                }
                .tag(0)
        }
    }
}

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

I was wondering if there was a workaround or how to keep the timer firing while the tab changes or pause it when the tab changes and then start it again when the tab is swapped back over.


Solution

  • It needs to attach the .onReceive to the TabView and it will be received on all tabs, like

    TabView {
    ...
    // << all tab items here
    ...
    }
    .onReceive(self.timer) { _ in
        self.differenceDate()
    }
    

    and iterate indexes inside of handler

    func differenceDate() {
       for numbers in eventNames.indices {
         // current body here
       }
    }