Select Multiple Images (UIImagePickerController or Photos.app Share UI) - ios

In iPhone OS 3.0, Apple added the ability to share multiple pictures at once using the "Share" button and selecting multiple images (where a checkmark is used).
I'd love to have a UIImagePickerController which lets the user select multiple images at once, rather than having to go one by one. Is there a way to do this, or do I have to wait until they add this feature?

If you are supporting only iOS 14 and up, you can use Apple's PHPickerViewController. It allows multiple image selection (while UIImagePickerController does not).
An additional benefit to using PHPickerViewController vs other libraries listed above is that the user will not need to grant permission to access your photo library.

Try this wonderful API in swift: ImagePicker. As all other image APIs, it is simple to use and it is very well updated.

1.install pod - pod "BSImagePicker", "~> 2.8"
inside info plist add row Privacy - Photo Library Usage Description
3.paste below code inside a .swift file-
import UIKit
import BSImagePicker
import Photos
class MultipleImgViC: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var SelectedAssets = [PHAsset]()
var photoArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func selectImages(_ sender: Any) {
let vc = BSImagePickerViewController()
self.bs_presentImagePickerController(vc, animated: true, select: { (assest: PHAsset) -> Void in
},
deselect: { (assest: PHAsset) -> Void in
}, cancel: { (assest: [PHAsset]) -> Void in
}, finish: { (assest: [PHAsset]) -> Void in
for i in 0..<assest.count
{
self.SelectedAssets.append(assest[i])
}
self.convertAssetToImages()
}, completion: nil)
}
#IBAction func dismissview(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
extension MultipleImgViC{
func convertAssetToImages() -> Void {
if SelectedAssets.count != 0{
for i in 0..<SelectedAssets.count{
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: SelectedAssets[i], targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: option, resultHandler: {(result,info) -> Void in
thumbnail = result!
})
let data = thumbnail.jpegData(compressionQuality: 0.7)
let newImage = UIImage(data: data!)
self.photoArray.append(newImage! as UIImage)
}
self.imageView.animationImages = self.photoArray
self.imageView.animationDuration = 3.0
self.imageView.startAnimating()
}
}
}
Note :- if pod file give "How to fix “SWIFT_VERSION '3.0' is unsupported, supported versions are: 4.0, 4.2, 5.0” error in Xcode 10.2?
" this error then solve it from this link:- https://stackoverflow.com/a/55901964/8537648
video reference: - https://youtu.be/B1DelPi1L0U
sample image:-

AssetLibrary + UICollectionView ^^
Basically, with StoryBoard, you import aUINavigationController, you change the root controller to anUICollectionViewController (will be your Album list), end add anotherUICollectionViewController (will be your photos list).
Then with Assetlibrary you retrieve user albums and user album content.
I will make a such component as soon as i have some time.

You can use this OpalImagePicker like this (Swift 4):
var imagePicker: OpalImagePickerController!
imagePicker = OpalImagePickerController()
imagePicker.imagePickerDelegate = self
imagePicker.selectionImage = UIImage(named: "aCheckImg")
imagePicker.maximumSelectionsAllowed = 3 // Number of selected images
present(imagePicker, animated: true, completion: nil)
And then implement its delegate:
func imagePickerDidCancel(_ picker: OpalImagePickerController) {
//Cancel action
}
func imagePicker(_ picker: OpalImagePickerController, didFinishPickingImages images: [UIImage]) {
}

No need for a 3rd party library. You can use PHPickerViewController.
https://developer.apple.com/documentation/photokit/phpickerviewcontroller
private func showImagePicker() {
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 5 // Selection limit. Set to 0 for unlimited.
configuration.filter = .images // he types of media that can be get.
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
}

Apple introduced PHPickerViewController in iOS14.
Advantages
No additional permission need to be implemented (its private by default)
Supports Multi-selection (limit can also be specified)
Zooming and previewing the selection
Documentation - https://developer.apple.com/documentation/photokit/phpickerviewcontroller
Video - https://developer.apple.com/videos/play/wwdc2020/10652/
Hope this helps!

