170

I would like to manually check if there are new updates for my app while the user is in it, and prompt him to download the new version. Can I do this by checking the version of my app in the app store - programatically?

emotality
  • 12,795
  • 4
  • 39
  • 60
user542584
  • 2,651
  • 2
  • 25
  • 29
  • 11
    You could put a random page on a web-server which only returns a string representation of the latest version. Download it and compare upon app startup and notify the user. (Quick and easy way) – Pangolin Jun 06 '11 at 19:06
  • 1
    thanks, but I was hoping for a better solution like some sort of API with which I can call the app store functionalities, like search for my app number and get the version data. Saves time to maintain a webserver just for this purpose, but thanks for the pointer anyway! – user542584 Jun 06 '11 at 19:19
  • I do the same thing as the first comment. I wrote a plist with one entry: an `NSNumber` version number. Then I uploaded it to my website. The same website I use for my app support and app webpages, then in `viewDidLoad`, I check the website for the version number there and I check the current version in my app. Then I have a premade `alertView` that automatically prompts to update the app. I can provide code if you would like. – Andrew Jun 06 '11 at 20:42
  • thanks, I guess I should try that too.. – user542584 Jun 07 '11 at 09:03
  • 2
    I have implemented a solution using Google Firebase. I use remoteConfig to hold a value of the required version and when the app opens I cross check the version of the app with the version which is set to the Firebase. If the version of the app is smaller than the version of the Firebase I show the user an alert. This way I can have on demand force update of the application. – Stefanos Christodoulides May 18 '21 at 09:39
  • Better to use Swift native type `OperatingSystemVersion` [Compare app versions after update using decimals like 2.5.2](https://stackoverflow.com/a/70964516/2303865) – Leo Dabus Mar 09 '22 at 13:27

30 Answers30

109

Here is a simple code snippet that lets you know if the current version is different

-(BOOL) needsUpdate{
    NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString* appID = infoDictionary[@"CFBundleIdentifier"];
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://itunes.apple.com/lookup?bundleId=%@", appID]];
    NSData* data = [NSData dataWithContentsOfURL:url];
    NSDictionary* lookup = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

    if ([lookup[@"resultCount"] integerValue] == 1){
        NSString* appStoreVersion = lookup[@"results"][0][@"version"];
        NSString* currentVersion = infoDictionary[@"CFBundleShortVersionString"];
        if (![appStoreVersion isEqualToString:currentVersion]){
            NSLog(@"Need to update [%@ != %@]", appStoreVersion, currentVersion);
            return YES;
        }
    }
    return NO;
}

Note: Make sure that when you enter the new version in iTunes, this matches the version in the app you are releasing. If not then the above code will always return YES regardless if the user updates.

datinc
  • 3,404
  • 3
  • 24
  • 33
  • 4
    super solution I ever Found +1 – Sanjay Changani Apr 22 '15 at 11:00
  • This solution is not considered to be best or Super as it has one FLAW in it. e.g. if you have live version 1.0 on store and you want to update new version you submitted 1.1 so if it approves it will work good but in the case if you see 1.1 has some bug or crash or apple rejected it then you need to update to 1.2 so after 1.0 your version on store is 1.2 in this way it always return TRUE and show notify even user downloads the latest version. – Mobeen Afzal Nov 14 '15 at 11:42
  • 1
    @MobeenAfzal, I think you miss understood the question and solution. The above solution compares the current version with the version on the store. If they do not match then it retunes YES, else it returns NO. No matter the history on the app store the above method will return YES if the current version is different then the app store version. Once the user updates... the current version is equal to the app store version. The above method should always return YES if the user's version is 1.0 and the app store version is 1.2. – datinc Nov 16 '15 at 03:20
  • @datinc yes, obviously it always return yes and that is what the problem is.The problem is you push the new version with CFBundleversion 1.7 from code and you have in itunes like 1.6 version. It means according to itunes your version is latest and according to code you already use 1.6 so you cannot upload it will be redundant binary and on itunes you have to made 1.6 because past was 1.5. so in this case even latest version is live but it always return YES YES and all the logic based on YES will always stay. – Mobeen Afzal Nov 17 '15 at 07:33
  • 1
    @MobeenAfzal I think I get what you are seeing. In code your version is 1.7, But in iTunes you uploaded the version as 1.6 so that your users don't know you skipped a version. Is that the case? If so then... what you need is a server (DropBox would do) to serve your apps version number and modify your code to access that endpoint. Let me know if this is what you are seeing and I will add a note of warning to the post. – datinc Nov 17 '15 at 15:39
  • @datinc yes I know for that i need to implement Proper version controlling.Your solution will work best if the versions will always be 1 time greater then the previous version. if the gap increases from 1 it will stuck to true always. :) Cheers. – Mobeen Afzal Nov 21 '15 at 21:01
  • 1
    @MobeenAfzal you your comment is misleading. If the version on the user's device is separated by any from the version on the appstore then the code will return YES as expected. Even if you release version 1.0 followed by version 1.111 it would still work perfectly. – datinc Nov 23 '15 at 19:26
  • @datinc I know it will return yes. Itunes version 1.1 Code version 1.3 It always Return TRUE, because bundle version will be 1.1 and in code it will be 1.3 it can never be synced and get true and all the logic on that functionality will always APPEAR even if no more version is available to download from appstore. I hope I it is clear now – Mobeen Afzal Nov 24 '15 at 20:18
  • I tried to open the destination url with a common bundld id in chrome for mac, it said "errorMessage":"Invalid value(s) for key(s): [country]", "queryParameters":{"output":"json", "callback":"A javascript function to handle your search results", "country":"ISO-2A country code", "limit":"The number of search results to return", "term":"A search string", "lang":"ISO-2A language code"}" – Itachi Mar 23 '16 at 03:38
  • 3
    We should show update only when appstore version is greater than current version as follows. if ([appStoreVersion compare:currentVersion options:NSNumericSearch] == NSOrderedDescending) { NSLog(@"\n\nNeed to update. Appstore version %@ is greater than %@",appStoreVersion, currentVersion); } – Nitesh Borad Mar 01 '17 at 12:49
  • 1
    This answer makes its request synchronously. This means with a bad connection, your app could be unusable for minutes until the request returns. – uliwitness Mar 03 '17 at 10:30
  • super solution +1 – Pramod Oct 31 '17 at 13:40
  • please use compare function otherwise when user upload app.apple tested time display update alertview or apple reject your app – Jigar Feb 15 '18 at 05:41
  • Do you know what happened if AppStore get info with old version of app? I already clean cache but no one result. in JSON file new version but Apple give old version by key [result][0][version] – Genevios Jul 02 '21 at 05:29
  • Please ensure we are handling offline mode(no internet). Otherwise this will cause app crashes. – Ayaz Rafai Dec 16 '22 at 09:16
89

Swift 3 version:

func isUpdateAvailable() throws -> Bool {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(identifier)") else {
        throw VersionError.invalidBundleInfo
    }
    let data = try Data(contentsOf: url)
    guard let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] else {
        throw VersionError.invalidResponse
    }
    if let result = (json["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String {
        return version != currentVersion
    }
    throw VersionError.invalidResponse
}

I think is better to throw an error instead of returning false, in this case I created a VersionError but it can be some other you define or NSError

enum VersionError: Error {
    case invalidResponse, invalidBundleInfo
}

Also consider to call this function from another thread, if the connection is slow it can block the current thread.

DispatchQueue.global().async {
    do {
        let update = try self.isUpdateAvailable()
        DispatchQueue.main.async {
            // show alert
        }
    } catch {
        print(error)
    }
}

Update

Using URLSession:

Instead of using Data(contentsOf: url) and block a thread, we can use URLSession:

func isUpdateAvailable(completion: @escaping (Bool?, Error?) -> Void) throws -> URLSessionDataTask {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(identifier)") else {
            throw VersionError.invalidBundleInfo
    }
    Log.debug(currentVersion)
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        do {
            if let error = error { throw error }
            guard let data = data else { throw VersionError.invalidResponse }
            let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any]
            guard let result = (json?["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String else {
                throw VersionError.invalidResponse
            }
            completion(version != currentVersion, nil)
        } catch {
            completion(nil, error)
        }
    }
    task.resume()
    return task
}

