Index out of range data nil swift 3 - ios

I have data coming from Json and populating it inside an UIImage using alamofireImage. One of those elements that I am getting back from Json is a String that has a URL to an image. If a particular Json string is null or blank I get the following error :
fatal error: Index out of range
EventEngine.getEventGalery(invitedID, limit: "2") { (result, error) in
DispatchQueue.main.async(execute: { () -> Void in
if let response = result as? EventGaleryResponse{
self.galery = response.data!
let jsonDict = self.galery
print("jsonDict \(jsonDict![0].photo!)")
if jsonDict![0].photo != nil {
self.imagearry1.af_setImage(withURL: URL(string: jsonDict![0].photo!)!)
}
if jsonDict![1].photo != nil {
self.imagearry2.af_setImage(withURL: URL(string:jsonDict![1].photo!)!)
}
if jsonDict![2].photo != nil {
self.imagearry3.af_setImage(withURL: URL(string: jsonDict![2].photo!)!)
}
}
})
}

Please never use ! operator without checks. I really suggest to use if let construction instead.
Some pseudocode (I dont know your types) :
EventEngine.getEventGalery(invitedID, limit: "2") { (result, error) in
DispatchQueue.main.async(execute: { () -> Void in
if let response = result as? EventGaleryResponse{
self.galery = response.data!
let jsonDict = self.galery
if let dict = jsonDict {
setPhotoFromDict(dict, 0, self.imagearry1)
setPhotoFromDict(dict, 1, self.imagearry2)
setPhotoFromDict(dict, 2, self.imagearry3)
} else {
print("cannot deserialise \(String(describing: jsonDict)")
}
}
})
}
private func setPhotoFromDict(<#DictType#> dict, Int index, <#ImageArrayType#> imageArary) {
if let photo = dict[index].photo as? String, let url = URL(string: photo) {
imageArary.af_setImage(withURL: url)
}
}
And the initial error comes from this line print("jsonDict \(jsonDict![0].photo!)"), I guess, because you access object without check

I think, you did not check if it is empty
EventEngine.getEventGalery(invitedID, limit: "2") { (result, error) in
DispatchQueue.main.async(execute: { () -> Void in
if let response = result as? EventGaleryResponse{
self.galery = response.data!
let jsonDict = self.galery
if jsonDict != nil {
if jsonDict.count > 0 && jsonDict![0].photo != nil {
self.imagearry1.af_setImage(withURL: URL(string: jsonDict![0].photo!)!)
}
if jsonDict.count > 1 && jsonDict![1].photo != nil {
self.imagearry2.af_setImage(withURL: URL(string:jsonDict![1].photo!)!)
}
if jsonDict.count > 2 && jsonDict![2].photo != nil {
self.imagearry3.af_setImage(withURL: URL(string: jsonDict![2].photo!)!)
}
}
}
})
}

Related

Swift: For loop to synchronous one by one until all function response

