Learn Courses My Dashboard

Why does my preview show this error 'Missing argument for parameters 'SongVM' in call'

I’m trying to make a music app but my preview shows this error how can I fix it?

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView()
    }
}

Here is an image of it

@Air_Walks

Previews require some data to make the View work in the Canvas or they require an instance of the ViewModel in order to provide some data.

Can you paste in your code for the whole DetailView.swift file so that we can advise how best to go about creating some test data or a means of accessing the App data.

Ok thanks for that here is the code

import SwiftUI

struct DetailView: View {
    
    var songVM: SongViewModel
    let song: Song
    @State private var value: Double = 0.0
    @State private var isEditing: Bool = false
    @Environment(\.dismiss) var dismiss
    var isPreview: Bool = false
    @EnvironmentObject var audioManager: AudioManager
    
    var body: some View {
            VStack {
                Image(song.imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .cornerRadius(10)
                    .frame(width: 180, height: 180)
                    .padding()
                
                Text(song.name)
                    .font(.title)
                    .fontWeight(.medium)
                HStack {
                 
                    if let player = audioManager.player {
                        
                        
                        VStack (spacing: 5) {
                            // Playback timeline
                            Slider(value: $value, in: 0...player.duration) { editing in
                                
                                print("editing", editing)
                                isEditing = editing
                                
                                if !editing {
                                    player.currentTime = value
                                }
                            }
                            .accentColor(Color.black)
                            
                            
                            // Playback time
                            HStack {
                                Text(DateComponentsFormatter.positional.string(from: player.currentTime) ?? "0:00")
                                
                                Spacer()
                                
                                Text(DateComponentsFormatter.positional.string(from: player.duration - player.currentTime) ?? "0:00")
                            }
                            .font(.caption)
                            .foregroundColor(Color.white)
                        }
                        
                        // MARK: Playback controls
                        HStack {
                                // MARK: Repeat Button
                                let color: Color = audioManager.isLooping ? .green : .white
                            PlaybackControlButton(systemName: "repeat", color: color) {
                                    audioManager.toggleLoop()
                            }
                            Spacer()
                            // MARK: Backward Button
                            PlaybackControlButton(systemName: "gobackward.10") {
                                player.currentTime -= 10
                            }
                            Spacer()
                            // MARK: Play/Pause Button
                            PlaybackControlButton(systemName: audioManager.isPlaying ? "pause.circle.fill" : "play.circle.fill", fontSize: 44) {
                                audioManager.playPause()
                            }
                            Spacer()
                            // MARK: Forward Button
                            PlaybackControlButton(systemName: "goforward.10") {
                                player.currentTime += 10
                            }
                            Spacer()
                            // MARK: Stop Button
                            PlaybackControlButton(systemName: "stop.fill") {
                                audioManager.stop()
                                dismiss()
                        }
                    }
                }
            }
        }
    }
}



struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView(songVM: SongViewModel, song: Song)
    }
}


Does your SongViewModel conform to ObservableObject and does it have a @Published variable which is a list of songs?

If so then consider injecting that SongViewModel into the parent View with
.environmentObject(SongViewModel())

(edit) By Parent View I mean the View that conforms to App such as this example:

@main
struct coder3000App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(ContentViewModel())
        }
    }
}

which will allow you to access the ViewModel in your child views using:
@EnviromentObject var songVM: SongViewModel

Then in your Preview add the enviromentObject(SongViewModel()) to the View like this:

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView()
            .enviromentObject(SongViewModel())
    }
}

You could then remove the variable you currently have:

var songVM: SongViewModel

Does that make sense?

You can have more than one environmentObject injected into the parent View.

(edit)
Regarding the song you are passing in to DetailView the solution for the Preview is to create a static variable in your Song model that creates an instance of test data that you can inject into the preview for the variable song.

This is my SongViewModel

import Foundation

final class SongViewModel: ObservableObject {
    private(set) var song: Song
    init(song: Song) {
        self.song = song
    }
}
struct Song: Identifiable {
    let id = UUID()
    let imageName: String
    let name: String

}

let songs = [
    Song(imageName: "song1", name: "DJ NYK"),
    Song(imageName: "song2", name: "John Wick Deconsecrated")
]

OK what about if you changed it to this:

import Foundation

final class SongViewModel: ObservableObject {
    @Published var songs = [Song]()
    let testSongs = [
        Song(imageName: "song1", name: "DJ NYK"),
        Song(imageName: "song2", name: "John Wick Deconsecrated")
    ]

    init() {
        self.songs = testSongs
    }
}

struct Song: Identifiable {
    let id = UUID()
    let imageName: String
    let name: String
}

My reasoning is that you are dealing with more than one song so they should be in an array hence I named it songs (plural) so you know that there is more than one.

I hope that I am not confusing you.

@Air_Walks

It looks like you may have your original code still there so you now have a duplicate.

Yes that was a mistake but for reference here is my MusicAppApp.swift file code after I tried your code into the SongViewModel

import SwiftUI

@main
struct MusicAppApp: App {
    
    @StateObject var audioManager = AudioManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(audioManager)
        }
    }
}


