3
\$\begingroup\$

I'm new to iOS development and I'm having trouble figuring out how to efficiently load my app's data at launch time. My app consists of a UITableView that is populated with a list of songs. Each cell displays data about the song: current user rating(if rated) and avg. user rating (avg. rating of all users). I'm using Parse for my backend data storage.

At launch time, my app runs two queries. The first query fetches songs to populate the table. It then calculates the avg. user rating (my backend schema includes count objects: rating1_count, rating2_count, etc. which are incremented when the user rates a song). It also orders the songs into separate data model arrays: topTracksOfTheWeek, topTracksOfTheMonth, topTracksAllTime.

The second query fetches all of the rated songs by the current user. It then compares this query to the first query to embed the user rating data into the first query to form the app's data model.

I'd like to limit the first query to 50 songs to optimize load time. However, with my current data model, I need to query all of the songs in my database to calculate all of the avg. user ratings and then order the songs into the topTracks arrays.

The only solution I can come up with is running a server-side script periodically to calculate the avg. user ratings. I would have to create a new class in my database for the topTracks arrays that are ordered by the script. In my app, I would do a lazily do a third query for the topTracks arrays.

Here is my data model code:

- (void)getDataSource
{

    PFUser *user = [PFUser currentUser];

    NSMutableArray *ratings;
    if (user) {
        PFQuery *queryRatings = [PFQuery queryWithClassName:@"Ratings"];
        [queryRatings whereKey: @"user" equalTo: user.username];
        [queryRatings orderByDescending:@"createdAt"];
        queryRatings.limit = 1000;
        ratings = [NSMutableArray arrayWithArray:[queryRatings findObjects]];
    }
    else
        [self displayLoginViewController];



    PFQuery *query = [PFQuery queryWithClassName:@"HotSongs"];
    [query orderByDescending:@"createdAt"];
    query.limit = 50;
    _parseTracksArray = [NSMutableArray arrayWithArray:[query findObjects]];

    if (!_favoriteTracks)   
        _favoriteTracks = [[NSMutableArray alloc] init];
    else
        [_favoriteTracks removeAllObjects];  
    if (!_topTracksOfTheWeek) 
        _topTracksOfTheWeek = [[NSMutableArray alloc] init];
    else
        [_topTracksOfTheWeek removeAllObjects];
    if (!_topTracksOfTheMonth)
        _topTracksOfTheMonth = [[NSMutableArray alloc] init];
    else
        [_topTracksOfTheMonth removeAllObjects];
    if (!_topTracksOfTheYear)
        _topTracksOfTheYear = [[NSMutableArray alloc] init];
    else
        [_topTracksOfTheYear removeAllObjects];


    //Create NSDate objects for Top Charts comparison
    NSDate *dateOfWeekFromNow = [NSDate dateWithTimeIntervalSinceNow:-604800]; // (60*60*24*7) in seconds
    NSDate *dateOfMonthFromNow = [NSDate dateWithTimeIntervalSinceNow:-2592000]; // (60*60*24*30)
    NSDate *dateOfYearFromNow = [NSDate dateWithTimeIntervalSinceNow:-31536000]; // (60*60*24*365)

    NSInteger i, a;
    for (a = 0; a < _parseTracksArray.count; a++) {

        NSMutableDictionary *dictionary = [_parseTracksArray objectAtIndex:a];
        NSString *trackId = [dictionary objectForKey:@"track_id"];

        NSNumber *rating_1 = @([[dictionary objectForKey:@"rating_1"] floatValue]);  
        NSNumber *rating_2 = @([[dictionary objectForKey:@"rating_2"] floatValue]);  
        NSNumber *rating_3 = @([[dictionary objectForKey:@"rating_3"] floatValue]);  
        NSNumber *rating_4 = @([[dictionary objectForKey:@"rating_4"] floatValue]);  
        NSNumber *rating_5 = @([[dictionary objectForKey:@"rating_5"] floatValue]);
        float totalRatingCount = rating_1.floatValue + rating_2.floatValue + rating_3.floatValue + rating_4.floatValue + rating_5.floatValue;

        if (totalRatingCount > 0) {
            float avgRating = (rating_1.floatValue + rating_2.floatValue*2 + rating_3.floatValue*3 + rating_4.floatValue*4 + rating_5.floatValue*5) / totalRatingCount;
            [dictionary setObject:@(avgRating) forKey:@"avgRating"];

            double confidenceRating = [self calculateConfidenceRating:avgRating basedOn:totalRatingCount];
            [dictionary setObject:@(confidenceRating) forKey:@"confidenceRating"];
        }


        BOOL isRatedHigh = NO;
        NSNumber *userRating = @0;  

        if (user) {
            for (i = 0; i < ratings.count; i++) {
                NSDictionary *dictionaryRatings = [ratings objectAtIndex:i];
                NSString *ratedTrackId = [dictionaryRatings objectForKey:@"track_id"];

                if ([trackId isEqualToString:ratedTrackId]) {
                    userRating = [dictionaryRatings objectForKey:@"userRating"];

                    if (userRating.intValue > 3) {
                        isRatedHigh = YES;
                    }
                }
            }
        }
        [dictionary setObject:userRating forKey:@"userRating"];

        if (isRatedHigh == YES) {
            [_favoriteTracks addObject:dictionary];
        }

        //Set createdAt object
        PFObject *object = [_parseTracksArray objectAtIndex:a];
        NSDate *dateTrackWasAddedToDatabase = [object createdAt];

        if ([dateTrackWasAddedToDatabase compare:dateOfYearFromNow] == NSOrderedDescending) {
            [_topTracksOfTheYear addObject:dictionary];
            if ([dateTrackWasAddedToDatabase compare:dateOfMonthFromNow] == NSOrderedDescending) {
                [_topTracksOfTheMonth addObject:dictionary];
                if ([dateTrackWasAddedToDatabase compare:dateOfWeekFromNow] == NSOrderedDescending) {
                    [_topTracksOfTheWeek addObject:dictionary];
                }
            }
        }
    }
    [self assignDataSource];
}