example:

_ = try? isUpdateAvailable { (update, error) in
    if let error = error {
        print(error)
    } else if let update = update {
        print(update)
    }
}
mylogon
  • 2,772
  • 2
  • 28
  • 42
juanjo
  • 3,737
  • 3
  • 39
  • 44
  • 1
    This answer makes its request synchronously. This means with a bad connection, your app could be unusable for minutes until the request returns. – uliwitness Mar 03 '17 at 10:28
  • 7
    I disagree, `DispatchQueue.global()` gives you a background queue, the data is loaded in that queue and only goes back to the main queue when the data is loaded. – juanjo Mar 03 '17 at 15:22
  • Whoops. Somehow I overlooked that second code snippet. Sadly, it seems I can't remove the downvote until your answer is edited again :-( BTW - Given dataWithContentsOfURL: actually goes through NSURLConnection's synchronous calls, which in turn just start an async thread and block, it'd probably be less overhead to just use the asynchronous NSURLSession calls. They'd even call you back on the main thread once you're done. – uliwitness Mar 04 '17 at 12:34
  • @juanjo,,,, not working for swift 3.0.1 , please can u upload updated for swift ??? – Kiran Jadhav Jul 21 '17 at 05:54
  • Is working for me with swift 3.1, what is the error? – juanjo Jul 21 '17 at 18:54
  • 8
    Note if you are only listed in a specific store I have found that you need to add a country code to the URL - eg GB https://itunes.apple.com/\(countryCode)/lookup?bundleId=\(identifier) – Ryan Heitner Sep 05 '17 at 08:41
  • @juanjo your code is working. But I have one problem when the update is available I want to redirect it to loginVC How can I do that – Muju Jan 14 '20 at 07:28
  • Hi, that depends on the structure of your controllers, I recommend you to ask another question to solve that and change the code inside the isUpdateAvailable completion closure – juanjo Jan 15 '20 at 00:12
  • I am facing a very weird issue. If i use the http version of the URL, it is returning me previous version which was uploaded on the App Store. However, if i use the https version of the url, the version is correct. Does any one know what might be the reason ? – Awais Fayyaz Dec 22 '21 at 07:25
  • Looks like this code returns only first two numbers of app version, for example: If its a 1.2.3 version on the App Store, the code will return just 1.2. Is there a way to get the last number too? Tnx. – stackich Dec 28 '21 at 13:16
  • 1
    Instead of using `(Bool?, Error?) -> Void` which is confusing to handle I would recommend using `(Result) → Void` instead. therefore we don't even need to use "throws" – SwiftiSwift Aug 02 '22 at 14:31
  • Everyone whose app is listed for specific countries please check @RyanHeitner 's comment. I was unable to get the code working until I checked his comment. – S.S.D Oct 06 '22 at 03:00
  • @SwiftiSwift `Result` is indeed better. Also `throws` was never needed and it's bad API design to have two methods of passing an error. – meaning-matters Jun 12 '23 at 17:00
  • `isXyz` is used for boolean. Because this is a function, which even gives its result asynchronously & via a completion block, `checkIfUpdateAvailable` would be a better name. – meaning-matters Jun 12 '23 at 17:04
29

Simplified a great answer posted on this thread. Using Swift 4 and Alamofire.

import Alamofire

class VersionCheck {
  
  public static let shared = VersionCheck()
  
  func isUpdateAvailable(callback: @escaping (Bool)->Void) {
    let bundleId = Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String
    Alamofire.request("https://itunes.apple.com/lookup?bundleId=\(bundleId)").responseJSON { response in
      if let json = response.result.value as? NSDictionary, let results = json["results"] as? NSArray, let entry = results.firstObject as? NSDictionary, let versionStore = entry["version"] as? String, let versionLocal = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
        let arrayStore = versionStore.split(separator: ".").compactMap { Int($0) }
        let arrayLocal = versionLocal.split(separator: ".").compactMap { Int($0) }

        if arrayLocal.count != arrayStore.count {
          callback(true) // different versioning system
          return
        }

        // check each segment of the version
        for (localSegment, storeSegment) in zip(arrayLocal, arrayStore) {
          if localSegment < storeSegment {
            callback(true)
            return
          }
        }
      }
      callback(false) // no new version or failed to fetch app store version
    }
  }
  
}

And then to use it:

VersionCheck.shared.isUpdateAvailable() { hasUpdates in
  print("is update available: \(hasUpdates)")
}
budiDino
  • 13,044
  • 8
  • 95
  • 91
  • 5
    My application is live on store but same api not returning version information. Response :`{ "resultCount":0, "results": [] }` – technerd Jul 24 '19 at 10:16
  • Just adding a note to version comparision, I would prefer, let serverVersion = "2.7" let localVersion = "2.6.5" let isUpdateAvailable = serverVersion.compare(localVersion, options: .numeric) == .orderedDescending rather than replacing the . with empty. – Chaitu Mar 05 '20 at 11:50
  • @Chaitu thank you for the suggestion. I ended up rewriting the comparison part of the code – budiDino Mar 06 '20 at 11:16
  • There should be `return` after some callbacks. – Libor Zapletal Nov 25 '21 at 19:58
  • @LiborZapletal thanks. Fixed the issue and also updated the code a bit – budiDino Nov 26 '21 at 10:30
  • pozz @budiDino. Looks like this code returns only first two numbers of app version, for example: If its a 1.2.3 version on the App Store, the code will return just 1.2. Is there a way to get the last number too? Tnx. – stackich Dec 28 '21 at 13:17
  • @stackich did you try checking the "version" returned from the itunes API: `https://itunes.apple.com/lookup?bundleId=\(bundleId)` and also your local version: `Bundle.main.infoDictionary?["CFBundleShortVersionString"]`? Not sure why any of those would return just first two segments if 3 segments exist :/ – budiDino Dec 28 '21 at 18:37
  • @budiDino my fault. I made a mistake putting http instead of https in iTunes url. The http works but somehow returns the previous version of my app which was in my case without 3 segments(only two). Seems like switching to https kinda removes cache and returns the latest app version. Thanks however :) – stackich Dec 29 '21 at 07:57
  • Ensure that the region/country of the App Store that you are targeting is correct. You can check this in your app's settings on App Store Connect. For example, if you want to retrieve app information from the United States App Store, you would replace /in/ with /us/ in the URL, like this: http://itunes.apple.com/us/lookup?bundleId= – Milan Savaliya M May 08 '23 at 06:14
24

Updated the swift 4 code from Anup Gupta

I have made some alterations to this code. Now the functions are called from a background queue, since the connection can be slow and therefore block the main thread.

I also made the CFBundleName optional, since the version presented had "CFBundleDisplayName" which didn't work probably in my version. So now if it's not present it won't crash but just won't display the App Name in the alert.

import UIKit

enum VersionError: Error {
    case invalidBundleInfo, invalidResponse
}

class LookupResult: Decodable {
    var results: [AppInfo]
}

class AppInfo: Decodable {
    var version: String
    var trackViewUrl: String
}

class AppUpdater: NSObject {

    private override init() {}
    static let shared = AppUpdater()

    func showUpdate(withConfirmation: Bool) {
        DispatchQueue.global().async {
            self.checkVersion(force : !withConfirmation)
        }
    }

    private  func checkVersion(force: Bool) {
        let info = Bundle.main.infoDictionary
        if let currentVersion = info?["CFBundleShortVersionString"] as? String {
            _ = getAppInfo { (info, error) in
                if let appStoreAppVersion = info?.version{
                    if let error = error {
                        print("error getting app store version: ", error)
                    } else if appStoreAppVersion == currentVersion {
                        print("Already on the last app version: ",currentVersion)
                    } else {
                        print("Needs update: AppStore Version: \(appStoreAppVersion) > Current version: ",currentVersion)
                        DispatchQueue.main.async {
                            let topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
                            topController.showAppUpdateAlert(Version: (info?.version)!, Force: force, AppURL: (info?.trackViewUrl)!)
                        }
                    }
                }
            }
        }
    }

