Search code examples
core-dataswiftui

Cannot rerender View on CoreData Update in SwiftUI


I am able to update the favorite list in the favorite section , but only after i restart the app, i have multiple answers suggesting to add @ObservedObject var asset: Artists etc and also adding the managed obbject context, i tried all, but the Favorite section will not update on coredata change , can any one kindly suggest a way out of this, below is the code of the file where i am hoping to see the Favorites being added and shown after coredata update but currently this view is getting updated only after i restart the app.

The code has been divided in sections where SongCell, shows each cell and its play button further extracted . An image is also shown of when i reload the app , to see what i want in Favorites section.Thanks.

[![enter image description here][1]][1]

import Foundation
import SwiftUI
import Combine
import AVFoundation

struct Favorites: View {
    
    
    
    
    @ObservedObject  var favListVM = FavoriteListVM()
    @ObservedObject var repo = FavoriteRepository()
    @Binding var favListVM1: FavoriteListVM
   
    
    
    var body: some View {
        
        VStack {
            
            NavigationView {
                List {
                    
                    ForEach(favListVM.favCellVMs) {
                        songCellVM in
                        
                     
                        SongCell(isVisible: $favListVM.isVisible, favCellVM: songCellVM, selectedSong: $favListVM.selectedSong, favListVM1: $favListVM1, isLoved: favListVM.isFavorite ?? false)
                    }
                }
                .navigationTitle("Favorites")
                .font(.subheadline)
            }
            
        
            if favListVM.isVisible  {
                HStack(alignment: .bottom){
                    Image(uiImage: UIImage(data: favListVM.selectedSong?.artistImage ?? Data()) ?? UIImage())
                        .resizable()
                        .frame(width: 50, height: 50, alignment: .leading)
                        .scaledToFit()
                        .cornerRadius(10)
                    Spacer()
                    VStack {
                        Text(favListVM.selectedSong?.songname ?? " ")
                        Text(favListVM.selectedSong?.artistname ?? " ")
                    }
                    
                    
                    ExtractedView(isVisible: $favListVM.isVisible, selectedSong: $favListVM.selectedSong, favoriteListVM2: $favListVM1, favCellVM: FavoriteCellVM(song: Song(album: favListVM.selectedSong?.album ?? "no album found", artistImage: favListVM.selectedSong?.artistImage, artistname: favListVM.selectedSong?.artistname ?? "unknown", genre: favListVM.selectedSong?.genre, songMp3: favListVM.selectedSong?.songMp3, songname: favListVM.selectedSong?.songname ?? "no songs found", id: favListVM.selectedSong?.id ?? UUID())))
                    
                }
            }
        }
        
    }
    
    struct SongCell: View {
        