- (double)calculateConfidenceRating: (float)avgRating basedOn: (float)voteCount
{
    const double baseConfidenceRating = 3;
    const double baseConfidence = log10(CONFIDENT_NUMBER_OF_VOTES); 
    double confidence = log10( 1 + voteCount ) / baseConfidence;
    double confidenceWeight = fmin( confidence, 1.0 );
    double confidenceRating = (1.0 - confidenceWeight) * baseConfidenceRating + confidenceWeight * avgRating;

    return confidenceRating;
}

- (void)assignDataSource
{
    switch (_dataSourceAssignment.intValue) {
        case 0:     _dataSource = [NSMutableArray arrayWithArray:_parseTracksArray];      break;
        case 1:     _dataSource = [NSMutableArray arrayWithArray:_favoriteTracks];        break;
        case 2:     _dataSource = [NSMutableArray arrayWithArray:_topTracksOfTheWeek];    break;
        case 3:     _dataSource = [NSMutableArray arrayWithArray:_topTracksOfTheMonth];   break;
        case 4:     _dataSource = [NSMutableArray arrayWithArray:_topTracksOfTheYear];    break;
        default:    break;
    }

    if (_dataSourceAssignment.intValue >= 2)  {

        NSSortDescriptor *avgRatingSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"confidenceRating" ascending:NO];
        NSArray *sortDescriptors = @[avgRatingSortDescriptor];
        NSArray *sortedArray = [_dataSource sortedArrayUsingDescriptors:sortDescriptors];
        _dataSource = [NSMutableArray arrayWithArray:sortedArray];
    }

    // Filter genres
    NSMutableArray *selectedGenresByUser = [[NSUserDefaults standardUserDefaults] objectForKey:@"selectedGenres"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"trackGenre in $SELECTED_GENRES"];
    predicate = [predicate predicateWithSubstitutionVariables:@{@"SELECTED_GENRES": [NSArray arrayWithArray:selectedGenresByUser]}];

    NSArray *filtered = [NSArray arrayWithArray:[_dataSource filteredArrayUsingPredicate: predicate]];
    _dataSource = [NSMutableArray arrayWithArray:filtered];

    [self.tableView reloadData];
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Consider following scheme -

Each song has a key numberOfRatings and a key averageRating. Their initial value is 0.

When user rates a song with newRating, you change the value of averageRating according to this formula

averageRating = ((averageRating * numberOfRatings) + newRating) / (numberOfRatings + 1)

And of course you increase numberOfRatings

numberOfRatings = (numberOfRatings + 1)

With this approach you can utilize queries (query only the first 50 songs according to their averageRating etc.) for your topTracks chart views, and you do not have to query all songs just to count their average rating - at any given moment you have an up-to-date averageRating stored for every song.

\$\endgroup\$
3
\$\begingroup\$

Just looking at the code, I recommend to address several things.


 NSDate *dateOfYearFromNow = [NSDate dateWithTimeIntervalSinceNow:-31536000];
 ...

This is incorrect (leap years, 31-day months). Use [NSCalendar dateByAddingComponents: toDate:] to subtract a year or a month from today's date.


_topTracksOfTheWeek = [[NSMutableArray alloc] init];
_favoriteTracks = [[NSMutableArray alloc] init];

You should encapsulate those into some class:

_topTrackOfTheWeek = [[TopTracksProvider alloc] initWithTopTracksFrom: database inTimePeriod:oneWeek withName:@"Top Songs of the Week"];
...

Your classes shouldn't be based on NSArray, but rather they should be different data providers that return corresponding views of the existing common database.

There are several advantages of this approach:

  1. Your classes will clearly separate the Model parts of your applications in the MVC framework.

  2. It's more reasonable to consider those to be different representations of the same data, rather then completely separate entities.

  3. This way you automatically get fresh data as the underlying database is updated.

  4. For example, the 'top songs' class can implement accessor to get the first 10 songs without holding the entire array of songs.

  5. As seen above, your 'top songs of the week', 'top songs of the year', etc. can be based on the same class, while favorites can be based on another class.

  6. You'll be able to easily add new classes for new data providers. For example, if a user adds 'Smart folder' that says 'all songs by Elton John', you create a new instance of some similar class, and use it in the same place as the ones you already had.

  7. As you see in the example, you encapsulate keep all of data source properties, such as a name, close to its other data.

You data source assignment, getting the title of data source, etc. then won't need any weird switch operators. Also, the recurring user might want to customize this list:

_dataSources = try to read the list from settings
if (!_dataSources)
    _dataSources = @[[_topTracksOfTheWeek, _favoriteTracks...] // default

 NSNumber *rating_5 = @([[dictionary objectForKey:@"rating_5"] floatValue])

The floatValue and NSNumber wrapper seem strange here; from your usage those seems to be integers.

You can simplify things with

 NSInteger rating_5 = dictionary[@"rating_5"].integerValue;
 ... rating_4 + rating_5 + ...

Or even better, create Core Data model class for a Song record with NSInteger property rating_5 then use one of the existing open-source libraries:

[incomingData parseAssumingEntity: Song.entity] // something like that

then you can encapsulate number of ratings and average rating in the model class -

@implementation Song 
- (NSUInteger)totalNumberOfRatings {
    return self.rating_1 + self.rating_2 + ... ;
}
- (CGFloat)averageRating {
    ... 
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.