PhotosPicker API (only)

This commit is contained in:
Shaps Benkau 2023-01-30 11:30:58 +00:00
parent 446ee623b7
commit 19a7cf0b27
8 changed files with 300 additions and 32 deletions

View File

@ -1,31 +1,44 @@
#if os(iOS)
import SwiftUI
import PhotosUI
@available(iOS, introduced: 13, deprecated: 16)
@available(iOS, introduced: 14, deprecated: 16)
public extension Backport where Wrapped == Any {
// Available when SwiftUI is imported with PhotosUI
/// A control that allows a user to choose photos and/or videos from the photo library.
///
/// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
struct PhotosPicker<Label>: View where Label: View {
@State private var showPicker: Bool = false
@State private var isPresented: Bool = false
@Binding var selection: [Backport<Any>.PhotosPickerItem]
private let filter: Backport<Any>.PHPickerFilter?
private let maxSelection: Int?
private let selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior
private let encoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy
private let library: PHPh otoLibrary
private let label: Label
public var body: some View {
Button {
showPicker = true
isPresented = true
} label: {
label
}
.sheet(isPresented: $showPicker) {
Text("Photo picker")
}
._photoPicker(
isPresented: $isPresented,
selection: $selection,
filter: filter,
maxSelectionCount: maxSelection,
selectionBehavior: selectionBehavior,
preferredItemEncoding: encoding,
library: library
)
}
}
}
@available(iOS 14, *)
public extension Backport.PhotosPicker where Wrapped == Any {
/// Creates a Photos picker that selects a `PhotosPickerItem` from a given photo library.
///
@ -45,10 +58,24 @@ public extension Backport.PhotosPicker where Wrapped == Any {
photoLibrary: PHPhotoLibrary = .shared(),
@ViewBuilder label: () -> Label
) {
_selection = .init(
get: {
[selection.wrappedValue].compactMap { $0 }
},
set: { newValue in
selection.wrappedValue = newValue.first
}
)
self.filter = filter
self.maxSelection = maxSelectionCount
self.selectionBehavior = .default
self.encoding = preferredItemEncoding
self.library = photoLibrary
self.label = label()
}
}
@available(iOS 14, *)
public extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
/// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`.
///
@ -67,7 +94,20 @@ public extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
photoLibrary: PHPhotoLibrary = .shared()
) {
self.label = Text(title)
_selection = .init(
get: {
[selection.wrappedValue].compactMap { $0 }
},
set: { newValue in
selection.wrappedValue = newValue.first
}
)
self.filter = filter
self.maxSelection = maxSelectionCount
self.selectionBehavior = .default
self.encoding = preferredItemEncoding
self.library = photoLibrary
label = Text(title)
}
}
@ -85,7 +125,7 @@ public extension Backport.PhotosPicker where Wrapped == Any {
/// - preferredItemEncoding: The encoding disambiguation policy of selected items. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used.
/// - photoLibrary: The photo library to choose from.
/// - label: The view that describes the action of choosing items from the photo library.
public init(
init(
selection: Binding<[Backport<Any>.PhotosPickerItem]>,
maxSelectionCount: Int? = nil,
selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior = .default,
@ -94,11 +134,18 @@ public extension Backport.PhotosPicker where Wrapped == Any {
photoLibrary: PHPhotoLibrary = .shared(),
@ViewBuilder label: () -> Label
) {
_selection = selection
self.filter = filter
self.maxSelection = maxSelectionCount
self.selectionBehavior = selectionBehavior
self.encoding = preferredItemEncoding
self.library = photoLibrary
self.label = label()
}
}
extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
@available(iOS 14, *)
public extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
/// Creates a Photos picker with its label generated from a string that selects a `PhotosPickerItem` from a given photo library.
///
/// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
@ -109,7 +156,7 @@ extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
/// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown.
/// - preferredItemEncoding: The encoding disambiguation policy of the selected item. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used.
/// - photoLibrary: The photo library to choose from.
public init<S>(
init<S>(
_ title: S,
selection: Binding<Backport<Any>.PhotosPickerItem?>,
maxSelectionCount: Int? = nil,
@ -117,6 +164,19 @@ extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
photoLibrary: PHPhotoLibrary = .shared()
) where S: StringProtocol {
_selection = .init(
get: {
[selection.wrappedValue].compactMap { $0 }
},
set: { newValue in
selection.wrappedValue = newValue.first
}
)
self.filter = filter
self.maxSelection = maxSelectionCount
self.selectionBehavior = .default
self.encoding = preferredItemEncoding
self.library = photoLibrary
self.label = Text(title)
}
}
@ -144,8 +204,13 @@ public extension Backport.PhotosPicker where Wrapped == Any, Label == Text {
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
photoLibrary: PHPhotoLibrary = .shared()
) where S: StringProtocol {
_selection = selection
self.filter = filter
self.maxSelection = maxSelectionCount
self.selectionBehavior = selectionBehavior
self.encoding = preferredItemEncoding
self.library = photoLibrary
self.label = Text(title)
}
}
#endif

View File

@ -1,5 +1,4 @@
#if os(iOS)
import SwiftUI
import PhotosUI
@ -37,13 +36,21 @@ public extension Backport where Wrapped == Any {
/// All supported content types of the item, in order of most preferred to least preferred.
public let supportedContentTypes: [String]
// internal let provider: NSItemProvider?
//
// internal init(itemIdentifier: String, provider: NSItemProvider) {
// self.itemIdentifier = itemIdentifier
// self.provider = provider
// supportedContentTypes = provider.registeredTypeIdentifiers
// }
/// Creates an item without any representation using an identifier.
///
/// - Parameters:
/// - itemIdentifier: The local identifier of the item.
public init(itemIdentifier: String) {
self.itemIdentifier = itemIdentifier
fatalError()
supportedContentTypes = []
}
/// Loads an object using a representation of the item by matching content types.
@ -56,10 +63,26 @@ public extension Backport where Wrapped == Any {
/// - type: The actual type of the object.
/// - Throws: The encountered error while loading the object.
/// - Returns: The loaded object, or `nil` if no supported content type is found.
// public func loadTransferable<T>(type: T.Type) async throws -> T? where T {
// fatalError()
// }
///
/// - Note: Supported types are `Data`, `UIImage` or `Image` exclusively. Attempting to pass any other value here will result in an error.
public func loadTransferable<T>(type: T.Type) async throws -> T? {
switch type {
case is Image.Type:
fatalError()
case is UIImage.Type:
fatalError()
case is Data.Type:
fatalError()
default:
throw PhotoError<T>()
}
}
private struct PhotoError<T>: LocalizedError {
var errorDescription: String? {
"Could not load photo as \(T.self)"
}
}
}
}
#endif

View File

@ -1,21 +1,33 @@
#if os(iOS)
import SwiftUI
import PhotosUI
@available(iOS, introduced: 15, deprecated: 16)
@available(iOS, deprecated: 16)
public extension Backport where Wrapped == Any {
// Available when SwiftUI is imported with PhotosUI
/// A value that determines how the Photos picker handles user selection.
struct PhotosPickerSelectionBehavior : Equatable, Hashable {
internal let rawValue: PHPickerConfiguration.Selection
enum PhotosPickerSelectionBehavior: Equatable, Hashable {
/// Uses the default selection behavior.
public static let `default`: Self = .init(rawValue: .default)
case `default`
/// Uses the selection order made by the user. Selected items are numbered.
public static let ordered: Self = .init(rawValue: .ordered)
case ordered
@available(iOS 15, *)
init(behaviour: PHPickerConfiguration.Selection) {
switch behaviour {
case .`default`: self = .`default`
case .ordered: self = .ordered
default: self = .`default`
}
}
@available(iOS 15, *)
var behaviour: PHPickerConfiguration.Selection {
switch self {
case .ordered: return .ordered
default: return .`default`
}
}
}
}
#endif

View File

@ -2,7 +2,7 @@
import SwiftUI
import PhotosUI
@available(iOS, deprecated: 16)
@available(iOS, introduced: 13, deprecated: 16)
public extension Backport where Wrapped == Any {
/// A filter that restricts which types of assets to show
struct PHPickerFilter: Equatable, Hashable {

View File

@ -0,0 +1,48 @@
#if os(iOS)
import SwiftUI
import PhotosUI
/// An observer used to observe changes on a `PHFetchResult`
internal final class PickerObserver<Result>: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
@Published
internal var result: PHFetchResult<PHAsset>
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
init(maxSelectionCount: Int?, filter: Backport<Any>.PHPickerFilter) {
let options = PHFetchOptions()
options.includeAssetSourceTypes = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced]
result = PHAsset.fetchAssets(with: options)
super.init()
PHPhotoLibrary.shared().register(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
result = changeInstance.changeDetails(for: result)?.fetchResultAfterChanges ?? result
}
}
/// Represents a `PHFetchResult` that can be used as a `RandomAccessCollection` in a SwiftUI view such as `List`, `ForEach`, etc...
internal struct MediaResults: RandomAccessCollection {
/// Represents the underlying results
private let result: PHFetchResult<PHAsset>
private let playbackStyle: PHAsset.PlaybackStyle
public var element: [Backport<Any>.PhotosPickerItem] {
filter { $0.playbackStyle == playbackStyle }
.map { Backport<Any>.PhotosPickerItem(itemIdentifier: $0.localIdentifier) }
}
/// Instantiates a new instance with the specified result
public init(_ result: PHFetchResult<PHAsset>, playbackStyle: PHAsset.PlaybackStyle) {
self.result = result
self.playbackStyle = playbackStyle
}
public var startIndex: Int { 0 }
public var endIndex: Int { result.count }
public subscript(position: Int) -> PHAsset { result.object(at: position) }
}
#endif

View File

@ -1,9 +1,8 @@
#if os(iOS)
import SwiftUI
import PhotosUI
@available(iOS, deprecated: 16)
@available(iOS, introduced: 13, deprecated: 16)
public extension Backport where Wrapped == Any {
/// A user selected asset from `PHPickerViewController`.
struct PHPickerResult: Equatable, Hashable {
@ -19,5 +18,4 @@ public extension Backport where Wrapped == Any {
}
}
}
#endif

View File

@ -0,0 +1,122 @@
#if os(iOS)
import SwiftUI
import PhotosUI
internal extension View {
@ViewBuilder
func _photoPicker(
isPresented: Binding<Bool>,
selection: Binding<[Backport<Any>.PhotosPickerItem]>,
filter: Backport<Any>.PHPickerFilter?,
maxSelectionCount: Int?,
selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior,
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy,
library: PHPhotoLibrary
) -> some View {
if #available(iOS 14, *) {
// var config = PHPickerConfiguration(photoLibrary: library)
// config.preferredAssetRepresentationMode = preferredItemEncoding.mode
// config.selectionLimit = maxSelectionCount ?? 0
// // config.filter = filter.filter
//
// if #available(iOS 15, *) {
// config.selection = selectionBehavior.behaviour
// }
//
// PickerViewController(
// isPresented: isPresented,
// selection: selection,
// configuration: config
// )
} else {
EmptyView()
}
}
}
@available(iOS 14, *)
private struct PickerViewController: UIViewControllerRepresentable {
@Binding var isPresented: Bool
@Binding var selection: [Backport<Any>.PhotosPickerItem]
let configuration: PHPickerConfiguration
func makeUIViewController(context: Context) -> Representable {
Representable(
isPresented: $isPresented,
selection: $selection,
configuration: configuration
)
}
func updateUIViewController(_ controller: Representable, context: Context) {
controller.selection = $selection
controller.configuration = configuration
}
}
@available(iOS 14, *)
private extension PickerViewController {
final class Representable: UIViewController, PHPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
private weak var controller: PHPickerViewController?
var isPresented: Binding<Bool> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: isPresented.wrappedValue
)
}
}
var selection: Binding<[Backport<Any>.PhotosPickerItem]>
var configuration: PHPickerConfiguration
init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, configuration: PHPickerConfiguration) {
self.isPresented = isPresented
self.selection = selection
self.configuration = configuration
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateControllerLifecycle(from oldValue: Bool, to newValue: Bool) {
switch (oldValue, newValue) {
case (false, true):
presentController()
case (true, false):
dismissController()
case (true, true), (false, false):
break
}
}
private func presentController() {
let controller = PHPickerViewController(configuration: configuration)
controller.presentationController?.delegate = self
controller.delegate = self
present(controller, animated: true)
self.controller = controller
}
private func dismissController() {
guard let controller else { return }
controller.presentedViewController?.dismiss(animated: true)
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
dismissController()
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
print(results)
#warning("TBD")
}
}
}
#endif

View File

@ -150,7 +150,7 @@ private extension ShareSheet {
}
private func dismissController() {
guard let controller = controller else { return }
guard let controller else { return }
controller.presentingViewController?.dismiss(animated: true)
}