OK you probably should also post your code that you have for your initial View where you list your songs. I’m trying to replicate what you have here (kind of) in order to help out.

It may also help me if you post your AudioManager code too.

Ok let me post all of that then

So this is all my code for my MusicAppApp.swift file

import SwiftUI

@main
struct MusicAppApp: App {
    @StateObject var audioManager = AudioManager()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(SongViewModel())
        }
    }
}

This is all my code for my SongViewModel file

import Foundation

final class SongViewModel: ObservableObject {
    @Published var songs = [Song]()
    let testSongs = [
        Song(imageName: "song1", name: "DJ NYK"),
        Song(imageName: "song2", name: "John Wick Deconsecrated")
    ]

    init() {
        self.songs = testSongs
    }
}

struct Song: Identifiable {
    let id = UUID()
    let imageName: String
    let name: String
}

This is all my code for my ContentView file

import SwiftUI

struct ContentView: View {
    var body: some View {
        List(songs) { song in
                songRow(song: song)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct songRow: View {
    
    let song: Song
    
    var body: some View {
        HStack {
            Image(song.imageName)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 60, height: 60)
                .cornerRadius(6)
            
            
            VStack (alignment: .leading) {
                Text(song.name)
                    .font(.system(size: 21, weight: .medium, design: .default))
            }
            .padding()
        }
    }
}

This is all my code for my DetailView file

import SwiftUI

struct DetailView: View {
    
    let song: Song
    @State private var value: Double = 0.0
    @State private var isEditing: Bool = false
    @Environment(\.dismiss) var dismiss
    var isPreview: Bool = false
    @EnvironmentObject var audioManager: AudioManager
    
    var body: some View {
            VStack {
                Image(song.imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .cornerRadius(10)
                    .frame(width: 180, height: 180)
                    .padding()
                
                Text(song.name)
                    .font(.title)
                    .fontWeight(.medium)
                HStack {
                 
                    if let player = audioManager.player {
                        
                        
                        VStack (spacing: 5) {
                            // Playback timeline
                            Slider(value: $value, in: 0...player.duration) { editing in
                                
                                print("editing", editing)
                                isEditing = editing
                                
                                if !editing {
                                    player.currentTime = value
                                }
                            }
                            .accentColor(Color.black)
                            
                            
                            // Playback time
                            HStack {
                                Text(DateComponentsFormatter.positional.string(from: player.currentTime) ?? "0:00")
                                
                                Spacer()
                                
                                Text(DateComponentsFormatter.positional.string(from: player.duration - player.currentTime) ?? "0:00")
                            }
                            .font(.caption)
                            .foregroundColor(Color.white)
                        }
                        
                        // MARK: Playback controls
                        HStack {
                                // MARK: Repeat Button
                                let color: Color = audioManager.isLooping ? .green : .white
                            PlaybackControlButton(systemName: "repeat", color: color) {
                                    audioManager.toggleLoop()
                            }
                            Spacer()
                            // MARK: Backward Button
                            PlaybackControlButton(systemName: "gobackward.10") {
                                player.currentTime -= 10
                            }
                            Spacer()
                            // MARK: Play/Pause Button
                            PlaybackControlButton(systemName: audioManager.isPlaying ? "pause.circle.fill" : "play.circle.fill", fontSize: 44) {
                                audioManager.playPause()
                            }
                            Spacer()
                            // MARK: Forward Button
                            PlaybackControlButton(systemName: "goforward.10") {
                                player.currentTime += 10
                            }
                            Spacer()
                            // MARK: Stop Button
                            PlaybackControlButton(systemName: "stop.fill") {
                                audioManager.stop()
                                dismiss()
                        }
                    }
                }
            }
        }
    }
}



struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView()
            .enviromentObject(SongViewModel())
    }
}

This is all my code for my PlaybackControlButton file

import SwiftUI

struct PlaybackControlButton: View {
    
    var systemName: String = "play"
    var fontSize: CGFloat = 24
    var color: Color = .white
    var action: () -> Void
    
    var body: some View {
        Button {
            action()
        } label: {
            Image(systemName: systemName)
                .font(.system(size: fontSize))
                .foregroundColor(color)
        }

    }
}

struct PlaybackControlButton_Previews: PreviewProvider {
    static var previews: some View {
        PlaybackControlButton(action: {})
            .preferredColorScheme(.dark)
    }
}

This is all my code for my AudioManager file

import Foundation
import AVKit


final class AudioManager: ObservableObject {
    static let shared = AudioManager()
    var player: AVAudioPlayer?
    @Published private(set) var isPlaying: Bool = false {
        didSet {
            print("isPlaying", isPlaying)
        }
    }
    @Published private(set) var isLooping: Bool = false
    
    func startPlayer(track: String, isPreview: Bool = false) {
        guard let url = Bundle.main.url(forResource: track, withExtension: "mp3") else {
            print("Resource not found: \(track)")
            return
        }
        
        do {
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            player = try AVAudioPlayer(contentsOf: url)
            
            if isPreview {
                player?.prepareToPlay()
            } else {
                player?.play()
                isPlaying = true
            }
        }
        catch {
            print("Fail to initialise player")
        }
    }
    
