I'm really struggling to wrap the new iOS 15 UISheetPresentationController
for use in SwiftUI (for a half-modal). I understand that I should inherit UIViewControllerRepresentable
. Based upon an example I have for a custom ImagePicker, I've not been able to make this work.
Can anyone help? In particular I don't know to get a handle on the presentedViewController
needed to init the UISheetPresentationController
itself:
func makeUIViewController(context: UIViewControllerRepresentableContext<KitSheet>) -> UISheetPresentationController {
let sheet = UISheetPresentationController(presentedViewController: <#T##UIViewController#>, presenting: <#T##UIViewController?#>)
sheet.delegate = context.coordinator
return sheet
}
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller
SwiftUI works seamlessly with the existing UI frameworks on all Apple platforms. For example, you can place UIKit views and view controllers inside SwiftUI views, and vice versa.
SwiftUI won't replace UIKit until those students @Stadford get into the management of software companies, so it will be a few more years yet.
Although SwiftUI does a good job of providing many of UIKit's UIView subclasses, it doesn't have them all yet at this time. Fortunately, it's not hard to create custom wrappers for a UIView that you want. As an example, let's create a simple SwiftUI wrapper for UITextView as the basis of a rich text editor.
If you want the Image Picker
import SwiftUI
///Sample usage
@available(iOS 15.0, macCatalyst 15.0,*)
struct ImagePickerParentView: View {
@State var isPresented = false
@State var selectedImage: UIImage? = nil
var body: some View {
print("ImagePickerParentView :: \(#function) :: isPresented == \(isPresented)")
return VStack{
if selectedImage != nil{
Image(uiImage: selectedImage!)
.resizable()
.frame(width: 100, height: 100)
}
Button("present image picker", action: {
isPresented.toggle()
}).imagePicker(isPresented: $isPresented, uiImage: $selectedImage, detents: [.medium()], largestUndimmedDetentIdentifier: .large)
}
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
extension View {
func imagePicker(isPresented: Binding<Bool>, uiImage: Binding<UIImage?>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool = false, preferredCornerRadius: CGFloat? = nil)-> some View {
print("\(#function) :: isPresented == \(isPresented)")
return modifier(ImagePickerViewModifier(isPresented: isPresented, uiImage: uiImage, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, preferredCornerRadius: preferredCornerRadius))
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct ImagePickerViewModifier: ViewModifier {
@Binding var isPresented: Bool
@Binding var uiImage: UIImage?
let detents : [UISheetPresentationController.Detent]
let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
let prefersGrabberVisible: Bool
let preferredCornerRadius: CGFloat?
func body(content: Content) -> some View {
print("ImagePickerViewModifier :: \(#function) :: isPresented == \(isPresented)")
return content.overlay(
AdaptiveImagePicker_UI(isPresented: $isPresented, uiImage: $uiImage, detents: detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, preferredCornerRadius: preferredCornerRadius).frame(width: 0, height: 0)
)
.onChange(of: isPresented, perform: { value in
print("AdaptiveSheet :: onChange :: isPresented == \(value)")
})
//}
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct AdaptiveImagePicker_UI: UIViewControllerRepresentable {
@Binding var isPresented: Bool
@Binding var uiImage: UIImage?
var detents : [UISheetPresentationController.Detent] = [.medium(), .large()]
var largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium
var prefersScrollingExpandsWhenScrolledToEdge: Bool = false
var prefersEdgeAttachedInCompactHeight: Bool = true
var prefersGrabberVisible: Bool = false
var preferredCornerRadius: CGFloat?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> AdaptiveImagePickerViewController {
print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
let vc = AdaptiveImagePickerViewController(coordinator: context.coordinator, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, preferredCornerRadius: preferredCornerRadius)
return vc
}
func updateUIViewController(_ uiViewController: AdaptiveImagePickerViewController, context: Context) {
print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
print("CustomSheet_UI :: \(#function) :: context.coordinator.parent.isPresented == \(context.coordinator.parent.isPresented)")
if isPresented {
uiViewController.presentImagePicker()
}else{
uiViewController.dismissModalView()
}
}
class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: AdaptiveImagePicker_UI
var isPresented: Bool = false
init(_ parent: AdaptiveImagePicker_UI) {
print("CustomSheet_UI :: \(#function) :: parent.isPresented == \(parent.isPresented)")
self.parent = parent
}
//Adjust the variable when the user dismisses with a swipe
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
print("CustomSheet_UI.Coordinator :: \(#function) :: parent.isPresented == \(parent.isPresented)")
if parent.isPresented{
parent.isPresented = false
}
}
//Adjust the variable when the user cancels
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
if parent.isPresented{
parent.isPresented = false
}
}
//Get access to the selected image
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage {
parent.uiImage = image
parent.isPresented = false
}
}
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
class AdaptiveImagePickerViewController: UIViewController {
var coordinator: AdaptiveImagePicker_UI.Coordinator
let detents : [UISheetPresentationController.Detent]
let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
let prefersGrabberVisible: Bool
let preferredCornerRadius: CGFloat?
private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
init(coordinator: AdaptiveImagePicker_UI.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool, preferredCornerRadius: CGFloat?) {
print("AdaptiveImagePickerViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
self.coordinator = coordinator
self.detents = detents
self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
self.prefersGrabberVisible = prefersGrabberVisible
self.preferredCornerRadius = preferredCornerRadius
super.init(nibName: nil, bundle: .main)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dismissModalView(){
print("AdaptiveImagePickerViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
dismiss(animated: true, completion: nil)
}
//This is mostly code from the Apple sample
//https://developer.apple.com/documentation/uikit/uiviewcontroller/customize_and_resize_sheets_in_uikit
func presentImagePicker(){
guard presentedViewController == nil else {
dismiss(animated: true, completion: {
self.presentImagePicker()
})
return
}
let imagePicker = UIImagePickerController()
imagePicker.delegate = coordinator
imagePicker.modalPresentationStyle = .popover
//Added the presentation controller delegate to detect if the user swipes to dismiss
imagePicker.presentationController?.delegate = coordinator as UIAdaptivePresentationControllerDelegate
if let hostPopover = imagePicker.popoverPresentationController {
hostPopover.sourceView = super.view
let sheet = hostPopover.adaptiveSheetPresentationController
//As of 13 Beta 4 if .medium() is the only detent in landscape error occurs
sheet.detents = (isLandscape ? [.large()] : detents)
sheet.largestUndimmedDetentIdentifier =
largestUndimmedDetentIdentifier
sheet.prefersScrollingExpandsWhenScrolledToEdge =
prefersScrollingExpandsWhenScrolledToEdge
sheet.prefersEdgeAttachedInCompactHeight =
prefersEdgeAttachedInCompactHeight
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
sheet.prefersGrabberVisible = prefersGrabberVisible
sheet.preferredCornerRadius = preferredCornerRadius
}
present(imagePicker, animated: true, completion: nil)
}
/// To compensate for l orientation
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape {
isLandscape = true
self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = [.large()]
} else {
isLandscape = false
self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = detents
}
super.viewWillTransition(to: size, with: coordinator)
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct ImagePickerParentView_Previews: PreviewProvider {
static var previews: some View {
ImagePickerParentView()
}
}
If you want one that takes any SwiftUI View
it only needs a few changes.
//This is the sample usage
@available(iOS 15.0, macCatalyst 15.0,*)
struct CustomSheetParentView: View {
@State var isPresented = false
var body: some View {
print("CustomSheetParentView :: \(#function) :: isPresented == \(isPresented)")
return VStack{
Button("present sheet", action: {
isPresented.toggle()
}).adaptiveSheet(isPresented: $isPresented, detents: [.medium()], largestUndimmedDetentIdentifier: .medium, disableSwipeToDismiss: false){
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 100, alignment: .center)
.foregroundColor(.clear)
.border(Color.blue, width: 3)
.overlay(
LazyVStack{
Text("Hello, World!")
Button("dismiss", action: {
print("dismiss button :: isPresented == \(isPresented)")
isPresented = false
})
CustomSheetParentView()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
print("onTap :: isPresented == \(isPresented)")
isPresented.toggle()
}
)
.background(Color(UIColor.systemBackground))
}
}
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct CustomSheetView_Previews: PreviewProvider {
static var previews: some View {
CustomSheetParentView()
}
}
//EVERYTHING from here down is Reusable and can be pasted into a project and then use `.adaptiveSheet` just like `.sheet`
@available(iOS 15.0, macCatalyst 15.0,*)
extension View {
func adaptiveSheet<T: View>(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool = false, disableSwipeToDismiss: Bool = false, preferredCornerRadius: CGFloat? = nil, @ViewBuilder content: @escaping () -> T)-> some View {
print("\(#function) :: isPresented == \(isPresented)")
return modifier(AdaptiveSheet<T>(isPresented: isPresented, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, disableSwipeToDismiss: disableSwipeToDismiss, preferredCornerRadius: preferredCornerRadius, sheetContent: content))
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct AdaptiveSheet<T: View>: ViewModifier {
@Binding var isPresented: Bool
let detents : [UISheetPresentationController.Detent]
let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
let prefersGrabberVisible: Bool
let disableSwipeToDismiss: Bool
let preferredCornerRadius: CGFloat?
@ViewBuilder let sheetContent: T
func body(content: Content) -> some View {
print("AdaptiveSheet :: \(#function) :: isPresented == \(isPresented)")
return content.overlay(
CustomSheet_UI(isPresented: $isPresented, detents: detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, disableSwipeToDismiss: disableSwipeToDismiss,preferredCornerRadius: preferredCornerRadius, content: {sheetContent}).frame(width: 0, height: 0)
)
.onChange(of: isPresented, perform: { value in
print("AdaptiveSheet :: onChange :: isPresented == \(value)")
})
//}
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct CustomSheet_UI<T: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
var detents : [UISheetPresentationController.Detent] = [.medium(), .large()]
var largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium
var prefersScrollingExpandsWhenScrolledToEdge: Bool = false
var prefersEdgeAttachedInCompactHeight: Bool = true
var prefersGrabberVisible: Bool = false
var disableSwipeToDismiss: Bool = false
var preferredCornerRadius: CGFloat?
@ViewBuilder let content: T
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> CustomSheetViewController<T> {
print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
let vc = CustomSheetViewController(coordinator: context.coordinator, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, disableSwipeToDismiss: disableSwipeToDismiss, preferredCornerRadius: preferredCornerRadius, content: {content})
return vc
}
func updateUIViewController(_ uiViewController: CustomSheetViewController<T>, context: Context) {
print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
print("CustomSheet_UI :: \(#function) :: context.coordinator.parent.isPresented == \(context.coordinator.parent.isPresented)")
if isPresented {
uiViewController.presentModalView()
}else{
uiViewController.dismissModalView()
}
}
class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
var parent: CustomSheet_UI
var isPresented: Bool = false
init(_ parent: CustomSheet_UI) {
print("CustomSheet_UI :: \(#function) :: parent.isPresented == \(parent.isPresented)")
self.parent = parent
}
//Adjust the variable when the user dismisses with a swipe
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
print("CustomSheet_UI.Coordinator :: \(#function) :: parent.isPresented == \(parent.isPresented)")
if parent.isPresented{
parent.isPresented = false
}
}
}
}
@available(iOS 15.0, macCatalyst 15.0,*)
class CustomSheetViewController<Content: View>: UIViewController {
let content: Content
var coordinator: CustomSheet_UI<Content>.Coordinator
let detents : [UISheetPresentationController.Detent]
let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
let prefersScrollingExpandsWhenScrolledToEdge: Bool
let prefersEdgeAttachedInCompactHeight: Bool
let prefersGrabberVisible: Bool
let disableSwipeToDismiss: Bool
let preferredCornerRadius: CGFloat?
private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
init(coordinator: CustomSheet_UI<Content>.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool, disableSwipeToDismiss: Bool, preferredCornerRadius: CGFloat?, @ViewBuilder content: @escaping () -> Content) {
print("CustomSheetViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
self.content = content()
self.coordinator = coordinator
self.detents = detents
self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
self.prefersGrabberVisible = prefersGrabberVisible
self.disableSwipeToDismiss = disableSwipeToDismiss
self.preferredCornerRadius = preferredCornerRadius
super.init(nibName: nil, bundle: .main)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dismissModalView(){
print("CustomSheetViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
dismiss(animated: true, completion: nil)
}
func presentModalView(){
print("CustomSheetViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
let hostingController = UIHostingController(rootView: content)
//allows background color to be decided by SwiftUI content.
// Incase you want to use a Material that gives transparency
hostingController.view.backgroundColor = nil
hostingController.modalPresentationStyle = .popover
hostingController.presentationController?.delegate = coordinator as UIAdaptivePresentationControllerDelegate
hostingController.modalTransitionStyle = .coverVertical
hostingController.isModalInPresentation = disableSwipeToDismiss
if let hostPopover = hostingController.popoverPresentationController {
hostPopover.sourceView = super.view
let sheet = hostPopover.adaptiveSheetPresentationController
//As of 13 Beta 4 if .medium() is the only detent in landscape error occurs
sheet.detents = (isLandscape ? [.large()] : detents)
sheet.largestUndimmedDetentIdentifier =
largestUndimmedDetentIdentifier
sheet.prefersScrollingExpandsWhenScrolledToEdge =
prefersScrollingExpandsWhenScrolledToEdge
sheet.prefersEdgeAttachedInCompactHeight =
prefersEdgeAttachedInCompactHeight
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
sheet.prefersGrabberVisible = prefersGrabberVisible
sheet.preferredCornerRadius = preferredCornerRadius
}
if presentedViewController == nil{
present(hostingController, animated: true, completion: nil)
}
}
/// To compensate for l orientation
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape {
isLandscape = true
self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = [.large()]
} else {
isLandscape = false
self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = detents
}
super.viewWillTransition(to: size, with: coordinator)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With