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 {
|
func inspect<ViewType: PlatformView>(selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void) -> some View {
|
||||||
inject(InspectionView(selector: selector, customize: customize))
|
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 {
|
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)
|
#if os(iOS)
|
||||||
//import SwiftUI
|
import SwiftUI
|
||||||
//import PhotosUI
|
import PhotosUI
|
||||||
//
|
|
||||||
//@available(iOS, deprecated: 16.0)
|
@available(iOS, deprecated: 16.0)
|
||||||
//extension Backport where Wrapped: View {
|
public extension Backport where Wrapped: View {
|
||||||
// /// Presents a Photos picker that selects a `PhotosPickerItem`.
|
/// 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.
|
/// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
||||||
// ///
|
///
|
||||||
// /// - Parameters:
|
/// - Parameters:
|
||||||
// /// - isPresented: The binding to whether the Photos picker should be shown.
|
/// - isPresented: The binding to whether the Photos picker should be shown.
|
||||||
// /// - selection: The item being shown and selected in the Photos picker.
|
/// - 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.
|
/// - 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.
|
/// - 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(
|
/// - photoLibrary: The photo library to choose from.
|
||||||
// isPresented: Binding<Bool>,
|
func photosPicker(
|
||||||
// selection: Binding<PhotosPickerItem?>,
|
isPresented: Binding<Bool>,
|
||||||
// matching filter: PHPickerFilter? = nil,
|
selection: Binding<Backport<Any>.PhotosPickerItem?>,
|
||||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic
|
matching filter: Backport<Any>.PHPickerFilter? = nil,
|
||||||
// ) -> some View {
|
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
||||||
//
|
photoLibrary: PHPhotoLibrary = .shared()
|
||||||
// }
|
) -> some View {
|
||||||
//
|
let binding = Binding(
|
||||||
//
|
get: {
|
||||||
// /// Presents a Photos picker that selects a collection of `PhotosPickerItem`.
|
[selection.wrappedValue].compactMap { $0 }
|
||||||
// ///
|
},
|
||||||
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
set: { newValue in
|
||||||
// ///
|
selection.wrappedValue = newValue.first
|
||||||
// /// - 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.
|
return content.sheet(isPresented: isPresented) {
|
||||||
// /// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`.
|
PhotosPickerView(
|
||||||
// /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown.
|
selection: binding,
|
||||||
// /// - 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.
|
filter: filter,
|
||||||
// public func photosPicker(
|
maxSelection: 1,
|
||||||
// isPresented: Binding<Bool>,
|
selectionBehavior: .default,
|
||||||
// selection: Binding<[PhotosPickerItem]>,
|
encoding: preferredItemEncoding,
|
||||||
// maxSelectionCount: Int? = nil,
|
library: photoLibrary
|
||||||
// selectionBehavior: PhotosPickerSelectionBehavior = .default,
|
)
|
||||||
// matching filter: PHPickerFilter? = nil,
|
}
|
||||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic
|
}
|
||||||
// ) -> 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.
|
||||||
// /// Presents a Photos picker that selects a `PhotosPickerItem` from a given photo library.
|
///
|
||||||
// ///
|
/// - Parameters:
|
||||||
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
/// - isPresented: The binding to whether the Photos picker should be shown.
|
||||||
// ///
|
/// - selection: All items being shown and selected in the Photos picker.
|
||||||
// /// - Parameters:
|
/// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system.
|
||||||
// /// - isPresented: The binding to whether the Photos picker should be shown.
|
/// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`.
|
||||||
// /// - 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.
|
||||||
// /// - 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.
|
||||||
// /// - 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.
|
||||||
// /// - photoLibrary: The photo library to choose from.
|
func photosPicker(
|
||||||
// public func photosPicker(
|
isPresented: Binding<Bool>,
|
||||||
// isPresented: Binding<Bool>,
|
selection: Binding<[Backport<Any>.PhotosPickerItem]>,
|
||||||
// selection: Binding<PhotosPickerItem?>,
|
maxSelectionCount: Int? = nil,
|
||||||
// matching filter: PHPickerFilter? = nil,
|
selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior = .default,
|
||||||
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
matching filter: Backport<Any>.PHPickerFilter? = nil,
|
||||||
// photoLibrary: PHPhotoLibrary
|
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
|
||||||
// ) -> some View {
|
photoLibrary: PHPhotoLibrary = .shared()
|
||||||
//
|
) -> some View {
|
||||||
// }
|
content.sheet(isPresented: isPresented) {
|
||||||
//
|
PhotosPickerView(
|
||||||
//
|
selection: selection,
|
||||||
// /// Presents a Photos picker that selects a collection of `PhotosPickerItem` from a given photo library.
|
filter: filter,
|
||||||
// ///
|
maxSelection: maxSelectionCount,
|
||||||
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
|
selectionBehavior: selectionBehavior,
|
||||||
// ///
|
encoding: preferredItemEncoding,
|
||||||
// /// - Parameters:
|
library: photoLibrary
|
||||||
// /// - 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.
|
#endif
|
||||||
// /// - 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
|
|
||||||
|
|
|
@ -25,89 +25,19 @@ public extension Backport<Any> {
|
||||||
} label: {
|
} label: {
|
||||||
label
|
label
|
||||||
}
|
}
|
||||||
._photoPicker(
|
.backport.photosPicker(
|
||||||
isPresented: $isPresented,
|
isPresented: $isPresented,
|
||||||
selection: $selection,
|
selection: $selection,
|
||||||
filter: filter,
|
|
||||||
maxSelectionCount: maxSelection,
|
maxSelectionCount: maxSelection,
|
||||||
selectionBehavior: selectionBehavior,
|
selectionBehavior: selectionBehavior,
|
||||||
|
matching: filter,
|
||||||
preferredItemEncoding: encoding,
|
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 {
|
public extension Backport<Any>.PhotosPicker {
|
||||||
/// Creates a Photos picker that selects a `PhotosPickerItem`.
|
/// Creates a Photos picker that selects a `PhotosPickerItem`.
|
||||||
///
|
///
|
||||||
|
@ -176,7 +106,6 @@ public extension Backport<Any>.PhotosPicker {
|
||||||
|
|
||||||
// MARK: Single selection
|
// MARK: Single selection
|
||||||
|
|
||||||
@available(iOS 14, *)
|
|
||||||
public extension Backport<Any>.PhotosPicker<Text> {
|
public extension Backport<Any>.PhotosPicker<Text> {
|
||||||
/// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`.
|
/// 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
|
// MARK: Multiple selection
|
||||||
|
|
||||||
@available(iOS 15, *)
|
|
||||||
public extension Backport<Any>.PhotosPicker {
|
public extension Backport<Any>.PhotosPicker {
|
||||||
/// Creates a Photos picker that selects a collection of `PhotosPickerItem`.
|
/// 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> {
|
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`.
|
/// 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.
|
/// All supported content types of the item, in order of most preferred to least preferred.
|
||||||
public let supportedContentTypes: [String]
|
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.
|
/// Creates an item without any representation using an identifier.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
|
|
@ -8,17 +8,9 @@ public extension Backport where Wrapped == Any {
|
||||||
/// A filter that restricts which types of assets to show
|
/// A filter that restricts which types of assets to show
|
||||||
struct PHPickerFilter: Equatable, Hashable {
|
struct PHPickerFilter: Equatable, Hashable {
|
||||||
internal let predicate: NSPredicate
|
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) {
|
internal init(predicate: NSPredicate) {
|
||||||
self.predicate = predicate
|
self.predicate = predicate
|
||||||
self.mediaTypes = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,34 +19,19 @@ public extension Backport where Wrapped == Any {
|
||||||
public extension Backport<Any>.PHPickerFilter {
|
public extension Backport<Any>.PHPickerFilter {
|
||||||
/// The filter for images.
|
/// The filter for images.
|
||||||
static var images: Self {
|
static var images: Self {
|
||||||
if #available(iOS 14, *) {
|
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image]))
|
||||||
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image]))
|
|
||||||
} else {
|
|
||||||
return .init(mediaTypes: [String(kUTTypeImage)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The filter for videos.
|
/// The filter for videos.
|
||||||
static var videos: Self {
|
static var videos: Self {
|
||||||
if #available(iOS 14, *) {
|
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video]))
|
||||||
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video]))
|
|
||||||
} else {
|
|
||||||
return .init(mediaTypes: [String(kUTTypeMovie)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The filter for live photos.
|
/// The filter for live photos.
|
||||||
static var livePhotos: Self {
|
static var livePhotos: Self {
|
||||||
if #available(iOS 14, *) {
|
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive]))
|
||||||
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive]))
|
|
||||||
} else {
|
|
||||||
return .init(mediaTypes: [String(kUTTypeMovie), String(kUTTypeLivePhoto)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 14, *)
|
|
||||||
public extension Backport<Any>.PHPickerFilter {
|
|
||||||
/// The filter for Depth Effect photos.
|
/// The filter for Depth Effect photos.
|
||||||
static var depthEffectPhotos: Self {
|
static var depthEffectPhotos: Self {
|
||||||
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoDepthEffect]))
|
.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.
|
/// Returns a new filter based on the asset playback style.
|
||||||
#warning("NEEDS TESTING!")
|
|
||||||
static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self {
|
static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self {
|
||||||
.init(predicate: NSPredicate(format: "(playbackStyle & %d) != 0", argumentArray: [playbackStyle.rawValue]))
|
.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)
|
@available(watchOS, deprecated: 9)
|
||||||
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
|
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if #available(iOS 15, *) {
|
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
|
||||||
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
content
|
content
|
||||||
#endif
|
#endif
|
||||||
|
@ -154,11 +150,7 @@ public extension Backport where Wrapped: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View {
|
func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if #available(iOS 15, *) {
|
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
|
||||||
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
content
|
content
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue