【Swift/iOS】強制アップデートの実装方法!iTunes Search API

【Swift/iOS】強制アップデートの実装方法!iTunes Search API

この記事からわかること

  • Swift/iOSアプリ強制アップデート実装する方法
  • iTunes Search API使い方
  • ストア情報取得するには?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

強制アップデートの仕組み

モバイルアプリ開発でよくある「強制アップデート」の仕組みはユーザーが使用しているアプリのバージョンと最新のアプリバージョンを比較し異なる場合にポップアップなどを表示させることで強制または半強制的にアップデートを促す方法です。

これによりユーザーには常に最新のアプリバージョンでの使用を徹底することもできるので、メジャーバージョンが変わった時や致命的なバグが見つかった際に速やかにアップデートをさせることができます。

起動中のアプリのバージョンを取得する

アプリ内から使用されているバージョンを取得するにはBundleを使用してCFBundleShortVersionStringの値を取得すればOKです。ここからX.X.Xのようなバージョンを文字列で取得することが可能です。

Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String

最新バージョンを取得する方法

アプリが比較対象とする最新のバージョンを管理/取得する方法はいくつかあります。

要はサーバー側に最新のアプリバージョンを設置してそれをアプリからAPIなどで取得できる仕組みを構築すれば良いだけなので方法はいくつかあります。ただ「Firebase Remote Config」や「自前のサーバー」などでは更新するごとに手動でサーバー側の値を変更する必要があります。そこでおすすめなのが「iTunes Search API」です。

iTunes Search APIとは?

iTunes Search API」はApple公式のAPIでiTunes Store, App Store, Apple Musicなどのコンテンツを検索・取得できるAPIです。このAPIは認証キーなどが不要かつ無料で誰でも利用できるので導入コストも軽く、APIなのでアプリ側からの取得ロジックの実装も簡単です。

取得だけでなく、検索などもできる便利なAPIなので詳細は公式リファレンス:iTunes Search APIを参考にしてください。ここでは該当アプリのストア情報を取得する方法にフォーカスしていきます。

つまりiTunes Search APIを使用することでストア情報が更新されたタイミングで自動的に強制アップデートを促すことが可能になります。

該当アプリのストア情報を取得

iTunes Search APIを使用してアプリのストア情報を取得するにはhttps://itunes.apple.com/lookup?id=[アプリID]形式のURLを構築すればOKです。アプリIDは公開前であればApp Store Connectの「アプリ情報」>「Apple ID」に掲載されており、公開していればストアをWebで開いた際のURLの最後にidXXXXXXXXXという形で確認することができます。

【AppStore】iOSアプリを海外向けに公開する設定方法!英語圏対応

これを元に以下のようにパスを構築してGETメソッドで叩いてみると「txt」ファイルが取得でき、その中にJSON形式でストア情報が記載されています。

// みんなの誕生日アプリ
https://itunes.apple.com/lookup?id=1673431227

中身は以下のような感じでその中にversionがあります。


