When developing SwiftUI applications, TabView is often the go-to tool for managing multiple views in a tabbed interface. However, in some scenarios, TabView can cause memory-related issues—specifically, views that aren't properly deallocated when navigating between tabs. This can lead to unintended memory retention and even performance degradation.

In this article, I’ll describe a real-world issue I encountered where switching from Tab 2 to Tab 3 caused the views to persist in memory unnecessarily—and how replacing TabView with a simple Group solved the problem.


The Problem: TabView Retaining Views in Memory

In my SwiftUI app, I had a TabView with three tabs. Each tab contained a relatively complex view that managed its own data, UI state, and in some cases, heavy memory objects like camera previews or navigation stacks.

TabView(selection: $selectedTab) {
    Tab1View()
        .tabItem { Label("Tab 1", systemImage: "1.circle") }
        .tag(1)

    Tab2View()
        .tabItem { Label("Tab 2", systemImage: "2.circle") }
        .tag(2)

    Tab3View()
        .tabItem { Label("Tab 3", systemImage: "3.circle") }
        .tag(3)
}

The issue came up when switching from Tab 2 to Tab 3. I noticed that Tab2View wasn’t getting deinitialized, even though it wasn’t on screen anymore. This caused some memory to hang around longer than it should, leading to higher resource usage—especially when users kept switching between tabs.

Simply switching between tabs caused the memory usage to keep climbing indefinitely, eventually leading the app to freeze.

ev2.png

After some investigation, I confirmed this wasn’t a bug in my code's logic. It was TabView itself keeping all views alive as long as the TabView existed, due to SwiftUI's caching behavior for better performance.

The Fix: Replacing TabView with Group

To regain control over memory and view lifecycle, I replaced TabView with a Group that conditionally loads and displays only the selected tab’s view. This manual approach ensures only one view is alive at a time:

Group {
    switch selectedTab {
    case 1:
        Tab1View()
    case 2:
        Tab2View()
    case 3:
        Tab3View()
    default:
        EmptyView()
    }
}

This change had two immediate benefits:

To navigate between views, I simply used a CustomView, segmented control, or custom tab bar to update the selectedTab state.


Conclusion