Where do I handle the initial value before the first onChange(of:)?

Viewed 528

I want to load some data when my app is first launched, and when the app is foregrounded I want to ensure I have the latest data.

The state is stored in a ViewModel class, which my view owns as a @StateObject. I read the ScenePhase from the Environment, and in onChange(of: scenePhase), I call a method on my ViewModel to start the reload if needed.

But when should I start the initial load?

  • ContentView.init is too early, because scenePhase is .background. And even if it were .active, I'm apparently not supposed to access StateObject from init — SwiftUI logs a runtime warning.
  • ViewModel.init is too early as well — theoretically, I think the view model could be created even if the app were never brought to the foreground.
  • The first time var body is accessed, the scenePhase is .active. onChange(of:) doesn't call the closure for its initial value, so it's never called until I background and re-foreground the app.
class ViewModel: ObservableObject {
  init() {
    // 1. reload() here? Could happen without the app entering the foreground.
  }

  func reload() { ... }
}


struct ContentView: View {
  @Environment(\.scenePhase) var scenePhase
  @StateObject var viewModel = ViewModel()

  init() {
    // 2. viewModel.reload() here?
    //  - problem 1: scenePhase == .background, not .active
    //  - problem 2: not supposed to access a @StateObject here anyway
  }

  var body: some View {
    (...)
      // The initial render happens when scenePhase == .active,
      // so I don't get the onChange callback until it changes again.
      .onChange(of: scenePhase) {
        if $0 == .active {
          viewModel.reload()
        }
      }
  }
}
1 Answers

If your ContentView is the root View you can just use onAppear.


However, if your ContentView can disappear and then reappear the above solution will not work.

A possible solution may be to inject a variable into the Environment:

struct LaunchAppKey: EnvironmentKey {
    static let defaultValue = Binding.constant(false)
}

extension EnvironmentValues {
    var isAppLaunched: Binding<Bool> {
        get { return self[LaunchAppKey] }
        set { self[LaunchAppKey] = newValue }
    }
}
@main
struct TestApp: App {
    @State private var isAppLaunched = false
    @State private var showContentView = true
    
    var body: some Scene {
        WindowGroup {
            VStack {
                if showContentView {
                    ContentView()
                } else {
                    Text("Some other view")
                }
            }
            .environment(\.isAppLaunched, $isAppLaunched)
            .onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                    showContentView.toggle()
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
                    showContentView.toggle()
                }
            }
        }
    }
}
struct ContentView: View {
    @Environment(\.isAppLaunched) var isAppLaunched
    
    var body: some View {
        (...)
        .onAppear {
            guard !isAppLaunched.wrappedValue else { return }
            isAppLaunched.wrappedValue = true
            print("apppear")
        }
    }
}
Related