raynertanxw
raynertanxw

Reputation: 283

SwiftUI Are Presentation Detents supported on iPad?

So I have a minimum target of iOS 16, am using .sheet() with .presentationDetents([.medium]) and it works fine on iOS (iPhone). But when I load it on iPadOS it's always a fullsized sheet, seemingly ignoring the presentation detents. Here is a minimal reproducable code that demonstrates this behaviour.

import SwiftUI

struct ContentView: View {
    @State var shouldShowSheet: Bool = false
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
            
            Button("Show Sheet") {
                shouldShowSheet.toggle()
            }
        }
        .padding()
        .sheet(isPresented: $shouldShowSheet, content: {
            VStack {
                Text("Some content")
                Text("Some more content")
                Text("Even more content")
                
                Button("Dismiss Sheet") {
                    shouldShowSheet.toggle()
                }
            }
            .presentationDetents([.medium])
        })
    }
}

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

Below are screenshots of the same code running on iOS and running on iPadOS enter image description here

enter image description here

I tried using .fraction, .medium, .height and none of them had any effect on iPad sheets. The sheet is always a full-sized one as shown in the image. But on iOS(iPhone) it works as expected.

I'm expecting sheets on iPads to also respect the presentation detents. How can I get different sized sheets on iPad?

Upvotes: 21

Views: 2788

Answers (3)

Victor Nethesh
Victor Nethesh

Reputation: 1

Using .sheet() with .presentationDetents([.medium]) works on iPhone but always appears fullscreen on iPad. This happens because iPadOS treats .sheet() differently, defaulting to a full-screen modal. Instead, use .popover() for iPad:

Button("Show Popover") {
  showPopover = true
}
.popover(isPresented: $showPopover) {
  Text("This is a popover").frame(width: 400, height: 300)
}

Upvotes: 0

guido
guido

Reputation: 2896

If you want to present a medium (or any size) sheet on iPad, here is a workaround. It's not great, but perhaps it's enough for you. Works with iOS16 and up.

  • It shows the regular .sheet on iPhone (with working detents)
  • It shows a slightly modified sheet (with close button) on iPad with a predefined frame (see code comments)
  • It doesn't support detents on iPad
  • Can't swipe to dismiss to dismiss on iPad, although you might be able to add that. (I didn't attempt that as the close button is good enough for my use case)
  • You can't use @Environment(\.dismiss) var dismiss to dismiss the sheet in code (if you need that functionality). Instead, you'll need to pass the isPresented binding to the sheet content and than do isPresented.toggle() to close the sheet in code.
  • You could add a GeometryReader to make it really medium size.

import Foundation
import SwiftUI
import DeviceKit  // https://github.com/devicekit/DeviceKit 

struct UniversalSheet: ViewModifier {

    @Binding var isPresented: Bool
    var onDismiss: (() -> Void)?
    var sheetContent: () -> any View
    
    func body(content: Content) -> some View {
        ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
            content
                .if(!Device.current.isPad, transform: {
                    $0.sheet(isPresented: $isPresented, onDismiss: onDismiss, content: {AnyView(sheetContent())})
                })
            if Device.current.isPad && isPresented {
                VStack(spacing: 0) {
                    
                    // This adds the close button
                    HStack {
                        Spacer()
                        Button(action: {
                            isPresented.toggle()
                        }, label: {
                            ZStack {
                                Image(systemName: "xmark.circle.fill")
                                    .resizable()
                                    .scaledToFit()
                                    .font(Font.body.weight(.bold))
                                    .scaleEffect(0.8)
                                    .foregroundColor(Color.gray.opacity(0.6))
                            }
                            .frame(width: 35, height: 35)
                        })
                        .padding([.top, .trailing], 8)
                    }
                    .background(Color(uiColor: .secondarySystemBackground))
                    
                    // This adds the actual sheet content
                    AnyView(sheetContent())
                        .frame(height: 370) // The fixed height of the sheet (on iPad) (excluding the close button)
                        .transition(.move(edge: .bottom))
                }
                .frame(width: 450) // The fixed width of the sheet (on iPad)
                .clipShape(RoundedRectangle(cornerRadius: 10))
                .padding(.bottom)
                .onDisappear() {
                    onDismiss?()
                }
                
            }
        }
    }
}

extension View {
    func universalSheet(isPresented: Binding, onDismiss: (() -> Void)?, content: @escaping () -> any View) -> some View {
        self.modifier(UniversalSheet(isPresented: isPresented, onDismiss: onDismiss, sheetContent: content))
    }

    @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

Note: universalSheet(isPresented: Binding should be universalSheet(isPresented: Binding<Bool> (somehow the codeblock can't handle < and > (let me know if you know how to change that).

USAGE: You use it just as you would the .sheet modifier:


.universalSheet(isPresented: $presentSheet, onDismiss: {
                somethingYouWantToExecuteOnSheetDismiss()
            }, content: {
                YourSheetView(presentSheet: $presentSheet)
            })

Upvotes: 0

yo1995
yo1995

Reputation: 637

You might be looking for the new presentationSizing(_:) API that is available in iOS 18+.

It allows configuring the shape and dimension of the modal presented sheet. From there you may build your own detent behavior on a regular-regular size class window such as full screen iPad.

Upvotes: 0

Related Questions