NSFetchedResultsController + UICollectionViewDiffableDataSource + CoreData - How to diff on the entire object? - core-data

I'm trying to use some of the new diffing classes built into iOS 13 along with Core Data. The problem I am running into is that controllerdidChangeContentWith doesn't work as expected. It passes me a snapshot reference, which is a reference to a
NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>
meaning I get a list of sections/Object ID's that have changed.
This part works wonderfully. But the problem comes when you get to the diffing in the collection view. In the WWDC video they happily call
dataSource.apply(snapshot, animatingDifferences: true)
and everything works magically, but that is not the case in the actual API.
In my initial attempt, I tried this:
resolvedSnapshot.appendItems(snapshot.itemIdentifiersInSection(withIdentifier: section).map {
controller.managedObjectContext.object(with: $0 as! NSManagedObjectID) as! Activity
}, toSection: .all)
And this works for populating the cells, but if data is changed on a cell (IE. the cell title) the specific cell is never reloaded. I took a look at the snapshot and it appears the issue is simply that I have references to these activity objects, so they are both getting updated simultaneously (Meaning the activity in the old snapshot is equivalent to the one in the new snapshot, so the hashes are equal.)
My current solution is using a struct that contains all my Activity class variables, but that disconnects it from CoreData. So my data source became:
var dataSource: UICollectionViewDiffableDataSource<Section, ActivityStruct>
That way the snapshot actually gets two different values, because it has two different objects to compare. This works, but it seems far from elegant, is this how we were meant to use this? Or is it just in a broken state right now? The WWDC video seems to imply it shouldn't require all this extra boilerplate.

I ran into the same issue and I think I figured out what works:
There are two classes: UICollectionViewDiffableDataSource and UICollectionViewDiffableDataSourceReference
From what I can tell, when you use the first, you're taking ownership as the "Source of Truth" so you create an object that acts as the data source. When you use the second (the data source reference), you defer the "Source of Truth" to another data source (in this case, CoreData).
You would instantiate a ...DataSourceReference essentially the same way as a ...DataSource:
dataSourceReference = UICollectionViewDiffableDataSourceReference(collectionView: collectionView, cellProvider: { (collectionView, indexPath, object) -> UICollectionViewCell? in
let identifier = <#cell identifier#>
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
<#cell configuration#>
return cell
})
And then later when you implement the NSFetchedResultsControllerDelegate, you can use the following method:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference)
{
dataSourceReference.applySnapshot(snapshot, animatingDifferences: true)
}
I watched the WWDC video as well and didn't see this referenced. Had to make a few mistakes to get here. I hope it works for you!

Related

Best practice core data same view for creating and editing [duplicate]

This question already has answers here:
swiftui how to fetch core data values from Detail to Edit views
(2 answers)
Closed 11 months ago.
I try to use core data to store persistent data on an iOS device. I've got user flows to create and edit domain objects with a few related and deeply nested objects.
Those user flows are very similar, so I would like to use the same views for those tasks, just deciding on appear if the view got an existing domain object passed or it needs to create a new one.
After testing different approaches, nothing seems to fit this context so I wonder if there are recommended ways for this situation?
The following options got tested:
initializing the core data object in init() results in an Modifying state during view update, this will cause undefined behavior. warning
initializing the core data object in .onAppear requires the #ObservedObject var domainObjectPassed: DomainObject to be optional, not quite what I'm looking for as well
Any suggestions?
Already did that. I extracted the same logic into one view and have two distinct wrapper views that should handle this problem. But I've got the same situation one level higher.
struct CreateView: View {
#ObservedObject private var domainObject: DomainObject
init(moc: NSManagedObjectContext) {
domainObject = DomainObject(context: moc)
domainObject.id = UUID()
try? moc.save()
}
var body: some View {
CustomizeView(domainObject: domainObject)
}
}
-> results in warning from the first option
struct CreateView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject private var domainObject: DomainObject? = nil
var body: some View {
CustomizeView(domainObject: domainObject)
.onAppear {
...
}
}
}
-> requires domainObject in CustomizeView to be optional, not what I'm looking for
I am using the same concept (one view for editing and creating), but in my case I find myself comfortable using an optional to get the core data object. If you can live with an optional, this could work.
In this way, you just need to check if the object passed is nil: if it is, create the object, otherwise use the object passed.
In a very schematic way, the view could be:
struct EditOrCreate: View {
var coreDataObject: MyCoreDataEntity
// Optional object at initializer
init(objectPassed: MyCoreDataEntity? = nil) {
if objectPassed == nil {
coreDataObject = MyCoreDataEntity(context: thePersistentContainerContext)
} else {
coreDataObject = objectPassed!
}
}
var body: some View {
// All the text fields and save
}
}
When creating, just call EditOrCreate().
When editing, just pass the object: EditOrCreate(objectPassed: theObjectBeingShown).
You can also make coreDataObject an optional variable (which is what I did in my code), depending on how you want to handle your logic - for example, to cancel the creation before saving: in that case, you need to check for nil after the user has confirmed the creation of the new object.
Some programmers believe that "Premature Optimization Is the Root of All Evil". What I've seen happens when creating generic view code with only a small number of cases that have not yet been thought through is that after creating the generic version, you realise that you need to customise one of them, so end up trying to override the default behaviour in certain cases, which gets real messy and you would have been better off just with separate versions in the first place.
In SwiftUI, the View structs are lightweight and there is no issue with creating lots of them. Try to break your View structs up to be as small as possible then you can compose them together to form your different use cases.
The time you save could be spent on learning more about how SwiftUI works and fixing the issues in the code you posted. E.g. we don't init objects in the View struct init, or in body. Those need to run fast because that code runs quite frequently and creating objects is a comparably heavy task, also those objects that are created are immediately discarded after SwiftUI has finished building the View struct hierarchy, diffed it from last time, and updated the actual UIKit Views on screen. I highly recommend watching every WWDC SwiftUI video, there is a lot to learn and there is a lot of magic going on under the hood.
Another thing you could spend time learning is Swift generics and protocols. These are powerful ways to build reusable code using value types instead of class inheritance that we would typically use as ObjC/UIKit developers which tends to be buggy. You can read more about here: Choosing Between Structures and Classes (Apple Developer)

