はじめに
文字列→塩基配列の相互変換ツールをつくってみた(アプリ版)でつくったアプリ
これに強制アップデート機能(半強制で抜け道あり)をつけてみました。
特に必要なわけではないですがこの記事([iOS]アプリに強制アップデート機能を導入すべき理由と、簡単に実装する方法)をみてやりたいと思い実装してみました。
が!!サーバーを用意するのはめんどくさいと思いサーバーなしで強制アップデート機能のようなものをつけてみました。
iOS&Mac
方法
iTunes Search APIというのがあるらしくこれを使うとアプリの情報が取れるそうです。
下記のURLのアプリIDに指定のアプリを設定するとそのアプリ情報が取得できます。
https://itunes.apple.com/lookup?id=[アプリID]
取得した情報からバージョンをみてBundleのバージョンと比較してアプリストアに遷移させるようにすれば強制アップデートのようなことができます。
ソース
struct AppStoreModel {
private let version = Version(version: Bundle.main.version!)
private var appId: String {
#if targetEnvironment(macCatalyst)
return "1494127578"
#else
return "1493994947"
#endif
}
private var url: URL {
return URL(string: "https://itunes.apple.com/lookup?id=\(appId)")!
}
var appStoreURL: URL {
return URL(string: "itms-apps://itunes.apple.com/app/id\(appId)")!
}
func checkVersion(completion: @escaping ((Result<AppVersionState, AppVersionError>) -> ())) {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let _ = error {
completion(.failure(.network))
return
}
guard let data = data else {
completion(.failure(.invalidData))
return
}
do {
let appVersion = try JSONDecoder().decode(AppVersion.self, from: data)
if let version = appVersion.version,
Version(version: version) > self.version {
completion(.success(.shouldUpdate))
} else {
completion(.success(.noUpdate))
}
} catch {
completion(.failure(.invalidJSON))
}
}
task.resume()
}
}
struct AppVersion: Decodable {
struct Result: Codable {
let version: String
let trackName: String
}
let name: String?
let version: String?
private enum CodingKeys: String, CodingKey {
case results = "results"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let results = try container.decodeIfPresent([Result].self, forKey: .results)
name = results?.first?.trackName
version = results?.first?.version
}
}
struct Version {
let major: Int
let minor: Int
let revision: Int
}
extension Version {
init(version: String) {
let versions = version.components(separatedBy: ".")
self.major = versions[safe: 0].flatMap { Int($0) } ?? 0
self.minor = versions[safe: 1].flatMap { Int($0) } ?? 0
self.revision = versions[safe: 2].flatMap { Int($0) } ?? 0
}
static func > (lhs: Version, rhs: Version) -> Bool {
if lhs.major > rhs.major {
return true
}
if lhs.major < rhs.major {
return false
}
// lhs.major == rhs.major
if lhs.minor > rhs.minor {
return true
}
if lhs.minor < rhs.minor {
return false
}
// lhs.major == rhs.major && lhs.minor == rhs.minor
if lhs.revision > rhs.revision {
return true
}
return false
}
}
enum AppVersionError: Error {
case network
case invalidData
case invalidJSON
}
enum AppVersionState {
case shouldUpdate
case noUpdate
}
extension Bundle {
var version: String? {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
}
}
SceneDelegate
の func scene(_ scene: UIScene, willConnectTo...
に下記を記載
guard let _ = (scene as? UIWindowScene) else { return }
let appStoreModel = AppStoreModel()
appStoreModel.checkVersion { [weak self] result in
DispatchQueue.main.async {
if case .success(.shouldUpdate) = result {
let alertController = UIAlertController(title: "アップデート", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default,
handler:
{ _ in
UIApplication.shared.open(appStoreModel.appStoreURL)
})
alertController.addAction(action)
self?.window?.rootViewController?.present(alertController, animated: true)
}
}
}
なんかめっちゃ長くなった...
最初は Version(version: version) > self.version
このバージョン比較を version != self.version
にしてたのですがこれだと審査のときに常にアラートが表示されリジェクトされました
この方法はストアに遷移したあとにもう一回アプリを表示したら普通に使えるので強制アップデートとまではいえませんが、アップデートを促すことはできるのでまあいいかな。
Android
方法
in-app Updates APIというのがあるのでこれを使えばいい感じにやってくれるみたいです。
使えるのはAndroid 5.0 (API level 21) 以上です。
下記の参考サイトに丁寧に書いてくれています
- Android in-app Updates API 解説と雑感
- [Android] アプリ内アップデート AppUpdateManager / FakeUpdateManager ( in-app updates API ) のまとめ
ソース
ソースも参考サイトにあるのですが少しつまずきました...
manager.appUpdateInfo.addOnCompleteListener { task ->
val info = task.result
when (info.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
manager.startUpdateFlowForResult(info, AppUpdateType.FLEXIBLE, activity, REQUEST_CODE)
}
else -> {
}
}
}
上記のような実装をしているとエミュレータで実行すると下記のようなエラーが発生しました。
com.google.android.play.core.tasks.RuntimeExecutionException: com.google.android.play.core.internal.aa: Failed to bind to the service.
at com.google.android.play.core.tasks.l.getResult(Unknown Source:18)
at am10.dnaconverter.models.AppUpdateModel$checkAppVersion$1.onComplete(AppUpdateModel.kt:19)
at com.google.android.play.core.tasks.a.run(Unknown Source:23)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: com.google.android.play.core.internal.aa: Failed to bind to the service.
at com.google.android.play.core.internal.t.b(Unknown Source:82)
at com.google.android.play.core.internal.t.a(Unknown Source:0)
at com.google.android.play.core.internal.v.a(Unknown Source:4)
at com.google.android.play.core.internal.r.run(Unknown Source:0)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.os.HandlerThread.run(HandlerThread.java:67)
これは一部の実機でも起こるようで addOnCompleteListener
を使う場合は task
が成功したかを下記のようにしっかりチェックしないといけないようです
manager.appUpdateInfo.addOnCompleteListener { task ->
if (task.isSuccessful) {
val info = task.result
when (info.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
manager.startUpdateFlowForResult(info, AppUpdateType.FLEXIBLE, activity, REQUEST_CODE)
}
else -> {
}
}
} else {
task.exception.printStackTrace()
}
}
参考サイトは addOnSuccessListener
を使ってました
全体としては下記のような実装になりました。
gradleに下記を追加
implementation 'com.google.android.play:core:1.6.4'
implementation 'com.google.android.material:material:1.0.0' // Snackbar用
class AppUpdateModel(context: Context) {
val manager = AppUpdateManagerFactory.create(context)
var listener: InstallStateUpdatedListener? = null
val REQUEST_CODE = 100
fun checkAppVersion(activity: Activity, callback: (() -> (Unit))?) {
listener = makeListener(callback)
manager.registerListener(listener)
manager.appUpdateInfo.addOnSuccessListener { info ->
when (info.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
manager.startUpdateFlowForResult(info, AppUpdateType.FLEXIBLE, activity, REQUEST_CODE)
}
else -> {
}
}
}
}
// ミス(20200126修正ここもCompleteじゃなくてSuccessじゃないと落ちる)
fun addOnSuccessListener(callback: (() -> (Unit))?) {
manager.appUpdateInfo.addOnSuccessListener { info ->
if (info.installStatus() == InstallStatus.DOWNLOADED) {
callback?.invoke()
}
}
}
fun completeUpdate() {
manager.completeUpdate()
}
private fun makeListener(callback: (() -> (Unit))?) : InstallStateUpdatedListener {
return InstallStateUpdatedListener {
if (it.installStatus() == InstallStatus.DOWNLOADED) {
callback?.invoke()
manager.unregisterListener(listener)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
appUpdateModel = AppUpdateModel(this)
appUpdateModel.checkAppVersion(this) {
popupSnackbarForCompleteUpdate()
}
}
override fun onResume() {
super.onResume()
appUpdateModel.addOnSuccessListener {
popupSnackbarForCompleteUpdate()
}
}
private fun popupSnackbarForCompleteUpdate() {
Snackbar.make(findViewById(R.id.root_layout),
"ダウンロード完了", Snackbar.LENGTH_INDEFINITE)
.setAction("更新") {
appUpdateModel.completeUpdate()
}
.show()
}
Google Playストアのアプリが更新判定を行うらしいのでどの時点で更新情報が受け取れるのかはわかりません。
これも戻るボタンとかで回避できるらしいので強制アップデートとまではいえないかもしれません。(そもそもGoogle Playストアのアプリがないと取れない?)
20200127追記
やっぱりストアに公開されてから即時反応というわけではなく、2端末(Android 6.0.1, 9.0)で確認したところ、公開されてから2~3時間後は特に反応はなく、10時間後に再度アプリを起動したところ無事下記の画面が表示されました
おまけ
サーバー用意してバージョン情報のJSONファイル置くのめんどくさいと思って今回の方法で実装しましたが、GitHubにJSONファイル置けばいいんじゃね?ふと思いました。(iOSアプリをリリースしてればもしかしたらプライバシーポリシー用にGitHub使ってるかもしれないですし)
試しにJSONファイル置いてるリポジトリでやってみたらJSON取れました
github.com
のところを raw.githubusercontent.com
するといけそうです!!
さいごに
特にこのアプリに強制アップデート必要ないですが、試しに実装してみましたmm
関係ないですがAndroidのアプリを最適化しようと思って下記をgradleに追加したらサイズがめっちゃ小さくなりました。
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
before | after |
---|---|
アプリサイズ 50%OFF !!!