Classes/RootViewController.m

/*
     File: RootViewController.m
 Abstract: The main view controller for this app, showing the preferred background color.
  Version: 1.2
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2013 Apple Inc. All Rights Reserved.
 
 */
 
#import "RootViewController.h"
 
typedef enum ColorIndex:uint32_t
{   // it is a good idea to give your enums a specific bit size.
    kColorWhite = 0,
    kColorRed,
    kColorGreen,
    kColorYellow,
} ColorIndex;
 
bool IsValidColorIndex(long long colorIndex)
{
    return kColorWhite <= colorIndex <= kColorYellow;
}
 
void AssertValidColorIndex(ColorIndex colorIndex)
{
    NSCAssert(IsValidColorIndex(colorIndex), @"Invalid Color Index %d", colorIndex);
}
 
 
#pragma mark -
 
@interface RootViewController() <UITableViewDelegate, UITableViewDataSource>
 
@property (nonatomic, strong) IBOutlet UIView *frontView;
@property (nonatomic, strong) IBOutlet UIView *backView;
 
@property (nonatomic, strong) NSArray *priorConstraints;
 
@end
 
 
#pragma mark -
 
@implementation RootViewController
 
#pragma mark - ColorIndex Conversion Methods
 
// convert a ColorIndex into a UIColor that can be used for a background
- (UIColor *)backgroundColorFromColorIndex:(ColorIndex)colorIndex
{
    AssertValidColorIndex(colorIndex);
    switch (colorIndex)
    {
        case kColorWhite:  return [UIColor whiteColor];
        case kColorRed:    return [UIColor redColor];
        case kColorGreen:  return [UIColor greenColor];
        case kColorYellow: return [UIColor yellowColor];
    }
    return nil;
}
 
// the display name for a color index
- (NSString *)colorNameFromColorIndex:(ColorIndex)colorIndex
{
    AssertValidColorIndex(colorIndex);
    switch (colorIndex)
    {
        case kColorWhite:  return NSLocalizedString(@"White",@"");
        case kColorRed:    return NSLocalizedString(@"Red",@"");
        case kColorGreen:  return NSLocalizedString(@"Green",@"");
        case kColorYellow: return NSLocalizedString(@"Yellow",@"");
    }
    
    return NSLocalizedString(@"Unknown Color",@"");
}
 
- (NSIndexPath*)indexPathForColorIndex:(ColorIndex)ci
{
    AssertValidColorIndex(ci);
    return [NSIndexPath indexPathForRow:ci inSection:0];
}
 
- (ColorIndex)colorIndexAtIndexPath:(NSIndexPath*)path;
{
    ColorIndex ci = path.row;
    AssertValidColorIndex(ci);
    return ci;
}
 
 
#pragma mark - Color Saving
 
// The key-value store is not a replacement for NSUserDefaults or other local techniques
// for saving the same data. The purpose of the key-value store is to share data between apps,
// but if iCloud is not enabled or is not available on a given device, you still might want
// to keep a local copy of the data.
//
// For more information, see the "Preferences and Settings Programming Guide: Storing Preferences in iCloud":
//  <http://developer.apple.com/library/ios/documentation/cocoa/Conceptual/UserDefaults/StoringPreferenceDatainiCloud/StoringPreferenceDatainiCloud.html#//apple_ref/doc/uid/10000059i-CH7-SW7>
//
// It is important to keep your NSUserDefaults and NSUbiquitousKeyValueStore values in sync.
// It helps to only update them from a method that updates them both.
 
 
// preference key for storing our background color (used both for NSUserDefaults and KVStore)
static NSString *kBackgroundColorKey = @"backgroundColor";
 
// We always read the chosen color from local NSUserDefaults.
// NSUbiquitousKeyValueStore is used to update NSUserDefaults.
// Default is kColorWhite (0) if no color has been chosen yet.
//
- (NSInteger) chosenColorIndex;
{
    return [[NSUserDefaults standardUserDefaults] integerForKey:kBackgroundColorKey];
}
 
