swiftswiftuiinitializationonappear

.onAppear vs init on @main and child views in SwiftUI


I'm seeking clarity on the reasonings - if any known - why the order of initialisation and view modifiers occurs.

I couldn't find any documentation on this but hoping someone might know why.

I have the following example code:

@main
struct TestApp: App {
  init() { print("@main: init" }
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onAppear { let _ = print("@main: onAppear" }
    }
  }
}

struct ContentView: View {
  var body: some View {
    VStack {}
      .onAppear { let _ = print("ContentView: onAppear" }
  }
}

When this runs, the console outputs the following:

@main - init
ContentView - onAppear
@main - onAppear

Does anyone know why or any reasoning why the order would be init, child view .onAppear, then main .onAppear?

I guess in my mind the order would be initialising all of the @main including the modifiers before any of the child views - but maybe there is a proper reason for the current order.


Solution

  • The documentation for onAppear says:

    The exact moment that SwiftUI calls this method depends on the specific view type that you apply it to, but the action closure completes before the first rendered frame appears.

    So depending on the type of view, onAppear could totally be called in any order, as long as it is "before the first rendered frame appears".

    Here is a possible explanation for your particular example.

    If you just "flatten" ContentView, you end up with a VStack with 2 onAppears:

    VStack {}
        // 1
        .onAppear { let _ = print("ContentView: onAppear" }
        // 2
        .onAppear { let _ = print("@main: onAppear" }
    

    Each onAppear is modifying everything "above" it. "@main: onAppear" is printed when everything above // 2 has "appeared". That means that the VStack must have already appeared. Therefore, "@main: onAppear" is printed after "ContentView: onAppear".

    And of course, "@main: init" is the first thing being printed, because without creating an instance of TestApp, SwiftUI cannot call the getter of body, and hence cannot know anything about what views you have.

    From my experiments, most SwiftUI "containers" have their onAppear called first, before the onAppear of the views in them.

    VStack {
        Text("Foo")
            .id("Foo")
            .onAppear {
                print("1")
            }
        
        Text("Foo")
            .onAppear {
                print("2")
            }
    }
    .onAppear {
        print("3")
    }
    
    // prints in the order of 3 2 1
    

    This aligns with your expectation, if I understand correctly. The VStack can appear first, without any subviews, then the subviews appear. Your ContentView cannot do something similar - it is literally just a wrapper around a single VStack.