【iOS】アプリ内にアップデート機能を導入する方法

日々iOSのプログラミングをしております。

さて、アプリケーションを作成しているとアップデートをしないといけない時がきます。
（データベースの書き換え、新機能の導入）

そこで強制アップデートや任意アップデートの機能を導入するやり方を記載していこうと思います。

##VersionUpdate.h

~~~ObjectiveC
//
//  SSVersionUpdater.h
//  SampleCodeUpdate
//
//  Created by http://sunstripe.main.jp on 2017/02/22.
//  Copyright © 2017年 Sunstripe. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@class SSVersionUpdater;
@protocol SSVersionUpdaterDelegate <NSObject>

@optional

- (void)versionUpdater:(SSVersionUpdater *)versionUpdater successedResponse:(NSDictionary *)responseObject;
- (void)versionUpdater:(SSVersionUpdater *)versionUpdater successedNoResponse:(NSDictionary *)responseObject;
- (void)versionUpdater:(SSVersionUpdater *)versionUpdater failuredError:(NSError *)error;

@end

/**
 * バージョンチェックを実行する
 */
@interface SSVersionUpdater : NSObject

/**
 * バージョンチェックを実行する
 */
- (void) executeVersionCheck;

@property NSString *customAlertTitle;
@property NSString *customAlertBody;
@property NSString *customAlertUpdateButtonTitle;
@property NSString *customAlertCancelButtonTitle;

@property (nonatomic,assign) id<SSVersionUpdaterDelegate> delegate;

@end 
~~~

##VersionUpdate.m

~~~ObjectiveC
//
//  SSVersionUpdater.m
//  SampleCodeUpdate
//
//  Created by http://sunstripe.main.jp on 2017/02/22.
//  Copyright © 2017年 sunstripe. All rights reserved.
//

#import "SSVersionUpdater.h"
//#import "AFHTTPSessionManager.h" // 参照：http://dev.classmethod.jp/smartphone/afsessionmanager/

/**
 * バージョンチェックを実行する
 */
@interface SSVersionUpdater () {
    NSDictionary *versionInfo;
    NSDictionary *responseInfo;
}

@end

@implementation SSVersionUpdater

/**
 * 開発環境の文字列を返す
 */
+ (NSString *)environmentString {
#ifdef DEBUG
    return @"WHITE";
#elif STAGE
    return @"STAGE";
#else
    return @"RELEASE";
#endif
}
//環境別に DEBUG = 1,STAGE = 1　Xcode のビルドターゲットを変更するようにすると便利。

/**
 * AppleStoreのURL文字列を返す
 */
+ (NSString *)appleStoreURLString {
    return @"https://itunes.apple.com/jp/app/id############?mt=8"; // ############### アプリを作成する時の　アプリIDになります。
}


/**
 * appVersionUpdate アラートの情報を取得する
 */
+ (NSDictionary *)appVersionUpdateAlertInfo {
    // ここについては、Plistなどを使ってURL管理をしたりすると便利です。
    return @{
             @"alert.title":@"アップロードのお知らせ",
             @"alert.body":@"最新バージョンのアプリが公開されています。AppStoreで最新のアプリにアップデートをお願いいたします",
             @"alert.update.button.title":@"AppStoreへ",
             @"alert.cancel.button.title":@"キャンセル",
             };
}

/**
 * appVersionUpdate APIを取得する
 */
+ (NSDictionary *)appVersionUpdateAPIInfo {
    // ここについては、Plistなどを使ってURL管理をしたりすると便利です。
    return @{
             @"name":@"アプリバージョンアップデートAPI",
             @"description":@"アプリのバージョンアップデートの任意か強制か必要なしかどうかのAPI",
             @"WHITE": @{
                     @"URL":@""//開発環境用のappVersionUpdateAPIURL
                     },
             @"STAGE": @{
                     @"URL":@""//試験環境用のappVersionUpdateAPIURL
                     },
             @"RELEASE": @{
                     @"URL":@""//本番環境用のappVersionUpdateAPIURL
                     },
             @"HTTPStatusCode" : @{
                     // 1000番台　案内（インフォメーション）
                     // 2000番台　正常処理
                     @"2000":@"[正常処理] 正常に処理されました",
                     // 3000番台　移転通知
                     @"3000":@"[移転通知] アプリケーションを削除されました",
                     // 4000番台　処理失敗
                     @"4000":@"[処理失敗]　パラメータが間違っています",
                     // 5000番台　サーバーエラー
                     @"5000":@"[サーバーエラー]　システムエラーです",
                     },
             };
}

