Storing RLMResults instead of re-fetching - ios

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.

Related

sort data using Realm

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.

Is it possible to sort RLMResults by the count of an RLMArray property?

I'd like to sort the objects contained by an RLMResults instance by the count of an RLMArray property of those objects.
Person.h:
#interface Person : RLMObject
#property RLMArray *children;
#end
SomeClass.m:
...
RLMResults *people = [Person allObjects];
RLMResults *sorted = [people sortedResultsUsingProperty:#"children.count" ascending:YES];
...
Using - sortedResultsUsingProperty: as done above doesn't work, however, giving an error: Cannot sort on 'people.count': sorting on key paths is not supported.
What's the best way to approach this problem?
So far you can only sort by actual properties. We want to add support for what you want to achieve here, see issue #1277 for reference. For now you'd need to introduce a property childrenCount, which you would maintain manually and keep in sync with children. Alternatively, you can extract your results into a NSArray and sort that in a similar manner via Foundation's API.
The latter proposal could be implemented like shown here:
RLMResults *people = [Person allObjects];
NSArray *peopleObjects = [people valueForKey:#"self"];
NSArray *sortedPeople [peopleObjects sortedArrayUsingComparator: ^(Person *a, Person *b) {
return a.children.count < b.children.count
}];

Insertion in one array effects other array?

I have 2 NSMutableArray.I am adding custom class objects into them. I have initialized them as below.
arr_post=[[NSMutableArray alloc]init];
copy_arr_user_post=[[NSMutableArray alloc]init];
I am adding the objects into them like this.
for(i=0;i<[arr_main count];i++)
{
Post *obj=[[Post alloc]init];
obj.name=#"abc";
obj.gender=#"male";
[arr_post addObject:obj];
[copy_arr_user_post addObject:obj];
}
Now when i remove object from arr_post & insert again then it is also effected in another array.
Post *post=[arr_post objectAtIndex:indexPath.row];
[arr_post removeObjectAtIndex:indexPath.row];
post.name=#"def";
[arr_post addObjectAtIndex:indexPath.row];
then def is also added in second array.
You misunderstand how arrays work.
Arrays store references to objects, not the objects themselves. Its a bit like keeping people's phone numbers. If I have a list of 10 phone numbers and you have the same list of phone numbers, and the person at index 1 in my list moves to a new address, when you call your person at index one and ask for their address they will give you the changed address too.
Both arrays point to the same objects.
If you change settings on one of your objects, you will see those changes if you look up the object in either array.
If you want your arrays to contain independent objects then implement NSCopying in your custom class and use code like this:
for(i=0;i<[arr_main count];i++)
{
Post *obj=[[Post alloc]init];
obj.name=#"abc";
obj.gender=#"male";
[arr_post addObject:obj];
Post objectCopy = [obj copy]; //create a copy of our object
[copy_arr_user_post addObject: objectCopy]; //insert copy into other array
}
But again, to do that you need to teach your custom Post class to conform to the NSCopying protocol so it understands the copy message. Take a look at the docs on the NSCopying protocol for more information.
Of course it does affect the other array because both arrays contain the same (identical) object.
In Objective-C objects are reference types. That means a pointer is stored in the arrays respectively which points to the same object.
If you want to have different objects you have to copy them for example
[copy_arr_user_post addObject:[obj copy]];
This happens because arrays store pointers to the data not the actual data.
if you want to have two different copies of an object you can use this method:
for(i=0;i<[arr_main count];i++)
{
Post *obj=[[Post alloc]init];
obj.name=#"abc";
obj.gender=#"male";
NSData* objectData = [NSKeyedArchiver archivedDataWithRootObject: obj];
Post* obj2= [NSKeyedUnarchiver unarchiveObjectWithData: objectData];
[arr_post addObject:obj];
[copy_arr_user_post addObject:obj2];
}
OR you can simply declare two objects:
for(i=0;i<[arr_main count];i++)
{
Post *obj1=[[Post alloc]init];
Post *obj2=[[Post alloc]init];
obj1.name=#"abc";
obj1.gender=#"male";
obj2.name=#"abc";
obj2.gender=#"male";
[arr_post addObject:obj1];
[copy_arr_user_post addObject:obj2];
}

Breaking NSArray into sorted groups by key

Noob here, could use some help.
I have an NSArray filled with MPMediaItems (songs) that I need to shuffle, by album. What I ultimately need to do:
1) Group tracks by album (total number of albums unknown).
2) Sort tracks within album by their track number.
3) Return a new array with all tracks (randomized by group, and still in order by number within their group).
I believe I know how I can do this by iterating through the array over and over, but this seems terribly inefficient. I suspect that an NSDictionary might be a better choice, but I don't even know where to begin.
Any help would be most appreciated!
Mike
Even though I haven't worked with MPMediaItems, I can explain how simple sorting and filtering can be for NSArray.
For the purpose of illustration I am creating an imaginary class called Song which may not be similar to MPMediaItem. Song class has two properties called albumName and trackNumber. Then we add a class method called + (NSDictionary *)songsGroupedByAlbumAndSortedByTrack:(NSArray *)songs that will accept an array of Song objects and returns an NSDictionary. Each key in the returned dictionary will be an albumName and the corresponding value will be an array of songs belonging to that album and sorted by track number.
#interface Song : NSObject
#property (nonatomic, copy) NSString *albumName;
#property (nonatomic, assign) NSInteger trackNumber;
#end
#implementation Song
+ (NSDictionary *)songsGroupedByAlbumAndSortedByTrack:(NSArray *)songs {
NSSortDescriptor *byTrack = [NSSortDescriptor sortDescriptorWithKey:#"trackNumber" ascending:YES];
NSArray *albumNames = [songs valueForKeyPath:#"#distinctUnionOfObjects.albumName"];
NSMutableDictionary *groupedByAlbumAndSortedByTrack = [[NSMutableDictionary alloc] init];
for (NSString *album in albumNames) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"albumName == %#",album];
NSArray *currentAlbumSongs = [songs filteredArrayUsingPredicate:predicate];
NSArray *currentAlbumSongsSortedByTrack = [currentAlbumSongs sortedArrayUsingDescriptors:#[byTrack]];
[groupedByAlbumAndSortedByTrack setObject:currentAlbumSongsSortedByTrack forKey:album];
}
return groupedByAlbumAndSortedByTrack;
}
#end
By using the NSSortDescriptor for sorting and NSPredicate for filtering we have eliminated a lot of looping through the array.
First step is to extract the album names from the array of songs which we accomplish using the KVC collectionOperator #distinctUnionOfObjects.
Then we iterate through the array of album names and add all songs belonging to that album to an NSArray.
The sample code given above is for sorting and filtering of NSArray in general.
A quick glance at the MPMediaItem class reference shows that apple already provides a lot of methods for manipulation of mediaItems. Please have a look at MPMediaPropertyPredicate.

iOS: core data to many relationship

I have a doubt how to manage a "to many relationship" in core data.
In my example I have the main identity 'Struct' that have a "to many relationship" with another identity called 'Id_loc'
Then, I have this object in a JSON file that is a Struct identity:
{"id":"s1",
"n":"Name Struct",
"id_loc":["l1","l2"]} //id_loc can contain many element
when I parse this object I have id_loc as an array.
Inside Struct class I have two methods:
- (void)addLocObject:(Id_loc *)value;
- (void)addLoc:(NSSet *)values;
then I do this to store id_loc array inside:
Struct *struct = [NSEntityDescription insertNewObjectForEntityForName:#"Struct" inManagedObjectContext:context];
NSArray *array_loc = [element objectForKey:#"id_loc"];
NSSet *set = [NSSet setWithArray:array_loc];
[struct addLoc:set];
Is it a right way?
Is it not necessary to call this?
Id_loc *loc = [NSEntityDescription insertNewObjectForEntityForName:#"Id_loc" inManagedObjectContext:context];
EDIT
Is it the right answer?
Struct *struct = [NSEntityDescription insertNewObjectForEntityForName:#"Struct" inManagedObjectContext:context];
NSArray *array_loc = [element objectForKey:#"id_loc"];
for (id loc in array_loc){
Id_loc *loc = [NSEntityDescription insertNewObjectForEntityForName:#"Id_loc" inManagedObjectContext:context];
loc.ident = loc;
[struct addLocObject:loc];
}
You cannot really save an NSSet into the CoreData just like that. NSSet contains other CoreData entities only which are related to your main object.
To save an array with data you need to use NSData property and use NSKeyedArchiver to archive your NSArray with NSStrings.
However even it's simplest solution there are some limitations. For example you won't be able to use and predicates on those properties. Therefore I would recommend to make another entity which is "Location" and create a location objects based on those "l1", "l2" values.
Yes you use addLocObject: or if you want to add multiple ones you use - (void)addLoc:(NSSet *)values; But you have to create those objects - create them in core data withing your context and than add it to main object.
Probably if you have ID you want also to select first existing locations and create them only if the don't exist.

Resources