You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 02:05:54