Custom bottom sheet container using SwiftUI

251 views Asked by At

I'm trying to create a custom expandable view, something like on gif below:

enter image description here

I'm trying to use offsets for a container, but there is a problem with content, when the content is scrollable (like ScrollView or List etc) as every scrollable catches drag events and handle them internally.

Is any way to scroll parent container when child scrollable view riches its edge corner? I saw, how native bottom sheet works in SwiftUI. This behaviour of scroll is pretty much like I need but I cannot use sheet() modifier because it expands not in scope of UI but like separate screen.

1

There are 1 answers

3
Jayant Badlani On

You can use .sheet() with presentationDetents as shown in the example below. The sheet height adjusts according to the content, when the content of the bottom sheet reaches up to 80% of the screen, it will start scrolling. You can also interact with the content behind and above the sheet, as demonstrated in this example with open and close buttons, hope this solves your query.

ScreenShot

import SwiftUI

struct ContentView: View {
    
    @State private var isSheetOpen = true
    @State private var bottomSheetHeight: CGFloat = 0
    private var maxHeightBottomSheet = (UIScreen.main.bounds.height * 0.8)
    
    
    var body: some View {
        
        VStack {
            
            Button(action: {
                isSheetOpen = true
            } ) {
                Text("Open Sheet")
                    .frame(maxWidth: .infinity)
                    .foregroundColor(.black)
            }
            .frame(height: 50)
            .background(Color.white)
            .padding(.horizontal, 50)
            .padding(.top, 100)
            
            Button(action: {
                isSheetOpen = false
            } ) {
                Text("Close Sheet")
                    .frame(maxWidth: .infinity)
                    .foregroundColor(.black)
            }
            .frame(height: 50)
            .background(Color.white)
            .padding(.horizontal, 50)
            
            Spacer()
        }
        .background(Color.blue.edgesIgnoringSafeArea(.all))
        
        .sheet(isPresented: $isSheetOpen, content: {
            BottomSheet(height: $bottomSheetHeight)
                .presentationDetents([.height(min(bottomSheetHeight, maxHeightBottomSheet))])
                .shadow(color: .black, radius: 5)
        })
        .ignoresSafeArea(.all)
    }
}


struct BottomSheet: View {
    
    @Binding var height: CGFloat
    
    var body: some View {
        
        ScrollView {
            VStack(spacing: 0) {
                
                // Handle bar
                Rectangle()
                    .frame(width: 100, height: 6)
                    .cornerRadius(3.0)
                    .foregroundColor(Color.gray)
                    .padding(.top, 10)
                
                Rectangle()
                    .frame(height:  100)
                    .foregroundColor(.clear)
                Text("Drag down to close sheet")
                Text("BottomSheet")
                
                Rectangle()
                    .frame(height:  250)
                    .foregroundColor(.clear)
            }
            .background(.green)
            .readSize { calculatedHeight in
                height = calculatedHeight.height
            }
        }
    }
}


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


extension View {
    func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
        background(
            GeometryReader { geometryProxy in
                Color.clear
                    .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
            }
        )
        .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
    }
}

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}