How about this way:
Open "photos.app" first, select multiple photos , and copy them ;
In your own app, try to retrieve those copies photos;
I knew that there are some apps did like this, but do not know how can achieve step 2.

Related

PHPhotoLibrary showing all photos not only selected

Trying to write a picker for the avatar in my app and when PHAuthorizationStatus is .limited and the user already selected some photos in LimitedLibraryPicker, I'm trying to open the picker in this way
private func openPHPicker() {
DispatchQueue.main.async { [weak self] in
var phPickerConfig = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
phPickerConfig.selectionLimit = 1
let phPickerVC = PHPickerViewController(configuration: phPickerConfig)
phPickerVC.delegate = self
self?.present(phPickerVC, animated: true)
}
}
And the result is that PHPickerViewController opens and shows all of the photos not only accessible.
I want to open picker with only selected at LimitedLibraryPicker photos, only that for which I have access.

iOS PHPickerViewController does not provide Exif/Meta-Data

I am trying to read out some metadata from images which I fetched with the PHPickerViewController.
var config = PHPickerConfiguration()
self.phPicker = PHPickerViewController(configuration: config)
if let picker = self.phPicker {
self.phPicker?.delegate = self
self.present(picker, animated: true) {
self.phPicker = nil
}
}
But the PHPickerResult.assetIdentifier to access those data is always nil.
extension ChatViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true) {
if results.isEmpty {
return
}
assert(results.first!.assetIdentifier != nil) // fails
}
}
}
The trick is the correct configuration of the PHPickerViewController.
var config = PHPickerConfiguration() // WRONG!
let picker = PHPickerViewController(configuration: config)
According to documentation you have to use the PHPhotoLibrary.
A PHPhotoLibrary provides access to the metadata and image data for
the photos, videos and related content in the user's photo library,
including content from the Camera Roll, iCloud Shared, Photo Stream,
imported, and synced from iTunes.
So just use PHPhotoLibrary in the constructor and everything works fine.
var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) // CORRECT!
let picker = PHPickerViewController(configuration: config)

Clearing UserDefaults.standard.data information doesn't actually delete it?

