Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate PHPickerViewController in Media library #21343

Merged
merged 9 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions WordPress/Classes/Services/MediaImportService.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import CocoaLumberjack
import PhotosUI

/// Encapsulates importing assets such as PHAssets, images, videos, or files at URLs to Media objects.
///
Expand Down Expand Up @@ -276,6 +277,11 @@ class MediaImportService: NSObject {
exporter.videoOptions = self.exporterVideoOptions
exporter.allowableFileExtensions = allowableFileExtensions.isEmpty ? MediaImportService.defaultAllowableFileExtensions : allowableFileExtensions
return exporter
case let provider as NSItemProvider:
let exporter = ItemProviderMediaExporter(provider: provider)
exporter.imageOptions = self.exporterImageOptions
exporter.videoOptions = self.exporterVideoOptions
return exporter
case let image as UIImage:
let exporter = MediaImageExporter(image: image, filename: nil)
exporter.options = self.exporterImageOptions
Expand Down
5 changes: 5 additions & 0 deletions WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ enum FeatureFlag: Int, CaseIterable {
case commentModerationUpdate
case compliancePopover
case domainFocus
case nativePhotoPicker

/// Returns a boolean indicating if the feature is enabled
var enabled: Bool {
Expand Down Expand Up @@ -134,6 +135,8 @@ enum FeatureFlag: Int, CaseIterable {
return true
case .domainFocus:
return false
case .nativePhotoPicker:
return false
}
}

Expand Down Expand Up @@ -236,6 +239,8 @@ extension FeatureFlag {
return "Compliance Popover"
case .domainFocus:
return "Domain Focus"
case .nativePhotoPicker:
return "Native Photo Picker"
}
}
}
Expand Down
117 changes: 117 additions & 0 deletions WordPress/Classes/Utility/Media/ItemProviderMediaExporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Foundation
import PhotosUI

/// Manages export of media assets: images and video.
final class ItemProviderMediaExporter: MediaExporter {
var mediaDirectoryType: MediaDirectory = .uploads
var imageOptions: MediaImageExporter.Options?
var videoOptions: MediaVideoExporter.Options?

private let provider: NSItemProvider

init(provider: NSItemProvider) {
self.provider = provider
}

func export(onCompletion: @escaping (MediaExport) -> Void, onError: @escaping (MediaExportError) -> Void) -> Progress {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works nearly identical to the existing MediaAssetExporter (PHAsset support) and shares some of the code with it.

Great post on NSItemProvider: https://www.humancode.us/2023/07/08/all-about-nsitemprovider.html.

let progress = Progress.discreteProgress(totalUnitCount: MediaExportProgressUnits.done)

// It's important to use the `MediaImageExporter` because it strips the
// GPS data and performs other image manipulations before the upload.
func processImage(at url: URL) throws {
let exporter = MediaImageExporter(url: url)
exporter.mediaDirectoryType = mediaDirectoryType
if let imageOptions {
exporter.options = imageOptions
}
// If image format is not supported, switch to `.heic`.
if exporter.options.exportImageType == nil,
let type = provider.registeredTypeIdentifiers.first,
!ItemProviderMediaExporter.supportedImageTypes.contains(type) {
exporter.options.exportImageType = UTType.heic.identifier
}
let exportProgress = exporter.export(onCompletion: onCompletion, onError: onError)
progress.addChild(exportProgress, withPendingUnitCount: MediaExportProgressUnits.halfDone)
}

// `MediaImageExporter` doesn't support GIF, so it requires special handling.
func processGIF(at url: URL) throws {
let pixelSize = url.pixelSize
let media = MediaExport(url: url, fileSize: url.fileSize, width: pixelSize.width, height: pixelSize.height, duration: nil)
onCompletion(media)
}

func processVideo(at url: URL) throws {
let exporter = MediaVideoExporter(url: url)
exporter.mediaDirectoryType = mediaDirectoryType
if let videoOptions {
exporter.options = videoOptions
}
let exportProgress = exporter.export(onCompletion: onCompletion, onError: onError)
progress.addChild(exportProgress, withPendingUnitCount: MediaExportProgressUnits.halfDone)
}

let loadProgress = provider.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
guard let url else {
onError(ExportError.underlyingError(error))
return
}
DDLogDebug("Loaded file representation (filename: '\(url.lastPathComponent)', types: \(self.provider.registeredTypeIdentifiers)")

// Retaining `self` on purpose.
do {
let copyURL = try self.mediaFileManager.makeLocalMediaURL(withFilename: url.lastPathComponent, fileExtension: url.pathExtension)
try FileManager.default.copyItem(at: url, to: copyURL)

if self.hasConformingType(.gif) {
try processGIF(at: copyURL)
} else if self.hasConformingType(.image) {
try processImage(at: copyURL)
} else if self.hasConformingType(.movie) || self.hasConformingType(.video) {
try processVideo(at: copyURL)
} else {
onError(ExportError.unsupportedContentType)
}
} catch {
onError(ExportError.underlyingError(error))
}
}
progress.addChild(loadProgress, withPendingUnitCount: MediaExportProgressUnits.halfDone)
return progress
}

