1

I am curious if there is a way to get the timers to update with the UI (as they are now) even while someone is scrolling. Additionally, I want to make sure that as the UI updates each second the screen does not freeze. This question has been updated in response to the helpful answers I received previously.

struct ContentView: View {
    var body: some View {
        NavigationView{
            NavigationLink(destination: ScrollTest()){
                HStack {
                     Text("Next Screen")
                }
            }
        }
    }
}
struct ScrollTest: View {
    @ObservedObject var timer = SectionTimer(duration: 60)
    var body: some View {
        HStack{
            List{
                Section(header: TimerNavigationView(timer: timer)){
                    ForEach((1...50).reversed(), id: \.self) {
                        Text("\($0)…").onAppear(){
                            self.timer.startTimer()
                        }

                    }
                }
            }.navigationBarItems(trailing: TimerNavigationView(timer: timer))
        }

    }
}
struct TimerNavigationView: View {
    @ObservedObject var timer: SectionTimer
    var body: some View{
        HStack {
            Text("\(timer.timeLeftFormatted) left")
            Spacer()
        }
    }
}
class SectionTimer:ObservableObject {
    private var endDate: Date
    private var timer: Timer?
    var timeRemaining: Double {
        didSet {
            self.setRemaining()
        }
    }
    @Published var timeLeftFormatted = ""

    init(duration: Int) {
        self.timeRemaining = Double(duration)
        self.endDate = Date().advanced(by: Double(duration))
        self.startTimer()

    }

    func startTimer() {
        guard self.timer == nil else {
            return
        }
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
            self.timeRemaining = self.endDate.timeIntervalSince(Date())
            if self.timeRemaining < 0 {
                timer.invalidate()
                self.timer = nil
            }
        })
    }

    private func setRemaining() {
        let min = max(floor(self.timeRemaining / 60),0)
        let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
        self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
    }

    func endTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
}
balt2
  • 75
  • 5

1 Answers1

1

Although SwiftUI has a timer, I don't think using it is the right approach in this case.

Your model should be handling the timing for you.

It also helps if your view observes its model object directly rather than trying to observe a member of an array in a property of your observable.

You didn't show your SectionTimer, but this is what I created:

class SectionTimer:ObservableObject {
    private var endDate: Date
    private var timer: Timer?
    var timeRemaining: Double {
        didSet {
            self.setRemaining()
        }
    }

    @Published var timeLeftFormatted = ""

    init(duration: Int) {
        self.timeRemaining = Double(duration)
        self.endDate = Date().advanced(by: Double(duration))
        self.startTimer()

    }

    func startTimer() {
        guard self.timer == nil else {
            return
        }
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
            self.timeRemaining = self.endDate.timeIntervalSince(Date())
            if self.timeRemaining < 0 {
                timer.invalidate()
                self.timer = nil
            }
        })
    }

    private func setRemaining() {
        let min = max(floor(self.timeRemaining / 60),0)
        let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
        self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
    }

    func endTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
}

It uses a Date rather than subtracting from a remaining counter; this is more accurate as timer's don't tick at precise intervals. It updates a timeLeftFormatted published property.

To use it I made the following changes to your TimerNavigationView -

struct TimerNavigationView: View {
    @ObservedObject var timer: SectionTimer
    var body: some View{
        HStack {
            Text("\(timer.timeLeftFormatted) left")
            Spacer()
        }
    }
}

You can see how putting the timer in the model vastly simplifies your view.

You would use it via .navigationBarItems(trailing: TimerNavigationView(timer: self.test.timers[self.test.currentSection]))

Update

The updated code in the question helped demonstrate the issue, and I found the solution in this answer

When the scrollview is scrolling the mode of the current RunLoop changes and the timer is not triggered.

The solution is to schedule the timer in the common mode yourself rather than relying on the default mode that you get with scheduledTimer -

func startTimer() {
    guard self.timer == nil else {
        return
    }

    self.timer = Timer(timeInterval: 0.2, repeats: true) { (timer) in
        self.timeRemaining = self.endDate.timeIntervalSince(Date())
        if self.timeRemaining < 0 {
            timer.invalidate()
            self.timer = nil
        }
    }
    RunLoop.current.add(self.timer!, forMode: .common)
} 
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • Thanks for your response. There are actually a few other things going on in the timer navigation view that make the oversimplification a bit problematic. Nonetheless, I tried implementing this and the same issue of the navigation bar freezing the whole screen whenever the time was updated. – balt2 Apr 26 '20 at 12:09
  • Perhaps you could update your question with an [mcve], What do you mean by the whole screen freezing? I used this code in a list view and I had no issue with the list scrolling or selecting a detail view – Paulw11 Apr 26 '20 at 12:25
  • Thank you all for the comments. I created a minimal reproducible example (inside the question now). I am curious if there is a way to get the timers to update with the UI even while someone is scrolling and ensure that each update does not stop whatever process the user is currently doing (eg. scrolling). – balt2 Apr 26 '20 at 16:23