CoreData - One-to-many relationship - swift

I'll post the following code then explain my struggle
This function saves a day (like sunday, monday, tuesday, etc):
func appendDaysToArray() {
let dayLabel = dayName.text
daysArray.append(dayLabel)
let dayEntity = NSEntityDescription.entityForName("TrainingDay", inManagedObjectContext: moc!)
let trainingday = TrainingDay(entity: dayEntity!, insertIntoManagedObjectContext: moc)
trainingday.day = dayName.text
var error: NSError?
moc?.save(&error)
if let err = error {
var status = err.localizedFailureReason
println("\(status)")
} else {
println("Day #\(dayName.text) saved successfully!")
}
}
and this one saves details as a name, a number of sets and a number of repetitions (like gym exercises):
func appendTrainingDetails () {
let nameLabel = exerciseName.text
namesArray.append(nameLabel)
let numberOfSets = setsNumber.text?.toInt()
setsArray.append(numberOfSets!)
let numberOfReps = repsNumber.text?.toInt()
repsArray.append(numberOfReps!)
let detailsEntity = NSEntityDescription.entityForName("TrainingDetails", inManagedObjectContext: moc!)
let trainingdetails = TrainingDetails(entity: detailsEntity!, insertIntoManagedObjectContext: moc)
trainingdetails.exerciseName = exerciseName.text
trainingdetails.setsNumber = setsNumber.text!
trainingdetails.repsNumber = repsNumber.text!
var error: NSError?
moc?.save(&error)
if let err = error {
var status = err.localizedFailureReason
println("\(status)")
} else {
println("Exercise: #\(exerciseName.text) saved successfully!")
println("Number of sets: #\(setsNumber.text) saved successfully!")
println("Number of reps: #\(repsNumber.text) saved successfully!")
}
}
My app is working ok, but what I actually need is this: for each DAY, I will have multiple exerciseNames, setsNumber and repsNumber. I set a one-to-many relationship, but I don't know how to attribute the TrainingDetails to each day in the daysArray.
Here are my 2 models:
import Foundation
import CoreData
class TrainingDay: NSManagedObject {
#NSManaged var day: String
#NSManaged var relationship1: NSSet
}
and
import Foundation
import CoreData
class TrainingDetails: NSManagedObject {
#NSManaged var exerciseName: String
#NSManaged var repsNumber: String
#NSManaged var setsNumber: String
#NSManaged var relationship2: TrainingDay
}
Later, I'll have a button for each day and, when pressed, they will update a tableView with the list of exercises for that specific day. That's why I need to set this one-to-many relationship.
How can I achieve this?
Sorry for any mistakes. Thanks in advance!!

The documentation for what you want to do is in this link, under the heading "To-Many Relationships".
Here is a short example. First, I recommend changing the names of your relationships to something more intuitive. It will really help:
class TrainingDay: NSManagedObject {
#NSManaged var day: String
#NSManaged var trainingDetails: NSSet
}
class TrainingDetails: NSManagedObject {
// ... other stuff here
#NSManaged var trainingDay: TrainingDay
}
Make sure any changes you make to the code are also made in the model graph. Make sure that the relationships are configured to be the inverse of each other in the model graph.
You can set the TrainingDay for a given TrainingDetails like this:
// This assumes you've created a TrainingDay called "trainingDay1", and a
// TrainingDetails object called "details".
details.trainingDay = trainingDay1
Core Data takes care of creating the inverse relationship as well, so the trainingDay1 object will automatically add details to its trainingDetails set.
If you are trying to add objects to the trainingDetails set in TrainingDay, you need to use the mutableSetValueForKey: method described in the documentation that I linked to. Basically, it looks like this:
var details = trainingDay1.mutableSetValueForKey("trainingDetails")
details.addObject(newTrainingDetails)
The mutableSetValueForKey: creates a proxy object. Any changes made to the set that it returns are effective on the set you are trying to modify.
Hope this helps!

Related

Swift CoreData: Unable to section tableView using sectionNameKeyPath with custom function

