Breaking NSArray into sorted groups by key - ios

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.

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.

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
}];

How to sort NSArray items into groups based on the same object order in a different NSArray? [duplicate]

This question already has answers here:
Sort NSArray of custom objects based on sorting of another NSArray of strings
(5 answers)
Closed 7 years ago.
I have an NSArray of unique UUIDs sorted in the proper order. I also have another NSArray of bookmarks with a UUID encoding. I would like to sort my NSArray of bookmarks based on their UUID property into groups.
For example, I have an NSArray: #[#"uuid1", #"uuid3", #"uuid2"] that has been sorted into this particular order. Now the other NSArray must sort all of its bookmarks in the same order as the first NSArray above.
So the second NSArray is: #[#"bookmark1", #"bookmark2", #"bookmark3", ...etc.]
Say bookmark 1 has the UUID property encoded as uuid2, bookmark 2 has the UUID encoding of uuid 1, but the bookmark 3 has encoding of uuid3. How can I sort and group these bookmarks so that it would be: #[#"bookmark2", #"bookmark3", #"bookmark1"]?
Thanks!
You could get rid of the second array and use a dictionary instead, which is keyed on the UUID.
NSArray *sortedIDs = #[ #"uuid1", #"uuid3", #"uuid2", ];
NSDictionary *items = #{
#"uuid1" : #[ bookmark1 ],
#"uuid2" : #[ bookmark2 ],
#"uuid3" : #[ bookmark3 ],
};
Now when you want the second bookmark you can access it with
NSArray *bookmarksForUUID = items[sortedIDs[1]];
If you wanted to build the structure above you could add a category like the below to NSArray
- (NSDictionary *)pas_groupBy:(id (^)(id object))block;
{
NSParameterAssert(block);
NSMutableDictionary *groupedDictionary = NSMutableDictionary.new;
for (id object in self) {
id key = block(object);
if (groupedDictionary[key]) {
[groupedDictionary[key] addObject:object];
} else {
groupedDictionary[key] = [NSMutableArray arrayWithObject:object];
}
}
return groupedDictionary;
}
Then assuming your bookmark objects look something like
#interface Bookmark : NSObject
#property (nonatomic, copy) NSString *UUID;
// other properties
// other properties
#end
You can use the category like this
NSDictionary *bookmarksMappedBySection = ({
return [bookmarks pas_groupBy:^(Bookmark *bookmark) {
return bookmark.UUID;
};
});
Create a class Bookmark. Add both the UUIDs and title as properties to this class. Now you can have just one array which holds objects of class Bookmark and each one will have access to its UUID and title (as well as any other properties you might want to add later on).
#interface Bookmark: NSObject
#property (nonatomic,strong) NSString *uuid;
#property (nonatomic,strong) NSString *title;
#end
Now if you have NSArray (lets call it myBookmarks) you can do the following:
for (Bookmark *bookmark in myBookmarks) {
NSLog(#"my UUID is %#",bookmark.uuid);
NSLog(#"my title is %#",bookmark.title);
}
If you need to store these items in NSUserDefaults all you have to do is adopt the NSCoding protocol. If you don't know how to do it here is a great and simple example.

Removing Objects from NSMutableDictionary

I have a NSDictionary named tableData that contains keys #"A" - #"Z" and for every key the value contains an array with Patient objects.
I then create a NSMutableDictionary named filteredPatients and initialize it with the tableData dictionary.
In this example I try to remove all the Patient objects from the array where the key is #"A"
#property (nonatomic, strong) NSDictionary *tableData;
#property (nonatomic, strong) NSMutableDictionary *filteredPatients;
self.filteredPatients = [[NSMutableDictionary alloc]initWithDictionary:self.tableData];
NSMutableArray *patientArray = [self.filteredPatients objectForKey:#"A"];
int count = patientArray.count - 1;
for (int i = count; i >= 0; i--)
{
[patientArray removeObjectAtIndex:i];
}
In this case, the Patient Objects are getting removed correctly from filteredPatients array with key #"A", but the problem lay in the fact that the same Patient Objects are getting removed from tableData array with key #"A". My question is why are the Patient Objects being removed from the tableData dictionary as well? How do I prevent this?
I must be doing something wrong, but this puzzles me because if I try to simply remove the the key #"A" from filteredPatients, it works perfectly fine and only removes the key from the dictionary I want.
[self.filteredPatients removeObjectForKey:#"A"];
I think your problem is you're expecting deep copy behaviour but you haven't asked for a deep copy. The dictionaries are different objects, but the value for any given key in both dictionaries points to the same mutable array. You could use initWithDictionary:self.tableData copyItems:YES instead, but you're going to need to think through what gets copied and what doesn't. I don't have enough information to help you with that.

Multiple NSMutableArrays, one sorted by Predicate, others reordered relatively

I have a Music App I am developing, and while I'm managing the TableViews, I'm sorting them into Sections. This is usually an easy process, but the with the way I manage I manage the data, it might not be a completely simple task.
I know it's not the best way to do it, but as the cell has multiple properties (titleLabel, textLabel, imageView), I store all the data in 3 separate arrays. When organising the songs by title, for section header, I use a predicate to set the titleLabel, so by using this code.
NSArray *predict = [mainArrayAll filteredArrayUsingPredicate:predicate];
cell.titleLabel.text = [predict objectAtIndex:indexPath.row];
Unfortunately, I also have a otherArrayAll, and others. These all have data such as the artist name, album artwork and so need to be relative to the songs they are for. Is there a way to reorder these other arrays in the same way the mainArrayAll array is? So the relative data is kept together?
In the above case I would suggest you to create a model class implementation to store all the values. Following MVC architecture is the best way here.
For eg:-
#interface Music : NSObject {}
#property (nonatomic, strong) NSString *title;
#property (nonatomic, strong) NSString *artistName;
#property (nonatomic, strong) NSString *albumName;
//etc...
#end
Now if you want to create an array of music, do it as:
Music *myMusic1 = [[Music alloc] init];
myMusic1.title = #"name";
myMusic1.artistName = #"artist";//etc.. do this in a loop based on other contents array
NSMutableArray *array = [NSMutableArray arrayWithObjects:myMusic1, myMusic2, ... nil];
Now to sort this array:
NSArray *sortedArray = [array sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
return [a.title compare:b.title];
}];
If you want to use predicates:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%K contains %#", #"title", #"name"];
NSArray *predict = [mainArrayAll filteredArrayUsingPredicate:predicate];
If you want to use these properties in a table view or so, you can use it as:
NSString *title = [[sortedArray objectAtIndex:indexPath.row] title];
NSString *artistName = [[sortedArray objectAtIndex:indexPath.row] artistName];

Resources