// update the chosen color
- (void) setChosenColorIndex:(ColorIndex)newColorIndex;
{
    AssertValidColorIndex(newColorIndex); //it is a programming error to attempt to save an invalid value.
    
    NSUbiquitousKeyValueStore *kvStore = [NSUbiquitousKeyValueStore defaultStore];
    [kvStore setLongLong:newColorIndex forKey:kBackgroundColorKey];
    
    [[NSUserDefaults standardUserDefaults] setInteger:newColorIndex forKey:kBackgroundColorKey];
    
    // update the UI
    [self updateFrontViewColor];
}
 
 
#pragma mark - View Methods
 
- (void)updateFrontViewColor;
{
    UIColor *chosenColor = [self backgroundColorFromColorIndex:self.chosenColorIndex];
    self.frontView.backgroundColor = chosenColor;
}
 
- (UIBarButtonItem *)doneButton
{
    return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                                                         target:self
                                                         action:@selector(flipAction:)];
}
 
- (UIBarButtonItem *)flipButton
{
    UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
    [infoButton addTarget:self action:@selector(flipAction:) forControlEvents:UIControlEventTouchUpInside];
    return [[UIBarButtonItem alloc] initWithCustomView:infoButton];
}
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // listen for key-value store changes to our preference value, externally from the cloud
    NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(updateCloudItems:)
                                                 name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
                                               object:store];
        // note: by passing "object", it tells the cloud that we want to use "key-value store"
        // (which will allow other devices to automatically sync)
 
    self.frontView.frame = self.view.bounds;
    self.backView.frame = self.view.bounds;
    
    [self.view addSubview:self.frontView]; // start out showing the front view
    
    // add our custom 'i' flip button as the nav bar's custom right view
    self.navigationItem.rightBarButtonItem = [self flipButton];
       
    // make sure we're showing the latest color.
    [self updateFrontViewColor];
    
    // Get any KVStore change since last launch,
    // This will spark the notification "NSUbiquitousKeyValueStoreDidChangeExternallyNotification",
    // to any interested party within this app who is listening for iCloud KVStore changes.
    //
    // It is important to only do this step *after* registering for notifications,
    // this prevents a notification arriving before code is ready to respond to it.
    //
    [store synchronize];
}
 
- (void)dealloc
{
    // Even though we are using ARC, we still need to manually stop observing any
    // NSNotificationCenter notifications.  Otherwise we could get "zombie" crashes when
    // NSNotificationCenter tries to notify us after our -dealloc finished.
    //
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
                                                  object:[NSUbiquitousKeyValueStore defaultStore]];
}
 
 
#pragma mark - Actions
 
// makes "subview" match the width and height of "superview" by adding the proper auto layout constraints
//
- (NSArray *)constrainSubview:(UIView *)subview toMatchWithSuperview:(UIView *)superview
{
    subview.translatesAutoresizingMaskIntoConstraints = NO;
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(subview);
    
    NSArray *constraints = [NSLayoutConstraint
                            constraintsWithVisualFormat:@"H:|[subview]|"
                            options:0
                            metrics:nil
                            views:viewsDictionary];
    constraints = [constraints arrayByAddingObjectsFromArray:
                   [NSLayoutConstraint
                    constraintsWithVisualFormat:@"V:|[subview]|"
                    options:0
                    metrics:nil
                    views:viewsDictionary]];
    [superview addConstraints:constraints];
    
    return constraints;
}
 
// called when the user presses the 'i' icon to change the app settings
//
- (void)flipAction:(id)sender
{
    BOOL goingToFrontView = (self.backView.superview != nil);
 
    UIView *fromView = goingToFrontView ? self.backView : self.frontView;
    UIView *toView = goingToFrontView ? self.frontView : self.backView;
    
    toView.frame = fromView.frame;
    
    UIViewAnimationOptions transitionDirection = goingToFrontView ? UIViewAnimationOptionTransitionFlipFromRight : UIViewAnimationOptionTransitionFlipFromLeft;
    UIBarButtonItem *finalRightBarButtonItem = goingToFrontView ? [self flipButton] : [self doneButton];
    
    NSArray *priorConstraints = self.priorConstraints;
    [UIView transitionFromView:fromView
                        toView:toView
                      duration:1.0
                       options:transitionDirection
                    completion:^(BOOL finished) {
                        // animation completed, adjust our done/info buttons accordingly
                        [self.navigationItem setRightBarButtonItem:finalRightBarButtonItem animated:YES];
                        
                        if (priorConstraints != nil)
                        {
                            [self.view removeConstraints:priorConstraints];
                        }
                    }];
    _priorConstraints = [self constrainSubview:toView toMatchWithSuperview:self.view];
}
 
 
#pragma mark - UITableViewDelegate
 
