Search code examples
swiftswiftuicore-data

How to correctly manage CoreData item and handle its related elements in SwiftUI


I am stuck with the following problem.

I have a view where i fetch a some data (SetlistsView) and present it in a List. The add and move functions work flawlessly and the list is updated as soon as I execute one of these. This is the code for that view:

struct SetlistsView: View {
    
    @Environment(\.managedObjectContext) private var managedObjectContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Setlist.id, ascending: true)],
        animation: .default
    )
    private var setlists : FetchedResults<Setlist>

    var body: some View {
        NavigationView {
            List {
                ForEach(setlists, id: \.self) { setlist in
                    SetlistRowView(setlist: setlist)
                }
                .onMove(perform: move)
            }
            .toolbar {
                ToolbarItem {
                    Button(action: add) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        }
    }
    
    func add() {
        let newSet = Setlist(context: managedObjectContext)
        newSet.id = fetchMaxId() + 1
        PersistenceController.shared.save()
    }
    
    func move(from oldIndex: IndexSet, to newIndex: Int) {
        var revisedItems: [Setlist] = setlists.map{$0}
        revisedItems.move(fromOffsets: oldIndex, toOffset: newIndex)
        for reverseIndex in stride(from: revisedItems.count-1, through: 0, by: -1) {
            revisedItems[reverseIndex].id = Int64(reverseIndex)
        }
        PersistenceController.shared.save()
    }
    
    private func fetchMaxId() -> Int64 {
        return (setlists.max(by: { a, b in a.id < b.id}))?.id ?? -1
    }
}

For each element in the list I instantiate a SetListRowView to which I pass the setlist element. This is the code for that view:

struct SetlistRowView: View {
    
    // Needs to be observed or edits made in SetlistView 
    // (like changing name or list order) won't update this view
    @ObservedObject var setlist             : Setlist
    
    var body: some View {
        
        NavigationLink {
            SongsView(setlist: setlist)
        } label: {
            Text(String(setlist.id))
        }
 
    }
    
}

This acts as a middleman in which I use a NavigationLink to pass the selected setlist to the SongsView, for which this is the code:

struct SongsView: View {
    
    @Environment(\.managedObjectContext) private var managedObjectContext
    
    @ObservedObject var setlist                 : Setlist
    @State private var songs                    : [Song]
    
    init(setlist: Setlist) {
        self.setlist = setlist
        let allObjects = setlist.songs?.allObjects as! [Song]
        self.songs = allObjects.sorted(by: {$0.id < $1.id})
    }
    
    var body: some View {
        
        List {
            ForEach(songs, id: \.self) { song in
                Text(String(song.id))
            }
            .onMove(perform: move)
        }
        .toolbar {
            ToolbarItem {
                Button(action: add) {
                    Label("Add Item", systemImage: "plus")
                }
            }
        }
        
    }
    
    func add() {
        let newSong = Song(context: managedObjectContext)
        newSong.id = fetchMaxId() + 1
        newSong.setlist = setlist
        PersistenceController.shared.save()
    }
    
    func move(from oldIndex: IndexSet, to newIndex: Int) {
        var revisedItems: [Song] = songs.map{$0}
        revisedItems.move(fromOffsets: oldIndex, toOffset: newIndex)
        for reverseIndex in stride(from: revisedItems.count-1, through: 0, by: -1) {
            revisedItems[reverseIndex].id = Int64(reverseIndex)
        }
        PersistenceController.shared.save()
    }
    
    private func fetchMaxId() -> Int64 {
        return (songs.max(by: { a, b in a.id < b.id}))?.id ?? -1
    }
    
}

And here's my problem. In the SongsView I the move and add functions don't work as they should. The add function actually inserts a new item but the view doesn't get refreshed, I have to get back into the SetlistsView and enter the SongsView again. The move function doesn't reassign the id property.

I don't get why these functions work in the SetlistsView but not in the SongsView. I tried changing the songs var as such

@FetchRequest private var songs             : FetchedResults<Song>
    
    init(setlist: Setlist) {
        self.setlist = setlist
        let predicate = NSPredicate(format: "setlist == %@", setlist)
        let sortDescriptors = [NSSortDescriptor(keyPath: \Song.id, ascending: true)]
        self._songs = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
    }

And this seems to work better, as the add function works, but if I add a name property to the Song object (in the CoreData database) I can see that, when the move action ends, the songs are put back into their original order and not actually swapped.

Why the behaviour is different in the two views and how can I fix it? Thank you


Solution

  • So I found the answer after digging a bit... It turns out that functions that work with CoreData items should perform on the same thread as the one assigned to the CoreData context. To do this the view context has a perform method: the add, remove, duplicate and move functions must all do their stuff inside this.

    So, for example, the move method must be edited as such:

    func move(from oldIndex: IndexSet, to newIndex: Int) {
        // This guarantees that the edits are performed in the same thread as the CoreData context
        managedObjectContext.perform {
            var revisedItems: [Song] = songs.map({$0})
            revisedItems.move(fromOffsets: oldIndex, toOffset: newIndex)
            for reverseIndex in stride(from: revisedItems.count-1, through: 0, by: -1) {
                revisedItems[reverseIndex].id = Int64(reverseIndex)
            }
            PersistenceController.shared.save()
        }
    }
    

    Probably the SetlistView got to work on the same thread as CoreData and the SongView on another, hence the reason for the different behaviour.