/// The list of image formats supported by the backend.
/// See https://wordpress.com/support/accepted-filetypes/.
///
/// One notable format missing from the list is `.webp`, which is not supported
/// by `CGImageDestinationCreateWithURL` and, in turn, `MediaImageExporter`.
///
/// If the format is not supported, the app fallbacks to `.heic` which is
/// similar to `.webp`: more efficient than traditional formats and supports
/// opacity, unlike `.jpeg`.
private static let supportedImageTypes: Set<String> = Set([
UTType.png,
UTType.jpeg,
UTType.gif,
UTType.heic,
UTType.svg
].map(\.identifier))

private func hasConformingType(_ type: UTType) -> Bool {
provider.hasItemConformingToTypeIdentifier(type.identifier)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a nice UTType-based API but it’s available only in iOS 16 :(

}

enum ExportError: MediaExportError {
case unsupportedContentType
case underlyingError(Error?)

var description: String {
switch self {
case .unsupportedContentType:
return NSLocalizedString("mediaExporter.error.unsupportedContentType", value: "Unsupported content type", comment: "An error message the app shows if media import fails")
case .underlyingError(let error):
return error?.localizedDescription ?? NSLocalizedString("mediaExporter.error.unknown", value: "The item could not be added to the Media library", comment: "An error message the app shows if media import fails")
}
}
}
}
1 change: 1 addition & 0 deletions WordPress/Classes/Utility/Media/MediaAssetExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import MobileCoreServices
import UniformTypeIdentifiers
import AVFoundation
import Photos

/// Media export handling of PHAssets
///
Expand Down
2 changes: 2 additions & 0 deletions WordPress/Classes/Utility/Media/MediaImageExporter.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Foundation
import CoreGraphics
import UIKit
import MobileCoreServices
import UniformTypeIdentifiers

Expand Down
1 change: 1 addition & 0 deletions WordPress/Classes/Utility/Media/MediaVideoExporter.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import MobileCoreServices
import UniformTypeIdentifiers
import AVFoundation

/// Media export handling of Videos from PHAssets or AVAssets.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import MobileCoreServices
import WPMediaPicker
import PhotosUI

/// Prepares the alert controller that will be presented when tapping the "+" button in Media Library
final class MediaLibraryMediaPickingCoordinator {
typealias PickersDelegate = StockPhotosPickerDelegate & WPMediaPickerViewControllerDelegate & TenorPickerDelegate
typealias PickersDelegate = StockPhotosPickerDelegate & WPMediaPickerViewControllerDelegate & TenorPickerDelegate & PHPickerViewControllerDelegate
private weak var delegate: PickersDelegate?
private var tenor: TenorPicker?

Expand Down Expand Up @@ -117,7 +118,13 @@ final class MediaLibraryMediaPickingCoordinator {
}

private func showMediaPicker(origin: UIViewController, blog: Blog) {
mediaLibrary.presentPicker(origin: origin, blog: blog)
if FeatureFlag.nativePhotoPicker.enabled {
let picker = PHPickerViewController(configuration: .make())
picker.delegate = self
origin.present(picker, animated: true)
} else {
mediaLibrary.presentPicker(origin: origin, blog: blog)
}
}
}

Expand All @@ -134,3 +141,9 @@ extension MediaLibraryMediaPickingCoordinator: StockPhotosPickerDelegate {
stockPhotos = nil
}
}

extension MediaLibraryMediaPickingCoordinator: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
delegate?.picker(picker, didFinishPicking: results)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import WordPressShared
import WPMediaPicker
import MobileCoreServices
import UniformTypeIdentifiers
import PhotosUI

/// Displays the user's media library in a grid
///
Expand Down Expand Up @@ -473,6 +474,17 @@ class MediaLibraryViewController: WPMediaPickerViewController {
}
}

extension MediaLibraryViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true)

for result in results {
let info = MediaAnalyticsInfo(origin: .mediaLibrary(.deviceLibrary), selectionMethod: .fullScreenPicker)
MediaCoordinator.shared.addMedia(from: result.itemProvider, to: blog, analyticsInfo: info)
}
}
}

// MARK: - UIDocumentPickerDelegate

extension MediaLibraryViewController: UIDocumentPickerDelegate {
Expand Down
kean marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import UIKit

extension NSItemProvider: ExportableAsset {
public var assetMediaType: MediaType {
if hasItemConformingToTypeIdentifier(UTType.image.identifier) {
return .image
} else if hasItemConformingToTypeIdentifier(UTType.video.identifier) ||
hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
return .video
} else {
return .document
}
}
}
kean marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import UIKit
import PhotosUI

extension PHPickerConfiguration {
/// Returns the picker configuration optimized for the Jetpack app.
static func make() -> PHPickerConfiguration {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.preferredAssetRepresentationMode = .compatible
configuration.selection = .ordered
configuration.selectionLimit = 0 // Unlimited
return configuration
}
}
Loading