WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Many To Many relationship, main view doesn't automatically update data when data are added in another view

Forums > SwiftUI

Hello there,

I'm very very new to swift, swift UI (only 10 days new !) and I'm stuck on something. I have a many to many relationship with SymptomesEntity and TraitementEntity (both just have a name(String) as attribute) Relationship are :

  • In TraitementEntity : (M) Relationship : symptomes - Destination : SymptomeEntity - Inverse : traitements
  • In SymptomeEntity : (M) Relationship : traitements - Destination : TraitementEntity - Inverse : symptomes
  • I'm just viewing Symptomes And Traitements on the main view, but when I launch a sheet and add a new symptome, the main view doesn't update the data. If I rerun the app, I can see my new symptome entered at the last run... If I put the code from the secondview (the sheet) directly in the main code, I can instantatly see the new symptome on the main view. I understand that the data is correctly saved but not correctly reload.

I don't really understand everything about what I code so every help is appreciated, I'm aware that my code is probably not very good at this point.

I've followed this tutorial : https://www.youtube.com/watch?v=huRKU-TAD3g&t=6s (and made some changes to see if It solved my problem but it did not)

Thank you :)

MVVPapp.swift

import SwiftUI

@main
struct MVVMApp: App {

    @StateObject private var dataController = CoreDataManager()

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

ContentView.swift


//  ContentView.swift

import SwiftUI
import CoreData

class CoreDataManager: ObservableObject{

    static let instance = CoreDataManager()
    let container = NSPersistentContainer(name: "Test")
    let context: NSManagedObjectContext 

    init() {
        container.loadPersistentStores { (description, error) in
            if let error = error {
                print("Error loading Core Data. \(error)")
            }
        }
        context = container.viewContext
    }

    func save() {
        do {
            try context.save()
            print("Saved successfully!")
        } catch let error {
            print("Error saving Core Data. \(error.localizedDescription)")
        }
    }

}

class CoreDataRelationshipViewModel: ObservableObject {

    let manager = CoreDataManager.instance
    @Published var symptomes: [SymptomeEntity] = []
    @Published var traitements : [TraitementEntity] = []

    init() {
        getSymptomes()
        getTraitements()
    }

    func Tfiltre(selection: String) -> [TraitementEntity] {

        let request = NSFetchRequest<TraitementEntity>(entityName: "TraitementEntity")

        let sort = NSSortDescriptor(keyPath: \TraitementEntity.name, ascending: true)
        request.sortDescriptors = [sort]

        let filter = NSPredicate(format: "name == %@", selection)
        request.predicate = filter

        do {
            traitements = try manager.context.fetch(request)

        } catch let error {
            print("Error fetching. \(error.localizedDescription)")
        }
        return traitements

    }

    func deleteS(indexSet: IndexSet) {
        guard let index = indexSet.first else { return }
        let symptome = symptomes[index]

        manager.context.delete(symptome)
        save()

    }

    func deleteT(indexSet: IndexSet) {
        guard let index = indexSet.first else { return }
        let traitement = traitements[index]

        manager.context.delete(traitement)
        save()

    }

    func getSymptomes(){

        let request = NSFetchRequest<SymptomeEntity>(entityName: "SymptomeEntity")

        do {
            symptomes = try manager.context.fetch(request)
        } catch let error {
            print("Error fetching. \(error.localizedDescription)")
        }

    }

    func getTraitements(){

        let request = NSFetchRequest<TraitementEntity>(entityName: "TraitementEntity")

        do {
            traitements = try manager.context.fetch(request)
        } catch let error {
            print("Error fetching. \(error.localizedDescription)")
        }

    }

    func addSymptome(name: String) {

        let newSymptome = SymptomeEntity(context: manager.context)
        newSymptome.name = name
        save()

    }

    func addTraitement(name: String){

        let newTraitement = TraitementEntity(context: manager.context)
        newTraitement.name = name
        save()

    }

    func save() {
    symptomes.removeAll()
    traitements.removeAll()

        //reload
       // DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.manager.save()
           self.getSymptomes()
           self.getTraitements()
print("Save avec get")

      //  }
    }

}

struct ContentView: View {

    @FetchRequest(entity: TraitementEntity.entity(), sortDescriptors: []) var traitements: FetchedResults<TraitementEntity>
    @FetchRequest(entity: SymptomeEntity.entity(), sortDescriptors: []) var symptomes: FetchedResults<TraitementEntity>

    @ObservedObject var vm = CoreDataRelationshipViewModel()

    @State private var showAddScreen  = false
    @State var buttonIntensité = true
    @State var valIntensite = ""
    @State private var dates = Date()
    @State private var selection : String?
    let valIntensites = ["vert", "rouge", "jaune"]

    @State private var showAddScreenS  = false

    @State private var name = ""
    @State private var typeIntensite = ""
    @Environment(\.dismiss) var dismiss

