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
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
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.
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())
}
}
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")
}
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.