问题
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?
回答1:
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.
回答2:
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: "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 {
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: "http://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)
}
}
回答3:
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!
回答4:
Thanks to Steve Moser for his link, here is my code:
NSString *appInfoUrl = @"http://itunes.apple.com/en/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"];
回答5:
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;
回答6:
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)
回答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.
回答8:
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)")
}
回答9:
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
}
回答10:
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
}
}];
回答11:
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 appStoreVersion = entry["version"] as? String, let installedVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
if let installed = Int(installedVersion.replacingOccurrences(of: ".", with: "")), let appStore = Int(appStoreVersion.replacingOccurrences(of: ".", with: "")), appStore > installed {
callback(true)
}
}
}
}
}
And then to use it:
VersionCheck.shared.isUpdateAvailable() { (hasUpdates) in
print("is update available: \(hasUpdates)")
}
回答12:
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
}
回答13:
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;
}
回答14:
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(' ');
// Release Notes
settings.updateReleaseNotes('v' + storeVersion, releaseNotes);
}
}
回答15:
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.
回答16:
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
回答17:
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")
}
}
}
回答18:
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();
}
回答19:
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)
- https://github.com/ArtSabintsev/Harpy for Objective-C
- https://github.com/ArtSabintsev/Siren for Swift
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
回答20:
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")
}
}
}
回答21:
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 👍 😊
回答22:
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!
回答23:
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())
}
}
回答24:
Updated for swift 3:
if you want to check the current version of your app, used below simple code:
let object = Bundle.main.infoDictionary?["CFBundleShortVersionString"]
let version = object as! String
print("version: \(version)")
来源:https://stackoverflow.com/questions/6256748/check-if-my-app-has-a-new-version-on-appstore