How to remove the empty space from TabView using page style in SwiftUI?

145 views Asked by At

I have a NavigationStack where I have a TabView and its style as page style and a List after the TabView.

Inside the TabView I have another custom view that has a horizontal List and I want to remove the space between the the inner child and the parent.

I tried with padding, plain list style but the space is still there.

I can use frame modifier but I don't want a hard coded value for the height.

Here is the code I tried:

import SwiftUI

struct CustomView: View {
    var dataArray: [String]

    var body: some View {
        List {
            LazyHStack {
                ForEach(dataArray, id: \.self) { item in
                    Button("\(item)") {
                    }
                    .buttonStyle(.bordered)
                }
            }
        
        }
        .scrollDisabled(true)
    }
}

struct ContentView: View {
    struct Sea: Hashable, Identifiable {
        let name: String
        let id = UUID()
    }

    struct OceanRegion: Identifiable {
        let name: String
        let seas: [Sea]
        let id = UUID()
    }

    private let oceanRegions: [OceanRegion] = [
        OceanRegion(name: "Pacific",
                    seas: [Sea(name: "Australasian Mediterranean"),
                           Sea(name: "Philippine"),
                           Sea(name: "Coral"),
                           Sea(name: "South China")]),
        OceanRegion(name: "Atlantic",
                    seas: [Sea(name: "American Mediterranean"),
                           Sea(name: "Sargasso"),
                           Sea(name: "Caribbean")]),
        OceanRegion(name: "Indian",
                    seas: [Sea(name: "Bay of Bengal")]),
        OceanRegion(name: "Southern",
                    seas: [Sea(name: "Weddell")]),
        OceanRegion(name: "Arctic",
                    seas: [Sea(name: "Greenland")])
    ]

    @State private var singleSelection: UUID?

    @State private var weekIndex = 0
    @State private var weeks = [ -1, 0, 1 ]

    @State private var dataArray1 = ["1","2","3","4","5","6","7"]
    @State private var dataArray2 = ["8","9","10","11","12","13","14"]
    @State private var dataArray3 = ["15","16","17","18","19","20","21"]

