├── .gitignore ├── JSUpdateLookup.h ├── JSUpdateLookup.m ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /JSUpdateLookup.h: -------------------------------------------------------------------------------- 1 | // 2 | // JSUpdateLookup 3 | // Copyright (c) 2013 John Sundell 4 | // 5 | 6 | #import 7 | 8 | @class JSUpdateLookup; 9 | @class JSUpdateInfo; 10 | 11 | typedef void(^JSUpdateLookupCompletionHandler)(JSUpdateInfo *updateInfo, NSError *error); 12 | 13 | /** 14 | * Delegate protocol to get notified of update lookup events 15 | */ 16 | @protocol JSUpdateLookupDelegate 17 | 18 | /** 19 | * Sent to the update lookup's delegate when it has finished loading 20 | * 21 | * @param updateLookup The update lookup instance that triggered the event. 22 | * @param updateInfo Object containing update info (see JSUpdateInfo). 23 | */ 24 | - (void)updateLookup:(JSUpdateLookup *)updateLookup didFinishWithInfo:(JSUpdateInfo *)updateInfo; 25 | 26 | /** 27 | * Sent to the update lookup's delegate if it failed to load update info 28 | * 29 | * @param updateLookup The update lookup instance that triggered the event. 30 | * @param error An error that identifies the reason why the lookup failed. 31 | */ 32 | - (void)updateLookup:(JSUpdateLookup *)updateLookup didFailWithError:(NSError *)error; 33 | 34 | @end 35 | 36 | /** 37 | * Class used to gather information about whether an app has an update available on the App Store 38 | */ 39 | @interface JSUpdateLookup : NSObject 40 | 41 | /** 42 | * Will be set to YES if this is the first time an update lookup was performed for this version, 43 | * and a lookup has previously been performed for an earlier version of the same app. 44 | */ 45 | @property (nonatomic, readonly) BOOL isFirstLookupForUpdatedApp; 46 | 47 | /** 48 | * Initialize an update lookup instance with an App ID and a delegate 49 | * 50 | * @param appID The App ID to lookup update info for. 51 | * @param delegate The object acting as a delegate to the update lookup. 52 | * @discussion The lookup will start loading as soon as it has been initialized. 53 | */ 54 | + (instancetype)updateLookupWithAppID:(NSUInteger)appID andDelegate:(id)delegate; 55 | 56 | /** 57 | * Initialize an update lookup instance with an App ID and a completion handler block 58 | * 59 | * @param appID The App ID to lookup update info for. 60 | * @param delegate The completion handler block to run when the lookup has completed. 61 | * @discussion The lookup will start loading as soon as it has been initialized. 62 | * The completion handler will be called whether or not the lookup was successful. 63 | */ 64 | + (instancetype)updateLookupWithAppID:(NSUInteger)appID andCompletionHandler:(JSUpdateLookupCompletionHandler)completionHandler; 65 | 66 | /** 67 | * Schedule that an update lookup should be performed when the app becomes active 68 | * (not by launching), after the set interval has passed 69 | * 70 | * @discussion Once the update has been successfully performed, a new one will be 71 | * scheduled using the same interval. 72 | * This is useful for performing periodic update checks. 73 | * The scheduled lookup will be cancelled if the app is terminated. 74 | */ 75 | - (void)scheduleLookupWithInterval:(NSTimeInterval)interval; 76 | 77 | /** 78 | * Cancel a previously scheduled update lookup 79 | */ 80 | - (void)cancelScheduledLookup; 81 | 82 | @end 83 | 84 | /** 85 | * Class containing update info about an app 86 | */ 87 | @interface JSUpdateInfo : NSObject; 88 | 89 | /** 90 | * Initialize an info object with a dictionary containing data from the App Store web service 91 | * 92 | * @discussion JSUpdateLookup will create a JSUpdateInfo instance for you. 93 | */ 94 | - (instancetype)initWithAppStoreInfo:(NSDictionary *)appStoreInfo; 95 | 96 | /** 97 | * The current version of this app (equivalent to CFBundleShortVersionString) 98 | */ 99 | @property (nonatomic, strong, readonly) NSString *currentAppVersion; 100 | 101 | /** 102 | * The latest version available on the App Store 103 | */ 104 | @property (nonatomic, strong, readonly) NSString *latestAppVersion; 105 | 106 | /** 107 | * Whether or not an update is available for this app 108 | */ 109 | @property (nonatomic, readonly) BOOL updateAvailable; 110 | 111 | /** 112 | * The latest release notes for the app 113 | */ 114 | @property (nonatomic, strong, readonly) NSString *releaseNotes; 115 | 116 | @end -------------------------------------------------------------------------------- /JSUpdateLookup.m: -------------------------------------------------------------------------------- 1 | // 2 | // JSUpdateLookup 3 | // Copyright (c) 2013 John Sundell 4 | // 5 | 6 | #import "JSUpdateLookup.h" 7 | 8 | @interface JSUpdateLookup() 9 | 10 | @property (nonatomic, readwrite) BOOL isFirstLookupForUpdatedApp; 11 | @property (nonatomic) NSUInteger appID; 12 | @property (nonatomic, weak) id delegate; 13 | @property (nonatomic, copy) JSUpdateLookupCompletionHandler completionHandler; 14 | @property (nonatomic, strong) NSURLConnection *connection; 15 | @property (nonatomic, strong) NSMutableData *connectionData; 16 | @property (nonatomic) NSTimeInterval scheduledLookupInterval; 17 | @property (nonatomic, strong) NSDate *nextScheduledLookupDate; 18 | 19 | @end 20 | 21 | @implementation JSUpdateLookup 22 | 23 | #pragma mark - Public class methods 24 | 25 | + (instancetype)updateLookupWithAppID:(NSUInteger)appID andDelegate:(id)delegate 26 | { 27 | JSUpdateLookup *updateLookup = [[JSUpdateLookup alloc] init]; 28 | updateLookup.appID = appID; 29 | updateLookup.delegate = delegate; 30 | [updateLookup start]; 31 | 32 | return updateLookup; 33 | } 34 | 35 | + (instancetype)updateLookupWithAppID:(NSUInteger)appID andCompletionHandler:(JSUpdateLookupCompletionHandler)completionHandler 36 | { 37 | JSUpdateLookup *updateLookup = [[JSUpdateLookup alloc] init]; 38 | updateLookup.appID = appID; 39 | updateLookup.completionHandler = completionHandler; 40 | [updateLookup start]; 41 | 42 | return updateLookup; 43 | } 44 | 45 | #pragma mark - Public instance methods 46 | 47 | - (void)scheduleLookupWithInterval:(NSTimeInterval)interval 48 | { 49 | if (!self.nextScheduledLookupDate) { 50 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; 51 | } 52 | 53 | self.scheduledLookupInterval = interval; 54 | [self setDateForNextScheduledLookup]; 55 | } 56 | 57 | - (void)cancelScheduledLookup 58 | { 59 | self.scheduledLookupInterval = 0; 60 | self.nextScheduledLookupDate = nil; 61 | 62 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 63 | } 64 | 65 | - (void)dealloc 66 | { 67 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 68 | } 69 | 70 | #pragma mark - Private instance methods 71 | 72 | - (void)start 73 | { 74 | NSString *requestURL = [NSString stringWithFormat:@"https://itunes.apple.com/lookup?id=%u",self.appID]; 75 | NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:requestURL]]; 76 | self.connection = [NSURLConnection connectionWithRequest:request delegate:self]; 77 | } 78 | 79 | - (void)cleanupConnection 80 | { 81 | self.connection = nil; 82 | self.connectionData = nil; 83 | } 84 | 85 | - (void)setDateForNextScheduledLookup 86 | { 87 | if (self.scheduledLookupInterval > 0) { 88 | self.nextScheduledLookupDate = [[NSDate date] dateByAddingTimeInterval:self.scheduledLookupInterval]; 89 | } 90 | } 91 | 92 | - (void)appDidBecomeActive 93 | { 94 | if ([self.nextScheduledLookupDate timeIntervalSinceNow] < -self.scheduledLookupInterval) { 95 | [self start]; 96 | } 97 | } 98 | 99 | #pragma mark - NSURLConnectionDelegate 100 | 101 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 102 | { 103 | [self cleanupConnection]; 104 | 105 | if (self.delegate) { 106 | return [self.delegate updateLookup:self didFailWithError:error]; 107 | } 108 | 109 | self.completionHandler(nil, error); 110 | } 111 | 112 | #pragma mark - NSURLConnectionDataDelegate 113 | 114 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 115 | { 116 | self.connectionData = [[NSMutableData alloc] init]; 117 | } 118 | 119 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 120 | { 121 | [self.connectionData appendData:data]; 122 | } 123 | 124 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection 125 | { 126 | NSDictionary *appStoreInfo = [NSJSONSerialization JSONObjectWithData:self.connectionData options:0 error:nil]; 127 | 128 | if (appStoreInfo) { 129 | JSUpdateInfo *updateInfo = [[JSUpdateInfo alloc] initWithAppStoreInfo:appStoreInfo]; 130 | 131 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 132 | NSString *lastLookupVersionStorageKey = @"JSVersionLookup_LastLookupVersion"; 133 | NSString *lastLookupVersionString = [userDefaults objectForKey:lastLookupVersionStorageKey]; 134 | 135 | self.isFirstLookupForUpdatedApp = lastLookupVersionString && ![lastLookupVersionString isEqualToString:updateInfo.currentAppVersion] && !updateInfo.updateAvailable; 136 | 137 | [userDefaults setObject:updateInfo.currentAppVersion forKey:lastLookupVersionStorageKey]; 138 | [userDefaults synchronize]; 139 | 140 | if(self.delegate) 141 | { 142 | [self.delegate updateLookup:self didFinishWithInfo:updateInfo]; 143 | } 144 | else 145 | { 146 | self.completionHandler(updateInfo, nil); 147 | } 148 | 149 | [self setDateForNextScheduledLookup]; 150 | } else { 151 | NSError *error = [NSError errorWithDomain:@"JSUpdateLookup" code:0 userInfo:@{@"error":@"Could not read data from App Store Web Service"}]; 152 | 153 | if (self.delegate) { 154 | [self.delegate updateLookup:self didFailWithError:error]; 155 | } else { 156 | self.completionHandler(nil, error); 157 | } 158 | } 159 | 160 | [self cleanupConnection]; 161 | } 162 | 163 | @end 164 | 165 | @implementation JSUpdateInfo 166 | 167 | - (instancetype)initWithAppStoreInfo:(NSDictionary *)appStoreInfo 168 | { 169 | if (!(self = [super init])) { 170 | return nil; 171 | } 172 | 173 | NSString *currentVersionString = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; 174 | NSString *latestVersionString = [[[appStoreInfo objectForKey:@"results"] firstObject] objectForKey:@"version"]; 175 | 176 | _currentAppVersion = currentVersionString; 177 | _latestAppVersion = latestVersionString; 178 | _updateAvailable = ![currentVersionString isEqualToString:latestVersionString]; 179 | _releaseNotes = [[[appStoreInfo objectForKey:@"results"] firstObject] objectForKey:@"releaseNotes"]; 180 | 181 | return self; 182 | } 183 | 184 | - (NSString *)description 185 | { 186 | return [NSString stringWithFormat:@"%@\nCurrent app version: %@\nLatest App Version: %@\nUpdate available: %u\nRelease notes:%@", [super description], self.currentAppVersion, self.latestAppVersion, self.updateAvailable, self.releaseNotes]; 187 | } 188 | 189 | @end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 John Sundell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSUpdateLookup 2 | ============== 3 | 4 | #### Don't be shy, tell your customers about your awesome new features! 5 | 6 | * Easily check if your iOS App Store app has an update available. 7 | * Uses the iTunes Store Web Service. 8 | * Get info about latest version and most recent release notes. 9 | * Check for first launch of a recently updated app (useful for iOS7's auto updates). 10 | 11 | #### Here's how to use JSUpdateLookup: 12 | 13 | ##### 1. Create a property for the lookup 14 | 15 | ```objective-c 16 | @property (nonatomic, strong) JSUpdateLookup *updateLookup; 17 | ``` 18 | 19 | ##### 2. Get your App ID from iTunes Connect 20 | 21 | ```objective-c 22 | NSUInteger myAppID = 012345678; 23 | ``` 24 | 25 | ##### 3. Initialize a lookup with either a delegate or a block 26 | 27 | ```objective-c 28 | self.updateLookup = [JSUpdateLookup updateLookupWithAppID:myAppID andDelegate:self]; 29 | ``` 30 | 31 | or 32 | 33 | ```objective-c 34 | self.updateLookup = [JSUpdateLookup updateLookupWithAppID:myAppID andCompletionHandler:^(JSUpdateInfo *updateInfo, NSError *error) { 35 | // Do stuff 36 | }]; 37 | ``` 38 | 39 | ##### 4. If you're using a delegate, implement the JSUpdateLookupDelegate protocol 40 | 41 | ```objective-c 42 | - (void)updateLookup:(JSUpdateLookup *)updateLookup didFailWithError:(NSError *)error 43 | { 44 | // Handle error 45 | } 46 | 47 | - (void)updateLookup:(JSUpdateLookup *)updateLookup didFinishWithInfo:(JSUpdateInfo *)updateInfo 48 | { 49 | // Do stuff 50 | } 51 | ``` 52 | 53 | ##### 5. The JSUpdateInfo object passed to your delegate or completion handler will contain the update info you need 54 | 55 | * Whether an app update is available 56 | * The current version of the app 57 | * The latest version available on the App Store 58 | * Most recent release notes 59 | 60 | Use this info to display an update prompt with the latest version & most recent release notes to the user. 61 | 62 | ##### Optionally: Schedule a periodic lookup each time the app becomes active 63 | 64 | JSUpdateLookup provides an easy way to periodically check for updates, as soon as the app becomes active. 65 | Just use the following method: 66 | 67 | ```objective-c 68 | [self.updateLookup scheduleLookupWithInterval:24 * 60 * 60]; 69 | ``` 70 | 71 | The above usage will check for updates each day (provided that the app is used). 72 | 73 | #### Hope that you'll enjoy using JSUpdateLookup! 74 | 75 | Why not give me a shout on Twitter: [@johnsundell](https://twitter.com/johnsundell) --------------------------------------------------------------------------------