        @Binding var isVisible: Bool
        @ObservedObject var favCellVM: FavoriteCellVM
        @State var playButton: Bool = false
        @Binding var selectedSong: Song?
        @Binding  var favListVM1: FavoriteListVM
        var isSelected: Bool { favCellVM.song.id == selectedSong?.id }
        @Environment(\.managedObjectContext) var managedObjectContext
        @State var isLoved:Bool
        
        
        @FetchRequest(entity: Artists.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Artists.artistname, ascending: true)]) var artists: FetchedResults<Artists>
        
        
        
        var onCommit: () -> () = {  }
        var body: some View {
            
            HStack {
                let result =  artists.filter { artist in
                    artist.id == favCellVM.song.id
                    
                }
                
                Image(uiImage: UIImage(data: favCellVM.song.artistImage ?? Data()) ?? UIImage())
                    .resizable()
                    .frame(width: 70, height: 70, alignment: .center)
                    .scaledToFit()
                    .cornerRadius(20)
                Spacer()
                Text(favCellVM.song.artistname)
                
                
                Button(action: {
                    
                    print(favCellVM.song.id!)
                    print(result[0].id!)
                    
                    if (result[0].isFavorite == nil){
                        result[0].isFavorite = true
                    }
                    else if(result[0].isFavorite == false) {
                        result[0].isFavorite = true
                    }
                    
                    else {
                        result[0].isFavorite = false
                    }
                    
                    
                    do {
                        try managedObjectContext.save()
                        print("done")
                        print(result)
                    }
                    catch {
                        print("\(error.localizedDescription)")
                    }
                    
                    
                    
                }) {  Image(systemName: result[0].isFavorite == true  ? "suit.heart.fill" : "suit.heart")
                    .resizable()
                    .frame(width: 25, height: 25, alignment: .center)
                    .padding()
                }
                .buttonStyle(PlainButtonStyle())
                
                //--
                
                ExtractedView(isVisible: $isVisible, selectedSong: $selectedSong, favoriteListVM2: $favListVM1,  favCellVM: favCellVM)
                
                
                
            }
        }
    }
    
    
    
    struct ExtractedView: View {
        @Binding var isVisible: Bool
        @Binding var selectedSong: Song?
        @Binding var favoriteListVM2: FavoriteListVM
        
        @ObservedObject var favCellVM: FavoriteCellVM
        var isSelected: Bool { favCellVM.song.id == selectedSong?.id }
        
        
        var body: some View {
            Button(action: {
                
                print(isSelected)
                isVisible.toggle()
                if isSelected  {
                    selectedSong = nil
                    favoriteListVM2.audioPlayer?.stop()
                    
                } else {
                    selectedSong = favCellVM.song
                    isVisible = true
                    do {
                        favoriteListVM2.audioPlayer?.stop()
                        favoriteListVM2.audioPlayer = try AVAudioPlayer(data: favCellVM.song.songMp3!)
                        favoriteListVM2.audioPlayer?.prepareToPlay()
                        favoriteListVM2.audioPlayer?.play()
                    } catch let error {
                        print("\(error.localizedDescription)")
                    }
                    
                }
                
                
            }){     Image(systemName: isSelected ? "pause.fill" : "play.fill")
                .resizable()
                .frame(width: 25, height: 25, alignment: .center)
                .padding()
            }
            .buttonStyle(PlainButtonStyle())
        }
    }
}

//Updated code after loremipsum answer

import Foundation
import SwiftUI
import Combine
import AVFoundation

struct Favorites: View {
    
    
    
    //  @ObservedObject var songsListVM = SongListVM()
   // @ObservedObject  var favListVM = FavoriteListVM()
  //  @StateObject  var favListVM: FavoriteListVM
    @StateObject var repo = FavoriteRepository()
    @ObservedObject var favListVM1: FavoriteListVM
   
    
    var body: some View {
        
        VStack {
            
            NavigationView {
                List {
                    
                    ForEach(favListVM1.favCellVMs) {
                        songCellVM in
                        
                        //                        SongCell(isVisible: $favListVM.isVisible , songCellVM: songCellVM, selectedSong: $favListVM.selectedSong, songsListVM1: $favListVM1)
                        SongCell(isVisible: $favListVM1.isVisible, favCellVM: songCellVM, selectedSong: $favListVM1.selectedSong,  isLoved: favListVM1.isFavorite ?? false)
                    }
                }
                .navigationTitle("Favorites")
                .font(.subheadline)
            }
            
            
            //--
            
            //--
            if favListVM1.isVisible  {
                HStack(alignment: .bottom){
                    Image(uiImage: UIImage(data: favListVM1.selectedSong?.artistImage ?? Data()) ?? UIImage())
                        .resizable()
                        .frame(width: 50, height: 50, alignment: .leading)
                        .scaledToFit()
                        .cornerRadius(10)
                    Spacer()
                    VStack {
                        Text(favListVM1.selectedSong?.songname ?? " ")
                        Text(favListVM1.selectedSong?.artistname ?? " ")
                    }
                    
                    
                    ExtractedView(isVisible: $favListVM1.isVisible, selectedSong: $favListVM1.selectedSong, favoriteListVM2: favListVM1, favCellVM: FavoriteCellVM(song: Song(album: favListVM1.selectedSong?.album ?? "no album found", artistImage: favListVM1.selectedSong?.artistImage, artistname: favListVM1.selectedSong?.artistname ?? "unknown", genre: favListVM1.selectedSong?.genre, songMp3: favListVM1.selectedSong?.songMp3, songname: favListVM1.selectedSong?.songname ?? "no songs found", id: favListVM1.selectedSong?.id ?? UUID())))
                    
                }
            }
        }
        
    }
    