I'm new to swift and practicing my best.
I have main function appointmentCall() when it executes and in response I may get multiple appointments. Then I pass appointmentId to appointmentDetail function for more details.
All I want to is how can I set For loop to synchronous process. Means it will not execute next appointment until first is finished. At the moment it executes all appointments.
I need appointment one by one executes all function once finished executes next appointment.
AppointmentCall
-> AppoinmentDetail -> processDetail -> Completed.
Code:
func appointmentCall(_ selectedDate:Date) {
DataProvider.main.serviceGetAppointment(date: selectedDate, callback: {success, result in
do{
if(success){
print(result as! Data)
let decoder = JSONDecoder()
let response = try decoder.decode(ResponseData.self, from: result! as! Data)
if let appointments = response.appointments {
self.appData = appointments.map { AppointmentDownloadModel(appointmentModel: $0)}
}
for eachApp in self.appData {
self.appointmentDetail(AppId: appId)
}
self.tableView.reloadData()
return true
}else{
return false
}
}catch let error {
DataProvider.main.token = nil
print(error as Any)
return false
}
})
}
func appointmentDetail(AppId: Int){
DataProvider.main.serviceGetAppointmentDetail(Id: AppId , callback: {success, result in
do{
if(success){
let decoder = JSONDecoder()
let resp = try decoder.decode(AppointmentDetail.self, from: result! as! Data)
self.AppDetailData = resp
self.processDetail(appId: AppId)
return true
}else{
return false
}
}catch let error {
print(error as Any)
return false
}
})
}
func processDetail(appId: Int) {
guard let detail = AppDetailData, AppDetailData?.appointmentId == appId else {
return
}
for firmParam in (detail.sectionList ?? []) {
for firmItem in firmParam.items! {
if firmItem.actionParamData != nil {
let str = firmItem.actionParamData
let param = str?.components(separatedBy: ":")
let final = param![1].replacingOccurrences(of: "}", with: "")
let fmId = final.components(separatedBy: ",")
let frmId = fmId[0]
self.firmDetails(actionParamData: Int(frmId) ?? 0)
}
//pdf download
if firmItem.actionType == 2 {
if firmItem.actionUrl != nil {
self.contentLength(link: firmItem.actionUrl!)
let fileURL = URL(fileURLWithPath: firmItem.actionUrl ?? "")
let fileTitle = firmItem.textField ?? ""
self.downloadPDFTask(pdfURL: firmItem.actionUrl ?? "")
}
}
}
}
}
If you use a recursive function with completion handler it would ensure you could essentially loop over an array and call a function to do something and know that it has completed before you do the same to your next value. See below to high level generic example
func doSomething()
{
// Call this function with the first index in the array
firstFunction(index: 0)
// Now were here and we know that every value in the member variable array has been processed by "secondFunction"
}
func firstFunction(index: Int)
{
let value = memberVariableArray[index]
secondFunction(String: value) { (returnValueFromSecondFunction) in
// do something with the return value from the second function
// Recall this function with the next index (if we have another one)
let newIndex = index + 1
if newIndex < memberVariableArray.count
{
self.firstFunction(index: newIndex)
}
}
}
func secondFunction(valueToDoSomethingWith: String, completion: #escaping (String) -> Void)
{
// Do something here
// Complete function
completion("The Value you want back")
}

Nested DispatchGroup

I try use nested DispatchGroup, there is my code :
class TranslateService {
private let myGroup = DispatchGroup()
func translateText(text:[String],closure:#escaping ((_ success:String?,_ error:Error?) -> Void)) {
var translateString: String = ""
var responseError: Error?
for index in 0...text.count - 1 {
let urlString = "https://translate.yandex.net/api/v1.5/tr.json/translate?key=trnsl.1.1.20171105T134956Z.795c7a0141d3061b.dc25bae76fa5740b2cdecb02396644dea58edd24&text=\(text[index])&lang=fa&format=plain&options=1"
if let allowString = Utilities.shareInstance.getQueryAllowedString(url: urlString) {
if let url = URL(string:allowString){
myGroup.enter()
Alamofire.request(url).responseJSON { response in
guard let responseData = response.data else {
self.myGroup.leave()
return
}
do {
let json = try JSONSerialization.jsonObject(with: responseData, options: [])
if let res = json as? [String:Any] {
if let code = res["code"] as? Int {
if code == 200 {
if let textArr = res["text"] as? [AnyObject] {
let flattArr = Utilities.shareInstance.flatStringMapArray(textArr)
if flattArr.count > 0 {
translateString += "،" + flattArr[0]
}
}
}
}
self.myGroup.leave()
}
}catch {
responseError = error
self.myGroup.leave()
}
}
self.myGroup.notify(queue: .main) {
print("Finished all requests.")
print(translateString)
closure(translateString, responseError)
}
}
}
}
}
}
class AddressService {
private let translateService: TranslateService = TranslateService()
private let myGroup = DispatchGroup()
func fetchAddressFromGeographicalLocation(latitude: Double, longitude: Double,closure:#escaping ((_ success:String,_ name:String,_ error:Error?) -> Void)) {
var address: String = ""
let name: String = ""
var responseError: Error?
if let url = URL(string:"https://maps.googleapis.com/maps/api/geocode/json?latlng=\(latitude),\(longitude)&key=AIzaSyAdEzHZfZWyjLMuuW92w5fkR86S3-opIF0&language=fa&region=IR&locale=fa"){
self.myGroup.enter()
Alamofire.request(url).responseJSON { response in
guard let responseData = response.data else {
self.myGroup.leave()
return
}
do {
let json = try JSONSerialization.jsonObject(with: responseData, options: [])
if let addressDic = json as? [String:Any] {
if let result = addressDic["results"] as? [AnyObject] {
if result.count > 0 {
let flattRes = Utilities.shareInstance.flatMapArray(result)
let item = flattRes[0]
address = item["formatted_address"] as? String ?? ""
var res = address
if res.isContainEnglishCharachter {
self.myGroup.enter()
let resArr = res.components(separatedBy: ",")
var all : [String] = []
for item in resArr {
if item != " " {
all.append(item)
}}
self.translateService.translateText(text: all, closure: {stringAdd,error in
self.myGroup.enter()
if error != nil {
self.myGroup.leave()
}else {
address = stringAdd ?? ""
self.myGroup.leave()
} })
}else {
self.myGroup.leave()
}
}
}
}
}catch {
responseError = error
self.myGroup.leave()
}
self.myGroup.notify(queue: .main) {
// print("Finished all requests.")
closure(address, name, responseError)
}
}
}
}
}
All I want is that the myGroup I put in the class AddressService waiting for the myGroup that I put in the class TranslateService.
but now self.myGroup.notify not call in the AddressService class, So closure not work.
How can solve this problem, Thank you for all the answers.
I think you are over complicating it.
If I understood a bit, what you want to do is the following:
Get an address from the Address service.
Translate some words of that address, one by one, using the translation service.
When using the Address service there is only one call being done, so there is no need to use Dispatch Groups at this point. Dispatch Groups are used to synchronize more than one async call.
For your Translation service you can make good use of the Dispatch groups, since you are doing calls to the service inside a for loop. The problem here is, that the implementation is slightly wrong. You are setting the notification block inside the for loop, and it should be outside, so that it gets only triggered once, when all the calls inside the loop are done.
So move this block outside the for loop in the Translation service:
self.myGroup.notify(queue: .main) {
print("Finished all requests.")
print(translateString)
closure(translateString, responseError)
}
Now "Finished all requests." will only be printed once, when all requests are done.
In the address service you do not need dispatch groups at all. Just wait until the completion block is called.
self.translateService.translateText(text: all, closure: {stringAdd,error in
Everything is done here already.
}

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)
}
}
})
}

