IluTov
IluTov

Reputation: 6862

Calculating group row at run time of a NSTableView

I have a NSTableView, filled with song titles.

The songs are ordered by artists, so I can display the artist name as a row view.

enter image description here


Code

Until now, I had to iterate through the table data and manually add the artists to the array.

NSMutableArray *songsAndArtists = [NSMutableArray array];

Artist *artist;
for (Song *song in self.songs) {
    if (artist != song.artist) {
        artist = song.artist;
        [songsAndArtists addObject:artist];
    }

    [songsAndArtists addObject:song];
}

This can be really slow if you have 1'000 - 5'000 songs.
I haven't really figured out how I could speed up this process.

Does anybody have an idea how to calculate this at runtime?


EDIT

I'll try to clarify what I meant:

NSTableView supports group rows. My table view displays all the songs in CoreData. I'd like to use the group rows to display the artist of the songs, like in the print screen I added above.

To do this I have to provide an array with an artist, followed by it's songs, followed by the next artist, and so on.

The code above shows how to insert the artists, but it's a pretty time-consuming process.

So my question, how can I speed this up?


HINT

Like nielsbot and Feloneous Cat suggested, iterating through all the artists won't work for me.

The user also has the option to search though the library. Therefore, not all the songs should actually appear in the list.


Solution

I just want you to let you know what the problem was:

The problem was, that I actually compared via NSString in the previous version.
Pretty stupid fault...

It takes about 0.1 seconds or less which is great:

tableData = [self addGroupRowsToArray:[self allSongs] withKeyPath:@"artist"];

- (NSArray *)addGroupRowsToArray:(NSArray *)array withKeyPath:(NSString *)keyPath {
    NSMutableArray *mixedArray = [NSMutableArray array];

    id groupRowItem;
    for (id arrayItem in array) {
        if (groupRowItem != [arrayItem valueForKeyPath:keyPath]) {
            groupRowItem = [arrayItem valueForKeyPath:keyPath];
            [mixedArray addObject:groupRowItem];
        }

        [mixedArray addObject:arrayItem];
    }

    return mixedArray;
}

Upvotes: 2

Views: 1385

Answers (3)

igrek
igrek

Reputation: 1425

Well, i've tried to reproduce your problem. I don't know where do you get data from so i've faked them just for performance test. I also tested the code on iPod 4gen, and it works without any lag. (tried either 50000 rows and it only hang upon start, then worked perfectly)

In general my approach differs from yours with data structures. So you use arrays, and i use dictionaries. Still i think if i used arrays there should be no issues anyway. Maybe i am getting wrong what you've asked?

Still, my approach seems to be better suitable for search to work(sry i've been too bothered to try to implement it). Indeed, now the search to take place, whole sections may be excluded without checking songs, unless you need to search songs of course, anyway this gives more flexibility.

okay here is a link: https://github.com/igorpakushin/BigList

will be glad to here some feedback, thank you

regards, Igor

Upvotes: 3

Feloneous Cat
Feloneous Cat

Reputation: 895

So, let's back up and look at what you REALLY have. You already have all the information that you need residing in songs. Why are you in essence duplicating it merely to break out the artist?

Think of it this way, if you have the following songs (song/artist)

(0)    "Death Eater", "Raging Machine Code"
(1)    "Interrupt",   "Raging Machine Code"
(2)    "Panic",       "Times Square Revolution"
(3)    "New Years",   "Times Square Revolution"
(4)    "Toast",       "Ed & Billy's Time Machine"
(5)    "Surge",       "Quiet Cat"
(6)    "Surveil",     "Quiet Cat"

What is wrong with this picture? We have the same information duplicated. Ideally, we want to have something like this:

"Raging Machine Code"     -> has an  array that contains
                             "Death Eater"
                             "Interrupt"
"Times Square Revolution" -> has an array that contains
                             "Panic"
                             "New Years"
"Ed & Billy's Time Machine" -> array that contains
                             "Toast"
"Quiet Cat"               -> array that contains
                             "Surge"
                             "Surveil"

For UITableView this makes things trivial - we tell it how many sections (four) and for each section how many songs. To generate the cell, we get an NSIndexPath that tells us the section and the row (section would be the artist and the row would be the song for that artist).

NSTableView doesn't do that. It gives us a row. However, if we do some work on the front end (i.e. the song list) then we can assure that life will be beautiful and fast (or at least faster). The key is to pre-calculate the number of songs per artist and store it.

So assume we are asked to display row 5. "Raging Machine Code" is 0-2 (artist, song, song). "Times Square Revolution" is 3-5. Ah! 5 is the last song, so we display "New Years"!

Try another one, assume we are to display row 6. "Raging..." was 0-2, "Times..." was 3-5, "Ed & Billy's" is 6-7. Bingo, we need to display the artist, "Ed & Billy's Time Machine"!

The idea is that we do much of this prework AHEAD of actually displaying the data. Looping through artists will be far faster than looping through ALL the songs. Plus now you are only doing simple math - no need to move things.

Sometimes how you store your data means the difference between success and failure. Whenever you see yourself having to "redefine a data structure" that usually means your data structure is faulty - it may be simple, but it causes more effort.

Hopefully this was helpful and didn't end up being "TLTR" (too long to read).

Upvotes: 3

nielsbot
nielsbot

Reputation: 16031

Is this faster?

-(NSArray*)songsAndArtists:(NSArray*)allArtists
{
    NSMutableArray * result = [ NSMutableArray array ] ;
    for( Artist * artist in allArtists )
    {
        [ result addObject:artist ] ;
        [ result addObjectsFromArray:artist.songs ] ;
    }
    return result ;
}

If you are getting "all artists" from Core Data, you can tell it to prefetch the objects in the songs relationship, which will speed things up further.

-(NSArray*)allArtists
{
    NSFetchRequest * request = [ NSFetchRequest fetchRequestWithEntityName:@"Artist" ] ;
    [ request setRelationshipKeyPathsForPrefetching:@[ @"songs" ] ] ;
    ...
    return results ;
}

Upvotes: 3

Related Questions