performFetch creating diffable data source snapshots with too much data - core-data

After executing an NSBatchBatchInsertRequest and calling fetchedResultsController.performFetch(), the frc’s delegate method controller(_:didChangeContentWith:) is called with a NSDiffableDataSourceSnapshot<Section, NSManagedObjectID> that unexpectedly contains all data in Core Data.
However I've only just inserted new data into Core Data. Applying a snapshot with all the data at each performFetch is causing a high memory load and jittery scrolling when I test this with thousands of cells.
Is there a way to make this delegate method only receive a snapshot with incremental updates to the data source?
Code
Here's how the NSFetchedResultsController is created:
let fetchedResultsController = NSFetchedResultsController<MyManagedObject>(
fetchRequest: fetchRequest,
managedObjectContext: persistentContainer.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
The NSFetchRequest:
let fetchRequest = MyManagedObject.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "value", ascending: true)]
fetchRequest.fetchBatchSize = 10
The snapshot is applied in this NSFetchedResultsControllerDelegate method below.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
dataSource.apply(snapshot as NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>, animatingDifferences: false)
}
The most obvious solution to me was implementing this delegate method only, but that method doesn't seem to get called:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith diff: CollectionDifference<NSManagedObjectID>) {
print(diff)
}
Data source to Core Data connection
When the data source is asked to make a cell for a specific NSManagedObjectID, the right MyManagedObject is found for that NSManagedObjectID using fetchedResultsController.managedObjectContext.object(with:).
Then the MyManagedObject is converted into an Item, which is finally used to configure the cell.

Related

Adding to a one-to-many relationship not updating UI

I have a one-to-many relationship in core data of plan -> recipe. plan.recipes is of type NSSet?, so I have created custom NSManagedObject classes with computed properties to convert these into arrays, adding an extra property recipesArray:
public var recipesArray: [Recipe] {
let set = recipes as? Set<Recipe> ?? []
return set.sorted {
$0.wrappedName < $1.wrappedName
}
}
I then display this list in a View using a ForEach, using the recipesArray property. A subview of this view calls plan.addToRecipes(recipe: Recipe), to add a new object to the relationship. I then save.
The issue is, the ForEach in the parent view does not react to this addition. If I refresh the view by navigating away, then the new recipe is shown, but the View is not automatically updated when the new recipe is added.
Does anyone know how to do this? Should I be using the original recipes property instead of this custom array one?
You need to make another FetchRequest for Recipes using a predicate that equals a given plan, e.g. something like this:
struct RecipesView: View {
var fetchRequest: FetchRequest<Recipe>
var recipes: FetchedResults<Recipe> { fetchRequest.wrappedValue }
init(plan: Plan) {
let sortDescriptors = ...
let predicate = NSPredicate(format: "plan = %#", plan)
fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate, animation: .default)
}
var body: some View {
ForEach(recipes) { recipe in
RecipeView(recipe: recipe) // it's important to not access recipe's properties (firing a fault) until inside a sub-view's body.
}
}
}
Note: There is currently a bug in FetchRequest that results in body always invoked even when this View is init with the same plan and thus same FetchRequest. This is because the FetchRequest struct inits a new object inside it every time, causing it and consequently this View to appear as changed to SwiftUI. I reported this bug so hopefully they fix it. You could workaround it in the meantime with a wrapper View that takes the plan as a let so body won't be called.

How to use the new nsPredicate dynamic property of #FetchRequest property wrapper with object passed into View