{
  "resultCount": 1,
  "results": [
    {
      "features": ["iosUniversal"],
      "advisories": [],
      "supportedDevices": [
        "iPhone5s-iPhone5s", "iPadAir-iPadAir", "iPadAirCellular-iPadAirCellular",
        "iPadMiniRetina-iPadMiniRetina", "iPadMiniRetinaCellular-iPadMiniRetinaCellular",
        "iPhone6-iPhone6", "iPhone6Plus-iPhone6Plus", "iPadAir2-iPadAir2",
        "iPadAir2Cellular-iPadAir2Cellular", "iPadMini3-iPadMini3",
        "iPadMini3Cellular-iPadMini3Cellular", "iPodTouchSixthGen-iPodTouchSixthGen",
        "iPhone6s-iPhone6s", "iPhone6sPlus-iPhone6sPlus", "iPadMini4-iPadMini4",
        "iPadMini4Cellular-iPadMini4Cellular", "iPadPro-iPadPro",
        "iPadProCellular-iPadProCellular", "iPadPro97-iPadPro97",
        "iPadPro97Cellular-iPadPro97Cellular", "iPhoneSE-iPhoneSE",
        "iPhone7-iPhone7", "iPhone7Plus-iPhone7Plus", "iPad611-iPad611",
        "iPad612-iPad612", "iPad71-iPad71", "iPad72-iPad72", "iPad73-iPad73",
        "iPad74-iPad74", "iPhone8-iPhone8", "iPhone8Plus-iPhone8Plus",
        "iPhoneX-iPhoneX", "iPad75-iPad75", "iPad76-iPad76", "iPhoneXS-iPhoneXS",
        "iPhoneXSMax-iPhoneXSMax", "iPhoneXR-iPhoneXR", "iPad812-iPad812",
        "iPad834-iPad834", "iPad856-iPad856", "iPad878-iPad878",
        "iPadMini5-iPadMini5", "iPadMini5Cellular-iPadMini5Cellular",
        "iPadAir3-iPadAir3", "iPadAir3Cellular-iPadAir3Cellular",
        "iPodTouchSeventhGen-iPodTouchSeventhGen", "iPhone11-iPhone11",
        "iPhone11Pro-iPhone11Pro", "iPadSeventhGen-iPadSeventhGen",
        "iPadSeventhGenCellular-iPadSeventhGenCellular", "iPhone11ProMax-iPhone11ProMax",
        "iPhoneSESecondGen-iPhoneSESecondGen", "iPadProSecondGen-iPadProSecondGen",
        "iPadProSecondGenCellular-iPadProSecondGenCellular",
        "iPadProFourthGen-iPadProFourthGen", "iPadProFourthGenCellular-iPadProFourthGenCellular",
        "iPhone12Mini-iPhone12Mini", "iPhone12-iPhone12", "iPhone12Pro-iPhone12Pro",
        "iPhone12ProMax-iPhone12ProMax", "iPadAir4-iPadAir4", "iPadAir4Cellular-iPadAir4Cellular",
        "iPadEighthGen-iPadEighthGen", "iPadEighthGenCellular-iPadEighthGenCellular",
        "iPadProThirdGen-iPadProThirdGen", "iPadProThirdGenCellular-iPadProThirdGenCellular",
        "iPadProFifthGen-iPadProFifthGen", "iPadProFifthGenCellular-iPadProFifthGenCellular",
        "iPhone13Pro-iPhone13Pro", "iPhone13ProMax-iPhone13ProMax",
        "iPhone13Mini-iPhone13Mini", "iPhone13-iPhone13", "iPadMiniSixthGen-iPadMiniSixthGen",
        "iPadMiniSixthGenCellular-iPadMiniSixthGenCellular", "iPadNinthGen-iPadNinthGen",
        "iPadNinthGenCellular-iPadNinthGenCellular", "iPhoneSEThirdGen-iPhoneSEThirdGen",
        "iPadAirFifthGen-iPadAirFifthGen", "iPadAirFifthGenCellular-iPadAirFifthGenCellular",
        "iPhone14-iPhone14", "iPhone14Plus-iPhone14Plus", "iPhone14Pro-iPhone14Pro",
        "iPhone14ProMax-iPhone14ProMax", "iPadTenthGen-iPadTenthGen",
        "iPadTenthGenCellular-iPadTenthGenCellular", "iPadPro11FourthGen-iPadPro11FourthGen",
        "iPadPro11FourthGenCellular-iPadPro11FourthGenCellular",
        "iPadProSixthGen-iPadProSixthGen", "iPadProSixthGenCellular-iPadProSixthGenCellular",
        "iPhone15-iPhone15", "iPhone15Plus-iPhone15Plus", "iPhone15Pro-iPhone15Pro",
        "iPhone15ProMax-iPhone15ProMax", "iPadAir11M2-iPadAir11M2",
        "iPadAir11M2Cellular-iPadAir11M2Cellular", "iPadAir13M2-iPadAir13M2",
        "iPadAir13M2Cellular-iPadAir13M2Cellular", "iPadPro11M4-iPadPro11M4",
        "iPadPro11M4Cellular-iPadPro11M4Cellular", "iPadPro13M4-iPadPro13M4",
        "iPadPro13M4Cellular-iPadPro13M4Cellular", "iPhone16-iPhone16",
        "iPhone16Plus-iPhone16Plus", "iPhone16Pro-iPhone16Pro",
        "iPhone16ProMax-iPhone16ProMax", "iPadMiniA17Pro-iPadMiniA17Pro",
        "iPadMiniA17ProCellular-iPadMiniA17ProCellular", "iPhone16e-iPhone16e"
      ],
      "isGameCenterEnabled": false,
      "kind": "software",
      "artistViewUrl": "https://apps.apple.com/us/developer/tatsuyuki-shibuya/id1639823174?uo=4",
      "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/18/e8/c0/18e8c0a7-dab8-a1b3-e7f4-3dea655ffa91/AppIcon-0-0-1x_U007epad-0-1-85-220.png/100x100bb.jpg",
      "minimumOsVersion": "16.0",
      "artistName": "tatsuyuki shibuya",
      "genres": ["Lifestyle", "Utilities"],
      "price": 0.00,
      "releaseDate": "2023-02-21T08:00:00Z",
      "bundleId": "com.ame.MinnanoTanjyoubi",
      "trackName": "みんなの誕生日 -メモするだけで通知が届く管理アプリ-",
      "releaseNotes": "いつもみんなの誕生日を使っていただきありがとうございます!!\n\n今回のアップデートの変更点は以下です。\n\n・軽微なバグを修正しました。",
      "version": "4.6.8",
      "description": "\大切な人の大切な日を忘れずにお祝いするためのアプリ/...",
      "trackViewUrl": "https://apps.apple.com/us/app/id1673431227?uo=4",
      "contentAdvisoryRating": "4+",
      "languageCodesISO2A": ["JA"],
      "fileSizeBytes": "24097792",
      "formattedPrice": "Free"
    }
  ]
}

