7

I present a UIImagePickerController within my application by presenting it with logic inside of a sheet modifier. In short, the following three types handle displaying and dismissing a instance of UIImagePickerController inside of a UIViewControllerRepresentable type, which works as expected:

struct DetailsView: View {

    enum Sheet: Hashable, Identifiable {
        case takePhoto

        var id: Int { hashValue }
    }

    @State private var activeSheet: Sheet?

    var body: some View {
        Text("Hello, World!")
            .sheet(item: $activeSheet) { (sheet) in self.view(for: sheet) }
    }

    private func view(for sheet: Sheet) -> some View {
        switch sheet {
        case .takePhoto: return PhotoSelectionView(showImagePicker: .init(get: { sheet == .takePhoto }, set: { (show) in self.activeSheet = show ? .takePhoto : nil }), image: $selectedImage, photoSource: .camera).edgesIgnoringSafeArea(.all)
        }
    }

}
struct ImagePicker: UIViewControllerRepresentable {

    @Binding var isShown: Bool

    @Binding var image: Image?

    let photoSource: PhotoSource

    func makeCoordinator() -> ImagePickerCoordinator {
        return ImagePickerCoordinator(isShown: $isShown, selectedImage: $image)
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    } 

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        if photoSource == .camera, UIImagePickerController.isSourceTypeAvailable(.camera) {
            imagePicker.sourceType = .camera
            imagePicker.cameraCaptureMode = .photo
        }
        imagePicker.delegate = context.coordinator
        return imagePicker
    }
}
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

    @Binding private var isShown: Bool

    @Binding private var selectedImage: Image?

    init(isShown: Binding<Bool>, selectedImage: Binding<Image?>) {
        _isShown = isShown
        _selectedImage = selectedImage
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        // handle photo selection 
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss()
    }

}

The issue that I am having is that the camera view is presented modally, which doesn't cover the entire screen. This causes the UIImagePickerController to appear to have broken layouts at times when the camera is the source, as if the camera was not made to be presented in this way. Setting imagePicker.modalPresentationStyle = .fullScreen does not result in a full-screen presentation.

Screenshot of UIImagePickerController with a camera source

How can I display the camera in a full-screen layout so that it does not appear in the card-like presentation style?

Nick Kohrn
  • 5,779
  • 3
  • 29
  • 49
  • 1
    In non-SwiftUI apps, you’d control this by setting the `modalPresentationStyle` of the view being presented. But that doesn’t work with `sheet`. There’s nowhere to specify the presentation style (and attempts to set this on the `UIImagePickerController` don’t work). See https://stackoverflow.com/a/58970681/1271826 for possible workarounds. – Rob Apr 22 '20 at 23:21
  • 1
    Just a friendly suggestion: In the future it might be useful if you could post a [MCVE](https://stackoverflow.com/help/mcve). Things like `PhotoSelectionView` and `PhotoSource` are not defined here, which made it exceedingly hard to manifest the problem and thereby deduce what was going on. You’ll probably get more responses in the future if you share enough code to reproduce the problem in question... – Rob Apr 22 '20 at 23:28
  • @Rob, the URL that you provided was helpful because I was missing the concept of `UIHostingController` being available to represent SwiftUI view hierarchies. Also, I appreciate the suggestion for creating a MCVE. I should have removed `PhotoSelectionView` and `PhotoSource` from my original code samples. – Nick Kohrn Apr 23 '20 at 10:54
  • it's crazy that there are no simple way to display full screen UIImagePicker in SwiftUI – GrandSteph May 02 '20 at 11:15

4 Answers4

14

combining @LaX and @GrandSteph's answers, I have:

.fullScreenCover(isPresented: $activeSheet, content: {
    ImagePicker(image: $inputImage, sourceType: .camera)
       .edgesIgnoringSafeArea(.all)
 })
ada10086
  • 292
  • 5
  • 8
2

With iOS 14 Apple has added the fullScreenCover(ispresented:ondismiss:content:) and fullScreenCover(item:ondismiss:content:) methods which do exactly what you are requesting.

From your example:

    var body: some View {
        Text("Hello, World!")
            .fullScreenCover(item: $activeSheet) { (sheet) in self.view(for: sheet) }
    }

Or, if you simply have a view you want to show:

   @State var customViewIsShown = false

    // ...

    var body: some View {
        Text("Hello, World!")
        .fullScreenCover(isPresented: $customViewIsShown) {
            YourCustomView(isShown: $customViewIsShown)
        }
    }
LaX
  • 453
  • 5
  • 16
0

Ok, take this answer with a large grain of salt. I'll probably be downvoted to hell because of that and all my coder kids with me ... But so should Apple for not providing an easy way to present full screen a UIImagePickerController in SwiftUI.

The very bad the trick is to create your picker in the rootView and have it ignore safe area. Then you pass all necessary parameters as bindings to the view needing the imagePicker

struct mainView: View {

    @State private var imagePicked = UIImage()
    @State private var showImagePicker = false
    @State private var pickerSource = UIImagePickerController.SourceType.camera

    var body: some View {
        ZStack {
            AvatarView(showImagePicker: self.$showImagePicker, pickerSource: self.$pickerSource , imagePicked: self.$imagePicked)

            if self.showImagePicker {
                ImagePickerView(isPresented: self.$showImagePicker, selectedImage: self.$imagePicked, source: .camera).edgesIgnoringSafeArea(.all)
            }
        }
    }
}

Of course, your Avatar view will have all the necessary code to update these bindings. Something like this

HStack () {
    Button(action: {
        self.showActionSheet.toggle()
    }) {
        MyImageView(image: self.imagePicked)
    }
    .actionSheet(isPresented: $showActionSheet, content: {
        ActionSheet(title: Text("Picture source"), buttons: [
            .default(Text("Camera"), action: {
                self.pickerSource = .camera
                self.showImagePicker.toggle()
            }),
            .default(Text("Photo Library"), action: {
                self.pickerSource = .photoLibrary
                self.showImagePicker.toggle()
            }),
            .destructive(Text("Cancel"))
        ])
    })
}

Honestly I was hesitant to provide this solution, this code makes my eyes bleed but figured someone might have a better idea when reading this and provide a real clean SwiftUI way of doing it.

The good thing about this solution is that it manages well the changes of orientation and let's face it, the UIImagePickerController doesn't fit properly a sheet. It's fine for photo library but not camera.

GrandSteph
  • 2,053
  • 1
  • 16
  • 23
0

If you wrap your ImagePicker in a container with black background, that will result in what I think you want to get:

.fullScreenCover(isPresented: $showPhotoPicker) {
    ImagePicker(sourceType: .camera, selectedImage: self.$image)
        // frame modifier adds a container with given size preferences
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.black)
}

where

Photo picker covering the whole screen

Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90