My first attempt was to set the property wrapper's nsPredicate dynamic property in .onAppear, but if the view gets reinitialized for any reason, the predicate set by .onAppear is lost. So I went back to using the init pattern.
Here is what I thought should work (but doesn't) and something that does work (however mysteriously):
struct ItemEditView : View {
var item: Item
#FetchRequest(fetchRequest: Attribute.fetchRequestAllInOrder(), animation: .default)
var attributes: FetchedResults<Attribute>
init(item: Item) {
self.item = item
// This is how I would have expected to set the dynamic property at View initialization, however
// it crashes on this statement
attributes.nsPredicate = NSPredicate(format: "item == %#", item)
// Not sure why the below works and the above does not.
// It seems to work as desired, however it receives this runtime warning:
// "Context in environment is not connected to a persistent store coordinator"
$attributes.projectedValue.wrappedValue.nsPredicate = NSPredicate(format: "item == %#", item)
}
var body: some View {
List {
ForEach(attributes) { attribute in
Text("Name:\(attribute.name) Order:\(attribute.order)")
}
}
}
}
So, why does the first assignment to nsPredicate crash? And after commenting out that first one, why does the second one work? Is the warning message a real issue? Is there a better way to do this? It seems like there should be a simple way to do this using the new dynamic properties.
It turns out that (re)setting the nsPredicate property of the #FetchRequest in onAppear is really the way to go. However, to make this work, you must make sure that your View's init() method does not get called again after onAppear is called. There are several valuable hints on how to accomplish this in the Demystify SwiftUI session from this year's WWDC (WWDC21-10022).

How do I transform SwiftUI fetch request results based on related objects?

I am building a SwiftUI list where I need a dynamic predicate. The approach is discussed here: https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui
Here is my code so far:
struct SomeView: View {
var collection: Collection
var messages: FetchRequest<Message>
init(collection: Collection) {
let predicate : NSPredicate = NSPredicate(format: "collection = %#", collection)
self.collection = collection
self.messages = FetchRequest<Message>(entity: Message.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)], predicate: predicate)
}
var body: some View {
List(messages.wrappedValue, id: \.uniqueIdentifier) { message in
// construct the UI
}
}
}
So far so good.
What I can’t figure out how to do: I need to transform the messages elements based on some other messages in the results (let’s say based on previous message for simplicity). messages[0] should look a particular way. messages[1] look depends on messages[0]. messages[2] depends on messages[1] and so on. I cannot precompute this, since it may vary across time. It should be computed in the context of this specific fetch request/result.
I could express this as some transient computed property on the Message object, which the view code could then use to branch out. I could have a function where I give a particular message and the array of messages, the function looks up the message and other messages and sets the state of a given message based on that. However, SwiftUI limits what I can do in View code, I can’t execute functions this way.
I can run map or flatmap where I access the wrappedValue, but those don’t let me access other elements of the collection to make decisions (I think?).
How would I run this kind of transformation in this context?
If I correctly understood your description (and taking into account that FetchedResults is a RandomAccessCollection) I would go with the following approach
var body: some View {
List(messages.wrappedValue, id: \.uniqueIdentifier) { message in
rowView(for: message, from: messages.wrappedValue)
}
}
func rowView(for message: Message, from result: FetchedResults<Message>) -> some View {
// having .starIndex, .endIndex, .position, etc. do any dependent calculations here
// and return corresponding View
}

Swift 3 Fetch Request Error (warning: could not load any Objective-C class information)

I recently received this error when fetching data from Core Data:
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
(lldb)
Here is my code:
// MARK: - Initialize Fetch Request
var fetchedResultsController = NSFetchedResultsController<Profile>()
func setFetchRequest() -> NSFetchRequest<Profile> {
let request = Profile.fetchRequest()
let sortDescriptor = SortDescriptor(key: "title", ascending: false)
do {
try moc?.fetch(request)
} catch {
print("Error With Request: \(error)")
}
request.sortDescriptors = [sortDescriptor]
return setFetchRequest()
}
// MARK: - Retrieve Fetch Request
func getFetchRequest() -> NSFetchedResultsController<Profile> {
fetchedResultsController = NSFetchedResultsController(fetchRequest: setFetchRequest(), managedObjectContext: moc!, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}
I crashed with this error where I have "try moc?.fetch(request)":
Thread 1 EXC_BAD_ACCESS (code=2, address=0x16fc07feo)
Are these errors connected or is this a bug in Swift 3 / Xcode 8?
You shouldn't take results from the ManagedObjectContext. If you want to use a NSFetchedResultsController class in your app? You'll need to access their methods. And all of the required or optional methods are comes from the NSFetchedResultsControllerDelegate protocol.
Try this
class YourTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var fetchedResultsController:NSFetchedResultsController<Profile>!
}
And then create a custom helper function like this one:
`func frc() {
let request:NSFetchRequest<Profile> = Profile.fetchRequest()
let sorter = SortDescriptor(key: "title", ascending: true)
request.sortDescriptors = [sorter]
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
// make sure the delegate is set to self
self.fetchedResultsController.delegate = self
do {
try self.fetchedResultsController.performFetch()
} catch {}
}
`
From this point you'll need a trigger to perform operations. So let's the system itself should be doing this when you call the viewDidLoad method or you can create a button instead. For example click the button to begin operations.
override func viewDidLoad() {
super.viewDidLoad()
self.frc()
self.tableView.reloadData()
}
It should be works.
Good luck!
Automatic Subclass Generation
Xcode 8 and Swift 3 comes with a new generation of subclassing called as Automatic Subclass Generation! How to create it? Well! So let's create a new Xcode 8 project, choose a Single View Application and then another window will appears called Choose options for your new project:. Give the name for your new project, make sure language is a Swift and Use Core Data check box is checked and then hit Create.
Go to the YourProjectName.xcdatamodeld file and click it. And then add an entity! So let's say your entity name is a Profile and create their Attributes respectively. It's important to know, because this is an Automatic Subclass Generation. Choose your entity and go to the Data Model Inspector ! Choose a Class Definition for the Codegen You can find a Codegen from here.
After selected the Class Definition, you can see Name text field automatically filled by your entity name like so. Again go to the your entity and click it. Click Command + S for save changes firstly and then click Command + B for rebuild, that's it. Automatic Subclass Generation is successfully created.
Remember
If you want to change your model? For example: If you want to add a new Attribute to your model? It's very easy, select a xcdatamodeld file and click your entity. Click the plus sign under Attributes and add your new Attribute. After your changes is completed? Don't forget to save changes. Again click Command + S and then click Command + B
Create A Managed Object
In the Swift 3 you can create a ManagedObject by using subclass initializer. It's very easy to implementing than ever before
let managedObject = Profile(context: self.managedObjectContext!)
You can see it's very easy! How to save values to the managedObject ? So let's say you have a title attribute of your model. Also title is a String.
managedObject.setValue("Well crafted API? Right?", forKey: "title")
or
managedObject.title = "Well crafted API? Right?"
Save values:
do {
try self.managedObjectContext.save()
print(managedObject)
} catch {}
It's works well on the Swift 3.0 beta and Xcode 8.0 beta.
Update
So, this is what I got working for Xcode 8 beta and Swift 3 beta Core Data
var fetchedResultsControler = NSFetchedResultsController<Profile>()
func frc() {
let request: NSFetchRequest<Profile> = Profile.fetchRequest()
let sortDescriptor = SortDescriptor(key: "title", ascending: true)
request.sortDescriptors = [sortDescriptor]
self.fetchedResultsControler = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.moc!, sectionNameKeyPath: nil, cacheName: nil)
self.fetchedResultsControler.delegate = self
do {
try self.fetchedResultsControler.performFetch()
} catch {
print("Error Fetching Data: \(error)")
}
}
and in viewDidLoad I have self.frc() at the top of the viewDidLoad.
So, in my Profile+CoreDataProperties.swift I copied a method Apple uses in their Master-Detail example when you create a new project:
extension Profile {
#nonobjc class func fetchRequest() -> NSFetchRequest<Profile> {
return NSFetchRequest<Profile>(entityName: "Profile");
}
#NSManaged var title: String?
#NSManaged var titleImage: Data
}
so that my fetch request is "native to my function." Pretty sure that's not the correct way to say that but it's helping me understand what is going on. The fetch request in Apple's example is green instead of blue. And it took me forever to notice that. I clicked on "Event" in Apple's example, and was conveniently taken to the created subclass, which was demonstrated in the Xcode 8 video at WWDC.
The files for e.g. Event.swift and Event+CoreDataProperties.swift are not exposed like they are in Xcode 7.x.x and earlier. You have to click on the entity in the code and you'll be taken to them. Maybe that was my problem? Anyway, I'm fetching data and images like a champ. Thanks a lot for your help #Mannopson!

