Cannot rerender View on CoreData Update in SwiftUI - core-data

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)
}
}}}

First of all use it's better to use #StateObject instead of #ObservedObject for ViewModel.
So the problem is that in your ViewModel, favCellVMs is not getting updated to changes , it is only set once at initialize.
as long as it is not updated, there is not new value to be published.
How to fix :
Usually favorite list can change from anywhere by the user , so write your SongRepository to something like this :
class SongRepository : ObservableObject {
#Published var favSongs : [SongModel] = []
}
in your App View add (#main):
#StateObject var songRepository = SongRepository
and pass it to your root view like this :
.environmentObject(songRepository)
and finally in your tabs add
#EnvoirmentObject var songRepository : SongRepository
now you can remove , add and read items from songRepository.favSongs form anywhere.
struct Favorites: View {
#EnvoirmentObject var songRepository : SongRepository
#StateObject var favListVM = FavoriteListVM()
//#ObservedObject var repo = FavoriteRepository()
//#Binding var favListVM1: FavoriteListVM
var body: some View {
VStack {
NavigationView {
List {
ForEach(songRepository.favSongs) {
songCellVM in
SongCell(SongModel)
}
} ....

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

Related

swiftui how to fetch core data values from Detail to Edit views

Learning swiftui by building an app with core data; stuck in an issue of data flow from Detail to Edit of AddEdit; the flows from AddEdit to List and from List to Detail are ok. Searched but didn't find useful info online or I don't understand. Here is a simplified project for the question. It complies ok on 13.2 beta and works on simulator, with the issue of blank Edit view from Detail.
views:
struct FileList: View {
#FetchRequest(sortDescriptors: [ NSSortDescriptor(keyPath: \Item.fileName, ascending: false) ], animation: .default) var items: FetchedResults<Item>
#State private var showAdd = false
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: FileDetail(item: item)) {
Text(item.fileName ?? "").font(.headline)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing: Button(action: {
showAdd = true
}, label: { Image(systemName: "plus.circle")
})
.sheet(isPresented: $showAdd) {
FileAddEdit(items: VM())
}
)
}
}
}
struct FileList_Previews: PreviewProvider {
static let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
FileList()
}
}
struct FileDetail: View {
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
#State var showingEdit = false
#ObservedObject var item: Item
var body: some View {
VStack {
Form {
Text(self.item.fileName ?? "File Name")
Button(action: {
showingEdit.toggle()
}, label: {
title: do { Text("Edit")
}
})
.sheet(isPresented: $showingEdit) {
FileAddEdit(items: VM())
}
}
}.navigationTitle("Detail")
}
}
struct FileDetails_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let item = Item(context: moc)
return NavigationView {
FileDetail(item: item)
}
}
}
struct FileAddEdit: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var items = VM()
var body: some View {
NavigationView {
VStack {
Form {
TextField("File Name", text: $items.fileName)
Button(action: {
items.writeData(context: moc)
}, label: {
title: do { Text(items.updateFile == nil ? "Add" : "Edit")
}})
}
}
.navigationTitle("\(items.updateFile == nil ? "Add" : "Edit")")
}
}
}
struct FileAddEdit_Previews: PreviewProvider {
static var previews: some View {
FileAddEdit(items: VM())
}
}
VM:
class VM: ObservableObject {
#Published var fileName = ""
#Published var id = UUID()
#Published var isNewData = false
#Published var updateFile : Item!
init() {
}
var temporaryStorage: [String] = []
func writeData(context : NSManagedObjectContext) {
if updateFile != nil {
updateCurrentFile()
} else {
createNewFile(context: context)
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}
func DetailItem(fileItem: Item){
fileName = fileItem.fileName ?? ""
id = fileItem.id ?? UUID()
updateFile = fileItem
}
func EditItem(fileItem: Item){
fileName = fileItem.fileName ?? ""
id = fileItem.id ?? UUID()
isNewData.toggle()
updateFile = fileItem
}
private func createNewFile(context : NSManagedObjectContext) {
let newFile = Item(context: context)
newFile.fileName = fileName
newFile.id = id
}
private func updateCurrentFile() {
updateFile.fileName = fileName
updateFile.id = id
}
private func resetData() {
fileName = ""
id = UUID()
isNewData.toggle()
updateFile = nil
}
}
Much appreciated for your time and advices!
Here is a simplified version of your code Just paste this code into your project and call YourAppParent() in a body somewhere in your app as high up as possible since it creates the container.
import SwiftUI
import CoreData
//Class to hold all the Persistence methods
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
///Creates an NSManagedObject of **ANY** type
func create<T: NSManagedObject>() -> T{
T(context: context)
//For adding Defaults see the `extension` all the way at the bottom of this post
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
//Figure out the type if you want type specific changes
if obj is FileEnt{
//Make type specific changes
let name = (obj as! FileEnt).fileName
print("I'm updating FileEnt \(name ?? "no name")")
}else{
print("I'm Something else")
}
save()
}
///Creates a sample FileEnt
//Look at the preview code for the `FileEdit` `View` to see when to use.
func addSample() -> FileEnt{
let sample: FileEnt = create()
sample.fileName = "Sample"
sample.fileDate = Date.distantFuture
return sample
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
func save(){
do{
try context.save()
}catch{
print(error)
}
}
}
//Entry Point
struct YourAppParent: View{
#StateObject var coreDataPersistence: CoreDataPersistence = .init()
var body: some View{
FileListView()
//#FetchRequest needs it
.environment(\.managedObjectContext, coreDataPersistence.context)
.environmentObject(coreDataPersistence)
}
}
struct FileListView: View {
#EnvironmentObject var persistence: CoreDataPersistence
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FileEnt.fileDate, ascending: true)],
animation: .default)
private var allFiles: FetchedResults<FileEnt>
var body: some View {
NavigationView{
List{
//Has to be lazy or it will create a bunch of objects because the view gets preloaded
LazyVStack{
NavigationLink(destination: FileAdd(), label: {
Text("Add file")
Spacer()
Image(systemName: "plus")
})
}
ForEach(allFiles) { aFile in
NavigationLink(destination: FileDetailView(aFile: aFile)) {
Text(aFile.fileDate?.description ?? "no date")
}.swipeActions(edge: .trailing, allowsFullSwipe: true, content: {
Button("delete", role: .destructive, action: {
persistence.delete(aFile)
})
})
}
}
}
}
}
struct FileListView_Previews: PreviewProvider {
static var previews: some View {
YourAppParent()
// let pers = CoreDataPersistence()
// FileListView()
// #FetchRequest needs it
// .environment(\.managedObjectContext, pers.context)
// .environmentObject(pers)
}
}
struct FileDetailView: View {
#EnvironmentObject var persistence: CoreDataPersistence
#ObservedObject var aFile: FileEnt
#State var showingFileEdit: Bool = false
var body: some View{
Form {
Text(aFile.fileName ?? "")
}
Button(action: {
showingFileEdit.toggle()
}, label: {
Text("Edit")
})
.sheet(isPresented: $showingFileEdit, onDismiss: {
//Discard any changes that were not saved
persistence.resetStore()
}) {
FileEdit(aFile: aFile)
//sheet needs reinject
.environmentObject(persistence)
}
}
}
///A Bridge to FileEdit that creates the object to be edited
struct FileAdd:View{
#EnvironmentObject var persistence: CoreDataPersistence
//This will not show changes to the variables in this View
#State var newFile: FileEnt? = nil
var body: some View{
Group{
if let aFile = newFile{
FileEdit(aFile: aFile)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
newFile = persistence.create()
})
}
}
.navigationBarHidden(true)
}
}
struct FileEdit: View {
#EnvironmentObject var persistence: CoreDataPersistence
#Environment(\.dismiss) var dismiss
//This will observe changes to variables
#ObservedObject var aFile: FileEnt
var viewHasIssues: Bool{
aFile.fileDate == nil || aFile.fileName == nil
}
var body: some View{
Form {
TextField("required", text: $aFile.fileName.bound)
//DatePicker can give the impression that a date != nil
if aFile.fileDate != nil{
DatePicker("filing date", selection: $aFile.fileDate.bound)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
//Set Default
aFile.fileDate = Date()
})
}
}
Button("save", role: .none, action: {
persistence.update(aFile)
dismiss()
}).disabled(viewHasIssues)
Button("cancel", role: .destructive, action: {
persistence.resetStore()
dismiss()
})
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}
extension Optional where Wrapped == Date {
var _bound: Date? {
get {
return self
}
set {
self = newValue
}
}
public var bound: Date {
get {
return _bound ?? Date.distantPast
}
set {
_bound = newValue
}
}
}
For adding a preview that requires an object you can use this code with the new CoreDataPersistence
///How to create a preview that requires a CoreData object.
struct FileEdit_Previews: PreviewProvider {
static let pers = CoreDataPersistence()
static var previews: some View {
VStack{
FileEdit(aFile: pers.addSample()).environmentObject(pers)
}
}
}
And since the create() is now generic you can use the Entity's extension to add defaults to the variables.
extension FileEnt{
public override func awakeFromInsert() {
//Set defaults here
self.fileName = ""
self.fileDate = Date()
}
}
Below is a working example I made that extends the default Core Data SwiftUI app template to add editing of the Item's timestamp in a sheet. The sheet loads the item in the child context so edits can be made and if cancelled the edits are discarded but if saved then the changes are pushed in to the view context and it is saved. If you are unfamilar with child contexts for editing I recommend Apple's old CoreDataBooks sample project.
The main thing you need to know is when we are using a sheet to edit something we use the version that takes an item rather than a boolean. That allows you to configure the editing View correctly.
import SwiftUI
import CoreData
struct ItemEditorConfig: Identifiable {
let id = UUID()
let context: NSManagedObjectContext
let item: Item
init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
// create the scratch pad context
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = viewContext
// load the item into the scratch pad
item = context.object(with: objectID) as! Item
}
}
struct ItemEditor: View {
#ObservedObject var item: Item // this is the scratch pad item
#Environment(\.managedObjectContext) private var context
#Environment(\.dismiss) private var dismiss
let onSave: () -> Void
#State var errorMessage: String?
var body: some View {
NavigationView {
Form {
Text(item.timestamp!, formatter: itemFormatter)
if let errorMessage = errorMessage {
Text(errorMessage)
}
Button("Update Time") {
item.timestamp = Date()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// first save the scratch pad context then call the handler which will save the view context.
do {
try context.save()
errorMessage = nil
onSave()
} catch {
let nsError = error as NSError
errorMessage = "Unresolved error \(nsError), \(nsError.userInfo)"
}
}
}
}
}
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var item: Item
#State var itemEditorConfig: ItemEditorConfig?
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: edit) {
Text("Edit")
}
}
}
.sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
ItemEditor(item: config.item) {
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
itemEditorConfig = nil
}
.environment(\.managedObjectContext, si.context)
}
}
func edit() {
itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: item.objectID)
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

