PhotosPicker API (only)
This commit is contained in:
parent
446ee623b7
commit
19a7cf0b27
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue