Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI automatically sizing bottom sheet

There are a lot of examples of bottom sheet out there for SwiftUI, however they all specify some type of maximum height the sheet can grow to using a GeometryReader. What I would like is to create a bottom sheet that becomes only as tall as the content within it. I've come up with the solution below using preference keys, but there must be a better solution. Perhaps using some type of dynamic scrollView is the solution?

struct ContentView: View{
    @State private var offset: CGFloat = 0
    @State private var size: CGSize = .zero

    var body: some View{
        ZStack(alignment:.bottom){
            VStack{
                Button(offset == 0 ? "Hide" : "Show"){
                    withAnimation(.linear(duration: 0.2)){
                        if offset == 0{
                            offset = size.height
                        } else {
                            offset = 0
                        }
                    }
                }
                .animation(nil)
                .padding()
                .font(.largeTitle)
                Spacer()
            }
            BottomView(offset: $offset, size: $size)
        }.edgesIgnoringSafeArea(.all)
    }
}

struct BottomView: View{
    @Binding var offset: CGFloat
    @Binding var size: CGSize

    var body: some View{
        VStack(spacing: 0){
            ForEach(0..<5){ value in
                Rectangle()
                    .fill(value.isMultiple(of: 2) ? Color.blue : Color.red)
                    .frame(height: 100)
            }
        }
        .offset(x: 0, y: offset)
        .getSize{
            size = $0
            offset = $0.height
        }
    }
}

struct SizePreferenceKey: PreferenceKey {
    struct SizePreferenceData {
        let bounds: Anchor<CGRect>
    }

    static var defaultValue: [SizePreferenceData] = []

    static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

struct SizePreferenceModifier: ViewModifier {
    let onAppear: (CGSize)->Void

    func body(content: Content) -> some View {
        content
            .anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { [SizePreferenceKey.SizePreferenceData( bounds: $0)] })
            .backgroundPreferenceValue(SizePreferenceKey.self) { preferences in
                GeometryReader { geo in
                    Color.clear
                        .onAppear{
                            let size = CGSize(width: geo.size.width, height: geo.size.height)
                            onAppear(size)
                        }
                }
            }
    }
}

extension View{
    func getSize(_ onAppear: @escaping (CGSize)->Void) -> some View {
        return self.modifier(SizePreferenceModifier(onAppear: onAppear))
    }
}
like image 619
Richard Witherspoon Avatar asked Sep 01 '26 22:09

Richard Witherspoon


2 Answers

Talk about over engineering the problem. All you have to do is specify a height of 0 if you want the sheet to be hidden, and not specify a height when it's shown. Additionally set the frame alignment to be top.

struct ContentView: View{
    @State private var hide = false

    var body: some View{
        ZStack(alignment: .bottom){
            Color.blue
                .overlay(
                    Text("Is hidden : \(hide.description)").foregroundColor(.white)
                        .padding(.bottom, 200)
                )
                .onTapGesture{
                    hide.toggle()
                }
            
            VStack(spacing: 0){
                ForEach(0..<5){ index in
                    Rectangle()
                        .foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
                        .frame(height: 50)
                        .layoutPriority(2)
                }
            }
            .layoutPriority(1)
            .frame(height: hide ? 0 : nil, alignment: .top)
            .animation(.linear(duration: 0.2))
        }.edgesIgnoringSafeArea(.all)
    }
}
like image 81
Richard Witherspoon Avatar answered Sep 05 '26 08:09

Richard Witherspoon


My approach is SwiftUI Sheet based solution feel free to check the gist

you just need to add the modifier to the view and let iOS do the rest for you, no need to re-do the math ;) Plus you will have the sheet native behavior (swipe to dismiss) and i added "tap elsewhere" to dismiss.

struct ContentView: View {
@State var activeSheet: Bool = false
@State var activeBottomSheet: Bool = false

var body: some View {
    VStack(spacing: 16){
                Button {
                    activeSheet.toggle()
                } label: {
                    HStack {
                        Text("Activate Normal sheet")
                            .padding()
                    }.background(
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(lineWidth: 2)
                            .foregroundColor(.yellow)
                    )
                }

                Button {
                    activeBottomSheet.toggle()
                } label: {
                    HStack {
                        Text("Activate Bottom sheet")
                            .padding()
                    }.background(
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(lineWidth: 2)
                            .foregroundColor(.yellow)
                    )
                }
            }
    .sheet(isPresented: $activeSheet) {
        // Regular sheet
        sheetView
    }
    .sheet(isPresented: $activeBottomSheet) {
        // Responsive sheet
        sheetView
            .asResponsiveSheet()
    }
}

var sheetView: some View {
    VStack(spacing: 0){
        ForEach(0..<5){ index in
            Rectangle()
                .foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
                .frame(height: 50)
        }
    }
}

iPhone: enter image description here iPad : enter image description here

like image 41
Bessem ben afia Avatar answered Sep 05 '26 08:09

Bessem ben afia



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!