I saved a UIImage to UserDefaults via .data with this code, where the key equals "petPhoto1":
#IBAction func addPhotoButton(_ sender: Any) {
let picker = UIImagePickerController()
picker.allowsEditing = false
picker.delegate = self
picker.mediaTypes = ["public.image"]
present(picker, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
image.storeInUserDefaults(for: "petPhoto\(activePet)")
UserDefaults.standard.set("yes", forKey: "doesImageExist\(activePet)")
}
dismiss(animated: true, completion: nil)
}
(unrelated stuff in between)
extension UIImage {
func storeInUserDefaults(with compressionQuality: CGFloat = 0.8, for key: String) {
guard let data = self.jpegData(compressionQuality: compressionQuality) else { return }
let encodedImage = try! PropertyListEncoder().encode(data)
UserDefaults.standard.set(encodedImage, forKey: key)
}
}
Now when I erase it like this:
UserDefaults.standard.set(nil, forKey: "petPhoto1")
I can still see that "Documents & Data" for my app under Settings is still full with the same size as the original image, indicating that it didn't actually delete it, even though it no longer displays when it gets loaded back from UserDefaults.
Can anyone figure out a way to fix this? Thanks!
By the way, in case it helps, here is other code related to this issue:
The code I use in the ImageViewController that I display the image after saving it:
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
activePet = UserDefaults.standard.string(forKey: "activePet")! // activePet = 1 (confirmed with debugging with other, unrelated code
imageView.image = try? UIImage.loadFromUserDefaults(with: "petPhoto\(activePet)")
}
extension UIImage {
static func loadFromUserDefaults(with key: String) throws -> UIImage? {
let activePet = UserDefaults.standard.string(forKey: "activePet")!
guard let data = UserDefaults.standard.data(forKey: "petPhoto\(activePet)") else {
return nil
}
do {
let decodedImageData = try PropertyListDecoder().decode(Data.self, from: data)
return UIImage(data: decodedImageData)
} catch let error {
throw error
}
}
}
When you do this:
UserDefaults.standard.set(nil, forKey: "petPhoto1")
The link between the key and the file saved will be removed synchronously. that means if you try to access the value for this key, it gives nil.
But this image needs to be cleared from storage too, that will be happening asynchronously [we don't have completion handler API support from apple to get this information].
Apple Documentation for reference:
At runtime, you use UserDefaults objects to read the defaults that
your app uses from a user’s defaults database. UserDefaults caches the
information to avoid having to open the user’s defaults database each
time you need a default value. When you set a default value, it’s
changed synchronously within your process, and asynchronously to
persistent storage and other processes.
What you can try:
First approch
Give some time for the file delete operation to get completed by OS. then try to access the image in the disk.
Second approch
Try observing to the changes in the directory using GCD. Refer: https://stackoverflow.com/a/26878163/5215474

How to retrieve PHAsset from PHPicker?

In WWDC20 apple introduced PHPicker - the modern replacement for UIImagePickerController.
I'm wondering if it's possible to retrieve PHAsset using the new photo picker?
Here is my code:
private func presentPicker(filter: PHPickerFilter) {
var configuration = PHPickerConfiguration()
configuration.filter = filter
configuration.selectionLimit = 0
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true)
}
I managed to find an answer from the developers of this framework on the apple forum:
Yes, PHPickerResult has the assetIdentifier property which can contain
a local identifier to fetch the PHAsset from the library. To have
PHPicker return asset identifiers, you need to initialize
PHPickerConfiguration with the library.
Please note that PHPicker does not extend the Limited Photos Library
access for the selected items if the user put your app in Limited
Photos Library mode. It would be a good opportunity to reconsider if
the app really needs direct Photos Library access or can work with
just the image and video data. But that really depend on the app.
The relevant section of the "Meet the new Photos picker" session
begins at 10m 20s.
Sample code for PhotoKit access looks like this:
import UIKit
import PhotosUI
class PhotoKitPickerViewController: UIViewController, PHPickerViewControllerDelegate {
#IBAction func presentPicker(_ sender: Any) {
let photoLibrary = PHPhotoLibrary.shared()
let configuration = PHPickerConfiguration(photoLibrary: photoLibrary)
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let identifiers = results.compactMap(\.assetIdentifier)
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)
// TODO: Do something with the fetch result if you have Photos Library access
}
}

iOS13 share sheet: how to set preview thumbnail when sharing UIImage

