21.7 C
New York
Saturday, September 7, 2024

ios – SwiftUI: Bug in timer when scrolling lap listing


Persevering with my research on SwiftUI, I’m growing a stopwatch. However I discovered a bug that I couldn’t remedy.

Steps to breed the error:

  • Begin the stopwatch (stopwatch is operating)
  • Click on the lap button a couple of instances (to fill the laps listing)
  • Scroll by means of the listing (up or down).
    The BUG is right here: the stopwatch pauses, I cease the scroll and the time begins once more

Observe the code:

import SwiftUI

struct StopwatchView: View {
    @State personal var timer: Timer? = nil
    @State personal var timeElapsed: TimeInterval = 0
    @State personal var isRunning: Bool = false
    @State personal var lapTimes: [TimeInterval] = []
    @State personal var lastLapTime: TimeInterval? = nil

    personal let timeFormatter: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .positional
        formatter.allowedUnits = [.minute, .second]
        formatter.zeroFormattingBehavior = [.pad]
        return formatter
    }()

    personal let millisecondsFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.minimumFractionDigits = 2
        formatter.maximumFractionDigits = 2
        formatter.minimumIntegerDigits = 2
        return formatter
    }()

    var physique: some View {
        VStack {
            Textual content("(formattedTime(timeElapsed))")
                .font(.system(dimension: 90, weight: .gentle))
                .padding(.horizontal, 10)
                .padding(.prime, 50)

            HStack {
                VStack(alignment: .main) {
                    Textual content("Quickest")
                        .font(.headline)
                        .foregroundColor(.inexperienced)
                    if let minLap = lapTimes.min() {
                        Textual content(formattedTime(minLap))
                            .foregroundColor(.white)
                            .padding(10)
                            .background(Shade.inexperienced)
                            .cornerRadius(5)
                    }
                }
                Spacer()
                VStack(alignment: .trailing) {
                    Textual content("Slowest")
                        .font(.headline)
                        .foregroundColor(.pink)
                    if let maxLap = lapTimes.max() {
                        Textual content(formattedTime(maxLap))
                            .foregroundColor(.white)
                            .padding(10)
                            .background(Shade.pink)
                            .cornerRadius(5)
                    }
                }
            }
            .padding(.horizontal, 30)
            .padding(.backside, 20)

            Listing {
                ForEach(lapTimes.indices, id: .self) { index in
                    HStack {
                        Textual content("Lap (lapTimes.rely - index)")
                        Spacer()
                        Textual content(formattedTime(lapTimes[index]))
                            .foregroundColor(colorForTime(at: index))
                    }
                }
            }
            .listStyle(PlainListStyle())
            .padding(.backside, 30)
            .onChange(of: lapTimes) { _ in
                scrollToTop()
            }

            Spacer()

            HStack(spacing: 30) {
                Button(motion: {
                    if isRunning {
                        stopTimer()
                    } else {
                        startTimer()
                    }
                }) {
                    Textual content(isRunning ? "Cease" : "Begin")
                        .foregroundColor(isRunning ? .pink : .inexperienced)
                        .font(.system(dimension: 24, weight: .daring))
                        .body(width: 120, top: 120)
                        .background(Circle().fill(Shade.black.opacity(0.1)))
                }

                Button(motion: {
                    if isRunning {
                        addLap()
                    } else {
                        resetTimer()
                    }
                }) {
                    Textual content(isRunning ? "Lap" : "Reset")
                        .foregroundColor(.black)
                        .font(.system(dimension: 24, weight: .daring))
                        .body(width: 120, top: 120)
                        .background(Circle().fill(Shade.black.opacity(0.1)))
                }
            }
            .padding(.backside, 50)
        }
    }

    personal func colorForTime(at index: Int) -> Shade {
        guard !lapTimes.isEmpty else { return .black }

        let maxTime = lapTimes.max() ?? 0
        let minTime = lapTimes.min() ?? 0

        if lapTimes[index] == maxTime {
            return .pink
        } else if lapTimes[index] == minTime {
            return .inexperienced
        } else {
            return .black
        }
    }

    personal func formattedTime(_ time: TimeInterval) -> String {
        let minutesAndSeconds = timeFormatter.string(from: time) ?? "00:00"
        let milliseconds = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
        return "(minutesAndSeconds):(String(format: "%02d", milliseconds))"
    }

    personal func startTimer() {
        isRunning = true
        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in
            timeElapsed += 0.01
        }
    }

    personal func stopTimer() {
        isRunning = false
        timer?.invalidate()
        timer = nil
    }

    personal func resetTimer() {
        timeElapsed = 0
        lapTimes.removeAll()
        lastLapTime = nil
    }

    personal func addLap() {
        if let lastLap = lastLapTime {
            let lapDuration = timeElapsed - lastLap
            lapTimes.insert(lapDuration, at: 0)
        } else {
            lapTimes.insert(timeElapsed, at: 0)
        }
        lastLapTime = timeElapsed
    }

    personal func scrollToTop() {
        DispatchQueue.major.async {
            if let scrollView = UIApplication.shared.home windows.first?.rootViewController?.view.subviews.first(the place: { $0 is UIScrollView }) as? UIScrollView {
                scrollView.setContentOffset(.zero, animated: true)
            }
        }
    }
}

#Preview {
    StopwatchView()
}

This code is in its preliminary part, I’ll modularize it, create views for every part to divide the tasks… however for now I need assistance to resolve the battle between the scroll and the timer.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles