Exploring SwiftUI: Custom Modal for iPad
Book

Exploring MusicKit and Apple Music API

Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!

If you’ve worked with the sheet modifier on iPad, you cannot control the presentation size of a modal sheet with SwiftUI. And the sheet gets too big on a 12.9” screen iPad.

There is a question on Stack Overflow that asks about SwiftUI sheet() modals with custom size on iPad. The most rated answer provides you with a custom formSheet modifier.

UIKit Navigation

For my use case, however, I’m using UIKit navigation. So, it made sense to explore an option where I can use the existing way of calling the UIViewController.present(_:animated:completion:) method. It uses modalTransitionStyle and modalPresentationStyle.

Custom UIHostingController

To start with, we create a custom UIHostingController with clear background color and custom transition and presentation style. You can also make it more generic by accepting the styles as a parameter:

class UISheetController<Content>: UIHostingController<Content> where Content: View {
  override init(rootView: Content) {
    super.init(rootView: rootView)
    
    view.backgroundColor = .clear
    modalPresentationStyle = .overCurrentContext
    modalTransitionStyle = .crossDissolve
  }
  
  @available(*, unavailable)
  required public init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

CustomSheetView

Then, we create a custom sheet view. It takes a view as a parameter and will dismiss the sheet if you tap outside the view. It takes the custom size as ratios of the whole screen:

struct CustomSheetView<Content: View>: View {
  @Environment(\.dismiss) private var dismiss
  private var content: Content
  private var heightRatio: CGFloat
  private var widthRatio: CGFloat
  
  init(height: CGFloat = 0.7, width: CGFloat = 0.7, @ViewBuilder content: @escaping () -> Content) {
    self.widthRatio = width
    self.heightRatio = height
    self.content = content()
  }
  
  var body: some View {
    ZStack {
      Color.black.opacity(0.5).onTapGesture { dismiss() }
      
      GeometryReader { geometry in
        let width =  geometry.size.width * widthRatio
        let height = geometry.size.height * heightRatio
        
        content
          .position(x: geometry.frame(in: .global).midX,
                    y: geometry.frame(in: .global).midY)
          .frame(width: width, height: height, alignment: .center)
      }
    }
    .edgesIgnoringSafeArea(.all)
  }
}

The ZStack has a Color that creates the effect of darkening the view behind the sheet, and the content is positioned in the middle.

Usage

You can use it like:

func presentAlert(for error: Error) {
  let view = CustomSheetView(height: 0.4, width: 0.3) { CustomAlertView(error: error) }
  let controller = UISheetController(rootView: view)
  
  navigationController?.present(controller, animated: true)
}

Conclusion

You can use the native way of creating a similar modifier like sheet to get a custom sheet. If you’re using UIKit navigation, you may prefer to use the solution mentioned in this article.

If you have a better approach, please tag @rudrankriyam on Twitter! I love constructive feedback and appreciate constructive criticism.

Thanks for reading, and I hope you’re enjoying it!

Book

Exploring MusicKit and Apple Music API

Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!

Written by

Rudrank Riyam

Hi, my name is Rudrank. I create apps for Apple Platforms while listening to music all day and night. Author of "Exploring MusicKit". Apple WWDC 2019 scholarship winner.