How to maintain scroll position in a SwiftUI TabView

Unfortunately this is simply not possible with built-in components given the current limitations of SwiftUI (iOS 13.x/Xcode 11.x).

Two reasons:

  1. SwiftUI completely disposes of your View when you switch away from the tab. This is why your scroll position is lost. It's not like UIKit where you have a bunch of offscreen UIViewControllers.
  2. There is no ScrollView equivalent of UIScrollView.contentOffset. That means you can't save your scroll state somewhere and restore it when the user returns to the tab.

Your easiest route is probably going to be using a UITabBarController filled with UIHostingControllers. That way you won't lose the state of each tab as users move between them.

Otherwise, you could create a custom tab container, and modify the opacity of each tab yourself (as opposed to conditionally including them in the hierarchy, which is what causes your problem currently).

var body: some View {
  ZStack {
    self.tab1Content.opacity(self.currentTab == .tab1 ? 1.0 : 0.0)
    self.tab2Content.opacity(self.currentTab == .tab2 ? 1.0 : 0.0)
    self.tab3Content.opacity(self.currentTab == .tab3 ? 1.0 : 0.0)
  }
}

I've used this technique when I wanted to keep a WKWebView from completely reloading every time a user briefly tabbed away.

I would recommend the first technique. Especially if this is your app's main navigation.


November 2020 update

TabView will now maintain scroll position in tabs when moving in between tabs, so the approaches in the answers to my original question are no longer necessary.