SwiftUI - View does not refresh

In the view whose code you see below I have a button that calls the searchView function. It displays an alert and in it we enter the number by which we are looking for an object and this object is returned by the getSong function. Up to this point everything is working fine. The problem I have encountered is that it now calls view and passes this found object, but the view does not refresh. I must have made some mistake that I can't locate or that I have forgotten. I would appreciate some help with this.
DetailView:
import CoreData
import SwiftUI
struct DetailView: View {
#State var song : Song
#State var isSelected: Bool
var body: some View {
VStack{
Text(song.content ?? "No content")
.padding()
Spacer()
}
.navigationBarTitle("\(song.number). \(song.title ?? "No title")", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
HStack{
Button(action: {
song.favorite.toggle()
isSelected=song.favorite
}) {
Image(systemName: "heart.fill")
.foregroundColor(isSelected ? .red : .blue)
}
Button(action: {
searchView()
}) {
Image(systemName: "magnifyingglass")
}
}
}
}
}
func searchView(){
let search = UIAlertController(title: "Go to the song", message: "Enter the number of the song you want to jump to.", preferredStyle: .alert)
search.addTextField {(number) in
number.placeholder = "Song number"
}
let go = UIAlertAction(title: "Go", style: .default) { (_) in
let songFound = PersistenceController.shared.getSong(number: (search.textFields![0].text)!)
DetailView(song: songFound!, isSelected: songFound!.favorite)
}
let cancel = UIAlertAction(title: "Cancel", style: .destructive) { (_) in
print("Cancel")
}
search.addAction(cancel)
search.addAction(go)
UIApplication.shared.windows.first?.rootViewController?.present(search, animated: true, completion: {
})
}
}
struct SongDetail_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let song = Song(context: moc)
song.number=0
song.title="Title"
song.content="Content"
song.author="Author"
song.favorite=false
return NavigationView {
DetailView(song: song, isSelected: song.favorite)
}
}
}
PersistenceController:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error \(error.localizedDescription)")
}
}
}
func save(completion: #escaping (Error?) -> () = {_ in}) {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
completion(nil)
} catch {
completion(error)
}
}
}
func delete(_ object: NSManagedObject, completion: #escaping (Error?) -> () = {_ in}) {
let context = container.viewContext
context.delete(object)
save(completion: completion)
}
func getSong(number: String) -> Song? {
guard let songNumber = Int64(number) else { return nil }
let context = container.viewContext
let request = NSFetchRequest<Song>(entityName: "Song")
request.predicate = NSPredicate(format: "number == %ld", songNumber)
do {
return try context.fetch(request).first
} catch {
print(error)
return nil
}
}
}

