How to take multiple photos using SwiftUI and UIImagePickerController - swift

I have a working implementation of SwiftUI + Camera using UIImagePickerController, but I am having trouble reloading the camera again after I take a photo (so i can take multiple pictures)
What happens is that after I take a picture, I get the "Retake/Use Photo". I select "Use Photo", and the delegate methods get called and I can grab the photo, great. After that however, the View remains the same, still showing a still image with the "Retake/Use Photo" buttons
How can I trigger an update so I get the Camera again, with the desire being: Show Camera, Hit Button, select "Use Photo", then the camera shows again
// SampleQuestion.swift
import SwiftUI
struct SampleQuestion: View {
#State var photos: [UIImage] = []
var body: some View {
return VStack{
CameraViewIntegratedController(userPhotos: $photos)
.frame(width: 400, height: 400)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
// CameraViewIntegratedController
import SwiftUI
import UIKit
struct CameraViewIntegratedController: UIViewControllerRepresentable {
#Binding var userPhotos: [UIImage]
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewIntegratedController>) -> UIImagePickerController {
let vc = UIImagePickerController()
vc.sourceType = .camera
vc.allowsEditing = true
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
var parent: CameraViewIntegratedController
init(_ imagePickerController: CameraViewIntegratedController) {
self.parent = imagePickerController
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info:[UIImagePickerController.InfoKey : Any]) {
guard let image = info[.editedImage] as? UIImage else {
print("No image found")
return
}
print("Image Taken Successfully, with size \(image.size)")
parent.userPhotos.append(image)
}
}
}
Ideally with after didFinishPickingMediaWithInfo I can reinitialize the camera. Or perhaps its on the SwiftUI side working with #State ?

Related

Is there a way to make PhotosPicker in SwiftUI use the camera instead?

Is there a way to make the PhotosUI PhotosPicker in SwiftUI source the photos directly from the Camera? I'm trying to change the PhotosPickers in my app to use the camera instead of forcing the user to select images from their image library. Is there a way to dictate the source of PhotosPicker images like you can with ImagePicker?
I did an online search and a lot of articles talked about PhotoPicker, but none of the examples had a way to make it use the camera. Should I just bite the bullet and switch everything to image picker or is there a way to make PhotoPicker use the camera?
Thanks.
step 1 : Create a dialogue to select the option for a camera of the photo
step 2 : Create a action sheet for for image picker
struct BaseView: View {
#State var showSelection: Bool = false
#State var showPicker: Bool = false
#State var type: UIImagePickerController.SourceType = .photoLibrary
var body: some View {
Zstack {
YourView()
}
.confirmationDialog(Lables.selectImage,
isPresented: $showSelection,
titleVisibility: .hidden) {
Button(ButtonName.camera) {
showPicker = true
type = .camera
}
Button(ButtonName.photos) {
showPicker = true
type = .photoLibrary
}
}
.fullScreenCover(isPresented: $showPicker) {
ImagePickerView(sourceType: profileImageVM.pickerType) { image in
// image your image
}
}
}
}
step 3: Create Image Picker
struct ImagePickerView: UIViewControllerRepresentable {
private var 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()
}
}
}

UI of Camera app frozen after user presses the "Use Image" button

In my camera app, I click a photo and press the use photo button. When the user presses the "Use Photo" button after clicking the photo, the photo gets saved in the photo library(if the user allows the app to) but the CameraView completely freezes. Here are parts of the code that might help:
ContentView:
struct ContentView: View {
var body: some View {
CameraView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Part of the CameraView:
extension CameraView {
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: CameraView
init(_ parent: CameraView) {
self.parent = parent
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
fatalError("Cancel button pressed!")
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
CustomizeView()
}
}
}
}
This is the block of code where Use Photo is controlled
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) //Where the CameraView freezes after this.
}

having a memory leak when implementing a image picker in SwiftUI using UIViewControllerRepresentable

