Appearance Issues
I've tried looking through the developer forums and could not find a post that exactly described my situation. I've found many posts that described unexpected onAppear
or onDisappear
calls, but none that touched on TabView
or NavigationView
/NavigationStack
container views being removed from the view tree.
Versions tested against with the issue:
- iOS 14.0 - iOS 16.1
- Xcode 13.4.1 - Xcode 14.1
I just filed a feedback (FB11721387
), but wanted to post here in case someone has a good workaround or solution.
Summary
When a TabView or NavigationView/NavigationStack is removed from the view tree, there can be unexpected additional calls to appearance
modifiers like onAppear
, task
, and onDisappear
for some of the views.
Example Series of Events:
// ... Assume Already Logged in to Tabbed App
// 1. Tab 2 - OnAppear
// 2. Tab 2 - Task
// 3. Tab 1 - OnDisappear // <- Tab we just left
// 4. Log Out Button Tapped
// 5. Login View - OnAppear
// 6. Login View - Task
// 7. Tab 1 - Task // <- Events 7-9 not expected
// 8. Tab 1 - OnAppear
// 9. Tab 1 - OnDisappear
// 10. Tab 2 - OnDisappear // <- This was expected
TabView Appearance Issues
For TabViews, you must select at least one other tab before changing state to another case that doesn't include the TabView. When removed from the view tree, all already-viewed, non-currently-viewed tab views will run through all appearance calls again, even when the state binding that controls the selected tab does not change.
Reproducible Steps
- Have an application where the root of the app has some conditional for logged-in and logged-out states, and the logged-in view has a TabView as the root with at least two tabs
- Run the app and "Log In"
- Select another non-root tab view
- Log out (bringing you to the Login View)
- Observe the already-viewed, non-currently-selected tab views run through their appearance calls again
NavigationView/NavigationStack Appearance Issues
For NavigationView and NavigationStack (potentially NavigationSplitView as well, but I didn't test this), you must push at least one other view onto the stack before changing state to another case that doesn't include the Navigation type. When removed from the view tree, the root view will run through all appearance calls again.
Reproducible Steps
- Have an application where the root of the app has some conditional for logged-in and logged-out states, and the logged-in view has a NavigationView/NavigationStack as the root and can push a view onto the stack to log out
- Run the app and "Log In"
- Push a view onto the stack
- Log out (bringing you to the Login View)
- Observe the Navigation Root view (Not the Navigation View itself) runs through the appearance calls again
Typical Example
Assume you have an app that has a Login view, and once logged in, you have a tabbed or some other typical navigational structure as the root of your authenticated app flow. Following any of the above situations will result in additional appearance callbacks for the affected views when logging out of the application.
Sample Project Pseudocode
struct ContentView: View {
@StateObject
private var viewModel = ViewModel()
var body: some View {
switch viewModel.state {
case .loading:
SplashView()
case .loggedOut:
LoginView()
case .loggedIn:
Authenticated() // Some TabView or other NavigationView/NavigationStack Root
}
}
}
Current Workaround
While not a great workaround that fills me with confidence, using OSSignposts to measure how quickly a view goes from onAppear
to onDisappear
has shown in my testing that when the bug occurs, it completes the lifecycle within ~15 ms with many completing as few as ~6 ms.
Using this and timing the quickest human interaction for switching tabs to be about 75ms in length, we've decided to implement a debounce plus filter behavior for an onAppear
overload that will only perform the provided action if onDisappear
isn't called before 30 ms has expired.
I understand this workaround is fragile, considering different devices and resource environments (performance throttling, low battery, etc) can play a factor.
Hoping for a more official answer or better ideas for workarounds. Thanks for any help that can be provided!