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)
}
}