[SwiftUI] BackgroundTaskを導入した際に Environment と EnvironmentObject を同時に使用して起こった不具合

# やりたいこと

iOSの`Capability`の機能`Background Task`を使用してバックグラウンドで位置情報を取得する機能を追加したい


# 実装

`LaunchPage`はスプラッシュ画面。`LaunchPage`で Firebase Firestore からドキュメントを取得して、
ドキュメントの内容に応じてグローバルな状態を決定する。(`LaunchPageViewModel`)。

```swift
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を有効にするため以下のコードを追加

```swift
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の変化を読み取れるようにした


```swift
//
//  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()`があるレベルでオブジェクトをインスタンス化しておく必要がある。

**改善前**
```swift
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()) // ⭐️
    }
  }
}
```

**改善後**

```swift
//
//  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")
    }
  }
}
 ```