I am trying to get the user to pick an image from their gallery. After doing some research I was able to accomplish this task by using a UIViewControllerRepresentable. However, after I opened up the memory debugger it shows that I have a memory leak
I created a basic example to which would make the code a lot simpler to read and still address the same issue without copy-pasting my entire code. the code below also shows a memory leak
// code for the views
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: ImagePickerView(),
label: {
Text("Navigate")
})
}
}
}
struct ImagePickerView: View {
#State var image = UIImage()
#State var showController: Bool = false
#State var didChoose : Bool = false
var body: some View {
Button(action: {
showController = true
}, label: {
Text("pick image")
})
.sheet(isPresented: $showController, content: {
ImagePicker(image: $image, didChoose: $didChoose)
})
}
}
struct ImagePicker : UIViewControllerRepresentable {
#Binding var image: UIImage
#Binding var didChoose: Bool
#Environment(\.presentationMode) var presentation
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIViewController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: ImagePicker.UIViewControllerType, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent : ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let pickedImage = info[.originalImage] as? UIImage else {
print("could not unwrap the image")
return
}
print(pickedImage)
self.parent.image = pickedImage
self.parent.didChoose = true
self.parent.presentation.wrappedValue.dismiss()
}
}
}
I suspect the cause to be the in the coordinator class however, ifailed to identify the reason behind it. is it involved with the parent?

Update image in UIImageView

I try to make UIKit element with replaceable image in it, that I can use like swiftUI element.
I stuck at the moment, when image in UIImageView should be refreshed with imageInBlackBox from ObservableObject. I try'd to set new imageInBlackBox from updateUIView and from imagePickerController after image has been selected. But both of this methods don't update UIImageView, it works only for swiftUI Image element, that I use for test.
How I should make imageView.image be refreshed after imageInBlackBox change?
//Data model
struct imageAdderDataHolder: Identifiable, Hashable {
var id: UUID = UUID()
var isShowingImagePicker:Bool = false
var imageInBlackBox:UIImage = UIImage(systemName: "photo")! // image for replace
var height: CGFloat = 160
var width: CGFloat = 160
}
//Data storage
class imageAdderData: ObservableObject{
init() {}
static let shared = imageAdderData()
#Published var img1: imageAdderDataHolder = imageAdderDataHolder()
}
struct simpleadder: UIViewRepresentable{
#ObservedObject var imageData: imageAdderData = .shared
let mainView: UIView = UIView()
var imageView: UIImageView = UIImageView()
func makeUIView(context: Context) -> UIView {
imageView.image = imageData.img1.imageInBlackBox
imageView.frame.size.width = imageData.img1.width
imageView.frame.size.height = imageData.img1.height
imageView.contentMode = .scaleAspectFit
mainView.addSubview(imageView)
return mainView
}
func updateUIView(_ uiView: UIView, context: Context) {
imageView.image = imageData.img1.imageInBlackBox // try to replace image
}
}
// swiftui view for test
struct photoadder: View {
#ObservedObject var imageData: imageAdderData = .shared
var body: some View {
VStack{
HStack{
simpleadder()
.frame(width: imageData.img1.width, height: imageData.img1.height)
.border(Color.black, width:1)
.sheet(isPresented: $imageData.img1.isShowingImagePicker, content: {
imagePickerUIView(isPresented: $imageData.img1.isShowingImagePicker)
})
Image(uiImage: imageData.img1.imageInBlackBox) // working element for test
.resizable()
.aspectRatio(contentMode: ContentMode.fit)
.frame(width: imageData.img1.width, height: imageData.img1.height)
.border(Color.black, width: 1)
}
Button("change image") {
imageData.img1.isShowingImagePicker = true
}
}
}
}
struct imagePickerUIView: UIViewControllerRepresentable {
#ObservedObject var imageData: imageAdderData = .shared
#Binding var isPresented: Bool
func makeUIViewController(context:
UIViewControllerRepresentableContext<imagePickerUIView>) ->
UIViewController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
func makeCoordinator() -> imagePickerUIView.Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
let parent: imagePickerUIView
init(parent: imagePickerUIView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info:
[UIImagePickerController.InfoKey : Any]) {
if let selectedImageFromPicker = info[.originalImage] as? UIImage {
// try to replace image
parent.imageData.img1.imageInBlackBox = selectedImageFromPicker
}
self.parent.isPresented = false
}
}
func updateUIViewController(_ uiViewController:
imagePickerUIView.UIViewControllerType, context:
UIViewControllerRepresentableContext<imagePickerUIView>) {
}
}
Here is a solution - in representable all views should be created inside makeUIView, because struct can be (and usually does) recreated on parent update (so internal instances will be recreated as well, while made UIKit views life-cycle persisted).
Tested with Xcode 12.4 / iOS 14.4.
Fixed code:
struct simpleadder: UIViewRepresentable{
#ObservedObject var imageData: imageAdderData = .shared
func makeUIView(context: Context) -> UIView {
let mainView: UIView = UIView() // << create here !!
let imageView: UIImageView = UIImageView() // << create here !!
imageView.image = imageData.img1.imageInBlackBox
imageView.frame.size.width = imageData.img1.width
imageView.frame.size.height = imageData.img1.height
imageView.contentMode = .scaleAspectFit
mainView.addSubview(imageView)
return mainView
}
func updateUIView(_ uiView: UIView, context: Context) {
// find needed view in run-time
if let imageView = uiView.subviews.first as? UIImageView {
imageView.image = imageData.img1.imageInBlackBox
}
}
}