[CDPaymentMethodModel name]: unrecognized selector sent to instance SwiftUI

struct WithProfileMenuView: View {
#Environment(\.managedObjectContext) var viewContext
#Environment(\.presentationMode) var presentationMode
#State var selectedPayment = CDPaymentMethodModel()
HStack{
Image(systemName: "creditcard.circle.fill")
Text ("Payment Method")
NavigationLink(
destination: PaymentPickerView(selectedPayment: $selectedPayment),
label: {
Spacer()
Text(selectedPayment.name ?? "")
})
}
struct PaymentPickerView: View {
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(entity: CDPaymentMethodModel.entity(),
sortDescriptors:[]
) var paymentList: FetchedResults<CDPaymentMethodModel>
#Environment(\.presentationMode) var mode
#Binding var selectedPayment : CDPaymentMethodModel
var body: some View {
List {
ForEach(paymentList, id: \.self) { payment in
Button(action: {
self.selectedPayment = payment
print("DEBUG\(selectedPayment)")
self.mode.wrappedValue.dismiss()
}) {
HStack{
Text(payment.name ?? "")
Spacer()
Text(payment.bank ?? "")
}
.font(.system(size: 13, weight: .regular, design: .rounded))
.foregroundColor(.black)
}
}
}
I am getting this error on the title when I add Text(selectedPayment.name ?? "") inside the WithProfileMenuView. What is the reason of this error?
If I remove Text(selectedPayment.name ?? "") then it works fine but I want to see selectedPaymnet on the list.

Core Data - Data Transfer SwiftUI

I am trying to transfer CDListModel to my Today View but I can not do it properly. So that I can see how many reminders I have totally today in list.reminders.count. Right now I am getting 0
struct Today: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var list: CDListModel
#State var isTodayTapped = false
var body: some View {
Button(action: {
self.isTodayTapped.toggle()
}) {
ZStack{
RoundedRectangle(cornerRadius: 15)
.foregroundColor(.white)
HStack(alignment: .top, spacing: 100) {
Text(String(list.reminders!.count))
}
}
}
.fullScreenCover(isPresented: $isTodayTapped) {
TodayView()
}
}
}
MainPageView
struct MainPageView: View {
#Environment(\.managedObjectContext) private var viewContext
var list: CDListModel
var body: some View {
ZStack {
NavigationView {
ZStack {
VStack{
HStack(spacing: 20){
Today(list: list) //What should I send here?
.environment(\.managedObjectContext, viewContext)
}
}
}
}
}
}
}
ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedList = ListModel(color: "", text: "", reminders: [])
var body: some View {
MainPageView(selectedList: $selectedList, list: CDListModel())
.environment(\.managedObjectContext, viewContext)
}
}
You do not need to transfer data from another view to Today view. Because the data you need is already inside the core data so you can reach to data using
#FetchRequest( entity: CDReminder.entity(),
sortDescriptors:
[NSSortDescriptor(keyPath: \CDReminder.date, ascending: true)]
)var reminder: FetchedResults<CDReminder>
inside the Today view without modifying any of the other views.
Change Today as below
struct Today: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest( entity: CDReminder.entity(),
sortDescriptors:
[NSSortDescriptor(keyPath: \CDReminder.date, ascending: true)]
)var reminder: FetchedResults<CDReminder>
#State var isTodayTapped = false
var body: some View {
Button(action: {
self.isTodayTapped.toggle()
}) {
ZStack{
RoundedRectangle(cornerRadius: 15)
.foregroundColor(.white)
HStack(alignment: .top, spacing: 100) {
Text(String(list.reminders!.count))
}
}
}
.fullScreenCover(isPresented: $isTodayTapped) {
TodayView()
}
}
}
MainPage as below
struct MainPageView: View {
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
ZStack {
NavigationView {
ZStack {
VStack{
HStack(spacing: 20){
Today()
.environment(\.managedObjectContext, viewContext)
}
}
}
}
}
}
}
ContentView as below
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedList = ListModel(color: "", text: "", reminders: [])
var body: some View {
MainPageView(selectedList: $selectedList)
.environment(\.managedObjectContext, viewContext)
}
}