How to change managed object class name before fetching

I have a Swift app that uses CoreData. I created List entity with class MyAppTarget.List. Everything is configured properly in .xcdatamodeld file. In order to fetch my entities from persistent store, I am using NSFetchedResultsController:
let fetchRequest = NSFetchRequest()
fetchRequest.entity = NSEntityDescription.entityForName("List", inManagedObjectContext: managedObjectContext)
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: "ListFetchedResultsControllerCache")
and it works like expected, returning array of MyAppTarget.List objects when fetching.
However, I would like to use it inside another target, for unit testing. I added List class to MyUnitTestTarget, so I can access it inside the unit test target. The problem is that the fetched results controller returns MyAppTarget.List objects, not the MyUnitTestTarget.List objects. In order to make the List entity testable, I have to make it public along with all methods that I need to use and I would like to avoid this.
I tried to change the managedObjectClassName property on NSEntityDescription:
fetchRequest.entity.managedObjectClassName = "MyUnitTestTarget.List"
but it generates exception:
failed: caught "NSInternalInconsistencyException", "Can't modify an immutable model."
The documentation states that
[...] once a description is used (when the managed object model to which it belongs is associated with a persistent store coordinator), it must not (indeed cannot) be changed. [...] If you need to modify a model that is in use, create a copy, modify the copy, and then discard the objects with the old model.
Unfortunately, I don't know how to implement this flow. I wonder if there is a way to change the managed object class name in runtime, before fetching the entities with NSFetchedResultsController?
It occurs that solution to my problem was pretty simple. In order to make it working, I had to create a copy of managedObjectModel, edit its entities and create NSPersistentStoreCoordinator with the new model. Changing the managedObjectClassName property on NSEntityDescription instance is possible only before model to which it belongs is associated with NSPersistentStoreCoordinator.
let testManagedObjectModel = managedObjectModel.copy() as NSManagedObjectModel
for entity in testManagedObjectModel.entities as [NSEntityDescription] {
if entity.name == "List" {
entity.managedObjectClassName = "CheckListsTests.List"
}
}
This also solves my other problem with unit testing CoreData model entities in Swift.
You can dynamically alter the class name of the NSManagedObject subclass with something like:
let managedObjectModel = NSManagedObjectModel.mergedModelFromBundles([NSBundle.mainBundle()])!
// Check if it is within the test environment
let environment = NSProcessInfo.processInfo().environment as! [String : AnyObject]
let isTestEnvironment = (environment["XCInjectBundle"] as? String)?.pathExtension == "xctest"
// Create the module name based on product name
let productName:String = NSBundle.mainBundle().infoDictionary?["CFBundleName"] as! String
let moduleName = (isTestEnvironment) ? productName + "Tests" : productName
let newManagedObjectModel:NSManagedObjectModel = managedObjectModel.copy() as! NSManagedObjectModel
for entity in newManagedObjectModel.entities as! [NSEntityDescription] {
entity.managedObjectClassName = "\(moduleName).\(entity.name!)"
}

Resources