Search code examples
iosswiftswiftuiuikit

What is the component similar to a modal in iPadOS with detents?


I've come across a UI component in iPadOS that behaves like a modal with detents, but I'm uncertain about its name or how to properly implement it. It appears to exhibit detent-like behavior, but when attempting to employ a sheet with detents, it doesn't function as expected on iPad.

enter image description here

enter image description here

Could someone please assist me in identifying this component within the iPadOS framework? Additionally, I'm curious whether it's specific to UIKit or if it can be utilized in SwiftUI as well. Any clarification or guidance on effectively using this component, especially with detents, would be greatly appreciated. Thank you.


Solution

  • I don't know if there's some native component which resembles a sheet but, you can achieve something really similar really simply with SwiftUI:

    struct ContentView: View {
        
        @State var isOpen = true
        
        var body: some View {
            NavigationStack {
                GeometryReader { proxy in
                    let size = proxy.size
                    ZStack {
                        Map()
                        
                        CustomMapSheet(size: size)
                        
                    }
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
        
        @ViewBuilder
        func CustomMapSheet(size: CGSize) -> some View {
            ZStack(alignment: .leading) {
                VStack {
                    Text("Content Here")
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .overlay(alignment: .top) {
                    Capsule()
                        .frame(width: 50, height: 4)
                        .padding(.top)
                        .clipShape(.rect)
                        .gesture(
                            DragGesture()
                                .onChanged({ value in
                                    let predictedY = value.predictedEndTranslation.height
                                    
                                    if predictedY > 400 {
                                        withAnimation(.snappy) {
                                            isOpen = false
                                        }
                                    } else if predictedY < 150 {
                                        withAnimation(.snappy) {
                                            isOpen = true
                                        }
                                    }
                                })
                        )
                }
            }
            .background(.thickMaterial, in: .rect(cornerRadius: 15, style: .continuous))
            .padding()
            .frame(width: size.width / 2.8, height: isOpen ? size.height : size.height / 8) // <-- This is for the size
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) // <-- This is to Position it to the leading side
            .offset(y: isOpen ? 0 : (size.height - size.height / 2) - 40) // To take it to the bottom when closed
        }
        
    }
    

    And there you have it

    Custom Map Sheet

    Of course you can improve it in multiple ways, but I think it's a good starting point. Let me know your thoughts!