    struct SongCell: View {
        
        @Binding var isVisible: Bool
        @ObservedObject var favCellVM: FavoriteCellVM
        @State var playButton: Bool = false
        @Binding var selectedSong: Song?
     //   @Binding  var favListVM1: FavoriteListVM
        var isSelected: Bool { favCellVM.song.id == selectedSong?.id }
        @Environment(\.managedObjectContext) var managedObjectContext
        @State var isLoved:Bool
        
        
        @FetchRequest(entity: Artists.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Artists.artistname, ascending: true)]) var artists: FetchedResults<Artists>
        
        
        
        var onCommit: () -> () = {  }
        var body: some View {
            
            HStack {
                let result =  artists.filter { artist in
                    artist.id == favCellVM.song.id
                    
                }
                
                Image(uiImage: UIImage(data: favCellVM.song.artistImage ?? Data()) ?? UIImage())
                    .resizable()
                    .frame(width: 70, height: 70, alignment: .center)
                    .scaledToFit()
                    .cornerRadius(20)
                Spacer()
                Text(favCellVM.song.artistname)
                
                
                Button(action: {
                    
                    print(favCellVM.song.id!)
                    print(result[0].id!)
                    
                    if (result[0].isFavorite == nil){
                        result[0].isFavorite = true
                    }
                    else if(result[0].isFavorite == false) {
                        result[0].isFavorite = true
                    }
                    
                    else {
                        result[0].isFavorite = false
                    }
                    
                    
                    do {
                        try managedObjectContext.save()
                        
                        print("done")
                      //  print(result)
                    }
                    catch {
                        print("\(error.localizedDescription)")
                    }
                    
                    
                    
                }) {  Image(systemName: result[0].isFavorite == true  ? "suit.heart.fill" : "suit.heart")
                    .resizable()
                    .frame(width: 25, height: 25, alignment: .center)
                    .padding()
                }
                .buttonStyle(PlainButtonStyle())
                
                //--
                
                ExtractedView(isVisible: $isVisible, selectedSong: $selectedSong, favoriteListVM2: favCellVM,  favCellVM: favCellVM)
                
                
                
            }
        }
    }
    
    
    
    struct ExtractedView: View {
        @Binding var isVisible: Bool
        @Binding var selectedSong: Song?
        @ObservedObject var favoriteListVM2: FavoriteListVM
        
        @ObservedObject var favCellVM: FavoriteCellVM
        var isSelected: Bool { favCellVM.song.id == selectedSong?.id }
        
        
        var body: some View {
            Button(action: {
                
                print(isSelected)
                isVisible.toggle()
                if isSelected  {
                    selectedSong = nil
                    favoriteListVM2.audioPlayer?.stop()
                    
                } else {
                    selectedSong = favCellVM.song
                    isVisible = true
                    do {
                        favoriteListVM2.audioPlayer?.stop()
                        favoriteListVM2.audioPlayer = try AVAudioPlayer(data: favCellVM.song.songMp3!)
                        favoriteListVM2.audioPlayer?.prepareToPlay()
                        favoriteListVM2.audioPlayer?.play()
                    } catch let error {
                        print("\(error.localizedDescription)")
                    }
                    
                }
                
                
            }){     Image(systemName: isSelected ? "pause.fill" : "play.fill")
                .resizable()
                .frame(width: 25, height: 25, alignment: .center)
                .padding()
            }
            .buttonStyle(PlainButtonStyle())
        }
    }
}

//Repository for favorite

import Foundation
import SwiftUI
import CoreData
import AVFoundation
import Combine

class FavoriteRepository: ObservableObject, Identifiable {
    @Published var song = [Song]()
   
       
    @Environment(\.managedObjectContext) var managedObjectContext
    
    @FetchRequest(entity: Artists.entity(), sortDescriptors: []) var artists1: FetchedResults<Artists>
   
   
 
    init(){
        
        loadData()
    }
    
    
    
