UIViewControllerRepresentable and property wrapper behavior in iOS14, using with CropViewController - ios

I'm using "UIImagePickerController" and "CropViewController" to modify an image selected from photo library. Since the target OS version is 13+, "UIViewControllerRepresentable" is utilized for treating these UIKit parts in SwiftUI. Xcode version is 12.0.
(Note) CropViewController is a 3rd party library.
Source is here: https://github.com/TimOliver/TOCropViewController
I found a strange behavior when launching on iOS14 and can't find out the cause. Please help me!!
The below is what I've done. I added "print()" in the code numbered from "//**** 1****" to '//**** 3****' to show you what is strange.
[ UIImagePickerController ]
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Binding var isPresented: Bool // this is a flag used for presenting "UIImagePickerController"
#Binding var materialImage: UIImage? // this is to be passed to "CropViewController"
#Binding var showCropView: Bool // this is a flag used for presenting "CropViewController"
func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
isPresented: $isPresented,
materialImage: $materialImage,
showCropView: $showCropView
)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {}
}
extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate {
#Binding var isPresented: Bool
#Binding var materialImage: UIImage?
#Binding var showCropView: Bool
public init(isPresented: Binding<Bool>, materialImage: Binding<UIImage?>, showCropView: Binding<Bool>) {
self._isPresented = isPresented
self._materialImage = materialImage
self._showCropView = showCropView
}
}
}
extension ImagePicker.Coordinator: UIImagePickerControllerDelegate {
public func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
DispatchQueue.mainSyncSafe {
self.materialImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
isPresented = false // close UIImagePicker
showCropView = true // launch CropViewController
print("materialImage #ImagePicker: \(materialImage)") // ****1****
}
}
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isPresented = false
}
}
[ CropViewController]
import SwiftUI
import CropViewController
struct CropView: UIViewControllerRepresentable {
var materialImage: UIImage? // selected image with ImagePickerController
#Binding var newImage: UIImage? // modified image with CropViewController
#Binding var isPresented: Bool // this is a flag to show/hide CropViewController
init(materialImage: UIImage?, newImage: Binding<UIImage?>, isPresented: Binding<Bool>){
self.materialImage = materialImage
self._newImage = newImage
self._isPresented = isPresented
}
func makeCoordinator() -> CropView.Coordinator {
return CropView.Coordinator(
isPresented: $isPresented,
newImage: $newImage
)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CropView>) -> CropViewController {
let cropViewController = CropViewController(image: materialImage ?? UIImage())
print("materialImage #CropView: \(materialImage)") // ****2****
cropViewController.delegate = context.coordinator
return cropViewController
}
func updateUIViewController(_ uiViewController: CropViewController,
context: UIViewControllerRepresentableContext<CropView>) {}
}
extension CropView {
class Coordinator: NSObject {
#Binding var isPresented: Bool
#Binding var newImage: UIImage?
public init(isPresented: Binding<Bool>, newImage: Binding<UIImage?>) {
self._isPresented = isPresented
self._newImage = newImage
}
}
}
extension CropView.Coordinator: ObservableObject, CropViewControllerDelegate {
func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
newImage = image
isPresented = false
NotificationCenter.default.post(name: .imageSelected, object: nil, userInfo: nil)
}
func cropViewController(_ cropViewController: CropViewController, didFinishCancelled cancelled: Bool) {
isPresented = false
}
}
[ Content.swift ]
import SwiftUI
struct Content: View {
#State var showImagePicker: Bool = false // flag for show/hide UIImagePickerController
#State var showCropView: Bool = false // flag for show/hide CropViewController
#State var materialImage: UIImage? // selected image with UIImagePickerController
#State var newImage: UIImage? // modified Image with CropViewController
var body: some View {
VStack{
// show some views
Spacer().frame(height: 0).sheet(isPresented: $showImagePicker) {
ImagePicker(isPresented: self.$showImagePicker, materialImage: self.$materialImage, showCropView: self.$showCropView)
}
Spacer().frame(height: 0.0).sheet(isPresented: self.$showCropView) {
CropView(materialImage: self.materialImage, newImage: self.$newImage, isPresented: self.$showCropView)
}
}
.onReceive( NotificationCenter.default.publisher(for: .imageSelected) ){_ in
print("materialImage #Content: \(materialImage)") // ****3****
}
}
}
When launching with a simulator of iOS13.5, debug print is like below.
There is an unkown message, but this is fine.
materialImage #ImagePicker: Optional(<UIImage:0x600002810510 anonymous {4032, 3024}>) // ****1****
materialImage #CropView: Optional(<UIImage:0x600002810510 anonymous {4032, 3024}>) // ****2****
2020-09-22 sbx[ ] [Common] _BSMachError: port c603; (os/kern) invalid capability (0x14) "Unable to insert COPY_SEND"
materialImage #CreateAdv_SwiftUIView: Optional(<UIImage:0x600002810510 anonymous {4032, 3024}>) // ****3****
With a simulator of iOS14, CropView screen is black and debug print is like below. "materialImage #CropView" is nil. And more strange, after dismiss this cropview screen (using cancel button), and select image again, it goes fine (I can select and modify an image).
Is there a bug in iOS14? Or is my implementation bad?
materialImage #ImagePicker: Optional(<UIImage:0x600003f3cf30 anonymous {4032, 3024}>) // ****1****
materialImage #CropView: nil // ****2****
2020-09-22 sbx[ ] [ScrollView] UIScrollView is ignoring an attempt to set zoomScale to a non-finite value: inf
2020-09-22 sbx[ ] [Unknown process name] CGContextTranslateCTM: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
materialImage #CreateAdv_SwiftUIView: Optional(<UIImage:0x600003f3cf30 anonymous {4032, 3024}>) // ****3****
2020-09-22 sbx[ ] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
I hope your advice! Thank you!!

Related

How can I go to the next screen after taking a picture? SwiftUI

I have a problem with the navigation.
I need to take a photo and after that open a new screen with the button and the taken image. Now it opens the same screen but with the taken image.
Here is the first screen
Here is the same screen, but I need another view here with another button
ImagePicker
import Foundation
import SwiftUI
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var isShown: Bool
#Binding var image: Image?
init(isShown: Binding<Bool>, image: Binding<Image?>) {
_isShown = isShown
_image = image
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
image = Image(uiImage: uiImage)
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
struct ImagePicker: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var image: Image?
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> ImagePickerCoordinator {
return ImagePickerCoordinator(isShown: $isShown, image: $image)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
if !UIImagePickerController.isSourceTypeAvailable(.camera){
picker.sourceType = .photoLibrary
} else {
picker.sourceType = .camera
}
return picker
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State private var showImagePicker: Bool = false
#State private var image: Image? = nil
var body: some View {
VStack {
image?.resizable()
.scaledToFit()
Button("Attach the image") {
self.showImagePicker = true
}.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(10)
}.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
}
}
PhotoCaptureView
import SwiftUI
struct PhotoCaptureView: View {
#Binding var showImagePicker: Bool
#Binding var image: Image?
var body: some View {
ImagePicker(isShown: $showImagePicker, image: $image)
}
}
#if DEBUG
struct PhotoCaptureView_Previews: PreviewProvider {
static var previews: some View {
PhotoCaptureView(showImagePicker: .constant(false), image: .constant(Image("")))
}
}
#endif
It's pretty simple, you can use NotificationCenter.
In didFinishPickingMediaWithInfo function, you can post a notification, and in ContentView use the publisher of notification to change a State value.
so in ContentView:
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("NotificationName"))) { _ in
move.toggle()
}
.sheet(isPresented: $move) {
AnotherContentView()
}
And in didFinishPickingMediaWithInfo function:
NotificationCenter.default.post(name: Notification.Name("NotificationName"), object: nil, userInfo: nil)

Value of type 'ImagePicker' has no member 'contentMode'

I'm trying to use contentMode like listed below but I don't understand why I receive the following error:
Cannot infer contextual base in reference to member 'scaleAspectFit'
Value of type 'ImagePicker' has no member 'contentMode'
if let uiImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
parent.contentMode = .scaleAspectFit <--- Error
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
}
Any hints?
Thank you!
attached you find the full code:
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.allowsEditing = true
// picker.mediaTypes = ["public.image"]
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let uiImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
parent.image.contentMode = .scaleAspectFit
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
Add contentMode property
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
#Binding var mode:UIView.ContentMode? // here
Then
parent.mode = .scaleAspectFit

Editing Images With PHPickerViewController SwiftUI

I want my user to be able to edit his image right after he picks it with the new PHPickerViewController, I was able to do it with UIImagePickercontroller like so:
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
typealias UIViewControllerType = UIImagePickerController
var sourceType : UIImagePickerController.SourceType = .camera
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let uiImage = info[.editedImage] as? UIImage {
parent.image = uiImage
} else {
print("no image!")
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.allowsEditing = true
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
But, when updating to Xcode 12.0 I need to support the iOS 14.0 users by implanting the PHPickerViewController like so:
#available(iOS 14, *)
struct PHPicker : UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return PHPicker.Coordinator(parent1: self)
}
#Binding var image : UIImage?
#Binding var picker : Bool
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiView: PHPickerViewController, context: Context) {
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
for img in results {
if img.itemProvider.canLoadObject(ofClass: UIImage.self){
img.itemProvider.loadObject(ofClass: UIImage.self) { (image, err) in
guard let image = image as? UIImage else {return}
self.parent.image = image
self.parent.picker = false
}
} else {
print("error getting image")
}
}
}
var parent : PHPicker
init(parent1: PHPicker) {
parent = parent1
}
}
}
How to I enable the user to edit his image (cutting, changing size, and so on...)?

