I have app that stores tree structure in CoreData.
There is an ManagedObject, "Item", and it has attributes:
itemId (string)
List item
title (string)
parentId (string)
parent (relationship to Item)
parentTitle (string)
parentId points to another Item object.
How do I make property parentTitle to be filled automatically with title of parent Item ?
While Martin's suggestion is a good solution for derived values, my question on yours is, why would you want this? You are not manipulating the value from the parent at all, ever. Since you are just accessing it, access the parent directly via KVC such as:
Item *item = ...;
NSString *title = [item valueForKeyPath:#"parent.title"];
//Do something with title
The only time you would want to use the keyPathsForValues... functionality is if you are changing something based on that value. If you are just accessing it, use KVC directly.
This is a possibility to achieve the desired functionality:
// implement in Item.m
// manages KVO notifications
+ (NSSet *)keyPathsForValuesAffectingParentTitle
{
return [NSSet setWithObjects:#"parent.title", nil];
}
// getter for parentTitle
- (NSString*) parentTitle
{
return [self valueForKeyPath:#"parent.title"];
}
additionally declare the property for parentTitle as readonly in Item.h
There is no need to declare a Core Data attribute "parentTitle".
The only problem I see with this solution is the following:
Item A is parent of item B
A gets turned into fault
B is still active and some View is bound to B.parentTitle
The view gets a notification because of the dependency declared with keyPathsForValuesAffecting, still object A is already faulted (and on shutdown unable to be unfaulted again) does Core Data manage such faulting&observation problems automatically?
Related
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.
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).
Background: I built an app w/ inexperience. It uses bindings, a NSArrayController subclass and Core Data/iCloud. The experience level caused a lot of code unnecessary writing, yet the app worked ok. A button linked to the NSArrayController add: method triggered newObject and addObject:. Removing never worked and led me to look at contentArray binding . A week ago, I began cleaning the app to more thoroughly use the simplicity of bindings. (Unfortunately, complexity also entered into this problem.)
Assumptions: 1) add: and remove: methods would automagically fire the methods to create and delete objects. Many simple tutorials seem to work this way. 2) The add:, addObject:, remove: and removeObject: methods do not need to be overridden.
Problem: addObject: and removeObject: do not get called (after some point of change) unless they are explicitly called in the add: or remove: method. I have never had the functionality of deleting both the array controller and managed object with a simple click on a remove button.
Question: What is required for the Core Data managed objects to be added and removed?
Other info: The array controller pictured below has Custom Class set to CheckinArrayController. The Core Data stack is initialized in AppDelegate. The newObject method has been overridden to preset some attributes.
EDIT: Override in array controller subclass add:, newObject, addObject:, and arrangeObjects:
Code in Array Controller (NSArrayController subclass)
#implementation CheckinArrayController
- (id)newObject {
id newObject = [super newObject];
// do object set up here ....
return newObject;
}
- (void)add:(id)sender {
/* without the next 2 no object is added, although this is called
NSManagedObject *newItem = [self newObject];
[self addObject:newItem]; */
}
- (void)addObject:(id)object {
[super addObject:object];
}
- (void)remove:(id)sender {
[super remove:sender];
}
- (void)removeObject:(id)object {
[super removeObject:object];
}
#end
Current array controller connections:
I would suggest creating a convenience init in your ManagedObject class. Here is another SO question that outlines this:
Designated Init for Managed Object
Call this init method when you want to create a new Managed Object. Your arraycontroller has a managedObjectContext from the bindings you have set up.
I use it like this:
let entity = NSEntityDescription.entity(forEntityName: "yourEntityName", in: managedObjectContext!)
let object = YourManagedObjectClassName(property1: "property1 entity: entity!, insertIntoManagedObjectContext: managedObjectContext)
I use the stock removeObject methods in the NSArrayController base class to delete objects.
removeObject Documentation
I would like to use the item that has been just been saved in the completion block of Magical Record saveWithBlock method. For example:
//Get the ID of an existing NSManagedObject to use in the save block (if it exists)
NSManagedObjectID *objectRef = [self.object objectID];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
//This method either loads an existing object and makes changes or creates a new entity in localContext
NSManagedObject *itemToSave = [self prepareItemInContext:localContext WithID: objectRef];
} completion:^(BOOL success, NSError *error) {
if (success) {
//here I want to get at the object 'itemToSave' that was either created in the save block (with a new objectID) or updated (with the ID objectRef)
Well, you need to have a reference to your external context to load the object with that ID:
NSManagedObjectContext *outsideContext = //...
NSManagedObjectID *objectID = //...
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
} completion:^(BOOL success, NSError *error) {
NSManagedObject *newlySavedObject = [outsideContext existingObjectWithID:objectID];
//...do stuff here
}];
Generally, however, I would discourage this usage. I would instead recommend keeping any predicates or means of reloading your data set handy, and dump and refetch fresh data from the store. This will give you proper object references. Another, more precise way of updating objects in other contexts is to listen to the NSManagedObjectContextDidSaveNotification and merge this updates into your context. From there, your data will be "refreshed" and as long as you're KVO'ing a property, or using a NSFetchedResultsController with a delegate, your updates will propagate to the UI (or other destination).
Either just use self.object or, if you create a new object and insert it (presumably because objectRef is nil) then you should get the corresponding new object from the main thread context and use that.
How you do that shuffle is the interesting part. It isn't exactly clear why you're using a background context at the moment so you can also consider changing that, which removes all of the complexity.
If you need to keep the background context then you need to decide on how to get that data back to the main thread. Generally, you could use performBlockAndWait: inside your current block to get the new object from the main context and then store it into a property on your class so you can use it in the completion block. This would be setting the self.object property.
I am creating a table view controller for an app that manages position assignments for a team. The sections with headers for defense, center, and offense for example will have names in them if a position Entity exists with that positionProperty. If that person is removed though through swiping, they become an alternate entity with same positionProperty.
I am trying to have the alternates for each position display when the edit button is tapped. Much like extra contact details appear when you edit a contact in the contacts app.
I have a fetchedResultsController returning the parent entity for alts/positions keyed by the positionProperty to define sections. (This may be the wrong way to do this... I am new to Core Data).
In setEditing:WithAnimation I have done the following. Attempting to search my fetched results and if any objects are of type Alternate, display that row. So in the enumeration, if it is type alternate I tried to call IndexPathForObjects:alt. This just returned nil...
if(editing){
[self.tableView beginUpdates];
for (MCAlternate *alt in fetchedResultsController.fetchedObjects) {
if ([alt isKindOfClass:[MCAlternate class]]) {
NSLog(#"The alternate is: %#", alt);
// This is where the error is trying to get indexPathForObject
NSIndexPath *index = [fetchedResultsController indexPathForObject:alt];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[index] withRowAnimation:UITableViewRowAnimationLeft];
}
}
[self.tableView endUpdates];
}
I have checked if the object exists in the results. It does. I have also tried called the getindexpath in a place with the object was just created by calling objectAtIndexPath and it still came back nil.
any suggestions are appreciated, Thanks!
Maybe you are not using your fetched results controller as it is foreseen. Your frc should fetch all the data to populate the table view.
You can still achieve the dynamic table content by tweaking your table view datasource methods. Suppose you are fetching the entity "Position" and the position object in question has a to-many attribute of entity "MCAlternate" called "alternates". You would then expand a certain section as follows:
-(void)expandSectionForPosition:(Position *)position {
int row = 0;
int section = [[frc indexPathForObject:position] section];
for (MCAlternate *alt in position.alternates) {
// update your datasource - e.g. by marking the position as
// "expanded"; make sure your numberOfRowsInSection reflects that
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath
indexPathForRow:i++ inSection:section]]
withRowAnimation:UITableViewRowAnimationLeft];
}
}
Your class check is not logical - you are casting as MCAlternate in the for loop anyway. Your call or getting the index path fails, because you typically do not have a separate fetched results controller.