    private func getAppInfo(completion: @escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
        guard let identifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String,
            let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
                DispatchQueue.main.async {
                    completion(nil, VersionError.invalidBundleInfo)
                }
                return nil
        }
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                if let error = error { throw error }
                guard let data = data else { throw VersionError.invalidResponse }
                let result = try JSONDecoder().decode(LookupResult.self, from: data)
                guard let info = result.results.first else { throw VersionError.invalidResponse }

                completion(info, nil)
            } catch {
                completion(nil, error)
            }
        }
        task.resume()
        return task
    }
}

extension UIViewController {
    @objc fileprivate func showAppUpdateAlert( Version : String, Force: Bool, AppURL: String) {
        let appName = Bundle.appName()

        let alertTitle = "New Version"
        let alertMessage = "\(appName) Version \(Version) is available on AppStore."

        let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)

        if !Force {
            let notNowButton = UIAlertAction(title: "Not Now", style: .default)
            alertController.addAction(notNowButton)
        }

        let updateButton = UIAlertAction(title: "Update", style: .default) { (action:UIAlertAction) in
            guard let url = URL(string: AppURL) else {
                return
            }
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else {
                UIApplication.shared.openURL(url)
            }
        }

        alertController.addAction(updateButton)
        self.present(alertController, animated: true, completion: nil)
    }
}
extension Bundle {
    static func appName() -> String {
        guard let dictionary = Bundle.main.infoDictionary else {
            return ""
        }
        if let version : String = dictionary["CFBundleName"] as? String {
            return version
        } else {
            return ""
        }
    }
}

I make this call for also adding the confirmation button:

AppUpdater.shared.showUpdate(withConfirmation: true)

Or call it to be called like this to have the force update option on:

AppUpdater.shared.showUpdate(withConfirmation: false)
Vasco
  • 837
  • 8
  • 9
  • Any ideas on how to test this? If it fails to work right, the only way to debug it is to somehow debug an older version than is in the app store. – David Rector Jan 29 '19 at 06:00
  • 2
    Ah, never mind the question. I can simply change my local version to be "older". – David Rector Jan 29 '19 at 06:01
  • 1
    I'm impressed with your code @Vasco. Just a simple question, why you have used 'http' instead of https in that url? – Master AgentX Mar 05 '19 at 13:33
  • Thanks a lot for sharing this solution @Vasco! I like it :) Why do you not use: let config = URLSessionConfiguration.background(withIdentifier: "com.example.MyExample.background") for the URLSession to achieve the background request? – mc_plectrum Nov 25 '19 at 05:48
  • You can also get rid of the force unwrap, as you check already if if let appStoreAppVersion = info?.version and same for the trackURL. – mc_plectrum Nov 25 '19 at 06:09
15

Since I was facing the same problem, I found the answer provided by Mario Hendricks. Unfornatelly when I tryed to aply his code on my project, XCode did complain about Casting problems saying "MDLMaterialProperty has no subscript members". His code was trying to set this MDLMaterial... as the type of the constant "lookupResult", making the casting to "Int" failing every single time. My solution was to provide a type annotation for my variable to NSDictionary to be clear about the kind of value I needed. With that, I could access the value "version" that I needed.

Obs: For this YOURBUNDLEID, you can get from your Xcode project.... "Targets > General > Identity > Bundle Identifier"

So here is the my code with some simplifications as well:

func appUpdateAvailable() -> Bool
{
    let storeInfoURL: String = "http://itunes.apple.com/lookup?bundleId=YOURBUNDLEID"
    var upgradeAvailable = false
    // Get the main bundle of the app so that we can determine the app's version number
    let bundle = NSBundle.mainBundle()
    if let infoDictionary = bundle.infoDictionary {
        // The URL for this app on the iTunes store uses the Apple ID for the  This never changes, so it is a constant
        let urlOnAppStore = NSURL(string: storeInfoURL)
        if let dataInJSON = NSData(contentsOfURL: urlOnAppStore!) {
            // Try to deserialize the JSON that we got
            if let dict: NSDictionary = try? NSJSONSerialization.JSONObjectWithData(dataInJSON, options: NSJSONReadingOptions.AllowFragments) as! [String: AnyObject] {
                if let results:NSArray = dict["results"] as? NSArray {
                    if let version = results[0].valueForKey("version") as? String {
                        // Get the version number of the current version installed on device
                        if let currentVersion = infoDictionary["CFBundleShortVersionString"] as? String {
                            // Check if they are the same. If not, an upgrade is available.
                            print("\(version)")
                            if version != currentVersion {
                                upgradeAvailable = true
                            }
                        }
                    }
                }
            }
        }
    }
    return upgradeAvailable
}

All suggestions for improvement of this code are welcome!

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
Yago Zardo
  • 431
  • 5
  • 9
  • 1
    This answer makes its request synchronously. This means with a bad connection, your app could be unusable for minutes until the request returns. – uliwitness Mar 03 '17 at 10:29
  • @Yago Zardo please use compare function otherwise when user upload app.apple tested time display update alertview or apple reject your app – Jigar Feb 15 '18 at 05:40
  • Hey @Jigar, thanks for the advice. I'm currently not using this method anymore on my app because now we are versioning everything in our server. Anyway, could you explain better what you said? I did not understand and it really looks a good thing to know. Thanks in advance. – Yago Zardo Feb 15 '18 at 16:16
  • Thank you @uliwitness for the tip, it really helped me to improve my code in general to learn about asynchronous and synchronous requests. – Yago Zardo Feb 15 '18 at 16:24
  • That link is a gem! – B3none Sep 27 '18 at 14:10
  • 1
    Love that pyramid. (Take a look at using `guard` instead of `if`.) – adamjansch Nov 09 '18 at 13:53
  • Any way to get the optional or force update flag? – Krunal Shah Aug 04 '23 at 12:45
14

Here is my code:

NSString *appInfoUrl = @"http://itunes.apple.com/lookup?bundleId=XXXXXXXXX";

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:[NSURL URLWithString:appInfoUrl]];
[request setHTTPMethod:@"GET"];

NSURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection  sendSynchronousRequest:request returningResponse: &response error: &error];
NSString *output = [NSString stringWithCString:[data bytes] length:[data length]];

NSError *e = nil;
NSData *jsonData = [output dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error: &e];

NSString *version = [[[jsonDict objectForKey:@"results"] objectAtIndex:0] objectForKey:@"version"];
Marin Bînzari
  • 5,208
  • 2
  • 26
  • 43
Roozbeh Zabihollahi
  • 7,207
  • 45
  • 39
  • 1
    very good and correct solution, just little update regarding the url is http://itunes.apple.com/en/lookup?bundleId=xxxxxxxxxx – S.J Mar 26 '14 at 07:13
  • 5
    Actually it didn't work for me with the `/en/` subpath. After removing it, it worked – gasparuff Jun 30 '16 at 09:08
  • This answer makes its request synchronously. This means with a bad connection, your app could be unusable for minutes until the request returns. – uliwitness Mar 03 '17 at 10:29
  • 1
    I had to use with the /en/ https://itunes.apple.com/lookup?bundleId=xxxxxxx, thanks @gasparuff – Fernando Perez Jan 22 '20 at 16:03
14

Just use ATAppUpdater. It is 1 line, thread-safe and fast. It also have delegate methods if you would like to track user action.

Here is an example:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[ATAppUpdater sharedUpdater] showUpdateWithConfirmation]; // 1 line of code
    // or
    [[ATAppUpdater sharedUpdater] showUpdateWithForce]; // 1 line of code

   return YES;
}

Optional delegate methods:

- (void)appUpdaterDidShowUpdateDialog;
- (void)appUpdaterUserDidLaunchAppStore;
- (void)appUpdaterUserDidCancel;
emotality
  • 12,795
  • 4
  • 39
  • 60
13

Here is my version using Swift 4 and popular Alamofire library (I use it in my apps anyway). Request is asynchronous and you can pass a callback to be notified when done.

import Alamofire

class VersionCheck {

