New Dev
New Dev

Reputation: 49590

Why is `.onAppear` called twice when view is inside TabView

I'm trying to load a remote resource, so I'm using .onAppear to do that. However, when I use if/else inside the view, it causes the it to fire twice.

class FooVM: ObservableObject {
   @Published var remoteText: String? = nil

   func load() {
      DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
         self.remoteText = "\(Date())"
      }
   }
}

struct Foo: View {

   @StateObject private var vm = FooVM()

   var body: some View {
      content
         .onAppear(perform: vm.load)
   }

   @ViewBuilder
   private var content: some View {
      if let text = vm.remoteText {
         Text(text)
      } else {
         Circle()
      }
   }
}

struct ContentView: View {
    var body: some View {
        TabView {
            Foo()
                .tabItem {
                    Image(systemName: "circle.fill")
                    Text("Home")
                } 
        }
    }
}

EDIT: Turns out it only repro's when the content is inside a TabView. I updated the code above

Why is onAppear (and thus load) firing twice? Actually, in the Playground, it fires repeatedly every 2 seconds, but in a simulator in fires only twice.

How do I avoid it? Bear in mind that this is a simplified example.


Interestingly, if it wasn't a choice between Text and Circle, but just a different text within Text, then it would fire only once:

Text(vm.remoteText ?? "loading...")

Upvotes: 2

Views: 1478

Answers (1)

nicksarno
nicksarno

Reputation: 4245

When your content is switching within the if statement, Foo() has to completely re-appear. The solution is to embed the content of Foo() within a ZStack (or similar) so that it doesn't need to re-appear the entire view and call .onAppear again. Here, the ZStack remains appeared while the content switches.

var body: some View {
        ZStack {
            content
        }
        .onAppear(perform: vm.load)
   }

Upvotes: 2

Related Questions