    var body: some View {
        NavigationStack {
            VStack {
                TabView(selection: $weekIndex) {
                    CustomView(dataArray: dataArray1).tag(weeks[0])
                    CustomView(dataArray: dataArray2).tag(weeks[1])
                    CustomView(dataArray: dataArray3).tag(weeks[2])
                }
                .tabViewStyle(.page(indexDisplayMode: .never))
                .indexViewStyle(.page(backgroundDisplayMode: .never))
            }
                
            List(selection: $singleSelection) {
                ForEach(oceanRegions) { region in
                    Section(header: Text("Major \(region.name) Ocean Seas")) {
                        ForEach(region.seas) { sea in
                            NavigationLink(destination: EmptyView()) {
                                Text(sea.name)
                            }
                        }
                    }
                }
            }
            .navigationTitle("Oceans and Seas")
            .toolbar {
                ToolbarItem {
                    Button(action: {}) {
                        Label("Add", systemImage: "plus")
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Here is a screenshot:

enter image description here

Is there a way to remove that empty space?

How can I expand the list inside the tab view to take all the width and zero padding at the top?

How can I set TabView's height dynamically, to take its child height?

2

There are 2 answers

0
s_diaconu On BEST ANSWER

As Benzy mentioned the TabView used inside NavigationStack is not recommended so I finaally found an alternative that works really well.

I used ScrollViewReader and ScrollView(.horizontal) together with some modifiers like .containerRelativeFrame(.horizontal), .scrollTargetLayout() and .scrollTargetBehavior(.paging). Other modifiers can be used to get the scroll position for example.

It is taking the minimum space and is swipeable.

The ScrollViewReader is not entirely necessary if the start is at index 0 but if the initial display is not index 0 then it is necessary to preset to which index to scrollTo. Also this can be controlled using a button for example.

Here is my code:

import SwiftUI

struct CustomView: View {
    var dataArray: [String]

    var body: some View {
        HStack {
            ForEach(dataArray, id: \.self) { item in
                Button("\(item)") {
                    print("\(item)")
                }
                .buttonStyle(.bordered)
            }
        }
    }
}

struct ContentView: View {
    struct Sea: Hashable, Identifiable {
        let name: String
        let id = UUID()
    }

    struct OceanRegion: Identifiable {
        let name: String
        let seas: [Sea]
        let id = UUID()
    }

    private let oceanRegions: [OceanRegion] = [
        OceanRegion(name: "Pacific",
                    seas: [Sea(name: "Australasian Mediterranean"),
                           Sea(name: "Philippine"),
                           Sea(name: "Coral"),
                           Sea(name: "South China")]),
        OceanRegion(name: "Atlantic",
                    seas: [Sea(name: "American Mediterranean"),
                           Sea(name: "Sargasso"),
                           Sea(name: "Caribbean")]),
        OceanRegion(name: "Indian",
                    seas: [Sea(name: "Bay of Bengal")]),
        OceanRegion(name: "Southern",
                    seas: [Sea(name: "Weddell")]),
        OceanRegion(name: "Arctic",
                    seas: [Sea(name: "Greenland")])
    ]

    @State private var singleSelection: UUID?

    @State private var weekIndex: Int?
    @State private var weeks = [ -1, 0, 1 ]

    @State private var dataArray1 = ["1","2","3","4","5","6","7"]
    @State private var dataArray2 = ["8","9","10","11","12","13","14"]
    @State private var dataArray3 = ["15","16","17","18","19","20","21"]

    var body: some View {
        NavigationStack {
            ScrollViewReader { value in
                ScrollView(.horizontal) {
                    HStack {
                        CustomView(dataArray: dataArray1)
                            .containerRelativeFrame(.horizontal)
                            .id(weeks[0])
                        CustomView(dataArray: dataArray2)
                            .containerRelativeFrame(.horizontal)
                            .id(weeks[1])
                        CustomView(dataArray: dataArray3)
                            .containerRelativeFrame(.horizontal)
                            .id(weeks[2])
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.paging)
                .scrollIndicators(.hidden)
                .onAppear {
                    value.scrollTo(weeks[1])
                }
                .scrollPosition(id: $weekIndex)
            }
        
            List(selection: $singleSelection) {
                ForEach(oceanRegions) { region in
                    Section(header: Text("Major \(region.name) Ocean Seas")) {
                        ForEach(region.seas) { sea in
                            NavigationLink(destination: EmptyView()) {
                                Text(sea.name)
                            }
                        }
                    }
                }
             }
            .navigationTitle("Oceans and Seas")
            .toolbar {
                ToolbarItem {
                    Button(action: {}) {
                        Label("Add", systemImage: "plus")
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

Thanks everyone!

1
Benzy Neez On

The reasons for the gaps can be explained as follows:

  • The small space at the top is because each CustomView contains a List and the default list style is .insetGrouped. This list style comes with padding above, below and at the sides.

  • The big space in the middle is because TabView and List are both greedy, so they want to use as much space as possible. Consequently, they are both given 50% of the available height.

Regarding the TabView, you are using a paged view style, but the index indicators have been disabled. So I am wondering why you are using a TabView at all? If the only reason is to allow a change of weekIndex using swipe then you might want to consider a simpler container and perhaps an alternative navigation mechanism. In any case, a Tabview should normally be the parent for a NavigationStack and not the other way around (as also mentioned in the comment by lorem ipsum).

I would suggest the following changes to resolve the spacing issues:

  • Replace the TabView with a simpler (non-greedy) container such as a ZStack.
  • Remove the List and LazyHStack (both of which are greedy) from CustomView. Just use a simple HStack and apply your own padding as required.
  • Implement your own way of navigating between groups of weeks. A simple solution would be to use buttons for selecting the previous/next group.
  • It might also be an idea to show a title above the week numbers.

These changes are illustrated below:

struct CustomView: View {
    var dataArray: [String]

    var body: some View {
        HStack {
            ForEach(dataArray, id: \.self) { item in
                // content as before
            }
        }
    }
}

struct ContentView: View {

    // other content as before

    private var prevNextButtons: some View {
        HStack {
            Button("Previous") {
                weekIndex = max(weekIndex - 1, weeks.first ?? 0)
            }
            .opacity(weekIndex == weeks.first ? 0 : 1)

            Spacer()

            Button("Next") {
                weekIndex = min(weekIndex + 1, weeks.last ?? 0)
            }
            .opacity(weekIndex == weeks.last ? 0 : 1)
        }
        .overlay {
            Text("Weeks")
                .font(.headline)
        }
    }

    private var weekGroup: some View {
        ZStack {
            if weekIndex == weeks[0] {
                CustomView(dataArray: dataArray1)
            } else if weekIndex == weeks[1] {
                CustomView(dataArray: dataArray2)
            } else {
                CustomView(dataArray: dataArray3)
            }
        }
        .padding(.vertical, 6)
        .frame(maxWidth: .infinity)
        .background {
            Color(UIColor.secondarySystemGroupedBackground)
                .clipShape(RoundedRectangle(cornerRadius: 8))
        }
    }

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                VStack {
                    prevNextButtons
                    weekGroup
                }
                .padding(.horizontal)
                .background(Color(UIColor.systemGroupedBackground))

                List(selection: $singleSelection) {
                    // content as before
                }
            }
            .animation(.easeInOut, value: weekIndex)
            .navigationTitle("Oceans and Seas")
            .toolbar {
                // content as before
            }
        }
    }
}

Screenshot