Introduces rudimentary Toolbar support for iOS 13
This commit is contained in:
parent
0ad99191d9
commit
cce0b59d3d
|
@ -84,6 +84,14 @@ extension View {
|
|||
func inspect<ViewType: PlatformView>(selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void) -> some View {
|
||||
inject(InspectionView(selector: selector, customize: customize))
|
||||
}
|
||||
|
||||
func controller(_ customize: @escaping (UIViewController?) -> Void) -> some View {
|
||||
inspect { inspector in
|
||||
inspector.sourceController.view
|
||||
} customize: { view in
|
||||
customize(view.parentController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct InspectionView<ViewType: PlatformView>: View {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import Photos
|
||||
|
||||
/// Represents a `PHFetchResult` that can be used as a `RandomAccessCollection` in a SwiftUI view such as `List`, `ForEach`, etc...
|
||||
internal struct MediaResults<Result>: RandomAccessCollection where Result: PHObject {
|
||||
|
||||
/// Represents the underlying results
|
||||
public private(set) var result: PHFetchResult<Result>
|
||||
|
||||
/// Instantiates a new instance with the specified result
|
||||
public init(_ result: PHFetchResult<Result>) {
|
||||
self.result = result
|
||||
}
|
||||
|
||||
public var startIndex: Int { 0 }
|
||||
public var endIndex: Int { result.count }
|
||||
public subscript(position: Int) -> Result { result.object(at: position) }
|
||||
|
||||
}
|
||||
|
||||
/// An observer used to observe changes on a `PHFetchResult`
|
||||
internal final class ResultsObserver<Result>: NSObject, ObservableObject, PHPhotoLibraryChangeObserver where Result: PHObject {
|
||||
|
||||
@Published
|
||||
internal var result: PHFetchResult<Result>
|
||||
|
||||
deinit {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
}
|
||||
|
||||
init(result: PHFetchResult<Result>) {
|
||||
self.result = result
|
||||
super.init()
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
}
|
||||
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
result = changeInstance.changeDetails(for: result)?.fetchResultAfterChanges ?? result
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import Foundation
|
||||
import Photos
|
||||
|
||||
internal extension PHFetchOptions {
|
||||
|
||||
/// The maximum number of objects to include in the fetch result.
|
||||
func fetchLimit(_ fetchLimit: Int) -> Self {
|
||||
self.fetchLimit = fetchLimit
|
||||
return self
|
||||
}
|
||||
|
||||
/// The set of source types for which to include assets in the fetch result.
|
||||
func includeAssetSourceTypes(_ sourceTypes: PHAssetSourceType) -> Self {
|
||||
self.includeAssetSourceTypes = sourceTypes
|
||||
return self
|
||||
}
|
||||
|
||||
/// Determines whether the fetch result includes all assets from burst photo sequences.
|
||||
func includeHiddenAssets(_ includeHiddenAssets: Bool) -> Self {
|
||||
self.includeHiddenAssets = includeHiddenAssets
|
||||
return self
|
||||
}
|
||||
|
||||
/// Determines whether the fetch result includes all assets from burst photo sequences.
|
||||
func includeAllBurstAssets(_ includeAllBurstAssets: Bool) -> Self {
|
||||
self.includeAllBurstAssets = includeAllBurstAssets
|
||||
return self
|
||||
}
|
||||
|
||||
/// Appends the specified sort to the current list of descriptors.
|
||||
/// - Parameters:
|
||||
/// - keyPath: The keyPath sort by
|
||||
/// - ascending: If true, the results will be in ascending order
|
||||
func sort<Root, Value>(by keyPath: KeyPath<Root, Value>, ascending: Bool = true) -> Self {
|
||||
var descriptors = sortDescriptors ?? []
|
||||
descriptors.append(NSSortDescriptor(keyPath: keyPath, ascending: ascending))
|
||||
self.sortDescriptors = descriptors
|
||||
return self
|
||||
}
|
||||
|
||||
/// Appends the specified predicate to the current list of predicates.
|
||||
/// - Parameter predicate: The predicate to append
|
||||
func filter(_ predicate: NSPredicate) -> Self {
|
||||
let predicates = [predicate, self.predicate].compactMap { $0 }
|
||||
self.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||
return self
|
||||
}
|
||||
|
||||
/// Replaces the sort descriptors.
|
||||
/// - Parameter sortDescriptors: The descriptors to sort results
|
||||
func sortDescriptors(_ sortDescriptors: [NSSortDescriptor]) -> Self {
|
||||
self.sortDescriptors = sortDescriptors
|
||||
return self
|
||||
}
|
||||
|
||||
/// Replaces the predicate.
|
||||
/// - Parameter predicate: The predicate to filter results
|
||||
func predicate(_ predicate: NSPredicate) -> Self {
|
||||
self.predicate = predicate
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import SwiftUI
|
||||
import Photos
|
||||
|
||||
extension PHObject: Identifiable {
|
||||
public var id: String { localIdentifier }
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import Photos
|
||||
import SwiftUI
|
||||
|
||||
/// Fetches a single asset
|
||||
@propertyWrapper
|
||||
struct FetchAsset: DynamicProperty {
|
||||
|
||||
@ObservedObject
|
||||
internal private(set) var observer: AssetObserver
|
||||
|
||||
/// Represents the fetched asset
|
||||
public var wrappedValue: MediaAsset {
|
||||
MediaAsset(asset: observer.asset)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchAsset {
|
||||
|
||||
/// Instantiates the fetch with an existing `PHAsset`
|
||||
/// - Parameter asset: The asset
|
||||
init(_ asset: PHAsset) {
|
||||
let observer = AssetObserver(asset: asset)
|
||||
self.init(observer: observer)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Represents the result of a `FetchAsset` request.
|
||||
struct MediaAsset {
|
||||
|
||||
public private(set) var asset: PHAsset?
|
||||
|
||||
public init(asset: PHAsset?) {
|
||||
self.asset = asset
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal final class AssetObserver: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
|
||||
|
||||
@Published
|
||||
internal var asset: PHAsset?
|
||||
|
||||
deinit {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
}
|
||||
|
||||
init(asset: PHAsset) {
|
||||
self.asset = asset
|
||||
super.init()
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
}
|
||||
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
guard let asset = asset else { return }
|
||||
self.asset = changeInstance.changeDetails(for: asset)?.objectAfterChanges
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import Photos
|
||||
import SwiftUI
|
||||
|
||||
/// Fetches a set of asset collections from the `Photos` framework
|
||||
@propertyWrapper
|
||||
internal struct FetchAssetCollection<Result>: DynamicProperty where Result: PHAssetCollection {
|
||||
|
||||
@ObservedObject
|
||||
internal private(set) var observer: ResultsObserver<Result>
|
||||
|
||||
/// Represents the results of the fetch
|
||||
public var wrappedValue: MediaResults<Result> {
|
||||
get { MediaResults(observer.result) }
|
||||
set { observer.result = newValue.result }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchAssetCollection {
|
||||
|
||||
/// Instantiates a fetch with an existing `PHFetchResult<Result>` instance
|
||||
init(result: PHFetchResult<Result>) {
|
||||
self.init(observer: ResultsObserver(result: result))
|
||||
}
|
||||
|
||||
/// Instantiates a fetch with a custom `PHFetchOptions` instance
|
||||
init(_ options: PHFetchOptions? = nil) {
|
||||
let result = PHAssetCollection.fetchTopLevelUserCollections(with: options)
|
||||
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
|
||||
}
|
||||
|
||||
/// Instantiates a fetch with a filter and sort options
|
||||
/// - Parameters:
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sort: The keyPaths to apply when sorting the results
|
||||
init<Value>(filter: NSPredicate? = nil,
|
||||
sort: [(KeyPath<PHAssetCollection, Value>, ascending: Bool)]) {
|
||||
let options = PHFetchOptions()
|
||||
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
|
||||
options.predicate = filter
|
||||
self.init(options)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchAssetCollection {
|
||||
|
||||
/// Instantiates a fetch for the specified album type and subtype
|
||||
/// - Parameters:
|
||||
/// - album: The album type to fetch
|
||||
/// - kind: The album subtype to fetch
|
||||
/// - options: Any additional options to apply to the fetch
|
||||
init(album: PHAssetCollectionType,
|
||||
kind: PHAssetCollectionSubtype = .any,
|
||||
options: PHFetchOptions? = nil) {
|
||||
let result = PHAssetCollection.fetchAssetCollections(with: album, subtype: kind, options: options)
|
||||
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
|
||||
}
|
||||
|
||||
/// Instantiates a fetch for the specified album type and subtype
|
||||
/// - Parameters:
|
||||
/// - album: The album type to fetch
|
||||
/// - kind: The album subtype to fetch
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
init(album: PHAssetCollectionType,
|
||||
kind: PHAssetCollectionSubtype = .any,
|
||||
fetchLimit: Int = 0,
|
||||
filter: NSPredicate? = nil) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.predicate = filter
|
||||
self.init(album: album, kind: kind, options: options)
|
||||
}
|
||||
|
||||
/// Instantiates a fetch for the specified album type and subtype
|
||||
/// - Parameters:
|
||||
/// - album: The album type to fetch
|
||||
/// - kind: The album subtype to fetch
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sort: The keyPaths to apply when sorting the results
|
||||
init<Value>(album: PHAssetCollectionType,
|
||||
kind: PHAssetCollectionSubtype = .any,
|
||||
fetchLimit: Int = 0,
|
||||
filter: NSPredicate? = nil,
|
||||
sort: [(KeyPath<PHAssetCollection, Value>, ascending: Bool)]) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
|
||||
options.predicate = filter
|
||||
self.init(album: album, kind: kind, options: options)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import Photos
|
||||
import SwiftUI
|
||||
|
||||
/// Fetches a set of assets from the `Photos` framework
|
||||
@propertyWrapper
|
||||
internal struct FetchAssetList<Result>: DynamicProperty where Result: PHAsset {
|
||||
|
||||
@ObservedObject
|
||||
internal private(set) var observer: ResultsObserver<Result>
|
||||
|
||||
/// Represents the results of the fetch
|
||||
public var wrappedValue: MediaResults<Result> {
|
||||
get { MediaResults(observer.result) }
|
||||
set { observer.result = newValue.result }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchAssetList {
|
||||
|
||||
/// Instantiates a fetch with an existing `PHFetchResult<Result>` instance
|
||||
init(_ result: PHFetchResult<PHAsset>) {
|
||||
observer = ResultsObserver(result: result as! PHFetchResult<Result>)
|
||||
}
|
||||
|
||||
/// Instantiates a fetch with a custom `PHFetchOptions` instance
|
||||
init(_ options: PHFetchOptions? = nil) {
|
||||
let result = PHAsset.fetchAssets(with: options)
|
||||
observer = ResultsObserver(result: result as! PHFetchResult<Result>)
|
||||
}
|
||||
|
||||
/// Instantiates a fetch by applying the specified sort and filter options
|
||||
/// - Parameters:
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sort: The keyPaths to apply when sorting the results
|
||||
/// - sourceTypes: The sourceTypes to include in the results
|
||||
/// - includeAllBurstAssets: If true, burst assets will be included in the results
|
||||
/// - includeHiddenAssets: If true, hidden assets will be included in the results
|
||||
init<Value>(filter: NSPredicate? = nil,
|
||||
sort: [(KeyPath<PHAsset, Value>, ascending: Bool)],
|
||||
sourceTypes: PHAssetSourceType = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced],
|
||||
includeAllBurstAssets: Bool = false,
|
||||
includeHiddenAssets: Bool = false) {
|
||||
let options = PHFetchOptions()
|
||||
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
|
||||
options.predicate = filter
|
||||
options.includeAssetSourceTypes = sourceTypes
|
||||
options.includeHiddenAssets = includeHiddenAssets
|
||||
options.includeAllBurstAssets = includeAllBurstAssets
|
||||
self.init(options)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchAssetList {
|
||||
|
||||
/// Fetches all assets in the specified collection
|
||||
/// - Parameters:
|
||||
/// - collection: The asset collection to filter by
|
||||
/// - options: Any additional options to apply to the request
|
||||
init(in collection: PHAssetCollection,
|
||||
options: PHFetchOptions? = nil) {
|
||||
let result = PHAsset.fetchAssets(in: collection, options: options)
|
||||
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
|
||||
}
|
||||
|
||||
/// Fetches all assets in the specified collection
|
||||
/// - Parameters:
|
||||
/// - collection: The asset collection to filter by
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sourceTypes: The sourceTypes to include in the results
|
||||
/// - includeAllBurstAssets: If true, burst assets will be included in the results
|
||||
/// - includeHiddenAssets: If true, hidden assets will be included in the results
|
||||
init(in collection: PHAssetCollection,
|
||||
fetchLimit: Int = 0,
|
||||
filter: NSPredicate? = nil,
|
||||
sourceTypes: PHAssetSourceType = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced],
|
||||
includeAllBurstAssets: Bool = false,
|
||||
includeHiddenAssets: Bool = false) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.predicate = filter
|
||||
options.includeAssetSourceTypes = sourceTypes
|
||||
options.includeHiddenAssets = includeHiddenAssets
|
||||
options.includeAllBurstAssets = includeAllBurstAssets
|
||||
self.init(in: collection, options: options)
|
||||
}
|
||||
|
||||
/// Fetches all assets in the specified collection
|
||||
/// - Parameters:
|
||||
/// - collection: The asset collection to filter by
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sort: The keyPaths to apply when sorting the results
|
||||
/// - sourceTypes: The sourceTypes to include in the results
|
||||
/// - includeAllBurstAssets: If true, burst assets will be included in the results
|
||||
/// - includeHiddenAssets: If true, hidden assets will be included in the results
|
||||
init<Value>(in collection: PHAssetCollection,
|
||||
fetchLimit: Int = 0,
|
||||
filter: NSPredicate? = nil,
|
||||
sort: [(KeyPath<PHAsset, Value>, ascending: Bool)],
|
||||
sourceTypes: PHAssetSourceType = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced],
|
||||
includeAllBurstAssets: Bool = false,
|
||||
includeHiddenAssets: Bool = false) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
|
||||
options.predicate = filter
|
||||
options.includeAssetSourceTypes = sourceTypes
|
||||
options.includeHiddenAssets = includeHiddenAssets
|
||||
options.includeAllBurstAssets = includeAllBurstAssets
|
||||
self.init(in: collection, options: options)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import Photos
|
||||
import SwiftUI
|
||||
|
||||
@propertyWrapper
|
||||
internal struct FetchCollectionList<Result>: DynamicProperty where Result: PHCollectionList {
|
||||
|
||||
@ObservedObject
|
||||
internal private(set) var observer: ResultsObserver<Result>
|
||||
|
||||
public var wrappedValue: MediaResults<Result> {
|
||||
get { MediaResults(observer.result) }
|
||||
set { observer.result = newValue.result }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchCollectionList {
|
||||
|
||||
/// Instantiates a fetch with an existing `PHFetchResult<Result>` instance
|
||||
init(_ result: PHFetchResult<PHAsset>) {
|
||||
observer = ResultsObserver(result: result as! PHFetchResult<Result>)
|
||||
}
|
||||
|
||||
/// Instantiates a fetch with a custom `PHFetchOptions` instance
|
||||
init(_ options: PHFetchOptions?) {
|
||||
let result = PHCollectionList.fetchTopLevelUserCollections(with: options)
|
||||
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
|
||||
}
|
||||
|
||||
/// Instantiates a fetch by applying the specified sort and filter options
|
||||
/// - Parameters:
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
init(filter: NSPredicate? = nil) {
|
||||
let options = PHFetchOptions()
|
||||
options.predicate = filter
|
||||
self.init(options)
|
||||
}
|
||||
|
||||
/// Instantiates a fetch by applying the specified sort and filter options
|
||||
/// - Parameters:
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sort: The keyPaths to apply when sorting the results
|
||||
init<Value>(fetchLimit: Int = 0,
|
||||
filter: NSPredicate? = nil,
|
||||
sort: [(KeyPath<PHCollectionList, Value>, ascending: Bool)]) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
|
||||
options.predicate = filter
|
||||
self.init(options)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension FetchCollectionList {
|
||||
|
||||
/// Fetches all lists of the specified type and subtyle
|
||||
/// - Parameters:
|
||||
/// - list: The list type to filter by
|
||||
/// - kind: The list subtype to filter by
|
||||
/// - options: Any additional options to apply to the request
|
||||
init(list: PHCollectionListType,
|
||||
kind: PHCollectionListSubtype = .any,
|
||||
options: PHFetchOptions? = nil) {
|
||||
let result = PHCollectionList.fetchCollectionLists(with: list, subtype: kind, options: options)
|
||||
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
|
||||
}
|
||||
|
||||
/// Fetches all lists of the specified type and subtyle
|
||||
/// - Parameters:
|
||||
/// - list: The list type to filter by
|
||||
/// - kind: The list subtype to filter by
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
init(list: PHCollectionListType,
|
||||
kind: PHCollectionListSubtype = .any,
|
||||
fetchLimit: Int = 0,
|
||||
filter: NSPredicate) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.predicate = filter
|
||||
self.init(list: list, kind: kind, options: options)
|
||||
}
|
||||
|
||||
/// Fetches all lists of the specified type and subtyle
|
||||
/// - Parameters:
|
||||
/// - list: The list type to filter by
|
||||
/// - kind: The list subtype to filter by
|
||||
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
|
||||
/// - filter: The predicate to apply when filtering the results
|
||||
/// - sort: The keyPaths to apply when sorting the results
|
||||
init<Value>(list: PHCollectionListType,
|
||||
kind: PHCollectionListSubtype = .any,
|
||||
fetchLimit: Int = 0,
|
||||
filter: NSPredicate? = nil,
|
||||
sort: [(KeyPath<PHCollectionList, Value>, ascending: Bool)]) {
|
||||
let options = PHFetchOptions()
|
||||
options.fetchLimit = fetchLimit
|
||||
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
|
||||
options.predicate = filter
|
||||
self.init(list: list, kind: kind, options: options)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
#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, *) {
|
||||
sheet(isPresented: isPresented) {
|
||||
|
||||
}
|
||||
// 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 {
|
||||
backport.background {
|
||||
LegacyPhotosViewController(isPresented: isPresented, selection: selection, filter: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private struct PhotosViewController: 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 PhotosViewController {
|
||||
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() {
|
||||
isPresented.wrappedValue = false
|
||||
guard let controller else { return }
|
||||
controller.presentedViewController?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
print(results)
|
||||
dismissController()
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
dismissController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
private struct LegacyPhotosViewController: UIViewControllerRepresentable {
|
||||
@Binding var isPresented: Bool
|
||||
@Binding var selection: [Backport<Any>.PhotosPickerItem]
|
||||
|
||||
let filter: Backport<Any>.PHPickerFilter?
|
||||
|
||||
func makeUIViewController(context: Context) -> Representable {
|
||||
Representable(
|
||||
isPresented: $isPresented,
|
||||
selection: $selection,
|
||||
filter: filter
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ controller: Representable, context: Context) {
|
||||
controller.selection = $selection
|
||||
controller.filter = filter
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
private extension LegacyPhotosViewController {
|
||||
final class Representable: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate {
|
||||
private weak var controller: UIImagePickerController?
|
||||
|
||||
var isPresented: Binding<Bool> {
|
||||
didSet {
|
||||
updateControllerLifecycle(
|
||||
from: oldValue.wrappedValue,
|
||||
to: isPresented.wrappedValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var selection: Binding<[Backport<Any>.PhotosPickerItem]>
|
||||
var filter: Backport<Any>.PHPickerFilter?
|
||||
|
||||
init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, filter: Backport<Any>.PHPickerFilter?) {
|
||||
self.isPresented = isPresented
|
||||
self.selection = selection
|
||||
self.filter = filter
|
||||
|
||||
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 = UIImagePickerController()
|
||||
|
||||
if let filter {
|
||||
controller.mediaTypes = filter.mediaTypes
|
||||
} else if let types = UIImagePickerController.availableMediaTypes(for: .photoLibrary) {
|
||||
controller.mediaTypes = types
|
||||
}
|
||||
|
||||
controller.allowsEditing = false
|
||||
controller.sourceType = .photoLibrary
|
||||
controller.videoQuality = .typeHigh
|
||||
controller.presentationController?.delegate = self
|
||||
controller.delegate = self
|
||||
|
||||
present(controller, animated: true)
|
||||
self.controller = controller
|
||||
}
|
||||
|
||||
private func dismissController() {
|
||||
isPresented.wrappedValue = false
|
||||
guard let controller else { return }
|
||||
controller.presentedViewController?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
print("TBD")
|
||||
print(info)
|
||||
dismissController()
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
dismissController()
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
dismissController()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -1,96 +1,80 @@
|
|||
//#if os(iOS)
|
||||
//import SwiftUI
|
||||
//import PhotosUI
|
||||
//
|
||||
//@available(iOS, deprecated: 16.0)
|
||||
//extension Backport where Wrapped: View {
|
||||
// /// Presents a Photos picker that selects a `PhotosPickerItem`.
|
||||
// ///
|
||||
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
||||
// ///
|
||||
// /// - Parameters:
|
||||
// /// - isPresented: The binding to whether the Photos picker should be shown.
|
||||
// /// - selection: The item being shown and selected in the Photos picker.
|
||||
// /// - 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.
|
||||
// public func photosPicker(
|
||||
// isPresented: Binding<Bool>,
|
||||
// selection: Binding<PhotosPickerItem?>,
|
||||
// matching filter: PHPickerFilter? = nil,
|
||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic
|
||||
// ) -> some View {
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /// Presents a Photos picker that selects a collection of `PhotosPickerItem`.
|
||||
// ///
|
||||
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
||||
// ///
|
||||
// /// - Parameters:
|
||||
// /// - isPresented: The binding to whether the Photos picker should be shown.
|
||||
// /// - selection: All items being shown and selected in the Photos picker.
|
||||
// /// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system.
|
||||
// /// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`.
|
||||
// /// - 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 selected items. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used.
|
||||
// public func photosPicker(
|
||||
// isPresented: Binding<Bool>,
|
||||
// selection: Binding<[PhotosPickerItem]>,
|
||||
// maxSelectionCount: Int? = nil,
|
||||
// selectionBehavior: PhotosPickerSelectionBehavior = .default,
|
||||
// matching filter: PHPickerFilter? = nil,
|
||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic
|
||||
// ) -> some View {
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /// Presents a Photos picker 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.
|
||||
// ///
|
||||
// /// - Parameters:
|
||||
// /// - isPresented: The binding to whether the Photos picker should be shown.
|
||||
// /// - selection: The item being shown and selected in the Photos picker.
|
||||
// /// - 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 func photosPicker(
|
||||
// isPresented: Binding<Bool>,
|
||||
// selection: Binding<PhotosPickerItem?>,
|
||||
// matching filter: PHPickerFilter? = nil,
|
||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
||||
// photoLibrary: PHPhotoLibrary
|
||||
// ) -> some View {
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /// Presents a Photos picker that selects a collection of `PhotosPickerItem` from a given photo library.
|
||||
// ///
|
||||
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
||||
// ///
|
||||
// /// - Parameters:
|
||||
// /// - isPresented: The binding to whether the Photos picker should be shown.
|
||||
// /// - selection: All items being shown and selected in the Photos picker.
|
||||
// /// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system.
|
||||
// /// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`.
|
||||
// /// - 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 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.
|
||||
// public func photosPicker(
|
||||
// isPresented: Binding<Bool>,
|
||||
// selection: Binding<[PhotosPickerItem]>,
|
||||
// maxSelectionCount: Int? = nil,
|
||||
// selectionBehavior: PhotosPickerSelectionBehavior = .default,
|
||||
// matching filter: PHPickerFilter? = nil,
|
||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
||||
// photoLibrary: PHPhotoLibrary
|
||||
// ) -> some View {
|
||||
//
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//#endif
|
||||
#if os(iOS)
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
@available(iOS, deprecated: 16.0)
|
||||
public extension Backport where Wrapped: View {
|
||||
/// Presents a Photos picker 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.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - isPresented: The binding to whether the Photos picker should be shown.
|
||||
/// - selection: The item being shown and selected in the Photos picker.
|
||||
/// - 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.
|
||||
func photosPicker(
|
||||
isPresented: Binding<Bool>,
|
||||
selection: Binding<Backport<Any>.PhotosPickerItem?>,
|
||||
matching filter: Backport<Any>.PHPickerFilter? = nil,
|
||||
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
||||
photoLibrary: PHPhotoLibrary = .shared()
|
||||
) -> some View {
|
||||
let binding = Binding(
|
||||
get: {
|
||||
[selection.wrappedValue].compactMap { $0 }
|
||||
},
|
||||
set: { newValue in
|
||||
selection.wrappedValue = newValue.first
|
||||
}
|
||||
)
|
||||
|
||||
return content.sheet(isPresented: isPresented) {
|
||||
PhotosPickerView(
|
||||
selection: binding,
|
||||
filter: filter,
|
||||
maxSelection: 1,
|
||||
selectionBehavior: .default,
|
||||
encoding: preferredItemEncoding,
|
||||
library: photoLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Presents a Photos picker that selects a collection of `PhotosPickerItem` from a given photo library.
|
||||
///
|
||||
/// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - isPresented: The binding to whether the Photos picker should be shown.
|
||||
/// - selection: All items being shown and selected in the Photos picker.
|
||||
/// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system.
|
||||
/// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`.
|
||||
/// - 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 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.
|
||||
func photosPicker(
|
||||
isPresented: Binding<Bool>,
|
||||
selection: Binding<[Backport<Any>.PhotosPickerItem]>,
|
||||
maxSelectionCount: Int? = nil,
|
||||
selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior = .default,
|
||||
matching filter: Backport<Any>.PHPickerFilter? = nil,
|
||||
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
||||
photoLibrary: PHPhotoLibrary = .shared()
|
||||
) -> some View {
|
||||
content.sheet(isPresented: isPresented) {
|
||||
PhotosPickerView(
|
||||
selection: selection,
|
||||
filter: filter,
|
||||
maxSelection: maxSelectionCount,
|
||||
selectionBehavior: selectionBehavior,
|
||||
encoding: preferredItemEncoding,
|
||||
library: photoLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -25,89 +25,19 @@ public extension Backport<Any> {
|
|||
} label: {
|
||||
label
|
||||
}
|
||||
._photoPicker(
|
||||
.backport.photosPicker(
|
||||
isPresented: $isPresented,
|
||||
selection: $selection,
|
||||
filter: filter,
|
||||
maxSelectionCount: maxSelection,
|
||||
selectionBehavior: selectionBehavior,
|
||||
matching: filter,
|
||||
preferredItemEncoding: encoding,
|
||||
library: library
|
||||
photoLibrary: library
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS, introduced: 13, deprecated: 14)
|
||||
public extension Backport<Any>.PhotosPicker {
|
||||
init(
|
||||
selection: Binding<Backport.PhotosPickerItem?>,
|
||||
matching filter: Backport.PHPickerFilter? = nil,
|
||||
@ViewBuilder label: () -> Label
|
||||
) {
|
||||
_selection = .init(
|
||||
get: {
|
||||
[selection.wrappedValue].compactMap { $0 }
|
||||
},
|
||||
set: { newValue in
|
||||
selection.wrappedValue = newValue.first
|
||||
}
|
||||
)
|
||||
self.filter = filter
|
||||
self.maxSelection = 1
|
||||
self.selectionBehavior = .default
|
||||
self.encoding = .automatic
|
||||
self.library = .shared()
|
||||
self.label = label()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS, introduced: 13, deprecated: 14)
|
||||
public extension Backport<Any>.PhotosPicker<Text> {
|
||||
init(
|
||||
_ titleKey: LocalizedStringKey,
|
||||
selection: Binding<Backport.PhotosPickerItem?>,
|
||||
matching filter: Backport.PHPickerFilter? = nil
|
||||
) {
|
||||
_selection = .init(
|
||||
get: {
|
||||
[selection.wrappedValue].compactMap { $0 }
|
||||
},
|
||||
set: { newValue in
|
||||
selection.wrappedValue = newValue.first
|
||||
}
|
||||
)
|
||||
self.filter = filter
|
||||
self.maxSelection = 1
|
||||
self.selectionBehavior = .default
|
||||
self.encoding = .automatic
|
||||
self.library = .shared()
|
||||
self.label = Text(titleKey)
|
||||
}
|
||||
|
||||
init<S>(
|
||||
_ title: S,
|
||||
selection: Binding<Backport.PhotosPickerItem?>,
|
||||
matching filter: Backport.PHPickerFilter? = nil
|
||||
) where S: StringProtocol {
|
||||
_selection = .init(
|
||||
get: {
|
||||
[selection.wrappedValue].compactMap { $0 }
|
||||
},
|
||||
set: { newValue in
|
||||
selection.wrappedValue = newValue.first
|
||||
}
|
||||
)
|
||||
self.filter = filter
|
||||
self.maxSelection = 1
|
||||
self.selectionBehavior = .default
|
||||
self.encoding = .automatic
|
||||
self.library = .shared()
|
||||
self.label = Text(title)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
public extension Backport<Any>.PhotosPicker {
|
||||
/// Creates a Photos picker that selects a `PhotosPickerItem`.
|
||||
///
|
||||
|
@ -176,7 +106,6 @@ public extension Backport<Any>.PhotosPicker {
|
|||
|
||||
// MARK: Single selection
|
||||
|
||||
@available(iOS 14, *)
|
||||
public extension Backport<Any>.PhotosPicker<Text> {
|
||||
/// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`.
|
||||
///
|
||||
|
@ -309,7 +238,6 @@ public extension Backport<Any>.PhotosPicker<Text> {
|
|||
|
||||
// MARK: Multiple selection
|
||||
|
||||
@available(iOS 15, *)
|
||||
public extension Backport<Any>.PhotosPicker {
|
||||
/// Creates a Photos picker that selects a collection of `PhotosPickerItem`.
|
||||
///
|
||||
|
@ -370,7 +298,6 @@ public extension Backport<Any>.PhotosPicker {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
public extension Backport<Any>.PhotosPicker<Text> {
|
||||
/// Creates a Photos picker with its label generated from a localized string key that selects a collection of `PhotosPickerItem`.
|
||||
///
|
||||
|
|
|
@ -36,14 +36,6 @@ 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:
|
||||
|
|
|
@ -8,17 +8,9 @@ public extension Backport where Wrapped == Any {
|
|||
/// A filter that restricts which types of assets to show
|
||||
struct PHPickerFilter: Equatable, Hashable {
|
||||
internal let predicate: NSPredicate
|
||||
// this enables us to support iOS 13 for images vs videos
|
||||
internal let mediaTypes: [String]
|
||||
|
||||
internal init(mediaTypes: [String]) {
|
||||
self.predicate = .init(value: true)
|
||||
self.mediaTypes = mediaTypes
|
||||
}
|
||||
|
||||
internal init(predicate: NSPredicate) {
|
||||
self.predicate = predicate
|
||||
self.mediaTypes = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,34 +19,19 @@ public extension Backport where Wrapped == Any {
|
|||
public extension Backport<Any>.PHPickerFilter {
|
||||
/// The filter for images.
|
||||
static var images: Self {
|
||||
if #available(iOS 14, *) {
|
||||
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image]))
|
||||
} else {
|
||||
return .init(mediaTypes: [String(kUTTypeImage)])
|
||||
}
|
||||
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image]))
|
||||
}
|
||||
|
||||
/// The filter for videos.
|
||||
static var videos: Self {
|
||||
if #available(iOS 14, *) {
|
||||
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video]))
|
||||
} else {
|
||||
return .init(mediaTypes: [String(kUTTypeMovie)])
|
||||
}
|
||||
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video]))
|
||||
}
|
||||
|
||||
/// The filter for live photos.
|
||||
static var livePhotos: Self {
|
||||
if #available(iOS 14, *) {
|
||||
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive]))
|
||||
} else {
|
||||
return .init(mediaTypes: [String(kUTTypeMovie), String(kUTTypeLivePhoto)])
|
||||
}
|
||||
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive]))
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
public extension Backport<Any>.PHPickerFilter {
|
||||
/// The filter for Depth Effect photos.
|
||||
static var depthEffectPhotos: Self {
|
||||
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoDepthEffect]))
|
||||
|
@ -81,7 +58,6 @@ public extension Backport<Any>.PHPickerFilter {
|
|||
}
|
||||
|
||||
/// Returns a new filter based on the asset playback style.
|
||||
#warning("NEEDS TESTING!")
|
||||
static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self {
|
||||
.init(predicate: NSPredicate(format: "(playbackStyle & %d) != 0", argumentArray: [playbackStyle.rawValue]))
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
#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
|
|
@ -0,0 +1,239 @@
|
|||
//#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, *) {
|
||||
// backport.background {
|
||||
// PhotosViewController(
|
||||
// isPresented: isPresented,
|
||||
// selection: selection,
|
||||
// filter: filter,
|
||||
// maxSelectionCount: maxSelectionCount,
|
||||
// selectionBehavior: selectionBehavior,
|
||||
// preferredItemEncoding: preferredItemEncoding,
|
||||
// library: library
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// backport.background {
|
||||
// LegacyPhotosViewController(isPresented: isPresented, selection: selection, filter: filter)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@available(iOS 14, *)
|
||||
//private struct PhotosViewController: UIViewControllerRepresentable {
|
||||
// @Binding var isPresented: Bool
|
||||
// @Binding var selection: [Backport<Any>.PhotosPickerItem]
|
||||
//
|
||||
// let options: PHFetchOptions
|
||||
//
|
||||
// init(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) {
|
||||
// _isPresented = isPresented
|
||||
// _selection = selection
|
||||
//
|
||||
// options = PHFetchOptions()
|
||||
// options.predicate = filter?.predicate
|
||||
//
|
||||
// if #available(iOS 15, *) {
|
||||
// configuration.selection = selectionBehavior.behaviour
|
||||
// }
|
||||
//
|
||||
// self.configuration = configuration
|
||||
// }
|
||||
//
|
||||
// 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 PhotosViewController {
|
||||
// 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() {
|
||||
// isPresented.wrappedValue = false
|
||||
// guard let controller else { return }
|
||||
// controller.presentedViewController?.dismiss(animated: true)
|
||||
// }
|
||||
//
|
||||
// func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
// print(results)
|
||||
// dismissController()
|
||||
// }
|
||||
//
|
||||
// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
// dismissController()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@available(iOS 13, *)
|
||||
//private struct LegacyPhotosViewController: UIViewControllerRepresentable {
|
||||
// @Binding var isPresented: Bool
|
||||
// @Binding var selection: [Backport<Any>.PhotosPickerItem]
|
||||
//
|
||||
// let filter: Backport<Any>.PHPickerFilter?
|
||||
//
|
||||
// func makeUIViewController(context: Context) -> Representable {
|
||||
// Representable(
|
||||
// isPresented: $isPresented,
|
||||
// selection: $selection,
|
||||
// filter: filter
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//
|
||||
// func updateUIViewController(_ controller: Representable, context: Context) {
|
||||
// controller.selection = $selection
|
||||
// controller.filter = filter
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@available(iOS 13, *)
|
||||
//private extension LegacyPhotosViewController {
|
||||
// final class Representable: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate {
|
||||
// private weak var controller: UIImagePickerController?
|
||||
//
|
||||
// var isPresented: Binding<Bool> {
|
||||
// didSet {
|
||||
// updateControllerLifecycle(
|
||||
// from: oldValue.wrappedValue,
|
||||
// to: isPresented.wrappedValue
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var selection: Binding<[Backport<Any>.PhotosPickerItem]>
|
||||
// var filter: Backport<Any>.PHPickerFilter?
|
||||
//
|
||||
// init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, filter: Backport<Any>.PHPickerFilter?) {
|
||||
// self.isPresented = isPresented
|
||||
// self.selection = selection
|
||||
// self.filter = filter
|
||||
//
|
||||
// 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 = UIImagePickerController()
|
||||
//
|
||||
// if let filter {
|
||||
// controller.mediaTypes = filter.mediaTypes
|
||||
// } else if let types = UIImagePickerController.availableMediaTypes(for: .photoLibrary) {
|
||||
// controller.mediaTypes = types
|
||||
// }
|
||||
//
|
||||
// controller.allowsEditing = false
|
||||
// controller.sourceType = .photoLibrary
|
||||
// controller.videoQuality = .typeHigh
|
||||
// controller.presentationController?.delegate = self
|
||||
// controller.delegate = self
|
||||
//
|
||||
// present(controller, animated: true)
|
||||
// self.controller = controller
|
||||
// }
|
||||
//
|
||||
// private func dismissController() {
|
||||
// isPresented.wrappedValue = false
|
||||
// guard let controller else { return }
|
||||
// controller.presentedViewController?.dismiss(animated: true)
|
||||
// }
|
||||
//
|
||||
// func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
// print("TBD")
|
||||
// print(info)
|
||||
// dismissController()
|
||||
// }
|
||||
//
|
||||
// func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
// dismissController()
|
||||
// }
|
||||
//
|
||||
// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
// dismissController()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//#endif
|
|
@ -0,0 +1,93 @@
|
|||
#if os(iOS)
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
internal struct PhotosPickerView: View {
|
||||
@Environment(\.backportDismiss) private var dismiss
|
||||
@Binding var selection: [Backport<Any>.PhotosPickerItem]
|
||||
|
||||
let filter: Backport<Any>.PHPickerFilter?
|
||||
let maxSelection: Int?
|
||||
let selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior
|
||||
let encoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy
|
||||
let library: PHPhotoLibrary
|
||||
|
||||
private enum Source: String, CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
case photos = "Photos"
|
||||
case albums = "Albums"
|
||||
}
|
||||
|
||||
@State private var source: Source = .photos
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
|
||||
}
|
||||
.navigationBarTitle(Text("Photos"), displayMode: .inline)
|
||||
.backport.toolbar {
|
||||
Backport.ToolbarItem(placement: .primaryAction) {
|
||||
Text("Done")
|
||||
}
|
||||
|
||||
Backport.ToolbarItem(placement: .cancellationAction) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Backport.ToolbarItem(placement: .principal) {
|
||||
Text("Principal")
|
||||
}
|
||||
}
|
||||
// .backport.toolbar {
|
||||
// picker
|
||||
// } leading: {
|
||||
// Button("Cancel") {
|
||||
// dismiss()
|
||||
// }
|
||||
// } trailing: {
|
||||
// Button("Add") {
|
||||
//
|
||||
// }
|
||||
// .font(.body.weight(.semibold))
|
||||
// .disabled(selection.isEmpty)
|
||||
// .opacity(maxSelection == 1 ? 0 : 1)
|
||||
// }
|
||||
// .controller { controller in
|
||||
// if #available(iOS 14, *) { } else {
|
||||
// guard controller?.navigationItem.titleView == nil else { return }
|
||||
// controller?.navigationItem.titleView = UIHostingController(rootView: picker, ignoreSafeArea: false).view
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.backport.interactiveDismissDisabled()
|
||||
}
|
||||
|
||||
private var picker: some View {
|
||||
Picker("", selection: $source) {
|
||||
ForEach(Source.allCases) { source in
|
||||
Text(source.rawValue)
|
||||
.tag(source)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
//private extension Backport where Wrapped: View {
|
||||
// @ViewBuilder
|
||||
// func toolbar<Leading: View, Trailing: View, Principal: View>(@ViewBuilder principal: () -> Principal, @ViewBuilder leading: () -> Leading, @ViewBuilder trailing: () -> Trailing) -> some View {
|
||||
// if #available(iOS 14, *) {
|
||||
// content.toolbar {
|
||||
//// ToolbarItem(placement: .navigationBarLeading, content: leading)
|
||||
//// ToolbarItem(placement: .navigationBarTrailing, content: trailing)
|
||||
//// ToolbarItem(placement: .principal, content: principal)
|
||||
// }
|
||||
// } else {
|
||||
// content
|
||||
// .navigationBarItems(leading: leading(), trailing: trailing())
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
#endif
|
|
@ -0,0 +1,122 @@
|
|||
#if os(iOS)
|
||||
import SwiftUI
|
||||
|
||||
public extension Backport<Any> {
|
||||
enum ToolbarItemPlacement {
|
||||
case automatic
|
||||
case primaryAction
|
||||
case confirmationAction
|
||||
case cancellationAction
|
||||
case destructiveAction
|
||||
case principal
|
||||
|
||||
var isLeading: Bool {
|
||||
switch self {
|
||||
case .cancellationAction:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isTrailing: Bool {
|
||||
switch self {
|
||||
case .automatic, .primaryAction, .confirmationAction, .destructiveAction:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ToolbarItem: View {
|
||||
let placement: Backport.ToolbarItemPlacement
|
||||
let content: AnyView
|
||||
|
||||
public init<Content: View>(placement: Backport.ToolbarItemPlacement, @ViewBuilder content: () -> Content) {
|
||||
self.placement = placement
|
||||
self.content = AnyView(content())
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where Element == Backport<Any>.ToolbarItem, Indices: RandomAccessCollection, Indices.Index: Hashable {
|
||||
@ViewBuilder var content: some View {
|
||||
if !isEmpty {
|
||||
HStack {
|
||||
ForEach(indices, id: \.self) { index in
|
||||
self[index].content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS, introduced: 13, deprecated: 14)
|
||||
public extension Backport where Wrapped: View {
|
||||
@ViewBuilder
|
||||
func toolbar(@BackportToolbarContentBuilder _ items: () -> [Backport<Any>.ToolbarItem]) -> some View {
|
||||
let items = items()
|
||||
content
|
||||
.navigationBarItems(leading: items.filter { $0.placement.isLeading }.content, trailing: items.filter { $0.placement.isTrailing }.content)
|
||||
.controller { controller in
|
||||
controller?.navigationItem.titleView = UIHostingController(
|
||||
rootView: items.filter { $0.placement == .principal }.content,
|
||||
ignoreSafeArea: false
|
||||
).view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@resultBuilder
|
||||
public struct BackportToolbarContentBuilder { }
|
||||
public extension BackportToolbarContentBuilder {
|
||||
static func buildBlock() -> [Backport<Any>.ToolbarItem] {
|
||||
[Backport<Any>.ToolbarItem(placement: .automatic, content: { EmptyView() })]
|
||||
}
|
||||
|
||||
static func buildBlock(_ content: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[content]
|
||||
}
|
||||
|
||||
static func buildIf(_ content: Backport<Any>.ToolbarItem?) -> [Backport<Any>.ToolbarItem?] {
|
||||
[content].compactMap { $0 }
|
||||
}
|
||||
|
||||
static func buildEither(first: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[first]
|
||||
}
|
||||
|
||||
static func buildEither(second: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[second]
|
||||
}
|
||||
|
||||
static func buildLimitedAvailability(_ content: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[content]
|
||||
}
|
||||
|
||||
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[c0, c1]
|
||||
}
|
||||
|
||||
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[c0, c1, c2]
|
||||
}
|
||||
|
||||
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem, _ c3: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[c0, c1, c2, c3]
|
||||
}
|
||||
|
||||
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem, _ c3: Backport<Any>.ToolbarItem, _ c4: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[c0, c1, c2, c3, c4]
|
||||
}
|
||||
|
||||
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem, _ c3: Backport<Any>.ToolbarItem, _ c4: Backport<Any>.ToolbarItem, _ c5: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
|
||||
[c0, c1, c2, c3, c4, c5]
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -74,11 +74,7 @@ public extension Backport where Wrapped: View {
|
|||
@available(watchOS, deprecated: 9)
|
||||
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
|
||||
#if os(iOS)
|
||||
if #available(iOS 15, *) {
|
||||
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
|
||||
} else {
|
||||
content
|
||||
}
|
||||
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
|
||||
#else
|
||||
content
|
||||
#endif
|
||||
|
@ -154,11 +150,7 @@ public extension Backport where Wrapped: View {
|
|||
@ViewBuilder
|
||||
func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View {
|
||||
#if os(iOS)
|
||||
if #available(iOS 15, *) {
|
||||
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
|
||||
} else {
|
||||
content
|
||||
}
|
||||
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
|
||||
#else
|
||||
content
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue