やりたいこと
iOSのCapability
の機能Background Task
を使用してバックグラウンドで位置情報を取得する機能を追加したい
実装
LaunchPage
はスプラッシュ画面。LaunchPage
で Firebase Firestore からドキュメントを取得して、
ドキュメントの内容に応じてグローバルな状態を決定する。(LaunchPageViewModel
)。
import FirebaseAppCheck
import FirebaseCore
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let providerFactory = AppCheckDebugProviderFactory()
AppCheck.setAppCheckProviderFactory(providerFactory)
FirebaseApp.configure()
return true
}
}
@main
struct DeviceFinderIOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
LaunchPage()
.environmentObject(LaunchPageViewModel())
}
}
}
追加したこと
Background Taskを有効にするため以下のコードを追加
import FirebaseAppCheck
import FirebaseCore
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let providerFactory = AppCheckDebugProviderFactory()
AppCheck.setAppCheckProviderFactory(providerFactory)
FirebaseApp.configure()
return true
}
}
@main
struct DeviceFinderIOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@Environment(\.scenePhase) private var phase // +
var body: some Scene {
WindowGroup {
LaunchPage()
.environmentObject(LaunchPageViewModel())
}
.onChange(of: phase) { newPhase in // +
switch newPhase { // +
case .background: scheduleAppRefresh() // +
default: break // +
} // +
} // +
.backgroundTask(.appRefresh(GEOLOCATION_REFRESH_TASK_IDENTIFIRE)) { // +
print("TODO: Write your code you want to execute") // +
} // +
}
}
問題
@Environmentを追加することによって、 @EnvironmentObject の値が変化しても LaunchPage
から次のページへ遷移しなくなってしまった。
対策
LaunchPage
で@State変数を用意してLaunchPage
内でstateの変化を読み取れるようにした
//
// LaunchPage.swift
// DeviceFinderIOS
//
// Created by KaitoKitaya on 2024/02/18.
//
import SwiftUI
struct LaunchPage: View {
let documentRepository = DocumentRepositoryImpl()
@EnvironmentObject var launchStateViewModel: LaunchPageViewModel
@State var state: DeviceRegisterState = .pending // +
var body: some View {
if state == .pending {
VStack(alignment: .center) {
Image(.splash)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}.onAppear {
Task {
state = launchStateViewModel.deviceRegisterState
// TODO: Firebase Fetch Error Handling
do {
let uuid = Util.getDeviceUUID() ?? ""
let deviceList = try await documentRepository.getAllDocuments(completion: nil)
if deviceList.map({ it in it.device_id }).contains(uuid) {
launchStateViewModel.deviceRegisterState = .registered
state = launchStateViewModel.deviceRegisterState // +
} else {
launchStateViewModel.deviceRegisterState = .notRegisterd
state = launchStateViewModel.deviceRegisterState // +
}
print("Has Registered ?: \(launchStateViewModel.deviceRegisterState)")
} catch {
print("[Launch Page]: \(error)")
}
}
}
} else {
EntrancePage()
}
}
}
#Preview{
LaunchPage()
}
結論
ワークアラウンド対応で解決したが、グローバルな状態をもつオブジェクトやメソッドは何かと不具合が発生しやすいので、できるだけローカル変数で状態を保持しておきたい。
追記
原因は⭐️の部分で、environmentObject
に毎回新しいインスタンスを作成し注入していたことが原因で画面描画のたびに不必要に状態が初期化されていた。したがって@main()
があるレベルでオブジェクトをインスタンス化しておく必要がある。
改善前
import FirebaseAppCheck
import FirebaseCore
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let providerFactory = AppCheckDebugProviderFactory()
AppCheck.setAppCheckProviderFactory(providerFactory)
FirebaseApp.configure()
return true
}
}
@main
struct DeviceFinderIOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
LaunchPage()
.environmentObject(LaunchPageViewModel()) // ⭐️
}
}
}
改善後
//
// DeviceFinderIOSApp.swift
// DeviceFinderIOS
//
// Created by KaitoKitaya on 2024/02/18.
//
import FirebaseAppCheck
import FirebaseCore
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let providerFactory = AppCheckDebugProviderFactory()
AppCheck.setAppCheckProviderFactory(providerFactory)
FirebaseApp.configure()
return true
}
}
@main
struct DeviceFinderIOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@Environment(\.scenePhase) private var phase
@StateObject private var launchPageViewModel = LaunchPageViewModel()
var body: some Scene {
WindowGroup {
LaunchPage().environmentObject(launchPageViewModel)
}
.onChange(of: phase) { newPhase in
switch newPhase {
case .background: scheduleAppRefresh()
default: break
}
}
.backgroundTask(.appRefresh(GEOLOCATION_REFRESH_TASK_IDENTIFIRE)) {
print("TODO: Write your code you want to execute")
}
}
}