    let typeIntensites = ["Chiffres", "Lettres", "Couleurs", "Emoji", "Nouveau"]
    @State private var showAddScreenT  = false

    var body: some View {
        NavigationView {
                    VStack {
                        Section("Symptômes"){
                            VStack{
                                List {
                                    ForEach (vm.symptomes) {symptome in
                                        SymptomeView(entity: symptome)
                                    }
                                    .onDelete(perform: vm.deleteS)
                                }
                                .sheet(isPresented: $showAddScreen) {AjoutSymptomeView()
                                }

                            }
                        }
                    }
                    .toolbar {
        #if os(iOS)
                        ToolbarItem(placement: .navigationBarTrailing) {
                            EditButton()
                        }
        #endif
                        ToolbarItem {
                            HStack{
                                Button {
                                    showAddScreen.toggle()
                                } label: {
                                    Label("Ajouter", systemImage: "bandage")
                                }
                                Button {
                                    showAddScreen.toggle()
                                } label: {
                                    Label("Ajouter", systemImage: "pills.fill")
                                }
                            }

                        }
                    }

                }
                .navigationTitle("Enregistrer")

                Section("Traitements"){

                        VStack{
                            List {
                                ForEach (vm.traitements) {traitement in
                                    TraitementView(entity: traitement)
                                }
                           //     .onDelete(perform: DeleteT)
                            }
                     //   .sheet(isPresented: $showAddScreenT) {AjoutTraitementView()}
                        }
                    }

                VStack{
                    List{
                        DatePicker("Date", selection: $dates, displayedComponents: [.date, .hourAndMinute])
                        VStack
                        {
                            Toggle(isOn: $buttonIntensité) {Text("Question sur l'intensité")}
                            if self.buttonIntensité == true
                                {
                            VStack(alignment: .leading) {
                                HStack {
                                    VStack {
                                        Picker("Intensité", selection: $valIntensite) {
                                            ForEach(valIntensites, id: \.self) {
                                                Text($0)
                                            }
                                        }
                                      }

                               }
                        }
                  }
                }

                }
                Section {
                    Button("Save") {
                        vm.addSymptome(name: "test")
                    }
                }

            }

        }
        }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

//
struct SymptomeView: View {

    let entity: SymptomeEntity

    var body: some View {
        VStack {
            Text(entity.name ?? "")

            if let traitements = entity.traitements?.allObjects as? [TraitementEntity] {
                Text("traitements:")
                ForEach(traitements) { traitement in
                    Text(traitement.name ?? "")
                }
            }
        }
    }
}
struct TraitementView: View {

    let entity: TraitementEntity

    var body: some View {
        VStack {
            Text(entity.name ?? "")

            if let symptomes = entity.symptomes?.allObjects as? [SymptomeEntity] {
                Text("symptômes:")
                ForEach(symptomes) { symptome in
                    Text(symptome.name ?? "")
                }
            }
        }
    }

}

struct SelectionCell: View {
        let s: String
        @Binding var selection: String?

            var body: some View {
                HStack {
                    Text(s)
                    Spacer()
                        if s == selection {
                                Image(systemName: "checkmark")
                                    .foregroundColor(.accentColor)
                        }
                    }
                        .contentShape(Rectangle())
                        .onTapGesture {
                            self.selection = self.s
                        }
                }

            }

AjoutSymptomeView.swift


//  SwiftUIView.swift

import SwiftUI

struct AjoutSymptomeView: View {

    @Environment(\.presentationMode) var presentationMode
    @Environment(\.dismiss) var dismiss

    @State private var name = ""
    @State private var typeIntensite = ""
    @State private var valIntensite = 0

    let typeIntensites = ["Chiffres", "Lettres", "Couleurs", "Emoji", "Nouveau"]
    @State private var showAddScreenT  = false
    @State private var selection : String?

    @ObservedObject var vm = CoreDataRelationshipViewModel()

