UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Making CoreData implementation conform to MVVM

Forums > SwiftUI

Hey, I am new to SwiftUI (2.0) and I'm trying to follow the MVVM pattern. While trying to implement CoreData, I ran into some issues. While I can access my data from the View, I can't find out how I would access it from my ViewModel. Can give me a tip how to make the implementation of CoreData conform to MVVM?

MyApp.swift:

import SwiftUI
import CoreData

@main
struct MyApp: App {

    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            MainView()
                .environment(\.managedObjectContext, persistentContainer.viewContext)
        }
        .onChange(of: scenePhase) { phase in
                    switch phase {
                    case .active:
                        print("active")
                    case .inactive:
                        print("inactive")
                    case .background:
                        print("background")
                        saveContext()
                    }
                }
    }

    var persistentContainer: NSPersistentContainer = {
            let container = NSPersistentContainer(name: "Model")
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            return container
        }()

        func saveContext() {
            let context = persistentContainer.viewContext
            if context.hasChanges {
                do {
                    try context.save()
                } catch {
                    let nserror = error as NSError
                    fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
                }
            }
        }
}

MainView.swift:

struct MainView: View {
     @FetchRequest(
    entity: MyModel.entity(),
    sortDescriptors: [ ]
  ) var model: FetchedResults<MyModel>
  @Environment(\.managedObjectContext) var managedObjectContext

    /// Code
}

2      

hi,

Core Data stores Entities that have Attributes (which include relationships among the Entities). the PersistentContainer that you access on disk/cloud that holds the Core Data store happens to be named "Model."

as far as i understand the MVVM design in the presence of Core Data, all of Core Data is basically your Model. when you set up a @FetchRequest in a View, that provides access to some/all entities in Core Data of a certain type, and so is a window into your Model, and thus it can often function as the ViewModel inside the View, since its role in life is to keep the View updated when the Core Data objects in the model behind the fetch request change.

in your code, you appear to have an Entity named "MyModel" and so the @FetchRequest variable var model is then (essentially) a list of all the entities in Core Data with entity type MyModel. it is not a single thing like "my whole model" as the variable name suggests. depending on how you use the model variable, its type will be (essentially) [MyModel]. (it's really FetchedResults<MyModel>, but most code you write will look as if its type were [MyModel].)

so i'd suggest that a little renaming might be of help here (too many things are called something like "model"). and what attributes constitute a "MyModel" entity? when we see the MyModel definition (and any other Core Data entities you have or intend to have), we can probably say more.

hope that helps,

DMG

3      

Hi @delawaremathguy

Thanks for your quick answer! Currently I'm using UserDefaults to save my model, but I want to transition this to using CoreData. My CoreData entity is base on the following model:

struct Account: Identifiable {
    var name: String
    var balance: Float
    var primaryColor: Color
    var secondaryColor: Color
    let id: UUID = UUID()

    init(name: String, balance: Float, primaryColor: Color, secondaryColor: Color) {
        self.name = name
        self.balance = balance
        self.primaryColor = primaryColor
        self.secondaryColor = secondaryColor
    }
}

This model is then linked to the view with the following viewmodel:

class AccountStore: ObservableObject {
    @Published var accounts = [Account]()

    var totalBalance: Float
        var totalBalance: Float = 0
        for account in accounts {
            totalBalance += account.balance
        }
        return totalBalance
    }

    init(accounts: [Account]) {
        self.accounts = accounts
    }

}

The viewmodel has a variable where all instances of my model are stored and a computed property that has to access these instances of my model, since I don't want to make these calculation in the view. I'm not sure how I would implement this computed variable else. Therefore I wanted to first load the instances from coredata into a viewmodel.

L4M

2      

hi,

so, what you do is create in Core Data an Entity named "Account," with attributes exactly as you have in the struct representation. Do this in the data model editor -- except for one caveat: Core Data does not have any natural understanding of what is a Color. assume for the moment that we bypass this detail: but you may wish to see more about storing colors in Core Data in this thread on the Apple Developer Forums.

when you build the project, XCode will generate the necessary support files for you, which provides a class Account to your code that fronts the actual Core Data implementation of the class. you usually do not see these files and they are not added to the Project Navigator, but they easily can be found (i'll note this later).

at this point, you'll need to do a few things.

  • add a new file to your project to add some functionailty to the Account class. i would name this Account+Extensions.swift and, as a starting point, it would look like this (which makes sure your app knows this class is Identifiable)
extension Account: Identifiable {
// more code to come
}
  • your Mainview code would start to look like this (at this stage, you don't really need the managedObjectContext: @FetchRequest knows how to find it already, but it will prove helpful below):
struct MainView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
     @FetchRequest(entity: Account.entity(), sortDescriptors: [ ]) 
     var accounts: FetchedResults<Account>
  • you would move the totalBalance function to be a function in your MainView:
func totalBalance() -> Float
  var totalBalance: Float = 0
  for account in accounts {
    totalBalance += account.balance
  }
  return totalBalance
}
  • populate the Core Data store with your existing records, if that's significant data you do not want to lose. this is an interesting exercise because of the name conflict between your existing struct and the new class associated with Core Data. i think by renaming your existing Account struct, you could run your app; read the old data from User Defaults, then create new Account class instances for each and save to Core Data. something like this might work:
func loadData() {
  if accounts.count == 0 { // no data is in Core Data yet, so load old data
  let oldAccounts = // however you find the old accounts in UserDefaults as a list of accounts
  for oldAccount in oldAccounts {
    let newAccount = Account(context: managedObjectContext)
    newAccount.name = oldAccount.name
    newAccount.balance = oldAccount.balance
    // and transfer remaining information
    }
  try? managedObjectContext.save()
  }
}

where to put this? put it in your MainView and call it as a modifier on your view using .onAppear(perform: loadData).

now your MainView can build out a List or something using the accounts variable, using a ForEach(accounts) mechanism.

How do you find the files generated by XCode for the Account class? right-click on Account in the code above and do Jump to Definition. this will show you the (minimal) class file. and then right-click on one of the fields of Account, such as name or balance and do the same to see how the properties are declared with @NSManaged. but you do not want to edit anything that's in these files.

that's enough here, i think, to digest. but if you want to see something related I have out there for public consumption, take a look at my sample ShoppingList app. i'm not a Core Data expert by any means, and i'm still working my way into SwiftUI (this project has made me ask so many questions that i have learned a lot, even if it does not yet show in the code). it is not a SwiftUI app like the new WWDC2020 -- it still has an AppDelegate and SceneDelegate, but i think your App structure above covers the basic injection of the managed object context into the MainView already. oh, and did i forget to mention: i store some colors in Core Data (well, sort of: i store the RGBA components as Doubles).

hope that helps,

DMG

3      

Thanks again! This post helped me understand the topic alot better. I will try to implement it as soon as I can. For saving colors I found a solution using UIColor and converting it to NSData for storage.

Have a nice day :) L4M

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.