I am an extreme rookie with CoreData and I am attempting to section my tableView using fetchedResultsController using a custom function. My current implementation did not manage to section the tableView and I am still given just 1 section.
I refer to the following posts in my implementation: here and here. I also adopted the transient property.
I first create the NSManagedObject subclass using Xcode (Editor -> Create NSMangedObject Subclass) and added the var sectionIdentifier to return a custom string. Unfortunately, my frc returns only 1 section.
// from the Conversation+CoreDataProperties.swift file automatically generated by Xcode
import Foundation
import CoreData
extension Conversation {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Conversation> {
return NSFetchRequest<Conversation>(entityName: "Conversation")
}
#NSManaged public var conversationStartTime: Double
#NSManaged public var profileImage: NSData?
#NSManaged public var recipientID: String?
#NSManaged public var recipientUsername: String?
#NSManaged public var shoutoutID: String?
#NSManaged public var unreadMessagesCount: Int32
var sectionIdentifier: String? {
let presentTimestamp = NSDate().timeIntervalSince1970
if conversationStartTime < presentTimestamp - Double(Constants.PermissibleDurationInMinutes * 60) {
return "Expired Conversations"
} else {
return "Active Conversations"
}
}
}
//at VC
lazy var fetchedResultsController: NSFetchedResultsController<Conversation> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<Conversation> = Conversation.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "conversationStartTime", ascending: true)]
let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
frc.delegate = self
do {
try frc.performFetch()
} catch let err {
print(err)
}
return frc
}()
Printing out the conversation entity in console returns this
<Conversation: 0x604000e93100> (entity: Conversation; id: 0xd000000003e00000 <x-coredata://91BC90B2-9A0C-45A7-9B82-844BE88BAFE0/Conversation/p248> ; data: {
conversationStartTime = "1521359598.88445";
profileImage = <ffd8ffe0 00104a46 49460001 02000001 00010000 ffed009c 50686f74 6f73686f 7020332e 30003842 494d0404 00000000 0080>;
recipientID = "-L7rvH71i-KUXvLQVDOh";
recipientUsername = Angemon;
sectionIdentifier = nil;
shoutoutID = "-L7rvH71i-KUXvLQVDOh";
unreadMessagesCount = 0; })
Somehow sectionIdentifier is always nil. Any advice why is this happening? At the end of the day, I want to divide my list of conversations into two sections, first section "Active Conversations" and second section "Expired Conversations" depending how long ago that conversation is.
UPDATED CODE:
// At Conversation+CoreDataProperties.swift
import Foundation
import CoreData
extension Conversation {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Conversation> {
return NSFetchRequest<Conversation>(entityName: "Conversation")
}
#NSManaged public var conversationStartTime: Double
#NSManaged public var profileImage: NSData?
#NSManaged public var recipientID: String?
#NSManaged public var recipientUsername: String?
#NSManaged public var shoutoutID: String?
#NSManaged public var unreadMessagesCount: Int32
#objc var sectionIdentifier: String {
willAccessValue(forKey: "sectionIdentifier")
let presentTimestamp = NSDate().timeIntervalSince1970
var text = ""
if conversationStartTime < presentTimestamp - Double(Constants.PermissibleDurationInMinutes * 60) {
text = "Expired Conversations"
} else {
text = "Active Conversations"
}
didAccessValue(forKey: "sectionIdentifier")
return text
}
}
//At VC
lazy var fetchedResultsController: NSFetchedResultsController<Conversation> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<Conversation> = Conversation.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(key: "conversationStartTime", ascending: false)
]
let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
frc.delegate = self
do {
try frc.performFetch()
} catch let err {
print(err)
}
return frc
}()
At my CoreData model screen:
This is a continuation on the answer from #Sandeep since I wanted to see if it was possible to group by a transient property without having a sort descriptor with it since one of your links implied it should be possible to do so.
After some experimentation I managed to conclude that it is possible if you fulfil two conditions:
You need a sort descriptor (and it needs to be the first one) on the persistent attribute you use for calculating your transient attribute
The sort order of the sort descriptor needs to match the transient attribute.
(I assume this applies for when you access multiple persistent attributes as well for your transient attribute, sort descriptors for all of them and sort order needs to match but I haven't tested)
As I see it you fulfil the first one but not the second one since your sort descriptor has ascending = true but since "Active" comes before "Expired" you have an implicit descending sort order for sectionIdentifier.
Here is a silly example I made with some existing code since I had some test data available for it. As you can see I divide the days of the month into three sections so that the will show up in reverse chronological order.
#objc var silly: String {
willAccessValue(forKey: #keyPath(silly))
var result: String = "In between"
let component = Calendar.current.component(Calendar.Component.day, from: date)
if component <= 13 {
result = "Last"
} else if component > 20 {
result = "At the Top"
}
didAccessValue(forKey: #keyPath(silly))
return result
}
And when setting up my fetched result controller I do
...
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Location.date), ascending: false)]
let fetchedResultController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: #keyPath(Location.silly),
cacheName: "Locations")
The will/didAccessValue(...) was necessary to handle insert and updates of new objects after the first execute of the request, otherwise the attribute is set to nil.