強制アップデートを実装してみる

強制アップデートを実際に実装してみたいと思います。まずは取得できるJSONレスポンスに合わせた構造体を定義しておきます。

struct AppInfo: Decodable {
    let results: [AppDetails]
}

struct AppDetails: Decodable {
    let version: String
}

あとはHTTP通信が行えれば良いのでURLSessionAlamofireなどを使用してAPIを叩きそのレスポンスからバージョンを取得できるように実装します。

今回はそれぞれの責務がわかりやすいように以下のように実装します。

View:ContentView・・・バージョン取得トリガーとアラート表示


struct ContentView: View {
    @StateObject  private var viewModel = VersionCheckViewModel()

    var body: some View {
        VStack {
            Text("アプリバージョンチェック")
                .fontWeight(.bold)
                .padding()
        }
        .onAppear {
            viewModel.checkForUpdate()
        }
        .alert(isPresented: $viewModel.showUpdateAlert) {
            Alert(
                title: Text("アップデートが必要です"),
                message: Text("最新バージョン (\(viewModel.latestVersion)) にアップデートしてください。"),
                primaryButton: .default(Text("アップデート"), action: {
                    if let url = URL(string: "https://apps.apple.com/jp/app/id\(ItunesAPIRepository.appId)") {
                        UIApplication.shared.open(url)
                    }
                }),
                secondaryButton: .cancel(Text("閉じる"))
            )
        }
    }
}

ViewModel:VersionCheckViewModel・・・Repository操作とバージョン比較


class VersionCheckViewModel: ObservableObject {
    @Published  var showUpdateAlert: Bool = false
    @Published  private(set) var latestVersion: String = ""
    
    private let repository = ItunesAPIRepository()

    public func checkForUpdate() {
        repository.fetchAppVersion { [weak self] fetchedVersion in
            guard let self, let fetchedVersion else { return }
            // 起動中のアプリのバージョンを取得
            let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
            
            /// バージョンを比較しアップデート対象ならアラート発火
            if self.isUpdateRequired(current: currentVersion, latest: fetchedVersion) {
                DispatchQueue.main.async {
                    self.latestVersion = fetchedVersion
                    self.showUpdateAlert = true
                }
            }
        }
    }

    /// バージョン比較
    private func isUpdateRequired(current: String, latest: String) -> Bool {
        return current.compare(latest, options: .numeric) == .orderedAscending
    }
}

Repository:ItunesAPIRepository・・・iTunes Search APIでバージョンフェッチ


class ItunesAPIRepository {
    
    private let endpoint: String = "https://itunes.apple.com/lookup?id="
    // FIXME: 適宜変更する
    static let appId: String = "1673431227"
    
    public func fetchAppVersion(completion: @escaping (String?) -> Void) {
        let urlString = endpoint + Self.appId
        guard let url = URL(string: urlString) else {
            completion(nil)
            return
        }

        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data, error == nil else {
                print("Error fetching data: \(error?.localizedDescription ?? "Unknown error")")
                completion(nil)
                return
            }

            do {
                let decodedData = try JSONDecoder().decode(AppInfo.self, from: data)
                let version = decodedData.results.first?.version
                DispatchQueue.main.async {
                    completion(version)
                }
            } catch {
                print("JSON decoding error: \(error.localizedDescription)")
                DispatchQueue.main.async {
                    completion(nil)
                }
            }
        }
        task.resume()
    }
}

実装してみると意外に簡単に強制アップデート機能を実装することができました。

iTunes Search APIで実装する注意点

「iTunes Search API」がベストプラクティスに思えすが注意も必要です。これはあくまでiTunes Search APIから取得できる値が変換した時に発生させることができるのでラグなどがある可能性もあります。手動の場合は確実にユーザーに対して定刻から強制アップデート通知を表示することができますが、iTunes Search APIはそうはいきません。

ストアでアップデートできるようになるタイミングとiTunes Search APIから取得できる値がリアルタイムで同じかは不明ですし、実際に公式ドキュメントを見ると以下のように記述されているので、注意はしておいた方が良いかもしれません。

App が承認されてから App Store で配信されるまで、最大で 24 時間かかります。

公式リファレンス:App の公開の概要

Androidアプリの強制アップデートはこちら

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index