The new share sheet on iOS13 shows a preview/thumbnail of the item being shared on its top left corner.
When sharing an UIImage using an UIActivityViewController I would expect a preview/thumbnail of the image being shared to be displayed there (like e.g. when sharing an image attached to the built in Mail app), but instead the share sheet is showing my app's icon.
What code/settings are required to show a thumbnail of the image being exported in the share sheet?
I have set up the UIActivityViewController as follows:
let image = UIImage(named: "test")!
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)
activityVC.popoverPresentationController?.sourceView = self.view
self.present(activityVC, animated: true, completion: nil)
The simplest code I've implemented to share a UIImage with better user experience:
Import the LinkPresentation framework:
#import <LinkPresentation/LPLinkMetadata.h> // for Obj-C
import LinkPresentation // for Swift, below
Present the UIActivityViewController in the UIViewController, with [image, self]:
let image = UIImage(named: "YourImage")!
let share = UIActivityViewController(activityItems: [image, self], applicationActivities: nil)
present(share, animated: true, completion: nil)
Make the UIViewController conform to UIActivityItemSource:
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return nil
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let image = UIImage(named: "YourImage")!
let imageProvider = NSItemProvider(object: image)
let metadata = LPLinkMetadata()
metadata.imageProvider = imageProvider
return metadata
}
Because UIImage has already conformed to NSItemProviderWriting, just serve it for NSItemProvider.
Since it's sharing a UIImage, any URL shouldn't be expected. Otherwise user may get URL sharing, rather than image sharing experience.
To accelerate the share sheet preview, feed LPLinkMetadata object with existing resources. No need to fetch it online again. Check the WWDC19 Tech Talks video What's New in Sharing for more details.
Update:
As of iOS 13.2.2 the standard way seems to be working as expected (when passing image URL(s) to UIActivityViewController), see #tatsuki.dev 's answer (now set as accepted answer):
On iOS 13.0 that was still not the case:
Original Answer:
I finally was able to figure out a solution to this issue.
To display the preview/thumbnail of the image being shared in the share sheet on iOS 13 it is necessary to adopt the UIActivityItemSource protocol, including its new (iOS13) activityViewControllerLinkMetadata method.
Starting from the code posted in the question, these would be the required steps:
Import the LinkPresentation framework:
import LinkPresentation
create an optional URL property in your UIViewController subclass
var urlOfImageToShare: URL?
Implement the UIActivityItemSource delegate methods as follows:
extension YourViewController: UIActivityItemSource {
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return UIImage() // an empty UIImage is sufficient to ensure share sheet shows right actions
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return urlOfImageToShare
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.title = "Description of image to share" // Preview Title
metadata.originalURL = urlOfImageToShare // determines the Preview Subtitle
metadata.url = urlOfImageToShare
metadata.imageProvider = NSItemProvider.init(contentsOf: urlOfImageToShare)
metadata.iconProvider = NSItemProvider.init(contentsOf: urlOfImageToShare)
return metadata
}
}
In the part of the code presenting the share sheet, the declaration of activityVC needs to be slightly changed. The activityItems parameter should be [self] instead of [image] as in the code posted in the question above:
//let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)
let activityVC = UIActivityViewController(activityItems: [self] , applicationActivities: nil)
This is necessary to have the UIActivityItemSource delegate methods declared above being called when presenting the share sheet.
Also, before presenting activityVC we need to set the value of urlOfImageToShare (which is needed by the UIActivityItemSource delegate methods):
urlOfImageToShare = yourImageURL // <<< update this to work with your code
The above steps should suffice if your app is not sharing very small or transparent images. The result looks like this:
In my tests while researching about this topic however, I had issues when providing images to metadata.iconProvider which were small (threshold seems to be 40 points) or non-opaque (transparent).
It seems like iOS uses metadata.imageProvider to generate the preview image if metadata.iconProvider delivers an image smaller than 40 points.
Also, on an actual device (iPhone Xs Max running iOS 13.1.2), the image provided by metadata.iconProvider would be displayed in reduced size on the share sheet in case it was not opaque:
On Simulator (iOS 13.0) this was not the case.
To work around these limitations, I followed these additional steps to ensure the preview image is always opaque and at least 40 points in size:
In the implementation of activityViewControllerLinkMetadata above, change the assignment of metadata.iconProvider as follows:
//metadata.iconProvider = NSItemProvider.init(contentsOf: urlOfImageToShare)
metadata.iconProvider = NSItemProvider.init(contentsOf: urlInTemporaryDirForSharePreviewImage(urlOfImageToShare))
Method urlInTemporaryDirForSharePreviewImage returns an URL to an opaque and if necessary enlarged copy of the image being shared created in the temporary directory:
func urlInTemporaryDirForSharePreviewImage(_ url: URL?) -> URL? {
if let imageURL = url,
let data = try? Data(contentsOf: imageURL),
let image = UIImage(data: data) {
let applicationTemporaryDirectoryURL = FileManager.default.temporaryDirectory
let sharePreviewURL = applicationTemporaryDirectoryURL.appendingPathComponent("sharePreview.png")
let resizedOpaqueImage = image.adjustedForShareSheetPreviewIconProvider()
if let data = resizedOpaqueImage.pngData() {
do {
try data.write(to: sharePreviewURL)
return sharePreviewURL
} catch {
print ("Error: \(error.localizedDescription)")
}
}
}
return nil
}
The actual generation of the new image is done using the following extension:
extension UIImage {
func adjustedForShareSheetPreviewIconProvider() -> UIImage {
let replaceTransparencyWithColor = UIColor.black // change as required
let minimumSize: CGFloat = 40.0 // points
let format = UIGraphicsImageRendererFormat.init()
format.opaque = true
format.scale = self.scale
let imageWidth = self.size.width
let imageHeight = self.size.height
let imageSmallestDimension = max(imageWidth, imageHeight)
let deviceScale = UIScreen.main.scale
let resizeFactor = minimumSize * deviceScale / (imageSmallestDimension * self.scale)
let size = resizeFactor > 1.0
? CGSize(width: imageWidth * resizeFactor, height: imageHeight * resizeFactor)
: self.size
return UIGraphicsImageRenderer(size: size, format: format).image { context in
let size = context.format.bounds.size
replaceTransparencyWithColor.setFill()
context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
self.draw(in: CGRect(origin: .zero, size: size))
}
}
}
Just pass the image urls to UIActivityViewController not the UIImage objects.
For example:
let imageURLs: [URL] = self.prepareImageURLs()
let activityViewController = UIActivityViewController(activityItems: imageURLs, applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)
You can see that the image name and the image properties are shown in the top of the UIActivityViewController. Hope it helps!
This code is only available for iOS 13 as a minimum target. I added a code example to use a share button in a SwiftUI view in case other people need it. This code also work for iPad.
You can use this class LinkMetadataManager and add the image of your choice. The very important part, is that you must have your image in your project directory, not in a Assets.xcassets folder. Otherwise, it won't work.
When everything will be setup, you will use the button this way in your SwiftUI view.
struct ContentView: View {
var body: some View {
VStack {
ShareButton()
}
}
}
This is the class that will be sharing your application with the Apple Store link. You can share whatever you want from that. You can see how the image is added using LPLinkMetadata as it is the part that interests you.
import LinkPresentation
// MARK: LinkMetadataManager
/// Transform url to metadata to populate to user.
final class LinkMetadataManager: NSObject, UIActivityItemSource {
var linkMetadata: LPLinkMetadata
let appTitle = "Your application name"
let appleStoreProductURL = "https://apps.apple.com/us/app/num8r/id1497392799" // The url of your app in Apple Store
let iconImage = "appIcon" // The name of the image file in your directory
let png = "png" // The extension of the image
init(linkMetadata: LPLinkMetadata = LPLinkMetadata()) {
self.linkMetadata = linkMetadata
}
}
// MARK: - Setup
extension LinkMetadataManager {
/// Creating metadata to population in the share sheet.
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
guard let url = URL(string: appleStoreProductUR) else { return linkMetadata }
linkMetadata.originalURL = url
linkMetadata.url = linkMetadata.originalURL
linkMetadata.title = appTitle
linkMetadata.iconProvider = NSItemProvider(
contentsOf: Bundle.main.url(forResource: iconImage, withExtension: png))
return linkMetadata
}
/// Showing empty string returns a share sheet with the minimum requirement.
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return String()
}
/// Sharing url of the application.
func activityViewController(_ activityViewController: UIActivityViewController,
itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return linkMetadata.url
}
}
Use this extension of View to trigger the share sheet on a SwiftUI view.
import SwiftUI
// MARK: View+ShareSheet
extension View {
/// Populate Apple share sheet to enable user to share Apple Store link.
func showAppShareSheet() {
guard let source = UIApplication.shared.windows.first?.rootViewController else {
return
}
let activityItemMetadata = LinkMetadataManager()
let activityVC = UIActivityViewController(
activityItems: [activityItemMetadata],
applicationActivities: nil)
if let popoverController = activityVC.popoverPresentationController {
popoverController.sourceView = source.view
popoverController.permittedArrowDirections = []
popoverController.sourceRect = CGRect(
x: source.view.bounds.midX,
y: source.view.bounds.midY,
width: .zero,
height: .zero)
}
source.present(activityVC, animated: true)
}
}
Then, create a ShareButton as a component to use it in any of your SwiftUI view. This is what is used in the ContentView.
import SwiftUI
// MARK: ShareButton
/// Share button to send app store link using the Apple
/// classic share screen for iPhone and iPad.
struct ShareButton: View {
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
ZStack {
Button(action: { showAppShareSheet() }) {
Image(systemName: "square.and.arrow.up")
.font(horizontalSizeClass == .compact ? .title2 : .title)
.foregroundColor(.accentColor)
}
.padding()
}
}
}

Resources