Looping Core Data Objects outside a Table/ Collection View

I want to loop through the objects in CoreData without using a tableView or CollectionView... But using a ViewController
I tried something like:
for var i = 0; i < numberOfExerciseItems; i++ {
let exerciseItemsfromDay = fetchedResultController.objectAtIndexPath(i) as! ExerciseItemModel
}
This obviously doesn't work since it is not of NSIndexPath type as you'd get in a table or CollectionView. Are there ways to do this outside a Table / Collection View? Thanks for the ideas in advance.
Edit
From this answer, I'd want to access an entity from an item in the items array in the loop :
let request = NSFetchRequest(entityName: "ExerciseItemModel")
let items = (try? context.executeFetchRequest(request)) as? [ExerciseItemModel] ?? []
In this loop
items.forEach {
print(items) //Displays all the objects in the console.
print(items[1].attribute //Throws the error Value of type'AnyObject' has no member 'attribute' and I cannot access an attribute from the items array . Not sure why!
}
Edit My ExerciseModel Class
import Foundation
import CoreData
#objc(ExerciseItemModel)
class ExerciseItemModel: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
}
extension ExerciseItemModel {
#NSManaged var exerciseType: String?
#NSManaged var exerciseName: String?
#NSManaged var durationOrSets: String?
#NSManaged var distanceOrReps: String?
#NSManaged var weight: String?
#NSManaged var backgroundImage: NSData?
#NSManaged var dayID: String?
#NSManaged var date: NSDate?
}
let request = NSFetchRequest(entityName: "ExerciseItemModel")
let items = (try? context.executeFetchRequest(request)) as? [ExerciseItemModel] ?? []
items.forEach {
// Do your stuff
}
A small example. Not exactly an answer so I'll delete it when a proper answer has been posted. This example will not show you any error messages if there are any, just keep that in mind.
Edit:
You're using forEach() incorrectly:
items.forEach {
print($0) // Print's the object in items
print($0.attribute) // Should print the attribute
}
print(items[0].attribute) // Probably same error, but it should be used outside the loop

Core Data Join Table Many to Many Relationship - Object Creation

I want to create a join table (BadgeProfile) for 2 entities: HubProfile & Badge. See pic below
Issue 1: I want to know how to create a "BadgeProfile" object. This object should map to one "HubProfile" and one "Badge" Object
Issue 2: Once the "BadgeProfile" object is created which can later be accessed by both "HubProfile" & "Badge". (example: HubProfile.BadgeProfile) How do I do that?
Below are the classes for the entities:
HubProfile
class HubProfile: NSManagedObject {
#NSManaged var id: NSNumber?
#NSManaged var name: String?
#NSManaged var hubBadgeProfiles: NSOrderedSet?
}
Badge
class Badge: NSManagedObject {
#NSManaged var id: NSNumber?
#NSManaged var name: String?
#NSManaged var score: NSNumber?
#NSManaged var badgeProfiles: NSOrderedSet?
}
BadgeProfile
class BadgeProfile: NSManagedObject {
#NSManaged var id: NSNumber?
#NSManaged var badge: Badge?
#NSManaged var hubProfile: HubProfile?
}
P.S: I am aware that I don't need to create a Join Table and can go with a Many to Many relationship between HubProfile <<-- -->> Badge. But I want to create the join table as it will make it very easy to interact with the backend DB.
To create the join table object:
Create it the same way you would create any other entity, e.g. via
NSEntityDescription.insertNewObjectForEntityForName(...)
Then just add the objects you want to link.
badgeProfile.hubProfile = hubProfile
badgeProfile.badge = badge
I would advise not to use ordered sets. They are complicated and error prone. The above, for example, becomes a real headache. Instead, use a number attribute in the BadgeProfile entity to keep track of the order yourself.
To access the joined object
Nothing special here. Get all badges for a profile:
profile.hubBadgeProfiles.map { $0.badge! }
(I think your attribute name is poorly chosen. I suggest badges, and the equivalent on the other side, profiles for the Badge entity.)
Get a specific badge:
profile.badges.filter { $0.badge!.name == "Gold" }.first as? Badge
Use the analog pattern to go in the other direction.
I think I have found the solution:
Creating Sample Objects:
func loadBadgeProfilesPart2() //Second Way to Create BadgeProfile
{
let hubProfileEntity = NSEntityDescription.entityForName("HubProfile", inManagedObjectContext: managedContext)
let hubProfile1 = HubProfile(entity: hubProfileEntity!, insertIntoManagedObjectContext: managedContext)
hubProfile1.id = 5
hubProfile1.name = "EFG"
let badgeEntity = NSEntityDescription.entityForName("Badge", inManagedObjectContext: managedContext)
let badge1 = Badge(entity: badgeEntity!, insertIntoManagedObjectContext: managedContext)
badge1.id = 5
badge1.name = "Polite"
let badgeProfileEntity = NSEntityDescription.entityForName("BadgeProfile", inManagedObjectContext: managedContext)
let badgeProfile1 = BadgeProfile(entity: badgeProfileEntity!, insertIntoManagedObjectContext: managedContext)
badgeProfile1.id = 3
let badgeProfile2 = BadgeProfile(entity: badgeProfileEntity!, insertIntoManagedObjectContext: managedContext)
badgeProfile2.id = 4
let badgeProfileSet: NSMutableOrderedSet = [] //Add to an NSOrderedSet
badgeProfileSet.addObject(badgeProfile1)
badgeProfileSet.addObject(badgeProfile2)
hubProfile1.hubBadgeProfiles = badgeProfileSet //Assign BadgeProfile to HubProfile
badge1.badgeProfiles = badgeProfileSet //Assign BadgeProfile to Badge
do
{
try! self.managedContext.save()
}
}
Fetching the Objects in TableView
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! BadgeProfileViewCell!
configureCell(cell, indexPath: indexPath)
return cell
}
func configureCell(cell: BadgeProfileViewCell, indexPath: NSIndexPath) //Showing in TableView
{
let badgeProfile = fetchedResultsController.objectAtIndexPath(indexPath) as! BadgeProfile
cell.nameLabel?.text = badgeProfile.hubProfile?.name
cell.badgeLabel?.text = badgeProfile.badge?.name
}
This seems to be working.

