Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Design for a chat app using Core Data

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

like image 411
Gilad Novik Avatar asked Mar 29 '12 22:03

Gilad Novik


2 Answers

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 !

  1. 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

  1. 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

like image 108
Pierre Avatar answered Oct 26 '22 14:10

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 = %@"

like image 42
Jody Hagins Avatar answered Oct 26 '22 13:10

Jody Hagins