14

I am calling API when tab item is appeared if there is any changes. Why onAppear called after called onDisappear?

Here is the simple example :

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationView {
                Text("Home")
                    .navigationTitle("Home")
                    .onAppear {
                        print("Home appeared")
                    }
                    .onDisappear {
                        print("Home disappeared")
                    }
            }
            .tabItem {
                Image(systemName: "house")
                Text("Home")
            }.tag(0)
        
            NavigationView {
                Text("Account")
                    .navigationTitle("Account")
                    .onAppear {
                        print("Account appeared")
                    }
                    .onDisappear {
                        print("Account disappeared")
                    }
            }
            .tabItem {
                Image(systemName: "gear")
                Text("Account")
            }.tag(1)
        }
    }
}    

Just run above code and we will see onAppear after onDisappear.

Home appeared
---After switch tab to Account---
Home disappeared
Account appeared
Home appeared

Is there any solution to avoid this?

Jay Patel
  • 2,642
  • 2
  • 18
  • 40
  • 1
    You don't control how SwiftUI refreshes views and better not to rely on it. Why do you need to avoid multiple `onAppear`? – pawello2222 Aug 04 '20 at 18:09
  • Because init for nested views (tab>navigation>view1>view2) are called while refresh entire view after switch tab. – Jay Patel Aug 04 '20 at 18:20
  • *why* is not constructive question as for me... would you consider possibility to reformulate your question in terms of what do you try to achieve (or solve), and then... someone might find *how* it could be done. – Asperi Aug 04 '20 at 18:50
  • 1
    I'm not sure why the negativity around this Question. We relied on viewWillAppear / viewWillDisappear working in a certain way for years. I am also having the same issues when running iOS13 compiled Apps on iOS14 Beta. When pushing a detail screen on, the .onAppear inside a NavigationView is called again. – Brett Aug 21 '20 at 01:22
  • 1
    Did you find a solution? This still appears to be the behaviour in iOS 14 stable. – Thomas Vos Sep 20 '20 at 14:21
  • Nope. I am still trying to solve – Jay Patel Sep 21 '20 at 06:49
  • I know this topic is old, but I encountered it today, and I would say it's a bug. Thankfully @Fx. gave a working workaround below. – Romain Aug 24 '21 at 15:37

4 Answers4

7

It's very annoying bug, imagine this scenario: Home view onAppear method contains a timer which is fetching data repeatedly. Timer is triggered invisibly by switching to the Account view.

Workaround:

  1. Create a standalone view for every embedded NavigationView content
  2. Pass the current selection value on to standalone view as @Binding parameter

E.g.:

struct ContentView: View {
    @State var selected: MenuItem = .HOME

    var body: some View {
        return TabView(selection: $selected) {
            HomeView(selectedMenuItem: $selected)
                .navigationViewStyle(StackNavigationViewStyle())
                .tabItem {
                    VStack {
                        Image(systemName: "house")
                        Text("Home")
                    }
                }
                .tag(MenuItem.HOME)
            
            AccountView(selectedMenuItem: $selected)
                .navigationViewStyle(StackNavigationViewStyle())
                .tabItem {
                    VStack {
                        Image(systemName: "gear")
                        Text("Account")
                    }
                }
                .tag(MenuItem.ACCOUNT)
        }
    }
}

enum MenuItem: Int, Codable {
    case HOME
    case ACCOUNT
}

HomeView:

struct HomeView: View {

    @Binding var selectedMenuItem: MenuItem

    var body: some View {
        return Text("Home")
            .onAppear(perform: {
                if MenuItem.HOME == selectedMenuItem {
                    print("-> HomeView")
                }
            })
    }
}

AccountView:

struct AccountView: View {

    @Binding var selectedMenuItem: MenuItem

    var body: some View {
        return Text("Account")
            .onAppear(perform: {
                if MenuItem.ACCOUNT == selectedMenuItem {
                    print("-> AccountView")
                }
            })
    }
}
vargab
  • 101
  • 1
  • 5
2

To whom it may help.

Because this behaviour I only could reproduce on iOS 14+, I end up using https://github.com/NicholasBellucci/StatefulTabView (which properly only get called when showed; but don't know if it's a bug or not, but it works with version 0.1.8) and TabView on iOS 13+.

Fx.
  • 61
  • 4
  • 1
    To me it's definitely an iOS 14 bug. Functionally speaking, onAppear of the initial view should not be called again (it's not appearing), and especially not after onAppear of the new view is called. Thanks for pointing out this package, I managed to workaround this problem in less than 5 minutes using StatefulTabView. It even works on macOS! – Romain Aug 24 '21 at 15:35
1

I'm not sure why you are seeing that behaviour in your App. But I can explain why I was seeing it in my App.

I had a very similar setup to you and was seeing the same behaviour running an iOS13 App on iOS14 beta. In my Home screen I had a custom Tab Bar that would animate in and out when a detail screen was displayed. The code for triggering the hiding of the Tab Bar was done in the .onAppear of the Detail screen. This was triggering the Home screen to be redrawn and the .onAppear to be called. I removed the animation and found a much better set up due to this bug and the Home screen .onAppear stopped being called.

So if you have something in your Account Screen .onAppear that has a visual effect on the Home Screen then try commenting it out and seeing if it fixes the issue.

Good Luck.

Brett
  • 1,647
  • 16
  • 34
0

I have been trying to understand this behavior for a number of days now. If you are working with a TabView, all of your onAppears() / onDisapear() will fire immediately on app init and never again. Which actually makes since I guess?

This was my solution to fix this:

import SwiftUI

enum TabItems {
    case one, two
}

struct ContentView: View {
    
    @State private var selection: TabItems = .one
    
    var body: some View {
        TabView(selection: $selection) {
            ViewOne(isSelected: $selection)
                .tabBarItem(tab: .one, selection: $selection)
            
            ViewTwo(isSelected: $selection)
                .tabBarItem(tab: .two, selection: $selection)
        }
    }
}

struct ViewOne: View {
    
    @Binding var isSelected: TabItems
    
    var body: some View {
        Text("View One")
            .onChange(of: isSelected) { _ in
                if isSelected == .one {
                    // Do something
                }
            }
    }
}

struct ViewTwo: View {
    
    @Binding var isSelected: TabItems
    
    var body: some View {
        Text("View Two")
            .onChange(of: isSelected) { _ in
                if isSelected == .two {
                    // Do something
                }
            }
    }
}

View Modifier for custom TabView

struct TabBarItemsPreferenceKey: PreferenceKey {
    
    static var defaultValue: [TabBarItem] = []
    
    static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
        value += nextValue()
    }
}


struct TabBarItemViewModifer: ViewModifier {
    
    let tab: TabBarItem
    @Binding var selection: TabBarItem
    
    func body(content: Content) -> some View {
        content
            .opacity(selection == tab ? 1.0 : 0.0)
            .preference(key: TabBarItemsPreferenceKey.self, value: [tab])
    }
}


extension View {
    func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View {
        modifier(TabBarItemViewModifer(tab: tab, selection: selection))
    }
}
Christopher
  • 73
  • 10
  • Also: you will want to have an onAppear on the first view that shows with the same logic (simply for the init of app) – Christopher Aug 20 '22 at 16:58
  • EDIT: you will have to basically repeat the same logic in onAppear() etc for this to fully work. Seems very wet, so if some one has a drier approach would love to hear. – Christopher Aug 20 '22 at 18:20
  • The above code is just a sample from a bigger project I am working on. You will have to tweak it to get it to work. – Christopher Aug 20 '22 at 18:22