    func loadData() {
      

     
      

        let context = PersistenceManager.shared.container.viewContext
        let fetchRequest: NSFetchRequest<Artists>
        
        fetchRequest = Artists.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "isFavorite == %@", NSNumber(value: true))
        let objects = try! context.fetch(fetchRequest)
        song = objects.map {
                        artist in

            Song(album: artist.album!, artistImage: artist.artistImage, artistname: artist.artistname!, genre: artist.genre, songMp3: artist.songMp3, songname: artist.songname!, id: artist.id)
        
            }
 
        }
    }
        

//Update after advise from loremipsum to remove the ViewModel and repository

import Foundation
import SwiftUI
import Combine
import AVFoundation

struct Favorites: View {
    
    @Binding var songLVM: SongListVM
   
    
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(entity: Artists.entity(), sortDescriptors: [], predicate: NSPredicate(format: "isFavorite == %@ ", NSNumber(value: true))) var artists1: FetchedResults<Artists>
   
    var body: some View {
        
        VStack {
            
            NavigationView {
                List {
                    
                    ForEach(artists1) {
                        artist in
                        
                        HStack {
                            Image(uiImage: UIImage(data: artist.artistImage ?? Data()) ?? UIImage())
                                .resizable()
                                .frame(width: 50, height: 50, alignment: .leading)
                                .scaledToFit()
                                .cornerRadius(10)
                            Spacer()
                            Text(artist.artistname ?? "no name")
                            Text(artist.songname ?? "no song name")
                            
                            //-
                            Button(action: {
                                
                                //    print(artist.song.id!)
                                print(artist.id!)
                                
                                if (artist.isFavorite == nil){
                                    artist.isFavorite = true
                                }
                                else if(artist.isFavorite == false) {
                                    artist.isFavorite = true
                                }
                                
                                else {
                                    artist.isFavorite = false
                                }
                                
                                
                                do {
                                    try managedObjectContext.save()
                                    
                                    print("done")
                                    //  print(result)
                                }
                                catch {
                                    print("\(error.localizedDescription)")
                                }
                                
                                
                                
                            }) {  Image(systemName: artist.isFavorite == true  ? "suit.heart.fill" : "suit.heart")
                                .resizable()
                                .frame(width: 25, height: 25, alignment: .center)
                                .padding()
                            }
                            .buttonStyle(PlainButtonStyle())
                            // --
                            Button(action: {
                                do {
                                        songLVM.audioPlayer?.stop()
                                        songLVM.audioPlayer = try AVAudioPlayer(data: artist.songMp3!)
                                        songLVM.audioPlayer?.prepareToPlay()
                                        songLVM.audioPlayer?.play()
                                        
                                    }
                                catch {
                                        print("\(error.localizedDescription)")
                                    }
                            }){ Image(systemName:  false ? "pause.fill" : "play.fill")
                                .resizable()
                                .frame(width: 25, height: 25, alignment: .center)
                                .padding()
                            }
                            .buttonStyle(PlainButtonStyle())
                            // --
                            
                        }
                        
                        
                    }
                    
                }
                .navigationTitle("Favorites")
                .font(.subheadline)
                
            }
}}}

