Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import

  • Loading branch information...
commit 40dbd77b1d07a456e8415e1fdc25fb1bb50aa008 0 parents
@mattt mattt authored
Showing with 9,857 additions and 0 deletions.
  1. +10 −0 .gitignore
  2. +10 −0 Classes/Categories/CLLocation+AFExtensions.h
  3. +94 −0 Classes/Categories/CLLocation+AFExtensions.m
  4. +26 −0 Classes/Categories/CLLocationManager+AFExtensions.h
  5. +89 −0 Classes/Categories/CLLocationManager+AFExtensions.m
  6. +73 −0 Classes/Categories/ISO8601DateFormatter.h
  7. +844 −0 Classes/Categories/ISO8601DateFormatter.m
  8. +7 −0 Classes/Categories/NSData+Base64.h
  9. +86 −0 Classes/Categories/NSData+Base64.m
  10. +16 −0 Classes/Controllers/AuthenticationViewController.h
  11. +34 −0 Classes/Controllers/AuthenticationViewController.m
  12. +25 −0 Classes/Controllers/CheckInSuccessViewController.h
  13. +54 −0 Classes/Controllers/CheckInSuccessViewController.m
  14. +28 −0 Classes/Controllers/PassportViewController.h
  15. +167 −0 Classes/Controllers/PassportViewController.m
  16. +38 −0 Classes/Controllers/SpotViewController.h
  17. +245 −0 Classes/Controllers/SpotViewController.m
  18. +21 −0 Classes/Controllers/SpotsViewController.h
  19. +175 −0 Classes/Controllers/SpotsViewController.m
  20. +25 −0 Classes/Gowalla_BasicAppDelegate.h
  21. +101 −0 Classes/Gowalla_BasicAppDelegate.m
  22. +23 −0 Classes/Helpers/GowallaAPI.h
  23. +60 −0 Classes/Helpers/GowallaAPI.m
  24. +21 −0 Classes/Models/AFObject.h
  25. +41 −0 Classes/Models/AFObject.m
  26. +26 −0 Classes/Models/CheckIn.h
  27. +45 −0 Classes/Models/CheckIn.m
  28. +34 −0 Classes/Models/Spot.h
  29. +69 −0 Classes/Models/Spot.m
  30. +29 −0 Classes/Models/User.h
  31. +55 −0 Classes/Models/User.m
  32. +20 −0 Classes/Views/AFImageLoadingCell.h
  33. +49 −0 Classes/Views/AFImageLoadingCell.m
  34. +596 −0 Gowalla-Basic.xcodeproj/project.pbxproj
  35. +19 −0 GowallaAPIKeys.h
  36. +21 −0 Gowalla_Basic_Prefix.pch
  37. +44 −0 Libraries/EGOHTTPRequest/EGOHTTPFormRequest.h
  38. +112 −0 Libraries/EGOHTTPRequest/EGOHTTPFormRequest.m
  39. +88 −0 Libraries/EGOHTTPRequest/EGOHTTPRequest.h
  40. +310 −0 Libraries/EGOHTTPRequest/EGOHTTPRequest.m
  41. +70 −0 Libraries/EGOImageLoader/EGOCache/EGOCache.h
  42. +295 −0 Libraries/EGOImageLoader/EGOCache/EGOCache.m
  43. +52 −0 Libraries/EGOImageLoader/EGOImageButton/EGOImageButton.h
  44. +108 −0 Libraries/EGOImageLoader/EGOImageButton/EGOImageButton.m
  45. +60 −0 Libraries/EGOImageLoader/EGOImageLoader/EGOImageLoadConnection.h
  46. +96 −0 Libraries/EGOImageLoader/EGOImageLoader/EGOImageLoadConnection.m
  47. +57 −0 Libraries/EGOImageLoader/EGOImageLoader/EGOImageLoader.h
  48. +192 −0 Libraries/EGOImageLoader/EGOImageLoader/EGOImageLoader.m
  49. +52 −0 Libraries/EGOImageLoader/EGOImageView/EGOImageView.h
  50. +107 −0 Libraries/EGOImageLoader/EGOImageView/EGOImageView.m
  51. +68 −0 Libraries/TouchJSON/CDataScanner.h
  52. +270 −0 Libraries/TouchJSON/CDataScanner.m
  53. +37 −0 Libraries/TouchJSON/Extensions/CDataScanner_Extensions.h
  54. +80 −0 Libraries/TouchJSON/Extensions/CDataScanner_Extensions.m
  55. +36 −0 Libraries/TouchJSON/Extensions/NSCharacterSet_Extensions.h
  56. +48 −0 Libraries/TouchJSON/Extensions/NSCharacterSet_Extensions.m
  57. +36 −0 Libraries/TouchJSON/Extensions/NSDictionary_JSONExtensions.h
  58. +41 −0 Libraries/TouchJSON/Extensions/NSDictionary_JSONExtensions.m
  59. +44 −0 Libraries/TouchJSON/Extensions/NSScanner_Extensions.h
  60. +118 −0 Libraries/TouchJSON/Extensions/NSScanner_Extensions.m
  61. +46 −0 Libraries/TouchJSON/JSON/CJSONDataSerializer.h
  62. +225 −0 Libraries/TouchJSON/JSON/CJSONDataSerializer.m
  63. +45 −0 Libraries/TouchJSON/JSON/CJSONDeserializer.h
  64. +95 −0 Libraries/TouchJSON/JSON/CJSONDeserializer.m
  65. +47 −0 Libraries/TouchJSON/JSON/CJSONScanner.h
  66. +545 −0 Libraries/TouchJSON/JSON/CJSONScanner.m
  67. +47 −0 Libraries/TouchJSON/JSON/CJSONSerializer.h
  68. +75 −0 Libraries/TouchJSON/JSON/CJSONSerializer.m
  69. +40 −0 Libraries/TouchJSON/JSON/CSerializedJSONData.h
  70. +54 −0 Libraries/TouchJSON/JSON/CSerializedJSONData.m
  71. +3 −0  README.markdown
  72. +41 −0 Resources/Info.plist
  73. +394 −0 Resources/XIB/AuthenticationView.xib
  74. +386 −0 Resources/XIB/CheckInSuccessView.xib
  75. +542 −0 Resources/XIB/MainWindow.xib
  76. +640 −0 Resources/XIB/PassportView.xib
  77. +745 −0 Resources/XIB/SpotView.xib
  78. +173 −0 Resources/XIB/SpotsView.xib
  79. +17 −0 main.m
