sort data using Realm - ios

I have recently moved to Realm from Coredata. In my app I am showing 50K + contacts .
The contact object is in the format:
Contact: firstName, lastName ,company
I am trying to fetch all the contacts in the Realm , and I am trying to display those contacts similar to the native contacts app in iPhone.
First I am creating the section header titles based on the contact first name:
-(NSArray *)getSectionTitleBasedOn:(NSString*)sortBy{
RLMResults *results = [self getMainDataSetFromRealm];
ContactSource *contactSource = results.firstObject;
NSMutableDictionary *nameDic = [NSMutableDictionary dictionary];
for (RealmContact *contact in contactSource.contacts){
if (contact.firstName.length>0) {
if ([sortBy isEqualToString:#"FirstName"]) {
[nameDic setObject:#"firstletter" forKey:[contact.firstName substringToIndex:1]];
}
}
}
NSLog(#"dic %#",nameDic);
return [[nameDic allKeys]sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)];
}
This gets me an array of letters which represent the title of section.
Now I am preparing the datasource for each section, so for section A, I am fetching all the contacts that begin with letter 'A'
-(void)prepareDataSource:(NSArray *)titleArr{
RLMResults *results = [self getMainDataSetFromRealm];
ContactSource *contactSource = results.firstObject;
__block NSMutableDictionary *dataSource = [NSMutableDictionary dictionary];
[titleArr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *sectionHeader = obj;
RLMResults *contactResults = [contactSource.contacts objectsWhere:[NSString stringWithFormat:#"firstName BEGINSWITH '%#'",sectionHeader]];
NSMutableArray *contactRowArr = [NSMutableArray array];
for (Contact *contact in contactResults){
[contactRowArr addObject:contact];
}
[dataSource setObject:contactRowArr forKey:sectionHeader];
}];
_dataSource = [dataSource copy];
[self.tableView reloadData];
}
This works really well, but takes 3-5 seconds to load table which is fine but I am looking for ways to improve this data fetch .

Realm works on a principle of lazy-loading, where objects and their properties aren't loaded until you actually 'touch' them for the first time.
As a result, if you do any operations where you're manually iterating through all Realm objects in a results set at once, or manually copying specific objects to an array, you're going to incur a performance hit that will increase the more objects you persist in Realm.
The best way to minimize the performance hit is to try and mitigate how many times you iterate through the results sets and avoid copying objects out of the array as much as possible. RLMResults behaves like an array, so for most scenarios, you can usually just use that object instead.
In the prepareDataSource method, instead of looping through each object and passing them to that NSMutableArray, instead you could consider passing the RLMResults object itself instead.
The method getSectionTitleBasedOn: also seems quite inefficient since you're iterating through every single object in order to check if an entry with a particular first character exists. Instead, you could create an index of the alphabet, and then do a Realm query for entries that start with each letter, and then check to see if the resulting RLMResults object has a positive count (Though I'm not sure if this will actually be any faster).
But in the end, sometimes when you're doing complex sorting like this, where there's no 'clever' way to avoid iterating through each object in a database (Even Realm has to internally load each object when performing a sort), performance hits are unavoidable, in which case you should also make sure your UI has provisions to show a 'working' indicator to the user.

Related

Storing RLMResults instead of re-fetching

I've just started using Realm for Objective-C, I've used Realm for Swift before and I can't remember having any problems with it.
I want to store the fetched objects and convert them to RLMObjects/NSMutableArrays and have them as ViewController's properties, so I won't have to fetch them again using predicates and descriptors and getting them through a loop to distinct them, because there are lots of data to fetch.
RLMResults *results = [Sales allObjects];
NSMutableArray<NSString *> resultsIDs* = [[NSMutableArray alloc] init];
NSMutableArray<Sales *> *uniqueSales = [[NSMutableArray alloc] init];
for (Sales *sale in results) {
NSString *id = sale.id;
if (![resultsIDs id]) {
[resultsIDs id];
[uniqueSales addObject:sale];
}
}
self.distinctProducts = uniqueSales;
I know RLMObjects are not thread-safe but since I add different data to different object models concurrently (and only once when the app launches), keeping a reference to the threads does not seem to be a good idea to me.
Just to clarify, RLMResults objects are live, meaning that if their underlying data is changed, they get updated automatically. As a result, there's never really a need to re-query or refresh the same results object.
If you still want to store a custom-sorted list of RLMObject instances, the most efficient way would be to create a separate Realm model class that has an RLMArray property in which you can then save all of your objects.
For example, maybe calling it a SalesList object:
RLM_ARRAY_TYPE(Sales)
#interface SalesList : RLMObject
#property RLMArray<Sales *><Sales> *sortedSales;
#end
It would then be a matter of simply keeping one SalesList object, and adding all of the Sales objects you sorted into the sortedSales array.

When iterating an NSMutableArray, how to delete one object then insert multiple objects?

For example, if we have an instance of NSMutableArray, with 10 objects in it. When iterating over it, we found that we have to delete object a[2] and a[8], and then insert 3 objects at a[2] and 4 objects at a[8] continuously, how to do that at lowest time cost?
Any idea will be thankful!
Edit : As #trojanfoe pointed out, it's nice to add that you should never edit an array while iterating it. This is true for many collection classes in many different languages; not just NSMutableArray & Objective-C. Doing so can easily leads to out-of-bounds index.
For your question, let's do it in two iteration.
First, we want to save the indexes we want to delete, so we will iterate on the sourceArray.
NSMutableArray * indexesToRemove = [NSMutableArray array];
[sourceArray enumerateObjectsUsingBlock:^(NSNumber * obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.integerValue%2 == 1) {
// Insert at first position
[indexesToRemove insertObject:#(idx) atIndex:0];
}
}];
It's important to save the index in an array and not a set, since you want to insert objects later. Also, add the new items at the beginning of the array is important, so you will iterate from the biggest index to the smallest, and will not have to shift the indexes according to the previously added items.
Now, you can, in a new iteration (this time on the index array), delete the items and add the new ones according to the indexes you saved :
[indexesToRemove enumerateObjectsUsingBlock:^(NSNumber * obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSUInteger indexToRemove = obj.unsignedIntegerValue;
// Delete the item from the source array
[sourceArray removeObjectAtIndex:indexToRemove];
// Create the items you want to insert, do whatever you want in this method :]
NSArray * itemsToAdd = [self generateElementsToAddAtIndex:indexToRemove];
// Create the indexSet according to the start index and the number of objects you want to insert
NSIndexSet * indexSet = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(indexToRemove, itemsToAdd.count)];
// Insert the objects
[sourceArray insertObjects:itemsToAdd atIndexes:indexSet];
}];
[myMutableArray replaceObjectAtIndex:2 withObject:"5"];
will it work ?
First you have to delete object then use below line of code to insert multiple objects:
NSMutableOrderedSet *orderedSet = [[NSMutableOrderedSet alloc] init];
[orderedSet insertObjects:#[#"Eezy", #"Tutorials"] atIndexes:
[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];
NSLog(#"Result: %#", orderedSet);
Refer below reference link:
http://ios.eezytutorials.com/nsmutableorderedset-by-example.php#.VwIJIhN95fg
For such a small array and so many operations, I consider replacing the array with a new one a good option - for performance as well as clarity.

distinctUnionOfObjects not working

I have created simple XCTest to test distinctUnionOfObjects. All the test cases are passing except one which is isKindOfClass (Last XCTAssertTrue). Any idea why it's changing the class when you do distinctUnionOfObjects.
- (void)testUsersPredicate
{
NSArray *usersBeforePredicate = [[self userData] users];
XCTAssertEqual([usersBeforePredicate count] , 34u, #"We need 34");
XCTAssertTrue([[usersBeforePredicate lastObject] isKindOfClass:[ICEUsersModelObject class]], #"Object is not ICEUsersModelObject class");
NSString *distinctUsersKeyPath = [NSString stringWithFormat:#"#distinctUnionOfObjects.%#", #"userName"];
NSArray* usersAfterPredicate = [usersBeforePredicate valueForKeyPath:distinctUsersKeyPath];
XCTAssertEqual([usersAfterPredicate count] , 30u, #"We need 30");
XCTAssertTrue([[usersAfterPredicate lastObject] isKindOfClass:[ICEUsersModelObject class]], #"Object is not ICEUsersModelObject class");
}
As the right key path on your distinctUnionOfObjects is userName, the -valueForKeyPath: call will return an NSArray of distinct userNames (not user objects).
From Apple's KVC Programming Guide:
The #distinctUnionOfObjects operator returns an array containing the distinct objects in the property specified by the key path to the right of the operator.
Change the last test case to check for [NSString class] and it should pass.
Alternatives
Using equality:
If the userNameproperty is supposed to serve as a unique identifier, you could enforce that by overriding -isEqual: and -hashon the user object to reflect this:
- (BOOL)isEqual:(id)object {
return ([object isKindOfClass:self.class] && [object.userName isEqual:self.userName]);
}
- (NSUInteger)hash {
return self.userName.hash;
}
This can benefit your overall model design and opens up a lot of additional options, like this one that obtains a collection of distinct users reg. userName in one line - NSSet is very fast when used for this:
NSArray *uniqueUsers = [[NSSet setWithArray:users] allObjects];
Note: I re-used the hashing function of NSString for the user hash, which has a subtle pitfall; -[NSString hash] only guarantees uniqueness for strings of up to 96 characters! This is not in the docs and took me almost a day to track down in production code. (see Apple's implementation of CFString.c - search for __CFStrHashCharacters)
Using NSPredicate:
Here's a, let's say, 'creative' solution that uses a predicate. However, some kind of iteration is needed, because the predicate condition would otherwise have to be a function of its own result:
NSMutableArray *__uniqueUsers = [NSMutableArray array];
[[users valueForKeyPath:#"#distinctUnionOfObjects.userName"] enumerateObjectsUsingBlock:^(id name, NSUInteger idx, BOOL *stop) {
NSArray *uniqueUser = [users filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id user, NSDictionary *bindings) {
return [user.userName isEqual:name];
}]];
if (uniqueUser.count > 0)
[__uniqueUsers addObject:uniqueUser.lastObject];
}];
NSArray *uniqueUsers = [NSArray arrayWithArray:__uniqueUsers];
It obtains a collection of unique userNames, iterates over it, selects exactly one user for each name and adds that to the output array.
You test has no sense. You can't use distinct in this way because as #mvanellen told you, you should change the class in NSString due the fact that you are searching for distinct username.
You instead are trying to get the list of objects considering distinct username, but it is conceptually wrong. You should try to get the list of the username if you want, but not of the entire objects.
Consider to have in your array:
ITEM 1: aav / otherValue1
ITEM 2: aav / otherValue2
ITEM 3: matteo / otherValue3
for sure the function would return ITEM 3, but being a distinct query, which from ITEM 1 and ITEM 2 should it takes?
Think about this ;)

Keep NSArray original order in NSCountedSet

I have an NSArray and I need to get data from two keys and put together in a NSMutableDictionary. One key has stringvalues and the other NSNumbervalues. When I try to create NSCountedSetwithout adding the keys I want to use to separate arrays, it doesn't work, because the objects are not identical, basically, I need to check if objectId is identical, don't matter if the other keys are different.
Here is the initial code:
for (PFObject *objeto in objects) {
PFObject *exercicio = objeto[#"exercicio"];
NSString *string = exercicio.objectId;
NSNumber *nota = objeto[#"nota"];
[exercicios addObject:string];
[notas addObject:nota];
So I create two NSMutableArraysand store the values I need. When I logthe arrays after this, they are perfectly ordered, meaning the NSStringis in the same indexof the NSNumberit belongs to in the other array. So far, so good.
Now, when I create the NSCountedSetwith the strings, it changes the order.
NSCountedSet *countedExercicios = [[NSCountedSet alloc] initWithArray:exercicios];.
My goal is to sum the NSNumbers pertaining to an specific object, therefore, when the order changes, I lose the connection between the two arrays.
I'm not sure what I could do to solve this problem, or even if there's a different approach to achieve the result I need.
You can create NSDictionary and add it to array. You will have just one array and you won't lose the connection, you can use objectId as a key and NSNumber as a value:
for (PFObject *objeto in objects) {
PFObject *exercicio = objeto[#"exercicio"];
NSString *string = exercicio.objectId;
NSNumber *nota = objeto[#"nota"];
NSDictionary *dict = #{string: nota};
[newArray addObject: dict];
}
When you need get all key (objectId) you can use NSPredictate.
Hope this help

Check through an Array for a specific object

I am trying to work out how many cells my table view should have. This is determined by results of a query which are stored in Array.
When the view is loaded the array might not exist or have any values so the cells should be 0.
How do I check through my array to check for a specific object. I understand I can use containsObject or equalTo...
My array would consists of objects like this:
{<GameTurn:TLED0qH44P:(null)> {\n GameRef = \"<Game:KgguI4ig4O>\";\n GameTurnImage = \"<PFFile: 0xb3da9d0>\";\n GameTurnWord = tester;\n OriginalImageCenterX = \"27.9\";\n OriginalImageCenterY = \"29.39375\";\n TurnCount = 1;\n UploadedBy = \"<PFUser:UgkZDtDsVC>\";\n}
There would be multiple entries of the above. For each entry I need to check if the UploadedBy key is equal to the PFUser currentUser. If it is add one cell, and so on.
So I need to get an overall count of the items in the array where that key is equalto the current user.
You can filter the array to get a new array of all matching objects:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"UploadedBy = %#", currentUser];
NSArray *filtered = [array filteredArrayUsingPredicate:predicate];
and use the filtered array as table view data source.
If the array comes from a Core Data fetch request, it would be more effective to
add the predicate to the fetch request already.
There are many ways you can filter an array in Objective-C. Here is one method using blocks and NSIndexSet.
You can grab all the indexes of your original array where the objects pass a test, specified in the block. Then create another array consisting of the objects at those indexes.
// get all indexes of objects passing your test
NSIndexSet *indexes = [myArray indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
// replace this with your equality logic
return [obj uploadedBy] == [PFUser currentUser];
}];
// Filled with just objects passing your test
NSArray *passingObjects = [myArray objectsAtIndexes: indexes];

Resources