Solution

  • Your code is not a Minimal Reproducible Example so it is impossible to know if this will fix it but a few "mistakes" I see.

    First, you should only initialize an ObservableObject inside a View using @StateObject so change all the code that has an init like this

    @ObservedObject  var favListVM = FavoriteListVM()
    @ObservedObject var repo = FavoriteRepository()
    

    To

    @StateObject  var favListVM = FavoriteListVM()
    @StateObject var repo = FavoriteRepository()
    

    Second, and ObservableObject shouldn't be an @Binding it should be a StateObject, ObservedObject or EnvironmentObject. So,

    @Binding var favListVM1: FavoriteListVM
    @Binding var favoriteListVM2: FavoriteListVM
    

    are being misused.

    Third, you seem to be using 2 different FavoriteListVM and one will not be able to see what the other is doing.

    The first instance is

    @Binding var favListVM1: FavoriteListVM
    

    and the second is

    @ObservedObject  var favListVM = FavoriteListVM()
    

    So, how do you fix this...

    In Favorites change

    @Binding var favListVM1: FavoriteListVM 
    

    To

    @ObservedObject var favListVM1: FavoriteListVM 
    

    Then delete @ObservedObject var favListVM = FavoriteListVM()

    And change the references to favListVM to say favListVM1

    In SongCell change @Binding var favListVM1: FavoriteListVM to @ObservedObject var favListVM1: FavoriteListVM

    In ExtractedView change

    @Binding var favoriteListVM2: FavoriteListVM
    

    To

    @ObservedObject var favoriteListVM2: FavoriteListVM
    

    Also, this @Published var favRepository = FavoriteRepository() has no idea what @ObservedObject var repo = FavoriteRepository() is doing. So, if you are expecting that one knows what the other is doing you will encounter a disconnect.

    Summary

    All the changes sum up to only have one FavoriteListVM in each View.

    The first FavoriteListVM should be an @StateObject which will likely be in the parent view of Favorites. with the exception of an array of FavoriteListVM that should be stored as you store the cell vms.

    And all subsequent references in the child Views should be an @ObservedObject not an @Binding.

    Every time you initialize something (example FavoriteRepository() and FavoriteListVM()) you are creating different instances that are completely separate from the other. Like having two people, two cars, two houses, two songs, etc. Stick to creating single as little instances as possible.

    Side note: Once you get it working get rid of the extra variables like

    @Binding var isVisible: Bool
    @Binding var selectedSong: Song?
    

    You have the view model there is no point in referencing it separately

    Last Update

    Replace this line

    @Binding var songLVM: SongListVM
    

    With

    @ObservedObject var avManager: ArtistsAVManager
    

    Now of course you have to make changes to SongListVM to look something like this

    //This class will keep track of everything to do with AVFoundation
    //Will replace SongListVM
    
    
    class ArtistsAVManager:ObservableObject{
        //You want this private because you dont want it modied on its own. Only via Play and Stop methods
        @Published private (set)var nowPlaying: Artists?
        @Published private (set)var status: Status = .stop
        @Published var alert: Alert?
        //Include other code you might have in SongListVM such as audioPlayer
        func play(artist: Artists){
            do {
                audioPlayer?.stop()
                audioPlayer = try AVAudioPlayer(data: artist.songMp3!)
                audioPlayer?.prepareToPlay()
                audioPlayer?.play()
                status = .play
                nowPlaying = artist
            }
            catch {
                print("\(error)")
                let nsError: NSError = error as NSError
                let message = "\(nsError.localizedDescription) \(nsError.localizedFailureReason ?? "") \(nsError.localizedRecoveryOptions?.first ?? "") \(nsError.localizedRecoverySuggestion ?? "")"
                alert = Alert(title: Text("Error: \(nsError.code)"), message: Text(message), dismissButton: .cancel())
                stop()
            }
            
            
        }
        
        func stop(){
            status = .stop
            audioPlayer?.stop()
            nowPlaying = nil
        }
        
        func pause(){
            status = .pause
            //Your code here
        }
        enum Status: String{
            case play
            case pause
            case stop
            
            func systemImageName() -> String{
                switch self {
                case .play:
                    return "play.fill"
                case .pause:
                    return "pause.fill"
                case .stop:
                    return "stop.fill"
                }
            }
        }
    }
    

    Now your play/pause button would look something like this

    Button(action: {
        if avManager.nowPlaying == artist{
            avManager.pause()
        }else{
            avManager.play(artist: artist)
        }
    }){ Image(systemName: (avManager.nowPlaying == artist && avManager.status != .pause) ? ArtistsAVManager.Status.pause.systemImageName() : ArtistsAVManager.Status.play.systemImageName())
            .resizable()
            .frame(width: 25, height: 25, alignment: .center)
            .padding()
    }
    .buttonStyle(PlainButtonStyle())
    

    And if you want to add a stop button you can do something like this

    if avManager.nowPlaying == artist && avManager.status != .stop{
        Button(action: {
            avManager.stop()
        }){ Image(systemName:  ArtistsAVManager.Status.stop.systemImageName())
                .resizable()
                .frame(width: 25, height: 25, alignment: .center)
                .padding()
        }
        .buttonStyle(PlainButtonStyle())
    }