SwiftUI中如何在TimerViews与AudioPlayerViews间传递计时器时长
解决SwiftUI中TimerViews到AudioPlayerViews的计时值传递问题
你的核心问题是TimerViews和AudioPlayerViews各自创建了独立的TimerViewModel实例,导致两个页面的计时数据无法共享。下面是具体的修复方案,同时优化倒计时逻辑避免显示错误:
步骤1:共享TimerViewModel实例
在顶层ContentView中创建唯一的TimerViewModel实例,并传递给所有需要使用的子页面,确保数据同步:
// ContentView.swift import SwiftUI struct ContentView: View { @StateObject private var sharedTimer = TimerViewModel() // 全局共享的ViewModel var body: some View { NavigationView { VStack { // 将共享ViewModel传递给TimerViews NavigationLink(destination: TimerViews(timerViewModel: sharedTimer)) { Text("Set Timer") } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
步骤2:修改TimerViews接收外部ViewModel
删除TimerViews内部的@StateObject声明,改为从外部接收共享的ViewModel,并在跳转时传递给AudioPlayerViews:
// TimerViews.swift import SwiftUI import Foundation struct TimerViews: View { @ObservedObject var timerViewModel: TimerViewModel // 从外部传入共享实例 @State private var audioPlayerModel = AudioPlayerModel() var body: some View { NavigationView { ZStack { Image("BgColor") .resizable() .aspectRatio(contentMode: .fill) .edgesIgnoringSafeArea(.all) VStack { // 保留原Picker逻辑不变 if timerViewModel.isPickerVisible { HStack { Picker("Hour", selection: $timerViewModel.selectedHours) { ForEach(0..<24) { Text("\(self.timerViewModel.hours[$0]) hour") } } Picker("Min", selection: $timerViewModel.selectedMins) { ForEach(0..<60) { Text("\(self.timerViewModel.minutes[$0]) min") .foregroundColor(.white) } } Picker("Sec", selection: $timerViewModel.selectedSecs) { ForEach(0..<60) { Text("\(self.timerViewModel.seconds[$0]) sec") .foregroundColor(.white) } } } .accentColor(.white) } // 保留原计时预览不变 HStack { Text(String(format: "%02d:%02d:%02d", timerViewModel.calculate() / 3600, (timerViewModel.calculate() / 60) % 60, timerViewModel.calculate() % 60)) .font(.system(size: 50, weight: .medium, design: .rounded)) .padding() } HStack { // 跳转时传递共享ViewModel给AudioPlayerViews NavigationLink(destination: AudioPlayerViews(timerModel: timerViewModel)) { Text("Start") } VStack { Button { timerViewModel.restart() timerViewModel.audioPlayer.setupAudio() } label: { Text("Restart") } } } } .foregroundColor(.white) } .onDisappear { timerViewModel.timer?.invalidate() } } } } struct TimerViews_Previews: PreviewProvider { static var previews: some View { TimerViews(timerViewModel: TimerViewModel()) } }
步骤3:修改AudioPlayerViews接收外部ViewModel
删除AudioPlayerViews内部的TimerViewModel实例,改为接收从TimerViews传递过来的共享实例,同时修正计时显示的格式错误:
// AudioPlayerViews.swift import SwiftUI import AVFoundation struct AudioPlayerViews: View { @ObservedObject var audioManager = AudioPlayerModel() @ObservedObject var timerModel: TimerViewModel // 接收共享的ViewModel @State private var showingCredits = false var body: some View { GeometryReader { geometry in ZStack { Image(audioManager.currentSong?.image ?? "1") .resizable() .edgesIgnoringSafeArea(.all) .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center) VStack { // 保留原Credit Info逻辑不变 HStack { Button { showingCredits = true } label: { Image(systemName: "info.circle") .foregroundColor(.white) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.trailing) } .sheet(isPresented: $showingCredits) { Text(audioManager.currentSong?.copyrightInfo ?? "No Info") .presentationDetents([.fraction(0.12)]) .presentationDragIndicator(.visible) .foregroundColor(.black) .font(.caption2) .padding(.top) .padding(.leading) .padding(.trailing) } } Spacer() // 修正计时显示格式,和TimerViews保持一致 HStack { Text(String(format: "%02d:%02d:%02d", timerModel.calculate() / 3600, (timerModel.calculate() / 60) % 60, timerModel.calculate() % 60)) .font(.system(size: 25, weight: .medium, design: .rounded)) .padding() } // 保留原音频按钮逻辑不变 HStack (spacing: 20) { Button { audioManager.playPrevSong() } label: { Image(systemName: "backward.circle.fill") .resizable() .frame(width: 40, height: 40) .scaledToFit() } Button { if audioManager.isPlaying { audioManager.audioPlayer?.pause() timerModel.pause() } else { audioManager.playAudio() timerModel.start() } audioManager.isPlaying.toggle() } label: { Image(systemName: audioManager.isPlaying ? "pause.circle.fill" : "play.circle.fill") .resizable() .frame(width: 60, height: 60) .scaledToFit() } .tint(.black) Button { audioManager.playNextSong() } label: { Image(systemName: "forward.circle.fill") .resizable() .frame(width: 40, height: 40) .scaledToFit() } } // 保留原歌曲标题逻辑不变 VStack { Text(audioManager.currentSong?.songName ?? "No Name") .bold() .font(.title2) .frame(maxWidth: .infinity, alignment: .center) Text(audioManager.currentSong?.composer ?? "No Composer") .font(.caption) .frame(maxWidth: .infinity, alignment: .center) } .padding(.bottom) } .foregroundColor(.white) } } } } struct AudioPlayerViews_Previews: PreviewProvider { static var previews: some View { AudioPlayerViews(timerModel: TimerViewModel()) } }
步骤4:优化TimerViewModel的倒计时逻辑
原倒计时逻辑直接修改selectedSecs会导致时分秒显示错误,改为用剩余总秒数管理,并在倒计时结束时自动停止音频:
// TimerViewModel.swift import Foundation class TimerViewModel: ObservableObject { @Published var hours = Array(0...23) @Published var minutes = Array(0...59) @Published var seconds = Array(0...59) @Published var selectedHours = 0 @Published var selectedMins = 0 @Published var selectedSecs = 5 @Published var isRunning = false @Published var isPickerVisible = true @Published var timer: Timer? @Published var audioPlayer = AudioPlayerModel() @Published var remainingSeconds: Int = 0 // 新增:管理剩余总秒数 func calculate() -> Int { let totalSecs = selectedSecs + selectedMins * 60 + selectedHours * 3600 remainingSeconds = totalSecs return totalSecs } func restart() { timer?.invalidate() isRunning = false isPickerVisible = true selectedHours = 0 selectedMins = 0 selectedSecs = 0 remainingSeconds = 0 audioPlayer.pauseAudio() } func start() { isRunning = true remainingSeconds = calculate() // 启动前初始化剩余秒数 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in guard let self = self else { return } self.remainingSeconds -= 1 // 倒计时结束时停止音频并重置 if self.remainingSeconds <= 0 { self.restart() self.audioPlayer.stopAudio() } // 更新时分秒用于界面显示 self.selectedHours = self.remainingSeconds / 3600 self.selectedMins = (self.remainingSeconds % 3600) / 60 self.selectedSecs = self.remainingSeconds % 60 } } func pause() { isRunning = false timer?.invalidate() } }
最终效果说明
- 用户在TimerViews设置的时长会通过共享的TimerViewModel同步到AudioPlayerViews,打开AudioPlayerViews时会直接显示设置好的时长。
- 点击play按钮后,倒计时和音频同步启动。
- 倒计时结束时,音频会自动停止,计时器重置。
内容的提问来源于stack exchange,提问作者Amelia Citra
相关产品推荐
相关产品推荐

