CoreData Many-to-Many et mise à jour des données

Bonjour,

je teste CoreData par Rapport à Realm et je coince sur les relations entre les entités.

Comme les List et ForEach dans SwiftUI ne savent pas afficher des Set, je les transforme en Array. Via une extension à mon Entity

J’ai tester ces deux techniques :

    public var groupsArray: [CDGroup] {
        let set = groups as? Set<CDGroup> ?? []
        let array = Array(set)
        return array
    }

ou

    public var groupsArray2: [CDGroup] {
        
        let set = groups as? Set<CDGroup> ?? []
        let array = set.map { $0 }
        return array
    }

Seulement, quand je crée et j’'ajoute un nouvel objet dans la relation d’un autre, ma liste ne se met pas à jour automatique. Il faut absolument sortir de l’App pour avec une mise à jour.

Que je travaille avec @FectRequest ou que je crée un ObservableObject avec une fonction qui charger et refresh l’objet parent, la liste dans objects dans la relation ne se met pas à jour jusqu’au chargement à nouveau de l’app.

Du coup c’est moins pratique… :slight_smile:

Si quelqu’un a une solution, je suis preneur.

Merci

Chris

Bonsoir,

Pour l’observableObject, as-tu pensé à déclarer ta variable en @Published ?

Voir What is the @Published property wrapper? - a free SwiftUI by Example tutorial

Cordialement
Nicolas

Oui oui ce n’est pas me soucis ici puisque la variable liée à l’entité dans core data se met bien à jour à chaque changement.

Ici c’est bien lorsque l’on ajoute une relation à l’entité principale, que cette liste ne se met pas à jour.

Bonsoir,

Il faudrait sans doute que tu postes plus de code en précisant bien ce qui se passe / ne se passe pas quand tu fais telle action.

Cordialement,
Nicolas

Donc on a deux entities. CDCourse et CDGroup

Il y a une relation many-to-may entre les deux.

La première vue réalise un @FetchRequest sur l’Entity Course et liste les différents objects.

@available(iOS 15.0, *)
struct CoursesList: View {
    
    
    
    @FetchRequest(
        entity: CDCourse.entity(),
        sortDescriptors: [])
    var courses: FetchedResults<CDCourse>
        
        
    var body: some View {
        VStack {
            ScrollView {
                ForEach(courses, id: \.self) { course in
                    NavigationLink(destination: CourseDetailsView(course:course)) {
                        CourseCard(course: course).padding()
                    }
                }
            }
        }
    }
}

On passe alors un des objects CDCourse à la vue suivante via la NavigationLink. Sur cette deuxième vue, on liste les CDGroup


@available(iOS 15.0, *)
struct CourseClassesView: View {
    
    var course:CDCourse
    
    var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
    @State var refreshing: Bool = false
    
    @StateObject var groups = Groups()
    @StateObject var courses = Courses()
    @State var groupsss:[CDGroup]
    
    @Binding var grooops:[CDGroup]
    
    @ObservedObject var couuurse: CDCourse
    
    var body: some View {
        if self.refreshing {
            Text("Refreshing")
        }else{
            List {
                ForEach(couuurse.groupsArray2, id: \.self) { group in
                    
                    Text("No Group Name")
                    
                }
            }
             .onReceive(didSave) { _ in
                    self.refreshing.toggle()
                }
                .frame(width: 200, height: 200, alignment: .center)
        }
    }
}

On peut alors créer un object CDGroup et l’ajouter, via la relation, à l’object CDCourse qu’on a passé. Pour cela, j’utilise cette fonction.

    func addGroupToCourse(course:CDCourse, group:CDGroup) {
        if let existingCourse = fetchCourse(withID: course.id!),
           let existingGroup = fetchGroup(withID: group.id!) {
            if ((course.groups?.contains(group)) != nil) {
                print("Course & Group found")
                print(existingCourse.groups!)
                existingCourse.creationDate = Date()
                existingCourse.addToGroups(existingGroup)
                saveContext()
                
                course.managedObjectContext?.refresh(course, mergeChanges: true)
                group.managedObjectContext?.refresh(group, mergeChanges: true)
            }
        }
    }