/**
 * バージョンチェックを実行する
 */
- (void) executeVersionCheck {
    
    NSDictionary *appVersionUpdateAPI = [SSVersionUpdater appVersionUpdateAPIInfo];
    // 開発環境を取得
    NSString *appVersionUpdateAPIURL = appVersionUpdateAPI[[SSVersionUpdater environmentString]][@"URL"];
    NSAssert(appVersionUpdateAPIURL, @"Set EndPointUrl Before Execute Check");
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain",@"application/json",nil];
    
    [manager GET:appVersionUpdateAPIURL parameters:[self sendParameters] progress:^(NSProgress * _Nonnull downloadProgress) {
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        [self successedResponse:responseObject];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"Request Operation Error! %@", error);
        [self failuredError:error];
    }];
}

/**
 * APIに必要な項目を記載
 */
- (NSDictionary *)sendParameters {
    NSMutableDictionary *param = [NSMutableDictionary new];
    param[@"application_name"] = @"SampleCodeUpdate";//アプリ名
    NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
    param[@"version"] = version; //1.10.0
    param[@"device_type"] = @"1";//iOS 1 Android 2
    param[@"status"] = @""; //未使用
    return [param copy];
}

/**
 * 必要に応じてアップデートのアナウンスを表示する
 */
- (void) showUpdateAnnounceIfNeeded {
    if(![self isVersionUpNeeded] ) {
        return;
    }
    [self showUpdateAnnounce];
}

/**
 * バージョンアップが必要かどうか
 */
- (BOOL) isVersionUpNeeded {
    BOOL neededVersionUpdate = [versionInfo[@"needed_version_update"] boolValue];
    return neededVersionUpdate;
}

/**
 * アップデートのアナウンスを表示する
 */
- (void) showUpdateAnnounce {
    // アラートの準備
    UIAlertController *alert = [UIAlertController
                                alertControllerWithTitle:[self alertTitle]
                                message:[self alertBody]
                                preferredStyle:UIAlertControllerStyleAlert
                                ];
    
    // force
    if ([versionInfo[@"update_type"] isEqualToString:@"force_update"]) {
        // 強制アップロード
        // 処理
    }
    
    // optional
    if([versionInfo[@"update_type"] isEqualToString:@"optional_update"]){
        // 任意アップロード
        [alert addAction:[UIAlertAction actionWithTitle:[self cancelButtonTitle]
                                                  style:UIAlertActionStyleDefault
                                                handler:^(UIAlertAction * _Nonnull action) {
                                                    if ([self.delegate respondsToSelector:@selector(versionUpdater:successedNoResponse:)]) {
                                                        [self.delegate versionUpdater:self successedNoResponse:responseInfo];
                                                    }
                                                }]];
    }
    
    // update_url
    [alert addAction:[UIAlertAction actionWithTitle:[self updateButtonTitle]
                                              style:UIAlertActionStyleDefault
                                            handler:^(UIAlertAction * _Nonnull action) {
                                                if ([self.delegate respondsToSelector:@selector(versionUpdater:successedResponse:)]) {
                                                    [self.delegate versionUpdater:self successedResponse:responseInfo];
                                                }
                                                NSURL *updateURL = [NSURL URLWithString:[self updateURLString]];
                                                [[UIApplication sharedApplication] openURL:updateURL];
                                            }]];
    
    // 最前面のViewControllerを取得する
    UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
    while (topController.presentedViewController) {
        topController = topController.presentedViewController;
    }
    
    [topController presentViewController:alert animated:YES completion: ^ (void) {
        NSLog(@"%@",@"[DEBUG] showUpdateAnnounce");
    }];
}