const NSInteger kNumberOfColorRows = 4;
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // clean out the old checkmark state
    for (NSInteger row = 0; row < kNumberOfColorRows; row++)
    {
        NSIndexPath *rowIndexPath = [NSIndexPath indexPathForRow:row inSection:0];
        [tableView cellForRowAtIndexPath:rowIndexPath].accessoryType = UITableViewCellAccessoryNone;
    }
    
    // apply the new checkmark state
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.accessoryType = UITableViewCellAccessoryCheckmark;
    
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    ColorIndex chosenColor = [self colorIndexAtIndexPath:indexPath];
    [self setChosenColorIndex:chosenColor];
}
 
 
#pragma mark - UITableViewDataSource
 
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return NSLocalizedString(@"Background Colors:",@"");
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return kNumberOfColorRows;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *kOneCellID = @"cellID";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kOneCellID];
    if (cell == nil){
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kOneCellID];
        cell.selectionStyle = UITableViewCellSelectionStyleBlue;
    }
    
    NSIndexPath *chosenIndex = [self indexPathForColorIndex:[self chosenColorIndex]];
    // checkmark the selected cell
    if ([indexPath isEqual:chosenIndex])
    {
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
    }
    else
    {
        cell.accessoryType = UITableViewCellAccessoryNone;
    }
    
    cell.textLabel.text = [self colorNameFromColorIndex:indexPath.row];
    
    return cell;
}
 
 
#pragma mark - Cloud support
 
// This method is called when the key-value store in the cloud has changed externally.
// The old color value is replaced with the new one. Additionally, NSUserDefaults is updated as well.
//
- (void)updateCloudItems:(NSNotification *)notification
{
    // We get more information from the notification, by using:
    // NSUbiquitousKeyValueStoreChangeReasonKey or NSUbiquitousKeyValueStoreChangedKeysKey constants
    // against the notification's useInfo.
    //
    NSDictionary *userInfo = [notification userInfo];
    
    // get the reason for the notification (initial download, external change or quota violation change)
    NSInteger reason = [[userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey] integerValue];
    
    // reason can be:
    //
    // NSUbiquitousKeyValueStoreServerChange:
    //      Value(s) were changed externally from other users/devices.
    //      Get the changes and update the corresponding keys locally.
    // 
    // NSUbiquitousKeyValueStoreInitialSyncChange:
    //      Initial downloads happen the first time a device is connected to an iCloud account,
    //      and when a user switches their primary iCloud account.
    //      Get the changes and update the corresponding keys locally.
    //
    // note: if you receive "NSUbiquitousKeyValueStoreInitialSyncChange" as the reason,
    // you can decide to "merge" your local values with the server values
    //
    if (reason == NSUbiquitousKeyValueStoreInitialSyncChange)
    {
        // do the merge
        // ... but for this sample we have only one value, so a merge is not necessary
    }
    
    NSArray *changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
    
    // check if any of the keys we care about were updated, and if so use the new value stored under that key.
    for (NSString *changedKey in changedKeys)
    {
        if ([changedKey isEqualToString:kBackgroundColorKey])
        {
            // Replace our "selectedColor" with the value from the cloud, but *only* if it's a value we know how to interpret.
            // It is important to validate any value that comes in through iCloud, because it could have been generated by a different version of your app.
            long long possibleColorIndexFromiCloud = [[NSUbiquitousKeyValueStore defaultStore] longLongForKey:kBackgroundColorKey];
            if (IsValidColorIndex(possibleColorIndexFromiCloud))
            {
                // we know the new value is valid, use it.
                [self setChosenColorIndex:(ColorIndex)possibleColorIndexFromiCloud];
            }
            else
            {
                // the value isn't something we can understand.
                // The best way to handle an unexpected value depends on what the value represents, and what your app does.
                // But a good rule of thumb is to ignore values you can not interpret and not apply the update.
                //
                NSLog(@"WARNING: Invalid kBackgroundColorKey value of %lld received from iCloud. This value will be ignored.", possibleColorIndexFromiCloud);
            }
        }
    }
}
 
@end