iOS 新規アップデートを自動でユーザーに知らせる

最新バージョンのアップデートがあることをユーザーに知らせ、アップデートに誘導したい。
毎回手動でアプリ内にハードコーディングしたり、Firebase Remote Configで制御するのは運用コストが上がりバグの原因にもなる。
この操作を完全に自動化する。

### はじめに
具体的にはAppStoreのAPIを利用して最新バージョンを取得する。
`https://itunes.apple.com/lookup?id={APP ID}&country=JP`
このAPIを叩くとAppStoreに乗っている様々な情報を取得できる。例えば、最新バージョン、最小のOS、リリースノート、ファイルサイズなどAppStoreに表示されているほとんどの情報が取得できる。ただ、国ごとにLocalizeしている場合Queryで`country=JP`などと指定する必要がある。
今回はこのうち必要な情報だけパースすることにする。

以下を取得する
```Swift
public import Foundation

public struct AppStoreAppInfoResponse: Sendable, Codable {
  public let results: [AppStoreAppInfo]
  public init(results: [AppStoreAppInfo]) {
    self.results = results
  }
}

public struct AppStoreAppInfo: Sendable, Codable, Hashable {
  public let version: String
  public let minimumOsVersion: String
  public let trackViewUrl: URL
  public let releaseNotes: String
  public let description: String
  public let fileSizeBytes: String
  public init(version: String, minimumOsVersion: String, trackViewUrl: URL, releaseNotes: String, description: String, fileSizeBytes: String) {
    self.version = version
    self.minimumOsVersion = minimumOsVersion
    self.trackViewUrl = trackViewUrl
    self.releaseNotes = releaseNotes
    self.description = description
    self.fileSizeBytes = fileSizeBytes
  }
}
```
次に取得したバージョンと現在のバージョンを比較するためComparableに準拠したStructを定義する。

``` Swift
import Foundation

public struct AppSemanticVersion: Sendable, Codable, Hashable {
  public struct VersionError: Error {}

  let major: Int
  let minor: Int
  let patch: Int

  public init(major: Int, minor: Int, patch: Int) {
    self.major = major
    self.minor = minor
    self.patch = patch
  }

  public init(from versionString: String) throws {
    let versionNumbers = versionString.split(separator: ".").compactMap { Int($0) }
    guard versionNumbers.count == 3 else {
      throw VersionError()
    }

    self.major = versionNumbers[0]
    self.minor = versionNumbers[1]
    self.patch = versionNumbers[2]
  }

  public func versionString() -> String {
    "\(major).\(minor).\(patch)"
  }
}

// MARK: Protocol Conformance

extension AppSemanticVersion: Comparable {
  public static func < (lhs: AppSemanticVersion, rhs: AppSemanticVersion) -> Bool {
    if lhs.major != rhs.major {
      return lhs.major < rhs.major
    } else if lhs.minor != rhs.minor {
      return lhs.minor < rhs.minor
    } else {
      return lhs.patch < rhs.patch
    }
  }
}

extension AppSemanticVersion: Equatable {
  public static func == (lhs: AppSemanticVersion, rhs: AppSemanticVersion) -> Bool {
    return lhs.major == rhs.major &&
    lhs.minor == rhs.minor &&
    lhs.patch == rhs.patch
  }
}
```

これで準備が整ったので実際に最新バージョンを取得するためのAPIを叩く

``` Swift

  /// AppStoreから最新のバージョン情報を取得.
  func getLatestVersionInfo() async -> AppStoreAppInfo? {
    guard let url = URL(string: "https://itunes.apple.com/lookup?id={Apple ID}&country=JP") else { return nil }
    let request = URLRequest(url: url)
    guard let (data, _) = try? await URLSession.shared.data(for: request) else { return nil }
    let response = try? JSONDecoder().decode(AppStoreAppInfoResponse.self, from: data)
    return response?.results.first
  }
```
また、現在のアプリのバージョンを`Bundle.main`から取得
``` Swift
let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")
```

取得したバージョンが最新か調べる
``` Swift
/// AppStoreから取得したAppStoreAppInfoから現在のバージョンが最新であるか確認する
/// フォーマットが不正なことはまず起こり得ないが、起こった場合はtrueを返す.
func isLatestVersion(currentVersion: String, appInfo: AppStoreAppInfo) -> Bool {
    guard let currentAppVersion = try? AppSemanticVersion(from: currentVersion) else { return true }
    guard let newAppVersion = try? AppSemanticVersion(from: appInfo.version) else { return true }
    return currentAppVersion >= newAppVersion
}
```

以上で最新バージョンを自動で取得でき、最新バージョンがAppStoreに上がっていれば自動でユーザーに知らせることができる

``` Swift 
guard let latestVersionInfo = await getLatestVersionInfo(),
      let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String else {
      return 
}
if !isLatestVersion(currentVersion: currentVersion, appInfo: latestVersionInfo) {
    /// アラートなどでユーザーに知らせる
    /// AppStoreAppInfoで最新バージョンのリリースノートもパースしているのでこれを直接表示するなど
}
```