            var body: some View {
                ZStack(alignment: .topLeading) {
                    Button(action: {
                        presentationMode.wrappedValue.dismiss()

                    }, label: {
                        Image(systemName: "xmark")
                            .foregroundColor(.black)
                            .font(.largeTitle)
                            .padding(20)
                    })

                }

            NavigationView {
                    Form {
                        Section {
                            TextField("Nom du symptôme *", text: $name)
                            Picker("Type d'Intensité", selection: $typeIntensite) {
                                ForEach(typeIntensites, id: \.self) {
                                    Text($0)
                                }
                            }
                        }

                        Section("Selectionner le(s) traitement(s)"){
                                List {
                                    ForEach (vm.traitements) {traitement in
                                            SelectionCell(s : traitement.name ?? "plop", selection: self.$selection)

                                    }
                                   // .onDelete(DeleteT)
                                    Section{
                                        Button {
                                            showAddScreenT.toggle()
                                        } label: {
                                            Label("Nouveau traitement", systemImage: "plus")
                                            }
                                    }
                                }
                        }

                        Section {
                            Button("Sauvegarder le nouveau symptôme") {
                                let newSymptome = SymptomeEntity(context: vm.manager.context)
                                newSymptome.name = name

                                if selection != nil {
                                    let traitements = vm.Tfiltre(selection: selection ?? "")
                                    traitements[0].addToSymptomes(newSymptome)
                                }
                                vm.save()
                                dismiss()
                            }
                        } .disabled(name.isEmpty)

                    }
                    .navigationTitle("Ajouter un symptôme")
                }

    }
}

   

@twoStraws cautions us to criticize ideas, not people. So I will try to be careful with my response.

This site, HackingWithSwift, is a community where we try to help others on their path to becoming iOS developers. We follow and support a 100 Day program that @twoStraws has carefully crafted with beginner, intermediate, and advanced topics.

@twoStraws asks that you program each day and follow the program. The forums are here to help answer questions to help those on this journey.

I sense that you are not following @twoStraws' excellent 100 Day program. You watched a video by Nick Sarno (he's great, by the way), but you come here and dump a load of code, much of it not even related to the problem, and are asking for our help to debug your code from Nick's YouTube videos. Maybe this is the idea that I feel deserves criticism.

Perhaps, instead, you might consider following the 100 Day program? When you reach the lessons on CoreData you'll have a solid understanding of Views, collecting data, and updating data models. This is what you really need, in my opinion.

From those lessons, you'll know that Views don't update because "data are added in another view." This is not how Swift works. Instead, you would know that views are updated when their underlying structs are modified. This could be from an animation sequence, and update to an @State variable, or quite possibly by receiving notifications from @Published vars in a class instance.

We could help you by pointing you to the right lesson in the 100 Days curriculum, or by asking which part of a lesson gave you trouble.

I have two suggestions for you.

  1. Please reconsider your request. You are asking a lot in a forum that you just joined. Perhaps consider joining the 100 Days program.
  2. Instead of asking your question here, contact Nick Sarno. He has a kofi link and would probably quickly answer your question about HIS video if you bought him a scalding espresso. His contact information is connected to his YouTube channel.

Good luck!

1      

thank you for your reply and sorry for my request.

I´ve followed some Paul Hudson videos, especially the one on relationship, but I had to move on to Nick Sarno because I didn´t find a video on many to many reltationship from Paul Hudson and was not able to do it by myself. my code was working (data updating without problem) before that but still on one to many relationship. because I didn´t find many to many relationship video, i tried writing my code from scratch with the tutorial from nick Sarno to implement many to many.

I did not followed the 100 days curriculum because I have some learning disability (sorry if it seems like it doesn´t make sense but I really struggle and i'm at loss of word to really explain the problem.) anyway, I know that i'm missing a lot of basic understanding of Swift. I'll try again doing the 100 days

could you point me to the right lesson in the 100 days curriculum ?

and again, I'm really sorry if I offended anyone with my request, that was not my intention at all. I apologize. (it´s not an excuse but I'm autistic so I may have misunderstood some basic rules and come off as rude - and english is not my main language so it doesn´t help)

thank you for your reply and for taking the time explaining why this was not ok

   

Hacking with Swift is sponsored by Emerge

SPONSORED Optimize your app’s startup time, binary size, and overall performance using Emerge’s advanced app optimization and monitoring tools. Reliably measure app size, speed up your app's startup time with Emerge's Launch Booster, and much more. Emerge is actively used by many of the top mobile development teams in the world.

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

I didn't read through all your code. But from your problem description your code seems to work. It just doesn't update the UI. It has nothing to do with your relationships.

SwiftUI updates the view when a value is changed. Basically, you need to reload the array with your data after saving and not just save it.

You also could try to save your data in

DispatchQueue.main.async {
   // your code here
}

For further information try: https://www.donnywals.com/appropriately-using-dispatchqueue-main/

But this doesn't necessarly update your array with your data. Perhaps someone here can explain it in more details than I can do it.

   

Your AjoutSymptomeView view will be updated when one of the vars marked @ObservedObject publishes a change. In your case, the only @ObservedObject is vm, and its published vars are: @Published var symptomes @Published var traitements

The values of those 2 vars are updated by the fetch requests performed in your funcs getSymptomes() and getTraitements(). If you update the SymptomeEntity or TraitementEntity tables, the symptomes and traitements are not automatically updated, hence the AjoutSymptomeView view is not updated.

If you want to update the view after you add a symptome, you need to update symptomes by calling getSymptomes(). Likewise, after you add a traitement, you need to update traitements by calling getTraitements().

1      