/**
 * アップデートURL
 */
- (NSString *) updateURLString {
    NSString *updateURLString = [SSVersionUpdater appleStoreURLString];
    if (versionInfo[@"update_url"]) {
        updateURLString = versionInfo[@"update_url"];
    }
    return updateURLString;
}

/**
 * アラートのタイトル
 */
- (NSString *) alertTitle {
    NSString *alertText = [SSVersionUpdater appVersionUpdateAlertInfo][@"alert.title"];
    if (!alertText) {
        alertText = @"アップロードのお知らせ";
    }
    if (_customAlertTitle) {
        alertText = _customAlertTitle;
    }
    if (versionInfo[@"update_alert_title"]) {
        alertText = versionInfo[@"update_alert_title"];
    }
    return alertText;
}

/**
 * アラートの本文
 */
- (NSString *) alertBody {
    NSString *alertBodyText = [SSVersionUpdater appVersionUpdateAlertInfo][@"alert.body"];
    if (!alertBodyText) {
        alertBodyText = @"最新バージョンのアプリが公開されています。AppStoreで最新のアプリにアップデートをお願いいたします";
    }
    if (_customAlertBody) {
        alertBodyText = _customAlertBody;
    }
    if (versionInfo[@"update_alert_body"]) {
        alertBodyText = versionInfo[@"update_alert_body"];
    }
    return alertBodyText;
}

/**
 * アップデートのボタン
 */
- (NSString *) updateButtonTitle {
    NSString *updateButtonText = [SSVersionUpdater appVersionUpdateAlertInfo][@"alert.update.button.title"];
    if (!updateButtonText) {
        updateButtonText = @"AppStoreへ";
    }
    if (_customAlertUpdateButtonTitle) {
        updateButtonText = _customAlertUpdateButtonTitle;
    }
    if (versionInfo[@"update_update_button_title"]) {
        updateButtonText = versionInfo[@"update_update_button_title"];
    }
    return updateButtonText;
}

/**
 * 任意アップデート用のキャンセルのボタン
 */
- (NSString *) cancelButtonTitle {
    NSString *cancelButtonText = [SSVersionUpdater appVersionUpdateAlertInfo][@"alert.cancel.button.title"];
    if (!cancelButtonText) {
        cancelButtonText = @"キャンセル";
    }
    if (_customAlertCancelButtonTitle) {
        cancelButtonText = _customAlertCancelButtonTitle;
    }
    return cancelButtonText;
}

#pragma - mark API

/**
 * API呼び出し　成功時に呼ばれるメソッド
 */
- (void) successedResponse : (NSDictionary *)responseObject {
    //成功時パラメータを格納
    responseInfo = responseObject;
    NSInteger code = [responseInfo[@"code"] integerValue];
    [SSVersionUpdater showMessageServerHttpStatusCode:code];
    if ([self isSuccessed]) {
        //成功時パラメータの格納
        versionInfo = responseInfo[@"version_info"];
        if (versionInfo) {
            [self showUpdateAnnounceIfNeeded];
        } else {
            if ([self.delegate respondsToSelector:@selector(versionUpdater:successedNoResponse:)]) {
                [self.delegate versionUpdater:self successedNoResponse:responseInfo];
            }
        }
    } else {
        NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:code userInfo:responseInfo];
        [self failuredError:error];
    }
}

/**
 * API呼び出し　失敗時に呼ばれるメソッド
 */
- (void) failuredError : (NSError *)error {
    if ([self.delegate respondsToSelector:@selector(versionUpdater:failuredError:)]) {
        [self.delegate versionUpdater:self failuredError:error];
    }
}

- (BOOL)isSuccessed {
    return ([responseInfo[@"result"] isEqualToString:@"successed"]);
}

