0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2024-02-25

やりたいこと

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")
    }
  }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?