Search code examples
iosswiftswiftuiswiftui-tabview

How to add a .background to ScrollView without breaking scroll-to-top integration with TabView?


When a ScrollView is nested in a TabView, you can press on the tab button and the scroll view will scroll to top.

import SwiftUI

struct SwiftUIView: View {
  let items = (1...100).map { "Item \($0)" }
  var body: some View {
    TabView {
      Tab("home", systemImage: "house") {
        ScrollView {
          ForEach(items, id: \.self) { item in
            Text(item)
              .frame(maxWidth: .infinity, alignment: .center)
          }
        }
      }
    }
  }
}

#Preview {
  SwiftUIView()
}

swiftui view without background set

If I modify this code to set a background color on the ScrollView, this scroll-to-top functionality breaks.

import SwiftUI

struct SwiftUIView: View {
  let items = (1...100).map { "Item \($0)" }
  var body: some View {
    TabView {
      Tab("home", systemImage: "house") {
        ScrollView {
          ForEach(items, id: \.self) { item in
            Text(item)
              .frame(maxWidth: .infinity, alignment: .center)
          }
        }
        // Set background on ScrollView.
        .background(Color.red)
      }
    }
  }
}

#Preview {
  SwiftUIView()
}

swiftui view with red background


Solution

  • This is a really strange problem. At first I thought it might be related to the safe area insets. However, I found that when any visible view is added to the background of the ScrollView then it breaks the scroll-to-top behavior, even if it's just a simple Text view.

    As a workaround, you can nest the ForEach inside a VStack (or LazyVStack) and then add a background color to the stack.

    It seems that the nested content is not in contact with the safe area inset, so adding .ignoresSafeArea() has no effect and the color doesn't reach the top of the screen. To workaround this problem too, you can add negative vertical padding to the background. The negative padding needs to be quite large, so that when the content inside the ScrollView is pulled down, the color fills the gap.

    ScrollView {
        VStack { // or LazyVStack
            ForEach(items, id: \.self) { item in
                Text(item)
                    .frame(maxWidth: .infinity, alignment: .center)
            }
        }
        .background {
            Color.red
                .padding(.vertical, -500)
        }
    }
    

    Animation