NavigationLink is activated automatically

I have the following code inside a CoreData SwiftUI project:
import SwiftUI
struct ContentView: View {
#FetchRequest(entity: TestObject.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \TestObject.name, ascending: true)]) var objects: FetchedResults<TestObject>
#Environment(\.managedObjectContext) var managedObjectContext
#State private var showFavorites = true
var body: some View {
NavigationView {
List {
if(showFavorites) {
Section(header: Text("Favorites").font(.headline).padding(.leading, -8)) {
ForEach(self.objects.filter({ (testObj) -> Bool in
return testObj.isFavorite
}), id: \.id) { obj in
NavigationLink(destination: DetailsView(testObject: obj)) {
Text(obj.name ?? "")
}
}
}
}
Section(header: Text("All").font(.headline).padding(.leading, -8)) {
ForEach(self.objects, id: \.id) { obj in
NavigationLink(destination: DetailsView(testObject: obj)) {
Text(obj.name ?? "")
}
}
}
}.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
.navigationBarTitle("Test", displayMode: .large)
.navigationBarItems(
trailing: Button(action: {
let newObj = TestObject(context: self.managedObjectContext)
newObj.name = "newTest"
newObj.id = UUID()
newObj.isFavorite = false
do {
try self.managedObjectContext.save()
} catch let error {
print(error)
print("error saving object")
}
}) {
Image(systemName: "plus")
}
)
}
}
}
struct DetailsView : View{
#ObservedObject var testObject : TestObject
var body : some View {
VStack {
Text(testObject.name ?? "")
}.navigationBarTitle("Detail")
.navigationBarItems(
trailing: HStack {
Button(action: {
self.testObject.isFavorite.toggle()
}) {
Image(systemName: self.testObject.isFavorite ? "heart.fill" : "heart")
}
}
)
}
}
And this entity in the xcdatamodeld file:
If i start the app for the first time, i can add new TestObjects via a tap on the plus image in the NavigationBar, which then get added to the list. If i then tap on one of the objects, i navigate to the DetailsView. On this View, tapping the NavigationBarButton with the heart should mark the object as favorite so that if the user navigates back, this is also displayed in the first part if the list, the favorites. It works so far, but i have this weird effect:
Video on Imgur
It looks like the NavigationLink for the extra element in the favorites section is automatically triggered. Any idea how i can fix this?
Thanks in advance
Try the following (cannot test your code, so just an idea)
struct DetailsView : View {
#ObservedObject var testObject : TestObject
#State private var isFavorite: Bool
init(testObject: TestObject) {
self.testObject = testObject
_isFavorite = State(initialValue: testObject.isFavorite)
}
var body : some View {
VStack {
Text(testObject.name ?? "")
}.navigationBarTitle("Detail")
.navigationBarItems(
trailing: HStack {
Button(action: {
self.isFavorite.toggle()
}) {
Image(systemName: self.isFavorite ? "heart.fill" : "heart")
}
}
)
.onDisappear {
DispatchQueue.main.async { // << this might be redundant, try w/ and w/o
self.testObject.isFavorite = self.isFavorite
}
}
}
}

Resources