Swift嵌套ObservableObject无法更新Timer视图问题求助
Swift间隔定时器视图无法实时更新问题
我用Swift开发一款小型间隔定时器应用,遇到视图更新异常:视图仅在**间隔结束(计时归0并启动新间隔)**时才会更新。原本以为通过ObservableObject和@Published属性就能自动触发视图更新,显然我对Swift视图与模型的交互逻辑存在认知偏差。
项目结构
RunningTimerView:父视图,嵌套TimerClockViewTimerClockView:展示计时信息的子视图IntervalTimerViewModel:负责业务逻辑与视图、模型的连接MyTimer:处理核心计时逻辑的类
已尝试的排查方案
- 切换使用
Timer.scheduledTimer和Timer.publish两种定时器实现 - 在
ViewModel和MyTimer中使用不同变量存储计时数值 - 为计时过程添加监听器回调
- 在不同位置初始化定时器实例
- 切换使用初始化传参和
EnvironmentObject两种方式传递ViewModel
ChatGPT-4判定代码逻辑无问题,但故障依旧。以下是精简后的核心代码,恳请提供排查思路:
TimerClockView
struct TimerClockView: View { @ObservedObject var viewModel: IntervalTimerViewModel // some code var body: some View { ZStack { // some code if viewModel.isPlaying { Text("\(viewModel.myTimer.currentProcessingDurationString)") .font(.system(size: customSize * 0.25)) Text("\(viewModel.myTimer.getTotalInMinutes())") .offset(y: customSize * 0.2) .font(.system(size: customSize * 0.15)) } else { Text("\(viewModel.myTimer.getTotalInMinutes())") .font(.system(size: customSize * 0.25)) } } .foregroundStyle(currentColor) } }
RunningTimerView
struct RunningTimerView: View { @ObservedObject var viewModel: IntervalTimerViewModel // some code var body: some View { ZStack { VStack { // some code TimerClockView(viewModel: viewModel,customSize: UIScreen.main.bounds.width * 0.8) // some code } .background(currentColor) } } }
IntervalTimerViewModel
class IntervalTimerViewModel: ObservableObject { @Published var settings: Settings @Published var intervalProfile: IntervalProfile @Published var myTimer: MyTimer @Published var currentInterval: Interval? @Published var currentExercise: Exercise? @Published var volumeLevel: Float = 50 @Published var intervalMode: IntervalMode? @Published var currentActivityType: ActivityType = .none @Published var isPlaying: Bool = false var currentRound: Int = 1 var totalRounds: Int = 0 var totalExercises: Int = 0 var currentExerciseIndex: Int = 0 init(intervalProfile: IntervalProfile, settings: Settings) { self.settings = settings self.intervalProfile = intervalProfile self.myTimer = MyTimer() guard let interval = intervalProfile.intervals.first else { return } self.currentInterval = interval self.currentExercise = interval.exercises.first self.totalRounds = interval.roundCounter self.totalExercises = interval.exerciseCounter self.intervalMode = interval.intervalMode setCurrentTimer(interval: interval) setTimerListener() } func skipToNextExercise() -> Bool { // some code } func setCurrentTimer(interval: Interval?) { if let interval = interval { myTimer.setTimes(exercises: interval.exerciseCounter, repetition: interval.roundCounter, activityDuration: interval.activityDuration, pauseDuration : interval.pauseDuration, variableDuration: interval.variableDuration, currentProcessingDuration: interval.activityDuration, warmUpTime: settings.defaultWarmUpTime) } } func setTimerListener() { myTimer.onExercisePauseTimerEnd = { [weak self] in self?.setNextActivity()} } // for when one interval ends and the next starts func setNextActivity() { guard let interval = currentInterval else { return} print("\(currentActivityType)") switch currentActivityType { case .none: myTimer.currentProcessingDuration = settings.defaultWarmUpTime currentActivityType = .warmUp break case .activity: if currentExerciseIndex + 1 < interval.exercises.count { currentExerciseIndex += 1 currentExercise = interval.exercises[currentExerciseIndex] myTimer.currentProcessingDuration = interval.pauseDuration currentActivityType = .pause } else { if currentRound < totalRounds { currentRound += 1 currentExerciseIndex = 0 currentActivityType = .roundPause myTimer.currentProcessingDuration = interval.variableDuration } else { stopTimer() return } } break case .pause: currentExercise = interval.exercises[currentExerciseIndex] myTimer.currentProcessingDuration = interval.activityDuration currentActivityType = .activity break case .roundPause: currentExerciseIndex = 0 currentExercise = interval.exercises[currentExerciseIndex] myTimer.currentProcessingDuration = interval.activityDuration currentActivityType = .activity break case .warmUp: currentExerciseIndex = 0 currentExercise = interval.exercises[currentExerciseIndex] myTimer.currentProcessingDuration = interval.activityDuration currentActivityType = .activity break case .coolDown: print("") // not yet implemented // Method could be used to define cooldown after sesion for stretching or whatever break } startTimer() } func startTimer() { if !isPlaying { changeVolume() myTimer.startTimer() isPlaying = true } } func pauseTimer() { if isPlaying { changeVolume() myTimer.pauseTimer() isPlaying = false } } func continueTimer() { if !isPlaying { changeVolume() isPlaying = true myTimer.continueTimer() } } func stopTimer() { isPlaying = false changeVolume() myTimer.stopTimer() resetActivities() } // some code }
MyTimer
class MyTimer: ObservableObject { @Published var totalDuration: Int = 0 @Published var currentProcessingDuration: Int = 0 @Published var totalDurationString: String = "" @Published var currentProcessingDurationString: String = "" @Published var audioPlayer: AVAudioPlayer? private var lastCurrentProcessingDuration: Int = 0 var onExercisePauseTimerEnd: (() -> Void)? @Published var timer: Timer? init() { audioPlayer = AVAudioPlayer() audioPlayer?.volume = loadAudioVolume() } func setTimes(exercises: Int = 0, repetition: Int = 0, activityDuration: Int = 0, pauseDuration : Int = 0, variableDuration: Int = 0, currentProcessingDuration: Int = 0, warmUpTime: Int = 0) { let totalActivity = exercises * activityDuration * repetition let activityPause = exercises * pauseDuration * repetition let roundPause = (repetition - 1) * variableDuration self.totalDuration = totalActivity + activityPause + roundPause + warmUpTime self.currentProcessingDuration = currentProcessingDuration self.totalDurationString = getTotalInMinutes() self.currentProcessingDurationString = getCurrentProcessingInMinutes() } func startTimer() { configureAudioPlayer(audioName: "startIntervalSound") timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in DispatchQueue.main.async { self?.tick() } } } func tick() { print("currentProcessingDuration: \(currentProcessingDuration)") print("totalDuration: \(totalDuration)") if currentProcessingDuration == lastCurrentProcessingDuration { configureAudioPlayer(audioName: "beepStartSound") }else if currentProcessingDuration <= 4 && currentProcessingDuration > 0 { configureAudioPlayer(audioName: "beepSound") } if currentProcessingDuration == 0 { configureAudioPlayer(audioName: "beepEndSound") onExercisePauseTimerEnd?() } if currentProcessingDuration > 0 && totalDuration > 0 { currentProcessingDuration -= 1 totalDuration -= 1 } if(totalDuration == 0) { stopTimer() return } } func pauseTimer() { configureAudioPlayer(audioName: "pauseIntervalSound") timer?.invalidate() } func continueTimer() { configureAudioPlayer(audioName: "continueIntervalSound") lastCurrentProcessingDuration = currentProcessingDuration startTimer() } func stopTimer() { configureAudioPlayer(audioName: "stopIntervalSound") timer?.invalidate() timer = nil AudioServicesPlaySystemSound(1100) } func getTotalInMinutes() -> String { return getTimeInMinutes(processedTime: totalDuration) } func getCurrentProcessingInMinutes() -> String { return getTimeInMinutes(processedTime: currentProcessingDuration) } func getTimeInMinutes(processedTime: Int) -> String { return String(format: "%d:%02d", processedTime / 60, processedTime%60) } // some code }
内容的提问来源于stack exchange,提问作者Innoriam
相关产品推荐
相关产品推荐