Tout se passe nickel, le CDGroup est bien ajouté à CDCourse via la relation mais la liste de la deuxième vue ne régit pas, pas de mise à jour. Même si l’on revient à la vue précédente, rien ne se passe.

Cette liste ne sera mise à jour qu’au lancement suivant de l’app.

J’espère être plus clair du coup. Je ne sais pas si cela aide ? :slight_smile:

Merci

Chris

Bonjour,

J’ai fait un projet qui me semble répondre à ton besoin:

La vue principale:

import SwiftUI
import CoreData

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

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \CDCourse.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<CDCourse>

    var body: some View {
        NavigationView{
            VStack {
                
                List {
                                        ForEach(items) { item in
                                            NavigationLink(destination: CourseDetailsView(aCourse:item)) {
                                                Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                                                                }
                                        }
                                        .onDelete(perform: deleteItems)
                                    }
                            
                            Button(action: addItem) {
                                Label("Add Item", systemImage: "plus")
                            }
            }
            
                    .toolbar {
                        #if os(iOS)
                        EditButton()
                        #endif

                        Button(action: addItem) {
                            Label("Add Item", systemImage: "plus")
                        }
                    }
        }
        
    }

    private func addItem() {
        withAnimation {
            let newItem = CDCourse(context: viewContext)
            newItem.timestamp = Date()
            newItem.courseName = "New course"

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

La vue secondaire:

import SwiftUI



struct CourseDetailsView: View {
    @Environment(\.managedObjectContext) private var viewContext
    var course:CDCourse
    @FetchRequest private var groups: FetchedResults<CDGroup>
    
    
    init(aCourse: CDCourse) {
        course = aCourse
        var predicate: NSPredicate
        predicate = NSPredicate(format: "course = %@", aCourse)
        _groups = FetchRequest(
            entity: CDGroup.entity(),
            sortDescriptors: [NSSortDescriptor(keyPath: \CDGroup.timestamp, ascending: false)],
            predicate: predicate,
            animation: .default)
    }
    
    var body: some View {
        
        VStack {
            
            List {
                        ForEach(groups) { group in
                            Text("\(group.description)")
                        }
                    }
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
        }
        
    }
    
    private func addItem() {
        withAnimation {
            let newItem = CDGroup(context: viewContext)
            newItem.timestamp = Date()
            newItem.groupName = "New group"
            newItem.course = course

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


Le modèle Core Data

Et le fichier Main généré automatiquement en créant un projet incluant Core Data

import SwiftUI

@main
struct TestCoreDataApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

La chose que je n’ai pas gérée: la relation many to many dans Core Data

Cordialement,
Nicolas

Il y a sûrement d’autres manières de faire ce que tu souhaites… C’est ce que j’ai trouvé dans un contexte similaire :grinning:

En complément: dans la prochaine version de Xcode, la gestion des @FetchRequest sera améliorée; il me semble qu’on pourra plus facilement gérer les predicate et sort descriptors, et ainsi éviter la déclaration init() dans la vue secondaire. À vérifier !

Voir https://developer.apple.com/wwdc21/10017

Et pour la relation many to many:

  • le predicate devient
predicate = NSPredicate(format: "ANY courses.courseName = %@", aCourse.courseName!)

Dans le addItem de la vue secondaire:

newItem.addToCourses(course)

Et après avoir renommé « course » en « courses » dans la relation many to many de CDGroup

Merci bcp d’avoir prit du temps pour regarder à mon soucis.

Petite question.

Sur la vue secondaire, tu récupères tous les groupes, pas juste ceux qui sont liés au cours ? si ? car je vois un @FetchRequest complet sur les groups.

Bonjour,

Le predicate permet de ne sélectionner que les CDGroup associés au CDcourse (sous réserve que le critère retenu soit pertinent : dans l’exemple many to many, le critère se fait sur le nom du CDcourse, à adapter si besoin)

Cordialement
Nicolas

Merci bcp pour le temps passé sur mon soucis.