Search code examples
swiftswiftuialignmenttabview

SwiftUI TabView exceeds height and therefore children are not centered


I am facing an issue with the TabView implementation and I don't know if I am using it wrong or there is an issue within SwiftUI. I wrote a small sample to reproduce it:

import SwiftUI

@main
struct MyPlaygroundAppApp: App {
    init() {
        let toolbarAppearance = UIToolbarAppearance()
        toolbarAppearance.backgroundColor = UIColor(Color.green)
        
        UIToolbar.appearance().standardAppearance = toolbarAppearance
        UIToolbar.appearance().scrollEdgeAppearance = toolbarAppearance
        UIToolbar.appearance().compactAppearance = toolbarAppearance
        UIToolbar.appearance().compactScrollEdgeAppearance = toolbarAppearance
    }
    
    var body: some Scene {
        WindowGroup {
            PlayContentView()
        }
    }
}

import SwiftUI

struct PlayContentView: View {
    var body: some View {
        TabView {
            ContentView()
        }
        .background(Color.green)
        .edgesIgnoringSafeArea(.top)
        .tabViewStyle(.page(indexDisplayMode: .never))
    }
}

struct ContentView: View {
    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                VStack(spacing: 0) {
                    ZStack {
                        VStack(spacing: 0) {
                            Text("Title")
                            Text("Text")
                        }
                    }
                    .frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, maxHeight: 80, alignment: .bottom)
                    .padding(.top, 12)
                    .background(Color.yellow)
                    Divider()
                        .overlay(Color.black)
                }
                
                ScrollView([.vertical, .horizontal], showsIndicators: false) {
                    VStack {
                    }
                    .frame(width: 200, height: 300)
                    .frame(maxWidth: .infinity)
                    .background(Color.gray)
                }
                .background(Color.blue)
            }
            .toolbar {
                ToolbarItem(placement: .status) {
                    Text("press me")
                }
            }
        }
    }
}

#Preview {
    PlayContentView()
}

The result looks like this: enter image description here

As you can see the gray box is not centered vertically, it has more space on the top as on the bottom. When I change the TabView to a ZStack then the space seems correct, so I think the issue lies within the TabView. I also changed the tabViewStyle to displayMode .always and then I can see that a small dot appears inside the bottom bar. So I think the TabView has not the correct height. When I disable the toolbar, then I can also see that the blue background goes to the bottom and then I also can see that gray box is centered. I guess that the TabView does not respect the toolbar correctly. Did anyone else had this issue before?


Solution

  • The reason why the gray square does not appear to be centered is because the (scrollable) blue area continues behind the toolbar. When the toolbar is hidden, the gray square is vertically centered.

    Here are two workarounds:

    1. Attach the .toolbar to either the TabView or the NavigationStack instead

    For example:

    NavigationStack {
        VStack(spacing: 0) {
            // ...
        }
    }
    .toolbar {
        // ...
    }
    

    2. Add padding below the VStack

    If the toolbar remains attached to the VStack, you can allow space for it by adding some bottom padding. However, I couldn't find a way to measure the height of the toolbar from the components being displayed, so you might have to resort to a hard-coded value.

    When testing on an iPhone 15 simulator with iOS 17.5, the padding needs to be 49pt, but this might change in a future iOS version. The good news is that it seems to remain constant even when a larger text size is used.

    NavigationStack {
        VStack(spacing: 0) {
            // ...
        }
        .padding(.bottom, 49)
        .toolbar {
            // ...
        }
    }
    

    An alternative approach would be to use a custom toolbar and position it at the bottom of your VStack. You could call this a third workaround.

    BTW, another way to set the background color of the toolbar is to use .toolbarBackground, instead of configuring UIToolbarAppearance in the init of the app. However, this only works for the second (and third) workaround, not the first:

    VStack(spacing: 0) {
        // ...
    }
    .padding(.bottom, 49)
    .toolbar {
        // ...
    }
    .toolbarBackground(.visible, for: .bottomBar)
    .toolbarBackground(.green, for: .bottomBar)