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
Bridgetoobjectivec Not Available on Swift Beta 5
Swift Error "Domain=Nscocoaerrordomain Code=3840 "Invalid Value Around Character 1."
Add UIview (From Xib) with Transparency to Scenekit
Cocoa: Simulating Command+Tab in Cgevent
How to Hide The Status Bar Programmatically in iOS 8
Swift Version Build Configuration
Show and Hide Window Instead of Terminating App on Close Click in Cocoa App
How to Access The Model Component of Reality Composer in Realitykit
All of My UIalertcontroller Messages Became Single Line
How to Observe Object's Property in Rxswift
Dictionary of a Protocol Swift 4
Detecting End of The Playback in Avaudioplayer
Xcode 9.2 Is Not Showing Swift 4.1
How to Convert Custom Object to Data Swift
How to Make Nsbutton Title at Bottom and Centered
Scenekit - Why Scnlight Created Automatically in Scnscene
Filter Array of Objects with Multiple Criteria and Types in Swift