I am trying to come up with a way to fetch the same data both when the application is running and when it is in the background. The code I have written so far works... I just think it is very messy and could probably be refactored, but I'm not sure where to start.
I have put all the files up on GitHub as they are rather large GitHub Repo.
Here's the code to review though:
#import "TabBarViewController.h"
#import "UIView+Border.h"
#import "UIColor+HexColors.h"
#import "Achievement.h"
#import "Achievement+Create.h"
#import "AppDelegate.h"
#import "AchievementModalViewController.h"
@interface TabBarViewController () <GameCenterManagerDelegate>
{
NSDictionary *dataTypes;
NSMutableDictionary *achievementData;
NSMutableArray *achievementsToUpdate;
}
@property (strong, nonatomic) HKHealthStore *healthStore;
@end
@implementation TabBarViewController
@synthesize homeButton, settingsButton, achievementsButton, profileButton, gameCenterManager;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.selectedIndex = 3;
UIView *bar = [[[NSBundle mainBundle] loadNibNamed:@"CustomTabBar" owner:self options:nil] objectAtIndex:0];
bar.frame = CGRectMake(0, self.view.bounds.size.height - 49, self.view.bounds.size.width, 49);
[self.view addSubview:bar];
UIView *profileBorder = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width / 4) - 1, 0, 1, 49)];
UIView *achievementsBorder = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width / 4) - 1, 0, 1, 49)];
UIView *settingsBorder = [[UIView alloc] initWithFrame:CGRectMake((self.view.frame.size.width / 4) - 1, 0, 1, 49)];
profileBorder.backgroundColor = [UIColor colorWithHexString:@"e7e7e7"];
achievementsBorder.backgroundColor = [UIColor colorWithHexString:@"e7e7e7"];
settingsBorder.backgroundColor = [UIColor colorWithHexString:@"e7e7e7"];
[profileButton addSubview:profileBorder];
[achievementsButton addSubview:achievementsBorder];
[settingsButton addSubview:settingsBorder];
UIBezierPath *path = [UIBezierPath bezierPathWithRect:bar.bounds];
bar.layer.masksToBounds = NO;
bar.layer.shadowColor = [UIColor colorWithHexString:@"000000"].CGColor;
bar.layer.shadowOffset = CGSizeMake(0, -.25);
bar.layer.shadowOpacity = 0.5;
bar.layer.shadowPath = path.CGPath;
[homeButton setSelected:YES];
self.gameCenterManager = [[GameCenterManager alloc] init];
[self.gameCenterManager setDelegate:self];
[self.gameCenterManager authenticateUser];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(updateAchievementData) userInfo:nil repeats:YES];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *firstRun = [defaults objectForKey:@"firstRun"];
__block NSMutableArray *achievements = [NSMutableArray new];
if ([firstRun isEqualToString:@"yes"]) {
dispatch_queue_t queue = dispatch_queue_create([@"achievement.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t query = dispatch_group_create();
dispatch_group_async(query, queue, ^{
//get all of achievements
PFQuery *earnedQuery = [PFQuery queryWithClassName:@"Achievement"];
[earnedQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
achievements = [objects mutableCopy];
[self saveAchievements:achievements andThenCheck:NO];
} else {
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
});
dispatch_group_notify(query, queue, ^{
NSLog(@"Finished loading achievements");
[defaults setObject:@"no" forKey:@"firstRun"];
});
}
}
- (void)displayModalViewControllerWithInfo:(NSDictionary *)info {
AchievementModalViewController *amvc = [[AchievementModalViewController alloc] initWithNibName:@"AchievementModalView" bundle:nil];
amvc.achievementTitle = info[@"title"];
amvc.achievementMessage = info[@"message"];
amvc.achievementDescription = info[@"description"];
amvc.achievementImage.image = [UIImage imageWithContentsOfFile:info[@"image"]];
[self presentViewController:amvc animated:YES completion:nil];
}
- (void)fetchNewDataWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
__block UIBackgroundFetchResult result = UIBackgroundFetchResultNoData;
dispatch_queue_t queue = dispatch_queue_create([@"my.query.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t queries = dispatch_group_create();
PFUser *currentUser = [PFUser currentUser];
[currentUser fetchIfNeededInBackground];
if (!self.healthStore) {
self.healthStore = [HKHealthStore new];
}
dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:1], HKQuantityTypeIdentifierStepCount,
[NSNumber numberWithInt:2], HKQuantityTypeIdentifierFlightsClimbed,
[NSNumber numberWithInt:3], HKQuantityTypeIdentifierDistanceWalkingRunning,
[NSNumber numberWithInt:4], HKQuantityTypeIdentifierDistanceCycling, nil];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *items = [defaults objectForKey:@"achievementItems"];
NSDate *lastCheckedDate = items[@"lastCheckedDate"];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *startDate = [calendar startOfDayForDate:currentUser[@"dateSignedUp"]];
[calendar setTimeZone:[NSTimeZone localTimeZone]];
if (lastCheckedDate)
startDate = lastCheckedDate;
NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];
for (NSString *key in dataTypes) {
dispatch_block_t queryBlock = ^{
__block BOOL success = NO;
dispatch_semaphore_t lock = dispatch_semaphore_create(0);
HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:key];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if (!error) {
success = YES;
if (!results) {
NSLog(@"No results were returned form query");
} else {
[self processNewDataWithResults:results andType:key];
dispatch_semaphore_signal(lock);
}
} else {
result = UIBackgroundFetchResultFailed;
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
[self.healthStore executeQuery:query];
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
if (success)
result = UIBackgroundFetchResultNewData;
};
dispatch_group_async(queries, queue, queryBlock);
}
dispatch_group_notify(queries, dispatch_get_main_queue(), ^{
dispatch_queue_t achievementQueue = dispatch_queue_create([@"achievement.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t achievementQueries = dispatch_group_create();
__block NSMutableArray *achievements = [NSMutableArray new];
dispatch_block_t achievementBlock = ^{
dispatch_semaphore_t lock = dispatch_semaphore_create(0);
//get all of achievements
PFQuery *earnedQuery = [PFQuery queryWithClassName:@"Achievement"];
[earnedQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
achievements = [objects mutableCopy];
[self saveAchievements:achievements andThenCheck:YES];
dispatch_semaphore_signal(lock);
} else {
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
};
dispatch_group_async(achievementQueries, achievementQueue, achievementBlock);
dispatch_group_notify(achievementQueries, achievementQueue, ^{
completionHandler(result);
});
});
}
- (void)updateAchievementData {
dispatch_queue_t queue = dispatch_queue_create([@"my.query.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t queries = dispatch_group_create();
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.managedObjectContext;
PFUser *currentUser = [PFUser currentUser];
[currentUser fetchIfNeededInBackground];
if (!self.healthStore) {
self.healthStore = [HKHealthStore new];
}
dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:1], HKQuantityTypeIdentifierStepCount,
[NSNumber numberWithInt:2], HKQuantityTypeIdentifierFlightsClimbed,
[NSNumber numberWithInt:3], HKQuantityTypeIdentifierDistanceWalkingRunning,
[NSNumber numberWithInt:4], HKQuantityTypeIdentifierDistanceCycling, nil];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *items = [defaults objectForKey:@"achievementItems"];
NSDate *lastCheckedDate = items[@"lastCheckedDate"];
NSDateFormatter *formatter = [NSDateFormatter new];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
[formatter setTimeZone:[NSTimeZone localTimeZone]];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *startDate = currentUser[@"dateSignedUp"];
if (lastCheckedDate)
startDate = lastCheckedDate;
startDate = [calendar startOfDayForDate:startDate];
NSLog(@"%@", startDate);
NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];
for (NSString *key in dataTypes) {
dispatch_block_t queryBlock = ^{
dispatch_semaphore_t lock = dispatch_semaphore_create(0);
HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:key];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if (!error) {
if (!results) {
NSLog(@"No results were returned form query");
} else {
[self processNewDataWithResults:results andType:key];
dispatch_semaphore_signal(lock);
}
} else {
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
[self.healthStore executeQuery:query];
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
};
dispatch_group_async(queries, queue, queryBlock);
}
dispatch_group_notify(queries, queue, ^{
[self storeDataOnServer:achievementData];
[self checkAchievements:nil withManagedContext:context];
});
}
- (void)saveAchievements:(NSMutableArray *)achievements andThenCheck:(BOOL)check {
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.managedObjectContext;
NSMutableArray *achievementsToCheck = [NSMutableArray new];
for (PFObject *object in achievements) {
Achievement *achievement = [Achievement achievementWithObject:object inManagedContext:context];
if ([achievement.earned isEqualToNumber:[NSNumber numberWithBool:NO]]) {
//only add achievements that the user has not earned yet!
[achievementsToCheck addObject:achievement];
}
}
if (check)
[self checkAchievements:achievementsToCheck withManagedContext:context];
}
- (void)checkAchievements:(NSArray *)achievements withManagedContext:(NSManagedObjectContext *)context {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *achievementItems = [defaults objectForKey:@"achievementItems"];
achievementsToUpdate = [NSMutableArray new];
NSDate *today = [NSDate date];
int userSteps = [[achievementItems objectForKey:@"steps"] intValue];
int userDistance = [[achievementItems objectForKey:@"distance"] intValue];
int userFlights = [[achievementItems objectForKey:@"altitude"] intValue];
NSLog(@"%@------", achievementItems);
if (achievements == nil) {
//called statically, use database
NSFetchRequest *request = [NSFetchRequest new];
[request setEntity:[NSEntityDescription entityForName:@"Achievement" inManagedObjectContext:context]];
[request setPredicate:[NSPredicate predicateWithFormat:@"earned == %@", [NSNumber numberWithBool:NO]]];
NSError *error;
achievements = [context executeFetchRequest:request error:&error];
}
for (Achievement *achievement in achievements) {
NSString *type = achievement.type;
int achievementCount = [achievement.count intValue];
if ([type isEqualToString:@"steps"]) {
//steps achievement
if (userSteps >= achievementCount) {
//user earned this achievement
achievement.earned = [NSNumber numberWithBool:YES];
achievement.earnedDate = today;
[achievementsToUpdate addObject:achievement];
}
} else if ([type isEqualToString:@"distance"]) {
//distance achievement
if (userDistance >= achievementCount) {
//user earned this achievement
achievement.earned = [NSNumber numberWithBool:YES];
achievement.earnedDate = today;
[achievementsToUpdate addObject:achievement];
}
} else if ([type isEqualToString:@"altitude"]) {
//altitude achievement
if (userFlights >= achievementCount) {
//user earned this achievement
achievement.earned = [NSNumber numberWithBool:YES];
achievement.earnedDate = today;
[achievementsToUpdate addObject:achievement];
}
}
}
for (Achievement *achievement in achievementsToUpdate) {
PFObject *achievementToSave = [PFObject objectWithClassName:@"EarnedAchievement"];
achievementToSave[@"achievementId"] = achievement.id;
achievementToSave[@"user"] = [PFUser currentUser];
achievementToSave[@"earnedOn"] = achievement.earnedDate;
[achievementToSave saveInBackground];
//update achievement in database and send the user a notification!
UILocalNotification *notification = [UILocalNotification new];
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];
notification.alertBody = @"Achievement Unlocked! Come take a look.";
notification.timeZone = [NSTimeZone localTimeZone];
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
NSMutableDictionary *info = [NSMutableDictionary new];
info[@"title"] = achievement.title;
info[@"message"] = achievement.name;
info[@"description"] = achievement.descriptor;
info[@"image"] = achievement.image;
[self displayModalViewControllerWithInfo:info];
}
NSError *error;
[context save:&error];
}
- (void)processNewDataWithResults:(NSArray *)results andType:(NSString *)type {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *unitPreference = [defaults objectForKey:@"unitPreference"];
achievementData = [[defaults objectForKey:@"achievementItems"] mutableCopy];
int typeValue = [[dataTypes objectForKey:type] intValue];
//initialization
double totalSteps = 0;
double totalFlights = 0;
double totalDistanceWalking = 0;
double totalDistanceCycling = 0;
int distanceWalkingCount = 0;
int distanceCyclingCount = 0;
int existingSteps = [achievementData[@"steps"] intValue];
int existingFlights = [achievementData[@"altitude"] intValue];
int existingDistance = [achievementData[@"distance"] intValue];
switch (typeValue) {
case 1:
{
//Steps Traveled
for (HKQuantitySample *sample in results) {
double steps = [[sample quantity] doubleValueForUnit:[HKUnit countUnit]];
totalSteps += steps;
}
double difference = totalSteps - existingSteps;
if (difference > 0)
existingSteps += (int)difference;
break;
}
case 2:
{
//Flights Climbed
for (HKQuantitySample *sample in results) {
double flights = [[sample quantity] doubleValueForUnit:[HKUnit countUnit]];
totalFlights += flights;
}
double difference = totalFlights - existingFlights;
if (difference > 0)
existingFlights += (int)difference;
break;
}
case 3:
{
//Distance Traveled
for (HKQuantitySample *sample in results) {
HKUnit *unit = nil;
if ([unitPreference isEqualToString:@"standard"]) {
unit = [HKUnit mileUnit];
} else if ([unitPreference isEqualToString:@"metric"]) {
unit = [HKUnit meterUnitWithMetricPrefix:HKMetricPrefixKilo];
}
double distance = [[sample quantity] doubleValueForUnit:unit];
totalDistanceWalking += distance;
distanceWalkingCount = (int)totalDistanceWalking;
}
break;
}
case 4:
{
//Cycling
for (HKQuantitySample *sample in results) {
HKUnit *unit = nil;
if ([unitPreference isEqualToString:@"standard"]) {
unit = [HKUnit mileUnit];
} else if ([unitPreference isEqualToString:@"metric"]) {
unit = [HKUnit meterUnitWithMetricPrefix:HKMetricPrefixKilo];
}
double miles = [[sample quantity] doubleValueForUnit:[HKUnit mileUnit]];
totalDistanceCycling += miles;
distanceCyclingCount = (int)totalDistanceCycling;
}
break;
}
default:
break;
}
double totalDistance = totalDistanceWalking + totalDistanceCycling;
double difference = totalDistance - existingDistance;
if (difference > 0)
existingDistance += (int)difference;
[achievementData setObject:[NSNumber numberWithInt:existingSteps] forKey:@"steps"];
[achievementData setObject:[NSNumber numberWithInt:existingFlights] forKey:@"altitude"];
[achievementData setObject:[NSNumber numberWithInt:existingDistance] forKey:@"distance"];
[achievementData setObject:[NSDate date] forKey:@"lastCheckedDate"];
[defaults setObject:achievementData forKey:@"achievementItems"];
}
- (void)storeDataOnServer:(NSMutableDictionary *)data {
NSDate *now = [NSDate date];
PFUser *currentUser = [PFUser currentUser];
PFQuery *query = [PFQuery queryWithClassName:@"Progress"];
[query whereKey:@"user" equalTo:currentUser];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
if ([objects count] > 0) {
PFObject *object = [objects firstObject];
//user exists, update
object[@"steps"] = data[@"steps"];
object[@"altitude"] = data[@"altitude"];
object[@"distance"] = data[@"distance"];
object[@"lastCheckedDate"] = now;
[object saveInBackground];
} else {
//user doesn't exist create new
PFObject *entry = [PFObject objectWithClassName:@"Progress"];
entry[@"user"] = currentUser;
entry[@"steps"] = data[@"steps"];
entry[@"altitude"] = data[@"altitude"];
entry[@"distance"] = data[@"distance"];
entry[@"lastCheckedDate"] = now;
[entry saveInBackground];
}
} else {
NSLog(@"Error: %@ %@", error, [error userInfo]);
}
}];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)goToProfile:(id)sender {
self.selectedIndex = 0;
[profileButton setSelected:YES];
[achievementsButton setSelected:NO];
[homeButton setSelected:NO];
[settingsButton setSelected:NO];
}
- (IBAction)goToAchievements:(id)sender {
self.selectedIndex = 1;
[profileButton setSelected:NO];
[achievementsButton setSelected:YES];
[homeButton setSelected:NO];
[settingsButton setSelected:NO];
}
- (IBAction)goToSettings:(id)sender {
self.selectedIndex = 2;
[profileButton setSelected:NO];
[achievementsButton setSelected:NO];
[homeButton setSelected:NO];
[settingsButton setSelected:YES];
}
- (IBAction)goToHome:(id)sender {
self.selectedIndex = 3;
[profileButton setSelected:NO];
[achievementsButton setSelected:NO];
[homeButton setSelected:YES];
[settingsButton setSelected:NO];
}
- (void)authenticateUserWithViewController:(UIViewController *)viewController {
[self presentViewController:viewController animated:YES completion:nil];
}
@end
If anyone has any questions or comments, please just let me know.