Realm Cannot invoke 'add' with an argument list of type '(Person)'

This error is so frustrating. I'm just trying to add an object to my Realm database and I have gotten to the point of just copying and pasting example code and it will not work. So I have an add person method that does this:
func addPerson(person person:Person){
realm.beginWrite()
realm.add(person)
realm.commitWrite()
}
And the realm variable is stored like this in the class header:
private var realm:Realm
Being initialized in the init() method as such:
realm = Realm()
My actual person class looks like this:
import UIKit
class Person {
var name:String?
var relation: Relations?
var title: String?
var importance:Double?
var events:Array<Event>?
var image:UIImage?
init(name:String,relation:Relations?,events:Array<Event>?,image:UIImage?){
self.relation = relation
if relation != nil{
self.title = relation!.title
self.importance = relation!.importance
}
else{
self.title = nil
self.importance = nil
}
self.events = events
self.image = image
self.name = name
}
init() {
}
}
The error is so frustrating because it seems like the Person class does not conform to the Object superclass, but in reality, that's the only option
This is just a summary of what I said in the comments:
Use Object as a subclass as it is necessary for a class to be a model in Realm.

Populating coredata many-to-many relationship

I have a Worker and Event Entity. I'm having trouble when creating a new worker for a Event the worker entity has a corresponding event but the event entity is not saving the workers.(shouldn't this be happening automatically when setting the inverse option)
class Event: NSManagedObject {
#NSManaged var id: NSNumber
#NSManaged var name: String
#NSManaged var startTime: String
#NSManaged var endTime: String
#NSManaged var workers: NSMutableSet
func addWorker(name: String, startTime: String, endTime: String) {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
var worker: Worker = NSEntityDescription.insertNewObjectForEntityForName("Worker", inManagedObjectContext: managedContext) as! Worker
worker.name = name
worker.startTime = startTime
worker.endTime = endTime
worker.id = Worker.getNewWorkerId()
worker.events.addObject(self)
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
}
}
You must also define the relationship to be "To Many" on both ends of the relation. To define the relationships as such, select the relationship in question and on the right-pane set 'Type' to 'To Many'.
Additionally, your many to many attributes in your subclasses of NSManagedObject should be of type NSSet and NSOrderedSet, not NSMutableSet. See the documentation here: Managed Object Accessor Methods.
If you want to add any additional information about the relationship an 'associative' or 'join' entity will be required.
(CoreData Many-To-Many Relationships)