SwiftUI exporting or sharing files

I'm wondering if there is a good way export or share a file through SwiftUI. There doesn't seem to be a way to wrap a UIActivityViewController and present it directly. I've used the UIViewControllerRepresentable to wrap a UIActivityViewController, and it crashes if I, say, present it in a SwiftUI Modal.
I was able to create a generic UIViewController and then from there call a method that presents the UIActivityViewController, but that's a lot of wrapping.
And if we want to share from the Mac using SwiftUI, is there a way to wrap NSSharingServicePicker?
Anyway, if anyone has an example of how they're doing this, it would be much appreciated.
You can define this function anywhere (preferably in the global scope):
#discardableResult
func share(
items: [Any],
excludedActivityTypes: [UIActivity.ActivityType]? = nil
) -> Bool {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return false
}
let vc = UIActivityViewController(
activityItems: items,
applicationActivities: nil
)
vc.excludedActivityTypes = excludedActivityTypes
vc.popoverPresentationController?.sourceView = source.view
source.present(vc, animated: true)
return true
}
You can use this function in a button action, or anywhere else needed:
Button(action: {
share(items: ["This is some text"])
}) {
Text("Share")
}
We can call the UIActivityViewController directly from the View (SwiftUI) without using UIViewControllerRepresentable.
import SwiftUI
enum Coordinator {
static func topViewController(_ viewController: UIViewController? = nil) -> UIViewController? {
let vc = viewController ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
if let navigationController = vc as? UINavigationController {
return topViewController(navigationController.topViewController)
} else if let tabBarController = vc as? UITabBarController {
return tabBarController.presentedViewController != nil ? topViewController(tabBarController.presentedViewController) : topViewController(tabBarController.selectedViewController)
} else if let presentedViewController = vc?.presentedViewController {
return topViewController(presentedViewController)
}
return vc
}
}
struct ActivityView: View {
var body: some View {
Button(action: {
self.shareApp()
}) {
Text("Share")
}
}
}
extension ActivityView {
func shareApp() {
let textToShare = "something..."
let activityViewController = UIActivityViewController(activityItems: [textToShare], applicationActivities: nil)
let viewController = Coordinator.topViewController()
activityViewController.popoverPresentationController?.sourceView = viewController?.view
viewController?.present(activityViewController, animated: true, completion: nil)
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView()
}
}
And this is a preview:
Hoping to help someone!
EDIT: Removed all code and references to UIButton.
Thanks to #Matteo_Pacini for his answer to this question for showing us this technique. As with his answer (and comment), (1) this is rough around the edges and (2) I'm not sure this is how Apple wants us to use UIViewControllerRepresentable and I really hope they provide a better SwiftUI ("SwiftierUI"?) replacement in a future beta.
I put in a lot of work in UIKit because I want this to look good on an iPad, where a sourceView is needed for the popover. The real trick is to display a (SwiftUI) View that gets the UIActivityViewController in the view hierarchy and trigger present from UIKit.
My needs were to present a single image to share, so things are targeted in that direction. Let's say you have an image, stored as a #State variable - in my example the image is called vermont.jpg and yes, things are hard-coded for that.
First, create a UIKit class of type `UIViewController to present the share popover:
class ActivityViewController : UIViewController {
var uiImage:UIImage!
#objc func shareImage() {
let vc = UIActivityViewController(activityItems: [uiImage!], applicationActivities: [])
vc.excludedActivityTypes = [
UIActivity.ActivityType.postToWeibo,
UIActivity.ActivityType.assignToContact,
UIActivity.ActivityType.addToReadingList,
UIActivity.ActivityType.postToVimeo,
UIActivity.ActivityType.postToTencentWeibo
]
present(vc,
animated: true,
completion: nil)
vc.popoverPresentationController?.sourceView = self.view
}
}
The main things are;
You need a "wrapper" UIViewController to be able to present things.
You need var uiImage:UIImage! to set the activityItems.
Next up, wrap this into a UIViewControllerRepresentable:
struct SwiftUIActivityViewController : UIViewControllerRepresentable {
let activityViewController = ActivityViewController()
func makeUIViewController(context: Context) -> ActivityViewController {
activityViewController
}
func updateUIViewController(_ uiViewController: ActivityViewController, context: Context) {
//
}
func shareImage(uiImage: UIImage) {
activityViewController.uiImage = uiImage
activityViewController.shareImage()
}
}
The only two things of note are:
Instantiating ActivityViewController to return it up to ContentView
Creating shareImage(uiImage:UIImage) to call it.
Finally, you have ContentView:
struct ContentView : View {
let activityViewController = SwiftUIActivityViewController()
#State var uiImage = UIImage(named: "vermont.jpg")
var body: some View {
VStack {
Button(action: {
self.activityViewController.shareImage(uiImage: self.uiImage!)
}) {
ZStack {
Image(systemName:"square.and.arrow.up").renderingMode(.original).font(Font.title.weight(.regular))
activityViewController
}
}.frame(width: 60, height: 60).border(Color.black, width: 2, cornerRadius: 2)
Divider()
Image(uiImage: uiImage!)
}
}
}
Note that there's some hard-coding and (ugh) force-unwrapping of uiImage, along with an unnecessary use of #State. These are there because I plan to use `UIImagePickerController next to tie this all together.
The things of note here:
Instantiating SwiftUIActivityViewController, and using shareImage as the Button action.
Using it to also be button display. Don't forget, even a UIViewControllerRepresentable is really just considered a SwiftUI View!
Change the name of the image to one you have in your project, and this should work. You'll get a centered 60x60 button with the image below it.
Most of the solutions here forget to populate the share sheet on the iPad.
So, if you intend to have an application not crashing on this device, you can use
this method where popoverController is used and add your desired activityItems as a parameter.
import SwiftUI
/// Share button to populate on any SwiftUI view.
///
struct ShareButton: View {
/// Your items you want to share to the world.
///
let itemsToShare = ["https://itunes.apple.com/app/id1234"]
var body: some View {
Button(action: { showShareSheet(with: itemsToShare) }) {
Image(systemName: "square.and.arrow.up")
.font(.title2)
.foregroundColor(.blue)
}
}
}
extension View {
/// Show the classic Apple share sheet on iPhone and iPad.
///
func showShareSheet(with activityItems: [Any]) {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return
}
let activityVC = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil)
if let popoverController = activityVC.popoverPresentationController {
popoverController.sourceView = source.view
popoverController.sourceRect = CGRect(x: source.view.bounds.midX,
y: source.view.bounds.midY,
width: .zero, height: .zero)
popoverController.permittedArrowDirections = []
}
source.present(activityVC, animated: true)
}
}
Take a look at AlanQuatermain -s SwiftUIShareSheetDemo
In a nutshell it looks like this:
#State private var showShareSheet = false
#State public var sharedItems : [Any] = []
Button(action: {
self.sharedItems = [UIImage(systemName: "house")!]
self.showShareSheet = true
}) {
Text("Share")
}.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: self.sharedItems)
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
// nothing to do here
}
}