    public static let shared = VersionCheck()

    var newVersionAvailable: Bool?
    var appStoreVersion: String?

    func checkAppStore(callback: ((_ versionAvailable: Bool?, _ version: String?)->Void)? = nil) {
        let ourBundleId = Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String
        Alamofire.request("https://itunes.apple.com/lookup?bundleId=\(ourBundleId)").responseJSON { response in
            var isNew: Bool?
            var versionStr: String?

            if let json = response.result.value as? NSDictionary,
               let results = json["results"] as? NSArray,
               let entry = results.firstObject as? NSDictionary,
               let appVersion = entry["version"] as? String,
               let ourVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
            {
                isNew = ourVersion != appVersion
                versionStr = appVersion
            }

            self.appStoreVersion = versionStr
            self.newVersionAvailable = isNew
            callback?(isNew, versionStr)
        }
    }
}

Usage is simple like this:

VersionCheck.shared.checkAppStore() { isNew, version in
        print("IS NEW VERSION AVAILABLE: \(isNew), APP STORE VERSION: \(version)")
    }
Northern Captain
  • 1,147
  • 3
  • 25
  • 32
  • 1
    problem with using ourVersion != appVersion is that it triggers when the App Store Review team checks the new version of the app. We convert those version strings to numbers and then isNew = appVersion > ourVersion. – budiDino Jun 05 '19 at 16:43
  • @budidino you are right, I just showed the common approach using Alamofire. How you interpret the version is totally dependent on your app and version structure. – Northern Captain Jun 07 '19 at 16:22
  • Just adding a note to version comparision, I would prefer, let serverVersion = "2.7" let localVersion = "2.6.5" let isUpdateAvailable = serverVersion.compare(localVersion, options: .numeric) == .orderedDescending rather than comparing with equal – Chaitu Mar 05 '20 at 11:51
12

Swift 5 (cache issue resolved)

enum VersionError: Error {
    case invalidResponse, invalidBundleInfo
}

@discardableResult
func isUpdateAvailable(completion: @escaping (Bool?, Error?) -> Void) throws -> URLSessionDataTask {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
            throw VersionError.invalidBundleInfo
    }
        
    let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData)
    
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        do {
            if let error = error { throw error }
            
            guard let data = data else { throw VersionError.invalidResponse }
                        
            let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any]
                        
            guard let result = (json?["results"] as? [Any])?.first as? [String: Any], let lastVersion = result["version"] as? String else {
                throw VersionError.invalidResponse
            }
            completion(lastVersion > currentVersion, nil)
        } catch {
            completion(nil, error)
        }
    }
    
    task.resume()
    return task
}

Implementation

            try? isUpdateAvailable {[self] (update, error) in
                if let error = error {
                    print(error)
                } else if update ?? false {
                    // show alert
                }
            }
Aloha
  • 129
  • 1
  • 5
7

Swift 3.1

func needsUpdate() -> Bool {
    let infoDictionary = Bundle.main.infoDictionary
    let appID = infoDictionary!["CFBundleIdentifier"] as! String
    let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(appID)")
    guard let data = try? Data(contentsOf: url) else {
      print("There is an error!")
      return false;
    }
    let lookup = (try? JSONSerialization.jsonObject(with: data! , options: [])) as? [String: Any]
    if let resultCount = lookup!["resultCount"] as? Int, resultCount == 1 {
        if let results = lookup!["results"] as? [[String:Any]] {
            if let appStoreVersion = results[0]["version"] as? String{
                let currentVersion = infoDictionary!["CFBundleShortVersionString"] as? String
                if !(appStoreVersion == currentVersion) {
                    print("Need to update [\(appStoreVersion) != \(currentVersion)]")
                    return true
                }
            }
        }
    }
    return false
}
Kassem Itani
  • 1,057
  • 10
  • 15
7

I saw many ways to check App update. so based on many answers I mix them and create my solution which is available on GitHub If Any update required Please let me know. This code for Swift 4

GitHub link To this code. https://github.com/anupgupta-arg/iOS-Swift-ArgAppUpdater

   import UIKit

enum VersionError: Error {
    case invalidBundleInfo, invalidResponse
}

class LookupResult: Decodable {
    var results: [AppInfo]
}

class AppInfo: Decodable {
    var version: String
    var trackViewUrl: String
    //let identifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String,
    // You can add many thing based on "http://itunes.apple.com/lookup?bundleId=\(identifier)"  response
    // here version and trackViewUrl are key of URL response
    // so you can add all key beased on your requirement.

}

class ArgAppUpdater: NSObject {
    private static var _instance: ArgAppUpdater?;

    private override init() {

    }

    public static func getSingleton() -> ArgAppUpdater {
        if (ArgAppUpdater._instance == nil) {
            ArgAppUpdater._instance = ArgAppUpdater.init();
        }
        return ArgAppUpdater._instance!;
    }

    private func getAppInfo(completion: @escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
        guard let identifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String,
            let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
                DispatchQueue.main.async {
                    completion(nil, VersionError.invalidBundleInfo)
                }
                return nil
        }
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                if let error = error { throw error }
                guard let data = data else { throw VersionError.invalidResponse }

                print("Data:::",data)
                print("response###",response!)

                let result = try JSONDecoder().decode(LookupResult.self, from: data)

                let dictionary = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves)

                print("dictionary",dictionary!)


                guard let info = result.results.first else { throw VersionError.invalidResponse }
                print("result:::",result)
                completion(info, nil)
            } catch {
                completion(nil, error)
            }
        }
        task.resume()

        print("task ******", task)
        return task
    }
    private  func checkVersion(force: Bool) {
        let info = Bundle.main.infoDictionary
        let currentVersion = info?["CFBundleShortVersionString"] as? String
        _ = getAppInfo { (info, error) in

            let appStoreAppVersion = info?.version

            if let error = error {
                print(error)



            }else if appStoreAppVersion!.compare(currentVersion!, options: .numeric) == .orderedDescending {
                //                print("needs update")
               // print("hiiii")
                DispatchQueue.main.async {
                    let topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!

                    topController.showAppUpdateAlert(Version: (info?.version)!, Force: force, AppURL: (info?.trackViewUrl)!)
            }

            }
        }


    }

    func showUpdateWithConfirmation() {
        checkVersion(force : false)


    }

    func showUpdateWithForce() {
        checkVersion(force : true)
    }



}

extension UIViewController {


    fileprivate func showAppUpdateAlert( Version : String, Force: Bool, AppURL: String) {
        print("AppURL:::::",AppURL)

        let bundleName = Bundle.main.infoDictionary!["CFBundleDisplayName"] as! String;
        let alertMessage = "\(bundleName) Version \(Version) is available on AppStore."
        let alertTitle = "New Version"


        let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)


        if !Force {
            let notNowButton = UIAlertAction(title: "Not Now", style: .default) { (action:UIAlertAction) in
                print("Don't Call API");


            }
            alertController.addAction(notNowButton)
        }

        let updateButton = UIAlertAction(title: "Update", style: .default) { (action:UIAlertAction) in
            print("Call API");
            print("No update")
            guard let url = URL(string: AppURL) else {
                return
            }
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else {
                UIApplication.shared.openURL(url)
            }

        }

        alertController.addAction(updateButton)
        self.present(alertController, animated: true, completion: nil)
    }
}

Refrence : https://stackoverflow.com/a/48810541/5855888 And https://github.com/emotality/ATAppUpdater

Happy Coding

Anup Gupta
  • 1,993
  • 2
  • 25
  • 40
7

Can I suggest this little library: https://github.com/nicklockwood/iVersion

Its purpose is to simplify the handling of remote plists to trigger notifications.

Nick Lockwood
  • 40,865
  • 11
  • 112
  • 103
Andrea
  • 638
  • 9
  • 20
  • 3
    You can check the App Store directly for Version number instead of hosting a plist file somewhere. Check out this answer: http://stackoverflow.com/a/6569307/142358 – Steve Moser Feb 10 '12 at 14:41
  • 1
    iVersion now uses the app store version automatically - the Plist is optional if you want to specify different release notes to the ones on iTunes, but you don't need to use it. – Nick Lockwood Mar 19 '12 at 02:17
  • 1
    This code could use some improvements, but is a lot better than the other answers that send a synchronous request. Still, the way it does threading is bad style. I'll file issues on Github. – uliwitness Mar 03 '17 at 10:38
  • The project is now deprecated – Zorayr Jul 22 '20 at 23:52