Can't load camera using SwiftUI

I have 2 buttons, one to load an image from the gallery and one to load from the camera (i.e. take photo). But no matter what I try I keep getting gallery load instead of the camera. I've been using the following code (from my google searches) in my UIImagePickerController:
//
// ImagePicker.swift
//
import Foundation
import SwiftUI
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate,
UIImagePickerControllerDelegate {
#Binding var isShown: Bool
#Binding var image: Image?
init(isShown: Binding<Bool>, image: Binding<Image?>) {
_isShown = isShown
_image = image
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
image = Image(uiImage: uiImage)
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
struct ImagePicker: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var image: Image?
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> ImagePickerCoordinator {
return ImagePickerCoordinator(isShown: $isShown, image: $image)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
}
struct CameraPicker: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var image: Image?
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<CameraPicker>) {
}
func makeCoordinator() -> ImagePickerCoordinator {
return ImagePickerCoordinator(isShown: $isShown, image: $image)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CameraPicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.allowsEditing = true
picker.delegate = context.coordinator
return picker
}
}
struct CameraView: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var image: Image?
func makeCoordinator() -> CameraView.Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CameraView>) -> UIViewController {
let cameraViewController = UIImagePickerController()
cameraViewController.delegate = context.coordinator
cameraViewController.sourceType = .camera
cameraViewController.allowsEditing = false
return cameraViewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CameraView>) {
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: CameraView
init(_ cameraView: CameraView) {
self.parent = cameraView
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
parent.image = Image(uiImage: uiImage)
parent.isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.isShown = false
}
}
}
I basically duplicated the ImagePicker to cater for the camera (CameraPicker). Also tried Camera view from web searches when I couldn't get anywhere.
I'm new to swift so please let me know if there is a better way to do this.
To get image from camera:
//
// PhotoCaptureView.swift
import SwiftUI
struct PhotoCaptureView: View {
#Binding var showImagePicker: Bool
#Binding var image: Image?
var body: some View {
CameraView(isShown: $showImagePicker, image: $image)
}
}
#if DEBUG
struct PhotoCaptureView_Previews: PreviewProvider {
static var previews: some View {
PhotoCaptureView(showImagePicker: .constant(false), image: .constant(Image("")))
}
}
#endif
To get image from gallery:
//
// GalleryCapture.swift
//
import SwiftUI
struct GalleryCaptureView: View {
#Binding var showImagePicker: Bool
#Binding var image: Image?
var body: some View {
ImagePicker(isShown: $showImagePicker, image: $image)
}
}
#if DEBUG
struct GalleryCaptureView_Previews: PreviewProvider {
static var previews: some View {
GalleryCaptureView(showImagePicker: .constant(false), image: .constant(Image("")))
}
}
#endif
My contentview:
//
// ContentView.swift
//
import SwiftUI
struct ContentView: View {
#State private var showImagePicker: Bool = false
#State private var image: Image? = nil
// Function that converts an UIImage to Image
func returnImage() -> Image
{
guard let img = image else {
fatalError("Unable to load image")
}
return img
// return Image(uiImage: img)
}
// Main Body
var body: some View {
NavigationView {
VStack {
VStack {
image?.resizable()
.frame(width: 299, height: 299)
.cornerRadius(6)
}
// returnImage()
// .resizable()
// .frame(width: 299, height: 299)
// .cornerRadius(6)
// .onTapGesture {
// self.showImagePicker = true
// }
HStack {
Button(action: {self.showImagePicker.toggle()}) {
Image(systemName: "photo")
.font(.largeTitle)
.foregroundColor(.white)
Text("Gallery")
.foregroundColor(.white)
}.padding()
.background(Color.blue)
.cornerRadius(10)
.sheet(isPresented: self.$showImagePicker) {
GalleryCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
Button(action: {self.showImagePicker.toggle()}) {
Image(systemName: "camera")
.font(.largeTitle)
.foregroundColor(.white)
Text("Camera")
.foregroundColor(.white)
}.padding()
.background(Color.green)
.cornerRadius(10)
.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
}
}
.navigationBarTitle("My first app")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I want to use MLkit so probably need to look at using UIImage instead of Image as well.
Turned out to be so simple in the end. My mistake was to use the same boolean as the one I used for gallery.
on button you have to self.showImagePicker to true like this :
var body: some View {
NavigationView{
VStack {
image?.resizable().scaledToFit()
Button("Open Camera"){
self.showImagePicker = true
}.padding()
.foregroundColor(Color.white)
.background(Color.purple)
.cornerRadius(10)
}.sheet(isPresented: self.$showImagePicker){
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
.navigationBarTitle(Text("Camera"))
}
}
inside the makeUIViewController method, you just need to add the following code right below picker.delegate = context.coordinator:
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
if !UIImagePickerController.isSourceTypeAvailable(.camera){
picker.sourceType = .photoLibrary
} else {
picker.sourceType = .camera
}
return picker
}
Note : Camera opens only in real device you cant open in simulator.
OR
compare ur code with this awesome code , its working from my side

How to open the ImagePicker in SwiftUI?

I need to open an image picker 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 SwiftUI.
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:
The picker is displayed in a sheet
the selected image appears without any sort of animation, and replaces the Show image picker button
struct 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:
#JAHelia found a bug on the picker when 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 12 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:
retrieves UIImage instead of Image
use .sheet to present the ImagePicker.
shows 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:
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)
}
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
We can use PhotosPicker since iOS 16.0, iPadOS 16.0, macOS 13.0, watchOS 9.0 and above.
Simple example:
import PhotosUI
import SwiftUI
struct MediaView: View {
#State private var photosPickerPresented = false
#State private var photoPickerItems = [PhotosPickerItem]()
var body: some View {
Button {
// Present photo Picker
photosPickerPresented.toggle()
} label: {
Text("Show Photos Picker")
}
.photosPicker(isPresented: $photosPickerPresented, selection: $photoPickerItems)
}
}
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.
I implemented a version that I think is more general and extensible. I used a Subject instead of a Binding to solve the problem where it's undoable/inappropriate to add another Binding in your view.
For example, you created a List showing a set of images stored in the underlying storage and you wanted to add an image with the image picker. In this case, it's very hard/ugly to have that image added to your underlying storage.
So I used a subject to transfer the image and you can simply observe it and add the new images to some storage, or if you want it to behave just like a Binding, it's one line of code, too. (modifying your State in your observation)
Then I wrapped the preferences into a ViewModel so it won't get cluttered if you want to have more subjects or configurations.
import SwiftUI
import Combine
struct ImagePickerView : UIViewControllerRepresentable {
#Binding var model: ImagePickerViewModel
typealias UIViewControllerType = UIImagePickerController
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
controller.allowsEditing = false
controller.mediaTypes = ["public.image"]
controller.sourceType = .photoLibrary
return controller
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerView>) {
// run right after making
}
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parentView: ImagePickerView
init(_ parentView: ImagePickerView) {
self.parentView = parentView
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parentView.model.isPresented = false
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
guard let uiImage = info[.originalImage] as? UIImage else { return }
let image = Image(uiImage: uiImage)
parentView.model.pickedImagesSubject?.send([image])
parentView.model.isPresented = false
}
}
}
struct ImagePickerViewModel {
var isPresented: Bool = false
let pickedImagesSubject: PassthroughSubject<[Image], Never>! = PassthroughSubject<[Image], Never>()
}
Usage:
struct SomeView : View {
#EnvironmentObject var storage: Storage
#State var imagePickerViewModel = ImagePickerViewModel()
var body: some View {
Button(action: { self.imagePickerViewModel.isPresented.toggle() }) { ... }
.sheet(isPresented: $imagePickerViewModel.isPresented) {
ImagePickerView(model: self.$imagePickerViewModel)
}
.onReceive(imagePickerViewModel.pickedImagesSubject) { (images: [Image]) -> Void in
withAnimation {
// modify your storage here
self.storage.images += images
}
}
}
}
I implemented it like this:
import SwiftUI
final class ImagePickerCoordinator: NSObject {
#Binding var image: UIImage?
#Binding var takePhoto: Bool
init(image: Binding<UIImage?>, takePhoto: Binding<Bool>) {
_image = image
_takePhoto = takePhoto
}
}
struct ShowImagePicker: UIViewControllerRepresentable {
#Binding var image: UIImage?
#Binding var takePhoto: Bool
func makeCoordinator() -> ImagePickerCoordinator {
ImagePickerCoordinator(image: $image, takePhoto: $takePhoto)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let pickerController = UIImagePickerController()
pickerController.delegate = context.coordinator
guard UIImagePickerController.isSourceTypeAvailable(.camera) else { return pickerController }
switch self.takePhoto {
case true:
pickerController.sourceType = .camera
case false:
pickerController.sourceType = .photoLibrary
}
pickerController.allowsEditing = true
return pickerController
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}
extension ImagePickerCoordinator: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let uiImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
self.image = uiImage
picker.dismiss(animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
}
Add the logic of just two buttons to your View that's enough...))

Resources