【Swift】よく使うアプリの起動時の分岐処理

Swift

私がよく使うアプリの起動時処理を紹介します。

アプリの起動時に行いたい処理はいくつかあると思います。
私は初回起動時に初期データの追加、チュートリアルの表示、ATTダイアログの表示を行います。
また更新後の起動時には更新内容の表示を行います。

このように大きく、初回起動、更新後、通常起動の3パターンに分けて起動処理を行っています。

アプリバージョンの取得

アプリバージョンの取得は以下のコードで行えます。

Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""

nilだった場合に空文字を返していますが、この辺はお好みでどうぞ。
基本的に正しく書けていれば失敗しない筈なので、
失敗時は強制アンラップで落とした方がミスが分かりやすくて良い気がします。

TARGETSのGeneralタブに設定したVersionの値が取得できます。
なので更新毎に個別で設定する必要はなく、
AppStoreに申請する為に設定を変更するとそのまま反映されます。

起動時の分岐処理

どこでも使い回ししやすいように専用のクラスを一つ作っています。
@AppStorage(UserDefaults)に保存した値と現在のアプリバージョンを比較し、enumで値を返しています。

AppStorage版

import SwiftUI

enum LaunchStatus {
    case FirstLaunch
    case NewVersionLaunch
    case Launched
}

class LaunchUtil {
    static let launchedVersionKey = "launchedVersion"
    @AppStorage(launchedVersionKey) static var launchedVersion = ""
    
    static var launchStatus: LaunchStatus {
        get{
            let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
            let launchedVersion = self.launchedVersion
            
            self.launchedVersion = version
            
            if launchedVersion == "" {
                return .FirstLaunch
            }
            
            return version == launchedVersion ? .Launched : .NewVersionLaunch
        }
    }
}

AppStorageを使うと変数として使用するとSave/Loadを勝手にしてくれるので非常にシンプルです。
デフォルト値は今回は空文字としていますが、好きな値を設定しても良いでしょう。

AppStorageをView以外で使っていますが今回の場合は特に気にする必要はありません。
詳細は余談を確認して下さい。

また、AppStorageを使用していますが、
UIKitで使用しても特に問題はありません。
SwiftUIをimportするだけで使えます。

UserDefaults版

enum LaunchStatus {
    case FirstLaunch
    case NewVersionLaunch
    case Launched
}

class LaunchUtil {
    static let launchedVersionKey = "launchedVersion"
    
    static var launchStatus: LaunchStatus {
        get{
            let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
            let launchedVersion = self.loadLaunchedVersion()
            
            self.saveLaunchedVersion()
            
            if launchedVersion == "" {
                return .FirstLaunch
            }
            
            return version == launchedVersion ? .Launched : .NewVersionLaunch
        }
    }
    
    
    static func saveLaunchedVersion(){
        let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
        UserDefaults.standard.set(version, forKey: self.launchedVersionKey)
    }
    
    static func loadLaunchedVersion() -> String{
        let version = UserDefaults.standard.string(forKey: self.launchedVersionKey) ?? ""
        return version
    }
}

お馴染みのUserDefaultsを使ったパターンです。
Save/Loadが長くなりましたが、特別な事はやってません。

敢えて言うならば、UserDefaultsの場合は初期値が空文字で固定なので、
初期値に好きな値を設定する場合は別途設定しておく必要がある点でしょうか。
空文字でも特に不都合はないと思いますが。

サンプルコードのsaveLaunchedVersionでバージョンの取得を再度行なっているのは、
おそらくこれを書いた当時は単独で呼ぶ可能性も考えていたんだと思います。

利用例

ざっくりと、どこで使うかの例です。
細かい処理を含めた例は後述。

Appのinit

分かりやすくアプリのinitで行います。
初期データの追加やFireBaseの初期化処理などに向いてます。
※AdMobなどATTダイアログが必要なものはinitは向きません。

Viewがらみの処理を含める場合にはなんらかの変数に値を持たせて、
該当Viewに渡す必要があるので多少面倒かも?

import SwiftUI

@main
struct LaunchProcessApp: App {
    
    init(){
        switch LaunchUtil.launchStatus {
            case .FirstLaunch : //初回起動
            case .NewVersionLaunch : //更新時
            case .Launched: //通常起動
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Viewのinit

AppやるよりはViewでやっているので表示処理に使いやすいです。
ただし@Stateな変数をinitでは初期化できない(強引にできない事もないが推奨されない)

import SwiftUI

struct ContentView: View {
    
    init(){
        switch LaunchUtil.launchStatus {
            case .FirstLaunch : //初回起動
            case .NewVersionLaunch : //更新時
            case .Launched: //通常起動
        }
    }
    
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

onAppear

私がよく使うのはここです。
@Stateも変数も問題なく使えるので、View絡みの処理も行いやすいです。
sheetでチュートリアルや更新内容を表示したりします。

onAppearなので複数回呼ばれる可能性がある事だけは注意が必要です。

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear{
                switch LaunchUtil.launchStatus {
                    case .FirstLaunch : //初回起動
                    case .NewVersionLaunch : //更新時
                    case .Launched: //通常起動
                }
            }
    }
}

AppDelegate

SwiftUI1.0系やUIKit向け

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        switch LaunchUtil.launchStatus {
            case .FirstLaunch : print("FirstLaunch")
            case .NewVersionLaunch : print("NewVersionLaunch")
            case .Launched: print("Launched")
        }
        
        return true
    }