6

This answer is modification to datinc's answer https://stackoverflow.com/a/25210143/2735358.

datinc's funtion compares version by string comparison. So, it will not compare version for greater than or less than.

But, this modified function compares version by NSNumericSearch (numeric comparison).

- (void)checkForUpdateWithHandler:(void(^)(BOOL isUpdateAvailable))updateHandler {

    NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString *appID = infoDictionary[@"CFBundleIdentifier"];
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://itunes.apple.com/lookup?bundleId=%@", appID]];
    NSLog(@"iTunes Lookup URL for the app: %@", url.absoluteString);

    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *theTask = [session dataTaskWithRequest:[NSURLRequest requestWithURL:url]
                                               completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

                                                   NSDictionary *lookup = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
                                                   NSLog(@"iTunes Lookup Data: %@", lookup);
                                                   if (lookup && [lookup[@"resultCount"] integerValue] == 1){
                                                       NSString *appStoreVersion = lookup[@"results"][0][@"version"];
                                                       NSString *currentVersion = infoDictionary[@"CFBundleShortVersionString"];

                                                       BOOL isUpdateAvailable = [appStoreVersion compare:currentVersion options:NSNumericSearch] == NSOrderedDescending;
                                                       if (isUpdateAvailable) {
                                                           NSLog(@"\n\nNeed to update. Appstore version %@ is greater than %@",appStoreVersion, currentVersion);
                                                       }
                                                       if (updateHandler) {
                                                           updateHandler(isUpdateAvailable);
                                                       }
                                                   }
                                               }];
    [theTask resume];
}

Use:

[self checkForUpdateWithHandler:^(BOOL isUpdateAvailable) {
    if (isUpdateAvailable) {
        // show alert
    }
}];
Community
  • 1
  • 1
Nitesh Borad
  • 4,583
  • 36
  • 51
5

Coming From a Hybrid Application POV, this is a javascript example, I have a Update Available footer on my main menu. If an update is available (ie. my version number within the config file is less than the version retrieved, display the footer) This will then direct the user to the app store, where the user can then click the update button.

I also get the whats new data (ie Release Notes) and display these in a modal on login if its the first time on this version.

The Update Available method can be ran as often as you like. Mine is ran every time the user navigates to the home screen.

function isUpdateAvailable() {
        $.ajax('https://itunes.apple.com/lookup?bundleId=BUNDLEID', {
            type: "GET",
            cache: false,
            dataType: 'json'
        }).done(function (data) {
            _isUpdateAvailable(data.results[0]);
        }).fail(function (jqXHR, textStatus, errorThrown) {
            commsErrorHandler(jqXHR, textStatus, false);
        });

}

Callback: Apple have an API, so very easy to get

function isUpdateAvailable_iOS (data) {
    var storeVersion = data.version;
    var releaseNotes = data.releaseNotes;
    // Check store Version Against My App Version ('1.14.3' -> 1143)
    var _storeV = parseInt(storeVersion.replace(/\./g, ''));
    var _appV = parseInt(appVersion.substring(1).replace(/\./g, ''));
    $('#ft-main-menu-btn').off();
    if (_storeV > _appV) {
        // Update Available
        $('#ft-main-menu-btn').text('Update Available');
        $('#ft-main-menu-btn').click(function () {
           // Open Store      
           window.open('https://itunes.apple.com/us/app/appname/idUniqueID', '_system');
        });

    } else {
        $('#ft-main-menu-btn').html('&nbsp;');
        // Release Notes
        settings.updateReleaseNotes('v' + storeVersion, releaseNotes);
    }
}
tyler_mitchell
  • 1,727
  • 1
  • 19
  • 27
5

Try this with a single function call:

func showAppStoreVersionUpdateAlert(isForceUpdate: Bool) {

    do {
        //Get Bundle Identifire from Info.plist
        guard let bundleIdentifire = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String else {
            print("No Bundle Info found.")
            throw CustomError.invalidIdentifires
        }

        // Build App Store URL
        guard let url = URL(string:"http://itunes.apple.com/lookup?bundleId=" + bundleIdentifire) else {
            print("Isse with generating URL.")
            throw CustomError.invalidURL
        }

        let serviceTask = URLSession.shared.dataTask(with: url) { (responseData, response, error) in

            do {
                // Check error
                if let error = error { throw error }
                //Parse response
                guard let data = responseData else { throw CustomError.jsonReading }
                let result = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
                let itunes = ItunesAppInfoItunes.init(fromDictionary: result as! [String : Any])
                print(itunes.results)
                if let itunesResult = itunes.results.first {
                    print("App Store Varsion: ",itunesResult.version)

                    //Get Bundle Version from Info.plist
                    guard let appShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
                        print("No Short Version Info found.")
                        throw CustomError.invalidVersion
                    }

                    if appShortVersion == itunesResult.version {
                        //App Store & Local App Have same Version.
                        print("Same Version at both side")
                    } else {
                        //Show Update alert
                        var message = ""
                        //Get Bundle Version from Info.plist
                        if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
                            message = "\(appName) has new version(\(itunesResult.version!)) available on App Store."
                        } else {
                            message = "This app has new version(\(itunesResult.version!)) available on App Store."
                        }

                        //Show Alert on the main thread
                        DispatchQueue.main.async {
                            self.showUpdateAlert(message: message, appStoreURL: itunesResult.trackViewUrl, isForceUpdate: isForceUpdate)
                        }
                    }
                }
            } catch {
                print(error)
            }
        }
        serviceTask.resume()
    } catch {
        print(error)
    }
}

Alert Function to open AppStore URL:

func showUpdateAlert(message : String, appStoreURL: String, isForceUpdate: Bool) {

    let controller = UIAlertController(title: "New Version", message: message, preferredStyle: .alert)

    //Optional Button
    if !isForceUpdate {
        controller.addAction(UIAlertAction(title: "Later", style: .cancel, handler: { (_) in }))
    }

    controller.addAction(UIAlertAction(title: "Update", style: .default, handler: { (_) in
        guard let url = URL(string: appStoreURL) else {
            return
        }
        if #available(iOS 10.0, *) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
            UIApplication.shared.openURL(url)
        }

    }))

    let applicationDelegate = UIApplication.shared.delegate as? AppDelegate
    applicationDelegate?.window?.rootViewController?.present(controller, animated: true)

}

How to call the above function:

AppStoreUpdate.shared.showAppStoreVersionUpdateAlert(isForceUpdate: false/true)

For more detail try below link with full code:

AppStoreUpdate.swift

ItunesAppInfoResult.swift

ItunesAppInfoItunes.swift

I hope this will helps!

CodeChanger
  • 7,953
  • 5
  • 49
  • 80
4

Swift 4

We can use the new JSONDecoder to parse the response from itunes.apple.com/lookup and represent it with Decodable classes or structs:

class LookupResult: Decodable {
    var results: [AppInfo]
}

class AppInfo: Decodable {
    var version: String
}

We can also add other properties to AppInfo in case we need the releaseNotes or some other property.

Now we can make an async request using URLSession:

func getAppInfo(completion: @escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
    guard let identifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String,
          let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
            DispatchQueue.main.async {
                completion(nil, VersionError.invalidBundleInfo)
            }
            return nil
    }
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        do {
            if let error = error { throw error }
            guard let data = data else { throw VersionError.invalidResponse }
            let result = try JSONDecoder().decode(LookupResult.self, from: data)
            guard let info = result.results.first else { throw VersionError.invalidResponse }

            completion(info, nil)
        } catch {
            completion(nil, error)
        }
    }
    task.resume()
    return task
}

enum VersionError: Error {
    case invalidBundleInfo, invalidResponse
}

this function receives a completion closure that will be called when the request is completed and returns an URLSessionDataTask in case we need to cancel the request, and can be called like this:

func checkVersion() {
    let info = Bundle.main.infoDictionary
    let currentVersion = info?["CFBundleShortVersionString"] as? String
    _ = getAppInfo { (info, error) in
        if let error = error {
            print(error)
        } else if info?.version == currentVersion {
            print("updated")
        } else {
            print("needs update")
        }
    }
}
juanjo
  • 3,737
  • 3
  • 39
  • 44
  • Where did you put this code? II see that you set LookupResult and AppInfo to decodable, but I don't see them saved anywhere. What am I missing here? – jessi Mar 08 '18 at 21:25
  • You declare the `LookupResult` and `AppInfo` classes somewhere in your project, in a separate file preferably: They are used when you decode the response: `JSONDecoder().decode(LookupResult.self, from: data)` and they contain the version string – juanjo Mar 08 '18 at 22:45
  • Based on your answer I create One file using your code Please check that [iOS-Swift-ArgAppUpdater](https://github.com/anupgupta-arg/iOS-Swift-ArgAppUpdater) – Anup Gupta Apr 04 '18 at 11:11
  • @jessi please check My code on GitHub I posted there your solution – Anup Gupta Apr 04 '18 at 11:19
  • I just tested this in swift 5. It works well. I am currious how to know .version is the version available from the App Store (Bundle.main.InfoDictionary)? or how to know the CFBundleVersionString is the current app plist version number? I can't make sense of apple documentation. It would be nice to know if there are other fields that could be used from the App Store, such as what is the description of changes in the new version. That would help the user know if they should update. But thats not in any plist so probably not available.. – Markv07 Mar 27 '21 at 20:29
4

FOR SWIFT 4 and 3.2:

First, we need to get the bundle id from bundle info dictionary, set isUpdaet as false.

    var isUpdate = false
    guard let bundleInfo = Bundle.main.infoDictionary,
        let currentVersion = bundleInfo["CFBundleShortVersionString"] as? String,
        //let identifier = bundleInfo["CFBundleIdentifier"] as? String,
        let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)")
        else{
        print("something wrong")
            completion(false)
        return
       }

Then we need to call a urlSession call for getting version from itunes.

    let task = URLSession.shared.dataTask(with: url) {
        (data, resopnse, error) in
        if error != nil{
             completion(false)
            print("something went wrong")
        }else{
            do{
                guard let reponseJson = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:Any],
                let result = (reponseJson["results"] as? [Any])?.first as? [String: Any],
                let version = result["version"] as? String
                else{
                     completion(false)
                    return
                }
                print("Current Ver:\(currentVersion)")
                print("Prev version:\(version)")
                if currentVersion != version{
                    completion(true)
                }else{
                    completion(false)
                }
            }
            catch{
                 completion(false)
                print("Something went wrong")
            }
        }
    }
    task.resume()

FULL CODE WILL BE LIKE THIS:

func checkForUpdate(completion:@escaping(Bool)->()){

    guard let bundleInfo = Bundle.main.infoDictionary,
        let currentVersion = bundleInfo["CFBundleShortVersionString"] as? String,
        //let identifier = bundleInfo["CFBundleIdentifier"] as? String,
        let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)")
        else{
        print("some thing wrong")
            completion(false)
        return
       }

    let task = URLSession.shared.dataTask(with: url) {
        (data, resopnse, error) in
        if error != nil{
             completion(false)
            print("something went wrong")
        }else{
            do{
                guard let reponseJson = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:Any],
                let result = (reponseJson["results"] as? [Any])?.first as? [String: Any],
                let version = result["version"] as? String
                else{
                     completion(false)
                    return
                }
                print("Current Ver:\(currentVersion)")
                print("Prev version:\(version)")
                if currentVersion != version{
                    completion(true)
                }else{
                    completion(false)
                }
            }
            catch{
                 completion(false)
                print("Something went wrong")
            }
        }
    }
    task.resume()
}

Then we can call the function anyware we need .

    checkForUpdate { (isUpdate) in
        print("Update needed:\(isUpdate)")
        if isUpdate{
            DispatchQueue.main.async {
                print("new update Available")
            }
        }
    }
Sandu
  • 436
  • 4
  • 8
3

Here is a swift method that does what some of the Objective-C answers suggest. Obviously, once you get the info from the app store JSON, you can extract the release notes, if you want them.