If you want more reading on Core Data, the book by Donny Wals is not always completey clear, but I believe it is the only core data book for Swift 5: https://www.donnywals.com

For SwiftUI, if you want a more concentrated and intense introduction than Paul's books and tutorials, try the Stanford lectures. The SwiftUI explanations in the 2021 lectures is much more helpful than 2020. 2022 lectures may be released in a month or so. Scroll below the introduction on this page: https://cs193p.sites.stanford.edu

   

hi,

Bob has it about right, and i would suggest you also consider additional possibilities.

(1) you might just use @FetchRequests in your views ... you are looking at only two entities (so far) ... and it looks to me like you're making the view model probably more elaborate than it need be.

(2) you might consider having your view model update its two @Published arrays by using NSFetchedResultsController objects (in the view model itself).

the Donny Wals book that Bob mentioned has information on how to hook up NSFetchedResultsControllers in this way; and you would not have to repeatedly call the funcs getSymptomes() and getTraitements() by hand. any change to a SymptomeEntity (S) object would regenerate the symptomes array automatically, and any change to a TraitementEntity object (T) would regenerate the traitements array automatically (with just a few lines of code in the view model) ... and in each case, causing the publisher to fire and update relevant views.

(3) more generally, be aware that if an S object is associated with a T object, changing an attribute on one of these will not automatically trigger any sort of update on the other. for example, if view A shows an @Observed object T and lists the names of all the associated S objects, changing an S object's name over in view B will not trigger an update back in view A.

if this is the type of update your app seems to be missing, then when adding, updating, or deleting a S object, be sure to tell all of its associated T objects that they might want to know about it (if any views are displaying information about S objects associated with a T). in your example of deleting an S:

 func deleteS(indexSet: IndexSet) {
  guard let index = indexSet.first else { return }
  let symptome = symptomes[index]

  if let traitements = symptome.traitements?.allObjects as? [TraitementEntity] {
    for traitement in traitements {
        traitement.objectWillChange.send()
    }
  }

  manager.context.delete(symptome)
  save()

}

hope some of this helps,

DMG

   

(Don't hesitate to tell me to open a new subject if needed, thanx)

Thanks to all for your replies. I've already tried getTraitement and getSymptome but It doesn't work, nor dispatchedQueue or ObjectWillChange so I suspect I'm missing something elsewhere....

So I rolled back to the previous version of my code, without MVVM. When closing the sheet, the data are updated without problem, but now I'm still stuck on another part (which originally made me do the next version) (My code is basically the same as this tutorial One-to-many relationships with Core Data, SwiftUI, and @FetchRequest) : I don't understand how can I add a newSymptome to an already existing traitement ?

I thought I need to use a fetchRequest with predicate on the traitement I want the symptome to be added but I can't seemed to make it work. Any help or idea ?

the new symptome : (working)


     let newSymptome = SymptomeEntity(context: moc)
                        newSymptome.id = UUID()
                        newSymptome.name = name

I selected a traitement from a list so :


  let Tselection = TraitementEntity(context : moc)
  Tselection.name = selection

  Tselection.symptomes = SymptomeEntity(context : moc) --> error : Cannot assign value of type 'SymptomeEntity' to type 'NSSet?'
  Tselection.symptomes?.wrappedName = name --> Value of type 'NSSet' has no member 'wrappedName'

but It has a wrappedName :


@NSManaged public var symptomes: NSSet?

    public var wrappedName: String {
        name ?? "Traitement inconnu"
    }

   

As you have defined relationships between Traitement and Symptomes when you set up the many-to-many relationships, you will find additional methods available for each entity type.

addTo… and removeFrom…. Using these is how you add or remove the links between a traitement item and a symptome item.

   

hi,

@Greenamberred is right; when Xcode generates an extension on each of the S and T objects, defining each property as @NSManaged and making it available to Swift, you'll see that a number of functions have been included to support addition and removal of object references for you.

how do you find these? a quick way is to find a reference in code to an attribute of some entity, say, to traitement.name, where traitment is an instance of Traitment. right-click/option-click on the name symbol and choose "Jump to definition," and you'll open up the Xcode-generated file that defines the functions you can use to work with relationships.

you should find an interface that looks something like this ... in my case, for an entity X which has a to-many relationship to an entity named Item via an attribute of X named items:

    @objc(addItems_Object:)
    @NSManaged public func addToItems_(_ value: Item)

    @objc(removeItems_Object:)
    @NSManaged public func removeFromItems_(_ value: Item)

    @objc(addItems_:)
    @NSManaged public func addToItems_(_ values: NSSet)

    @objc(removeItems_:)
    @NSManaged public func removeFromItems_(_ values: NSSet)

hope that helps,

DMG

   

Thanks a lot DMG and Greenambered, I managed to make it work :) I was stuck because I didn't understand how to "fetch" an object in order to add it. Thank you again for your help :)

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.