Why can't I use a relationship in a NSManagedObject subclass computed property? (CoreData, swift) - ios

I'm using CoreData and I have a Book entity and a ReadingSession entity. Each Book has many ReadingSessions.
If I add this computed property to the Book class, it works:
var sessions: [ReadingSession] {
let request = NSFetchRequest(entityName: "ReadingSession")
let predicate = NSPredicate(format: "book = %#", self)
request.predicate = predicate
return try! DataController.sharedInstance.managedObjectContext.executeFetchRequest(request) as! [ReadingSession]
}
But if I add this one, it doesn't:
var sessions: [ReadingSession] {
return readingSession?.allObjects as! [ReadingSession]
}
This last example sometimes returns the correct array and sometimes just returns an empty array.
I tried the same thing with other relationships inside computed properties and the results are the same.
Why is that? Is there a reason why I shouldn't try doing this? Is my first example a valid workaround, or will it cause me problems later on? Should I give up using computed properties for this and just repeat the code when I need it?
Thanks in advance!
Daniel

Answering my own question, as Wain pointed out in the comments I should be able to use relationships inside computed properties, and my problem was actually somewhere else.
If you're interested in the details read the next paragraph, but, long story short, if you're having the same problem you should look into your relationships and make sure they're set properly as To One or To Many. Also check if you're setting all your properties in the right places and only when necessary.
I edited my question to remove lots of unnecessary details and make it more readable, but in the end the problem was that I had a User entity with a selectedBook property which was set when a user selected a row. I had set it up as a To Many relationship, but a user can have only one selectedBook at a time, so it should have been a To One relationship there. Also when I created a book I set user.selectedBook to it, but the selectedBook property should only be set when a user selected a book from a row. So I was setting and trying to access some of my relationships at all the right times. I tried to access a user.selectedBook before a user had even selected a row, for instance, and then it obviously returned nil, which messed up many other computed properties. Now I fixed all that and I'm able to access all my relationships from within computed properties without any issues.

Related

Is there a way to access properties of an x-coredata:// object returned from an NSFetchRequest?

