How to Use @Fetchrequest Outside a View

How to use @Fetchrequest outside a View

I came across the your question while trying to figure out how achieve a similar result myself. I'm guessing you may have cracked this by now, but for the benefit of those stumbling across the same question I'll have a go at answering, or at least pointing at where the answer might be be found :-)

As has been mentioned the @FetchRequest property wrapper doesn't work outside of View components (which seems a bit of an odd functional omission)

The best alternative I've found is to implement essentially the same thing with a Combine publisher for updates from the NSFetchedResultsController as suggested by Apostolos in his answer here and detailed in a linked Medium post

I've put together a simple proof of concept example that tests and demonstrates the approach with iOS14 and its new life cycle management. This demo app can be found in my GitHub repo over here

Good luck.

Swift: CoreData FetchRequest within Class doesn't work

You can't use the @Environment and @FetchRequest outside of a View "properly" @Environment get initialized but usually doesn't update.

To fetch from CoreData you'll have to use the "old way". Attached you'll see the link with more information. It is old but a lot of it still applies.

let moc = …
let employeesFetch = NSFetchRequest(entityName: "Employee")

do {
let fetchedEmployees = try moc.executeFetchRequest(employeesFetch) as! [EmployeeMO]
} catch {
fatalError("Failed to fetch employees: \(error)")
}

How to re-use the same FetchRequest, rather than the same request in several views?

You can use the Environment to pass your models to children without having to passing an array through several layers of views. You start by creating your own EnvirnomentKey

public struct ModelEnvironmentKey: EnvironmentKey {
public static var defaultValue: [Model] = []
}

public extension EnvironmentValues {
var models: [Model] {
get { self[ModelEnvironmentKey] }
set { self[ModelEnvironmentKey] = newValue }
}
}

public extension View {
func setModels(_ models: [Model]) -> some View {
environment(\.models, models)
}
}

I like using ViewModifiers to setup my environment, following the Single-Responsibility Principle:

struct ModelsLoaderViewModifier: ViewModifier {
@FetchRequest(entity: Model(), sortDescriptors: [])
var models: FetchedResults<Model>

func body(content: Content) -> some View {
content
.setModels(Array(models))
}
}

extension View {
func loadModels() -> some View {
modifier(ModelsLoaderViewModifier)
}
}

I then would add this modifier pretty high on the view hierarchy.

@main
struct BudgetApp: App {
@ObservedObject var persistenceManager = PersistenceManager(usage: .main)
let startup = Startup()

var body: some Scene {
WindowGroup {
ContentView()
.loadModels()
}
}
}

Now ContentView can read from the environment:

struct ContentView: View {
@Environment(\.models) var models

var body: some View {
List {
ForEach(models) { model in
Text(model.name)
}
}
}
}

SwiftUI FetchRequest Error: 'A fetch request must have an entity'

Try the code from the SwiftUI app project template when Core Data support is checked:

MyApp.swift

import SwiftUI

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

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

Persistance.swift

import CoreData

struct PersistenceController {
static let shared = PersistenceController()

static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
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)")
}
return result
}()

let container: NSPersistentContainer

init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 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.

/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

swiftUI Core Data @FetchRequest single object

[Continue my comment]... assuming we have Person entity and somehow stored/get some id for some object, here is a demo of possible view that get that object by id and work with it

Note: NSManagedObject conforms to ObservableObject

struct DemoPersonView: View {
@ObservedObject var person: Person

init(id objectID: NSManagedObjectID, in context: NSManagedObjectContext) {
if let person = try? context.existingObject(with: objectID) as? Person {
self.person = person
} else {
// if there is no object with that id, create new one
self.person = Person(context: context)
try? context.save()
}
}

var body: some View {
VStack {
Text("User: \(person.name ?? "<Unknown>")")

// ... other possible code to manage/edit person
}
}
}

Tested with Xcode 12 / iOS 14

SwiftUI FetchRequest predicate with data from an ObservedObject causes property initialisers run before self is available

Here is one way:

struct GroupDetailView: View {

private var fetchRequest: FetchRequest<GroupMember>
private var members: FetchedResults<GroupMember> {
fetchRequest.wrappedValue
}

init(group: Group) {
fetchRequest = FetchRequest(sortDescriptors: [SortDescriptor(\.firstName)], predicate: group.membersPredicate)
}

var body: some View {

}
}

You might not have seen the request and results broken up like this, the source of that idea is a comment in the header file for @SectionedFetchRequest.

You also need to add the lazy var to your Group class extension to hang on to the NSPredicate object so it can be reused if this View is init again. In the Model editor, for Group choose generate class extension only. Then in Editor menu, choose create NSManagedObject subclass, pick the folder where the swift files are and select the app's target. It makes both files so delete the class extension and keep the class. You might need to clean build a few times for it to stop using its auto-generated class file. Then edit it to this:

import Foundation
import CoreData

@objc(Group)
public class Group: NSManagedObject {

lazy var membersPredicate = {
NSPredicate(format: "group = %@", self)
}()
}


Related Topics



Leave a reply



Submit