    //略
}

SceneDelegate

SwiftUI1.0系やUIKit向け
iPadOSなどマルチシーン対応の場合は複数回呼ばれるので注意。

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        switch LaunchUtil.launchStatus {
            case .FirstLaunch : print("FirstLaunch")
            case .NewVersionLaunch : print("NewVersionLaunch")
            case .Launched: print("Launched")
        }
        
        guard let _ = (scene as? UIWindowScene) else { return }
    }

    //略
}

具体的な処理例

私がよく使うパターンを紹介します。

初回起動時にCoreDataへの初期データ追加、
更新時は更新内容の提示、
通常時は特に処理なしです。

onAppearパターンで行います。

struct ContentView: View {
    
    @Environment(\.managedObjectContext) private var viewContext
    @State var tutorialSheet = false
    @State var versionNoticeSheet = false
    
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear{
                switch LaunchUtil.launchStatus {
                    case .FirstLaunch :
                        self.addDefaultData()
                        self.tutorialSheet = true
                    case .NewVersionLaunch :
                        self.versionNoticeSheet = true
                    case .Launched:
                        break
                }
            }
            .sheet(isPresented: self.$tutorialSheet){
                Text("Tutorial")
            }
            .sheet(isPresented: self.versionNoticeSheet){
                Text("VersionNotice")
            }
    }
    
    func addDefaultData(){
        //CoreDataへのデータ追加 略
    }
}

onAppearなので複数回呼ばれる可能性がありますが、
Launchedには処理はなく、2回目以降は必ずこちらが呼ばれるので問題ありません。

FireBaseはinitで初期化してしまっているのでここにはありません。
AdMobについては少々面倒になっているので余談を見て下さい。

余談

AppStorageの注意

AppStorageをView以外で使う際は若干注意が必要ですが、
今回は基本的に1箇所で1回のみ動作すればOKな上なので問題ありません。
更にstaticな変数としているので大丈夫でしょう。

注意が必要なのはView以外で複数のインスタンスから同じAppStorageにアクセスする場合です。

詳しくは以下の記事を参照して下さい。

AppStorageとUserDefaults

AppStorageとUserDefaultsは読み書きする先が同じです。
同じKeyを使えばどちらを使っても同じ値が取得できます。
ですので移行も簡単です。

AppStorageはSwiftUI向けに使いやすいようにPropertyWrapperになったものです。
多少癖はありますがUserDefaultsよりも簡潔に書けるので使ってみると良いでしょう。

AdMobの初期化はどこで行う?

AdMobの初期はATTダイアログの仕様変更で面倒になりました。
Viewの表示が完了している状態で呼び出さなければATTダイアログが出ないようになりました。

その為、初回起動時だけは必ずViewの表示が完了している状態で行います。
2回目移行はATTダイアログは出ないので初期化してしまっても問題ありません。

import SwiftUI

import AppTrackingTransparency
import GoogleMobileAds

struct ContentView: View {
    
    @Environment(\.managedObjectContext) private var viewContext
    @State var tutorialSheet = false
    @State var versionNoticeSheet = false
    
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear{
                switch LaunchUtil.launchStatus {
                    case .FirstLaunch :
                        self.addDefaultData()
                        self.tutorialSheet = true
                    case .NewVersionLaunch :
                        self.versionNoticeSheet = true
                        self.initAdMob() //AdMob初期化
                    case .Launched:
                        self.initAdMob() //AdMob初期化
                }
            }
            .sheet(isPresented: self.$tutorialSheet){
                Text("Tutorial")
                    .onDisappear{
                        self.initAdMob() //AdMob初期化
                    }
            }
            .sheet(isPresented: self.$versionNoticeSheet){
                Text("VersionNotice")
            }
    }
    
    func initAdMob(){
        ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
            // Tracking authorization completed. Start loading ads here.
            GADMobileAds.sharedInstance().start(completionHandler: nil)
        })
    }
    
    func addDefaultData(){
        //略
    }
}

初回起動時はTutorial SheetのonDisappearでAdMobの初期化を行なっています。
Tutorialが不要ならATTダイアログの説明をsheetで出してもいいでしょう。

2回目以降はswitchの分岐後に出してしまっています。
複数回onAppearが呼ばれ、複数回初期化されるのが気になる場合は、
何らかのフラグを持たせて1回のみにすると良いと思います。

詳しくは以下の記事を参照

コメント

タイトルとURLをコピーしました