How to get the page title with swift in Share Extension - ios

I'm developing share extension which is used on safari.
I could get url on share extension. but I cant get page title.
let puclicURL = String(kUTTypeURL)
if itemProvider.hasItemConformingToTypeIdentifier(puclicURL) {
itemProvider.loadItem(forTypeIdentifier: puclicURL, options: nil, completionHandler: {
(item, error) in
if let url: NSURL = item as? NSURL {
print("url", url)
// I want page title also
}
}
}
And, I tried below code.https://stackoverflow.com/a/33139355/5060282
I think below code can run only in Action Extension. not Share Extension.
let propertyList = String(kUTTypePropertyList)
if itemProvider.hasItemConformingToTypeIdentifier(propertyList) {
itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in
let dictionary = item as! NSDictionary
OperationQueue.main.addOperation {
let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! NSDictionary
let title = NSURL(string: (results["title"] as! String))
//yay, you got the title now
print(title)
}
})
} else {
print("error")
}
// But, error...

Related

Function that return current URL with completion Handler

I have a ShareExtension in which I like need to get the current URL. This is my function for it:
var html: String?
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if (url as? URL) != nil {
html = (self.getHTMLfromURL(url: url as? URL))
}
}
}
My problem is that I need the html but when using that variable right after that function html is still empty. I think I need some sort of completion handler but I tried different things now and can not get it right...
This is how my whole function looks like at the moment (not working, as html becomes an empty String)
#objc func actionButtonTapped(){
do {
var html: String?
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if (url as? URL) != nil {
html = (self.getHTMLfromURL(url: url as? URL))
}
}
}
let doc: Document = try SwiftSoup.parse(html ?? "")
let priceClasses: Elements = try doc.select("[class~=(?i)price]")
for priceClass: Element in priceClasses.array() {
let priceText : String = try priceClass.text()
print(try priceClass.className())
print("pricetext: \(priceText)")
}
let srcs: Elements = try doc.select("img[src]")
let srcsStringArray: [String?] = srcs.array().map { try? $0.attr("src").description }
for imageName in srcsStringArray {
print(imageName!)
}
} catch Exception.Error( _, let message) {
print(message)
} catch {
print("error")
}
}
The Goal is to have a extra function to get the url (1st code example) with a completion handler in which I can work with the created html.
the problem was that I didn't realize that I already had a completionHandler with loadItems. So what I did now was to put the whole do & catch block in another method and called it in the completion handler like this:
#objc func actionButtonTapped(){
var html: String?
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = item.attachments?.first,
itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if (url as? URL) != nil {
html = (self.getHTMLfromURL(url: url as? URL))
print("bruh")
self.doStuff(html: html)
}
}
}
}
func doStuff(html: String?){
do {
let doc: Document = try SwiftSoup.parse(html ?? "")
let priceClasses: Elements? = try doc.select("[class~=(?i)price]")
for priceClass: Element in priceClasses!.array() {
let priceText : String = try priceClass.text()
print(try priceClass.className())
print("pricetext: \(priceText)")
}
let srcs: Elements = try doc.select("img[src]")
let srcsStringArray: [String?] = srcs.array().map { try? $0.attr("src").description }
for imageName in srcsStringArray {
print(imageName!)
}
} catch Exception.Error( _, let message) {
print(message)
} catch {
print("error")
}
}

iOS custom share extension: load selected image

I'm working on a share extension for my app, to be able to use it for sharing images directly from the library. I have a view controller and layout set up, but I'm struggling with getting the selected image to show in the UIImageView inside the view controller.
For now, my getImage() is as follows:
func getImage() {
if let inputItem = extensionContext!.inputItems.first as? NSExtensionItem {
if let itemProvider = inputItem.attachments?.first as? NSItemProvider {
itemProvider.loadItem(forTypeIdentifier: kUTTypeJPEG as String) { [unowned self] (imageData, error) in
if let item = imageData as? Data {
self.imageView.image = UIImage(data: item)
}
}
}
}
}
...but the image is not loading. What am I doing wrong here?
Found the problem. Turned out I needed to do a type identifier conformance check first:
func getImage() {
if let inputItem = extensionContext!.inputItems.first as? NSExtensionItem {
if let itemProvider = inputItem.attachments?.first as? NSItemProvider {
// This line was missing
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeJPEG as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeJPEG as String) { [unowned self] (imageData, error) in
if let item = imageData as? Data {
self.imageView.image = UIImage(data: item)
}
}
}
}
}
}

How to create an iOS share extension for contacts

