我想创建一个带有计时器的音频播放器。动作顺序如下:
用户使用选择器(TimerViews)选择计时器持续时间。
当用户点击计时器上的“开始”时,页面将重定向到音频播放器页面(AudioPlayerViews)。
在AudioPlayerViews页面上,计时器和音频不会立即启动。相反,计时器将显示用户在 TimerViews 页面上输入的值。
当用户单击 AudioPlayerViews 上的“播放”时,倒计时器和歌曲将开始。
当倒计时完成或到达00:00:00时,歌曲将自动停止。
我坚持将计时器值从 TimerViews 页面传递到 AudioPlayerViews 页面。
有人可以帮助我吗?这是到目前为止的代码。
//
// ContentView.swift
//
import SwiftUI
struct ContentView: View {
@State private var isTimerVisible = false
@StateObject private var timer = TimerViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: TimerViews()) {
Text("Set Timer")
}
if isTimerVisible {
AudioPlayerViews()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// TimerViews.swift
//
import SwiftUI
import Foundation
struct TimerViews: View {
@StateObject private var timerViewModel = TimerViewModel()
@State private var audioPlayerModel = AudioPlayerModel()
var body: some View {
NavigationView {
ZStack {
//MARK: Background
Image("BgColor")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
VStack {
//MARK: Timer 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 {
//MARK: Timer Preview
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 {
NavigationLink(destination: AudioPlayerViews()) {
Text("Start")
}
VStack {
//MARK: Restart Timer Button
Button {
timerViewModel.restart()
timerViewModel.audioPlayer.setupAudio()
} label: {
Text("Restart")
}
}
}
}
.foregroundColor(.white)
}
.onDisappear {
// Invalidate the timer when the view disappears
timerViewModel.timer?.invalidate()
}
}
}
}
struct TimerViews_Previews: PreviewProvider {
static var previews: some View {
TimerViews()
}
}
//
// 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()
func calculate() -> Int {
let totalSecs = selectedSecs + selectedMins * 60 + selectedHours * 3600
return totalSecs
}
func restart() {
timer?.invalidate()
isRunning = false
isPickerVisible = true
selectedHours = 0
selectedMins = 0
selectedSecs = 0
audioPlayer.pauseAudio()
}
func start() {
isRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.selectedSecs -= 1
if self.calculate() <= 0 {
self.restart()
}
}
}
func pause() {
isRunning = false
timer?.invalidate()
}
}
//
// AudioPlayerViews.swift
//
import SwiftUI
import AVFoundation
struct AudioPlayerViews: View {
@ObservedObject var audioManager = AudioPlayerModel()
@ObservedObject var timerModel = TimerViewModel()
@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 {
//MARK: 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()
//MARK: Timer Countdown
HStack {
Text(String(format: "%02d:%02d:%02d", timerModel.selectedHours / 3600, timerModel.selectedMins / 60, timerModel.selectedSecs % 60))
.font(.system(size: 25, weight: .medium, design: .rounded))
.padding()
}
//MARK: Audio Button
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()
}
}
//MARK: Song Title
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()
}
}
//
// AudioPlayerModel.swift
//
import AVFoundation
struct Song: Identifiable {
let id = UUID()
let songName: String
let composer: String
let audioFileName: String
let image: String
let copyrightInfo: String
}
class AudioPlayerModel: NSObject, ObservableObject, AVAudioPlayerDelegate {
@Published private var timerView = ContentView()
private var currentIndex = 0
@Published var audioPlayer : AVAudioPlayer?
@Published var isPlaying = false
@Published var currentTime: TimeInterval = 0
@Published var songs: [Song] = [
Song(songName: "Signal to Noise", composer: "Scott Buckley", audioFileName: "Signal to Noise.mp3", image: "1", copyrightInfo: "www.scottbuckley.com.au Music promoted by https://www.chosic.com/free-music/all/ Creative Commons CC BY 4.0 https://creativecommons.org/licenses/by/4.0/ Image: https://www.pxfuel.com/"),
Song(songName: "Arnor", composer: "Alex Productions", audioFileName: "Arnor.mp3", image: "2", copyrightInfo: "https://onsound.eu/ Music promoted by https://www.chosic.com/free-music/all/ Creative Commons CC BY 3.0 https://creativecommons.org/licenses/by/3.0/ Image: https://www.pxfuel.com/"),
]
@Published var currentSong: Song?
override init() {
super .init()
currentSong = self.songs.first
setupAudio()
}
func setupAudio() {
guard let currentSong = currentSong else { return }
if let audioPlayer = audioPlayer {
audioPlayer.stop()
audioPlayer.delegate = nil
}
if let audioFileURL = Bundle.main.url(forResource: currentSong.audioFileName, withExtension: nil) {
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioFileURL)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
isPlaying = false
} catch {
print("Error: \(error)")
}
}
}
func autoRepeat() {
guard let currentSong = currentSong else { return }
if let audioPlayer = audioPlayer {
audioPlayer.stop()
audioPlayer.delegate = nil
}
if let audioFileURL = Bundle.main.url(forResource: currentSong.audioFileName, withExtension: nil) {
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioFileURL)
audioPlayer?.delegate = self
audioPlayer?.play()
} catch {
print("Error: \(error)")
}
}
}
func playNextSong() {
if currentIndex == songs.count - 1 {
currentIndex = 0 // Kembali ke lagu pertama jika mencapai akhir
} else {
currentIndex += 1
}
currentSong = songs[currentIndex]
autoRepeat()
}
func stopAudio() {
audioPlayer?.stop()
isPlaying = false
}
func playPrevSong() {
currentIndex -= 1
if currentIndex < 0 {
currentIndex = songs.count - 1
}
currentSong = songs[currentIndex]
autoRepeat()
}
func playAudio() {
audioPlayer?.play()
}
func pauseAudio() {
audioPlayer?.pause()
}
func formatTime(_ timeInterval: TimeInterval) -> String {
let minutes = Int(timeInterval / 60)
let seconds = Int(timeInterval.truncatingRemainder(dividingBy: 60))
return String(format: "%02d: %02d", minutes, seconds)
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
isPlaying = true
currentTime = 0
playNextSong()
}
}
为了引用和传递已在 viewModel 中发布的数据,您不应在每个视图中对其进行初始化。相反,将 viewModel 数据初始化为 ContentView 中的
@StateObject
,然后使用 @ObservedObject var timerModel: TimerViewModel
传递它。另外,当连续使用 NavigationView
时,请记住 NavigationView 本身携带数据。因此,您应该删除子视图中声明的NavigationView
,以确保单个数据层次结构有效运行。高亮需要修改部分的代码如下:
内容视图
struct ContentView: View {
@State private var isTimerVisible = false
@StateObject private var timer = TimerViewModel()
var body: some View {
// As NavigationView has been deprecated, it's recommended to use NavigationStack instead
NavigationStack {
VStack {
// add parameter
NavigationLink(destination: TimerViews(timerViewModel: timer)) {
Text("Set Timer")
}
if isTimerVisible {
// add parameter
AudioPlayerViews(timerModel: timer)
}
}
}
}
}
计时器视图
import SwiftUI
import Foundation
struct TimerViews: View {
// Receive it as a parameter from the parent view instead of initializing
@ObservedObject var timerViewModel: TimerViewModel
@State private var audioPlayerModel = AudioPlayerModel()
var body: some View {
// remove NavigationView
ZStack {
Color.purple
//MARK: Background
Image("BgColor")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
VStack {
//MARK: Timer 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 {
//MARK: Timer Preview
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 {
// add parameter
NavigationLink(destination: AudioPlayerViews(timerModel: timerViewModel)) {
Text("Start")
}
VStack {
//MARK: Restart Timer Button
Button {
timerViewModel.restart()
timerViewModel.audioPlayer.setupAudio()
} label: {
Text("Restart")
}
}
}
}
.foregroundColor(.white)
}
.onDisappear {
// Invalidate the timer when the view disappears
timerViewModel.timer?.invalidate()
}
}
}
音频播放器视图
import SwiftUI
import AVFoundation
struct AudioPlayerViews: View {
@ObservedObject var audioManager = AudioPlayerModel()
// same change as TimerView
@ObservedObject var timerModel: TimerViewModel
@State private var showingCredits = false
var body: some View {
// AudioPlayerViews code ...
}
}
此外,您似乎通过分别传递小时、分钟和秒来混合方法,而计时器仅转换秒。所以,如果你按照我告诉你的方式修复它,它可能只会通过并在“秒”内工作。您需要检查单位以确定如何使用计时器。
func start() {
isRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else { return }
// your only using selectedSecs here
self.selectedSecs -= 1
if self.calculate() <= 0 {
self.restart()
}
}
}
很抱歉我无法解决您的所有问题,但希望我的解决方案对您有所帮助。祝你好运。