func appUpdateAvailable(storeInfoURL: String) -> Bool
{
    var upgradeAvailable = false

    // Get the main bundle of the app so that we can determine the app's version number
    let bundle = NSBundle.mainBundle()
    if let infoDictionary = bundle.infoDictionary {
        // The URL for this app on the iTunes store uses the Apple ID for the  This never changes, so it is a constant
        let urlOnAppStore = NSURL(string: storeInfoURL)
        if let dataInJSON = NSData(contentsOfURL: urlOnAppStore!) {
            // Try to deserialize the JSON that we got
            if let lookupResults = try? NSJSONSerialization.JSONObjectWithData(dataInJSON, options: NSJSONReadingOptions()) {
                // Determine how many results we got. There should be exactly one, but will be zero if the URL was wrong
                if let resultCount = lookupResults["resultCount"] as? Int {
                    if resultCount == 1 {
                        // Get the version number of the version in the App Store
                        if let appStoreVersion = lookupResults["results"]!![0]["version"] as? String {
                            // Get the version number of the current version
                            if let currentVersion = infoDictionary["CFBundleShortVersionString"] as? String {
                                // Check if they are the same. If not, an upgrade is available.
                                if appStoreVersion != currentVersion {
                                    upgradeAvailable = true                      
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return upgradeAvailable
}
Mario Hendricks
  • 727
  • 8
  • 7
3

If you are not setting content type in NSUrlRequest then for sure you wont get response, so try the below code it works fine for me. Hope it helps....

-(BOOL) isUpdateAvailable{
    NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString* appID = infoDictionary[@"CFBundleIdentifier"];
    NSString *urlString = [NSString stringWithFormat:@"https://itunes.apple.com/lookup?bundleId=%@",appID];

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setURL:[NSURL URLWithString:urlString]];
    [request setHTTPMethod:@"GET"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];

    NSURLResponse *response;
    NSError *error;
    NSData *data = [NSURLConnection  sendSynchronousRequest:request returningResponse: &response error: &error];
    NSError *e = nil;
    NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error: &e];

    self.versionInAppStore = [[[jsonDict objectForKey:@"results"] objectAtIndex:0] objectForKey:@"version"];

    self.localAppVersion = infoDictionary[@"CFBundleShortVersionString"];

    if ([self.versionInAppStore compare:self.localAppVersion options:NSNumericSearch] == NSOrderedDescending) {
        // currentVersion is lower than the version
        return YES;
    }
    return NO;
}
ganka
  • 191
  • 1
  • 11
  • This answer makes its request synchronously. This means with a bad connection, your app could be unusable for minutes until the request returns. – uliwitness Mar 03 '17 at 10:28
3

Warning: Most of the answers given retrieve the URL synchronously (using -dataWithContentsOfURL: or -sendSynchronousRequest:. This is bad, as it means that your application will be unresponsive for several minutes if the mobile connection drops while the request is in progress. never do internet access synchronously on the main thread.

The correct answer is to use asynchronous API:

    NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString* appID = infoDictionary[@"CFBundleIdentifier"];
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://itunes.apple.com/lookup?bundleId=%@", appID]];
    NSURLSession         *  session = [NSURLSession sharedSession];
    NSURLSessionDataTask *  theTask = [session dataTaskWithRequest: [NSURLRequest requestWithURL: url] completionHandler:
    ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
    {
        NSDictionary<NSString*,NSArray*>* lookup = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        if ([lookup[@"resultCount"] integerValue] == 1)
        {
            NSString* appStoreVersion = lookup[@"results"].firstObject[@"version"];
           NSString* currentVersion = infoDictionary[@"CFBundleShortVersionString"];

            if ([appStoreVersion compare:currentVersion options:NSNumericSearch] == NSOrderedDescending) {
                // *** Present alert about updating to user ***
            }
        }
    }];
    [theTask resume];

The default time-out for network connections is several minutes., and even if the request goes through, it can be slow enough over a bad EDGE connection to take that long. You don't want your app to be unusable in that case. To test things like this, it is useful to run your networking code with Apple's Network Link Conditioner.

uliwitness
  • 8,532
  • 36
  • 58
3
func isUpdateAvailable() -> Bool {
    guard
        let info = Bundle.main.infoDictionary,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)"),
        let data = try? Data(contentsOf: url),
        let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],
        let results = json?["results"] as? [[String: Any]],
        results.count > 0,
        let versionString = results[0]["version"] as? String
        else {
            return false
    }

    return AppVersion(versionString) > AppVersion.marketingVersion
}

to compare version string :

https://github.com/eure/AppVersionMonitor

Lova
  • 101
  • 1
  • 3
3

This question was asked in 2011, I found it in 2018 while searching for some way for not only to check new version of app in App Store but also to notify user about it.

After small research I came to conclusion that answer of juanjo (related to Swift 3) https://stackoverflow.com/a/40939740/1218405 is the optimal solution if you want to do this in code by yourself

Also I can suggest two great projects on GitHub (2300+ stars each)

Example for Siren (AppDelegate.swift)

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

      let siren = Siren.shared
      siren.checkVersion(checkType: .immediately)

      return true
    }
  • You also can show different types of alerts about new version (allowing to skip version or forcing user to update)
  • You can specify how often version check should take place (daily / weekly / immediately)
  • You can specify how many days after new version released to app store alert should appear
moonvader
  • 19,761
  • 18
  • 67
  • 116
  • Links to an existing answer are not answers. Additionally, links to libraries are also not answers unless you explicitly add how the link answers the question to your answer (add code examples, etc). – JAL Jan 17 '18 at 17:31
3

C# equivalency of @datinc, in as much as obtaining the Apple App Store version. Included code to obtain version for both the bundle or the AssemblyInfo file.

EDIT:: Please note the region, "/us/", included in the urlString. This country code will need to be handled/changed accordingly.

string GetAppStoreVersion()
{
    string version = "";

    NSDictionary infoDictionary = NSBundle
        .MainBundle
        .InfoDictionary;

    String appID = infoDictionary["CFBundleIdentifier"].ToString();

    NSString urlString = 
        new NSString(@"http://itunes.apple.com/us/lookup?bundleId=" + appID);
    NSUrl url = new NSUrl(new System.Uri(urlString).AbsoluteUri);

    NSData data = NSData.FromUrl(url);

    if (data == null)
    {
        /* <-- error obtaining data from url --> */
        return "";
    }

    NSError e = null;
    NSDictionary lookup = (NSDictionary)NSJsonSerialization
        .Deserialize(data, NSJsonReadingOptions.AllowFragments, out e);

    if (lookup == null)
    {
        /* <-- error, most probably no internet or bad connectivity --> */
        return "";
    }

    if (lookup["resultCount"].Description.Equals("1"))
    {
        NSObject nsObject = lookup["results"];
        NSString nsString = new NSString("version");
        String line = nsObject
            .ValueForKey(nsString)
            .Description;

        /* <-- format string --> */
        string[] digits = Regex.Split(line, @"\D+");
        for (int i = 0; i < digits.Length; i++)
        {
            if (int.TryParse(digits[i], out int intTest))
            {
                if (version.Length > 0)
                    version += "." + digits[i];
                else
                    version += digits[i];
            }
        }
    }

    return version;
}

string GetBundleVersion()
{
        return NSBundle
            .MainBundle
            .InfoDictionary["CFBundleShortVersionString"]
            .ToString();
}

string GetAssemblyInfoVersion()
{
        var assembly = typeof(App).GetTypeInfo().Assembly;
        var assemblyName = new AssemblyName(assembly.FullName);
        return assemblyName.Version.ToString();
}
jtth
  • 876
  • 1
  • 12
  • 40
2

I would like to start from the answer here adding some lines that are useful when you change the middle number version (example from 1.0.10 to 1.1.0).

The answer here reacts like 1.0.10 is newer than 1.1.0 so that's my alternative solution:

import Alamofire

class VersionCheck {

  public static let shared = VersionCheck()

  func isUpdateAvailable(callback: @escaping (Bool)->Void) {
    let bundleId = Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String
    Alamofire.request("https://itunes.apple.com/lookup?bundleId=\(bundleId)").responseJSON { response in
      if let json = response.result.value as? NSDictionary, let results = json["results"] as? NSArray, let entry = results.firstObject as? NSDictionary, let versionStore = entry["version"] as? String, let versionLocal = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
        let arrayStore = versionStore.split(separator: ".")
        let arrayLocal = versionLocal.split(separator: ".")

        if arrayLocal.count != arrayStore.count {
          callback(true) // different versioning system
          return
        }

        // check each segment of the version
        for (key, value) in arrayLocal.enumerated() {
          if Int(value)! < Int(arrayStore[key])! {
            callback(true)
            return
          } else if Int(value)! > Int(arrayStore[key])! {
            callback(false)
            return
          }
        }
      }
      callback(false) // no new version or failed to fetch app store version
      return
    }
  }

}

Usage is always the same:

VersionCheck.shared.isUpdateAvailable() { hasUpdates in
  print("is update available: \(hasUpdates)")
}
Alain
  • 105
  • 7
2
 -(BOOL) needsUpdate{
  NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
  NSString* appID = infoDictionary[@"CFBundleIdentifier"];
  NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://itunes.apple.com/lookup?bundleId=%@", appID]];
  NSData* data = [NSData dataWithContentsOfURL:url];
  NSDictionary* lookup = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

if ([lookup[@"resultCount"] integerValue] == 1){
    NSString* appStoreVersion = lookup[@"results"][0][@"version"];
    NSString* currentVersion = infoDictionary[@"CFBundleShortVersionString"];
    if (![appStoreVersion isEqualToString:currentVersion]){
        float appVersion = [appStoreVersion floatValue];
        float ourVersion = [currentVersion floatValue];
        if (appVersion <= ourVersion) {
            return NO;
        }
        NSLog(@"Need to update [%@ != %@]", appStoreVersion, currentVersion);
        return YES;
    }
}
return NO;
}

Sometimes this URL shows http://itunes.apple.com/lookup?bundleId old version. So pop-up does not disappear. Add this lines

float appVersion = [appStoreVersion floatValue];
float ourVersion = [currentVersion floatValue];
if (appVersion <= ourVersion) {
        return NO;
}
    
shan
  • 136
  • 7
1

My code proposal. Based on the answers by @datinc and @Mario-Hendricks

You should of course, replace dlog_Error with your logging func call.

This kind of code structure should prevent your app from crashing in the event of an error. For fetching the appStoreAppVersion is not imperative, and should not lead to fatal errors. And yet, with this kind of code structure, you will still get your non-fatal error logged.

class func appStoreAppVersion() -> String?
{
    guard let bundleInfo = NSBundle.mainBundle().infoDictionary else {
        dlog_Error("Counldn't fetch bundleInfo.")
        return nil
    }
    let bundleId = bundleInfo[kCFBundleIdentifierKey as String] as! String
    // dbug__print("bundleId = \(bundleId)")

    let address = "http://itunes.apple.com/lookup?bundleId=\(bundleId)"
    // dbug__print("address = \(address)")

    guard let url = NSURLComponents.init(string: address)?.URL else {
        dlog_Error("Malformed internet address: \(address)")
        return nil
    }
    guard let data = NSData.init(contentsOfURL: url) else {
        if Util.isInternetAvailable() {
            dlog_MajorWarning("Web server request failed. Yet internet is reachable. Url was: \(address)")
        }// else: internet is unreachable. All ok. It is of course impossible to fetch the appStoreAppVersion like this.
        return nil
    }
    // dbug__print("data.length = \(data.length)")

    if data.length < 100 { //: We got 42 for a wrong address. And aproximately 4684 for a good response
        dlog_MajorWarning("Web server message is unexpectedly short: \(data.length) bytes")
    }

    guard let response = try? NSJSONSerialization.JSONObjectWithData(data, options: []) else {
        dlog_Error("Failed to parse server response.")
        return nil
    }
    guard let responseDic = response as? [String: AnyObject] else {
        dlog_Error("Not a dictionary keyed with strings. Response with unexpected format.")
        return nil
    }
    guard let resultCount = responseDic["resultCount"] else {
        dlog_Error("No resultCount found.")
        return nil
    }
    guard let count = resultCount as? Int else { //: Swift will handle NSNumber.integerValue
        dlog_Error("Server response resultCount is not an NSNumber.integer.")
        return nil
    }
    //:~ Determine how many results we got. There should be exactly one, but will be zero if the URL was wrong
    guard count == 1 else {
        dlog_Error("Server response resultCount=\(count), but was expected to be 1. URL (\(address)) must be wrong or something.")
        return nil
    }
    guard let rawResults = responseDic["results"] else {
        dlog_Error("Response does not contain a field called results. Results with unexpected format.")
        return nil
    }
    guard let resultsArray = rawResults as? [AnyObject] else {
        dlog_Error("Not an array of results. Results with unexpected format.")
        return nil
    }
    guard let resultsDic = resultsArray[0] as? [String: AnyObject] else {
        dlog_Error("Not a dictionary keyed with strings. Results with unexpected format.")
        return nil
    }
    guard let rawVersion = resultsDic["version"] else {
        dlog_Error("The key version is not part of the results")
        return nil
    }
    guard let versionStr = rawVersion as? String else {
        dlog_Error("Version is not a String")
        return nil
    }
    return versionStr.e_trimmed()
}

extension String {
    func e_trimmed() -> String
    {
        return stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
    }
}
SirEnder
  • 564
  • 4
  • 14
  • 1
    This answer makes its request synchronously. This means with a bad connection, your app could be unusable for minutes until the request returns. – uliwitness Mar 03 '17 at 10:27
1

You need the following:

  1. Server side logic/service to maintain the version number whenever you submit new version of app (ipa) to app store. This would also let you fetch the version to the client side.
  2. Client side logic

If the version from server is higher than the one installed on the device, prompt the user to update the app.

Here is a code snippet to check/compare version number following a format of number and dot format (ex. 1.2.0)

var currVer = "1.2.0";
var newVer = "1.2.1";
var arr1 = currVer.split(".");
var arr2 = newVer.split(".");
var intArray1 = arr1.map(function(txt){return (txt.length===0?0:parseInt(txt));});
var intArray2 = arr2.map(function(txt){return (txt.length===0?0:parseInt(txt));});
var l1 = intArray1.length;
var l2 = intArray2.length;
var isOutdated=false;



if(l1>0){
  if(l2>0){
    // compare both currentversion and new version is not empty
    if(l1==l2){
      for(i=0;i<l1;i++){
        if(intArray2[i]>intArray1[i]){
          // tag as outdated if matched digit of newVersion is greater than the matching digit of current version
          isOutdated=true;
          break;
        }
        
      }
      
    }
    else{
      if((l2-l1)>0){
        for(i=0;i<(l2-l1);i++){
          intArray1.push(0);
        }
        
      }
      if((l1-l2)>0){
        for(i=0;i<(l1-l2);i++){
          intArray2.push(0);
        }
        
      }
      l1 = intArray1.length;
      l2 = intArray2.length;
      
      for(i=0;i<l1;i++){
        if(intArray2[i]>intArray1[i]){
          // tag as outdated if matched digit of newVersion is greater than the matching digit of current version
          isOutdated=true;
          break;
        }
        
      }
    }
  }
  else{
    // if there's no new version, tag as not outdated
    isOutdated = false;
  }
  
}
else{
  // if current version is empty, tag as not outdated
  isOutdated = false;
}

document.getElementById("versionTxt").innerHTML = currVer + " -> " + JSON.stringify(intArray1);


document.getElementById("versionTxt2").innerHTML = newVer + " -> " + JSON.stringify(intArray2);

document.getElementById("isOutdatedTxt").innerHTML = "Outdated? " + isOutdated.toString();
<span id="versionTxt"></span> <br />
<span id="txtLength"></span> <br />
<span id="versionTxt2"></span> <br />
<span id="txtLength2"></span> <br />

<span id="lengthCompare"></span> <br />

<span id="isOutdatedTxt"></span>
Rye
  • 11
  • 2
1

Here is my solution :

  func isUpdateAvailableOrNot() throws -> Bool {
        guard let info = Bundle.main.infoDictionary,
            let currentVersion = info["CFBundleShortVersionString"] as? String,
            let identifier = info["CFBundleIdentifier"] as? String,
            let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
                throw VersionError.invalidBundleInfo
        }
        let data = try Data(contentsOf: url)
        guard let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] else {
            throw VersionError.invalidResponse
        }
        if let result = (json["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String {
            print("version in app store", version,currentVersion);
            
            return version != currentVersion
        }
        throw VersionError.invalidResponse
    }



//Now on your first view controller write this code on viewdidload()

 DispatchQueue.global().async {
            do {
                let update = try self.globalObjectHome.isUpdateAvailableOrNot()
                
                print("update",update)
                DispatchQueue.main.async {
                    if update{
                        self.AlertBox();
                    }
                    
                }
            } catch {
                print(error)
            }
        }


 func AlertBox(){
        var versionInfo = ""
        do {
            versionInfo = try self.globalObjectHome.getAppStoreVersion()
        }catch {
            print(error)
        }
 
        
        let alertMessage = "A new version of  APPNAME Application is available,Please update to version "+versionInfo;
        let alert = UIAlertController(title: "New Version Available", message: alertMessage, preferredStyle: UIAlertControllerStyle.alert)
  
        let okBtn = UIAlertAction(title: "Update", style: .default, handler: {(_ action: UIAlertAction) -> Void in
            if let url = URL(string: “Your application App Store Url”),
                UIApplication.shared.canOpenURL(url){
                if #available(iOS 10.0, *) {
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                } else {
                    UIApplication.shared.openURL(url)
                }
            }
        })
        let noBtn = UIAlertAction(title:"Skip this Version" , style: .destructive, handler: {(_ action: UIAlertAction) -> Void in
        })
        alert.addAction(okBtn)
        alert.addAction(noBtn)
        self.present(alert, animated: true, completion: nil)
        
    }
Iva
  • 2,447
  • 1
  • 18
  • 28
Davender Verma
  • 503
  • 2
  • 12
0

Here the answer from @aloha as Publisher:

func isUpdateAvailable() -> AnyPublisher<Bool, VersionError> {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
        return Fail<Bool, VersionError>(error: VersionError.invalidBundleInfo)
            .eraseToAnyPublisher()
    }

    return URLSession.shared
        .dataTaskPublisher(for: URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData))
        .tryMap { data, response -> Bool in
            guard let json = try? JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any],
                  let result = (json["results"] as? [Any])?.first as? [String: Any],
                  let lastVersion = result["version"] as? String
            else {
                throw VersionError.invalidResponse
            }
            return lastVersion > currentVersion
        }
        .mapError { _ in
            VersionError.invalidResponse
        }
        .eraseToAnyPublisher()
}
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 25 '22 at 19:58
0

I've made a pod for this

pod 'Up2Dater'

snapshot

sample:

   #import Up2Dater

   let updater = Up2Dater()
   updater.isNewVersionAvailable { result in
        switch result {
            case.success(let model):
                // if the model is nil, there's no new version
                print(model?.version, model?.releaseNotes, model?.appStorePath)
            case .failure(let error):
                print(error.description)
        }
    }

and it's better to compare string version rather then use relational operator (like < or >=) (e.g. "3.1.7" < "3.1.10")

    func isNewer(_ version: String, 
                 then bundleVersion: String) -> Bool {
        switch version.compare(bundleVersion, options: .numeric) {
            case .orderedSame,
                 .orderedAscending:
                return false
            case .orderedDescending:
                return true
        }
    }
12q
  • 21
  • 4
  • You probably should add your Github repo here, so people can see the code. https://github.com/12q/Up2Dater – Houman Jul 01 '22 at 08:25