Returning a value from a Swift function containing a closure

I've written a function that should return a value but the value comes from a closure. The problem is if I try to return a value from inside the closure it treats this as being the return value from the completion handler.
private func loadData() throws -> [Item] {
var items = [Item]()
let jsonUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?units=metric&cnt=7&q=coventry,uk"
print(jsonUrl)
let session = NSURLSession.sharedSession()
guard let shotsUrl = NSURL(string: jsonUrl) else {
throw JSONError.InvalidURL(jsonUrl)
}
session.dataTaskWithURL(shotsUrl, completionHandler: {(data, response, error) -> Void in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(json)
guard let days:[AnyObject] = (json["list"] as! [AnyObject]) else {
throw JSONError.InvalidArray
}
for day in days {
guard let timestamp:Double = day["dt"] as? Double else {
throw JSONError.InvalidKey("dt")
}
print(timestamp)
let date = NSDate(timeIntervalSince1970: NSTimeInterval(timestamp))
guard let weather:[AnyObject] = day["weather"] as? [AnyObject] else {
throw JSONError.InvalidArray
}
guard let desc:String = weather[0]["description"] as? String else {
throw JSONError.InvalidKey("description")
}
guard let icon:String = weather[0]["icon"] as? String else {
throw JSONError.InvalidKey("icon")
}
guard let url = NSURL(string: "http://openweathermap.org/img/w/\(icon).png") else {
throw JSONError.InvalidURL("http://openweathermap.org/img/w/\(icon).png")
}
guard let data = NSData(contentsOfURL: url) else {
throw JSONError.InvalidData
}
guard let image = UIImage(data: data) else {
throw JSONError.InvalidImage
}
guard let temp:AnyObject = day["temp"] else {
throw JSONError.InvalidKey("temp")
}
guard let max:Float = temp["max"] as? Float else {
throw JSONError.InvalidKey("max")
}
let newDay = Item(date: date, description: desc, maxTemp: max, icon: image)
print(newDay)
items.append(newDay)
}
return items // this line fails because I'm in the closure. I want this to be the value returned by the loadData() function.
} catch {
print("Fetch failed: \((error as NSError).localizedDescription)")
}
})
}
Add a completion handler (named dataHandler in my example) to your loadData function:
private func loadData(dataHandler: ([Item])->()) throws {
var items = [Item]()
let jsonUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?units=metric&cnt=7&q=coventry,uk"
print(jsonUrl)
let session = NSURLSession.sharedSession()
guard let shotsUrl = NSURL(string: jsonUrl) else {
throw JSONError.InvalidURL(jsonUrl)
}
session.dataTaskWithURL(shotsUrl, completionHandler: {(data, response, error) -> Void in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(json)
guard let days:[AnyObject] = (json["list"] as! [AnyObject]) else {
throw JSONError.InvalidArray
}
for day in days {
guard let timestamp:Double = day["dt"] as? Double else {
throw JSONError.InvalidKey("dt")
}
print(timestamp)
let date = NSDate(timeIntervalSince1970: NSTimeInterval(timestamp))
guard let weather:[AnyObject] = day["weather"] as? [AnyObject] else {
throw JSONError.InvalidArray
}
guard let desc:String = weather[0]["description"] as? String else {
throw JSONError.InvalidKey("description")
}
guard let icon:String = weather[0]["icon"] as? String else {
throw JSONError.InvalidKey("icon")
}
guard let url = NSURL(string: "http://openweathermap.org/img/w/\(icon).png") else {
throw JSONError.InvalidURL("http://openweathermap.org/img/w/\(icon).png")
}
guard let data = NSData(contentsOfURL: url) else {
throw JSONError.InvalidData
}
guard let image = UIImage(data: data) else {
throw JSONError.InvalidImage
}
guard let temp:AnyObject = day["temp"] else {
throw JSONError.InvalidKey("temp")
}
guard let max:Float = temp["max"] as? Float else {
throw JSONError.InvalidKey("max")
}
let newDay = Item(date: date, description: desc, maxTemp: max, icon: image)
print(newDay)
items.append(newDay)
}
dataHandler(items)
} catch {
print("Fetch failed: \((error as NSError).localizedDescription)")
}
}).resume()
}
do {
try loadData { itemsArray in
print(itemsArray)
}
} catch {
print(error)
}
I've tested it in a Playground and it works without errors:

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