TL;DR: Is there a way to programmatically read/recall (NOT write!) an instance of a Core Data entity using the p-numbered "serial number" that's tacked on to the instance's x-coredata:// identifier? Is this a good/bad idea?
I'm using a method similar to the following to retrieve the instances of an Entity called from a Core Data data store:
var managedContext: NSManagedObjectContext!
let fetchRequest : NSFetchRequest<TrackInfo> = TrackInfo.fetchRequest()
fetchResults = try! managedContext.fetch(fetchRequest)
for (i, _) in Global.Vars.numberOfTrackButtons! {
let workingTrackInfo = fetchResults.randomElement()!
print("current track is: \(workingTrackInfo)")
The list of tracks comes back in fetchResults as an array, and I can select one of them at random (fetchResults.randomElement()). From there, I can examine the details of that one item by coercing it to a string and displaying it in the console (the print statement). I don't list the code below, but using workingTrackInfo I am able to see that instance, read its properties into other variables, etc.
In the console, iOS/Xcode lists the selected item as follows:
current track is: <MyProjectName.TrackInfo: 0x60000374c2d0> (entity:
TrackInfo; id: 0xa7dc809ab862d89d
<x-coredata://2B5DDCDB-0F2C-4CDF-A7B9-D4C43785FDE7/TrackInfo/p22>;
data: <fault>)
The line beginning with x-coredata: got my attention. It's formatted like a URL, consisting of what I assume is a UUID for the specific Core Data store associated with the current build of the app (i.e. not a stable address that you could hardcode; you'd need to programmatically look up the Core Data store, similar to the functions we use for programmatically locating the Documents Folder, App Bundle, etc.) The third item is the name of the Entity in my Core Data model -- easy enough.
But that last number is what I'm curious about. From examining the SQLite database associated with this data store, it appears to be a sort of "instance serial number" associated with the Z_PK field in the data model.
I AM NOT interested in trying to circumvent Core Data's normal mechanisms to modify the contents of a managed object. Apple is very clear about that being a bad idea.
What I AM interested in is whether it's possible to address a particular Core Data instance using this "serial number".**
In my application, where I'm randomly selecting one track out of what might be hundreds or even thousands of tracks, I'd be interested in, among other things, the ability to select a single track on the basis of that p-number serial, where I simply ask for an individual instance by generating a random p-number, tack it on to a x-coredata:// statement formatted like the one listed above, and loading the result (on a read-only basis!) into a variable for further use elsewhere in the app.
For testing purposes, I've tried simply hardcoding x-coredata://2B5DDCDB-0F2C-4CDF-A7B9-D4C43785FDE7/TrackInfo/p22 as a URL, but XCode doesn't seem to like it. Is there some other data Type (e.g. an NSManagedObject?) that allows you to set an x-coredata:// "URL" as its contents?
QUESTIONS: Has anyone done anything like this; are there any memory/threading considerations why grabbing instance names in this manner is a bad idea (I'm an iOS/Core Data noob, so I don't know what I don't know; please humor me!); what would the syntax/method for these types of statements be?
Thanks!
You are quite close.
x-coredata://2B5DDCDB-0F2C-4CDF-A7B9-D4C43785FDE7/TrackInfo/p22
is the uriRepresentation() of the NSManagedObjectID of the record.
You get this URL from an NSManagedObject with
let workingTrackInfo = fetchResults.randomElement()!
let objectIDURL = workingTrackInfo.objectID.uriRepresentation()
With this URL you can get the managed Object ID from the NSPersistentStoreCoordinator and the coordinator from the managed object context.
Then call object(with: on the context to get the object.
let persistentStoreCoordinator = managedContext.persistentStoreCoordinator!
if let objectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: objectIDURL) {
let object = managedContext.object(with: objectID) as! TrackInfo
print(object)
}

objectID many to many relationship

I need some help with my Swift rookie programming...
In a many-to-many relationship, I have a NSManagedObjectID that I`ve segued from another view controller.
To retrive data from it I have used this:
var elevid :NSManagedObjectID?
let person = context.object(with: studentId!)
nameTextField.text = person.value(forKey: "name") as? String
This works fine, but when I try to get an attribute from a relationship I`m stuck.
I´ve tried this:
let isAtSchool = person.value(forKeyPath: "isAtSchool.monday") as! Bool
but I get an error telling me:
Could not cast value of type '__NSSingleObjectSetI' (0x10ac63aa8) to 'NSNumber' (0x109e5a4a8).
If I use ? after as instead of ! it returns nil.
Someone know how to do this?
When you ask for the name property you're asking for a single value, so that's no problem. But when you're using this key path you're traversing a to-many relationship. There could be 2 or 10 or a million related objects, but you you're asking for a single Bool. How is that supposed to work?
It's not clear what you actually want in this situation. Of those potentially millions of related objects, how do you want to calculate the value of that Bool? Probably you want to do something like pick out a single related object out of those (potential) millions and get the Bool from that single instance. But for all I know you might want to scan over all of them and see what the most common Bool value is.
How to change your code depends on what you really need, how that single Bool value should be determined. One way or another you need to get from (potential) millions of related objects to a single Bool-- and you can't do that via a key path lookup.

Manipulate Relationships in Core Data

I have a NSManagedObject subclass defined like this:
extension Item {
#NSManaged var name: String
#NSManaged var parent: Item
#NSManaged var children: [Item]
}
In order to set the 'parent' attribute, I use this:
anItem.parent = aParent
And I know it will automatically append anItem to aParent.children too.
However, I would like the child item to be added at a specific position.
This idea is to have a sorted array of children.
I know I could simply do this:
aParent.childen.insert(anItem, atIndex: specificPosition)
But is there any method to override or operator to overload in order to automatically achieve that ?
Inserting the child at a particular position will not ensure the same sort order later when you fetch.
You will need an additional attribute to define the sort order during the fetch.
Have a look at this answer
A couple misconceptions here:
CoreData to-many relationships are not of type Array, they are Set or NSSet. This can be verified by having your subclass generated by Xcode.
It is possible to to have an ordered relationship - just take a look at the relationship inspector in your Core Data Model. This will change it to a NSOrderedSet
MAJOR CAVEAT to (2) - The order is determined exclusively by the order that you add them - see this link for more info.
It is much more memory intensive to store these as ordered, if you can, have an attribute you can use to order them after fetching.

Will CoreData duplicate objects?

I have an NSManagedObject called "Routine," that has already been saved to my data model. It has a to-many relationship to another NSManagedObject called "Workout". I want to edit the Routine in order to add more workout relationships to it to it.
let routine = fetchedResultsController.objectAtIndexPath(indexPath) as! Routine
The ViewController where I edit the Routine in my data model contains an array of Workout objects:
var myWorkouts = [Workout]()
Some of the workouts in the "myWorkouts" array may have been already associated with Routine, whereas others may not have (a new workout). I create a relationship between each workout and the routine like this:
for workout in myWorkouts {
routine!.setValue(NSSet(objects: workout), forKey: "workout")
}
My Question: If a relationship between a Routine and a Workout has already been created, will the above for-loop create a duplicate of that workout to associate with my routine, or will it only create new relationships for previously unassociated workouts?
I hope my questions makes sense. Thanks in advance for the help!
Routine CoreDataProperties File
import Foundation
import CoreData
extension Routine {
#NSManaged var name: String?
#NSManaged var workout: Set<Workout>?
}
So, you're working with Sets, which means that they only always contain each value once. Therefore, regardless of the enclosing objects (in this case NSManagedObjects), there will only be one in there. You're good - re-stating the relationship won't change anything.
I might suggest, however, that you can do a couple of things to make your life easier:
If you haven't already, create the concrete subclasses using Xcode's built in tools, so you can directly access relationships and properties.
In the concrete subclasses +NSManagedObjectProperties file, redefine those to-many relationships from NSSets? to Set<MyClass>?. This allows you to call Swift-native functions, and works correctly, as Set is bridged from NSSet.
In the future, just call routine.workout = workout, which is much clearer than the way your code defines setting the relationship.

Realm linkingObjects array vs. Results

I have a Book class and a ReadingSession class, with relationships declared like so:
public class Book: Object {
// (…)
var readingSessions: [ReadingSession] {
return linkingObjects(ReadingSession.self, forProperty: "book")
}
}
public class ReadingSession: Object {
// (…)
dynamic var book: Book?
}
Now I wanted to use Realm’s Results’ methods like filter() and sum() on that readingSessions property, but I can't, because it's a regular array.
So I added another computed property to my Book class:
var sessions: Results<ReadingSession> {
let realm = try! Realm()
return realm.objects(ReadingSession).filter("book == %#", self)
}
Now when I need to use those methods I go for the sessions porperty, and when I don't I use the readingSessions array.
So my question is: why is the first way, using the linkingObjects() method, recommended in Realm’s documentation? Is there a reason why I shouldn't completely replace that property with my latest one, using Realm’s Results? In my experience, working with Realm’s Results is usually a lot faster, besides the extra methods, and even if I need a regular array I can just convert the Results then. Is there any disadvantage to doing that?
Thanks in advance,
Daniel
For now, there is no better way than what you're currently doing. Using linkingObjects would be likely faster as the relationship can be navigated internally bidirectionally and the query doesn't need to be evaluated for the full dataset, so it also depends on the size of that. We plan to change that behavior as seen in issue #1508.

Resources