I am trying to create an iOS share extension to share contacts using the following code:
let contactType = "public.vcard"
override func viewDidLoad() {
let items = extensionContext?.inputItems
var itemProvider: NSItemProvider?
if items != nil && items!.isEmpty == false {
let item = items![0] as! NSExtensionItem
if let attachments = item.attachments {
if !attachments.isEmpty {
itemProvider = attachments[0] as? NSItemProvider
}
}
}
if itemProvider?.hasItemConformingToTypeIdentifier(contactType) == true {
itemProvider?.loadItemForTypeIdentifier(contactType, options: nil) { item, error in
if error == nil {
print("item: \(item)")
}
}
}
}
I am getting the following output:
item: Optional(<42454749 4e3a5643 4152440d 0a564552 53494f4e 3a332e30 0d0a5052 4f444944 3a2d2f2f 4170706c 6520496e 632e2f2f 6950686f 6e65204f 5320392e 322f2f45 4e0d0a4e 3a42656e 6e657474 3b436872 69733b3b 3b0d0a46 4e3a2043 68726973 20204265 6e6e6574 74200d0a 54454c3b 74797065 3d484f4d 453b7479 70653d56 4f494345 3b747970 653d7072 65663a28 36313529 c2a03432 352d3637 31390d0a 454e443a 56434152 440d0a>)
If i set a breakpoint I see that item has a type of NSSecureCoding. My question is how do I turn this into a CNContact?
The item you are getting is of type NSData. You have to transform it to String to get the vCard String:
if itemProvider?.hasItemConformingToTypeIdentifier(contactType) == true {
itemProvider?.loadItemForTypeIdentifier(contactType, options: nil) { item, error in
if error == nil {
if let data = item as? NSData, let vCardString = String(data: data, encoding: NSUTF8StringEncoding) {
print(vCardString)
}
}
}
}
When you are sharing any contacts, itemProvider contains vCard format of CNContact. To access that contact, you need to convert that data (type NSSecureCoding) to CNContact.
let uttypeContact = kUTTypeContact as String
if itemProvider.hasItemConformingToTypeIdentifier(uttypeContact) {
itemProvider.loadItemForTypeIdentifier(uttypeContact, options: nil, completionHandler: { (data, error) in
if error == nil {
do {
if let data = data as? NSData {
let contact = try CNContactVCardSerialization.contactsWithData(data)
print(contact)
}
} catch {}
}
})
}
[Update]
For more details https://forums.developer.apple.com/message/92115#92115
Updated answer of #joern to Swift 3:
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeContact as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeContact as String, options: nil, completionHandler: { (contact, error) in
if let contactData = contact as? Data, let vCardString = String(data: contactData, encoding: .utf8) {
print(vCardString)
}
})
}
Also for an Action Extension you need to add the OperationQueue:
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeContact as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeContact as String, options: nil, completionHandler: { (contact, error) in
OperationQueue.main.addOperation {
if let contactData = contact as? Data, let vCardString = String(data: contactData, encoding: .utf8) {
print(vCardString)
}
}
})
}

Getting the URL in SLComposeServiceViewController in a Share Extension for Safari on iOS

This question may have been asked previously in a different form, but I am attempting a slightly deeper attempt at understanding Share extensions and therefore, this question has more detail than the previously asked version of this question.
When writing a Share Extension, we can subclass SLComposeServiceViewController and get the viewDidLoad() and didSelectPost() events among others, but the only properties on the VC are contentText and textView and placeholder properties according to Apple documentation
https://developer.apple.com/library/prerelease/ios/documentation/Social/Reference/SLComposeServiceViewController_Class/
Given that this is the case, what is the best way of doing the following :
Firstly, populating the VC that appears with the URL of the website
Secondly, Accessing the URL to pass to the sharedDefaults as follows
let shareDefaults = NSUserDefaults(suiteName: "groupName")
shareDefaults?.setObject(self.contentText, forKey: "stringKey")
shareDefaults?.synchronize()
so as to be able to save it for later access from the app.
Any help regarding getting the URL would be greatly appreciated.
You can access the URL like this:
- (void)didSelectPost {
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:#"public.url"]) {
[itemProvider loadItemForTypeIdentifier:#"public.url"
options:nil
completionHandler:^(NSURL *url, NSError *error) {
// Do what you want to do with url
[self.extensionContext completeRequestReturningItems:#[]
completionHandler:nil];
}];
}
}
Here is the same in Swift:
override func didSelectPost() {
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
if let itemProvider = item.attachments?.first as? NSItemProvider {
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItemForTypeIdentifier("public.url", options: nil, completionHandler: { (url, error) -> Void in
if let shareURL = url as? NSURL {
// do what you want to do with shareURL
}
self.extensionContext?.completeRequestReturningItems([], completionHandler:nil)
})
}
}
}
}
Updated for Swift 5:
override func didSelectPost() {
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
if let itemProvider = item.attachments?.first {
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { (url, error) -> Void in
if let shareURL = url as? NSURL {
// do what you want to do with shareURL
}
self.extensionContext?.completeRequest(returningItems: [], completionHandler:nil)
})
}
}
}
}

Check if my app has a new version on AppStore

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?
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.
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)
}
}
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)")
}
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)
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!
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;
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"];
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)")
}
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
}
}
Can I suggest this little library:
https://github.com/nicklockwood/iVersion
Its purpose is to simplify the handling of remote plists to trigger notifications.
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 👍 😊
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
}
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
}
}];
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);
}
}
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")
}
}
}
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")
}
}
}
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!
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
}
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;
}
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.
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
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
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();
}
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)")
}
-(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;
}
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())
}
}
You need the following:
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.
Client side logic
Get the version of app installed on the device using cordova plugin. https://github.com/Rareloop/cordova-plugin-app-version.
Fetch the version from the server and compare it to the version returned by the cordova plugin.
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>
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)
        
    }
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()
}
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
}
}

Resources