Search code examples
xcodeswiftuitabbarbottom-sheetxcode15

Bottom Sheet Above Tab Bar like Find My app


How would you make the .sheet presented above the tab bar wihtout covering it in SwiftUi? I've seen a somewhat solution but not one in SwiftUi. enter image description here

So far anything I try just places the sheet overlaying the tab bar

import SwiftUI

struct ContentView: View {
    @State private var showSheet = true

    var body: some View {
        TabView {
            HomeView(showSheet: $showSheet)
                .tabItem {
                    Label("Home", systemImage: "house")
                }
            Text("Second Tab")
                .tabItem {
                    Label("Second", systemImage: "2.circle")
                }
        }
    }
}

struct HomeView: View {
    @Binding var showSheet: Bool

    var body: some View {
        VStack {
            Button("Show TabView Sheet") {
                showSheet.toggle()
            }
            .sheet(isPresented: $showSheet) {
                SheetContent()
                    .presentationDetents([.medium, .large])
                    .interactiveDismissDisabled()
                    .presentationBackgroundInteraction(.enabled(upThrough: .large))
            }
        }
    }
}

struct SheetContent: View {
    var body: some View {
        VStack {
            Text("First Tab Content")
            Text("More Content in the Sheet")
            // Add more content here as needed
        }
    }
}

#Preview {
    ContentView()
}


Solution

  • In your example, the sheet cannot be dismissed interactively, but background interaction has been enabled. So I would suggest, an overlay could be used to replace the sheet:

    // ContentView
    
    HomeView(showSheet: $showSheet)
        .tabItem {
            Label("Home", systemImage: "house")
        }
        .toolbarBackground(.visible, for: .tabBar)
        .toolbarBackground(.background, for: .tabBar)
    
    struct HomeView: View {
        @Binding var showSheet: Bool
    
        var body: some View {
            VStack {
                Button("Show TabView Sheet") {
                    showSheet.toggle()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .overlay(alignment: .bottom) {
                if showSheet {
                    SheetContent()
                        .transition(.move(edge: .bottom))
                }
            }
            .animation(.easeInOut, value: showSheet)
        }
    }
    
    struct SheetContent: View {
        var body: some View {
            VStack {
                Text("First Tab Content")
                Text("More Content in the Sheet")
                // Add more content here as needed
            }
            .frame(maxWidth: .infinity, minHeight: 200)
            .background {
                UnevenRoundedRectangle(cornerRadii: .init(topLeading: 20, topTrailing: 20))
                    .fill(.yellow)
            }
        }
    }
    

    Animation


    EDIT You were asking in a comment if it would be possible to resize the overlay. You would need to implement this yourself using a drag gesture. Perhaps something like this:

    struct SheetContent: View {
        let minHeight: CGFloat = 200
        let maxHeight: CGFloat = 450
        @State private var extraHeight = CGFloat.zero
        @State private var dragHeight = CGFloat.zero
    
        var body: some View {
            VStack {
                Text("First Tab Content")
                Text("More Content in the Sheet")
                // Add more content here as needed
            }
            .frame(maxWidth: .infinity, maxHeight: minHeight + extraHeight)
            .offset(y: -dragHeight / 2)
            .background {
                UnevenRoundedRectangle(cornerRadii: .init(topLeading: 20, topTrailing: 20))
                    .padding(.bottom, -300)
                    .foregroundStyle(.yellow)
                    .offset(y: -dragHeight)
            }
            .overlay(alignment: .top) {
                Capsule()
                    .frame(width: 36, height: 5)
                    .foregroundStyle(.secondary)
                    .padding(5)
                    .offset(y: -dragHeight)
                    .gesture(
                        DragGesture()
                            .onChanged { val in
                                let dy = -val.translation.height
                                let minDragHeight = minHeight - (minHeight + extraHeight)
                                let maxDragHeight = maxHeight - (minHeight + extraHeight)
                                dragHeight = min(max(dy, minDragHeight), maxDragHeight)
                            }
                            .onEnded { val in
                                extraHeight = extraHeight + dragHeight
                                dragHeight = 0
                            }
                    )
            }
        }
    }
    

    Aniamtion