SwiftUI多计时器管理:循环生成Timer实例的问题排查与解决方案咨询
Let's break down your issues and fix them step by step. First, let's understand why you're seeing these problems:
Why Your Current Code Isn't Working
Timers running N times faster: You're using a single shared
TimerDataViewModelfor all 10 timers. Every time aCurrentDateViewappears, it callstimerData.start()—which creates a brand newTimereach time. So after 10 views load, you have 10 separate Timers all firing every second and updating the samesecondsOriginalvariable. That's why your time is ticking 10x faster than expected.Can't stop individual timers: Again, since all timers share the same view model, there's no way to distinguish which timer corresponds to which stop button. When you call
stop(), you're only invalidating the last created Timer (since eachstart()overwrites thetimerproperty), and all views are bound to the same time state anyway.
The Fix: Individual Timer Models
The solution is to give each timer its own state and Timer instance. We'll create a model for a single timer, then manage an array of these models in a parent view model.
Step 1: Create a Single Timer Model
This class handles all logic for one timer—starting, stopping, and tracking elapsed time. We'll use [weak self] to avoid memory leaks, and guard against duplicate starts:
import Foundation import SwiftUI class SingleTimerModel: ObservableObject { @Published var secondsElapsed = 0.0 @Published var minutesElapsed = 0.0 @Published var hoursElapsed = 0.0 private var timer: Timer? private var startTime: Double = 0.0 func start() { // Don't start a timer if one's already running guard timer == nil else { return } startTime = secondsElapsed timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in guard let self = self else { return } self.startTime += 1 let totalSeconds = Int(self.startTime) self.secondsElapsed = Double(totalSeconds % 60) self.minutesElapsed = Double(totalSeconds / 60 % 60) self.hoursElapsed = Double(totalSeconds / 3600 % 24) } } func stop() { timer?.invalidate() timer = nil // Reset so we can start again later if needed } }
Step 2: Create a Timer Manager View Model
This parent view model will hold an array of SingleTimerModel instances, making it easy to manage multiple timers:
class TimerManagerViewModel: ObservableObject { @Published var timers: [SingleTimerModel] = [] // Initialize with the number of timers you need init(timerCount: Int) { for _ in 0..<timerCount { timers.append(SingleTimerModel()) } } }
Step 3: Update Your Views
Now we'll adjust the views to use the new models. Each CurrentDateView will bind to a single SingleTimerModel, and the stop button will target that specific model:
import SwiftUI struct ContentView: View { // Initialize with 10 timers @StateObject var timerManager = TimerManagerViewModel(timerCount: 10) var body: some View { ScrollView { // Loop through our array of timers ForEach(0..<timerManager.timers.count, id: \.self) { index in CurrentDateView(timerModel: timerManager.timers[index]) Button(action: { // Stop ONLY the timer at this index timerManager.timers[index].stop() }, label: { Text("STOP THIS TIMER") .foregroundColor(.white) .padding() .background(Color.red) .cornerRadius(8) }) .padding(.bottom, 15) } } .padding() } } struct CurrentDateView: View { @ObservedObject var timerModel: SingleTimerModel var body: some View { Text("\(Int(timerModel.hoursElapsed), specifier: \"%02d\"):\(Int(timerModel.minutesElapsed), specifier: \"%02d\"):\(Int(timerModel.secondsElapsed), specifier: \"%02d\")") .fontWeight(.bold) .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(8) .onAppear { // Start this specific timer when the view loads timerModel.start() } } }
Key Improvements
- No more overlapping timers: Each timer has its own
SingleTimerModel, so starting one doesn't affect the others. - Individual control: Each stop button targets exactly the timer it's paired with—no more global stop that affects everything.
- Memory safety: Using
[weak self]in the Timer closure prevents memory leaks, and we reset thetimerproperty when stopping so we can restart if needed.
Optional: Better ForEach Handling
If you ever need to add/remove timers dynamically (instead of a fixed 10), give SingleTimerModel an id property:
class SingleTimerModel: ObservableObject { let id = UUID() // Add this // ... rest of your code }
Then update the ForEach to use this id:
ForEach(timerManager.timers) { timerModel in CurrentDateView(timerModel: timerModel) Button(action: { timerModel.stop() }) { Text("STOP THIS TIMER") } }
This ensures SwiftUI correctly tracks each timer view even when the array changes.
内容的提问来源于stack exchange,提问作者NARIO