+ (void)showMessageServerHttpStatusCode:(NSInteger )HTTPStatusCode
{
    NSString *HTTPStatusCodeString = [NSString stringWithFormat:@"%ld",(long)HTTPStatusCode];
    NSLog(@"ServerHttpStatusCode : %@",[SSVersionUpdater appVersionUpdateAPIInfo][@"HTTPStatusCode"][HTTPStatusCodeString]);
}

@end
~~~

以下、APIの返答の例

###JSON アップデートなし

~~~JSON
{
 "version_info":{
   "update_type":"no_update",
   "needed_version_update":0,
 },
 "result":"successed",
 "code":2000,
 "messages":[
   "No Update"
 ],
}
~~~

###JSON 任意アップデート

~~~JSON
{
 "version_info":{
   "update_type":"optional_update",
   "needed_version_update":1,
 },
 "result":"successed",
 "code":2000,
 "messages":[
  "Optional Update"
 ],
}
~~~

###JSON 強制アップデート

~~~JSON
{
 "version_info":{
   "update_type":"force_update",
   "needed_version_update":1,
 },
 "result":"successed",
 "code":2000,
 "messages":[
  "Force Update"
 ],
}
~~~

###JSON 失敗 例：パラメータエラー

~~~JSON
{
 "result":"failured",
 "code":4000,
 "messages":[
  "Parameter Error"
 ],
}
~~~

###JSON アプリケーション削除

~~~JSON
    {
        "version_info":{
            "update_type":"optional_update",
            "needed_version_update":1,
            "update_url":"http://sunstripe.main.jp",
            "update_alert_title":"アプリ削除のお知らせ",
            "update_alert_body":"サポートが終了いたしました。ご利用ありがとうございました",
            "update_update_button_title":"サポートサイトへ";
        },
        "result":"successed",
        "code":3000,
        "messages":[
                    "Not Support Update"
                    ],
    }

~~~

## 関連記事

【About】(http://qiita.com/sunstripe) - サンストライプ

---

## 制作チーム：サンストライプ

[![sunstripe_logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80600/322f2d60-f19d-af9e-2b6f-7290ca546848.png)](http://sunstripe.main.jp/)
http://sunstripe.main.jp/

(月１WEBコンテンツをリリースして便利な世の中を作っていくぞ！！ボランティアプログラマー/デザイナー/イラストレーター/その他クリエイター声優募集中！!)

### 地域情報 THEメディア
THE メディア 地域活性化をテーマに様々なリリース情報も含め、記事をお届けしてます！！
https://the.themedia.jp/

### ゼロからはじめる演劇ワークショップ
多様化の時代に向けて他者理解を鍛える

https://workshop.themedia.jp/

### プログラミングワークショップ・ウェブ塾の開講！！！
様々なテーマでプログラミングに囚われずに取り組んでいきます。
詳しくはこちら　↓　↓　↓　↓　↓　↓　↓　↓
[プログラミングサロン 月１だけのプログラミング学習塾](https://workshop.themedia.jp/posts/7057553?fbclid=IwAR32CfRQ7RvDOhvuogoSwqxDLMOU7Jvuzljlnx-TMc27qBPX1BnWsfLcBR8
)

### 協力応援 / 支援者の集い
## チーム：サンストライプ
#### プログラミングラボ
https://twitter.com/sunstripe2011

一緒にポートフォリオを作りませんか？現場の体験やそれぞれの立場から年齢関係なく作品を作りたい方々と一緒にチームを作って、作品を作っています。現場に行きたい人には、職場紹介や職場の体験や悩み相談なども受けております。
様々な職種からプログラミングの知識を得たい、デザインの知識を得たい、データーベースの知識を得たいという人が集まっております。
週１のミーティングにそれぞれの近況と作業報告して、たまにリモート飲み会などをしております！！

興味がある方は、DMに話しかけてみてください。

## トラストヒューマン
http://trusthuman.co.jp/
私たちは何よりも信頼､人と考えてます｡

｢コンサルティング｣と｢クリエイティブ｣の両角度から｢人材戦略パートナー｣としてトータル的にサポートします！！

キャリア教育事業
広域学習支援プラットフォーム『のびのび日和』
https://slc-lab.amebaownd.com/

#スポンサー募集