Design for a chat app using Core Data - iphone

I'm writing a chat app and I'm in the process of changing my db to use Core Data. I currently use sqlite directly but I want to take advantage of iCloud feature so I'm switching the engine.
My main table is called Entry with the following properties:
NSInteger type;
NSDate* timestamp;
NSString* username;
NSString* session;
NSString* body;
where 'type' can be:
1 - message
2 - file transfer (which then 'body' represents a file name in the documents folder)
3 - user joined
4 - user left
My app also supports multi-user chat (hence why the 'user joined'/'user left' types). All messages belong to the same conversation (multi-chat only), will have a valid 'session' property.
In my chat history, my problem is how to achieve the 'load more' like Apple did in the SMS app: I will query based on 'username=%# AND session IS NULL' or 'session=%#' to show that history and use a LIMIT of 50 sorted by reversed 'timestamp'.
I then want to have a button "Load more" which will load the next 50 messages - I'm not sure how to do it with Core Data.
My next question is how to show the list of conversations. Right now with raw sqlite, I perform a join on 2 queries: the first is the last message of each user and the second is the last message of each multi-user conversation. I then sort them all by date.
Since Core Data does not support joins, I'm not sure how to perform this query.
Thanks

Having an app that does exactly the same thing, here are my insights.
First of all you should consider coredata and multithreading wisely before coding. If you need help on that let me know.
The model
You are working with entities in Coredata, which can be considered like tables in sqlite, but in a more abstract way. You should review Apple's documentation for that.
We can find at least three different entities in your case : User, Conversation, and Message. (be careful with the last one, I had an issue with the entity called Message when importing the SMS Framework, you should consider prefixing the name of the entity..)
An issue with coredata is that you can not store directly arrays (may be with some unknown type) but anyway. So two solutions to store your users : either in a NSString when they will be delimited by comas and a simple regex or split will give you the number of users..
so your model could look like :
Conversation{
messages<-->>Message.conversation
lastMessage<-->Message.whateverName
//optional
users<<-->>User.conversation
}
Message{
conversation<<-->Conversation.messages
whatevername<-->Conversation.lastmessage // "whatever as it does not really matter"
}
User{
conversations<<-->>Conversation.users
}
Conversation must have an to-many relationship to Message and Message a to-one relationship to Conversation.
--EDIT
If you want to display the last message of a conversation just like the message App (or my app), you can add one relationship with message. It won't store the message twice in the database/coredata. Indeed, you create a coredata object (in this case a message) and that you add it to a conversation, what happen inside is that a conversation store the coredata ID for that object. Adding one relationship for this message (lastMessage) will only store another ID, and not another object.
--end of EDIT
Users are slightly different because they can be part of multiple conversations (because of the group conversation) that is why you need a many-to-many relation ship.
You can add as many attributes as you want, but that's the minimal requirement !
Implementation
then in your code, if you want to mimic the behavior of iMessage, here is what I did :
in the first controller, where you can see all the conversation : use a NSFetchedResultController. The query should be only about the entity Conversation.
When clicking on a row, what I did is that the new view has the conversation object and another NSFtechedResultController. I then query only the entity Message but with a predicate specifying that I only want this conversation.
If you want to check my app to see the fluidity, go to this link.
EDIT
Code snippet to find the last Message Of A Conversation
Beware: This is a temporary answer before finding a better way to do it (i.e. when using fetched properties)
NSFetchRequest * req = [[NSFetchRequest alloc] init];
[req setEntity:[NSEntityDescription entityForName:#"Message" inManagedObjectContext:context]];
[req setPredicate:[NSPredicate predicateWithFormat:#"conversation == %#", self]]; /* did that from a Conversation object.. */
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"sent_date" ascending:NO];
[req setSortDescriptors:[NSArray arrayWithObject:sort]];
[sort release];
NSError * error = nil;
NSArray * messages = [context executeFetchRequest:req error:&error];
[req release];
if ([messages count] > 0) { /* sanity check */
return [messages objectAtIndex:0];
}
return nil;
--end of EDIT
Hope this help !
Pierre

First, your mental model is all wrong. You should not think of core data as a SQL database. Yes, most of the time it uses SQL, but it is merely an implementation detail. You should think in terms of object graphs.
Next, for your "50 items" issue, look at NSFetchRequest. You can tell it where to start (fetchOffset), and how many items to fetch (fetchLimit). There are other options for you as well. If your total number of items is relatively small, you can just fetch the entire array (and only fault so many at a time - see fetchBatchSize).
For your "join" consider how objects are related to each other, not database table joins. Unfortunately, I do not understand what you are trying to achieve with that part of the question. However, you can mimic "joined" tables by using the dot notation when forming your predicate.
EDIT
When you create a conversation object, you can include a to-many relationship to something like "participants" which would be a set of all the users that participated in that conversation. The inverse would also be a to-many relationship in "user" that contained all the conversations that user participated in (I assume your database has multiple users???).
So, to get all the conversations in which a particular user participated, you could do something like fetch on "Participant" with a predicate similar to "ALL participants.username = %#"

Related

Core data: NSFetchedResultsController with objects are in a relationship

I have two entities CIDMPost and CIDMUser. CIDMPost has a one-to-many relationship with CIDMUser named invitees.
Now I have an instance of CIDMPost lets say postObject from where I can easily get the invitees by writing postObject.invitees which will return NSSet.
Now my requirement is I want those invitees (postObject.invitees) as NSFetchedResultsController to show in a UITableView with style Group. Also its need to satisfy the below.
Grouped by invitationStatus (an attribute of CIDMUser)
Order by invitationStatus ASC
Declaration:
I tried few things to fulfil my requirement but got a crashed and the reason was Invalid to many relationship. To know the solution of this crash I posted 'a question' an hour ago. But after few conversation with Daij-Djan I had come to know that the whole process I was trying is wrong.
Just fetch the users in your fetched results controller and apply the sort descriptors as desired filter by invitation with a predicate. Use the status field as the sectionNameKeyPath argument when creating the fetched results controller.
NSPredicate(format: "%# in invitations", post)
where post is the post object to which you want to display the invitees, and invitations is the reverse to-many relationship to the post to which users are invited.
(Note that I am assuming a user can get invited to more than one post, which seems logical. In your problem statement you mention a one-to-many relationship which it seems to me should thus be many-to-many).

How to model my Core Data entity?

I want to store NoteObjects in Core Data. Normally, a NoteObject has a NSString *mainText and an NSMutableArray *arrayOfTags (an array of NSStrings). I want to now use Core Data, but arrays are a tricky matter with core data. Typically a NoteObject won't have more than 50 tags in its array. So how should I model this? I have two options:
Use a transformable property to store the array
Use a to-many relationship, which I've read is the more "legit" way to do it.
Which one should I use and why? And how would I implement a to-many relationship with my simple structure? I can't seem to wrap my fingers around that concept.
Use to-many relationship. Because it's way better and easier during fetch requests. See the screenshots below. Pay attention to the Relationship manager on the right side, set "To-Many Relationship" from your NoteObject to Tags. Ignore the Player entity.
Oh and pay attention to the "Delete Rule". You might want to delete all the tags associated with a given NoteObject. So set it to Cascade in that case.
NoteObject entity
Tag entity
--Edit:
To add multiple tags you need to first fetch your NoteObject - I assume there will be some sort of ID parameter which you'll use to distinguish NoteObjects. CoreData will automatically generate the add/remove methods for Tags. You'll need to use code similar to the one below:
- (void)addTags:(NSArray *)tags toNoteObjectWithID:(NSString *)noteID {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"NoteObject"];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"noteID == %#", noteID];
[fetchRequest setPredicate:pred];
NoteObject *noteObject = [[[self managedObjectContext] executeFetchRequest:fetchRequest error:nil] lastObject];
for (NSString *tag in tags) {
Tag *t = [NSEntityDescription insertNewObjectForEntityForName:#"Tag"
inManagedObjectContext:[self managedObjectContext]];
t.body = tag;
t.noteObject = noteObject;
[noteObject addTagsObject:t];
}
[self saveContext];
}
You could use a transformable property, but then you need to write the transformer.
If you use a toMany relationship, you have to create an additional entity for tags, which presumably has only one attribute - the string value, and a single relationship. Extrapolating a little, I would guess that you have a finite set of values tags can take on, and you might someday want all notes that have tag X - then you would be able to fetch the entity containing the string value for X and then use that to fetch the NSArray of objects that have X in the relationship (whatever you called it).
Arrays were only tricky in Core Data because they weren't supported prior to iOS 5, so you had to include some attribute (like creation date) by which they could be sorted. If you don't mind requiring iOS 5, you can use ordered relationships. Results are returned in an NSOrderedSet, which is a lot like (and can can be converted to) an array. Or, just re-think the problem -- is the order of the tags on a note important to the note or the user? Would it be okay if you just display the tags in alphabetical order? If so, even better -- just use a plain old (unordered) to-many relationship.

Updating/Setting relationships in Core Data when users enter data from different tableviews

I have 3 entities in my core data app: Criteria--->>Rank(has 1 attribute:criteriaRank)<<---Option
Let's a user enters a few options in a tableview and then goes to another tableview where they enter some number of criteria. The way my app is suppose to behave is that for each criteria that the user enters, it should create a Rank object so that when a user goes back to the options tableview and select an option, it should allow them to rank each criteria they entered.
Right now, when I create a new criteria I set it the following way,
//Create new Criteria and set its context
Criteria *newCriteria = [NSEntityDescription insertNewObjectForEntityForName:#"Criteria" inManagedObjectContext:context];
for (Option *anOption in sortedOptions) {
Rank *newCriteriaRank = [NSEntityDescription insertNewObjectForEntityForName:#"Rank" inManagedObjectContext:context];
[newCriteria addRankingsObject:newCriteriaRank];
[anOption addRankingsObject: newCriteriaRank];
}
But then what if I have to go back and add another option?
I guess what I am asking is what is a good way to set the relationships so that each side is updated when the other get changed.
I looked in KVO and Notifications, but I am not sure exactly where to start.
Thanks.
Select your .xcdatamodel, select one of the entities. Under relationship, select the relationship in question and then select an Inverse. This will help maintain your RFI.
This assumes that you have one relationship on each entity that points at the other. If you don't have relationships on both entities, then you'll have to add one in order to get inverse working properly.

CoreData basics – to-many relationship array data

As I am fairly new to CoreData and coming from a MySQL-DB background, the CoreData Moddeling is kind of hard to understand at some point. I am sure you can help me out with this basic question.
CoreData model-descripton:
My database-model basically consists of two entities. The first one is called "Manager", the second one is called "Zipcodes". The "Manager" has 3 attributes, which are negligible at the moment. The important thing in my opinion is here the relationship called "zipcodes". The "Zipcodes"-Entity has an attribute called zip, which is a 16 int. It has a relationship as well, called "manager".
No I'll get to the point: Each manager has multiple zicodes in which he is responsible for all sales. The problem is now that I've setup an manager entity and want to link multiple ziplcodes to him. The zipcodes per manager are seperated in one comma seperated string. (12345,56789,...)
First of all I am creating an Manager Entity.
Manager *manager = [NSEntityDescription insertNewObjectForEntityForName:#"Manager" inManagedObjectContext:self.managedObjectContext];
The next step is seperating all zicodes to an array.
Manager *manager = [NSEntityDescription insertNewObjectForEntityForName:#"Manager" inManagedObjectContext:self.managedObjectContext];
NSArray *zipcodesArray = [[dict objectForKey:#"zipcodes"] componentsSeparatedByString:#","];
for (NSString *zip in zipcodesArray) {
???
}
So now that's the point where I am stuck. As later on I have to check the zipcodes via a searchBar they should be separated in the database. Do I now have to create a managedObjectModel for each zipcode? How do I connect all of them with the "one" manager entity? I'am sure there is a way to achieve that but I don't really know how.
Hopefully my question is understandable. If there's anything you would like to know, feel free to ask.
Thank you guys!
for (NSString *zip in zipcodesArray) {
NSManagedObject* zipcode = [NSEntityDescription insertNewObjectForEntityName:#"Zipcode"
inManagedObjectContext:self.managedObjectContext];
[zipcode setValue:zip forKey:#"zip"];
[zipcode setValue:manager forKey:#"manager"];
}
By establishing the relation from the zipcode to the manager on the last line, Core Data will automatically take care of inserting the zipcode into the relation from the manager back to the zipcodes.
Create a managed object instance of Zipcode. Set that object's zip attribute to the value of the string from your zipcodesArray. When you're done, save the managed object to your data store, check for errors, and repeat until you've walked all the way through your zip code array.

Editing Core Data row from entity linked to managedObjectContext via relationship

I need to edit a row of data in an Entity that has a relationship with my main Entity from my fetchedResultsController, in this case "theUser" being an instance of my User entity.
I basically need to edit one of the CannedMessage rows that already exist and save it. I can access the "Messages" fine as you see below, but am unsure once I have found the CannedMessage I want as to how I save it back into the managedObjectContext for "theUser"
Any advice?
NSArray *msgs = [theUser.Messages allObjects];
NSPredicate *activeMatch = [NSPredicate predicateWithFormat:#"defaultMessage == 1"];
NSArray *matched = [msgs filteredArrayUsingPredicate:activeMatch];
CannedMessage *msgToEdit;
for(CannedMessage *msg in matched) {
msgToEdit = msg;
}
Your trouble is that your thinking in SQL terms instead of Core Data's object oriented terms. The data you are looking for is not in an SQL row but in the attribute of a managed object. In this case (I assume) you are looking for an attribute of a CannedMessage instance.
The matched array will contain either managed objects initialized with the CannedMessage entity or an instance of a dedicated NSManagedObject subclass (if you setup one which it looks like you did.)
Lets say the attribute is named theMsg. To access the attribute in the generic managed objects:
for(CannedMessage *msg in matched) {
msgToEdit = [msg valueForKey:#"theMsg"];
}
... to access a custom class:
for(CannedMessage *msg in matched) {
msgToEdit = msg.theMsg;
}
It's really important when learning Core Data to simply forget everything you know about SQL. Nothing about SQL truly translates into Core Data. Core Data is not an object-oriented wrapped around SQL. Entities are not tables, relationships are not link tables or joins, attributes are not columns and values are not rows. Instead, Core Data creates objects just like you would if you manually wrote a custom class to model a real world object, event or condition. Core Data uses SQL almost as an after thought as one of its many persistence options.
In my experience, the more you know about SQL, the harder it is to shift gears to Core Data and other object graph APIs. You want to translate the new stuff to what you have already mastered. It is natural but resist the urge.