NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

Sharing Core Data FetchRequest results in MVVM model

Forums > SwiftUI

(I’m going to clip my code as much as possible to not to frustrate people with 500 lines files)

I guess, I finally overcame struggle with Core Data and can normally catch and operate nessesary data in my Views, but I’ve got a situation when I follow MVVM logic, duming all my logic to a separate file.

I’ve got my ContentView where all the visuals happening, here’s the excerpt (as a reality check):

struct ContentView: View {
    @EnvironmentObject var dataController: DataController
    @StateObject private var viewModel = ViewModel() 

    var body: some View { 
        <...>
            Text(viewModel.MegaFunction1())
            Text(viewModel.MegaFunction2())
            Text(viewModel.MegaFunction2())
        <...>
    }
}

DataController's byte-in-byte shamelessly stolen timidly borrowed from TwoStraw’s Portfolio Project. Main Project File looks like this:

@main
struct TheBestAppEverMade: App {
    @StateObject var dataController: DataController

    init() {
        let dataController = DataController()
        _dataController = StateObject(wrappedValue: dataController)
    }

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

And here’s the beginning of my ViewModel:

extension ContentView {
    @MainActor class ViewModel: ObservableObject {
        @EnvironmentObject var dataController: DataController
        @Environment(\.managedObjectContext) var managedObjectContext

@FetchRequest(entity: SomeEntity.entity(), sortDescriptors: [])
        private var someEntity: FetchedResults<SomeEntity>

Down the code I have couple of functions, extensively using fetched data. The work perfectly well, if I move all into one file, but if I call them from ViewModel, I’ve got this:

Accessing StateObject's object without being installed on a View. This will create a new instance each time.

It returns me functions’ results with optional values. I thought that if my ViewModel installed as an extension to my view, then its computed data should be already in ContentView. I guess, I was wrong, so my first attempt to fix it was to make fetchet results @Published. No avail, now it angry with property wrapper because of data type, for some reason. Fetch request line says:

Generic parameter 'Result' could not be inferred

And @Published var someEntity: FetchedResults<SomeEntity> says

Composed wrapper type 'Published<FetchedResults<SomeEntity>>' does not match type of 'FetchRequest<Result>.wrappedValue', which is 'FetchedResults<Result>'

And this is where I stuck. I don’t really understand why ‘just’ var and @Published var cast two different types. And, maybe I should share my data with ContentView some other way? Maybe as an enviroment object? And what does “wrapped value” mean here, in the first place?

Help, please!

P.S. I also thought about @Binding, but I hope it’s the wrong way.

   

You can only use @EnvironmentObject, @Environment and @FetchRequest inside a SwiftUI View.

You would need to pass your dataController as a parameter to your ViewModel and you would need to create a FetchRequest manually in your ViewModel.

   

I thought, this is exactly what I did. This is my ViewModel file:

extension ContentView {
    @MainActor class ViewModel: ObservableObject {
        @EnvironmentObject var dataController: DataController
        @Environment(\.managedObjectContext) var managedObjectContext

@FetchRequest(entity: SomeEntity.entity(), sortDescriptors: [])
        private var someEntity: FetchedResults<SomeEntity>

<...> 

Or, may be I don't understant what does "as a parameter" mean.

   

The property wrappers don't work in your ViewModel. Even when it's a extension of a View. You need to declare them as you would declare them in a normal class.

var dataController: DataController
var managedObjectContext: NSManagedObjectContext
@Published var someEntity: FetchedResults<SomeEntity>

Then declare a init() with your parameters and do what you need to do to set them up. Unless I got your question completely wrong.

   

No avail. Even when it's properly declared in ViewModel, the line of @Published var someEntity: FetchedResults<SomeEntity> returns the very same error:

Composed wrapper type 'Published<FetchedResults<SomeEntity>>' does not match type of 'FetchRequest<Result>.wrappedValue', which is 'FetchedResults<Result>'

   

It seems you can only use FetchResults with @FetchRequest. It seems it doesn't work without it.

As you have access to HWS+ you can take a look how to transfer to a ViewModel and how to deal with CoreData. https://www.hackingwithswift.com/plus/ultimate-portfolio-app/bringing-mvvm-into-our-swiftui-project-part-1

   

I do have access to HWS+, thanks for the link. I'm just not that far yet in Ultimate Portfolio.

I keep issue open and will report the solution when figure it out.

   

hi,

i think what you're trying to do is move the functionality of an @FetchRequest from a view to an @ObservableObject such as your ViewModel (or perhaps even your DataController).

you might consider having the view model manage an NSFetchedResultsController, and connect its fetched results with one of the object's @Published properties.

something like this (remember to import CoreData).

class ViewModel: NSObject, ObservableObject {

  // assume we're managing "Player" objects in Core Data.  
    @Published var players = [Player]()
    private let playerFRC: NSFetchedResultsController<Player>

  // you'll need to pass in a managed object context
  init(context: NSManagedObjectContext) {

   // set up a fetch request
    let fetchRequest: NSFetchRequest<Player> = Player.fetchRequest()
         fetchRequest.sortDescriptors = [
            NSSortDescriptor(keyPath: \Player.lastName_, ascending: true),
            NSSortDescriptor(keyPath: \Player.firstName_, ascending: true)
        ]
            // create a FRC with this fetch request
        playerFRC = NSFetchedResultsController(fetchRequest: fetchRequest,
      managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)

    // you also need to make this object the delegate of the controller
        playerFRC.delegate = self
    // then get initial array of players from Core Data
        try? playerFRC.performFetch()
    players = playerFRC.fetchedObjects ?? []
  }
}

and here's how you make the object respond as the delegate of the fetched results controller ... this is called when changes in Core Data are observed.

extension ViewModel: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
            // update the list ofplayers
        if controller == playerFRC {
            players = playerFRC.fetchedObjects ?? []
     }
    }
}

hope that helps,

DMG

   

Hacking with Swift is sponsored by Stream

SPONSORED Build Chat messaging quickly with Stream Chat. The Stream iOS Chat SDK is highly flexible, customizable, and crazy optimized for performance. Take advantage of this top-notch developer experience, get started for free today!

Click here

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.