    func playPause() {
        guard let player = player else {
            print("Instance of audio player not found")
            return
        }
        
        if player.isPlaying {
            player.pause()
            isPlaying = false
        } else {
            player.play()
            isPlaying = true
        }
    }
    
    func  stop() {
        guard let player = player else {return}
        
        if player.isPlaying {
            player.stop()
            isPlaying = false
        }
    }
    
    func toggleLoop() {
        guard let player = player else {return}
    
    player.numberOfLoops = player.numberOfLoops == 0 ? -1 : 0
    isLooping = player.numberOfLoops != 0
    print("IsLooping", isLooping)
    }
}

And finally this is all my code for my Extensions file

import Foundation

extension DateComponentsFormatter {
    static let abbreviated: DateComponentsFormatter = {
        print("Initialising DataComponentsFormatter.abbreviated")
        let formatter = DateComponentsFormatter()
        
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        
        return formatter
    }()
    
    static let positional: DateComponentsFormatter = {
        print("Initialising DataComponentsFormatter.positional")
        let formatter = DateComponentsFormatter()
        
        formatter.allowedUnits = [.minute, .second]
        formatter.unitsStyle = .positional
        formatter.zeroFormattingBehavior = .pad
        
        return formatter
    }()
}

OK the good news is that I have no errors after some tidying up.

I’ll just add a couple of random images to stop the complier complaining about there not being a song1 or song2 in the asset catalogue.

I’ve got the DetailView to display but none of the Audio Control buttons are showing up so my guess is that there is something not right with the AudioManager and at this stage I can’t figure out what the issue is.

This is the code in ContentView to present DetailView.

struct ContentView: View {
    @EnvironmentObject var model: SongViewModel
    @State private var isDetailShowing = false

    var body: some View {
        VStack {
            List(model.songs) { song in
                    songRow(song: song)
                    .onTapGesture {
                        isDetailShowing = true
                    }
                    .sheet(isPresented: $isDetailShowing) {
                        DetailView(song: song)
                    }
            }
        }

    }
}

Could you send your code for the DetailView because mine still has errors?

DetailView as follows:

struct DetailView: View {

    @EnvironmentObject var songVM: SongViewModel
    let song: Song
    @State private var value: Double = 0.0
    @State private var isEditing: Bool = false
    @Environment(\.dismiss) var dismiss
    var isPreview: Bool = false
    @EnvironmentObject var audioManager: AudioManager

    var body: some View {
            VStack {
                Image(song.imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .cornerRadius(10)
                    .frame(width: 180, height: 180)
                    .padding()

                Text(song.name)
                    .font(.title)
                    .fontWeight(.medium)
                HStack {

                    if let player = audioManager.player {


                        VStack (spacing: 5) {
                            // Playback timeline
                            Slider(value: $value, in: 0...player.duration) { editing in

                                print("editing", editing)
                                isEditing = editing

                                if !editing {
                                    player.currentTime = value
                                }
                            }
                            .accentColor(Color.black)


                            // Playback time
                            HStack {
                                Text(DateComponentsFormatter.positional.string(from: player.currentTime) ?? "0:00")

                                Spacer()

                                Text(DateComponentsFormatter.positional.string(from: player.duration - player.currentTime) ?? "0:00")
                            }
                            .font(.caption)
                            .foregroundColor(Color.white)
                        }

                        // MARK: Playback controls
                        HStack {
                                // MARK: Repeat Button
                                let color: Color = audioManager.isLooping ? .green : .white
                            PlaybackControlButton(systemName: "repeat", color: color) {
                                    audioManager.toggleLoop()
                            }
                            Spacer()
                            // MARK: Backward Button
                            PlaybackControlButton(systemName: "gobackward.10") {
                                player.currentTime -= 10
                            }
                            Spacer()
                            // MARK: Play/Pause Button
                            PlaybackControlButton(systemName: audioManager.isPlaying ? "pause.circle.fill" : "play.circle.fill", fontSize: 44) {
                                audioManager.playPause()
                            }
                            Spacer()
                            // MARK: Forward Button
                            PlaybackControlButton(systemName: "goforward.10") {
                                player.currentTime += 10
                            }
                            Spacer()
                            // MARK: Stop Button
                            PlaybackControlButton(systemName: "stop.fill") {
                                audioManager.stop()
                                dismiss()
                        }
                    }
                }
            }
        }
    }
}


struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView(song: Song.testSong)
            .environmentObject(SongViewModel())
            .environmentObject(AudioManager())
    }
}

My preview shows up with this error

Ah yes, you need this:

struct Song: Identifiable {
    let id = UUID()
    let imageName: String
    let name: String

    static let testSong = Song(imageName: "song1", name: "DJ NYK")
}


So I can delete that part and replace it with what you sent now?

No, testSongs populates the array songs that you are listing in ContentView. I just created a static instance of a song that could be used in a Preview so that the Preview worked.

Still have a problem with ContentView passing the right song to DetailView. Will see if I can come up with a solution that works.

Ok thank you for that it worked, I’ll try to come up with solutions also.