I need to open the ImagePicker in my app using SwiftUI, how can I do that?
I thought about using the UIImagePickerController, but I don\'t know how to do that in Swi
Here's a version that works in Xcode 11 beta 4.
It uses a BindableObject singleton (ImagePicker.shared) with two properties: .view and .image.
See usage below (ImagePickerTestView)
import SwiftUI
import Combine
final class ImagePicker : BindableObject {
static let shared : ImagePicker = ImagePicker()
private init() {} //force using the singleton: ImagePicker.shared
let view = ImagePicker.View()
let coordinator = ImagePicker.Coordinator()
// Bindable Object part
let willChange = PassthroughSubject<Image?, Never>()
@Published var image: Image? = nil {
didSet {
if image != nil {
willChange.send(image)
}
}
}
}
extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
// UIImagePickerControllerDelegate
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
ImagePicker.shared.image = Image(uiImage: uiImage)
picker.dismiss(animated:true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated:true)
}
}
struct View: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
ImagePicker.shared.coordinator
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker.View>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker.View>) {
}
}
}
struct ImagePickerTestView: View {
@State var showingPicker = false
@State var image : Image? = nil
// you could use ImagePicker.shared.image directly
var body: some View {
VStack {
Button("Show image picker") {
self.showingPicker = true
}
image?
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300)
}.sheet(isPresented: $showingPicker,
onDismiss: {
// do whatever you need here
}, content: {
ImagePicker.shared.view
})
.onReceive(ImagePicker.shared.$image) { image in
// This gets called when the image is picked.
// sheet/onDismiss gets called when the picker completely leaves the screen
self.image = image
}
}
}
#if DEBUG
struct ImagePicker_Previews : PreviewProvider {
static var previews: some View {
ImagePickerTestView()
}
}
#endif
You need to wrap UIImagePickerController
in a struct implementing UIViewControllerRepresentable
.
For more about UIViewControllerRepresentable
, please check this amazing WWDC 2019 talk:
Integrating SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let sourceType: UIImagePickerController.SourceType
let onImagePicked: (UIImage) -> Void
final class Coordinator: NSObject,
UINavigationControllerDelegate,
UIImagePickerControllerDelegate {
@Binding
private var presentationMode: PresentationMode
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (UIImage) -> Void
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (UIImage) -> Void) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
onImagePicked(uiImage)
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode,
sourceType: sourceType,
onImagePicked: onImagePicked)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
Here's a simple view to test it:
Show image picker
buttonstruct ContentView: View {
@State var showImagePicker: Bool = false
@State var image: Image? = nil
var body: some View {
ZStack {
VStack {
Button(action: {
self.showImagePicker.toggle()
}) {
Text("Show image picker")
}
image?.resizable().frame(width: 100, height: 100)
}
.sheet(isPresented: $showImagePicker) {
ImagePicker(sourceType: .photoLibrary) { image in
self.image = Image(uiImage: image)
}
}
}
}
}
I hope this helps as a starting point!
I'm sure Apple will make this easier to do once SwiftUI
is out of beta.
Tested on Xcode 11.4
Bugs:
sourceType
is not the camera.
You won't be able to drag down the sheet - I haven't been able to find a solution yet.Cleaned up version for Xcode 11.4 available via SPM as Swift Package:
https://github.com/ralfebert/ImagePickerView
Source:
import SwiftUI
public struct ImagePickerView: UIViewControllerRepresentable {
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (UIImage) -> Void
@Environment(\.presentationMode) private var presentationMode
public init(sourceType: UIImagePickerController.SourceType, onImagePicked: @escaping (UIImage) -> Void) {
self.sourceType = sourceType
self.onImagePicked = onImagePicked
}
public func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = self.sourceType
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(
onDismiss: { self.presentationMode.wrappedValue.dismiss() },
onImagePicked: self.onImagePicked
)
}
final public class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
private let onDismiss: () -> Void
private let onImagePicked: (UIImage) -> Void
init(onDismiss: @escaping () -> Void, onImagePicked: @escaping (UIImage) -> Void) {
self.onDismiss = onDismiss
self.onImagePicked = onImagePicked
}
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage {
self.onImagePicked(image)
}
self.onDismiss()
}
public func imagePickerControllerDidCancel(_: UIImagePickerController) {
self.onDismiss()
}
}
}
Based on @user:2890168 I made a version that:
UIImage
instead of Image
.sheet
to present the ImagePicker
. ActionSheet
to help users to remove or change the image.struct LibraryImage: View {
@State var showAction: Bool = false
@State var showImagePicker: Bool = false
@State var uiImage: UIImage? = nil
var sheet: ActionSheet {
ActionSheet(
title: Text("Action"),
message: Text("Quotemark"),
buttons: [
.default(Text("Change"), action: {
self.showAction = false
self.showImagePicker = true
}),
.cancel(Text("Close"), action: {
self.showAction = false
}),
.destructive(Text("Remove"), action: {
self.showAction = false
self.uiImage = nil
})
])
}
var body: some View {
VStack {
if (uiImage == nil) {
Image(systemName: "camera.on.rectangle")
.accentColor(Color.App.purple)
.background(
Color.App.gray
.frame(width: 100, height: 100)
.cornerRadius(6))
.onTapGesture {
self.showImagePicker = true
}
} else {
Image(uiImage: uiImage!)
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(6)
.onTapGesture {
self.showAction = true
}
}
}
.sheet(isPresented: $showImagePicker, onDismiss: {
self.showImagePicker = false
}, content: {
ImagePicker(isShown: self.$showImagePicker, uiImage: self.$uiImage)
})
.actionSheet(isPresented: $showAction) {
sheet
}
}
}
The default body
of LibraryImage
is an Image
that shows a camera icon that is tappable by the users.
On tap event, the image picker is shown with a sheet
modifier. After the image selection, the LibraryImage
body is recomputed and now shows the Image
defined in else statement (because uiImage
property now contains the image picked by the user).
Now, on tap event the ActionSheet
is shown.
The edited image picker:
struct ImagePicker: UIViewControllerRepresentable {
@Binding var isShown: Bool
@Binding var uiImage: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var isShown: Bool
@Binding var uiImage: UIImage?
init(isShown: Binding<Bool>, uiImage: Binding<UIImage?>) {
_isShown = isShown
_uiImage = uiImage
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let imagePicked = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
uiImage = imagePicked
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, uiImage: $uiImage)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
default behaviour:
I'm very new at Swift, but I was able to get it with the following.
This will load up an image picker modal and let you select a photo, and it will then update an @State
variable from a parent.
If this works for you, you can replace the @State
with something that can span across multiple components, such as @EnvironmentObject
so other components can get updated as well.
Hope this helps!
// ImagePicker.swift
struct ImagePicker : View {
@State var image: UIImage? = nil
var body: some View {
ImagePickerViewController(image: $image)
}
}
// ImagePickerViewController.swift
import UIKit
import AVFoundation
import SwiftUI
struct ImagePickerViewController: UIViewControllerRepresentable {
@Binding var image: UIImage?
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerViewController>) {
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePickerViewController>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = UIImagePickerController.SourceType.photoLibrary
imagePicker.allowsEditing = false
imagePicker.delegate = context.coordinator
return imagePicker
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
var parent: ImagePickerViewController
init(_ parent: ImagePickerViewController) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let imagePicked = info[.originalImage] as! UIImage
parent.image = imagePicked
picker.dismiss(animated: true, completion: nil)
}
}
}
Usage:
// SampleView.swift
struct SampleView : View {
var body: some View {
PresentationLink(destination: ImagePicker().environmentObject(self.userData), label: {
Text("Import Photo")
})
}
}
Once again, I am fresh into Swift so if anyone has some comments, please let me know! Happy to learn more.
iOS 14 Xcode 12 - Photo Picker SwiftUI with Reusable View with limits allowed
struct ImagePickerView: UIViewControllerRepresentable {
@Binding var images: [UIImage]
@Binding var showPicker: Bool
var selectionLimit: Int
func makeUIViewController(context: Context) -> some UIViewController {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = selectionLimit
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: ImagePickerView
init(parent: ImagePickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.showPicker.toggle()
for img in results {
if img.itemProvider.canLoadObject(ofClass: UIImage.self) {
img.itemProvider.loadObject(ofClass: UIImage.self) { (image, err) in
guard let image1 = image else { return }
DispatchQueue.main.async {
self.parent.images.append(image1 as! UIImage)
}
}
} else {
// Handle Error
parent.showPicker.toggle()
}
}
}
}
}
then in View you can do
VStack {
Image(systemName: "camera.viewfinder")
.resizable()
.aspectRatio(contentMode: .fit)
.onTapGesture {
self.viewModel.pickerBool.toggle()
}
}
.sheet(isPresented: self.$viewModel.pickerBool) {
ImagePickerView(images: self.$viewModel.images, showPicker: self.$viewModel.pickerBool, selectionLimit: 3)
}