FileDocument with UIManagedDocument/core data

When using SwiftUI to create a document based app, the default document type is to subclass FileDocument.
All examples lead to simple value types to be used in this document type.
I'm looking to create a UIManagedDocument in SwiftUI but there doesn't seem to be any mention of using FileDocument with core data. I noticed a ReferenceFileDocument but this leads to no examples either...
Has anyone had any experience of using either SwiftUI document type for core data based documents?
After some more months, I came across this question once again.
Since my last comment on September 18th, I've worked myself on solving the puzzle of building a SwiftUI document-based app using Core Data.
Looking more in-depth I learned that the UIManagedDocument (respectively its parent UIDocument) infrastructure is really close/similar to what SwiftUI tries to implement. SwiftUI even uses UIDocument in the background to do "its magic". UIDocument and UIManagedDocument are simply some more archaic remnants of times where Objective-C was the dominant language. There was no Swift and there were no value-types.
In general I can give you the following tips to solve your challenge using Core Data within a UIManagedDocument:
first of all, if you want to use Core Data, you will have to use a package/bundle based document format. This means your UTType will have to conform to .package (=com.apple.package in your Info.plist file). You won't be able to make Core Data work with only a plain file document.
extension UTType {
static var exampleDocument: UTType {
UTType(exportedAs: "com.example.mydocument", conformingTo: .package)
}
}
use a ReferenceFileDocument based class to build a wrapper document for your UIManagedDocument. This is necessary because you will have to know about the time when the object is released. In deinit you will have to call managedDocument.close() to ensure the UIManagedDocument is properly closed and no data is lost.
the required function init(configuration:) is going to be called when an existing document is opened. Sadly, it is of no use when working with UIManagedDocument because we have only access to the FileWrapper of the document and not the URL. But the URL is what you need to initialize the UIManagedDocument.
the required function snapshot(contentType:) and fileWrappper(snapshot:, configuration:) is only used to create a new empty document. (This is because we won't use the SwiftUI integrated UndoManager but the one from UIManagedDocument respectively Core Datas NSManagedObjectContext.) Therefore it is not relevant what your type for Snapshot is. You can use a Date or Int because the snapshot taken with the first function is not what you are going to write in the second function.
The fileWrappper(snapshot:, configuration:) function should return the file structure of an empty UIManagedDocument. This means, it should contain a directory StoreContent and an empty file with the filename of the persistent store (default is persistentStore) as in the screenshot below.
The persistentStore-shm and persistentStore-wal files are going to be created automatically when Core Data is starting up, so we do not have to create them in advance.
I am using the following expression to create the FileWrapper representing the document: (MyManagedDocument is my UIManagedDocument subclass)
FileWrapper(directoryWithFileWrappers: [
"StoreContent" : FileWrapper(directoryWithFileWrappers: [
MyManagedDocument.persistentStoreName : FileWrapper(regularFileWithContents: Data())
])
])
above steps allow us to create an empty document. But it still cannot be connected to our UIManagedDocument subclass, because we have no idea where the document (represented by the FileWrapper we have created) is located. Luckily SwiftUI is passing us the URL of the currently opened document in the ReferenceFileDocumentConfiguration which is accessible in the DocumentGroup content closure. The property fileURL can then be used to finally create and open our UIManagedDocument instance from the wrapper. I'm doing this as follows: (file.document is an instance of our ReferenceFileDocument class)
DocumentGroup(newDocument: { DemoDocument() }) { file in
ContentView()
.onAppear {
if let url = file.fileURL {
file.document.open(fileURL: url)
}
}
}
in my open(fileURL:) method, I then instantiate the UIManagedDocument subclass and call open to properly initialize it.
With above steps you will be able to display your document and access its managedObjectContext in a view similar to this: (DocumentView is a regular SwiftUI view using for example #FetchRequest to query data)
struct ContentView: View {
#EnvironmentObject var document: DemoDocument
var body: some View {
if let managedDocument = document.managedDocument {
DocumentView()
.environment(\.managedObjectContext, managedDocument.managedObjectContext)
} else {
ProgressView("Loading")
}
}
}
But you will soon encounter some issues/crashes:
You see the app freeze when a document is opened. It seems as if UIManagedDocument open or close won't finish/return.
This is due to some deadlock. (You might remember, that I initially told you that SwiftUI is using UIDocument behind the scene? This is probably the cause of the deadlock: we are running already some open while we try to execute another open command.
Workaround: run all calls to open and close on a background queue.
Your app crashes when you try to open another document after having previously closed one. You might see errors as:
warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Item' so +entity is unable to disambiguate.
warning: 'Item' (0x6000023c4420) from NSManagedObjectModel (0x600003781220) claims 'Item'.
warning: 'Item' (0x6000023ecb00) from NSManagedObjectModel (0x600003787930) claims 'Item'.
error: +[Item entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
After having debugged this for some hours, I learned that the NSManagedObjectModel (instantiated by our UIManagedDocument) is not released between closing a document and opening another. (Which, by good reason, is not necessary as we would use the same model anyway for the next file we open). The solution I found to this problem was to override the managedObjectModel variable for my UIManagedDocument subclass and return the NSManagedObjectModel which I'm loading "manually" from my apps bundle. I suppose there are nicer ways to do this, but here is the code I'm using:
class MyManagedDocument: UIManagedDocument {
// We fetch the ManagedObjectModel only once and cache it statically
private static let managedObjectModel: NSManagedObjectModel = {
guard let url = Bundle(for: MyManagedDocument.self).url(forResource: "Model", withExtension: "momd") else {
fatalError("Model.xcdatamodeld not found in bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: url) else {
fatalError("Model.xcdatamodeld not load from bundle")
}
return mom
}()
// Make sure to use always the same instance of the model, otherwise we get crashes when opening another document
override var managedObjectModel: NSManagedObjectModel {
Self.managedObjectModel
}
}
So this answer has become really lengthy, but I hope it is helpful to others struggling with this topic. I've put up this gist with my working example to copy and explore.

CodedUI- Best way to create and use UIObject Repository( that requires minimum effort when UI changes)

I started working with CodedUI few months before to automate a desktop Application(WPF).
Just checking out for the best ways to create a framework for my Application.
As, I have seen in other automation tools, I feel the heart of an automation framework using any tool(UI Based) is the way it's object Repository is created i.e. how well the UI objects are defined. A Cleaner and well defined Object Repository always proves to be very helpful when it comes to updating your tests.
I am trying to discover the best way to store my UIObjects so that in case of any UI changes in my Application, I have to put minimum effort to update my automation test.
Also, If an Object changes in application, updating it only at one place should solve the problem.
This can be any kind of change like :
->change in just a property(This I feel would be very easy to update in automation Test. The best and Easiet way I feel is to simply update the .uitest file(the xml file) if possible.)
->change in hierarchy and position
->entirely new object added
For the 2nd and 3rd changes, updating scripts become a difficult job, esp if the UIObject is being referred at may places, in many TestMethods, or Modules.
Also, I have generally seen that in Test Methods, Variable Declarations are done to create a reference to the UIMap objects and those variables are further used in the TestMethod Code.
So, in this case If the UI of my application changes, I will have to update the variable decalaration in each of the Test Methods. I want to reduce this effort to changing the variable decalaration only at one place. OfCourse, I cannot have all the code inside only one Test Method. One way that came to my mind is as:
Can't I have simply one common place for all these Variable decalarations. We can give a unique and understandable name to each UIObject e.g.: The decalratoions will look like:
UITabPage UITabPage = this.UIMap.UISimWindow.UISelectEquipmentTabList.UITabPage;
WpfRow UIRow = this.UIMap.UISimWindow.UISelectEquipmentTabList.UITabPage.UIEquipmentDetailsTable.UIRow;
WpfText UIEquipmentTagText = this.UIMap.UISimWindow.UISelectEquipmentTabList.UITabPage.UIEquipmentDetailsTable.UIRow.UITagCell.UIEquipmentTagText;
WpfCheckBox UIEquipmentCheckBox = this.UIMap.UISimWindow.UISelectEquipmentTabList.UITabPage.UIEquipmentDetailsTable.UIRow.UICheckBoxCell.UICheckBox;
....
....
and use these variables wherever required. Hence, In case of any chnages also, there will be only one place where you will need to update thse objects.
But for this, These varaibles must be made STATIC. What can be problem with making these Object Variables static?
Please provide your suggestion on this topic. May be what I am thinking is not possible or practical. I just want to choose the best way to start with before I go too far with the automation scripts and realize later that my approach wasn't a good one.
Thanks in Advance,
Shruti
Look into using descriptive programming instead of using the UIMaps.
Make a static class with generic functions to assist. Going to give you some examples of how to set it up.
For example:
public WinWindow parentwin(string ParentControlName)
{
var parentwin = new WinWindow();
parentwin.SearchProperties.Add("Control Name", ParentControlName);
return parentwin;
}
public WinWindow childwin(string ChildWinControlName, string ParentControlName)
{
var childwin = new WinWindow(parentwin(ParentControlName));
childwin.SearchProperties.Add("Control Name", ChildWinControlName);
return childwin;
}
public WinButton button(string ButtonName,string ChildWinControlName, string ParentControlName)
{
var childwin = childwin(ChildWinControlName,ParentControlName);
var button = new WinButton(childwin);
button.SearchProperties.Add("Name", ButtonName);
}
public void ClickButton(string ButtonName,string ChildWinControlName, string ParentControlName)
{
var button = button(ButtonName,ChildWinControlName,ParentControlName);
Mouse.Click(button);
}
public void ChangeFocus(WinWindow NewFocus)
{
var NewFocus = new NewFocus();
NewFocus.SetFocus();
}
public void ChangeFocus(WinWindow NewFocusChild, string c)
{
var a = new NewFocus();
a.SetFocus();
}
ChangeFocus(childwin("WelcomeForm", "MainForm");
ClickButton("&OK", "WelcomeForm", "MainForm");

NSManagedObject stops receiving merged changes after obtainPermanentIDsForObjects

I have a hierarchy of managed object contexts, like so:
A
/ \
B C
...where A's parent is the persistent store. A and C use a private queue, and B uses the main queue. I've found that doesn't seem to matter, but I'll note it anyway.
So, here is the best way to replicate the issue:
- Context B creates an object. Because I need to reference this same object multiple places, I obtain a permanent object ID via obtainPermanentIDsForObjects. I then save.
- Context A inherits the change, saves it out to the permanent store. Context C sees the change notification and merges the changes in. All is good so far.
- I grab the object from context C and change one of the properties. I then save. Context A inherits the change and saves it to disk.
This is where it gets weird. Context B never sees the change. There is a change notification I intercept like so:
-(void)contextDidSaveNotification:(NSNotification *)notification
{
NSManagedObjectContext *context = [notification object];
if(context!=_moc.parentContext)
return;
NSLog(#"Context %# did save, merging into %#, info:\n%#", context, _moc, [notification userInfo]);
[_moc mergeChangesFromContextDidSaveNotification:notification];
}
And I see the change go out, and seeing it merged in to context B. But the object I created in the first step never sees any changes.
I've traced this all back to one very specific thing. If I don't obtain a permanent object ID on the object in the very first step, the changes get merged back to the original object in context B just fine. But this leaves me without a reliable object ID to use in other contexts (as far as I know).
This seems like it could be a bug. I hope not. But for some reason when I obtain a permanent object ID it blocks changes from being merged back in from other contexts.
Has anyone else seen this behavior? Any solutions?
(I know changes from context C are making it in to A, and A is saving them to disk. If I relaunch the app the changes are there from disk.)

NSFetchResults update delegate works first time, crashes second time

I've looked around SO for similar answers, but my issue seems a litte different.
I have a UITableView that is tied to a NSFetchResultsController. The goal is to pull up some data, add a couple rows into the Context, and the table is automatically updated. Simple, right?
init -> empty table -> performFetch -> create some objects in the Context -> delegate sees this and updates my table.
I'm using the boilerplate NSFetchResultsController for noticing when the current context has been modified.
When I run this with a clean Simulator/iOS platform, the NSFetchController successfully recognizes that data in the Context has been updated. But if I run the app a second time, I get the following error:
CoreData: error: Serious application error. Exception was caught during Core Data change
processing. This is usually a bug within an observer of
NSManagedObjectContextObjectsDidChangeNotification. *** -[__NSArrayI objectAtIndex:]:
index 40 beyond bounds for empty array with userInfo (null)
The crash occurs on calling [self.tableView beginUpdates];
In my debugging I can see that '[fetchedResultsController fetchedObjects]' is completely empty and I think thats the problem - shouldn't this be updating with my test data since I modified the context? I'm using the Apple Recipe and CoreDataBooks examples as reference.
I think this is because you Data Modle in class just not fit the entity in you .xcdatamodeld file.

Resources