View Controllers/SetupViewController.m
/* |
File: SetupViewController.h |
Contains: Lets the user configure the gallery to view. |
Written by: DTS |
Copyright: Copyright (c) 2010 Apple Inc. All Rights Reserved. |
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. |
*/ |
#import "SetupViewController.h" |
#import "NetworkManager.h" |
@interface SetupViewController () <UITextFieldDelegate> |
// private properties |
@property (nonatomic, assign, readonly ) BOOL canSave; |
@property (nonatomic, retain, readonly ) NSMutableArray * choices; |
@property (nonatomic, assign, readwrite) BOOL choicesDirty; |
@property (nonatomic, assign, readwrite) NSUInteger choiceIndex; |
@property (nonatomic, copy, readwrite) NSString * otherChoice; |
@property (nonatomic, retain, readwrite) UITextField * activeTextField; |
// forward declarations |
- (NSString *)smartURLStringForString:(NSString *)str; |
- (IBAction)saveAction:(id)sender; |
@end |
@implementation SetupViewController |
+ (void)resetChoices |
// See comment in header. |
{ |
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"setupChoices"]; |
} |
- (id)initWithGalleryURLString:(NSString *)galleryURLString |
{ |
// galleryURLString may be nil |
self = [super initWithStyle:UITableViewStylePlain]; |
if (self != nil) { |
NSUInteger choiceIndex; |
NSUInteger choiceCount; |
// Get the current list of choices, or start with the defaults. |
self->_choices = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"setupChoices"] mutableCopy]; |
if (self->_choices == nil) { |
#if TARGET_IPHONE_SIMULATOR |
#define HOSTNAME "localhost" |
#else |
#define HOSTNAME "worker.local." |
#endif |
self->_choices = [[NSMutableArray alloc] initWithObjects: |
@"http://" HOSTNAME "/TestGallery/index.xml", |
@"http://" HOSTNAME "/TestGallery/index2.xml", |
@"http://" HOSTNAME "/TestGallery/index-empty.xml", |
@"http://" HOSTNAME "/TestGallery/index-big.xml", |
@"http://" HOSTNAME "/TestGallery/index-giant.xml", |
@"http://" HOSTNAME "/TestGallery/oddballs.xml", |
@"http://" HOSTNAME "/TestGallery/changes.xml", |
@"http://" HOSTNAME "/TestGallery/broken-empty.xml", |
@"http://" HOSTNAME "/TestGallery/broken-html.html", |
@"http://" HOSTNAME "/TestGallery/broken-html.xml", |
@"http://" HOSTNAME "/TestGallery/broken-text.txt", |
@"http://" HOSTNAME "/TestGallery/broken-text.xml", |
@"http://" HOSTNAME "/TestGallery/broken-xml.xml", |
@"http://" HOSTNAME "/TestGallery/broken-attributes.xml", |
@"http://" HOSTNAME "/TestGallery/broken-images.xml", |
nil |
]; |
} |
assert(self->_choices != nil); |
// Eliminate anything that doesn't look like a URL. |
choiceCount = [self->_choices count]; |
for (choiceIndex = 0; choiceIndex < choiceCount; choiceIndex++) { |
NSString * tmp; |
tmp = [self->_choices objectAtIndex:choiceIndex]; |
if ( ! [tmp isKindOfClass:[NSString class]] ) { |
tmp = nil; |
} else { |
tmp = [self smartURLStringForString:tmp]; |
} |
if ( (tmp == nil) || ([tmp length] == 0) ) { |
[self->_choices removeObjectAtIndex:choiceIndex]; |
choiceIndex -= 1; |
choiceCount -= 1; |
} else { |
[self->_choices replaceObjectAtIndex:choiceIndex withObject:tmp]; |
} |
} |
// Get the current choice. If there is no current choice, we select the "other" |
// row. If there is a current choice, set up choiceIndex to point to it. |
// If the current choice isn't in the the choices list, add an item in that |
// list to make it so (and set choicesDirty so that we save back the new |
// list of the user taps Save). |
if (galleryURLString == nil) { |
self->_choiceIndex = [self->_choices count]; |
} else { |
self->_choiceIndex = [self->_choices indexOfObject:galleryURLString]; |
if (self->_choiceIndex == NSNotFound) { |
self->_choiceIndex = [self->_choices count]; |
[self->_choices addObject:[[galleryURLString copy] autorelease]]; |
self->_choicesDirty = YES; |
} |
} |
// Add an observer to update the enabled state on the Save button. |
[self addObserver:self forKeyPath:@"canSave" options:NSKeyValueObservingOptionInitial context:&self->_choiceIndex]; |
} |
return self; |
} |
- (void)dealloc |
{ |
[self removeObserver:self forKeyPath:@"canSave"]; |
[self->_choices release]; |
[self->_otherChoice release]; |
[self->_activeTextField release]; |
[super dealloc]; |
} |
@synthesize delegate = _delegate; |
@synthesize choices = _choices; |
@synthesize choicesDirty = _choicesDirty; |
@synthesize choiceIndex = _choiceIndex; |
@synthesize otherChoice = _otherChoice; |
@synthesize activeTextField = _activeTextField; |
- (NSString *)smartURLStringForString:(NSString *)str |
// Returns a URL string for the specified string, handling all sorts of edge cases. |
// This can returns one of three different types of result: |
// |
// o If str is empty (or nil), it returns the empty string (@""). |
// o If str is an invalid URL, it returns nil. |
// o If string is a valid URL, it returns a non-nil, non-empty string. |
{ |
NSString * result; |
NSRange schemeMarkerRange; |
NSString * scheme; |
result = nil; |
// Treat nil as empty and then trim any whitespace. |
if (str == nil) { |
str = @""; |
} |
str = [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; |
if ( (str == nil) || ([str length] == 0) ) { |
result = @""; |
} else { |
NSURL * resultURL; |
schemeMarkerRange = [str rangeOfString:@"://"]; |
resultURL = nil; |
if (schemeMarkerRange.location == NSNotFound) { |
// If the string does not contain "://", add the "http://" prefix. |
resultURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", str]]; |
} else { |
// Check the scheme to see if it's one we support. |
scheme = [str substringWithRange:NSMakeRange(0, schemeMarkerRange.location)]; |
assert(scheme != nil); |
if ( ([scheme compare:@"http" options:NSCaseInsensitiveSearch] == NSOrderedSame) |
|| ([scheme compare:@"https" options:NSCaseInsensitiveSearch] == NSOrderedSame) ) { |
resultURL = [NSURL URLWithString:str]; |
} else { |
// It looks like this is some unsupported URL scheme. |
} |
} |
// If we managed to create a URL, get the result string from that. |
if (resultURL != nil) { |
if ( [resultURL host] != nil ) { |
result = [resultURL absoluteString]; |
} |
} |
} |
assert( (result == nil) || ([result length] == 0) || ([NSURL URLWithString:result] != nil) ); |
return result; |
} |
- (NSString *)effectiveChoice |
// Returns the current choice displayed in the UI, which is either one of the selected |
// choices or the string from the "other" row. This has the same post condition as |
// -smartURLStringForString:. |
{ |
NSString * result; |
if (self.choiceIndex < [self.choices count]) { |
result = [self.choices objectAtIndex:self.choiceIndex]; |
assert( [NSURL URLWithString:result] != nil ); |
} else { |
result = [self smartURLStringForString:self.otherChoice]; |
} |
assert( (result == nil) || ([result length] == 0) || ([NSURL URLWithString:result] != nil) ); |
return result; |
} |
+ (NSSet *)keyPathsForValuesAffectingCanSave |
{ |
return [NSSet setWithObjects:@"otherChoice", @"choiceIndex", nil]; |
} |
- (BOOL)canSave |
// Returns YES if the current choice displayed in the UI is valid enough to be saved. |
{ |
BOOL result; |
result = (self.choiceIndex != [self.choices count]); |
if ( ! result ) { |
result = ([self effectiveChoice] != nil); |
} |
return result; |
} |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context |
{ |
if (context == &self->_choiceIndex) { |
// Called as our canSave property changes. We respond by enabling or disabling |
// the Save bar button item. |
assert([keyPath isEqual:@"canSave"]); |
assert(object == self); |
self.navigationItem.rightBarButtonItem.enabled = self.canSave; |
} else if (NO) { // Disabled because the super class does nothing useful with it. |
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
} |
} |
#pragma mark * View controller stuff |
- (void)viewDidDisappear:(BOOL)animated |
{ |
[super viewDidDisappear:animated]; |
assert(self.activeTextField == nil); // We shouldn't disappear with an active text field. |
} |
#pragma mark * Table view callbacks |
- (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)section |
{ |
#pragma unused(tv) |
#pragma unused(section) |
assert(tv == self.tableView); |
assert(section == 0); |
return [self.choices count] + 1; // +1 to account for "other" row |
} |
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
#pragma unused(tv) |
#pragma unused(indexPath) |
BOOL otherCell; |
NSString * cellID; |
UITableViewCell * cell; |
UITextField * textField; |
assert(tv == self.tableView); |
assert(indexPath != NULL); |
assert(indexPath.section == 0); |
assert(indexPath.row < ([self.choices count] + 1)); |
// Use one cell identifier for the "other" row, and another for all the normal rows. |
otherCell = (indexPath.row == [self.choices count]); |
if (otherCell) { |
cellID = @"otherCell"; |
} else { |
cellID = @"cell"; |
} |
// Create the cell itself. Doing this for the "other" row is a little complex (-: |
cell = [self.tableView dequeueReusableCellWithIdentifier:cellID]; |
if (cell == nil) { |
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID] autorelease]; |
assert(cell != nil); |
if (otherCell) { |
CGRect frame; |
frame = CGRectZero; |
frame.size = cell.contentView.frame.size; |
frame.origin.x += 10.0f; |
frame.size.width -= 20.0f; |
frame.origin.y = 6.0f; |
frame.size.height -= 12.0f; |
textField = [[[UITextField alloc] initWithFrame:frame] autorelease]; |
assert(textField != nil); |
textField.tag = 666; |
textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; |
textField.autoresizingMask = UIViewAutoresizingFlexibleWidth; |
textField.placeholder = @"other"; |
textField.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]]; |
textField.autocapitalizationType = UITextAutocapitalizationTypeNone; |
textField.autocorrectionType = UITextAutocorrectionTypeNo; |
textField.keyboardType = UIKeyboardTypeURL; |
textField.clearButtonMode = UITextFieldViewModeWhileEditing; |
textField.delegate = self; |
[cell.contentView addSubview:textField]; |
} else { |
cell.textLabel.lineBreakMode = UILineBreakModeMiddleTruncation; |
cell.textLabel.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]]; |
} |
} |
// Set up the cell. |
if (indexPath.row < [self.choices count]) { |
// A standard cell. Just set the text label to the corresponding element |
// of the choices array. |
cell.textLabel.text = [self.choices objectAtIndex:indexPath.row]; |
} else { |
// The "other" cell. Find the text field embedded in the cell and set its |
// text to the current other choice. |
textField = (UITextField *) [cell.contentView viewWithTag:666]; |
assert([textField isKindOfClass:[UITextField class]]); |
textField.text = self.otherChoice; |
} |
cell.accessoryType = indexPath.row == self.choiceIndex ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; |
return cell; |
} |
- (void)chooseRow:(NSUInteger)row |
// Choose the specified row. This updates both the UI (that is, the checkmark |
// accessory view) and our choiceIndex property. |
{ |
UITableViewCell * cell; |
if (row != self.choiceIndex) { |
// If we're leaving the "other" row, take the keyboard focus away from it. |
if ( (row < [self.choices count]) && (self.activeTextField != nil) ) { |
[self.activeTextField resignFirstResponder]; |
} |
// Uncheck the currently checked cell, change the choice, and then recheck the newly checked cell. |
cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:self.choiceIndex inSection:0]]; |
if (cell != nil) { |
cell.accessoryType = UITableViewCellAccessoryNone; |
} |
self.choiceIndex = row; |
cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:self.choiceIndex inSection:0]]; |
if (cell != nil) { |
cell.accessoryType = UITableViewCellAccessoryCheckmark; |
} |
} |
} |
- (void)tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
#pragma unused(tv) |
#pragma unused(indexPath) |
assert(tv == self.tableView); |
assert(indexPath != NULL); |
assert(indexPath.section == 0); |
assert(indexPath.row < ([self.choices count] + 1)); |
[self chooseRow:indexPath.row]; |
[self.tableView deselectRowAtIndexPath:indexPath animated:YES]; |
} |
- (UITableViewCellEditingStyle)tableView:(UITableView *)tv editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath |
// While there's no way to put the table view in edit mode, we still support |
// "swipe to delete" for everything except the "other" row. |
{ |
#pragma unused(tv) |
#pragma unused(indexPath) |
assert(tv == self.tableView); |
assert(indexPath != NULL); |
assert(indexPath.section == 0); |
assert(indexPath.row < ([self.choices count] + 1)); |
return (indexPath.row < [self.choices count]) ? UITableViewCellEditingStyleDelete : UITableViewCellEditingStyleNone; |
} |
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath |
// Implement the mechanics of "swipe to delete". |
{ |
#pragma unused(tv) |
#pragma unused(editingStyle) |
#pragma unused(indexPath) |
assert(tv == self.tableView); |
assert(editingStyle == UITableViewCellEditingStyleDelete); |
assert(indexPath != NULL); |
assert(indexPath.section == 0); |
assert(indexPath.row < [self.choices count]); |
// If the user is deleting the currently chosen row, choose another one. There |
// are three cases: |
// |
// o If this is the last remaining normal choice, choose the "other" row. |
// o If this is the last normal choice, choose the row before this. |
// o Otherwise, choose the row after this row. |
if (indexPath.row == self.choiceIndex) { |
assert([self.choices count] != 0); // because the user has swiped to delete, and that's only possible for normal choices |
if ( [self.choices count] == 1 ) { |
[self chooseRow:1]; // We're about to delete the last remaining normal choice; switch to the "other" choice. |
} else if (indexPath.row == ([self.choices count] - 1)) { |
[self chooseRow:indexPath.row - 1]; // We're about to delete the last normal choice; switch to the previous choice. |
} else { |
[self chooseRow:indexPath.row + 1]; // We're about to delete some common-or-garden normal chocie; switch to the next choice. |
} |
} |
[self.choices removeObjectAtIndex:indexPath.row]; |
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; |
// If the choice index is after the row we just deleted, step it back by one. |
assert(indexPath.row != self.choiceIndex); // because we moved away from it in the previous code |
if (indexPath.row < self.choiceIndex) { |
self.choiceIndex -= 1; |
} |
self.choicesDirty = YES; |
} |
#pragma mark * Text field callbacks |
- (void)textFieldDidBeginEditing:(UITextField *)textField |
// There are there things to do here: |
// |
// o record the active text field so that we have a reference to it for |
// other purposes (like tell it to resignFirstResponder if the user |
// taps on another row) |
// o choose the "other" row so that the UI and chosenIndex reflect that |
// o add an observer for the UITextFieldTextDidChangeNotification so that |
// we can track the content of the text field in order to enable and |
// disable our Save button |
{ |
self.activeTextField = textField; |
[self chooseRow:[self.choices count]]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldDidChange:) name:UITextFieldTextDidChangeNotification object:self.activeTextField]; |
} |
- (void)textFieldDidEndEditing:(UITextField *)textField |
{ |
NSString * finalString; |
NSString * urlStr; |
assert(textField == self.activeTextField); |
#pragma unused(textField) |
// Push the text field value back to our property. In the process, |
// if it's a valid URL, put the full URL string back into the text field. |
// This allows the user to type "foo.com" and, when they're done, see |
// "http://foo.com". |
finalString = self.activeTextField.text; |
urlStr = [self smartURLStringForString:finalString]; |
if (urlStr != nil) { |
finalString = urlStr; |
self.activeTextField.text = finalString; |
} |
self.otherChoice = finalString; |
// Undo two of the three things done in -textFieldDidBeginEditing:. It's not |
// necessary to undo the last one; whether we choose a row other than the "other" |
// row is determined by other factors. |
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:self.activeTextField]; |
self.activeTextField = nil; |
} |
- (void)textFieldDidChange:(NSNotification *)note |
// As the text field in the "other" row changes, reflect that change to our |
// otherChoice property, which updates the Save button state via KVO. |
{ |
assert([note object] == self.activeTextField); |
#pragma unused(note) |
self.otherChoice = self.activeTextField.text; |
} |
- (BOOL)textFieldShouldReturn:(UITextField *)textField |
{ |
[self saveAction:textField]; |
return NO; |
} |
#pragma mark * Actions |
- (IBAction)saveAction:(id)sender |
// Called when the user taps the Save button. |
{ |
#pragma unused(sender) |
NSString * value; |
NSMutableArray * newChoices; |
// The following is necessary to flush the final URL string out to self.otherChoice. |
if (self.activeTextField != nil) { |
[self.activeTextField resignFirstResponder]; |
} |
// Get the value we're going to save. |
value = [self effectiveChoice]; |
assert(value != nil); // Save should be disabled in this case. |
// If the value is from the "other" field, add it to the choices array |
// (if appropriate). |
newChoices = [[self.choices mutableCopy] autorelease]; |
assert(newChoices != nil); |
if (self.choiceIndex == [self.choices count]) { |
if ([value length] != 0) { // don't add the empty string |
if ( ! [self.choices containsObject:value] ) { // don't repeat an existing value |
[newChoices addObject:value]; |
self.choicesDirty = YES; |
} |
} |
} |
// If the choices list is dirty, save it back to the user defaults. |
if (self.choicesDirty) { |
[[NSUserDefaults standardUserDefaults] setObject:newChoices forKey:@"setupChoices"]; |
self.choicesDirty = NO; |
} |
// Commit the choice of gallery to the network manager. This triggers a world |
// of reconfiguration via KVO. |
[self.delegate setupViewController:self didChooseString:value]; |
} |
- (IBAction)cancelAction:(id)sender |
// Called when the user taps the Cancel button. We just tell our delegate about it. |
{ |
#pragma unused(sender) |
[self.delegate setupViewControllerDidCancel:self]; |
} |
- (void)presentModallyOn:(UIViewController *)parent animated:(BOOL)animated |
{ |
UINavigationController * navController; |
navController = [[[UINavigationController alloc] initWithRootViewController:self] autorelease]; |
assert(navController != nil); |
self.navigationItem.title = @"Setup"; |
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveAction:) ] autorelease]; |
self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelAction:)] autorelease]; |
[parent presentModalViewController:navController animated:animated]; |
} |
@end |
Copyright © 2010 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-10-22