10 .gitignore
@@ -0,0 +1,10 @@
+build
+*.mode1v3
+*.mode2v3
+*.nib
+.DS_Store
+*.swp
+*.pbxuser
+*.perspective
+*.perspectivev3
+profile
10 Classes/Categories/CLLocation+AFExtensions.h
@@ -0,0 +1,10 @@
+#import <CoreLocation/CoreLocation.h>
+
+@interface CLLocation (AFExtensions)
+
+- (double)bearingInRadiansTowardsLocation:(CLLocation *)towardsLocation;
+- (double)bearingInDegreesTowardsLocation:(CLLocation *)towardsLocation;
+- (CLLocation *)locationAtDistance:(CLLocationDistance)atDistance alongBearingInRadians:(double)bearingInRadians;
++ (CLLocation *)locationWithCoordinate:(CLLocationCoordinate2D)someCoordinate;
+
+@end
94 Classes/Categories/CLLocation+AFExtensions.m
@@ -0,0 +1,94 @@
+#import "CLLocation+AFExtensions.h"
+
+#define kPi 3.141592653589793
+
+// got these from: http://blog.digitalagua.com/2008/06/30/how-to-convert-degrees-to-radians-radians-to-degrees-in-objective-c/
+CGFloat DEG2RAD(CGFloat degrees) { return degrees * M_PI / 180; };
+CGFloat RAD2DEG(CGFloat radians) { return radians * 180 / M_PI; };
+
+@implementation CLLocation (AFExtensions)
+
+// calculate the bearing in the direction of towardsLocation from this location's coordinate
+// Formula: θ = atan2(sin(Δlong).cos(lat2), cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong))
+// Based on the formula as described at http://www.movable-type.co.uk/scripts/latlong.html
+// original JavaScript implementation © 2002-2006 Chris Veness
+- (double)bearingInRadiansTowardsLocation:(CLLocation *)towardsLocation {
+ double lat1 = DEG2RAD(self.coordinate.latitude);
+ double lon1 = DEG2RAD(self.coordinate.longitude);
+ double lat2 = DEG2RAD(towardsLocation.coordinate.latitude);
+ double lon2 = DEG2RAD(towardsLocation.coordinate.longitude);
+ double dLon = lon2 - lon1;
+ double y = sin(dLon) * cos(lat2);
+ double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
+ double bearing = atan2(y, x) + (2 * kPi);
+ // atan2 works on a range of -π to 0 to π, so add on 2π and perform a modulo check
+ if (bearing > (2 * kPi)) {
+ bearing = bearing - (2 * kPi);
+ }
+ return bearing;
+}
+
+- (double)bearingInDegreesTowardsLocation:(CLLocation *)towardsLocation {
+ return RAD2DEG([self bearingInRadiansTowardsLocation:towardsLocation]);
+}
+
+// calculate an endpoint given a startpoint, bearing and distance
+// Vincenty 'Direct' formula based on the formula as described at http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html
+// original JavaScript implementation © 2002-2006 Chris Veness
+- (CLLocation *)locationAtDistance:(CLLocationDistance)atDistance alongBearingInRadians:(double)bearingInRadians {
+ double lat1 = DEG2RAD(self.coordinate.latitude);
+ double lon1 = DEG2RAD(self.coordinate.longitude);
+
+ double a = 6378137, b = 6356752.3142, f = 1/298.257223563; // WGS-84 ellipsiod
+ double s = atDistance;
+ double alpha1 = bearingInRadians;
+ double sinAlpha1 = sin(alpha1);
+ double cosAlpha1 = cos(alpha1);
+
+ double tanU1 = (1 - f) * tan(lat1);
+ double cosU1 = 1 / sqrt((1 + tanU1 * tanU1));
+ double sinU1 = tanU1 * cosU1;
+ double sigma1 = atan2(tanU1, cosAlpha1);
+ double sinAlpha = cosU1 * sinAlpha1;
+ double cosSqAlpha = 1 - sinAlpha * sinAlpha;
+ double uSq = cosSqAlpha * (a * a - b * b) / (b * b);
+ double A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
+ double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
+
+ double sigma = s / (b * A);
+ double sigmaP = 2 * kPi;
+
+ double cos2SigmaM;
+ double sinSigma;
+ double cosSigma;
+
+ while (abs(sigma - sigmaP) > 1e-12) {
+ cos2SigmaM = cos(2 * sigma1 + sigma);
+ sinSigma = sin(sigma);
+ cosSigma = cos(sigma);
+ double deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
+ sigmaP = sigma;
+ sigma = s / (b * A) + deltaSigma;
+ }
+
+ double tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1;
+ double lat2 = atan2(sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1 - f) * sqrt(sinAlpha * sinAlpha + tmp * tmp));
+ double lambda = atan2(sinSigma * sinAlpha1, cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1);
+ double C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
+ double L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));
+
+ double lon2 = lon1 + L;
+
+ // create a new CLLocation for this point
+ CLLocation *edgePoint = [[CLLocation alloc] initWithLatitude:RAD2DEG(lat2) longitude:RAD2DEG(lon2)];
+ [edgePoint autorelease];
+
+ return edgePoint;
+}
+
++ (CLLocation *)locationWithCoordinate:(CLLocationCoordinate2D)someCoordinate {
+ return [[[CLLocation alloc] initWithLatitude:someCoordinate.latitude
+ longitude:someCoordinate.longitude] autorelease];
+}
+
+@end
26 Classes/Categories/CLLocationManager+AFExtensions.h
@@ -0,0 +1,26 @@
+//
+// CLLocationManager+AFExtensions.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <CoreLocation/CoreLocation.h>
+
+typedef enum {
+ GPSAccuracyZeroBars,
+ GPSAccuracyOneBar,
+ GPSAccuracyTwoBars,
+ GPSAccuracyThreeBars,
+} GPSAccuracyLevel;
+
+@interface CLLocationManager (AFExtensions)
+
+- (BOOL)withinRadius:(CLLocationDistance)radius ofLocation:(CLLocation *)someLocation;
+- (NSString *)directionToLocation:(CLLocation *)someLocation;
+- (NSString *)distanceToLocation:(CLLocation *)someLocation;
+- (NSString *)distanceAndDirectionTo:(CLLocation *)someLocation;
+
+@end
89 Classes/Categories/CLLocationManager+AFExtensions.m
@@ -0,0 +1,89 @@
+//
+// CLLocationManager+AFExtensions.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "CLLocationManager+AFExtensions.h"
+#import "CLLocation+AFExtensions.h"
+
+@implementation CLLocationManager (AFExtensions)
+
+- (GPSAccuracyLevel)accuracyLevel {
+ if (!self.location) {
+ return GPSAccuracyZeroBars;
+ }
+
+ CLLocationAccuracy accuracy = self.location.horizontalAccuracy;
+ if (accuracy > kCLLocationAccuracyKilometer ) {
+ return GPSAccuracyOneBar;
+ } else if (accuracy > kCLLocationAccuracyHundredMeters) {
+ return GPSAccuracyTwoBars;
+ } else {
+ return GPSAccuracyThreeBars;
+ }
+}
+
+- (BOOL)withinRadius:(CLLocationDistance)radius ofLocation:(CLLocation *)someLocation {
+ return self.location && someLocation && radius > [self.location distanceFromLocation:someLocation];
+}
+
+- (NSString *)directionToLocation:(CLLocation *)someLocation {
+ if(!self.location || !someLocation) {
+ return NSLocalizedString(@"unknown distance", nil);
+ }
+
+ double bearing = [self.location bearingInDegreesTowardsLocation:someLocation];
+
+ if(bearing > 337.5) {
+ return NSLocalizedString(@"north", nil);
+ } else if(bearing > 292.5) {
+ return NSLocalizedString(@"northwest", nil);
+ } else if(bearing > 247.5) {
+ return NSLocalizedString(@"west", nil);
+ } else if(bearing > 202.5) {
+ return NSLocalizedString(@"southwest", nil);
+ } else if(bearing > 157.5) {
+ return NSLocalizedString(@"south", nil);
+ } else if(bearing > 112.5) {
+ return NSLocalizedString(@"southeast", nil);
+ } else if(bearing > 67.5) {
+ return NSLocalizedString(@"east", nil);
+ } else if(bearing > 22.5) {
+ return NSLocalizedString(@"northeast", nil);
+ } else if(bearing > 0) {
+ return NSLocalizedString(@"north", nil);
+ } else {
+ return NSLocalizedString(@"unknown direction", nil);
+ }
+}
+
+- (NSString *)distanceToLocation:(CLLocation *)someLocation {
+ if(!self.location || !someLocation) {
+ return NSLocalizedString(@"unknown distance", nil);
+ }
+
+ float meters = [self.location distanceFromLocation:someLocation];
+ if(meters > 1000.0) {
+ float km = meters * 0.001;
+ if(km > 5.0) {
+ return [NSString stringWithFormat:NSLocalizedString(@"%0.0f km", nil), km];
+ } else {
+ return [NSString stringWithFormat:NSLocalizedString(@"%0.1f km", nil), km];
+ }
+ } else {
+ return [NSString stringWithFormat:NSLocalizedString(@"%0.0f meters", nil), meters];
+ }
+}
+
+- (NSString *)distanceAndDirectionTo:(CLLocation *)someLocation {
+ if(!self.location || !someLocation) {
+ return NSLocalizedString(@"unknown distance", nil);
+ }
+
+ return [NSString stringWithFormat:@"%@ %@", [self distanceToLocation:someLocation], [self directionToLocation:someLocation]];
+}
+
+@end
73 Classes/Categories/ISO8601DateFormatter.h
@@ -0,0 +1,73 @@
+/*ISO8601DateFormatter.h
+ *
+ *Created by Peter Hosey on 2009-04-11.
+ *Copyright 2009 Peter Hosey. All rights reserved.
+ */
+
+#import <Foundation/Foundation.h>
+
+/*This class converts dates to and from ISO 8601 strings. A good introduction to ISO 8601: <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>
+ *
+ *Parsing can be done strictly, or not. When you parse loosely, leading whitespace is ignored, as is anything after the date.
+ *The loose parser will return an NSDate for this string: @" \t\r\n\f\t 2006-03-02!!!"
+ *Leading non-whitespace will not be ignored; the string will be rejected, and nil returned. See the README that came with this addition.
+ *
+ *The strict parser will only accept a string if the date is the entire string. The above string would be rejected immediately, solely on these grounds.
+ *Also, the loose parser provides some extensions that the strict parser doesn't.
+ *For example, the standard says for "-DDD" (an ordinal date in the implied year) that the logical representation (meaning, hierarchically) would be "--DDD", but because that extra hyphen is "superfluous", it was omitted.
+ *The loose parser will accept the extra hyphen; the strict parser will not.
+ *A full list of these extensions is in the README file.
+ */
+
+/*The format to either expect or produce.
+ *Calendar format is YYYY-MM-DD.
+ *Ordinal format is YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01.
+ *Week format is YYYY-Www-D, where ww ranges from 1 to 53 (the 'W' is literal) and D ranges from 1 to 7; for example, 2009-W05-07.
+ */
+enum {
+ ISO8601DateFormatCalendar,
+ ISO8601DateFormatOrdinal,
+ ISO8601DateFormatWeek,
+};
+typedef NSUInteger ISO8601DateFormat;
+
+//The default separator for time values. Currently, this is ':'.
+extern unichar ISO8601DefaultTimeSeparatorCharacter;
+
+@interface ISO8601DateFormatter: NSFormatter
+{
+ NSTimeZone *defaultTimeZone;
+ ISO8601DateFormat format;
+ unichar timeSeparator;
+ unichar timeZoneSeparator;
+ BOOL includeTime;
+ BOOL parsesStrictly;
+}
+
+@property(retain) NSTimeZone *defaultTimeZone;
+
+#pragma mark Parsing
+
+//As a formatter, this object converts strings to dates.
+
+@property BOOL parsesStrictly;
+
+- (NSDateComponents *) dateComponentsFromString:(NSString *)string;
+- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone;
+- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange;
+
+- (NSDate *) dateFromString:(NSString *)string;
+- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone;
+- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange;
+
+#pragma mark Unparsing
+
+@property ISO8601DateFormat format;
+@property BOOL includeTime;
+@property unichar timeSeparator;
+@property unichar timeZoneSeparator;
+
+- (NSString *) stringFromDate:(NSDate *)date;
+- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone;
+
+@end
844 Classes/Categories/ISO8601DateFormatter.m
@@ -0,0 +1,844 @@
+/*ISO8601DateFormatter.m
+ *
+ *Created by Peter Hosey on 2009-04-11.
+ *Copyright 2009 Peter Hosey. All rights reserved.
+ */
+
+#import <Foundation/Foundation.h>
+#import "ISO8601DateFormatter.h"
+
+#ifndef DEFAULT_TIME_SEPARATOR
+# define DEFAULT_TIME_SEPARATOR ':'
+#endif
+unichar ISO8601DefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR;
+
+//Unicode date formats.
+#define ISO_CALENDAR_DATE_FORMAT @"yyyy-MM-dd"
+//#define ISO_WEEK_DATE_FORMAT @"YYYY-'W'ww-ee" //Doesn't actually work because NSDateComponents counts the weekday starting at 1.
+#define ISO_ORDINAL_DATE_FORMAT @"yyyy-DDD"
+#define ISO_TIME_FORMAT @"HH:mm:ss"
+#define ISO_TIME_WITH_TIMEZONE_FORMAT ISO_TIME_FORMAT @"Z"
+//printf formats.
+#define ISO_TIMEZONE_UTC_FORMAT @"Z"
+#define ISO_TIMEZONE_OFFSET_FORMAT @"%+02d%@%02d"
+
+@interface ISO8601DateFormatter(UnparsingPrivate)
+
+- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep;
+
+- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone;
+- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone;
+
+@end
+
+@implementation ISO8601DateFormatter
+
+- (id) init {
+ if ((self = [super init])) {
+ format = ISO8601DateFormatCalendar;
+ timeSeparator = ISO8601DefaultTimeSeparatorCharacter;
+ includeTime = NO;
+ parsesStrictly = NO;
+ }
+ return self;
+}
+- (void) dealloc {
+ [defaultTimeZone release];
+ [super dealloc];
+}
+
+@synthesize defaultTimeZone;
+
+//The following properties are only here because GCC doesn't like @synthesize in category implementations.
+
+#pragma mark Parsing
+
+@synthesize parsesStrictly;
+
+static unsigned read_segment(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits);
+static unsigned read_segment_4digits(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits);
+static unsigned read_segment_2digits(const unsigned char *str, const unsigned char **next);
+static double read_double(const unsigned char *str, const unsigned char **next);
+static BOOL is_leap_year(unsigned year);
+
+/*Valid ISO 8601 date formats:
+ *
+ *YYYYMMDD
+ *YYYY-MM-DD
+ *YYYY-MM
+ *YYYY
+ *YY //century
+ * //Implied century: YY is 00-99
+ * YYMMDD
+ * YY-MM-DD
+ * -YYMM
+ * -YY-MM
+ * -YY
+ * //Implied year
+ * --MMDD
+ * --MM-DD
+ * --MM
+ * //Implied year and month
+ * ---DD
+ * //Ordinal dates: DDD is the number of the day in the year (1-366)
+ *YYYYDDD
+ *YYYY-DDD
+ * YYDDD
+ * YY-DDD
+ * -DDD
+ * //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week
+ *yyyyWwwd
+ *yyyy-Www-d
+ *yyyyWww
+ *yyyy-Www
+ *yyWwwd
+ *yy-Www-d
+ *yyWww
+ *yy-Www
+ * //Year of the implied decade
+ *-yWwwd
+ *-y-Www-d
+ *-yWww
+ *-y-Www
+ * //Week and day of implied year
+ * -Wwwd
+ * -Www-d
+ * //Week only of implied year
+ * -Www
+ * //Day only of implied week
+ * -W-d
+ */
+
+- (NSDateComponents *) dateComponentsFromString:(NSString *)string {
+ return [self dateComponentsFromString:string timeZone:NULL];
+}
+- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone {
+ return [self dateComponentsFromString:string timeZone:outTimeZone range:NULL];
+}
+- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange {
+ NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+ calendar.firstWeekday = 2; //Monday
+ NSDate *now = [NSDate date];
+
+ NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease];
+ NSDateComponents *nowComponents = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now];
+
+ unsigned
+ //Date
+ year,
+ month_or_week,
+ day,
+ //Time
+ hour = 0U;
+ NSTimeInterval
+ minute = 0.0,
+ second = 0.0;
+ //Time zone
+ signed tz_hour = 0;
+ signed tz_minute = 0;
+
+ enum {
+ monthAndDate,
+ week,
+ dateOnly
+ } dateSpecification = monthAndDate;
+
+ BOOL strict = self.parsesStrictly;
+ unichar timeSep = self.timeSeparator;
+
+ if (strict) timeSep = ISO8601DefaultTimeSeparatorCharacter;
+ NSAssert(timeSep != '\0', @"Time separator must not be NUL.");
+
+ BOOL isValidDate = ([string length] > 0U);
+ NSTimeZone *timeZone = nil;
+
+ const unsigned char *ch = (const unsigned char *)[string UTF8String];
+
+ NSRange range = { 0U, 0U };
+ const unsigned char *start_of_date;
+ if (strict && isspace(*ch)) {
+ range.location = NSNotFound;
+ isValidDate = NO;
+ } else {
+ //Skip leading whitespace.
+ unsigned i = 0U;
+ for(unsigned len = strlen((const char *)ch); i < len; ++i) {
+ if (!isspace(ch[i]))
+ break;
+ }
+
+ range.location = i;
+ ch += i;
+ start_of_date = ch;
+
+ unsigned segment;
+ unsigned num_leading_hyphens = 0U, num_digits = 0U;
+
+ if (*ch == 'T') {
+ //There is no date here, only a time. Set the date to now; then we'll parse the time.
+ isValidDate = isdigit(*++ch);
+
+ year = nowComponents.year;
+ month_or_week = nowComponents.month;
+ day = nowComponents.day;
+ } else {
+ while(*ch == '-') {
+ ++num_leading_hyphens;
+ ++ch;
+ }
+
+ segment = read_segment(ch, &ch, &num_digits);
+ switch(num_digits) {
+ case 0:
+ if (*ch == 'W') {
+ if ((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) {
+ year = nowComponents.year;
+ month_or_week = 1U;
+ ch += 2;
+ goto parseDayAfterWeek;
+ } else if (num_leading_hyphens == 1U) {
+ year = nowComponents.year;
+ goto parseWeekAndDay;
+ } else
+ isValidDate = NO;
+ } else
+ isValidDate = NO;
+ break;
+
+ case 8: //YYYY MM DD
+ if (num_leading_hyphens > 0U)
+ isValidDate = NO;
+ else {
+ day = segment % 100U;
+ segment /= 100U;
+ month_or_week = segment % 100U;
+ year = segment / 100U;
+ }
+ break;
+
+ case 6: //YYMMDD (implicit century)
+ if (num_leading_hyphens > 0U)
+ isValidDate = NO;
+ else {
+ day = segment % 100U;
+ segment /= 100U;
+ month_or_week = segment % 100U;
+ year = nowComponents.year;
+ year -= (year % 100U);
+ year += segment / 100U;
+ }
+ break;
+
+ case 4:
+ switch(num_leading_hyphens) {
+ case 0: //YYYY
+ year = segment;
+
+ if (*ch == '-') ++ch;
+
+ if (!isdigit(*ch)) {
+ if (*ch == 'W')
+ goto parseWeekAndDay;
+ else
+ month_or_week = day = 1U;
+ } else {
+ segment = read_segment(ch, &ch, &num_digits);
+ switch(num_digits) {
+ case 4: //MMDD
+ day = segment % 100U;
+ month_or_week = segment / 100U;
+ break;
+
+ case 2: //MM
+ month_or_week = segment;
+
+ if (*ch == '-') ++ch;
+ if (!isdigit(*ch))
+ day = 1U;
+ else
+ day = read_segment(ch, &ch, NULL);
+ break;
+
+ case 3: //DDD
+ day = segment % 1000U;
+ dateSpecification = dateOnly;
+ if (strict && (day > (365U + is_leap_year(year))))
+ isValidDate = NO;
+ break;
+
+ default:
+ isValidDate = NO;
+ }
+ }
+ break;
+
+ case 1: //YYMM
+ month_or_week = segment % 100U;
+ year = segment / 100U;
+
+ if (*ch == '-') ++ch;
+ if (!isdigit(*ch))
+ day = 1U;
+ else
+ day = read_segment(ch, &ch, NULL);
+
+ break;
+
+ case 2: //MMDD
+ day = segment % 100U;
+ month_or_week = segment / 100U;
+ year = nowComponents.year;
+
+ break;
+
+ default:
+ isValidDate = NO;
+ } //switch(num_leading_hyphens) (4 digits)
+ break;
+
+ case 1:
+ if (strict) {
+ //Two digits only - never just one.
+ if (num_leading_hyphens == 1U) {
+ if (*ch == '-') ++ch;
+ if (*++ch == 'W') {
+ year = nowComponents.year;
+ year -= (year % 10U);
+ year += segment;
+ goto parseWeekAndDay;
+ } else
+ isValidDate = NO;
+ } else
+ isValidDate = NO;
+ break;
+ }
+ case 2:
+ switch(num_leading_hyphens) {
+ case 0:
+ if (*ch == '-') {
+ //Implicit century
+ year = nowComponents.year;
+ year -= (year % 100U);
+ year += segment;
+
+ if (*++ch == 'W')
+ goto parseWeekAndDay;
+ else if (!isdigit(*ch)) {
+ goto centuryOnly;
+ } else {
+ //Get month and/or date.
+ segment = read_segment_4digits(ch, &ch, &num_digits);
+ NSLog(@"(%@) parsing month; segment is %u and ch is %s", string, segment, ch);
+ switch(num_digits) {
+ case 4: //YY-MMDD
+ day = segment % 100U;
+ month_or_week = segment / 100U;
+ break;
+
+ case 1: //YY-M; YY-M-DD (extension)
+ if (strict) {
+ isValidDate = NO;
+ break;
+ }
+ case 2: //YY-MM; YY-MM-DD
+ month_or_week = segment;
+ if (*ch == '-') {
+ if (isdigit(*++ch))
+ day = read_segment_2digits(ch, &ch);
+ else
+ day = 1U;
+ } else
+ day = 1U;
+ break;
+
+ case 3: //Ordinal date.
+ day = segment;
+ dateSpecification = dateOnly;
+ break;
+ }
+ }
+ } else if (*ch == 'W') {
+ year = nowComponents.year;
+ year -= (year % 100U);
+ year += segment;
+
+ parseWeekAndDay: //*ch should be 'W' here.
+ if (!isdigit(*++ch)) {
+ //Not really a week-based date; just a year followed by '-W'.
+ if (strict)
+ isValidDate = NO;
+ else
+ month_or_week = day = 1U;
+ } else {
+ month_or_week = read_segment_2digits(ch, &ch);
+ if (*ch == '-') ++ch;
+ parseDayAfterWeek:
+ day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U;
+ dateSpecification = week;
+ }
+ } else {
+ //Century only. Assume current year.
+ centuryOnly:
+ year = segment * 100U + nowComponents.year % 100U;
+ month_or_week = day = 1U;
+ }
+ break;
+
+ case 1:; //-YY; -YY-MM (implicit century)
+ NSLog(@"(%@) found %u digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %u", string, num_digits, segment);
+ unsigned current_year = nowComponents.year;
+ unsigned century = (current_year % 100U);
+ year = segment + (current_year - century);
+ if (num_digits == 1U) //implied decade
+ year += century - (current_year % 10U);
+
+ if (*ch == '-') {
+ ++ch;
+ month_or_week = read_segment_2digits(ch, &ch);
+ NSLog(@"(%@) month is %u", string, month_or_week);
+ }
+
+ day = 1U;
+ break;
+
+ case 2: //--MM; --MM-DD
+ year = nowComponents.year;
+ month_or_week = segment;
+ if (*ch == '-') {
+ ++ch;
+ day = read_segment_2digits(ch, &ch);
+ }
+ break;
+
+ case 3: //---DD
+ year = nowComponents.year;
+ month_or_week = nowComponents.month;
+ day = segment;
+ break;
+
+ default:
+ isValidDate = NO;
+ } //switch(num_leading_hyphens) (2 digits)
+ break;
+
+ case 7: //YYYY DDD (ordinal date)
+ if (num_leading_hyphens > 0U)
+ isValidDate = NO;
+ else {
+ day = segment % 1000U;
+ year = segment / 1000U;
+ dateSpecification = dateOnly;
+ if (strict && (day > (365U + is_leap_year(year))))
+ isValidDate = NO;
+ }
+ break;
+
+ case 3: //--DDD (ordinal date, implicit year)
+ //Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen.
+ if ((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict))
+ isValidDate = NO;
+ else {
+ day = segment;
+ year = nowComponents.year;
+ dateSpecification = dateOnly;
+ if (strict && (day > (365U + is_leap_year(year))))
+ isValidDate = NO;
+ }
+ break;
+
+ default:
+ isValidDate = NO;
+ }
+ }
+
+ if (isValidDate) {
+ if (isspace(*ch) || (*ch == 'T')) ++ch;
+
+ if (isdigit(*ch)) {
+ hour = read_segment_2digits(ch, &ch);
+ if (*ch == timeSep) {
+ ++ch;
+ if ((timeSep == ',') || (timeSep == '.')) {
+ //We can't do fractional minutes when '.' is the segment separator.
+ //Only allow whole minutes and whole seconds.
+ minute = read_segment_2digits(ch, &ch);
+ if (*ch == timeSep) {
+ ++ch;
+ second = read_segment_2digits(ch, &ch);
+ }
+ } else {
+ //Allow a fractional minute.
+ //If we don't get a fraction, look for a seconds segment.
+ //Otherwise, the fraction of a minute is the seconds.
+ minute = read_double(ch, &ch);
+ second = modf(minute, &minute);
+ if (second > DBL_EPSILON)
+ second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30).
+ else if (*ch == timeSep) {
+ ++ch;
+ second = read_double(ch, &ch);
+ }
+ }
+ }
+
+ switch(*ch) {
+ case 'Z':
+ timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
+ break;
+
+ case '+':
+ case '-':;
+ BOOL negative = (*ch == '-');
+ if (isdigit(*++ch)) {
+ //Read hour offset.
+ segment = *ch - '0';
+ if (isdigit(*++ch)) {
+ segment *= 10U;
+ segment += *(ch++) - '0';
+ }
+ tz_hour = (signed)segment;
+ if (negative) tz_hour = -tz_hour;
+
+ //Optional separator.
+ if (*ch == timeSep) ++ch;
+
+ if (isdigit(*ch)) {
+ //Read minute offset.
+ segment = *ch - '0';
+ if (isdigit(*++ch)) {
+ segment *= 10U;
+ segment += *ch - '0';
+ }
+ tz_minute = segment;
+ if (negative) tz_minute = -tz_minute;
+ }
+
+ timeZone = [NSTimeZone timeZoneForSecondsFromGMT:(tz_hour * 3600) + (tz_minute * 60)];
+ }
+ }
+ }
+ }
+
+ if (isValidDate) {
+ components.year = year;
+ components.day = day;
+ components.hour = hour;
+ components.minute = minute;
+ components.second = second;
+
+ switch(dateSpecification) {
+ case monthAndDate:
+ components.month = month_or_week;
+ break;
+
+ case week:;
+ //Adapted from <http://personal.ecu.edu/mccartyr/ISOwdALG.txt>.
+ //This works by converting the week date into an ordinal date, then letting the next case handle it.
+ unsigned prevYear = year - 1U;
+ unsigned YY = prevYear % 100U;
+ unsigned C = prevYear - YY;
+ unsigned G = YY + YY / 4U;
+ unsigned isLeapYear = (((C / 100U) % 4U) * 5U);
+ unsigned Jan1Weekday = (isLeapYear + G) % 7U;
+ enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ };
+ components.day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2));
+
+ case dateOnly: //An "ordinal date".
+ break;
+ }
+ }
+ } //if (!(strict && isdigit(ch[0])))
+
+ if (outRange) {
+ if (isValidDate)
+ range.length = ch - start_of_date;
+ else
+ range.location = NSNotFound;
+
+ *outRange = range;
+ }
+ if (outTimeZone) {
+ *outTimeZone = timeZone;
+ }
+
+ return components;
+}
+
+- (NSDate *) dateFromString:(NSString *)string {
+ return [self dateFromString:string timeZone:NULL];
+}
+- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone {
+ return [self dateFromString:string timeZone:outTimeZone range:NULL];
+}
+- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange {
+ NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+ calendar.firstWeekday = 2; //Monday
+
+ NSTimeZone *timeZone = nil;
+ NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange];
+ if (outTimeZone)
+ *outTimeZone = timeZone;
+ calendar.timeZone = timeZone;
+
+ return [calendar dateFromComponents:components];
+}
+
+- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error {
+ NSDate *date = [self dateFromString:string];
+ if (outValue)
+ *outValue = date;
+ return (date != nil);
+}
+
+#pragma mark Unparsing
+
+@synthesize format;
+@synthesize includeTime;
+@synthesize timeSeparator;
+@synthesize timeZoneSeparator;
+
+- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep {
+ if (timeSep != ':') {
+ NSMutableString *timeFormatMutable = [[timeFormat mutableCopy] autorelease];
+ [timeFormatMutable replaceOccurrencesOfString:@":"
+ withString:[NSString stringWithCharacters:&timeSep length:1U]
+ options:NSBackwardsSearch | NSLiteralSearch
+ range:(NSRange){ 0UL, [timeFormat length] }];
+ timeFormat = timeFormatMutable;
+ }
+ return timeFormat;
+}
+
+- (NSString *) stringFromDate:(NSDate *)date {
+ NSTimeZone *timeZone = self.defaultTimeZone;
+ if (!timeZone) timeZone = [NSTimeZone defaultTimeZone];
+ return [self stringFromDate:date timeZone:timeZone];
+}
+
+- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone {
+ switch (self.format) {
+ case ISO8601DateFormatCalendar:
+ return [self stringFromDate:date formatString:ISO_CALENDAR_DATE_FORMAT timeZone:timeZone];
+ case ISO8601DateFormatWeek:
+ return [self weekDateStringForDate:date timeZone:timeZone];
+ case ISO8601DateFormatOrdinal:
+ return [self stringFromDate:date formatString:ISO_ORDINAL_DATE_FORMAT timeZone:timeZone];
+ default:
+ [NSException raise:NSInternalInconsistencyException format:@"self.format was %d, not calendar (%d), week (%d), or ordinal (%d)", self.format, ISO8601DateFormatCalendar, ISO8601DateFormatWeek, ISO8601DateFormatOrdinal];
+ return nil;
+ }
+}
+
+- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone {
+ if (includeTime)
+ dateFormat = [dateFormat stringByAppendingFormat:@"'T'%@", [self replaceColonsInString:ISO_TIME_FORMAT withTimeSeparator:self.timeSeparator]];
+
+ NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+ calendar.firstWeekday = 2; //Monday
+
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ formatter.formatterBehavior = NSDateFormatterBehavior10_4;
+ formatter.dateFormat = dateFormat;
+ formatter.calendar = calendar;
+
+ NSString *str = [formatter stringForObjectValue:date];
+
+ [formatter release];
+
+ if (includeTime) {
+ int offset = [timeZone secondsFromGMT];
+ offset /= 60; //bring down to minutes
+ if (offset == 0) {
+ str = [str stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT];
+ } else {
+ NSString *separator = timeZoneSeparator ? [NSString stringWithFormat:@"%c", timeZoneSeparator] : @"";
+ str = [str stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT, offset / 60, separator, offset % 60];
+ }
+ }
+ return str;
+}
+
+- (NSString *) stringForObjectValue:(id)value {
+ NSParameterAssert([value isKindOfClass:[NSDate class]]);
+
+ return [self stringFromDate:(NSDate *)value];
+}
+
+/*Adapted from:
+ * Algorithm for Converting Gregorian Dates to ISO 8601 Week Date
+ * Rick McCarty, 1999
+ * http://personal.ecu.edu/mccartyr/ISOwdALG.txt
+ */
+- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone {
+ NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+ calendar.timeZone = timeZone;
+ NSDateComponents *components = [calendar components:NSYearCalendarUnit | NSWeekdayCalendarUnit | NSDayCalendarUnit fromDate:date];
+
+ //Determine the ordinal date.
+ NSDateComponents *startOfYearComponents = [calendar components:NSYearCalendarUnit fromDate:date];
+ startOfYearComponents.month = 1;
+ startOfYearComponents.day = 1;
+ NSDateComponents *ordinalComponents = [calendar components:NSDayCalendarUnit fromDate:[calendar dateFromComponents:startOfYearComponents] toDate:date options:0];
+ ordinalComponents.day += 1;
+
+ enum {
+ monday, tuesday, wednesday, thursday, friday, saturday, sunday
+ };
+ enum {
+ january = 1, february, march,
+ april, may, june,
+ july, august, september,
+ october, november, december
+ };
+
+ int year = components.year;
+ int week = 0;
+ //The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5.
+ int dayOfWeek = (components.weekday + 5) % 7;
+ int dayOfYear = ordinalComponents.day;
+
+ int prevYear = year - 1;
+
+ BOOL yearIsLeapYear = is_leap_year(year);
+ BOOL prevYearIsLeapYear = is_leap_year(prevYear);
+
+ int YY = prevYear % 100;
+ int C = prevYear - YY;
+ int G = YY + YY / 4;
+ int Jan1Weekday = (((((C / 100) % 4) * 5) + G) % 7);
+
+ int weekday = ((dayOfYear + Jan1Weekday) - 1) % 7;
+
+ if((dayOfYear <= (7 - Jan1Weekday)) && (Jan1Weekday > thursday)) {
+ week = 52 + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear));
+ --year;
+ } else {
+ int lengthOfYear = 365 + yearIsLeapYear;
+ if((lengthOfYear - dayOfYear) < (thursday - weekday)) {
+ ++year;
+ week = 1;
+ } else {
+ int J = dayOfYear + (sunday - weekday) + Jan1Weekday;
+ week = J / 7 - (Jan1Weekday > thursday);
+ }
+ }
+
+ NSString *timeString;
+ if(includeTime) {
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ unichar timeSep = self.timeSeparator;
+ if (!timeSep) timeSep = ISO8601DefaultTimeSeparatorCharacter;
+ formatter.dateFormat = [self replaceColonsInString:ISO_TIME_WITH_TIMEZONE_FORMAT withTimeSeparator:timeSep];
+
+ timeString = [formatter stringForObjectValue:self];
+
+ [formatter release];
+ } else
+ timeString = @"";
+
+ return [NSString stringWithFormat:@"%u-W%02u-%02u%@", (unsigned)year, (unsigned)week, ((unsigned)dayOfWeek) + 1U, timeString];
+}
+
+@end
+
+static unsigned read_segment(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
+ unsigned num_digits = 0U;
+ unsigned value = 0U;
+
+ while(isdigit(*str)) {
+ value *= 10U;
+ value += *str - '0';
+ ++num_digits;
+ ++str;
+ }
+
+ if (next) *next = str;
+ if (out_num_digits) *out_num_digits = num_digits;
+
+ return value;
+}
+static unsigned read_segment_4digits(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
+ unsigned num_digits = 0U;
+ unsigned value = 0U;
+
+ if (isdigit(*str)) {
+ value += *(str++) - '0';
+ ++num_digits;
+ }
+
+ if (isdigit(*str)) {
+ value *= 10U;
+ value += *(str++) - '0';
+ ++num_digits;
+ }
+
+ if (isdigit(*str)) {
+ value *= 10U;
+ value += *(str++) - '0';
+ ++num_digits;
+ }
+
+ if (isdigit(*str)) {
+ value *= 10U;
+ value += *(str++) - '0';
+ ++num_digits;
+ }
+
+ if (next) *next = str;
+ if (out_num_digits) *out_num_digits = num_digits;
+
+ return value;
+}
+static unsigned read_segment_2digits(const unsigned char *str, const unsigned char **next) {
+ unsigned value = 0U;
+
+ if (isdigit(*str))
+ value += *str - '0';
+
+ if (isdigit(*++str)) {
+ value *= 10U;
+ value += *(str++) - '0';
+ }
+
+ if (next) *next = str;
+
+ return value;
+}
+
+//strtod doesn't support ',' as a separator. This does.
+static double read_double(const unsigned char *str, const unsigned char **next) {
+ double value = 0.0;
+
+ if (str) {
+ unsigned int_value = 0;
+
+ while(isdigit(*str)) {
+ int_value *= 10U;
+ int_value += (*(str++) - '0');
+ }
+ value = int_value;
+
+ if (((*str == ',') || (*str == '.'))) {
+ ++str;
+
+ register double multiplier, multiplier_multiplier;
+ multiplier = multiplier_multiplier = 0.1;
+
+ while(isdigit(*str)) {
+ value += (*(str++) - '0') * multiplier;
+ multiplier *= multiplier_multiplier;
+ }
+ }
+ }
+
+ if (next) *next = str;
+
+ return value;
+}
+
+static BOOL is_leap_year(unsigned year) {
+ return \
+ ((year % 4U) == 0U)
+ && (((year % 100U) != 0U)
+ || ((year % 400U) == 0U));
+}
7 Classes/Categories/NSData+Base64.h
@@ -0,0 +1,7 @@
+#import <Foundation/Foundation.h>
+
+char *NewBase64Encode(const void *inputBuffer, size_t length, bool separateLines, size_t *outputLength);
+
+@interface NSData (Base64)
+- (NSString *)base64EncodedString;
+@end
86 Classes/Categories/NSData+Base64.m
@@ -0,0 +1,86 @@
+#import "NSData+Base64.h"
+
+static unsigned char base64EncodeLookup[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+#define xx 65
+
+#define BINARY_UNIT_SIZE 3
+#define BASE64_UNIT_SIZE 4
+
+char *NewBase64Encode(const void *buffer, size_t length, bool separateLines, size_t *outputLength) {
+ const unsigned char *inputBuffer = (const unsigned char *)buffer;
+
+#define MAX_NUM_PADDING_CHARS 2
+#define OUTPUT_LINE_LENGTH 64
+#define INPUT_LINE_LENGTH ((OUTPUT_LINE_LENGTH / BASE64_UNIT_SIZE) * BINARY_UNIT_SIZE)
+#define CR_LF_SIZE 2
+
+ size_t outputBufferSize = ((length / BINARY_UNIT_SIZE) + ((length % BINARY_UNIT_SIZE) ? 1 : 0)) * BASE64_UNIT_SIZE;
+ if (separateLines) {
+ outputBufferSize += (outputBufferSize / OUTPUT_LINE_LENGTH) * CR_LF_SIZE;
+ }
+
+ outputBufferSize += 1;
+
+ char *outputBuffer = (char *)malloc(outputBufferSize);
+ if (!outputBuffer) {
+ return NULL;
+ }
+
+ size_t i = 0;
+ size_t j = 0;
+ const size_t lineLength = separateLines ? INPUT_LINE_LENGTH : length;
+ size_t lineEnd = lineLength;
+
+ while (true) {
+ if (lineEnd > length) {
+ lineEnd = length;
+ }
+
+ for (; i + BINARY_UNIT_SIZE - 1 < lineEnd; i += BINARY_UNIT_SIZE) {
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
+ outputBuffer[j++] = base64EncodeLookup[((inputBuffer[i] & 0x03) << 4) | ((inputBuffer[i + 1] & 0xF0) >> 4)];
+ outputBuffer[j++] = base64EncodeLookup[((inputBuffer[i + 1] & 0x0F) << 2) | ((inputBuffer[i + 2] & 0xC0) >> 6)];
+ outputBuffer[j++] = base64EncodeLookup[inputBuffer[i + 2] & 0x3F];
+ }
+
+ if (lineEnd == length) {
+ break;
+ }
+
+ outputBuffer[j++] = '\r';
+ outputBuffer[j++] = '\n';
+ lineEnd += lineLength;
+ }
+
+ if (i + 1 < length) {
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
+ outputBuffer[j++] = base64EncodeLookup[((inputBuffer[i] & 0x03) << 4)
+ | ((inputBuffer[i + 1] & 0xF0) >> 4)];
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i + 1] & 0x0F) << 2];
+ outputBuffer[j++] = '=';
+ } else if (i < length) {
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0xFC) >> 2];
+ outputBuffer[j++] = base64EncodeLookup[(inputBuffer[i] & 0x03) << 4];
+ outputBuffer[j++] = '=';
+ outputBuffer[j++] = '=';
+ }
+ outputBuffer[j] = 0;
+
+ if (outputLength) {
+ *outputLength = j;
+ }
+ return outputBuffer;
+}
+
+@implementation NSData (Base64)
+
+- (NSString *)base64EncodedString {
+ size_t outputLength;
+ char *outputBuffer = NewBase64Encode([self bytes], [self length], true, &outputLength);
+ NSString *result = [[[NSString alloc] initWithBytes:outputBuffer length:outputLength encoding:NSASCIIStringEncoding] autorelease];
+ free(outputBuffer);
+ return result;
+}
+
+@end
16 Classes/Controllers/AuthenticationViewController.h
@@ -0,0 +1,16 @@
+//
+// OAuthViewController.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/30.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+
+@interface AuthenticationViewController : UIViewController <UIWebViewDelegate> {
+ IBOutlet UIWebView * webView;
+}
+
+@end
34 Classes/Controllers/AuthenticationViewController.m
@@ -0,0 +1,34 @@
+//
+// OAuthViewController.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/30.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "AuthenticationViewController.h"
+
+@implementation AuthenticationViewController
+
+
+#pragma mark -
+#pragma mark View Lifecycle
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ NSString * OAuthURLString = [kGowallaOAuthURL stringByAppendingFormat:@"?redirect_uri=%@&client_id=%@", kGowallaRedirectURI, kGowallaAPIKey];
+ NSURL * OAuthURL = [NSURL URLWithString:OAuthURLString];
+ NSURLRequest * OAuthURLRequest = [[[NSURLRequest alloc] initWithURL:OAuthURL] autorelease];
+
+ [webView loadRequest:OAuthURLRequest];
+}
+
+#pragma mark -
+#pragma mark UIWebViewDelegate
+
+- (void)webViewDidStartLoad:(UIWebView *)wv {
+ NSLog(@"webViewDidStartLoad: %@", [wv.request URL]);
+}
+
+@end
25 Classes/Controllers/CheckInSuccessViewController.h
@@ -0,0 +1,25 @@
+//
+// CheckInSuccessViewController.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/30.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class CheckIn;
+
+@interface CheckInSuccessViewController : UIViewController {
+ CheckIn * checkIn;
+ NSString * detailHTML;
+
+ IBOutlet UIWebView * webView;
+}
+
+@property (nonatomic, retain) CheckIn * checkIn;
+@property (nonatomic, retain) NSString * detailHTML;
+
+- (id)initWithCheckIn:(CheckIn *)someCheckIn detailHTML:(NSString *)html;
+
+@end
54 Classes/Controllers/CheckInSuccessViewController.m
@@ -0,0 +1,54 @@
+//
+// CheckInSuccessViewController.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/30.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "CheckInSuccessViewController.h"
+
+#import "CheckIn.h"
+
+
+@implementation CheckInSuccessViewController
+
+@synthesize checkIn;
+@synthesize detailHTML;
+
+- (id)initWithCheckIn:(CheckIn *)someCheckIn detailHTML:(NSString *)html {
+ if (self = [super initWithNibName:@"CheckInSuccessView" bundle:nil]) {
+ self.checkIn = someCheckIn;
+ self.detailHTML = html;
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [checkIn release];
+ [detailHTML release];
+ [super dealloc];
+}
+
+#pragma mark -
+#pragma mark View Lifecycle
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ self.navigationItem.hidesBackButton = YES;
+ self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
+ target:nil
+ action:@selector(dismissModalViewControllerAnimated:)] autorelease];
+
+ [webView loadHTMLString:self.detailHTML baseURL:[NSURL URLWithString:@"http://api.gowalla.com/"]];
+}
+
+- (void)viewDidUnload {
+ [super viewDidUnload];
+ webView.delegate = nil;
+ webView = nil;
+}
+
+@end
28 Classes/Controllers/PassportViewController.h
@@ -0,0 +1,28 @@
+//
+// PassportViewController.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/07/01.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class User;
+@class EGOImageView;
+
+@interface PassportViewController : UITableViewController {
+ User * user;
+ NSArray * checkIns;
+
+ IBOutlet UILabel * nameLabel;
+ IBOutlet UILabel * hometownLabel;
+ IBOutlet EGOImageView * imageView;
+}
+
+@property (nonatomic, retain) User * user;
+@property (nonatomic, retain) NSArray * checkIns;
+
+- (id)initWithGowallaUser:(User *)someUser;
+
+@end
167 Classes/Controllers/PassportViewController.m
@@ -0,0 +1,167 @@
+//
+// PassportViewController.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/07/01.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "PassportViewController.h"
+#import "SpotViewController.h"
+
+#import "User.h"
+#import "CheckIn.h"
+#import "Spot.h"
+
+#import "EGOHTTPRequest.h"
+
+#import "AFImageLoadingCell.h"
+#import "EGOImageView.h"
+
+#import "GowallaAPI.h"
+
+@interface PassportViewController ()
+- (void)updateContent;
+@end
+
+static NSDateFormatter * _dateFormatter;
+
+@implementation PassportViewController
+
+@synthesize user;
+@synthesize checkIns;
+
+- (id)initWithGowallaUser:(User *)someUser {
+ if (self = [super initWithNibName:@"PassportView" bundle:nil]) {
+ self.user = someUser;
+ }
+
+ return self;
+}
+
+#pragma mark -
+#pragma mark View Lifecycle
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ self.title = self.user.name;
+
+ self.tableView.rowHeight = 60.0f;
+
+ [self updateContent];
+
+ [[GowallaAPI requestForPath:[self.user.url path]
+ parameters:nil
+ delegate:self
+ selector:@selector(userRequestDidFinish:)] startAsynchronous];
+
+ [[GowallaAPI requestForPath:[[self.user.url path] stringByAppendingPathComponent:@"events"]
+ parameters:nil
+ delegate:self
+ selector:@selector(checkInsRequestDidFinish:)] startAsynchronous];
+}
+
+- (void)viewDidUnload {
+ [super viewDidUnload];
+
+ nameLabel = nil;
+ hometownLabel = nil;
+ imageView = nil;
+}
+
+- (void)updateContent {
+ nameLabel.text = self.user.name;
+ hometownLabel.text = self.user.hometown;
+ [imageView setImageURL:self.user.imageURL];
+}
+
+#pragma mark -
+#pragma mark EGOHTTPRequest
+
+- (void)userRequestDidFinish:(EGOHTTPRequest *)request {
+ NSLog(@"requestDidFinish: %@", request.responseString);
+ NSDictionary * response = [NSDictionary dictionaryWithJSONData:request.responseData
+ error:nil];
+
+ if (request.responseStatusCode == 200) {
+ self.user = [[User alloc] initWithDictionary:response];
+ self.checkIns = self.user.checkIns;
+ [self updateContent];
+ [self.tableView reloadData];
+ }
+}
+
+- (void)checkInsRequestDidFinish:(EGOHTTPRequest *)request {
+ NSLog(@"requestDidFinish: %@", request.responseString);
+ NSDictionary * response = [NSDictionary dictionaryWithJSONData:request.responseData
+ error:nil];
+
+ if (request.responseStatusCode == 200) {
+ NSMutableArray * mutableCheckins = [NSArray array];
+ for (NSDictionary * dictionary in [response valueForKey:@"activity"]) {
+ CheckIn * checkIn = [[CheckIn alloc] initWithDictionary:dictionary];
+ [mutableCheckins addObject:checkIn];
+ [checkIn release];
+ }
+
+ self.checkIns = [NSArray arrayWithArray:mutableCheckins];
+ [self.tableView reloadData];
+ }
+}
+
+
+#pragma mark -
+#pragma mark UITableViewDataSource
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+ return [self.checkIns count];
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
+ if ([self.checkIns count] > 0) {
+ return NSLocalizedString(@"Recent Check-Ins", nil);
+ }
+
+ return nil;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+
+ static NSString * CellIdentifier = @"Cell";
+
+ AFImageLoadingCell * cell = (AFImageLoadingCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
+ if (cell == nil) {
+ cell = [[[AFImageLoadingCell alloc] initWithStyle:UITableViewCellStyleSubtitle
+ reuseIdentifier:CellIdentifier] autorelease];
+ }
+
+ if (_dateFormatter == nil) {
+ _dateFormatter = [[NSDateFormatter alloc] init];
+ [_dateFormatter setDoesRelativeDateFormatting:YES];
+ [_dateFormatter setTimeStyle:NSDateFormatterShortStyle];
+ [_dateFormatter setDateStyle:NSDateFormatterMediumStyle];
+ [_dateFormatter setLocale:[NSLocale currentLocale]];
+ }
+
+ CheckIn * checkIn = [self.checkIns objectAtIndex:indexPath.row];
+
+ [cell setImageURL:checkIn.spot.imageURL];
+ cell.textLabel.text = checkIn.spot.name;
+ cell.detailTextLabel.text = [_dateFormatter stringFromDate:checkIn.timestamp];
+
+ return cell;
+}
+
+#pragma mark -
+#pragma mark UITableViewDelegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+ CheckIn * checkIn = [self.checkIns objectAtIndex:indexPath.row];
+ SpotViewController * viewController = [[[SpotViewController alloc] initWithSpot:checkIn.spot] autorelease];
+ [self.navigationController pushViewController:viewController animated:YES];
+}
+
+
+@end
+
38 Classes/Controllers/SpotViewController.h
@@ -0,0 +1,38 @@
+//
+// SpotViewController.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <CoreLocation/CoreLocation.h>
+#import <MapKit/MapKit.h>
+#import <QuartzCore/QuartzCore.h>
+
+@class Spot;
+@class EGOImageView;
+
+@interface SpotViewController : UITableViewController <CLLocationManagerDelegate> {
+ Spot * spot;
+ NSArray * checkIns;
+
+ CLLocationManager * locationManager;
+
+ IBOutlet UILabel * nameLabel;
+ IBOutlet EGOImageView * imageView;
+ IBOutlet UIButton * checkInButton;
+ IBOutlet MKMapView * mapView;
+}
+
+@property (nonatomic, retain) Spot * spot;
+@property (nonatomic, retain) NSArray * checkIns;
+
+@property (nonatomic, retain) CLLocationManager * locationManager;
+
+- (id)initWithSpot:(Spot *)someSpot;
+
+- (IBAction)checkIn:(id)sender;
+
+@end
245 Classes/Controllers/SpotViewController.m
@@ -0,0 +1,245 @@
+//
+// SpotViewController.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "SpotViewController.h"
+#import "CheckInSuccessViewController.h"
+#import "PassportViewController.h"
+
+#import "Spot.h"
+#import "User.h"
+#import "CheckIn.h"
+
+#import "EGOHTTPRequest.h"
+#import "EGOHTTPFormRequest.h"
+
+#import "AFImageLoadingCell.h"
+
+#import "CLLocationManager+AFExtensions.h"
+#import "NSData+Base64.h"
+#import "GowallaAPI.h"
+
+@interface SpotViewController ()
+- (void)updateContent;
+- (void)handleCheckInState;
+@end
+
+static NSDateFormatter * _dateFormatter;
+
+static EGOHTTPRequest * spotRequest;
+static EGOHTTPFormRequest * checkInRequest;
+
+@implementation SpotViewController
+
+@synthesize spot;
+@synthesize checkIns;
+@synthesize locationManager;
+
+- (id)initWithSpot:(Spot *)someSpot {
+ if (self = [super initWithNibName:@"SpotView" bundle:nil]) {
+ self.spot = someSpot;
+
+ self.locationManager = [[CLLocationManager alloc] init];
+ self.locationManager.delegate = self;
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
+ self.locationManager.distanceFilter = 80.0;
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ self.locationManager.delegate = nil;
+
+ [spot release];
+ [locationManager release];
+ [super dealloc];
+}
+
+- (void)handleCheckInState {
+ if ([Spot canCheckInAtSpot:self.spot fromLocation:self.locationManager.location]) {
+ [checkInButton setTitle:NSLocalizedString(@"Check In", nil)
+ forState:UIControlStateNormal];
+ checkInButton.enabled = YES;
+ } else {
+ [checkInButton setTitle:[self.locationManager distanceAndDirectionTo:self.spot.location]
+ forState:UIControlStateDisabled];
+ checkInButton.enabled = NO;
+ }
+}
+
+#pragma mark -
+#pragma mark View Lifecycle
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ self.title = self.spot.name;
+
+ self.tableView.rowHeight = 60.0f;
+
+ mapView.layer.cornerRadius = 8.0f;
+ mapView.layer.borderWidth = 1.0f;
+ mapView.layer.borderColor = [[UIColor lightGrayColor] CGColor];
+
+ [self updateContent];
+
+ spotRequest = [GowallaAPI requestForPath:[self.spot.url path]
+ parameters:nil
+ delegate:self
+ selector:@selector(requestDidFinish:)];
+
+ [spotRequest startAsynchronous];
+
+ [self.locationManager startUpdatingLocation];
+}
+
+- (void)viewDidUnload {
+ [super viewDidUnload];
+ [self.locationManager stopUpdatingLocation];
+}
+
+- (void)updateContent {
+ nameLabel.text = self.spot.name;
+ [imageView setImageURL:self.spot.imageURL];
+
+ checkInButton.titleLabel.numberOfLines = 2;
+ checkInButton.titleLabel.adjustsFontSizeToFitWidth = YES;
+
+ [mapView addAnnotation:self.spot];
+ [mapView setRegion:MKCoordinateRegionMakeWithDistance(self.spot.coordinate, 1000, 1000) animated:YES];
+}
+
+#pragma mark -
+#pragma mark IBAction
+
+- (IBAction)checkIn:(id)sender {
+ [checkInButton setEnabled:NO];
+
+ CLLocation * currentLocation = self.locationManager.location;
+ CLLocationAccuracy accuracy = currentLocation.horizontalAccuracy;
+ CLLocationDegrees latitude = currentLocation.coordinate.latitude;
+ CLLocationDegrees longitude = currentLocation.coordinate.longitude;
+
+ NSString * URLString = [@"http://api.gowalla.com/checkins.json" stringByAppendingFormat:@"?oauth_token=%@", [[NSUserDefaults standardUserDefaults] objectForKey:@"gowalla_basic_oauth_access_token"]];
+
+ checkInRequest = [[EGOHTTPFormRequest alloc] initWithURL:[NSURL URLWithString:URLString]
+ delegate:self];
+ [checkInRequest setDidFailSelector:@selector(requestDidFinish:)];
+
+ [checkInRequest setPostValue:self.spot.identifier
+ forKey:@"spot_id"];
+ [checkInRequest setPostValue:[[NSNumber numberWithDouble:latitude] stringValue]
+ forKey:@"lat"];
+ [checkInRequest setPostValue:[[NSNumber numberWithDouble:longitude] stringValue]
+ forKey:@"lng"];
+ [checkInRequest setPostValue:[[NSNumber numberWithDouble:accuracy] stringValue]
+ forKey:@"accuracy"];
+
+
+ [checkInRequest startSynchronous];
+}
+
+#pragma mark -
+#pragma mark EGOHTTPRequest
+
+- (void)requestDidFinish:(EGOHTTPRequest *)request {
+ NSLog(@"requestDidFinish: %@", request.responseString);
+ NSDictionary * response = [NSDictionary dictionaryWithJSONData:request.responseData
+ error:nil];
+
+ if (request.responseStatusCode == 200) {
+ if ([request isEqual:spotRequest]) {
+ self.spot = [[Spot alloc] initWithDictionary:response];
+ self.checkIns = self.spot.checkIns;
+
+ [self.tableView reloadData];
+ } else if ([request isEqual:checkInRequest]) {
+ CheckIn * checkIn = [[CheckIn alloc] initWithDictionary:response];
+ NSString * html = [response valueForKey:@"detail_html"];
+
+ CheckInSuccessViewController * viewController = [[[CheckInSuccessViewController alloc] initWithCheckIn:checkIn
+ detailHTML:html] autorelease];
+ UINavigationController * modalNavigationController = [[[UINavigationController alloc] initWithRootViewController:viewController] autorelease];
+
+ [self.navigationController presentModalViewController:modalNavigationController animated:YES];
+ }
+ } else {
+ [[[[UIAlertView alloc] initWithTitle:[response valueForKey:@"title"]
+ message:[response valueForKey:@"message"]
+ delegate:nil
+ cancelButtonTitle:NSLocalizedString(@"OK", nil)
+ otherButtonTitles:nil] autorelease] show];
+ }
+
+ [self handleCheckInState];
+}
+
+#pragma mark -
+#pragma mark CLLocationManagerDelegate
+
+- (void)locationManager:(CLLocationManager *)manager
+ didUpdateToLocation:(CLLocation *)newLocation
+ fromLocation:(CLLocation *)oldLocation
+{
+ NSLog(@"locationManager:didUpdateToLocation:fromLocation:");
+ [self handleCheckInState];
+}
+
+#pragma mark -
+#pragma mark UITableViewDataSource
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+ return [self.checkIns count];
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
+ if ([self.checkIns count] > 0) {
+ return NSLocalizedString(@"Recent Check-Ins", nil);
+ }
+
+ return nil;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+
+ static NSString * CellIdentifier = @"Cell";
+
+ AFImageLoadingCell * cell = (AFImageLoadingCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
+ if (cell == nil) {
+ cell = [[[AFImageLoadingCell alloc] initWithStyle:UITableViewCellStyleSubtitle
+ reuseIdentifier:CellIdentifier] autorelease];
+ }
+
+ if (_dateFormatter == nil) {
+ _dateFormatter = [[NSDateFormatter alloc] init];
+ [_dateFormatter setDoesRelativeDateFormatting:YES];
+ [_dateFormatter setTimeStyle:NSDateFormatterShortStyle];
+ [_dateFormatter setDateStyle:NSDateFormatterMediumStyle];
+ [_dateFormatter setLocale:[NSLocale currentLocale]];
+ }
+
+ CheckIn * checkIn = [self.checkIns objectAtIndex:indexPath.row];
+
+ [cell setImageURL:checkIn.user.imageURL];
+ cell.textLabel.text = checkIn.user.name;
+ cell.detailTextLabel.text = [_dateFormatter stringFromDate:checkIn.timestamp];
+
+ return cell;
+}
+
+#pragma mark -
+#pragma mark UITableViewDelegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+ CheckIn * checkIn = [self.checkIns objectAtIndex:indexPath.row];
+ PassportViewController * viewController = [[[PassportViewController alloc] initWithGowallaUser:checkIn.user] autorelease];
+ [self.navigationController pushViewController:viewController animated:YES];
+}
+
+@end
+
21 Classes/Controllers/SpotsViewController.h
@@ -0,0 +1,21 @@
+//
+// SpotsViewController.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <CoreLocation/CoreLocation.h>
+
+
+@interface SpotsViewController : UITableViewController <CLLocationManagerDelegate> {
+ NSArray * spots;
+ CLLocationManager * locationManager;
+}
+
+@property (nonatomic, retain) NSArray * spots;
+@property (nonatomic, retain) CLLocationManager * locationManager;
+
+@end
175 Classes/Controllers/SpotsViewController.m
@@ -0,0 +1,175 @@
+//
+// SpotsViewController.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "SpotsViewController.h"
+#import "SpotViewController.h"
+
+#import "Spot.h"
+
+#import "GowallaAPI.h"
+
+#import "AFImageLoadingCell.h"
+
+#import "CLLocationManager+AFExtensions.h"
+
+
+@implementation SpotsViewController
+
+@synthesize spots;
+@synthesize locationManager;
+
+#pragma mark -
+#pragma mark Initialization
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ if (self = [super initWithCoder:aDecoder]) {
+ self.spots = [NSArray array];
+
+ self.locationManager = [[CLLocationManager alloc] init];
+ self.locationManager.delegate = self;
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
+ self.locationManager.distanceFilter = 80.0;
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ self.locationManager.delegate = nil;
+
+ [spots release];
+ [locationManager release];
+ [super dealloc];
+}
+
+
+#pragma mark -
+#pragma mark View Lifecycle
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ self.title = NSLocalizedString(@"Spots", nil);
+
+ self.tableView.rowHeight = 60.0f;
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ [self.locationManager startUpdatingLocation];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+ [super viewWillDisappear:animated];
+ [self.locationManager stopUpdatingLocation];
+}
+
+- (void)viewDidUnload {
+ [super viewDidUnload];
+
+ [EGOHTTPRequest cancelRequestsForDelegate:self];
+ [self.locationManager stopUpdatingLocation];
+}
+
+#pragma mark -
+#pragma mark EGOHTTPRequest
+
+- (void)requestDidFinish:(EGOHTTPRequest *)request {
+ NSLog(@"requestDidFinish:");
+
+ if (request.responseStatusCode == 200) {
+ NSDictionary * response = [NSDictionary dictionaryWithJSONData:request.responseData
+ error:nil];
+ NSMutableSet * someSpots = [NSMutableSet set];
+ for (NSDictionary * dictionary in [response valueForKey:@"spots"]) {
+ Spot * spot = [[Spot alloc] initWithDictionary:dictionary];
+ [someSpots addObject:spot];
+ [spot release];
+ }
+
+ self.spots = [[someSpots allObjects] sortedArrayUsingComparator:^(id a, id b) {
+ CLLocation * currentLocation = self.locationManager.location;
+ CLLocationDistance distanceToA = [currentLocation distanceFromLocation:[a location]];
+ CLLocationDistance distanceToB = [currentLocation distanceFromLocation:[b location]];
+ if (distanceToA < distanceToB) {
+ return NSOrderedAscending;
+ } else if (distanceToA > distanceToB) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+
+ [self.tableView reloadData];
+ } else {
+ NSLog(@"requestDidFail: %@", request.error);
+ }
+}
+
+#pragma mark -
+#pragma mark CLLocationManagerDelegate
+
+- (void)locationManager:(CLLocationManager *)manager
+ didUpdateToLocation:(CLLocation *)newLocation
+ fromLocation:(CLLocation *)oldLocation
+{
+ NSLog(@"locationManager:didUpdateToLocation:fromLocation:");
+
+ CLLocationDegrees latitude = newLocation.coordinate.latitude;
+ CLLocationDegrees longitude = newLocation.coordinate.longitude;
+
+ NSMutableDictionary * parameters = [NSMutableDictionary dictionary];
+ [parameters setValue:[NSString stringWithFormat:@"%+.9f", latitude]
+ forKey:@"lat"];
+ [parameters setValue:[NSString stringWithFormat:@"%+.9f", longitude]
+ forKey:@"lng"];
+
+ [[GowallaAPI requestForPath:@"spots"
+ parameters:parameters
+ delegate:self
+ selector:@selector(requestDidFinish:)] startAsynchronous];
+}
+
+#pragma mark -
+#pragma mark UITableViewDataSource
+
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+ return [self.spots count];
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+
+ static NSString *CellIdentifier = @"Cell";
+
+ AFImageLoadingCell *cell = (AFImageLoadingCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
+ if (cell == nil) {
+ cell = [[[AFImageLoadingCell alloc] initWithStyle:UITableViewCellStyleSubtitle
+ reuseIdentifier:CellIdentifier] autorelease];
+ }
+
+ Spot * spot = [self.spots objectAtIndex:indexPath.row];
+
+ [cell setImageURL:spot.imageURL];
+ cell.textLabel.text = spot.name;
+ cell.detailTextLabel.text = [self.locationManager distanceAndDirectionTo:spot.location];
+ cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+
+ return cell;
+}
+
+#pragma mark -
+#pragma mark UITableViewDelegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+ SpotViewController * viewController = [[[SpotViewController alloc] initWithSpot:[self.spots objectAtIndex:indexPath.row]] autorelease];
+ [self.navigationController pushViewController:viewController animated:YES];
+}
+
+@end
+
25 Classes/Gowalla_BasicAppDelegate.h
@@ -0,0 +1,25 @@
+//
+// Gowalla_BasicAppDelegate.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright Mattt Thompson 2010. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class AuthenticationViewController;
+
+@interface Gowalla_BasicAppDelegate : NSObject <UIApplicationDelegate> {
+
+ UIWindow * window;
+ UINavigationController * navigationController;
+ AuthenticationViewController * authenticationViewController;
+}
+
+@property (nonatomic, retain) IBOutlet UIWindow * window;
+@property (nonatomic, retain) IBOutlet UINavigationController * navigationController;
+@property (nonatomic, retain) AuthenticationViewController * authenticationViewController;
+
+@end
+
101 Classes/Gowalla_BasicAppDelegate.m
@@ -0,0 +1,101 @@
+//
+// Gowalla_BasicAppDelegate.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright Mattt Thompson 2010. All rights reserved.
+//
+
+#import "Gowalla_BasicAppDelegate.h"
+
+#import "AuthenticationViewController.h"
+
+#import "EGOHTTPFormRequest.h"
+
+@implementation Gowalla_BasicAppDelegate
+
+@synthesize window;
+@synthesize navigationController;
+@synthesize authenticationViewController;
+
+#pragma mark -
+#pragma mark Application Lifecycle
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ [window addSubview:navigationController.view];
+ [window makeKeyAndVisible];
+
+ NSString * OAuthToken = [[NSUserDefaults standardUserDefaults] objectForKey:kGowallaBasicOAuthAccessTokenPreferenceKey];
+ NSDate * OAuthTokenExpirationDate = [[NSUserDefaults standardUserDefaults] objectForKey:kGowallaBasicOAuthTokenExpirationPreferenceKey];
+ if (OAuthToken == nil || [OAuthTokenExpirationDate compare:[NSDate date]] == NSOrderedAscending) {
+ authenticationViewController = [[[AuthenticationViewController alloc] initWithNibName:@"AuthenticationView" bundle:nil] autorelease];
+ [navigationController presentModalViewController:authenticationViewController
+ animated:YES];
+ }
+
+ return YES;
+}
+
+#pragma mark -
+#pragma mark OAuth
+
+- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
+ NSLog(@"application:handleOpenURL: %@", url);
+ NSString * URLString = [url absoluteString];
+ NSString * codeKey = @"code=";
+ NSRange codeRange = [URLString rangeOfString:codeKey];
+ NSString * code = [URLString substringFromIndex:codeRange.location + [codeKey length]];
+
+
+ NSURL * OAuthTokenURL = [NSURL URLWithString:@"https://api.gowalla.com/api/oauth/token"];
+ EGOHTTPFormRequest * request = [[[EGOHTTPFormRequest alloc] initWithURL:OAuthTokenURL delegate:self] autorelease];
+ [request setPostValue:@"authorization_code"
+ forKey:@"grant_type"];
+ [request setPostValue:kGowallaAPIKey
+ forKey:@"client_id"];
+ [request setPostValue:kGowallaAPISecret
+ forKey:@"client_secret"];
+ [request setPostValue:code
+ forKey:@"code"];
+ [request setPostValue:kGowallaRedirectURI
+ forKey:@"redirect_uri"];
+
+ [request setDidFinishSelector:@selector(applicationDidAuthorizeWithRequest:)];
+ [request startSynchronous];
+
+ return YES;
+}
+
+- (void)applicationDidAuthorizeWithRequest:(EGOHTTPFormRequest *)request {
+ NSLog(@"applicationDidAuthorizeWithRequest: %@", request.responseString);
+ NSDictionary * response = [NSDictionary dictionaryWithJSONData:[request responseData] error:nil];
+
+ [[NSUserDefaults standardUserDefaults] setObject:[response valueForKey:@"access_token"]
+ forKey:kGowallaBasicOAuthAccessTokenPreferenceKey];
+ [[NSUserDefaults standardUserDefaults] setObject:[response valueForKey:@"refresh_token"]
+ forKey:kGowallaBasicOAuthRefreshTokenPreferenceKey];
+
+ NSDate * expirationDate = [NSDate dateWithTimeIntervalSinceNow:[[response valueForKey:@"expires_in"] doubleValue]];
+ [[NSUserDefaults standardUserDefaults] setObject:expirationDate
+ forKey:kGowallaBasicOAuthTokenExpirationPreferenceKey];
+ [[NSUserDefaults standardUserDefaults] synchronize];
+
+ [authenticationViewController dismissModalViewControllerAnimated:YES];
+}
+
+#pragma mark -
+#pragma mark Memory management
+
+- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
+
+}
+
+- (void)dealloc {
+ [navigationController release];
+ [window release];
+ [authenticationViewController release];
+ [super dealloc];
+}
+
+@end
+
23 Classes/Helpers/GowallaAPI.h
@@ -0,0 +1,23 @@
+//
+// GowallaAPI.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/30.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+#import "EGOHTTPRequest.h"
+#import "EGOHTTPFormRequest.h"
+
+#define kGowallaAPIBaseURL @"http://api.gowalla.com/"
+
+@interface GowallaAPI : NSObject
+
++ (EGOHTTPRequest *)requestForPath:(NSString *)path
+ parameters:(NSDictionary *)keyedParameters
+ delegate:(id)delegate
+ selector:(SEL)selector;
+
+@end
60 Classes/Helpers/GowallaAPI.m
@@ -0,0 +1,60 @@
+//
+// GowallaAPI.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/30.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "GowallaAPI.h"
+
+@implementation GowallaAPI
+
++ (EGOHTTPRequest *)requestForPath:(NSString *)path
+ parameters:(NSDictionary *)keyedParameters
+ delegate:(id)delegate
+ selector:(SEL)selector
+{
+ NSMutableDictionary * mutableKeyedParameters = [NSMutableDictionary dictionaryWithDictionary:keyedParameters];
+
+ NSString * OAuthToken = [[NSUserDefaults standardUserDefaults] objectForKey:kGowallaBasicOAuthAccessTokenPreferenceKey];
+ if (OAuthToken) {
+ [mutableKeyedParameters setObject:OAuthToken
+ forKey:@"oauth_token"];
+ }
+
+ NSMutableArray * parameters = [NSMutableArray array];
+ for (id key in [mutableKeyedParameters allKeys]) {
+ NSString * parameter = [NSString stringWithFormat:@"%@=%@", key, [mutableKeyedParameters valueForKey:key]];
+ [parameters addObject:parameter];
+ }
+
+ NSURL * url = [NSURL URLWithString:[path stringByAppendingFormat:@"?%@", [parameters componentsJoinedByString:@"&"]]
+ relativeToURL:[NSURL URLWithString:kGowallaAPIBaseURL]];
+
+
+ EGOHTTPRequest * request = [[EGOHTTPRequest alloc] initWithURL:url];
+ [request setDelegate:delegate];
+ [request setDidFinishSelector:selector];
+
+ // Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ [request addRequestHeader:@"Accept"
+ value:@"application/json"];
+
+ // Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ NSString *preferredLanguageCodes = [[NSLocale preferredLanguages] componentsJoinedByString:@", "];
+ [request addRequestHeader:@"Accept-Language"
+ value:[NSString stringWithFormat:@"%@, en-us;q=0.8", preferredLanguageCodes]];
+
+ // Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ [request addRequestHeader:@"Content-Type"
+ value:@"application/json"];
+
+ // X-Gowalla-API-Key HTTP Header; see http://api.gowalla.com/api/docs
+ [request addRequestHeader:@"X-Gowalla-API-Key"
+ value:kGowallaAPIKey];
+
+ return [request autorelease];
+}
+
+@end
21 Classes/Models/AFObject.h
@@ -0,0 +1,21 @@
+//
+// AFObject.h
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+
+@interface AFObject : NSObject {
+ NSURL * url;
+}
+
+@property (nonatomic, retain) NSURL * url;
+@property (readonly) NSString * identifier;
+
+- (id)initWithDictionary:(NSDictionary *)dictionary;
+
+@end
41 Classes/Models/AFObject.m
@@ -0,0 +1,41 @@
+//
+// AFObject.m
+// Gowalla-Basic
+//
+// Created by Mattt Thompson on 10/06/29.
+// Copyright 2010 Mattt Thompson. All rights reserved.
+//
+
+#import "AFObject.h"
+
+@implementation AFObject
+
+@synthesize url;
+
+- (id)initWithDictionary:(NSDictionary *)dictionary {
+ if (self = [super init]) {
+ self.url = [NSURL URLWithString:[dictionary valueForKey:@"url"]
+ relativeToURL:[NSURL URLWithString:@"http://api.gowalla.com"]];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [url release];
+ [super dealloc];
+}
+