├── .gitignore ├── CHANGELOG.markdown ├── LICENSE ├── README.markdown ├── SGAPI.podspec ├── SGAPI.xcworkspace └── contents.xcworkspacedata ├── SGAPI ├── Categories │ ├── NSDate+ISO8601.h │ └── NSDate+ISO8601.m ├── ItemSets │ ├── SGItemSet.h │ ├── SGItemSet.m │ ├── SGKEventSet.h │ ├── SGKEventSet.m │ ├── SGPerformerSet.h │ ├── SGPerformerSet.m │ ├── SGVenueSet.h │ └── SGVenueSet.m ├── Items │ ├── SGItem.h │ ├── SGItem.m │ ├── SGKEvent.h │ ├── SGKEvent.m │ ├── SGPerformer.h │ ├── SGPerformer.m │ ├── SGVenue.h │ └── SGVenue.m ├── SGAPI.h ├── SGDataManager.h ├── SGDataManager.m ├── SGQuery.h └── SGQuery.m └── TestsApp ├── Podfile ├── TestsApp.xcodeproj └── project.pbxproj ├── TestsApp ├── AppDelegate.h ├── AppDelegate.m ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Info.plist └── main.m └── TestsAppTests ├── Info.plist ├── SGKEventSetTests.m ├── SGPerformerSetTests.m └── SGQueryTests.m /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | TestsApp/Pods 3 | Podfile.lock 4 | xcuserdata 5 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | ## 1.3.1 2 | 3 | - Updated SGHTTPRequest dependency 4 | 5 | ## 1.3.0 6 | 7 | - Added support for setting client secret 8 | - Improved caching system 9 | - Improved compatibility with App Extensions 10 | - Reworked query parameter and filter building to use NSURLQueryItem, bumped min deployment target to iOS 8 11 | - Add ability to clear location on queries 12 | - Added a way to get the total number of items in an SGItemSet 13 | - Added some nullability hints for swift 14 | 15 | ## 1.2.0 16 | 17 | - Added support for WatchOS 2 18 | - Removed SGImageCache Dependency 19 | - Added swift nullable hits to some methods 20 | - Added ability to cache SGItemSet results to disk 21 | - Miscellaneous bug fixes 22 | 23 | ## 1.1.1 24 | 25 | - Fixed a bug that could happen with null data 26 | - Added from and to date parameters for SGQuery to fetch results with date ranges (where supported) 27 | - Added some keywords to make SGItemSet play nicer with swift 1.2 28 | 29 | ## 1.1.0 30 | 31 | - Stricter type checking/casting of response values 32 | - Added `requestHeaders` dictionary to SGQuery 33 | - Misc minor bug fixes and performance improvements 34 | 35 | ## 1.0.3 36 | 37 | Leave console logging up to SGHTTPRequest 38 | 39 | ## 1.0.2 40 | 41 | Response dict null cleansing fix 42 | 43 | ## 1.0.1 44 | 45 | Added aid and rid properties to SGQuery 46 | 47 | ## 1.0.0 48 | 49 | Initial release 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, SeatGeek, Inc 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## SGAPI 2 | 3 | SGAPI is an iOS SDK for querying the [SeatGeek Platform API](http://platform.seatgeek.com), 4 | a comprehensive directory of live events in the United States and Canada. 5 | 6 | ### CocoaPods Setup 7 | 8 | ``` 9 | pod 'SGAPI' 10 | ``` 11 | 12 | ### Example Usage 13 | 14 | `SGAPI` provides model classes `SGKEvent`, `SGPerformer`, `SGVenue`, and item set 15 | classes `SGKEventSet`, `SGPerformerSet`, `SGVenueSet` for paginated fetching. 16 | 17 | ```objc 18 | #import 19 | ``` 20 | 21 | ### Fetching Events 22 | 23 | Create `SGKEventSet` instances to fetch paginated `SGKEvent` results. See the 24 | [SeatGeek Platform docs](http://platform.seatgeek.com/#events) for available query parameters. 25 | 26 | ```objc 27 | // find all 'new york mets' events 28 | SGKEventSet *events = SGKEventSet.eventsSet; 29 | events.query.search = @"new york mets"; 30 | events.query.perPage = 30; 31 | ``` 32 | 33 | The `onPageLoaded` block property is called on successful page load. The `onPageLoadFailed` 34 | block property is called when a request fails. 35 | 36 | ```objc 37 | events.onPageLoaded = ^(NSOrderedSet *results) { 38 | for (SGKEvent *event in results) { 39 | NSLog(@"event: %@", event.title); 40 | } 41 | }; 42 | 43 | events.onPageLoadFailed = ^(NSError *error) { 44 | NSLog(@"error: %@", error); 45 | }; 46 | ``` 47 | 48 | ``` 49 | [events fetchNextPage]; 50 | ``` 51 | 52 | ### Fetching Performers 53 | 54 | Create `SGPerformerSet` instances to fetch paginated `SGPerformer` results. See the 55 | [SeatGeek Platform docs](http://platform.seatgeek.com/#performers) for available query 56 | parameters. 57 | 58 | ```objc 59 | // find all performers matching 'imagine dragons' 60 | SGPerformerSet *performers = SGPerformerSet.performersSet; 61 | performers.query.search = @"imagine dragons"; 62 | ``` 63 | 64 | ```objc 65 | performers.onPageLoaded = ^(NSOrderedSet *results) { 66 | if (results.count) { 67 | SGPerformer *performer = results[0]; 68 | NSLog(@"performer: %@", performer.name); 69 | } 70 | }; 71 | ``` 72 | 73 | ``` 74 | [performers fetchNextPage]; 75 | ``` 76 | 77 | ### Fetching Venues 78 | 79 | Create `SGVenueSet` instances to fetch paginated `SGVenue` objects. See the 80 | [SeatGeek Platform docs](http://platform.seatgeek.com/#venues) for available query parameters. 81 | 82 | ```objc 83 | // find all venues matching 'new york' 84 | SGVenueSet *venues = SGVenueSet.venuesSet; 85 | venues.query.search = @"new york"; 86 | ``` 87 | 88 | ```objc 89 | venues.onPageLoaded = ^(NSOrderedSet *results) { 90 | for (SGVenue *venue in results) { 91 | NSLog(@"venue: %@", venue.name); 92 | } 93 | }; 94 | ``` 95 | 96 | ``` 97 | [performers fetchNextPage]; 98 | ``` 99 | 100 | ### Familiar Item Set Properties 101 | 102 | Item sets (`SGKEventSet`, `SGPerformerSet`, `SGVenueSet`) support subscripting and common set 103 | properties. 104 | 105 | ```objc 106 | // count, firstObject, and lastObject 107 | if (events.count) { 108 | NSLog(@"first event: %@", [events.firstObject title]); 109 | NSLog(@"last event: %@", [events.lastObject title]); 110 | } 111 | 112 | // subscripting 113 | if (events.count >= 3) { 114 | NSLog(@"third event: %@", [events[2] title]); 115 | } 116 | 117 | // iterate over an NSArray of SGKEvent in the set 118 | for (SGKEvent *event in events.array) { 119 | NSLog(@"event: %@", event.title); 120 | } 121 | 122 | // iterate over an NSOrderedSet of SGKEvent in the set 123 | for (SGKEvent *event in events.orderedSet) { 124 | NSLog(@"event: %@", event.title); 125 | } 126 | ``` 127 | 128 | ### SGQuery 129 | 130 | You can modify the `query` of each set to change default values and filters. 131 | 132 | ```objc 133 | events.query.perPage = 100; 134 | events.query.location = CLLocationCoordinate2DMake(40.752, -73.972) // New York City 135 | events.query.range = @"200mi" // 200 mile search range 136 | ``` 137 | 138 | If you would rather use your own network fetching code, you can construct standalone `SGQuery` 139 | instances for URL construction. 140 | 141 | ```objc 142 | SGQuery *query = SGQuery.eventsQuery; 143 | [query addFilter:@"taxonomies.name" value:@"sports"]; 144 | query.search = @"new york"; 145 | 146 | NSLog(@"%@", query.URL); 147 | // http://api.seatgeek.com/2/events?q=new+york&taxonomies.name=sports 148 | ``` 149 | -------------------------------------------------------------------------------- /SGAPI.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SGAPI" 3 | s.version = "1.5.0" 4 | s.summary = "An iOS SDK for querying the SeatGeek Platform web service." 5 | s.homepage = "http://platform.seatgeek.com" 6 | s.license = { :type => "BSD", :file => "LICENSE" } 7 | s.author = "SeatGeek" 8 | s.ios.deployment_target = '9.0' 9 | s.source = { :git => "https://github.com/seatgeek/SGAPI.git", :tag => "1.5.0" } 10 | s.source_files = "SGAPI/**/*.{h,m}" 11 | s.requires_arc = true 12 | s.dependency "SGHTTPRequest/Core", '>= 1.9' 13 | end 14 | -------------------------------------------------------------------------------- /SGAPI.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 19 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 38 | 40 | 41 | 43 | 44 | 46 | 47 | 49 | 50 | 52 | 53 | 55 | 56 | 58 | 59 | 61 | 62 | 63 | 66 | 68 | 69 | 71 | 72 | 74 | 75 | 77 | 78 | 80 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 100 | 101 | 103 | 104 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /SGAPI/Categories/NSDate+ISO8601.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+NSDate_ISO8601.h 3 | // Pods 4 | // 5 | // Created by James Van-As on 17/04/15. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface NSDate (ISO8601) 12 | - (NSString *)ISO8601; 13 | @end 14 | -------------------------------------------------------------------------------- /SGAPI/Categories/NSDate+ISO8601.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+NSDate_ISO8601.m 3 | // Pods 4 | // 5 | // Created by James Van-As on 17/04/15. 6 | // 7 | // 8 | 9 | #import "NSDate+ISO8601.h" 10 | 11 | @implementation NSDate (ISO8601) 12 | 13 | - (NSString *)ISO8601 { 14 | static NSDateFormatter *dateFormatter; 15 | static dispatch_once_t onceToken; 16 | dispatch_once(&onceToken, ^{ 17 | dateFormatter = NSDateFormatter.new; 18 | dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 19 | dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZZZ"; 20 | }); 21 | return [dateFormatter stringFromDate:self]; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGItemSet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 18/01/14. 3 | // 4 | 5 | #import 6 | #import 7 | 8 | #define SGItemSetFetchStarted @"SGItemSetFetchStarted" 9 | #define SGItemSetFetchSucceeded @"SGItemSetFetchSucceeded" 10 | #define SGItemSetFetchFailed @"SGItemSetFetchFailed" 11 | 12 | @class SGQuery, SGDataManager; 13 | 14 | /** 15 | * `SGItemSet` is the abstract base class for item sets, providing the core 16 | * request handling and pagination. Create instances of `SGKEventSet`, 17 | * `SGPerformerSet`, and `SGVenueSet` to fetch paginated results. 18 | */ 19 | 20 | @interface SGItemSet : SGItem 21 | 22 | #pragma mark - Subclasses implement / override 23 | 24 | /** Subclasses must implement this */ 25 | - (nonnull id)itemForDict:(nonnull NSDictionary *)dict; 26 | 27 | /** 28 | * Subclasses can implement this method to pull other data out of the server 29 | * response and set their properties. 30 | */ 31 | - (void)doAdditionalProcessingWithServerResponseDict:(nonnull NSDictionary *)dict; 32 | 33 | #pragma mark - Modifying the API query 34 | 35 | /** @name Modifying the API query */ 36 | 37 | /** 38 | An instance for defining the parameters and filters of the API query. 39 | 40 | SGKEventSet *events = SGKEventSet.eventsQuery; 41 | events.query.search = @"new york mets"; 42 | events.query.perPage = 30; 43 | */ 44 | @property (nullable, nonatomic, strong) SGQuery *query; 45 | 46 | #pragma mark - State change callbacks 47 | 48 | /** @name State change callbacks */ 49 | 50 | /** 51 | A block assigned to `onPageLoaded` will be called after each result page 52 | request has completed. 53 | 54 | events.onPageLoaded = ^(NSOrderedSet *results) { 55 | for (SGKEvent *event in results) { 56 | NSLog(@"event: %@", event.title); 57 | } 58 | }; 59 | */ 60 | @property (nullable, nonatomic, copy) void (^onPageLoaded)(NSOrderedSet* __nonnull newItems); 61 | 62 | /** 63 | A block assigned to `onPageLoadFailed` will be called after a page request 64 | failed to load. 65 | 66 | events.onPageLoadFailed = ^(NSError *error) { 67 | NSLog(@"error: %@", error); 68 | }; 69 | */ 70 | @property (nullable, nonatomic, copy) void (^onPageLoadFailed)(NSError* __nonnull error); 71 | 72 | /** 73 | A block assigned to `onPageLoadRetry` will be called after a page request 74 | failed to load, and then begins to fetch again due to network reachability 75 | 76 | events.onPageLoadRetry = ^ { 77 | [self showLoadingSpinner]; 78 | }; 79 | */ 80 | @property (nullable, nonatomic, copy) void (^onPageLoadRetry)(void); 81 | 82 | #pragma mark - Fetching results 83 | 84 | /** @name Fetching results */ 85 | 86 | /** 87 | * Fetch the next page of results. If the set is already 88 | * `fetchNextPage` will do nothing. If all results have already been fetched 89 | * () `fetchNextPage` will do nothing. 90 | */ 91 | - (void)fetchNextPage; 92 | 93 | /** 94 | * Fetch a specific page of results. Usually you will want to call 95 | * `fetchNextPage` instead, but in some cases you may want to refetch a specific 96 | * page. 97 | * 98 | * Note that item sets contain only unique items, so a page refetch will only 99 | * add any new results to the set, without duplicates. If the hasn't 100 | * been modified then most likely no new items will be added. 101 | */ 102 | - (void)fetchPage:(int)page; 103 | 104 | /** 105 | * Cancel an in progress results fetch. 106 | */ 107 | - (void)cancelFetch; 108 | 109 | /** 110 | * Returns YES if the last page of results has already been fetched. 111 | */ 112 | - (BOOL)lastPageAlreadyFetched; 113 | 114 | /** 115 | * Returns YES if a results fetch is in progress. 116 | */ 117 | - (BOOL)fetching; 118 | 119 | #pragma mark - Content inspection 120 | 121 | /** @name Content inspection */ 122 | 123 | /** 124 | * The date of last successful fetch 125 | */ 126 | - (nullable NSDate *)lastFetched; 127 | 128 | /** 129 | * Returns the page number of the last fetched page. 130 | */ 131 | - (int)lastFetchedPage; 132 | 133 | /** 134 | * Returns the total number of pages available for the . Note that if 135 | * no pages have been fetched yet, `totalPages` will return 0. 136 | */ 137 | - (int)totalPages; 138 | 139 | /** 140 | * Returns an `NSArray` of the items in the set. 141 | */ 142 | - (nullable NSArray *)array; 143 | 144 | /** 145 | * Returns an `NSOrderedSet` of the items in the set. 146 | */ 147 | - (nullable NSOrderedSet *)orderedSet; 148 | 149 | /** 150 | * Returns the first item in the set. 151 | */ 152 | - (nullable id)firstObject; 153 | 154 | /** 155 | * Returns the last item in the set. 156 | */ 157 | - (nullable id)lastObject; 158 | 159 | /** 160 | * Returns a count of the items in the set so far. Note that this is not the 161 | * total available for the query, but instead the number of items fetched so far. 162 | */ 163 | - (NSUInteger)count; 164 | 165 | /** 166 | * Returns a total number of the items in the set. Note that this is not the 167 | * number of items received so far, but instead the total number of items in all pages. 168 | */ 169 | - (NSUInteger)total; 170 | 171 | #pragma mark - Caching results 172 | 173 | /** @name Caching results */ 174 | 175 | /** 176 | * Set cacheKey to a unique value within the item set class's context and the item set 177 | * will immediately restore any matching previous results from cache, and will save 178 | * any future results to cache, for restoring between sessions. 179 | */ 180 | @property (nullable, nonatomic, strong) NSString *cacheKey; 181 | 182 | /** 183 | * Force the result items to be cached again. Requires `cacheKey` to be set. 184 | */ 185 | - (void)cacheContents; 186 | 187 | /** @name Resetting internal state */ 188 | 189 | /** 190 | * Reset the internal state of the set. Note that this doesn't reset the 191 | * parameters. After being reset, a subsequent will fetch the 192 | * first page of results. 193 | */ 194 | - (void)reset; 195 | 196 | #pragma mark - Status bar indicator 197 | 198 | /** @name Status bar indicator */ 199 | 200 | /** 201 | * Whether to show a status bar activity indicator while fetching results. 202 | * Default is YES. 203 | */ 204 | @property (nonatomic, assign) BOOL allowStatusBarSpinner; 205 | 206 | #pragma mark - Composite properties 207 | 208 | @property (nonatomic, weak, nullable) SGItemSet *parentSet; 209 | @property (nonatomic, weak, nullable) SGItem *parentItem; 210 | 211 | #pragma mark - Ignore plz 212 | 213 | @property (nonnull, nonatomic, copy) NSString *resultArrayKey; 214 | @property (nullable, nonatomic, strong, readonly) NSDictionary *meta; 215 | - (nullable id)objectAtIndexedSubscript:(NSUInteger)index; 216 | - (nullable NSDictionary *)lastResponseDict; 217 | @property (nullable, nonatomic, weak) SGDataManager *dataManager; 218 | - (void)setNeedsRefresh; 219 | - (BOOL)needsRefresh; 220 | 221 | @end 222 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGItemSet.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 18/01/14. 3 | // 4 | 5 | #import 6 | #import "SGItemSet.h" 7 | #import "SGHTTPRequest.h" 8 | #import "SGQuery.h" 9 | #import "SGItem.h" 10 | #import "SGDataManager.h" 11 | #import 12 | 13 | @interface SGItemSet () 14 | @property (nonatomic, strong) NSMutableOrderedSet *items; 15 | @property (nonatomic, strong) NSDictionary *meta; 16 | @property (nonatomic, assign) BOOL fetching; 17 | @property (nonatomic, assign) int lastFetchedPage; 18 | @property (nonatomic, strong) SGHTTPRequest *request; 19 | @property (nonatomic, strong) NSDictionary *lastResponseDict; 20 | @property (nonatomic, strong) NSDate *lastFetched; 21 | @property (nonatomic, assign) BOOL needsRefresh; 22 | @end 23 | 24 | @implementation SGItemSet 25 | // use the SGItem implementation of all these properties: 26 | @dynamic query; 27 | @dynamic parentSet; 28 | @dynamic parentItem; 29 | @dynamic dataManager; 30 | @dynamic lastFetched; 31 | 32 | - (id)init { 33 | self = [super init]; 34 | self.allowStatusBarSpinner = YES; 35 | self.needsRefresh = YES; 36 | return self; 37 | } 38 | 39 | #pragma mark - For subclasses 40 | 41 | // abstract. implemented in subclasses 42 | - (nonnull id)itemForDict:(nonnull NSDictionary *)dict { 43 | #ifdef DEBUG 44 | NSAssert(NO, @"Called the abstract itemForDict: on SGItemSet. Don't do that."); 45 | #endif 46 | return nil; 47 | } 48 | 49 | - (void)doAdditionalProcessingWithServerResponseDict:(nonnull NSDictionary *)dict { 50 | } 51 | 52 | #pragma mark - Fetching and Processing 53 | 54 | - (void)reset { 55 | self.items = nil; 56 | self.lastFetchedPage = 0; 57 | self.meta = nil; 58 | self.lastFetched = nil; 59 | [self cancelFetch]; 60 | } 61 | 62 | - (void)fetchNextPage { 63 | if (!self.lastPageAlreadyFetched) { 64 | [self fetchPage:self.lastFetchedPage + 1]; 65 | } 66 | } 67 | 68 | - (void)fetchPage:(int)page { 69 | if (self.fetching) { 70 | return; 71 | } 72 | self.fetching = YES; 73 | 74 | self.query.page = page; 75 | 76 | SGHTTPRequest *req = [self.query requestWithMethod:SGHTTPRequestMethodGet]; 77 | req.showActivityIndicator = self.allowStatusBarSpinner; 78 | 79 | if (SGQuery.consoleLogging) { 80 | req.logging = req.logging | (SGHTTPLogRequests | SGHTTPLogErrors); 81 | } 82 | 83 | __weakSelf me = self; 84 | req.onSuccess = ^(SGHTTPRequest *_req) { 85 | [me processResults:_req.responseData url:_req.url.absoluteString]; 86 | }; 87 | 88 | req.onFailure = ^(SGHTTPRequest *_req) { 89 | me.fetching = NO; 90 | if (me.onPageLoadFailed) { 91 | me.onPageLoadFailed(_req.error); 92 | } 93 | [me trigger:SGItemSetFetchFailed withContext:_req.error]; 94 | }; 95 | 96 | req.onNetworkReachable = ^{ 97 | if (me.onPageLoadRetry) { 98 | me.onPageLoadRetry(); 99 | } 100 | [me fetchNextPage]; 101 | }; 102 | 103 | [req start]; 104 | self.request = req; 105 | [self trigger:SGItemSetFetchStarted]; 106 | } 107 | 108 | - (void)cancelFetch { 109 | [self.request cancel]; 110 | self.fetching = NO; 111 | } 112 | 113 | - (void)processResults:(NSData *)data url:(NSString *)url { 114 | __weakSelf me = self; 115 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 116 | NSError *error = nil; 117 | NSDictionary *dict = [SGJSONSerialization JSONObjectWithData:data error:&error logURL:url]; 118 | me.lastResponseDict = dict; 119 | 120 | if (error) { 121 | dispatch_async(dispatch_get_main_queue(), ^{ 122 | if (me.onPageLoadFailed) { 123 | me.onPageLoadFailed(error); 124 | } 125 | [me trigger:SGItemSetFetchFailed withContext:error]; 126 | }); 127 | return; 128 | } 129 | 130 | NSArray *results = [dict valueForKeyPath:me.resultArrayKey]; 131 | if (![results isKindOfClass:NSArray.class]) { 132 | results = nil; 133 | } 134 | 135 | NSDictionary *metaDict = dict[@"meta"] ?: me.meta; 136 | if (!metaDict) { 137 | // assume this endpoint cannot be paginated. 138 | metaDict = @{@"per_page" : @(me.query.perPage ?: results.count), 139 | @"total" : @(results.count), 140 | @"page" : @(1)}.copy; 141 | } 142 | 143 | NSMutableOrderedSet *newItems = NSMutableOrderedSet.orderedSet; 144 | for (NSDictionary *itemDict in results) { 145 | SGItem *item = [me itemForDict:itemDict]; 146 | item.lastFetched = NSDate.date; 147 | item.parentSet = me; 148 | if (me.dataManager) { 149 | item.dataManager = me.dataManager; 150 | } 151 | if (item) { 152 | [newItems addObject:item]; 153 | } 154 | } 155 | 156 | dispatch_async(dispatch_get_main_queue(), ^{ 157 | me.meta = metaDict; 158 | [me doAdditionalProcessingWithServerResponseDict:dict]; 159 | NSMutableOrderedSet *reallyNewItems; 160 | if (me.items) { 161 | reallyNewItems = newItems.mutableCopy; 162 | [reallyNewItems minusOrderedSet:me.items]; 163 | [me.items unionOrderedSet:newItems]; 164 | } else { 165 | me.items = newItems; 166 | reallyNewItems = newItems; 167 | } 168 | me.fetching = NO; 169 | me.needsRefresh = NO; 170 | me.lastFetched = NSDate.date; 171 | [me cacheContents]; 172 | if (me.onPageLoaded) { 173 | me.onPageLoaded(reallyNewItems); 174 | } 175 | [me trigger:SGItemSetFetchSucceeded withContext:reallyNewItems]; 176 | }); 177 | }); 178 | } 179 | 180 | #pragma mark - Caching 181 | 182 | + (SGFileCache *)cache { 183 | static SGFileCache *cache; 184 | static dispatch_once_t onceToken; 185 | dispatch_once(&onceToken, ^{ 186 | cache = [SGFileCache cacheFor:NSStringFromClass(self)]; 187 | cache.maxDiskCacheSizeMB = 0; // unlimited cache size 188 | [cache clearExpiredFiles]; 189 | }); 190 | return cache; 191 | } 192 | 193 | - (SGFileCache *)cache { 194 | return self.class.cache; 195 | } 196 | 197 | - (void)setCacheKey:(NSString *)cacheKey { 198 | _cacheKey = cacheKey; 199 | [self loadCachedContents]; 200 | } 201 | 202 | - (NSString *)internalCacheKey { 203 | return self.cacheKey.length ? self.cacheKey : nil; 204 | } 205 | 206 | - (NSDate *)cacheExpiryDate { 207 | // expire after 1 month. If it hasn't been used by then just get a fresh copy 208 | return [NSDate.date dateByAddingTimeInterval:2592000]; 209 | } 210 | 211 | - (void)cacheContents { 212 | if (!self.internalCacheKey.length || !self.lastFetched) { 213 | return; 214 | } 215 | NSDictionary *cacheDict = @{@"items":self.items, @"lastFetched":self.lastFetched}; 216 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:cacheDict]; 217 | [self.cache cacheData:data for:self.internalCacheKey expiryDate:self.cacheExpiryDate]; 218 | } 219 | 220 | - (BOOL)haveCachedContents { 221 | return [self.cache hasCachedDataFor:self.internalCacheKey]; 222 | } 223 | 224 | - (BOOL)loadCachedContents { 225 | if (!self.haveCachedContents) { 226 | return NO; 227 | } 228 | NSData *data = [self.cache cachedDataFor:self.internalCacheKey]; 229 | NSDictionary *cachedContents = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 230 | if (![cachedContents isKindOfClass:NSDictionary.class]) { 231 | return NO; 232 | } 233 | self.items = [cachedContents[@"items"] mutableCopy]; 234 | self.lastFetched = cachedContents[@"lastFetched"]; 235 | for (SGItem *item in self.items) { 236 | item.parentSet = self; 237 | } 238 | return YES; 239 | } 240 | 241 | #pragma mark - Setters 242 | 243 | - (void)setMeta:(NSDictionary *)meta { 244 | _meta = meta; 245 | self.lastFetchedPage = [meta[@"page"] intValue]; 246 | } 247 | 248 | - (void)setNeedsRefresh { 249 | self.needsRefresh = YES; 250 | } 251 | 252 | #pragma mark - Getters 253 | 254 | - (BOOL)lastPageAlreadyFetched { 255 | if (self.items && self.items.count == 0) { 256 | return YES; 257 | } 258 | if (!self.meta) { 259 | return NO; 260 | } 261 | return self.lastFetchedPage == self.totalPages || !self.totalPages; 262 | } 263 | 264 | - (BOOL)fetching { 265 | return _fetching; 266 | } 267 | 268 | - (int)lastFetchedPage { 269 | return _lastFetchedPage; 270 | } 271 | 272 | - (int)totalPages { 273 | if (![self.meta[@"per_page"] intValue]) { 274 | return 0; 275 | } 276 | return (int)ceilf([self.meta[@"total"] floatValue] / [self.meta[@"per_page"] intValue]); 277 | } 278 | 279 | - (NSUInteger)total { 280 | return [self.meta[@"total"] unsignedIntegerValue]; 281 | } 282 | 283 | #pragma mark - Subscripting 284 | 285 | - (id)objectAtIndexedSubscript:(NSUInteger)index { 286 | return self.items[index]; 287 | } 288 | 289 | #pragma mark - Set Duckness 290 | 291 | - (NSArray *)array { 292 | return self.items.array; 293 | } 294 | 295 | - (NSOrderedSet *)orderedSet { 296 | return self.items; 297 | } 298 | 299 | - (id)firstObject { 300 | return self.items.firstObject; 301 | } 302 | 303 | - (id)lastObject { 304 | return self.items.lastObject; 305 | } 306 | 307 | - (NSUInteger)count { 308 | return self.items.count; 309 | } 310 | 311 | @end 312 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGKEventSet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 18/01/14. 3 | // 4 | 5 | #import "SGItemSet.h" 6 | #import "SGQuery.h" 7 | #import "SGKEvent.h" 8 | 9 | /** 10 | `SGKEventSet` provides paginated results of `SGKEvent` items by quering the 11 | [/events](http://platform.seatgeek.com/#events) and 12 | [/recommendations](http://platform.seatgeek.com/#recommendations) endpoints. `SGKEventSet` extends from , which provides the base result 13 | fetching and pagination interface. 14 | 15 | SGKEventSet *events = SGKEventSet.eventsSet; 16 | events.query.search = @"new york mets"; 17 | 18 | events.onPageLoaded = ^(NSOrderedSet *results) { 19 | for (SGKEvent *event in results) { 20 | NSLog(@"event: %@", event.title); 21 | } 22 | }; 23 | 24 | [events fetchNextPage]; 25 | */ 26 | 27 | @interface SGKEventSet : SGItemSet 28 | 29 | /** @name Creating a set */ 30 | 31 | /** 32 | Returns a new `SGKEventSet` instance for the 33 | [/events](http://platform.seatgeek.com/#events) endpoint. Modify the 34 | [query](-[SGItemSet query]) to add parameters and filters. 35 | 36 | SGKEventSet *events = SGKEventSet.eventsSet; 37 | events.query.search = @"new york mets"; 38 | */ 39 | + (instancetype)eventsSet; 40 | 41 | /** 42 | Returns a new `SGKEventSet` instance for the 43 | [/recommendations](http://platform.seatgeek.com/#recommendations) endpoint. A recommendations query should be seeded with an event or one or more performers. 44 | 45 | Events similar to Taylor Swift in New York: 46 | 47 | SGKEventSet *events = SGKEventSet.recommendationsSet; 48 | [events.query addFilter:@"performers.id" value:@(35)]; 49 | [events.query addFilter:@"postal_code" value:@(10014)]; 50 | 51 | Events similar to an event in New York: 52 | 53 | SGKEventSet *events = SGKEventSet.recommendationsSet; 54 | [events.query addFilter:@"events.id" value:@(1162104)]; 55 | [events.query addFilter:@"postal_code" value:@(10014)]; 56 | 57 | @warning The [/recommendations](http://platform.seatgeek.com/#recommendations) 58 | endpoint requires an API key. See [SGQuery.clientId](+[SGQuery setClientId:]) 59 | for details. 60 | */ 61 | + (instancetype)recommendationsSet; 62 | 63 | /** @name Creating a set with a base filter */ 64 | 65 | /** 66 | * Returns a new `SGKEventSet` instance for the 67 | * [/events](http://platform.seatgeek.com/#events) endpoint with a venue filter 68 | * applied. 69 | */ 70 | + (instancetype)setForVenue:(SGVenue *)venue; 71 | 72 | /** 73 | * Returns a new `SGKEventSet` instance for the 74 | * [/events](http://platform.seatgeek.com/#events) endpoint with a performer filter 75 | * applied. 76 | */ 77 | + (instancetype)setForPerformer:(SGPerformer *)performer; 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGKEventSet.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 18/01/14. 3 | // 4 | 5 | #import "SGKEventSet.h" 6 | #import "SGPerformer.h" 7 | #import "SGVenue.h" 8 | 9 | @implementation SGKEventSet 10 | 11 | #pragma mark - Set Factories 12 | 13 | + (instancetype)eventsSet { 14 | SGKEventSet *events = self.new; 15 | events.query = SGQuery.eventsQuery; 16 | events.resultArrayKey = @"events"; 17 | return events; 18 | } 19 | 20 | + (instancetype)recommendationsSet { 21 | SGKEventSet *events = self.new; 22 | events.query = SGQuery.recommendationsQuery; 23 | events.resultArrayKey = @"recommendations"; 24 | return events; 25 | } 26 | 27 | + (instancetype)setForVenue:(SGVenue *)venue { 28 | SGKEventSet *events = self.eventsSet; 29 | [events.query addFilter:@"venue.id" value:venue.ID]; 30 | return events; 31 | } 32 | 33 | + (instancetype)setForPerformer:(SGPerformer *)performer { 34 | SGKEventSet *events = self.eventsSet; 35 | [events.query addFilter:@"performers.id" value:performer.ID]; 36 | return events; 37 | } 38 | 39 | #pragma mark - Item Factory 40 | 41 | - (id)itemForDict:(NSDictionary *)dict { 42 | return dict[@"event"] 43 | ? [SGKEvent itemForDict:dict[@"event"]] 44 | : [SGKEvent itemForDict:dict]; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGPerformerSet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 18/01/14. 3 | // 4 | 5 | #import "SGItemSet.h" 6 | #import "SGQuery.h" 7 | #import "SGPerformer.h" 8 | 9 | /** 10 | `SGPerformerSet` provides paginated results of `SGPerformer` items by quering the 11 | [/performers](http://platform.seatgeek.com/#performers) endpoint. `SGPerformerSet` 12 | extends from , which provides the base result fetching and pagination 13 | interface. 14 | 15 | SGPerformerSet *performers = SGPerformerSet.performersSet; 16 | performers.query.search = @"imagine dragons"; 17 | 18 | performers.onPageLoaded = ^(NSOrderedSet *results) { 19 | for (SGPerformer *performer in results) { 20 | NSLog(@"performer: %@", performer.name); 21 | } 22 | }; 23 | 24 | [performers fetchNextPage]; 25 | */ 26 | 27 | @interface SGPerformerSet : SGItemSet 28 | 29 | /** 30 | Returns a new `SGPerformerSet` instance for the 31 | [/performers](http://platform.seatgeek.com/#performers) endpoint. Modify the 32 | [query](-[SGItemSet query]) to add parameters and filters. 33 | 34 | SGPerformerSet *performers = SGPerformerSet.performersSet; 35 | performers.query.search = @"imagine dragons"; 36 | */ 37 | + (instancetype)performersSet; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGPerformerSet.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 18/01/14. 3 | // 4 | 5 | #import "SGPerformerSet.h" 6 | 7 | @implementation SGPerformerSet 8 | 9 | + (instancetype)performersSet { 10 | SGPerformerSet *performers = self.new; 11 | performers.query = SGQuery.performersQuery; 12 | performers.resultArrayKey = @"performers"; 13 | return performers; 14 | } 15 | 16 | #pragma mark - Item Factory 17 | 18 | - (id)itemForDict:(NSDictionary *)dict { 19 | return dict[@"performer"] 20 | ? [SGPerformer itemForDict:dict[@"performer"]] 21 | : [SGPerformer itemForDict:dict]; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGVenueSet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 30/05/14. 3 | // 4 | 5 | #import "SGItemSet.h" 6 | #import "SGQuery.h" 7 | #import "SGVenue.h" 8 | 9 | /** 10 | `SGVenueSet` provides paginated results of `SGVenue` items by quering the 11 | [/venues](http://platform.seatgeek.com/#venues) endpoint. `SGVenueSet` 12 | extends from , which provides the base result fetching and pagination 13 | interface. 14 | 15 | SGVenueSet *venues = [SGVenueSet setForCity:@"rockford"]; 16 | 17 | venues.onPageLoaded = ^(NSOrderedSet *results) { 18 | for (SGVenue *venue in results) { 19 | NSLog(@"venue: %@", venue.name); 20 | } 21 | }; 22 | 23 | [venues fetchNextPage]; 24 | */ 25 | 26 | @interface SGVenueSet : SGItemSet 27 | 28 | /** @name Creating a set */ 29 | 30 | /** 31 | Returns a new `SGVenueSet` instance for the 32 | [/venues](http://platform.seatgeek.com/#venues) endpoint. Modify the 33 | [query](-[SGItemSet query]) to add parameters and filters. 34 | 35 | SGVenueSet *venues = SGVenueSet.venuesSet; 36 | venues.query.search = @"yankee stadium"; 37 | */ 38 | + (instancetype)venuesSet; 39 | 40 | /** @name Creating a set with a base filter */ 41 | 42 | /** 43 | Returns a new `SGVenueSet` instance for the 44 | [/venues](http://platform.seatgeek.com/#venues) endpoint with a city filter 45 | applied. 46 | 47 | SGVenueSet *venues = [SGVenueSet setForCity:@"rockford"]; 48 | */ 49 | + (instancetype)setForCity:(NSString *)city; 50 | 51 | /** 52 | Returns a new `SGVenueSet` instance for the 53 | [/venues](http://platform.seatgeek.com/#venues) endpoint with a state filter 54 | applied. 55 | 56 | SGVenueSet *venues = [SGVenueSet setForState:@"IL"]; 57 | */ 58 | + (instancetype)setForState:(NSString *)state; 59 | 60 | /** 61 | Returns a new `SGVenueSet` instance for the 62 | [/venues](http://platform.seatgeek.com/#venues) endpoint with a country filter 63 | applied. 64 | 65 | SGVenueSet *venues = [SGVenueSet setForCountry:@"US"]; 66 | */ 67 | + (instancetype)setForCountry:(NSString *)country; 68 | 69 | /** 70 | Returns a new `SGVenueSet` instance for the 71 | [/venues](http://platform.seatgeek.com/#venues) endpoint with a postal code filter 72 | applied. 73 | 74 | SGVenueSet *venues = [SGVenueSet setForPostalCode:@(90210)]; 75 | */ 76 | + (instancetype)setForPostalCode:(NSString *)postalCode; 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /SGAPI/ItemSets/SGVenueSet.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 30/05/14. 3 | // 4 | 5 | #import "SGVenueSet.h" 6 | 7 | @implementation SGVenueSet 8 | 9 | + (instancetype)venuesSet { 10 | SGVenueSet *venues = self.new; 11 | venues.query = SGQuery.venuesQuery; 12 | venues.resultArrayKey = @"venues"; 13 | return venues; 14 | } 15 | 16 | + (instancetype)setForCity:(NSString *)city { 17 | SGVenueSet *venues = self.venuesSet; 18 | [venues.query addFilter:@"city" value:city]; 19 | return venues; 20 | } 21 | 22 | + (instancetype)setForState:(NSString *)state { 23 | SGVenueSet *venues = self.venuesSet; 24 | [venues.query addFilter:@"state" value:state]; 25 | return venues; 26 | } 27 | 28 | + (instancetype)setForCountry:(NSString *)country { 29 | SGVenueSet *venues = self.venuesSet; 30 | [venues.query addFilter:@"country" value:country]; 31 | return venues; 32 | } 33 | 34 | + (instancetype)setForPostalCode:(NSString *)postalCode { 35 | SGVenueSet *venues = self.venuesSet; 36 | [venues.query addFilter:@"postal_code" value:postalCode]; 37 | return venues; 38 | } 39 | 40 | #pragma mark - Item Factory 41 | 42 | - (id)itemForDict:(NSDictionary *)dict { 43 | return dict[@"venue"] 44 | ? [SGVenue itemForDict:dict[@"venue"]] 45 | : [SGVenue itemForDict:dict]; 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /SGAPI/Items/SGItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #define SGItemFetchStarted @"SGItemFetchStarted" 6 | #define SGItemFetchSucceeded @"SGItemFetchSucceeded" 7 | #define SGItemFetchFailed @"SGItemFetchFailed" 8 | 9 | @class SGItemSet, SGQuery, SGDataManager, SGFileCache; 10 | 11 | /** 12 | * `SGItem` is the abstract model class for result items. The concrete models 13 | * are , , . 14 | */ 15 | 16 | @interface SGItem : NSObject 17 | 18 | #pragma mark - Fields 19 | 20 | /** @name Properties common to all item types */ 21 | 22 | /** 23 | * The type specific unique ID for the API result item. 24 | */ 25 | @property (nullable, nonatomic, readonly, strong) NSString *ID; 26 | 27 | /** 28 | * The item's [seatgeek.com](http://seatgeek.com) website URL. 29 | */ 30 | @property (nullable, nonatomic, readonly, copy) NSString *url; 31 | 32 | /** 33 | * `score` indicates the item's relative popularity within its type. Scores are 34 | * floating point values in the range of 0 to 1. See 35 | * [SeatGeek Platform docs](http://platform.seatgeek.com) for further details. 36 | */ 37 | @property (nullable, nonatomic, readonly, strong) NSNumber *score; 38 | 39 | /** 40 | * Type specific statistics for the result item. For example an event item might 41 | * include a listing count and average/high/low price values. 42 | */ 43 | @property (nullable, nonatomic, readonly, strong) NSDictionary *stats; 44 | 45 | /** 46 | * Some result items have a `title` value and some a `name`. For convenience 47 | * both can be accessed via `title`. 48 | */ 49 | - (nullable NSString *)title; 50 | 51 | #pragma mark - Fetching and caching 52 | 53 | /** @name Fetching and caching */ 54 | 55 | @property (nullable, nonatomic, strong) SGQuery *query; 56 | - (nullable NSString *)cacheKey; 57 | 58 | /** 59 | * Fetch the results based on the item's ID. If the item is already 60 | * `fetch` will do nothing. 61 | */ 62 | - (void)fetch; 63 | 64 | /** 65 | * Calls `fetch` on the item and recursively on all of its child items. 66 | * If one of these items is already or does not , 67 | * then its `fetch` will do nothing. 68 | */ 69 | - (void)fetchItemAndChildrenIfNeeded; 70 | 71 | /** 72 | * Returns YES if a fetch is in progress. 73 | */ 74 | - (BOOL)fetching; 75 | 76 | /** 77 | * The date of last successful fetch 78 | */ 79 | @property (nullable, nonatomic, strong) NSDate *lastFetched; 80 | 81 | /** 82 | * Seconds since `lastFetched` 83 | */ 84 | - (NSTimeInterval)fetchAge; 85 | 86 | /** 87 | * Some endpoints return partial item documents when returning arrays. 88 | * This property will be true in those cases. 89 | */ 90 | - (BOOL)hasPartialContents; 91 | 92 | /** 93 | * Replace current contents with contents from cache. Returns NO if nothing found in cache. 94 | */ 95 | - (BOOL)loadCachedContents; 96 | 97 | /** 98 | * Force the item contents to be cached again. Requires `cacheKey` to be set. 99 | */ 100 | - (void)cacheContents; 101 | 102 | /** 103 | * If this method returns YES (the default) item sets will cache their contents on successful fetch, 104 | * when a cache key has been provided. 105 | */ 106 | - (BOOL)shouldCacheOnFetch; 107 | 108 | /** 109 | * The date for cached items to expire. 110 | * Defaults to 1 month. 111 | */ 112 | @property (nonatomic, strong, nullable) NSDate *cacheExpiryDate; 113 | 114 | /// the SGFileCache for this class 115 | + (nonnull SGFileCache *)cache; 116 | 117 | #pragma mark - Composite properties 118 | 119 | /** @name Composite properties */ 120 | 121 | @property (nonatomic, weak, nullable) SGItemSet *parentSet; 122 | @property (nonatomic, weak, nullable) SGItem *parentItem; 123 | - (nullable NSArray *)childItems; 124 | 125 | #pragma mark - Raw results 126 | 127 | /** @name Raw result data */ 128 | 129 | /** 130 | * The raw API result dictionary. 131 | */ 132 | @property (nullable, nonatomic, strong) NSDictionary *dict; 133 | 134 | + (nonnull NSDateFormatter *)localDateParser; 135 | + (nonnull NSDateFormatter *)utcDateParser; 136 | 137 | #pragma mark - Data Manager 138 | 139 | @property (nullable, nonatomic, weak) SGDataManager *dataManager; 140 | - (void)setNeedsRefresh; 141 | - (BOOL)needsRefresh; 142 | 143 | #pragma mark - Ignore plz 144 | 145 | + (nonnull NSDictionary *)resultFields; 146 | @property (nonnull, nonatomic, copy) NSString *resultItemKey; 147 | + (nonnull id)itemForDict:(nullable NSDictionary *)dict; 148 | + (nullable id)valueFor:(nullable id)value withType:(nonnull Class)requiredType; 149 | - (nullable NSError *)lastFetchError; 150 | 151 | @end 152 | -------------------------------------------------------------------------------- /SGAPI/Items/SGItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import 6 | #import "SGItem.h" 7 | #import "SGQuery.h" 8 | #import 9 | #import 10 | #import 11 | 12 | static NSDateFormatter *_formatterLocal, *_formatterUTC; 13 | 14 | @interface SGItem () 15 | @property (nullable, nonatomic, strong) NSString *ID; 16 | @property (nonatomic, assign) BOOL fetching; 17 | @property (nonatomic, assign) BOOL needsRefresh; 18 | @property (nonatomic, assign) BOOL hasPartialContents; 19 | @property (nonatomic, strong) SGHTTPRequest *request; 20 | @property (nonatomic, strong) NSError *lastFetchError; 21 | @end 22 | 23 | @implementation SGItem 24 | 25 | // abstract. implemented in subclass 26 | + (NSDictionary *)resultFields { 27 | return @{}; 28 | } 29 | 30 | + (id)itemForDict:(NSDictionary *)dict { 31 | SGItem *item = self.new; 32 | item.dict = dict; 33 | return item; 34 | } 35 | 36 | - (nullable instancetype)initWithCoder:(NSCoder *)coder { 37 | self = [super init]; 38 | self.dict = [[coder decodeObjectForKey:@"dict"] sghttp_nullCleansedWithLoggingURL:nil]; 39 | self.lastFetched = [coder decodeObjectForKey:@"lastFetched"]; 40 | return self; 41 | } 42 | 43 | - (void)encodeWithCoder:(NSCoder *)coder { 44 | [coder encodeObject:self.dict forKey:@"dict"]; 45 | [coder encodeObject:self.lastFetched forKey:@"lastFetched"]; 46 | } 47 | 48 | #pragma mark - Fetching and caching 49 | 50 | - (void)fetchItemAndChildrenIfNeeded { 51 | if (self.needsRefresh && !self.fetching) { 52 | [self fetch]; 53 | } 54 | for (SGItem *child in self.childItems) { 55 | [child fetchItemAndChildrenIfNeeded]; 56 | } 57 | } 58 | 59 | - (void)fetch { 60 | if (self.fetching) { 61 | return; 62 | } 63 | 64 | if (!self.query || !self.resultItemKey) { 65 | #ifdef DEBUG 66 | NSAssert(NO, @"Called fetch on an SGitem that doesn't know how to fetch. Don't do that."); 67 | #endif 68 | return; 69 | } 70 | 71 | self.fetching = YES; 72 | 73 | SGHTTPRequest *req = [self.query requestWithMethod:SGHTTPRequestMethodGet]; 74 | if (SGQuery.consoleLogging) { 75 | req.logging = req.logging | (SGHTTPLogRequests | SGHTTPLogErrors); 76 | } 77 | 78 | __weakSelf me = self; 79 | req.onSuccess = ^(SGHTTPRequest *_req) { 80 | NSDictionary *responseDict = [SGJSONSerialization JSONObjectWithData:_req.responseData]; 81 | NSDictionary *itemDict = [responseDict valueForKeyPath:self.resultItemKey]; 82 | if (!itemDict) { 83 | [me trigger:SGItemFetchFailed]; 84 | } 85 | me.dict = itemDict; 86 | me.lastFetchError = nil; 87 | me.fetching = NO; 88 | me.hasPartialContents = NO; 89 | me.lastFetched = NSDate.date; 90 | if (me.shouldCacheOnFetch) { 91 | [me cacheContents]; 92 | } 93 | [me trigger:SGItemFetchSucceeded withContext:me]; 94 | }; 95 | 96 | req.onFailure = ^(SGHTTPRequest *_req) { 97 | me.fetching = NO; 98 | me.lastFetchError = _req.error; 99 | [me trigger:SGItemFetchFailed withContext:_req.error]; 100 | }; 101 | 102 | req.onNetworkReachable = ^{ 103 | [me fetch]; 104 | }; 105 | 106 | [req start]; 107 | self.request = req; 108 | [self trigger:SGItemFetchStarted withContext:self]; 109 | } 110 | 111 | - (NSString *)internalCacheKey { 112 | return self.cacheKey.length ? self.cacheKey : nil; 113 | } 114 | 115 | - (NSDate *)cacheExpiryDate { 116 | if (_cacheExpiryDate) { 117 | return _cacheExpiryDate; 118 | } 119 | if (self.parentItem) { 120 | return self.parentItem.cacheExpiryDate; 121 | } 122 | // expire after 1 month. If it hasn't been used by then just get a fresh copy 123 | _cacheExpiryDate = [NSDate.date dateByAddingTimeInterval:2592000]; 124 | return _cacheExpiryDate; 125 | } 126 | 127 | + (SGFileCache *)cache { 128 | NSString *cacheName = NSStringFromClass(self); 129 | 130 | static NSMutableArray *configuredCaches; 131 | static dispatch_once_t onceToken; 132 | dispatch_once(&onceToken, ^{ 133 | configuredCaches = NSMutableArray.new; 134 | }); 135 | 136 | SGFileCache *cache; 137 | 138 | @synchronized (SGItem.class) { 139 | cache = [SGFileCache cacheFor:cacheName]; 140 | if (![configuredCaches containsObject:cacheName]) { 141 | // only perform cache setup once per cache 142 | [configuredCaches addObject:cacheName]; 143 | cache.maxDiskCacheSizeMB = 0; // unlimited cache size 144 | [cache clearExpiredFiles]; 145 | } 146 | return cache; 147 | } 148 | } 149 | 150 | - (SGFileCache *)cache { 151 | return self.class.cache; 152 | } 153 | 154 | - (void)cacheContents { 155 | if (!self.internalCacheKey.length || self.hasPartialContents || !self.lastFetched) { 156 | return; 157 | } 158 | NSDictionary *cacheDict = @{@"dict":self.dict, @"lastFetched":self.lastFetched}; 159 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:cacheDict]; 160 | 161 | [self.cache cacheData:data 162 | for:self.internalCacheKey 163 | expiryDate:self.cacheExpiryDate]; 164 | } 165 | 166 | - (BOOL)haveCachedContents { 167 | return [self.cache hasCachedDataFor:self.internalCacheKey]; 168 | } 169 | 170 | - (BOOL)loadCachedContents { 171 | if (!self.haveCachedContents) { 172 | return NO; 173 | } 174 | NSData *data = [self.cache cachedDataFor:self.internalCacheKey]; 175 | NSDictionary *cachedContents = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 176 | if (![cachedContents isKindOfClass:NSDictionary.class]) { 177 | return NO; 178 | } 179 | self.dict = cachedContents[@"dict"]; 180 | self.lastFetched = cachedContents[@"lastFetched"]; 181 | self.hasPartialContents = NO; 182 | return YES; 183 | } 184 | 185 | - (BOOL)shouldCacheOnFetch { 186 | return YES; 187 | } 188 | 189 | #pragma mark - Setters 190 | 191 | - (void)setDict:(NSDictionary *)dict { 192 | _dict = dict; 193 | 194 | NSDictionary *keys = self.class.resultFields; 195 | 196 | for (NSString *key in self.class.resultFields.allKeys) { 197 | id value = [_dict valueForKeyPath:keys[key]]; 198 | if (value) { 199 | const char * type = property_getAttributes(class_getProperty(self.class, key.UTF8String)); 200 | NSString * typeString = [NSString stringWithUTF8String:type]; 201 | NSArray * attributes = [typeString componentsSeparatedByString:@","]; 202 | NSString * typeAttribute = [attributes objectAtIndex:0]; 203 | 204 | if ([typeAttribute hasPrefix:@"T@"] && [typeAttribute length] > 1) { 205 | NSString * typeClassName = [typeAttribute substringWithRange:NSMakeRange(3, [typeAttribute length]-4)]; //turns @"NSDate" into NSDate 206 | Class typeClass = NSClassFromString(typeClassName); 207 | if (typeClass != nil) { 208 | [self setValue:[self.class valueFor:value withType:typeClass] 209 | forKey:key]; 210 | } 211 | #ifdef DEBUG 212 | NSAssert(typeClass != nil, @"%@ could not find class with name %@ for property", NSStringFromClass(self.class), typeClassName); 213 | #endif 214 | } else { 215 | // the property is a primitive. Let cocoa handle it for us: 216 | /*** 217 | Similarly, setValue:forKey: determines the data type required by the appropriate accessor 218 | or instance variable for the specified key. If the data type is not an object, then the 219 | value is extracted from the passed object using the appropriate -Value method. 220 | **/ 221 | [self setValue:value forKey:key]; 222 | } 223 | } 224 | } 225 | 226 | self.needsRefresh = NO; 227 | } 228 | 229 | - (void)setNeedsRefresh { 230 | self.needsRefresh = YES; 231 | } 232 | 233 | #pragma mark - Shared Date Parsers 234 | 235 | + (NSDateFormatter *)localDateParser { 236 | static dispatch_once_t onceToken; 237 | dispatch_once(&onceToken, ^{ 238 | _formatterLocal = [[NSDateFormatter alloc] init]; 239 | _formatterLocal.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; 240 | _formatterLocal.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; 241 | }); 242 | return _formatterLocal; 243 | } 244 | 245 | + (NSDateFormatter *)utcDateParser { 246 | static dispatch_once_t onceToken; 247 | dispatch_once(&onceToken, ^{ 248 | _formatterUTC = [[NSDateFormatter alloc] init]; 249 | _formatterUTC.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; 250 | _formatterUTC.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; 251 | _formatterUTC.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; 252 | }); 253 | return _formatterUTC; 254 | } 255 | 256 | #pragma mark - Comparisons 257 | 258 | - (BOOL)isEqual:(id)other { 259 | if (other == self) { 260 | return YES; 261 | } 262 | if ([other class] == self.class && [[other ID] isEqualToString:self.ID]) { 263 | return YES; 264 | } 265 | return NO; 266 | } 267 | 268 | - (NSUInteger)hash { 269 | return self.ID.hash; 270 | } 271 | 272 | #pragma mark - Type Conversion 273 | 274 | + (nullable id)valueFor:(nullable id)value withType:(nonnull Class)requiredType { 275 | 276 | // nil or correct type, so send it back 277 | if (!value || [value isKindOfClass:requiredType]) { 278 | return value; 279 | } 280 | 281 | // NSNull? who ever thought that was a good idea? kill it with fire 282 | if ([value isKindOfClass:NSNull.class]) { 283 | return nil; 284 | } 285 | 286 | if (requiredType == NSString.class) { // wanted a string 287 | if ([value isKindOfClass:NSNumber.class]) { // got a number 288 | return [value stringValue]; 289 | } 290 | } else if (requiredType == NSNumber.class) { // wanted a number 291 | if ([value isKindOfClass:NSString.class]) { // got a string 292 | return @([value floatValue]); 293 | } 294 | } 295 | 296 | return nil; // can't help you. nil's all you're gonna get 297 | } 298 | 299 | #pragma mark - Getters 300 | 301 | - (BOOL)fetching { 302 | return _fetching; 303 | } 304 | 305 | - (NSTimeInterval)fetchAge { 306 | if (!self.lastFetched) { 307 | return -NSDate.distantPast.timeIntervalSinceNow; 308 | } else { 309 | return -self.lastFetched.timeIntervalSinceNow; 310 | } 311 | } 312 | 313 | - (NSString *)title { 314 | return nil; 315 | } 316 | 317 | - (NSArray *)childItems { 318 | return nil; 319 | } 320 | 321 | - (NSString *)cacheKey { 322 | return nil; 323 | } 324 | 325 | - (NSString *)description { 326 | return [NSString stringWithFormat:@"<%@ id:%@ title:%@ score:%@>", 327 | NSStringFromClass(self.class), self.ID, self.title, 328 | self.score]; 329 | } 330 | 331 | @end 332 | -------------------------------------------------------------------------------- /SGAPI/Items/SGKEvent.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import "SGItem.h" 6 | 7 | @class SGVenue, SGPerformer; 8 | 9 | /** 10 | * The `SGKEvent` model wraps individual item results from the 11 | * [/events](http://platform.seatgeek.com/#events) and 12 | * [/recommendations](http://platform.seatgeek.com/#recommendations) endpoints. 13 | * `SGKEvent` extends from , which contains properties common to all item 14 | * types. 15 | */ 16 | 17 | @interface SGKEvent : SGItem 18 | 19 | #pragma mark - Relationships 20 | 21 | /** @name Relationships */ 22 | 23 | /** 24 | * An instance for the event's venue. 25 | */ 26 | @property (nonatomic, strong) SGVenue *venue; 27 | 28 | /** 29 | * An array of instances for performers performing at the event. 30 | */ 31 | @property (nonatomic, strong) NSArray *performers; 32 | 33 | /** 34 | * An for the event's primary performer. 35 | */ 36 | @property (nonatomic, strong) SGPerformer *primaryPerformer; 37 | 38 | - (void)setupRelationships; 39 | 40 | #pragma mark - Fields 41 | 42 | /** @name Event properties */ 43 | 44 | /** 45 | * The event title. 46 | */ 47 | @property (nonatomic, readonly, copy) NSString *title; 48 | 49 | /** 50 | Either the same as or a shortened event title if one is available. 51 | 52 | title = "Milwaukee Brewers at New York Mets" 53 | shortTitle = "Brewers at Mets" 54 | */ 55 | @property (nonatomic, readonly, copy) NSString *shortTitle; 56 | 57 | /** 58 | * The event's most specific [taxonomy](<taxonomies>). eg `concert`, 59 | * `music_festival`, `mlb`. 60 | */ 61 | @property (nonatomic, readonly, copy) NSString *type; 62 | 63 | /** 64 | * The event's date and time as at its location. For example an event advertised 65 | * as starting at 6pm in NYC will have an 6pm `localDate`. 66 | */ 67 | @property (nonatomic, readonly, strong) NSDate *localDate; 68 | 69 | /** 70 | * The event's UTC date and time. For example an event advertised as starting at 6pm 71 | * in NYC will have an 11pm `utcDate` (EST == UTC - 5). 72 | */ 73 | @property (nonatomic, readonly, strong) NSDate *utcDate; 74 | 75 | /** 76 | * The UTC date for when the event was announced. 77 | */ 78 | @property (nonatomic, readonly, strong) NSDate *announceDate; 79 | 80 | /** 81 | * The UTC date for when the event will expire. 82 | */ 83 | @property (nonatomic, readonly, strong) NSDate *visibleUntil; 84 | 85 | /** 86 | * The UTC date for when the event was added to the database. 87 | */ 88 | @property (nonatomic, readonly, strong) NSDate *createdAt; 89 | 90 | /** 91 | * Whether the event is allocated seating or general admission. 92 | */ 93 | @property (nonatomic, readonly, assign) BOOL generalAdmission; 94 | 95 | /** 96 | * Will be YES if an exact start time is not yet known. 97 | */ 98 | @property (nonatomic, readonly, assign) BOOL timeTbd; 99 | 100 | /** 101 | * Will be YES if an exact date is not yet known. 102 | */ 103 | @property (nonatomic, readonly, assign) BOOL dateTbd; 104 | 105 | /** 106 | * An array of taxonomies, from least specific to most specific. 107 | */ 108 | @property (nonatomic, readonly, strong) NSArray *taxonomies; 109 | 110 | /** 111 | * Links to the event on other services across the web. 112 | */ 113 | @property (nonatomic, readonly, strong) NSArray *links; 114 | 115 | @end 116 | -------------------------------------------------------------------------------- /SGAPI/Items/SGKEvent.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import "SGKEvent.h" 6 | #import "SGVenue.h" 7 | #import "SGPerformer.h" 8 | 9 | @implementation SGKEvent 10 | 11 | + (NSDictionary *)resultFields { 12 | return @{ 13 | @"ID":@"id", 14 | @"title":@"title", 15 | @"shortTitle":@"short_title", 16 | @"score":@"score", 17 | @"url":@"url", 18 | @"type":@"type", 19 | @"taxonomies":@"taxonomies", 20 | @"stats":@"stats", 21 | @"links":@"links", 22 | @"generalAdmission":@"general_admission", 23 | @"timeTbd":@"time_tbd", 24 | @"dateTbd":@"date_tbd" 25 | 26 | }; 27 | } 28 | 29 | - (void)setupRelationships { 30 | self.venue = [SGVenue itemForDict:self.dict[@"venue"]]; 31 | self.venue.parentItem = self; 32 | 33 | NSMutableArray *performers = @[].mutableCopy; 34 | for (NSDictionary *performerDict in self.dict[@"performers"]) { 35 | SGPerformer *performer = [SGPerformer itemForDict:performerDict]; 36 | performer.parentItem = self; 37 | [performers addObject:performer]; 38 | if (performerDict[@"primary"]) { 39 | self.primaryPerformer = performer; 40 | } 41 | } 42 | self.performers = performers; 43 | } 44 | 45 | #pragma mark - Setters 46 | 47 | - (void)setDict:(NSDictionary *)dict { 48 | super.dict = dict; 49 | 50 | @synchronized (self.class.localDateParser) { 51 | _localDate = [self.class.localDateParser dateFromString:self.dict[@"datetime_local"]]; 52 | _utcDate = [self.class.utcDateParser dateFromString:self.dict[@"datetime_utc"]]; 53 | _announceDate = [self.class.utcDateParser dateFromString:self.dict[@"announce_date"]]; 54 | _visibleUntil = [self.class.utcDateParser dateFromString:self.dict[@"visible_until_utc"]]; 55 | _createdAt = [self.class.utcDateParser dateFromString:self.dict[@"created_at"]]; 56 | } 57 | 58 | [self setupRelationships]; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /SGAPI/Items/SGPerformer.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import "SGItem.h" 6 | 7 | /** 8 | * The `SGPerformer` model wraps individual item results from the 9 | * [/performers](http://platform.seatgeek.com/#events) endpoint. 10 | * `SGPerformer` extends from <SGItem>, which contains properties common to all item 11 | * types. 12 | */ 13 | 14 | @interface SGPerformer : SGItem 15 | 16 | /** @name Performer properties */ 17 | 18 | /** 19 | * The performer's name. 20 | */ 21 | @property (nonatomic, readonly, copy) NSString *name; 22 | 23 | /** 24 | Either the same as <name> or a shortened name if one is available. 25 | 26 | name = "New York Mets" 27 | shortName = "Mets" 28 | */ 29 | @property (nonatomic, readonly, copy) NSString *shortName; 30 | 31 | /** 32 | * A unique [slug](http://en.wikipedia.org/wiki/Slug_(web_publishing)#Slug) for the 33 | * performer. 34 | */ 35 | @property (nonatomic, readonly, copy) NSString *slug; 36 | 37 | /** 38 | * The performers's most specific [taxonomy](<taxonomies>). 39 | */ 40 | @property (nonatomic, readonly, copy) NSString *type; 41 | 42 | /** 43 | * An array of taxonomies, from least specific to most specific. 44 | */ 45 | @property (nonatomic, readonly, strong) NSArray *taxonomies; 46 | 47 | /** 48 | * A URL for an image of the performer. 49 | */ 50 | @property (nonatomic, readonly, copy) NSString *imageURL; 51 | 52 | /** 53 | * Links to the performer on other services across the web. 54 | */ 55 | @property (nonatomic, readonly, strong) NSArray *links; 56 | 57 | /** 58 | * Will be YES if the performer has upcoming events. 59 | */ 60 | @property (nonatomic, readonly, assign) BOOL hasUpcomingEvents; 61 | 62 | /** 63 | * The ID for the performer's home venue, if one exists. 64 | */ 65 | @property (nonatomic, readonly, strong) NSNumber *homeVenueId; 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /SGAPI/Items/SGPerformer.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import "SGPerformer.h" 6 | 7 | @implementation SGPerformer 8 | 9 | + (NSDictionary *)resultFields { 10 | return @{ 11 | @"ID":@"id", 12 | @"name":@"name", 13 | @"url":@"url", 14 | @"score":@"score", 15 | @"shortName":@"short_name", 16 | @"slug":@"slug", 17 | @"type":@"type", 18 | @"imageURL":@"image", 19 | @"stats":@"stats", 20 | @"taxonomies":@"taxonomies", 21 | @"links":@"links", 22 | @"homeVenueId":@"home_venue_id", 23 | @"hasUpcomingEvents":@"has_upcoming_events" 24 | }; 25 | } 26 | 27 | #pragma mark - Getters 28 | 29 | - (NSString *)title { 30 | return self.name; 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /SGAPI/Items/SGVenue.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import <CoreLocation/CoreLocation.h> 6 | #import "SGItem.h" 7 | 8 | /** 9 | * The `SGVenue` model wraps individual item results from the 10 | * [/venues](http://platform.seatgeek.com/#events) endpoint. 11 | * `SGVenue` extends from <SGItem>, which contains properties common to all item 12 | * types. 13 | */ 14 | 15 | @interface SGVenue : SGItem 16 | 17 | /** @name Performer properties */ 18 | 19 | /** 20 | * The venue name. 21 | */ 22 | @property (nonatomic, readonly, copy) NSString *name; 23 | 24 | /** 25 | * A unique [slug](http://en.wikipedia.org/wiki/Slug_(web_publishing)#Slug) for the 26 | * venue. 27 | */ 28 | @property (nonatomic, readonly, copy) NSString *slug; 29 | 30 | /** 31 | * The venue's street address. 32 | */ 33 | @property (nonatomic, readonly, copy) NSString *address; 34 | 35 | /** 36 | * A second line of the venue's address, if one exists. 37 | */ 38 | @property (nonatomic, readonly, copy) NSString *extendedAddress; 39 | 40 | /** 41 | * The venue's city. 42 | */ 43 | @property (nonatomic, readonly, copy) NSString *city; 44 | 45 | /** 46 | * The venue's state. 47 | */ 48 | @property (nonatomic, readonly, copy) NSString *state; 49 | 50 | /** 51 | * The venue's country. 52 | */ 53 | @property (nonatomic, readonly, copy) NSString *country; 54 | 55 | /** 56 | * The venues postal code. 57 | */ 58 | @property (nonatomic, readonly, strong) NSString *postalCode; 59 | 60 | /** 61 | * A string of the format "<city>, <state>, <postalCode>" for US/Canada 62 | * and "<city>, <country>" for everywhere else. 63 | */ 64 | @property (nonatomic, readonly, copy) NSString *displayLocation; 65 | 66 | /** 67 | * The venue's location coordinate, if known. 68 | */ 69 | @property (nonatomic, readonly, assign) CLLocationCoordinate2D location; 70 | 71 | /** 72 | * A timezone string for the venue. eg "America/New_York". 73 | */ 74 | @property (nonatomic, readonly, copy) NSString *timezone; 75 | 76 | /** 77 | * A URL for an image of the venue, if one is available. 78 | */ 79 | @property (nonatomic, readonly, copy) NSString *imageURL; 80 | 81 | #pragma mark - Helpers 82 | 83 | /** @name Helpers */ 84 | 85 | /** 86 | * Returns YES if the location isn't {0, 0}. 87 | */ 88 | - (BOOL)locationIsValid; 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /SGAPI/Items/SGVenue.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 7/01/13. 3 | // 4 | 5 | #import "SGVenue.h" 6 | 7 | @implementation SGVenue 8 | 9 | + (NSDictionary *)resultFields { 10 | return @{ 11 | @"ID":@"id", 12 | @"name":@"name", 13 | @"slug":@"slug", 14 | @"url":@"url", 15 | @"score":@"score", 16 | @"address":@"address", 17 | @"extendedAddress":@"extended_address", 18 | @"city":@"city", 19 | @"state":@"state", 20 | @"country":@"country", 21 | @"postalCode":@"postal_code", 22 | @"imageURL":@"image", 23 | @"displayLocation":@"display_location", 24 | @"timezone":@"timezone", 25 | @"stats":@"stats" 26 | }; 27 | } 28 | 29 | #pragma mark - Setters 30 | 31 | - (void)setDict:(NSDictionary *)dict { 32 | super.dict = dict; 33 | 34 | if ([self.dict[@"location"][@"lat"] isKindOfClass:NSNumber.class] 35 | && [self.dict[@"location"][@"lon"] isKindOfClass:NSNumber.class]) { 36 | _location.latitude = [self.dict[@"location"][@"lat"] doubleValue]; 37 | _location.longitude = [self.dict[@"location"][@"lon"] doubleValue]; 38 | } else { 39 | _location = CLLocationCoordinate2DMake(0, 0); 40 | } 41 | } 42 | 43 | #pragma mark - Getters 44 | 45 | - (NSString *)title { 46 | return self.name; 47 | } 48 | 49 | - (BOOL)locationIsValid { 50 | return self.location.latitude || self.location.longitude; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /SGAPI/SGAPI.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 31/05/14. 3 | // 4 | 5 | #import "SGQuery.h" 6 | #import "SGKEventSet.h" 7 | #import "SGPerformerSet.h" 8 | #import "SGVenueSet.h" 9 | -------------------------------------------------------------------------------- /SGAPI/SGDataManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // SGDataManager.h 3 | // SeatGeek 4 | // 5 | // Created by David McNerney on 10/5/15. 6 | // Copyright © 2015 SeatGeek. All rights reserved. 7 | // 8 | 9 | #import <Foundation/Foundation.h> 10 | 11 | @class SGItemSet, SGItem; 12 | 13 | /** 14 | Abstract base class. SGDataManagers do the following: 15 | 16 | - fetch objects from server endpoints 17 | - keep those objects in memory for the use of client code that needs them throughout the app 18 | - possibly save objects and associated resources for offline access 19 | - emit events so that client code can update UI etc 20 | */ 21 | @interface SGDataManager : NSObject 22 | 23 | @property (nonnull, nonatomic, strong) SGItemSet *itemSet; 24 | 25 | + (nonnull instancetype)managerForItemSet:(nonnull SGItemSet *)itemSet; 26 | 27 | - (nullable NSArray *)resultItems; 28 | 29 | #pragma mark - Refreshing data 30 | 31 | /** 32 | * Refetch any data marked as `needsRefresh`. Should keep the old data until 33 | * a successful server response replaces it. 34 | */ 35 | - (void)refresh; 36 | 37 | /** 38 | * Returns YES if the last refresh failed. 39 | */ 40 | - (BOOL)lastRefreshFailed; 41 | 42 | #pragma mark - Flagging data as in need of refresh 43 | 44 | /** 45 | * Mark a specific item as `needsRefresh`. Will be refetched on next `refresh` call. 46 | */ 47 | - (void)needToRefreshItemOfKind:(nonnull Class)itemClass withID:(nonnull NSString *)itemID; 48 | 49 | #pragma mark - Used by subclasses 50 | 51 | /** 52 | * Code that has caused the server to create an object, and received it back in 53 | * the response to its POST, can add the new object to the manager with this method, so 54 | * it's included before the next item set fetch completes. 55 | */ 56 | - (void)addResultItem:(nonnull SGItem *)item; 57 | 58 | /** 59 | * Similar to above, for when you've received an updated version of one of our result items. 60 | */ 61 | - (void)replaceResultItem:(nonnull SGItem *)updatedItem; 62 | 63 | /** 64 | * Code that has caused or noted a change that would remove an item from the result set 65 | * can use this method to remove it immediately, rather than waiting for a refetch 66 | * of the item set to complete. 67 | */ 68 | - (void)removeResultItem:(nonnull SGItem *)item; 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /SGAPI/SGDataManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // SGDataManager.m 3 | // SeatGeek 4 | // 5 | // Created by David McNerney on 10/5/15. 6 | // Copyright © 2015 SeatGeek. All rights reserved. 7 | // 8 | 9 | #import <MGEvents/MGEvents.h> 10 | #import "SGDataManager.h" 11 | #import "SGItemSet.h" 12 | #import "SGQuery.h" 13 | #import "SGItem.h" 14 | 15 | @interface SGDataManager () 16 | @property (nonatomic, strong) NSArray *resultItems; 17 | @property (nonatomic, assign) BOOL lastRefreshFailed; 18 | @end 19 | 20 | @implementation SGDataManager 21 | 22 | + (instancetype)managerForItemSet:(SGItemSet *)itemSet { 23 | SGDataManager *manager = self.new; 24 | manager.itemSet = itemSet; 25 | return manager; 26 | } 27 | 28 | #pragma mark - Refreshing data 29 | 30 | - (void)refresh { 31 | 32 | // need to refresh the entire itemSet? 33 | if (self.itemSet.needsRefresh && !self.itemSet.fetching) { 34 | [self.itemSet reset]; 35 | [self.itemSet fetchNextPage]; 36 | return; 37 | } 38 | 39 | // need to refresh any individual items? 40 | for (SGItem *item in self.itemSet.orderedSet) { 41 | [item fetchItemAndChildrenIfNeeded]; 42 | } 43 | } 44 | 45 | #pragma mark - Flagging data as in need of refresh 46 | 47 | - (void)needToRefreshItemOfKind:(Class)itemClass withID:(NSString *)itemID { 48 | BOOL weHaveIt = [self setNeedsToRefreshOnItemOfKind:itemClass withID:itemID in:self.itemSet.array]; 49 | if (!weHaveIt) { // we don't have the item, so refresh the entire set instead 50 | [self.itemSet setNeedsRefresh]; 51 | } 52 | } 53 | 54 | // returns NO if no matching item is found in our existing data 55 | - (BOOL)setNeedsToRefreshOnItemOfKind:(Class)itemClass withID:(NSString *)itemID in:(NSArray *)items { 56 | for (SGItem *item in items) { 57 | if ([item isKindOfClass:itemClass] && [item.ID isEqualToString:itemID]) { 58 | [item setNeedsRefresh]; 59 | return YES; 60 | } 61 | if ([self setNeedsToRefreshOnItemOfKind:itemClass withID:itemID in:item.childItems]) { 62 | return YES; 63 | } 64 | } 65 | return NO; 66 | } 67 | 68 | #pragma mark - Setters 69 | 70 | - (void)setItemSet:(SGItemSet *)itemSet { 71 | _itemSet = itemSet; 72 | 73 | self.itemSet.dataManager = self; 74 | 75 | // pick up any cached results 76 | self.resultItems = itemSet.array; 77 | 78 | __weakSelf me = self; 79 | [self when:itemSet does:SGItemSetFetchSucceeded doWithContext:^(NSOrderedSet *newItems) { 80 | me.lastRefreshFailed = NO; 81 | me.resultItems = me.itemSet.array; 82 | }]; 83 | 84 | [self when:itemSet does:SGItemSetFetchFailed do:^{ 85 | me.lastRefreshFailed = YES; 86 | }]; 87 | } 88 | 89 | #pragma mark - Used by subclasses 90 | 91 | - (void)addResultItem:(SGItem *)item { 92 | if (self.resultItems) { 93 | self.resultItems = [self.resultItems arrayByAddingObject:item]; 94 | } else { 95 | self.resultItems = @[ item ]; 96 | } 97 | } 98 | 99 | - (void)replaceResultItem:(SGItem *)updatedItem { 100 | NSMutableArray *updatedResultItems = [self.resultItems mutableCopy]; 101 | NSInteger index = [updatedResultItems indexOfObject:updatedItem]; 102 | if (index != NSNotFound) { 103 | updatedResultItems[index] = updatedItem; 104 | self.resultItems = updatedResultItems; 105 | } 106 | } 107 | 108 | - (void)removeResultItem:(nonnull SGItem *)item { 109 | NSMutableArray *updatedResultItems = [self.resultItems mutableCopy]; 110 | [updatedResultItems removeObject:item]; 111 | self.resultItems = updatedResultItems; 112 | } 113 | 114 | @end 115 | -------------------------------------------------------------------------------- /SGAPI/SGQuery.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 27/05/14. 3 | // 4 | 5 | #import <CoreLocation/CoreLocation.h> 6 | #ifndef SGHTTPREQUEST 7 | #define SGHTTPREQUEST <SGHTTPRequest/SGHTTPRequest.h> 8 | #endif 9 | #import SGHTTPREQUEST 10 | 11 | #define SGAPI_BASEFORMAT @"https://api.%@/2" 12 | #define SGAPI_BASEDOMAIN @"seatgeek.com" 13 | 14 | #ifndef __weakSelf 15 | #define __weakSelf __weak typeof(self) 16 | #endif 17 | 18 | /** 19 | * `SGQuery` builds URLs for querying the SeatGeek Platform. See the 20 | * [SeatGeek Platform docs](http://platform.seatgeek.com/) for available endpoints 21 | * and parameters. 22 | */ 23 | 24 | @interface SGQuery : NSObject 25 | 26 | /** @name Setup */ 27 | 28 | /** 29 | Some SeatGeek Platform endpoints require an 30 | [API key](https://seatgeek.com/account/develop). Set `clientId` to your API key 31 | in your AppDelegate's `application:didFinishLaunchingWithOptions:` 32 | 33 | SGQuery.clientId = @"my_API_key"; 34 | */ 35 | + (void)setClientId:(nonnull NSString *)clientId; 36 | 37 | /** 38 | Some SeatGeek Platform endpoints require an 39 | [API key](https://seatgeek.com/account/develop). Set `clientSecret` to your client secret 40 | in your AppDelegate's `application:didFinishLaunchingWithOptions:` 41 | 42 | SGQuery.clientSecret = @"my_client_secret"; 43 | */ 44 | + (void)setClientSecret:(nonnull NSString *)clientSecret; 45 | 46 | /** 47 | An optional `aid` value to append to all queries. Set this value in your 48 | AppDelegate's `application:didFinishLaunchingWithOptions:` 49 | 50 | SGQuery.aid = @"my_aid"; 51 | */ 52 | + (void)setAid:(nonnull NSString *)aid; 53 | 54 | /** 55 | An optional `pid` value to append to all queries. Set this value in your 56 | AppDelegate's `application:didFinishLaunchingWithOptions:` 57 | 58 | SGQuery.pid = @"my_pid"; 59 | */ 60 | + (void)setPid:(nonnull NSString *)pid; 61 | 62 | /** 63 | An optional `rid` value to append to all queries. Set this value in your 64 | AppDelegate's `application:didFinishLaunchingWithOptions:` 65 | 66 | SGQuery.rid = @"my_rid"; 67 | */ 68 | + (void)setRid:(nonnull NSString *)rid; 69 | 70 | /** 71 | * Output debug information to console. Default is NO. 72 | */ 73 | + (void)setConsoleLogging:(BOOL)logging; 74 | + (BOOL)consoleLogging; 75 | 76 | #pragma mark - Events 77 | 78 | /** @name Event queries */ 79 | 80 | /** 81 | * Returns a new `SGQuery` instance for the 82 | * [/events](http://platform.seatgeek.com/#events) endpoint. 83 | */ 84 | + (nonnull SGQuery *)eventsQuery; 85 | 86 | /** 87 | * Returns a new `SGQuery` instance for the `/recommendations` endpoint. 88 | * @warning The [/recommendations](http://platform.seatgeek.com/#recommendations) 89 | * endpoint requires an API key. See <setClientId:> for details. 90 | */ 91 | + (nonnull SGQuery *)recommendationsQuery; 92 | 93 | /** 94 | * Returns a new `SGQuery` instance for fetching a single event by id. 95 | */ 96 | + (nonnull SGQuery *)eventQueryForId:(nonnull NSNumber *)eventId; 97 | 98 | #pragma mark - Performers 99 | 100 | /** @name Performer queries */ 101 | 102 | /** 103 | * Returns a new `SGQuery` instance for the 104 | * [/performers](http://platform.seatgeek.com/#performers) endpoint. 105 | */ 106 | + (nonnull SGQuery *)performersQuery; 107 | 108 | /** 109 | * Returns a new `SGQuery` instance for fetching a single performer by id. 110 | */ 111 | + (nonnull SGQuery *)performerQueryForId:(nonnull NSNumber *)performerId; 112 | 113 | /** 114 | * Returns a new `SGQuery` instance for fetching a single performer by slug. 115 | */ 116 | + (nonnull SGQuery *)performerQueryForSlug:(nonnull NSString *)slug; 117 | 118 | #pragma mark - Venues 119 | 120 | /** @name Venue queries */ 121 | 122 | /** 123 | * Returns a new `SGQuery` instance for the 124 | * [/venues](http://platform.seatgeek.com/#venues) endpoint. 125 | */ 126 | + (nonnull SGQuery *)venuesQuery; 127 | 128 | /** 129 | * Returns a new `SGQuery` instance for fetching a single venue by id. 130 | */ 131 | + (nonnull SGQuery *)venueQueryForId:(nonnull NSNumber *)venueId; 132 | 133 | #pragma mark - The Payoff 134 | 135 | /** @name The payoff */ 136 | 137 | /** 138 | Returns an `NSURL` for the constructed API query. 139 | 140 | SGQuery *query = SGQuery.eventsQuery; 141 | query.search = @"imagine dragons"; 142 | 143 | NSLog(@"%@", query.URL); 144 | // https://api.seatgeek.com/2/events?q=imagine+dragons 145 | */ 146 | @property (nonnull, readonly) NSURL *URL; 147 | 148 | - (nonnull SGHTTPRequest *)requestWithMethod:(SGHTTPRequestMethod)method; 149 | 150 | #pragma mark - Pagination 151 | 152 | /** @name Pagination */ 153 | 154 | /** 155 | * The results page to fetch. Page numbers start from 1. 156 | */ 157 | @property (nonatomic, assign) NSUInteger page; 158 | 159 | /** 160 | * The number of results to return per page. Default is 10. 161 | */ 162 | @property (nonatomic, assign) NSUInteger perPage; 163 | 164 | #pragma mark - Keyword searches 165 | 166 | /** @name Keyword searches */ 167 | 168 | /** 169 | Apply a keyword search to the query. 170 | 171 | SGQuery *query = SGQuery.eventsQuery; 172 | query.search = @"imagine dragons"; 173 | */ 174 | @property (nonatomic, copy, nullable) NSString *search; 175 | 176 | #pragma mark - Location Parameters (for 'events' and 'venues') 177 | 178 | /** @name Geolocation filters */ 179 | 180 | /** 181 | * Filter results by a location coordinate. 182 | */ 183 | @property (nonatomic, assign) CLLocationCoordinate2D location; 184 | 185 | /** 186 | * Clears the location parameter 187 | */ 188 | - (void)clearLocation; 189 | 190 | /** 191 | * Specify a range for location based filters. Accepts miles ("mi") and kilometres 192 | * ("km"). Default is "30mi". 193 | */ 194 | @property (nonatomic, copy, nullable) NSString *range; 195 | 196 | #pragma mark - Date Range Parameters (for 'events') 197 | 198 | /** 199 | * Specify a from date (inclusive) for results 200 | */ 201 | @property (nonatomic, copy, nullable) NSDate *fromDate; 202 | 203 | /** 204 | * Specify a to date (inclusive) for results 205 | */ 206 | @property (nonatomic, copy, nullable) NSDate *toDate; 207 | 208 | #pragma mark - Freeform Parameters and Filters 209 | 210 | /** @name Other parameters and filters */ 211 | 212 | /** 213 | Set a query parameter. Setting a parameter will override its previous value. 214 | See the [API docs](http://platform.seatgeek.com/) for available parameters. 215 | 216 | [query setParameter:@"format" value:@"xml"]; 217 | [query setParameter:@"sort" value:@"announce_date.desc"]; 218 | */ 219 | - (void)setParameter:(nonnull NSString *)param value:(nullable id)value; 220 | 221 | /** 222 | Add a results filter. Filters are stacked, and the same filters can be applied multiple times with different values. See the 223 | [API docs](http://platform.seatgeek.com/) for available filters. 224 | 225 | [query addFilter:@"performers.slug" value:@"new-york-mets"]; 226 | [query addFilter:@"performers.slug" value:@"new-york-yankees"]; 227 | */ 228 | - (void)addFilter:(nonnull NSString *)filter value:(nullable id)value; 229 | 230 | @property (nonatomic, strong, nullable) NSDictionary *requestHeaders; 231 | 232 | // ignore plz 233 | // note: these constructors truncate query params! 234 | + (nonnull SGQuery *)queryWithPath:(nonnull NSString *)path; 235 | + (nonnull SGQuery *)queryWithBaseUrl:(nonnull NSString *)baseUrl; 236 | + (nonnull SGQuery *)queryWithBaseUrl:(nullable NSString *)baseUrl path:(nullable NSString *)path; 237 | + (nonnull NSMutableDictionary *)globalParameters; 238 | + (nonnull NSString *)defaultBaseDomain; 239 | + (nonnull NSString *)baseURL; 240 | + (void)setBaseURL:(nonnull NSString *)url; 241 | - (void)setPath:(nullable NSString *)path; 242 | - (void)rebuildQuery; 243 | 244 | @end 245 | -------------------------------------------------------------------------------- /SGAPI/SGQuery.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by matt on 27/05/14. 3 | // 4 | 5 | #import "SGQuery.h" 6 | #import "NSDate+ISO8601.h" 7 | 8 | NSString *_gBaseURL; 9 | BOOL _gConsoleLogging; 10 | NSMutableDictionary *_globalParams; 11 | 12 | @interface SGQuery () 13 | @property (nonatomic, strong) NSString *baseUrl; 14 | @property (nonatomic, strong) NSString *path; 15 | @property (nonatomic, strong) NSArray *queryItems; 16 | /// parameters that were baked in to the path at creation time 17 | @property (nonatomic, strong) NSMutableDictionary *bakedParameters; 18 | @property (nonatomic, strong) NSMutableDictionary *parameters; 19 | @property (nonatomic, strong) NSMutableDictionary *filters; 20 | @end 21 | 22 | @implementation SGQuery 23 | 24 | + (void)initialize { 25 | self.baseURL = [NSString stringWithFormat:self.baseFormat, self.defaultBaseDomain]; 26 | } 27 | 28 | + (NSString *)defaultBaseDomain { 29 | return SGAPI_BASEDOMAIN; 30 | } 31 | 32 | + (NSString *)baseFormat { 33 | return SGAPI_BASEFORMAT; 34 | } 35 | 36 | + (SGQuery *)queryWithPath:(NSString *)path { 37 | return [SGQuery queryWithBaseUrl:nil path:path]; 38 | } 39 | 40 | + (SGQuery *)queryWithBaseUrl:(NSString *)baseUrl { 41 | return [SGQuery queryWithBaseUrl:baseUrl path:nil]; 42 | } 43 | 44 | + (SGQuery *)queryWithBaseUrl:(NSString *)baseUrl path:(NSString *)path { 45 | SGQuery *query = self.new; 46 | query.baseUrl = baseUrl; 47 | query.path = path; 48 | [query rebuildQuery]; 49 | return query; 50 | } 51 | 52 | #pragma mark - Events Query Factories 53 | 54 | + (SGQuery *)eventsQuery { 55 | return [self queryWithPath:@"/events"]; 56 | } 57 | 58 | + (SGQuery *)recommendationsQuery { 59 | return [self queryWithPath:@"/recommendations"]; 60 | } 61 | 62 | + (SGQuery *)eventQueryForId:(NSNumber *)eventId { 63 | id path = [NSString stringWithFormat:@"/events/%@", eventId]; 64 | return [self queryWithPath:path]; 65 | } 66 | 67 | #pragma mark - Performers Query Factories 68 | 69 | + (SGQuery *)performersQuery { 70 | return [self queryWithPath:@"/performers"]; 71 | } 72 | 73 | + (SGQuery *)performerQueryForId:(NSNumber *)performerId { 74 | id path = [NSString stringWithFormat:@"/performers/%@", performerId]; 75 | return [self queryWithPath:path]; 76 | } 77 | 78 | + (SGQuery *)performerQueryForSlug:(NSString *)slug { 79 | SGQuery *query = self.performersQuery; 80 | [query setParameter:@"slug" value:slug]; 81 | return query; 82 | } 83 | 84 | #pragma mark - Venues Query Factories 85 | 86 | + (SGQuery *)venuesQuery { 87 | return [self queryWithPath:@"/venues"]; 88 | } 89 | 90 | + (SGQuery *)venueQueryForId:(NSNumber *)venueId { 91 | id path = [NSString stringWithFormat:@"/venues/%@", venueId]; 92 | return [self queryWithPath:path]; 93 | } 94 | 95 | #pragma mark - Query Rebuilding 96 | 97 | - (void)setParameter:(NSString *)param value:(id)value { 98 | if (value) { 99 | self.parameters[param] = value; 100 | if (self.bakedParameters[param]) { 101 | [self.bakedParameters removeObjectForKey:param]; 102 | } 103 | } else { 104 | [self.parameters removeObjectForKey:param]; 105 | [self.bakedParameters removeObjectForKey:param]; 106 | } 107 | [self rebuildQuery]; 108 | } 109 | 110 | - (void)addFilter:(NSString *)filter value:(id)value { 111 | if (value) { 112 | self.filters[filter] = value; 113 | if (self.bakedParameters[filter]) { 114 | [self.bakedParameters removeObjectForKey:filter]; 115 | } 116 | } else { 117 | [self.filters removeObjectForKey:filter]; 118 | [self.bakedParameters removeObjectForKey:filter]; 119 | } 120 | [self rebuildQuery]; 121 | } 122 | 123 | - (NSArray *)queryItemsForParameters:(NSDictionary *)parameters { 124 | NSMutableArray *queryItems = NSMutableArray.new; 125 | for (NSString *key in parameters) { 126 | id value = parameters[key]; 127 | if ([value isKindOfClass:NSArray.class]) { 128 | for (id arrayValue in value) { 129 | NSString *arrayValueString = [self queryItemValueAsString:arrayValue]; 130 | [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:arrayValueString]]; 131 | } 132 | } else { 133 | value = [self queryItemValueAsString:value]; 134 | [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:value]]; 135 | } 136 | } 137 | return queryItems; 138 | } 139 | 140 | - (NSString *)queryItemValueAsString:(id)value { 141 | return [value isKindOfClass:NSString.class] ? value : [NSString stringWithFormat:@"%@", value]; 142 | } 143 | 144 | - (void)rebuildQuery { 145 | self.queryItems = @[]; 146 | if (!self.URL) { 147 | return; 148 | } 149 | 150 | NSMutableArray *queryItems = NSMutableArray.array; 151 | [queryItems addObjectsFromArray:[self queryItemsForParameters:self.bakedParameters]]; 152 | [queryItems addObjectsFromArray:[self queryItemsForParameters:self.class.globalParameters]]; 153 | [queryItems addObjectsFromArray:[self queryItemsForParameters:self.parameters]]; 154 | [queryItems addObjectsFromArray:[self queryItemsForParameters:self.filters]]; 155 | self.queryItems = queryItems.copy; 156 | } 157 | 158 | #pragma mark - Parameter Setters 159 | 160 | - (void)setPath:(NSString *)path { 161 | _path = path; 162 | 163 | [self.bakedParameters removeAllObjects]; 164 | if ([path rangeOfString:@"?"].location == NSNotFound) { 165 | return; 166 | } 167 | NSString *query = [path componentsSeparatedByString:@"?"].lastObject; 168 | NSArray *queryComponents = [query componentsSeparatedByString:@"&"]; 169 | for (NSString *component in queryComponents) { 170 | NSArray *paramValuePair = [component componentsSeparatedByString:@"="]; 171 | if (paramValuePair.count != 2) { 172 | continue; 173 | } 174 | self.bakedParameters[paramValuePair.firstObject] = paramValuePair.lastObject; 175 | } 176 | } 177 | 178 | - (void)setPerPage:(NSUInteger)perPage { 179 | _perPage = perPage; 180 | [self setParameter:@"per_page" value:@(perPage)]; 181 | } 182 | 183 | - (void)setPage:(NSUInteger)page { 184 | _page = page; 185 | [self setParameter:@"page" value:@(page)]; 186 | } 187 | 188 | - (void)setLocation:(CLLocationCoordinate2D)location { 189 | _location = location; 190 | [self setParameter:@"lat" value:@(location.latitude)]; 191 | [self setParameter:@"lon" value:@(location.longitude)]; 192 | } 193 | 194 | - (void)clearLocation { 195 | _location = (CLLocationCoordinate2D){.latitude = 0, .longitude = 0}; 196 | [self setParameter:@"lat" value:nil]; 197 | [self setParameter:@"lon" value:nil]; 198 | } 199 | 200 | - (void)setRange:(NSString *)range { 201 | // todo: validate 202 | _range = range.copy; 203 | [self setParameter:@"range" value:range]; 204 | } 205 | 206 | - (void)setFromDate:(NSDate *)fromDate { 207 | _fromDate = fromDate.copy; 208 | [self setParameter:@"datetime_local.gte" value:fromDate.ISO8601]; 209 | } 210 | 211 | - (void)setToDate:(NSDate *)toDate { 212 | _toDate = toDate.copy; 213 | [self setParameter:@"datetime_local.lte" value:toDate.ISO8601]; 214 | } 215 | 216 | - (void)setSearch:(NSString *)search { 217 | _search = search; 218 | [self setParameter:@"q" value:search]; 219 | } 220 | 221 | + (void)setClientId:(NSString *)clientId { 222 | self.globalParameters[@"client_id"] = clientId; 223 | } 224 | 225 | + (void)setClientSecret:(NSString *)clientSecret { 226 | self.globalParameters[@"client_secret"] = clientSecret; 227 | } 228 | 229 | + (void)setAid:(NSString *)aid { 230 | self.globalParameters[@"aid"] = aid; 231 | } 232 | 233 | + (void)setPid:(NSString *)pid { 234 | self.globalParameters[@"pid"] = pid; 235 | } 236 | 237 | + (void)setRid:(NSString *)rid { 238 | self.globalParameters[@"rid"] = rid; 239 | } 240 | 241 | + (NSString *)baseURL { 242 | return _gBaseURL; 243 | } 244 | 245 | + (void)setBaseURL:(NSString *)url { 246 | _gBaseURL = url; 247 | } 248 | 249 | #pragma mark - Console Logging 250 | 251 | + (void)setConsoleLogging:(BOOL)logging { 252 | _gConsoleLogging = logging; 253 | } 254 | 255 | + (BOOL)consoleLogging { 256 | return _gConsoleLogging; 257 | } 258 | 259 | #pragma mark - Getters 260 | 261 | - (NSURL *)URL { 262 | NSString *baseUrl = self.baseUrl ?: _gBaseURL; 263 | NSString *path = self.path ?: @""; 264 | NSString *url = [baseUrl stringByAppendingString:path]; 265 | NSURLComponents *bits = [NSURLComponents componentsWithString:url]; 266 | bits.queryItems = self.queryItems; 267 | return bits.URL ?: NSURL.new; 268 | } 269 | 270 | - (SGHTTPRequest *)requestWithMethod:(SGHTTPRequestMethod)method { 271 | SGHTTPRequest *request; 272 | switch (method) { 273 | case SGHTTPRequestMethodGet: 274 | request = [SGHTTPRequest requestWithURL:self.URL]; 275 | break; 276 | case SGHTTPRequestMethodPost: 277 | request = [SGHTTPRequest postRequestWithURL:self.URL]; 278 | break; 279 | case SGHTTPRequestMethodDelete: 280 | request = [SGHTTPRequest deleteRequestWithURL:self.URL]; 281 | break; 282 | case SGHTTPRequestMethodPut: 283 | request = [SGHTTPRequest putRequestWithURL:self.URL]; 284 | break; 285 | case SGHTTPRequestMethodPatch: 286 | request = [SGHTTPRequest patchRequestWithURL:self.URL]; 287 | break; 288 | case SGHTTPRequestMethodMultipartPost: 289 | NSAssert(NO, @"SGQuery does not support multi-part post requests directly."); 290 | request = [SGHTTPRequest postRequestWithURL:self.URL]; 291 | break; 292 | } 293 | request.requestHeaders = self.requestHeaders.copy; 294 | return request; 295 | } 296 | 297 | - (NSMutableDictionary *)parameters { 298 | if (!_parameters) { 299 | _parameters = @{}.mutableCopy; 300 | } 301 | return _parameters; 302 | } 303 | 304 | - (NSMutableDictionary *)bakedParameters { 305 | if (!_bakedParameters) { 306 | _bakedParameters = @{}.mutableCopy; 307 | } 308 | return _bakedParameters; 309 | } 310 | 311 | - (NSMutableDictionary *)filters { 312 | if (!_filters) { 313 | _filters = @{}.mutableCopy; 314 | } 315 | return _filters; 316 | } 317 | 318 | + (NSMutableDictionary *)globalParameters { 319 | if (!_globalParams) { 320 | _globalParams = @{}.mutableCopy; 321 | } 322 | return _globalParams; 323 | } 324 | 325 | @end 326 | -------------------------------------------------------------------------------- /TestsApp/Podfile: -------------------------------------------------------------------------------- 1 | xcodeproj 'TestsApp' 2 | workspace '../SGAPI' 3 | link_with 'TestsAppTests' 4 | platform :ios, '7.0' 5 | 6 | pod 'SGAPI', :path => '../' 7 | pod 'Expecta' 8 | -------------------------------------------------------------------------------- /TestsApp/TestsApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 02F4B94AE45042EB9A680F19 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B245890084545A0B41314DE /* libPods.a */; }; 11 | 4763B0D37A44493490F7ECBC /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B245890084545A0B41314DE /* libPods.a */; }; 12 | C9474DE5195DD707007E92DD /* SGQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C9474DE4195DD707007E92DD /* SGQueryTests.m */; }; 13 | C9474DE7195DD96A007E92DD /* SGPerformerSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C9474DE6195DD96A007E92DD /* SGPerformerSetTests.m */; }; 14 | C9B48518195DCE6A00285C8B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C9B48517195DCE6A00285C8B /* main.m */; }; 15 | C9B4851B195DCE6A00285C8B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = C9B4851A195DCE6A00285C8B /* AppDelegate.m */; }; 16 | C9B4851D195DCE6A00285C8B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9B4851C195DCE6A00285C8B /* Images.xcassets */; }; 17 | C9B48533195DCEC900285C8B /* SGKEventSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C9B48532195DCEC900285C8B /* SGKEventSetTests.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | C9B48523195DCE6A00285C8B /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = C9B4850A195DCE6A00285C8B /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = C9B48511195DCE6A00285C8B; 26 | remoteInfo = TestsApp; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 0C8D74DCE2F34C8F95DC9391 /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = "<group>"; }; 32 | 7B245890084545A0B41314DE /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | C9474DE4195DD707007E92DD /* SGQueryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SGQueryTests.m; sourceTree = "<group>"; }; 34 | C9474DE6195DD96A007E92DD /* SGPerformerSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SGPerformerSetTests.m; sourceTree = "<group>"; }; 35 | C9B48512195DCE6A00285C8B /* TestsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestsApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | C9B48516195DCE6A00285C8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 37 | C9B48517195DCE6A00285C8B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 38 | C9B48519195DCE6A00285C8B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; }; 39 | C9B4851A195DCE6A00285C8B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; }; 40 | C9B4851C195DCE6A00285C8B /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; }; 41 | C9B48522195DCE6A00285C8B /* TestsAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestsAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | C9B48527195DCE6A00285C8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 43 | C9B48532195DCEC900285C8B /* SGKEventSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SGKEventSetTests.m; sourceTree = "<group>"; }; 44 | C9B48534195DCED100285C8B /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = "<group>"; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | C9B4850F195DCE6A00285C8B /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | 02F4B94AE45042EB9A680F19 /* libPods.a in Frameworks */, 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | C9B4851F195DCE6A00285C8B /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | 4763B0D37A44493490F7ECBC /* libPods.a in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | A45F344EF75540A99E3D450D /* Frameworks */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 7B245890084545A0B41314DE /* libPods.a */, 71 | ); 72 | name = Frameworks; 73 | sourceTree = "<group>"; 74 | }; 75 | C9B48509195DCE6A00285C8B = { 76 | isa = PBXGroup; 77 | children = ( 78 | C9B48534195DCED100285C8B /* Podfile */, 79 | C9B48514195DCE6A00285C8B /* TestsApp */, 80 | C9B48525195DCE6A00285C8B /* TestsAppTests */, 81 | C9B48513195DCE6A00285C8B /* Products */, 82 | 0C8D74DCE2F34C8F95DC9391 /* Pods.xcconfig */, 83 | A45F344EF75540A99E3D450D /* Frameworks */, 84 | ); 85 | sourceTree = "<group>"; 86 | }; 87 | C9B48513195DCE6A00285C8B /* Products */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | C9B48512195DCE6A00285C8B /* TestsApp.app */, 91 | C9B48522195DCE6A00285C8B /* TestsAppTests.xctest */, 92 | ); 93 | name = Products; 94 | sourceTree = "<group>"; 95 | }; 96 | C9B48514195DCE6A00285C8B /* TestsApp */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | C9B48519195DCE6A00285C8B /* AppDelegate.h */, 100 | C9B4851A195DCE6A00285C8B /* AppDelegate.m */, 101 | C9B4851C195DCE6A00285C8B /* Images.xcassets */, 102 | C9B48515195DCE6A00285C8B /* Supporting Files */, 103 | ); 104 | path = TestsApp; 105 | sourceTree = "<group>"; 106 | }; 107 | C9B48515195DCE6A00285C8B /* Supporting Files */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | C9B48516195DCE6A00285C8B /* Info.plist */, 111 | C9B48517195DCE6A00285C8B /* main.m */, 112 | ); 113 | name = "Supporting Files"; 114 | sourceTree = "<group>"; 115 | }; 116 | C9B48525195DCE6A00285C8B /* TestsAppTests */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | C9474DE4195DD707007E92DD /* SGQueryTests.m */, 120 | C9B48532195DCEC900285C8B /* SGKEventSetTests.m */, 121 | C9474DE6195DD96A007E92DD /* SGPerformerSetTests.m */, 122 | C9B48526195DCE6A00285C8B /* Supporting Files */, 123 | ); 124 | path = TestsAppTests; 125 | sourceTree = "<group>"; 126 | }; 127 | C9B48526195DCE6A00285C8B /* Supporting Files */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | C9B48527195DCE6A00285C8B /* Info.plist */, 131 | ); 132 | name = "Supporting Files"; 133 | sourceTree = "<group>"; 134 | }; 135 | /* End PBXGroup section */ 136 | 137 | /* Begin PBXNativeTarget section */ 138 | C9B48511195DCE6A00285C8B /* TestsApp */ = { 139 | isa = PBXNativeTarget; 140 | buildConfigurationList = C9B4852C195DCE6A00285C8B /* Build configuration list for PBXNativeTarget "TestsApp" */; 141 | buildPhases = ( 142 | EC5254860CDC493F8FF2C094 /* Check Pods Manifest.lock */, 143 | C9B4850E195DCE6A00285C8B /* Sources */, 144 | C9B4850F195DCE6A00285C8B /* Frameworks */, 145 | C9B48510195DCE6A00285C8B /* Resources */, 146 | C77EF6FE03794FA79996AF5E /* Copy Pods Resources */, 147 | ); 148 | buildRules = ( 149 | ); 150 | dependencies = ( 151 | ); 152 | name = TestsApp; 153 | productName = TestsApp; 154 | productReference = C9B48512195DCE6A00285C8B /* TestsApp.app */; 155 | productType = "com.apple.product-type.application"; 156 | }; 157 | C9B48521195DCE6A00285C8B /* TestsAppTests */ = { 158 | isa = PBXNativeTarget; 159 | buildConfigurationList = C9B4852F195DCE6A00285C8B /* Build configuration list for PBXNativeTarget "TestsAppTests" */; 160 | buildPhases = ( 161 | 6F5B917C70EE41F3B793A0F3 /* Check Pods Manifest.lock */, 162 | C9B4851E195DCE6A00285C8B /* Sources */, 163 | C9B4851F195DCE6A00285C8B /* Frameworks */, 164 | C9B48520195DCE6A00285C8B /* Resources */, 165 | 9D580E745B714EDFB10B8C87 /* Copy Pods Resources */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | C9B48524195DCE6A00285C8B /* PBXTargetDependency */, 171 | ); 172 | name = TestsAppTests; 173 | productName = TestsAppTests; 174 | productReference = C9B48522195DCE6A00285C8B /* TestsAppTests.xctest */; 175 | productType = "com.apple.product-type.bundle.unit-test"; 176 | }; 177 | /* End PBXNativeTarget section */ 178 | 179 | /* Begin PBXProject section */ 180 | C9B4850A195DCE6A00285C8B /* Project object */ = { 181 | isa = PBXProject; 182 | attributes = { 183 | LastUpgradeCheck = 0600; 184 | ORGANIZATIONNAME = SeatGeek; 185 | TargetAttributes = { 186 | C9B48511195DCE6A00285C8B = { 187 | CreatedOnToolsVersion = 6.0; 188 | }; 189 | C9B48521195DCE6A00285C8B = { 190 | CreatedOnToolsVersion = 6.0; 191 | TestTargetID = C9B48511195DCE6A00285C8B; 192 | }; 193 | }; 194 | }; 195 | buildConfigurationList = C9B4850D195DCE6A00285C8B /* Build configuration list for PBXProject "TestsApp" */; 196 | compatibilityVersion = "Xcode 3.2"; 197 | developmentRegion = English; 198 | hasScannedForEncodings = 0; 199 | knownRegions = ( 200 | English, 201 | en, 202 | ); 203 | mainGroup = C9B48509195DCE6A00285C8B; 204 | productRefGroup = C9B48513195DCE6A00285C8B /* Products */; 205 | projectDirPath = ""; 206 | projectRoot = ""; 207 | targets = ( 208 | C9B48511195DCE6A00285C8B /* TestsApp */, 209 | C9B48521195DCE6A00285C8B /* TestsAppTests */, 210 | ); 211 | }; 212 | /* End PBXProject section */ 213 | 214 | /* Begin PBXResourcesBuildPhase section */ 215 | C9B48510195DCE6A00285C8B /* Resources */ = { 216 | isa = PBXResourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | C9B4851D195DCE6A00285C8B /* Images.xcassets in Resources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | C9B48520195DCE6A00285C8B /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | runOnlyForDeploymentPostprocessing = 0; 229 | }; 230 | /* End PBXResourcesBuildPhase section */ 231 | 232 | /* Begin PBXShellScriptBuildPhase section */ 233 | 6F5B917C70EE41F3B793A0F3 /* Check Pods Manifest.lock */ = { 234 | isa = PBXShellScriptBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | ); 238 | inputPaths = ( 239 | ); 240 | name = "Check Pods Manifest.lock"; 241 | outputPaths = ( 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | shellPath = /bin/sh; 245 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 246 | showEnvVarsInLog = 0; 247 | }; 248 | 9D580E745B714EDFB10B8C87 /* Copy Pods Resources */ = { 249 | isa = PBXShellScriptBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | ); 253 | inputPaths = ( 254 | ); 255 | name = "Copy Pods Resources"; 256 | outputPaths = ( 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | shellPath = /bin/sh; 260 | shellScript = "\"${SRCROOT}/Pods/Pods-resources.sh\"\n"; 261 | showEnvVarsInLog = 0; 262 | }; 263 | C77EF6FE03794FA79996AF5E /* Copy Pods Resources */ = { 264 | isa = PBXShellScriptBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | ); 268 | inputPaths = ( 269 | ); 270 | name = "Copy Pods Resources"; 271 | outputPaths = ( 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | shellPath = /bin/sh; 275 | shellScript = "\"${SRCROOT}/Pods/Pods-resources.sh\"\n"; 276 | showEnvVarsInLog = 0; 277 | }; 278 | EC5254860CDC493F8FF2C094 /* Check Pods Manifest.lock */ = { 279 | isa = PBXShellScriptBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | ); 283 | inputPaths = ( 284 | ); 285 | name = "Check Pods Manifest.lock"; 286 | outputPaths = ( 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | shellPath = /bin/sh; 290 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 291 | showEnvVarsInLog = 0; 292 | }; 293 | /* End PBXShellScriptBuildPhase section */ 294 | 295 | /* Begin PBXSourcesBuildPhase section */ 296 | C9B4850E195DCE6A00285C8B /* Sources */ = { 297 | isa = PBXSourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | C9B4851B195DCE6A00285C8B /* AppDelegate.m in Sources */, 301 | C9B48518195DCE6A00285C8B /* main.m in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | C9B4851E195DCE6A00285C8B /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | C9474DE5195DD707007E92DD /* SGQueryTests.m in Sources */, 310 | C9474DE7195DD96A007E92DD /* SGPerformerSetTests.m in Sources */, 311 | C9B48533195DCEC900285C8B /* SGKEventSetTests.m in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | /* End PBXSourcesBuildPhase section */ 316 | 317 | /* Begin PBXTargetDependency section */ 318 | C9B48524195DCE6A00285C8B /* PBXTargetDependency */ = { 319 | isa = PBXTargetDependency; 320 | target = C9B48511195DCE6A00285C8B /* TestsApp */; 321 | targetProxy = C9B48523195DCE6A00285C8B /* PBXContainerItemProxy */; 322 | }; 323 | /* End PBXTargetDependency section */ 324 | 325 | /* Begin XCBuildConfiguration section */ 326 | C9B4852A195DCE6A00285C8B /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 331 | CLANG_CXX_LIBRARY = "libc++"; 332 | CLANG_ENABLE_MODULES = YES; 333 | CLANG_ENABLE_OBJC_ARC = YES; 334 | CLANG_WARN_BOOL_CONVERSION = YES; 335 | CLANG_WARN_CONSTANT_CONVERSION = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_EMPTY_BODY = YES; 338 | CLANG_WARN_ENUM_CONVERSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 344 | COPY_PHASE_STRIP = NO; 345 | ENABLE_STRICT_OBJC_MSGSEND = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu99; 347 | GCC_DYNAMIC_NO_PIC = NO; 348 | GCC_OPTIMIZATION_LEVEL = 0; 349 | GCC_PREPROCESSOR_DEFINITIONS = ( 350 | "DEBUG=1", 351 | "$(inherited)", 352 | ); 353 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 354 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 355 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 356 | GCC_WARN_UNDECLARED_SELECTOR = YES; 357 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 358 | GCC_WARN_UNUSED_FUNCTION = YES; 359 | GCC_WARN_UNUSED_VARIABLE = YES; 360 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 361 | METAL_ENABLE_DEBUG_INFO = YES; 362 | ONLY_ACTIVE_ARCH = YES; 363 | SDKROOT = iphoneos; 364 | TARGETED_DEVICE_FAMILY = "1,2"; 365 | }; 366 | name = Debug; 367 | }; 368 | C9B4852B195DCE6A00285C8B /* Release */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | ALWAYS_SEARCH_USER_PATHS = NO; 372 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 373 | CLANG_CXX_LIBRARY = "libc++"; 374 | CLANG_ENABLE_MODULES = YES; 375 | CLANG_ENABLE_OBJC_ARC = YES; 376 | CLANG_WARN_BOOL_CONVERSION = YES; 377 | CLANG_WARN_CONSTANT_CONVERSION = YES; 378 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 379 | CLANG_WARN_EMPTY_BODY = YES; 380 | CLANG_WARN_ENUM_CONVERSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 386 | COPY_PHASE_STRIP = YES; 387 | ENABLE_NS_ASSERTIONS = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 397 | METAL_ENABLE_DEBUG_INFO = NO; 398 | SDKROOT = iphoneos; 399 | TARGETED_DEVICE_FAMILY = "1,2"; 400 | VALIDATE_PRODUCT = YES; 401 | }; 402 | name = Release; 403 | }; 404 | C9B4852D195DCE6A00285C8B /* Debug */ = { 405 | isa = XCBuildConfiguration; 406 | baseConfigurationReference = 0C8D74DCE2F34C8F95DC9391 /* Pods.xcconfig */; 407 | buildSettings = { 408 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 409 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 410 | INFOPLIST_FILE = TestsApp/Info.plist; 411 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 412 | PRODUCT_NAME = "$(TARGET_NAME)"; 413 | }; 414 | name = Debug; 415 | }; 416 | C9B4852E195DCE6A00285C8B /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | baseConfigurationReference = 0C8D74DCE2F34C8F95DC9391 /* Pods.xcconfig */; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 422 | INFOPLIST_FILE = TestsApp/Info.plist; 423 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 424 | PRODUCT_NAME = "$(TARGET_NAME)"; 425 | }; 426 | name = Release; 427 | }; 428 | C9B48530195DCE6A00285C8B /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | baseConfigurationReference = 0C8D74DCE2F34C8F95DC9391 /* Pods.xcconfig */; 431 | buildSettings = { 432 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/TestsApp.app/TestsApp"; 433 | FRAMEWORK_SEARCH_PATHS = ( 434 | "$(SDKROOT)/Developer/Library/Frameworks", 435 | "$(inherited)", 436 | ); 437 | GCC_PREPROCESSOR_DEFINITIONS = ( 438 | "DEBUG=1", 439 | "$(inherited)", 440 | ); 441 | INFOPLIST_FILE = TestsAppTests/Info.plist; 442 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 443 | METAL_ENABLE_DEBUG_INFO = YES; 444 | PRODUCT_NAME = "$(TARGET_NAME)"; 445 | TEST_HOST = "$(BUNDLE_LOADER)"; 446 | }; 447 | name = Debug; 448 | }; 449 | C9B48531195DCE6A00285C8B /* Release */ = { 450 | isa = XCBuildConfiguration; 451 | baseConfigurationReference = 0C8D74DCE2F34C8F95DC9391 /* Pods.xcconfig */; 452 | buildSettings = { 453 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/TestsApp.app/TestsApp"; 454 | FRAMEWORK_SEARCH_PATHS = ( 455 | "$(SDKROOT)/Developer/Library/Frameworks", 456 | "$(inherited)", 457 | ); 458 | INFOPLIST_FILE = TestsAppTests/Info.plist; 459 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 460 | METAL_ENABLE_DEBUG_INFO = NO; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | TEST_HOST = "$(BUNDLE_LOADER)"; 463 | }; 464 | name = Release; 465 | }; 466 | /* End XCBuildConfiguration section */ 467 | 468 | /* Begin XCConfigurationList section */ 469 | C9B4850D195DCE6A00285C8B /* Build configuration list for PBXProject "TestsApp" */ = { 470 | isa = XCConfigurationList; 471 | buildConfigurations = ( 472 | C9B4852A195DCE6A00285C8B /* Debug */, 473 | C9B4852B195DCE6A00285C8B /* Release */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | C9B4852C195DCE6A00285C8B /* Build configuration list for PBXNativeTarget "TestsApp" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | C9B4852D195DCE6A00285C8B /* Debug */, 482 | C9B4852E195DCE6A00285C8B /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | C9B4852F195DCE6A00285C8B /* Build configuration list for PBXNativeTarget "TestsAppTests" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | C9B48530195DCE6A00285C8B /* Debug */, 491 | C9B48531195DCE6A00285C8B /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | /* End XCConfigurationList section */ 497 | }; 498 | rootObject = C9B4850A195DCE6A00285C8B /* Project object */; 499 | } 500 | -------------------------------------------------------------------------------- /TestsApp/TestsApp/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // TestsApp 4 | // 5 | // Created by Matt Greenfield on 27/06/14. 6 | // Copyright (c) 2014 SeatGeek. All rights reserved. 7 | // 8 | 9 | #import <UIKit/UIKit.h> 10 | 11 | @interface AppDelegate : UIResponder <UIApplicationDelegate> 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /TestsApp/TestsApp/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // TestsApp 4 | // 5 | // Created by Matt Greenfield on 27/06/14. 6 | // Copyright (c) 2014 SeatGeek. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | 14 | @end 15 | 16 | @implementation AppDelegate 17 | 18 | 19 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 20 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 21 | // Override point for customization after application launch. 22 | self.window.backgroundColor = [UIColor whiteColor]; 23 | [self.window makeKeyAndVisible]; 24 | return YES; 25 | } 26 | 27 | - (void)applicationWillResignActive:(UIApplication *)application { 28 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 29 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 30 | } 31 | 32 | - (void)applicationDidEnterBackground:(UIApplication *)application { 33 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 34 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 35 | } 36 | 37 | - (void)applicationWillEnterForeground:(UIApplication *)application { 38 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 39 | } 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | - (void)applicationWillTerminate:(UIApplication *)application { 46 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /TestsApp/TestsApp/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "29x29", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "40x40", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "76x76", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /TestsApp/TestsApp/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /TestsApp/TestsApp/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>${EXECUTABLE_NAME}</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>com.seatgeek.${PRODUCT_NAME:rfc1034identifier}</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>${PRODUCT_NAME}</string> 15 | <key>CFBundlePackageType</key> 16 | <string>APPL</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>1.0</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>1</string> 23 | <key>LSRequiresIPhoneOS</key> 24 | <true/> 25 | <key>UIRequiredDeviceCapabilities</key> 26 | <array> 27 | <string>armv7</string> 28 | </array> 29 | </dict> 30 | </plist> 31 | -------------------------------------------------------------------------------- /TestsApp/TestsApp/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TestsApp 4 | // 5 | // Created by Matt Greenfield on 27/06/14. 6 | // Copyright (c) 2014 SeatGeek. All rights reserved. 7 | // 8 | 9 | #import <UIKit/UIKit.h> 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TestsApp/TestsAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>${EXECUTABLE_NAME}</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>com.seatgeek.${PRODUCT_NAME:rfc1034identifier}</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>${PRODUCT_NAME}</string> 15 | <key>CFBundlePackageType</key> 16 | <string>BNDL</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>1.0</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>1</string> 23 | </dict> 24 | </plist> 25 | -------------------------------------------------------------------------------- /TestsApp/TestsAppTests/SGKEventSetTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SGKEventSetTests.m 3 | // SGPlatformTests 4 | // 5 | // Created by Matt Greenfield on 27/06/14. 6 | // Copyright (c) 2014 SeatGeek. All rights reserved. 7 | // 8 | 9 | #define EXP_SHORTHAND YES 10 | 11 | #import <XCTest/XCTest.h> 12 | #import <Expecta/Expecta.h> 13 | #import <SGAPI/SGAPI.h> 14 | 15 | @interface SGKEventSetTests : XCTestCase 16 | 17 | @end 18 | 19 | @implementation SGKEventSetTests 20 | 21 | + (void)setUp { 22 | [super setUp]; 23 | Expecta.asynchronousTestTimeout = 10; 24 | } 25 | 26 | + (void)tearDown { 27 | [super tearDown]; 28 | } 29 | 30 | - (void)testMetsEvents { 31 | SGEKventSet *events = SGEKventSet.eventsSet; 32 | events.query.search = @"new york mets"; 33 | 34 | __weak SGEKventSet *wEvents = events; 35 | events.onPageLoaded = ^(NSOrderedSet *results) { 36 | expect(wEvents.fetching).to.beFalsy(); 37 | expect(wEvents.count).will.beGreaterThan(0); 38 | }; 39 | 40 | [events fetchNextPage]; 41 | 42 | expect(events.fetching).to.beTruthy(); 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /TestsApp/TestsAppTests/SGPerformerSetTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SGPerformerSetTests.m 3 | // TestsApp 4 | // 5 | // Created by Matt Greenfield on 27/06/14. 6 | // Copyright (c) 2014 SeatGeek. All rights reserved. 7 | // 8 | 9 | #define EXP_SHORTHAND YES 10 | 11 | #import <XCTest/XCTest.h> 12 | #import <Expecta/Expecta.h> 13 | #import <SGAPI/SGAPI.h> 14 | 15 | @interface SGPerformerSetTests : XCTestCase 16 | 17 | @end 18 | 19 | @implementation SGPerformerSetTests 20 | 21 | + (void)setUp { 22 | [super setUp]; 23 | Expecta.asynchronousTestTimeout = 10; 24 | } 25 | 26 | - (void)testYankees { 27 | __block NSOrderedSet *results = nil; 28 | 29 | SGPerformerSet *performers = SGPerformerSet.performersSet; 30 | performers.query.search = @"yankees"; 31 | 32 | performers.onPageLoaded = ^(NSOrderedSet *_results) { 33 | NSLog(@"results:%@", _results); 34 | results = _results; 35 | }; 36 | 37 | [performers fetchNextPage]; 38 | expect(performers.fetching).to.beTruthy(); 39 | 40 | // async tests 41 | expect(results.count).will.beGreaterThan(0); 42 | expect([results.firstObject name]).will.equal(@"New York Yankees"); 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /TestsApp/TestsAppTests/SGQueryTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SGQueryTests.m 3 | // TestsApp 4 | // 5 | // Created by Matt Greenfield on 27/06/14. 6 | // Copyright (c) 2014 SeatGeek. All rights reserved. 7 | // 8 | 9 | #define EXP_SHORTHAND YES 10 | 11 | #import <XCTest/XCTest.h> 12 | #import <Expecta/Expecta.h> 13 | #import <SGAPI/SGAPI.h> 14 | 15 | @interface SGQueryTests : XCTestCase 16 | 17 | @end 18 | 19 | @implementation SGQueryTests 20 | 21 | - (void)setUp { 22 | [super setUp]; 23 | Expecta.asynchronousTestTimeout = 10; 24 | } 25 | 26 | - (void)testEventsQuery { 27 | SGQuery *query = SGQuery.eventsQuery; 28 | expect(query.URL.absoluteString).to.equal(@"http://api.seatgeek.com/2/events"); 29 | } 30 | 31 | @end 32 | --------------------------------------------------------------------------------