[IOS] Casually implement the semi-forced update mechanism

Introduction

In this article, I will show you how to implement ** a mechanism that tells the user that a new version has been released when the app is launched and prompts for an update (called a semi-forced update in this article) **.

Forced update is a mechanism that you often see in game apps. By making the app unavailable until you update it, you can always run with the latest version. However, it is inconvenient not to be able to use it when you want to use it immediately because you are not always in a communication environment where users can update. From the point of view of such usability, I like the semi-forcedness that I kept before prompting the update.

By the way, if there is a discrepancy in the version of the application installed by the user, it will be difficult to modify the existing function as well as not being able to provide the latest function. For example, when migrating a DB for several generations, I sometimes step on an unexpected bug that the version is flying.

It is desirable to have users always use the latest version, and I think it is better to actively adopt a mechanism to encourage updates.

Implementation using iTunes Search API

When implementing the semi-forced update mechanism, the following flow can be considered.

No Title.png

  1. Ask the server for the latest version when the app is launched
  2. Compare the version returned from the server with the current version
  3. Display a UI that prompts the user to upgrade if it is not the latest

iTunes Search API

You may prepare your own server to return the latest version, but here is the [iTunes Search API](https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search- Use api /). Using this API, you can get information on apps distributed on the App Store for free. The app information also includes the version information released on the App Store.

By using the iTunes Search API, ** the version information of the app that can be obtained will be automatically updated when the app is actually delivered from the App Store **, so the delivered version by yourself It is easy to operate because there is no need to set up a server to manage iTunes and you will not forget to update the latest version.

AppStore class

I have implemented an AppStore class that hits the iTunes Search API like this: (I am using Alamofire v5 for the communication library)

import Foundation
import Alamofire

typealias LookUpResult = [String: Any]

enum AppStoreError: Error {
    case networkError
    case invalidResponseData
}

class AppStore {

    private static let lastCheckVersionDateKey = "\(Bundle.main.bundleIdentifier!).lastCheckVersionDateKey"

    static func checkVersion(completion: @escaping (_ isOlder: Bool) -> Void) {
        let lastDate = UserDefaults.standard.integer(forKey: lastCheckVersionDateKey)
        let now = currentDate

        //Skip until the date changes
        guard lastDate < now else { return }

        UserDefaults.standard.set(now, forKey: lastCheckVersionDateKey)

        lookUp { (result: Result<LookUpResult, AppStoreError>) in
            do {
                let lookUpResult = try result.get()

                if let storeVersion = lookUpResult["version"] as? String {
                    let storeVerInt = versionToInt(storeVersion)
                    let currentVerInt = versionToInt(Bundle.version)
                    completion(storeVerInt > currentVerInt)
                }
            }
            catch {
                completion(false)
            }
        }
    }

    static func versionToInt(_ ver: String) -> Int {
        let arr = ver.split(separator: ".").map { Int($0) ?? 0 }

        switch arr.count {
            case 3:
                return arr[0] * 1000 * 1000 + arr[1] * 1000 + arr[2]
            case 2:
                return arr[0] * 1000 * 1000 + arr[1] * 1000
            case 1:
                return arr[0] * 1000 * 1000
            default:
                assertionFailure("Illegal version string.")
                return 0
        }
    }

    ///Open App Store
    static func open() {
        if let url = URL(string: storeURLString), UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
        }
    }
}

private extension AppStore {

    static var iTunesID: String {
        "<YOUR_ITUNES_ID>"
    }

    ///App Store App Page
    static var storeURLString: String {
        "https://apps.apple.com/jp/app/XXXXXXX/id" + iTunesID
    }

    /// iTunes Search API
    static var lookUpURLString: String {
        "https://itunes.apple.com/lookup?id=" + iTunesID
    }

    ///Returns an integer such as 20201116 generated from the current date and time
    static var currentDate: Int {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.locale = .current
        formatter.dateFormat = "yyyyMMdd"
        return Int(formatter.string(from: Date()))!
    }

    static func lookUp(completion: @escaping (Result<LookUpResult, AppStoreError>) -> Void) {
        AF.request(lookUpURLString).responseJSON(queue: .main, options: .allowFragments) { (response: AFDataResponse<Any>) in
            let result: Result<LookUpResult, AppStoreError>

            if let error = response.error {
                result = .failure(.networkError)
            }
            else {
                if let value = response.value as? [String: Any],
                   let results = value["results"] as? [LookUpResult],
                   let obj = results.first {
                    result = .success(obj)
                }
                else {
                    result = .failure(.invalidResponseData)
                }
            }

            completion(result)
        }
    }
}

extension Bundle {
    /// Info.Get the version number in the plist. major.minor.It is assumed that it is in patch format
    static var version: String {
        return Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
    }
}

Replace <YOUR_ITUNES_ID> with the iTunes ID of the app for which you want to get version information. The iTunes ID is displayed in the URL when you open the app page on the App Store in your browser. Similarly, replace storeURLString with the URL of the App Store.

Slice.png

In the above implementation, the version is checked for updates once a day. For version comparison, the version string in * major.minor.patch * format is divided by dots and converted to Int type (see the versionToInt (_ :) method). With the above method, minor and patch can only take 1000 steps from 0 to 999, but that's enough.

When using the AppStore class, create the following method in ViewController and call viewDidAppear or UIApplication.willEnterForegroundNotification with the observed method.

private extension ViewController {

    func checkVersion() {
        AppStore.checkVersion { (isOlder: Bool) in
            guard isOlder else { return }

            let alertController = UIAlertController(title: "There is a new version!", message: "Please update.", preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "update", style: .default) { action in
                AppStore.open()
            })
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
            self.present(alertController, animated: true)
        }
    }
}

With the above implementation, it is now possible to inform the user that a new version has been released.

Recommended Posts

[IOS] Casually implement the semi-forced update mechanism
Implement the UICollectionView of iOS14 with the minimum required code.