From cce0b59d3de18e997bcaebe524b17cfda7bade0d Mon Sep 17 00:00:00 2001 From: Shaps Benkau Date: Mon, 30 Jan 2023 22:09:25 +0000 Subject: [PATCH] Introduces rudimentary Toolbar support for iOS 13 --- .../Internal/Inspect+UIKit.swift | 8 + .../PhotosPicker/Core/MediaResults.swift | 40 +++ .../PhotosPicker/Core/PHFetchOptions.swift | 63 +++++ .../Core/PHObject+Identifiable.swift | 6 + .../PhotosPicker/Fetch/FetchAsset.swift | 60 +++++ .../Fetch/FetchAssetCollection.swift | 95 +++++++ .../PhotosPicker/Fetch/FetchAssetList.swift | 116 +++++++++ .../Fetch/FetchCollectionList.swift | 105 ++++++++ .../Shared/PhotosPicker/Hosts.swift | 231 ----------------- .../PhotosPicker/PhotosPicker+View.swift | 176 ++++++------- .../Shared/PhotosPicker/PhotosPicker.swift | 79 +----- .../PhotosPicker/PhotosPickerItem.swift | 8 - .../Shared/PhotosPicker/PickerFilter.swift | 30 +-- .../Shared/PhotosPicker/PickerObserver.swift | 48 ---- .../Shared/PhotosPicker/UI/Hosts.swift | 239 ++++++++++++++++++ .../PhotosPicker/UI/PhotosPickerView.swift | 93 +++++++ .../Shared/Toolbar/Toolbar.swift | 122 +++++++++ .../InteractiveDismiss.swift | 12 +- 18 files changed, 1035 insertions(+), 496 deletions(-) create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Core/MediaResults.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHFetchOptions.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHObject+Identifiable.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAsset.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetCollection.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetList.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchCollectionList.swift delete mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/Hosts.swift delete mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/PickerObserver.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift create mode 100644 Sources/SwiftUIBackports/Shared/PhotosPicker/UI/PhotosPickerView.swift create mode 100644 Sources/SwiftUIBackports/Shared/Toolbar/Toolbar.swift diff --git a/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift b/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift index b28f77c..2083b71 100644 --- a/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift +++ b/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift @@ -84,6 +84,14 @@ extension View { func inspect(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: View { diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/MediaResults.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/MediaResults.swift new file mode 100644 index 0000000..9da6ece --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/MediaResults.swift @@ -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: RandomAccessCollection where Result: PHObject { + + /// Represents the underlying results + public private(set) var result: PHFetchResult + + /// Instantiates a new instance with the specified result + public init(_ result: PHFetchResult) { + 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: NSObject, ObservableObject, PHPhotoLibraryChangeObserver where Result: PHObject { + + @Published + internal var result: PHFetchResult + + deinit { + PHPhotoLibrary.shared().unregisterChangeObserver(self) + } + + init(result: PHFetchResult) { + self.result = result + super.init() + PHPhotoLibrary.shared().register(self) + } + + func photoLibraryDidChange(_ changeInstance: PHChange) { + result = changeInstance.changeDetails(for: result)?.fetchResultAfterChanges ?? result + } + +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHFetchOptions.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHFetchOptions.swift new file mode 100644 index 0000000..899236b --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHFetchOptions.swift @@ -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(by keyPath: KeyPath, 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 + } + +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHObject+Identifiable.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHObject+Identifiable.swift new file mode 100644 index 0000000..3f2543f --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHObject+Identifiable.swift @@ -0,0 +1,6 @@ +import SwiftUI +import Photos + +extension PHObject: Identifiable { + public var id: String { localIdentifier } +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAsset.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAsset.swift new file mode 100644 index 0000000..3e8a2dc --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAsset.swift @@ -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 + } + +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetCollection.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetCollection.swift new file mode 100644 index 0000000..db19931 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetCollection.swift @@ -0,0 +1,95 @@ +import Photos +import SwiftUI + +/// Fetches a set of asset collections from the `Photos` framework +@propertyWrapper +internal struct FetchAssetCollection: DynamicProperty where Result: PHAssetCollection { + + @ObservedObject + internal private(set) var observer: ResultsObserver + + /// Represents the results of the fetch + public var wrappedValue: MediaResults { + get { MediaResults(observer.result) } + set { observer.result = newValue.result } + } + +} + +internal extension FetchAssetCollection { + + /// Instantiates a fetch with an existing `PHFetchResult` instance + init(result: PHFetchResult) { + 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)) + } + + /// 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(filter: NSPredicate? = nil, + sort: [(KeyPath, 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)) + } + + /// 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(album: PHAssetCollectionType, + kind: PHAssetCollectionSubtype = .any, + fetchLimit: Int = 0, + filter: NSPredicate? = nil, + sort: [(KeyPath, 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) + } + +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetList.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetList.swift new file mode 100644 index 0000000..23a15ab --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetList.swift @@ -0,0 +1,116 @@ +import Photos +import SwiftUI + +/// Fetches a set of assets from the `Photos` framework +@propertyWrapper +internal struct FetchAssetList: DynamicProperty where Result: PHAsset { + + @ObservedObject + internal private(set) var observer: ResultsObserver + + /// Represents the results of the fetch + public var wrappedValue: MediaResults { + get { MediaResults(observer.result) } + set { observer.result = newValue.result } + } + +} + +internal extension FetchAssetList { + + /// Instantiates a fetch with an existing `PHFetchResult` instance + init(_ result: PHFetchResult) { + observer = ResultsObserver(result: result as! PHFetchResult) + } + + /// Instantiates a fetch with a custom `PHFetchOptions` instance + init(_ options: PHFetchOptions? = nil) { + let result = PHAsset.fetchAssets(with: options) + observer = ResultsObserver(result: result as! PHFetchResult) + } + + /// 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(filter: NSPredicate? = nil, + sort: [(KeyPath, 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)) + } + + /// 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(in collection: PHAssetCollection, + fetchLimit: Int = 0, + filter: NSPredicate? = nil, + sort: [(KeyPath, 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) + } + +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchCollectionList.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchCollectionList.swift new file mode 100644 index 0000000..80831f4 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchCollectionList.swift @@ -0,0 +1,105 @@ +import Photos +import SwiftUI + +@propertyWrapper +internal struct FetchCollectionList: DynamicProperty where Result: PHCollectionList { + + @ObservedObject + internal private(set) var observer: ResultsObserver + + public var wrappedValue: MediaResults { + get { MediaResults(observer.result) } + set { observer.result = newValue.result } + } + +} + +internal extension FetchCollectionList { + + /// Instantiates a fetch with an existing `PHFetchResult` instance + init(_ result: PHFetchResult) { + observer = ResultsObserver(result: result as! PHFetchResult) + } + + /// 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)) + } + + /// 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(fetchLimit: Int = 0, + filter: NSPredicate? = nil, + sort: [(KeyPath, 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)) + } + + /// 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(list: PHCollectionListType, + kind: PHCollectionListSubtype = .any, + fetchLimit: Int = 0, + filter: NSPredicate? = nil, + sort: [(KeyPath, 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) + } + +} diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/Hosts.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/Hosts.swift deleted file mode 100644 index bb242b8..0000000 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/Hosts.swift +++ /dev/null @@ -1,231 +0,0 @@ -#if os(iOS) -import SwiftUI -import PhotosUI - -internal extension View { - @ViewBuilder - func _photoPicker( - isPresented: Binding, - selection: Binding<[Backport.PhotosPickerItem]>, - filter: Backport.PHPickerFilter?, - maxSelectionCount: Int?, - selectionBehavior: Backport.PhotosPickerSelectionBehavior, - preferredItemEncoding: Backport.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.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 { - didSet { - updateControllerLifecycle( - from: oldValue.wrappedValue, - to: isPresented.wrappedValue - ) - } - } - - var selection: Binding<[Backport.PhotosPickerItem]> - var configuration: PHPickerConfiguration - - init(isPresented: Binding, selection: Binding<[Backport.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.PhotosPickerItem] - - let filter: Backport.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 { - didSet { - updateControllerLifecycle( - from: oldValue.wrappedValue, - to: isPresented.wrappedValue - ) - } - } - - var selection: Binding<[Backport.PhotosPickerItem]> - var filter: Backport.PHPickerFilter? - - init(isPresented: Binding, selection: Binding<[Backport.PhotosPickerItem]>, filter: Backport.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 diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift index b64f542..0ba00d2 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift @@ -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, -// selection: Binding, -// 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, -// 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, -// selection: Binding, -// 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, -// 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, + selection: Binding.PhotosPickerItem?>, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.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, + selection: Binding<[Backport.PhotosPickerItem]>, + maxSelectionCount: Int? = nil, + selectionBehavior: Backport.PhotosPickerSelectionBehavior = .default, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.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 diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift index df22564..4e25a2b 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift @@ -25,89 +25,19 @@ public extension Backport { } 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.PhotosPicker { - init( - selection: Binding, - 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.PhotosPicker { - init( - _ titleKey: LocalizedStringKey, - selection: Binding, - 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( - _ title: S, - selection: Binding, - 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.PhotosPicker { /// Creates a Photos picker that selects a `PhotosPickerItem`. /// @@ -176,7 +106,6 @@ public extension Backport.PhotosPicker { // MARK: Single selection -@available(iOS 14, *) public extension Backport.PhotosPicker { /// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`. /// @@ -309,7 +238,6 @@ public extension Backport.PhotosPicker { // MARK: Multiple selection -@available(iOS 15, *) public extension Backport.PhotosPicker { /// Creates a Photos picker that selects a collection of `PhotosPickerItem`. /// @@ -370,7 +298,6 @@ public extension Backport.PhotosPicker { } } -@available(iOS 15, *) public extension Backport.PhotosPicker { /// Creates a Photos picker with its label generated from a localized string key that selects a collection of `PhotosPickerItem`. /// diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift index 7e68fe0..4fe7cbf 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift @@ -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: diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift index 9cd7438..7da88ff 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift @@ -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.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.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.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])) } diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerObserver.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerObserver.swift deleted file mode 100644 index a3d2915..0000000 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerObserver.swift +++ /dev/null @@ -1,48 +0,0 @@ -#if os(iOS) -import SwiftUI -import PhotosUI - -/// An observer used to observe changes on a `PHFetchResult` -internal final class PickerObserver: NSObject, ObservableObject, PHPhotoLibraryChangeObserver { - @Published - internal var result: PHFetchResult - - deinit { - PHPhotoLibrary.shared().unregisterChangeObserver(self) - } - - init(maxSelectionCount: Int?, filter: Backport.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 - private let playbackStyle: PHAsset.PlaybackStyle - - public var element: [Backport.PhotosPickerItem] { - filter { $0.playbackStyle == playbackStyle } - .map { Backport.PhotosPickerItem(itemIdentifier: $0.localIdentifier) } - } - - /// Instantiates a new instance with the specified result - public init(_ result: PHFetchResult, 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 diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift new file mode 100644 index 0000000..9bd674d --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift @@ -0,0 +1,239 @@ +//#if os(iOS) +//import SwiftUI +//import PhotosUI +// +//internal extension View { +// @ViewBuilder +// func _photoPicker( +// isPresented: Binding, +// selection: Binding<[Backport.PhotosPickerItem]>, +// filter: Backport.PHPickerFilter?, +// maxSelectionCount: Int?, +// selectionBehavior: Backport.PhotosPickerSelectionBehavior, +// preferredItemEncoding: Backport.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.PhotosPickerItem] +// +// let options: PHFetchOptions +// +// init(isPresented: Binding, selection: Binding<[Backport.PhotosPickerItem]>, filter: Backport.PHPickerFilter?, maxSelectionCount: Int?, selectionBehavior: Backport.PhotosPickerSelectionBehavior, preferredItemEncoding: Backport.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 { +// didSet { +// updateControllerLifecycle( +// from: oldValue.wrappedValue, +// to: isPresented.wrappedValue +// ) +// } +// } +// +// var selection: Binding<[Backport.PhotosPickerItem]> +// var configuration: PHPickerConfiguration +// +// init(isPresented: Binding, selection: Binding<[Backport.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.PhotosPickerItem] +// +// let filter: Backport.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 { +// didSet { +// updateControllerLifecycle( +// from: oldValue.wrappedValue, +// to: isPresented.wrappedValue +// ) +// } +// } +// +// var selection: Binding<[Backport.PhotosPickerItem]> +// var filter: Backport.PHPickerFilter? +// +// init(isPresented: Binding, selection: Binding<[Backport.PhotosPickerItem]>, filter: Backport.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 diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/PhotosPickerView.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/PhotosPickerView.swift new file mode 100644 index 0000000..e139407 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/PhotosPickerView.swift @@ -0,0 +1,93 @@ +#if os(iOS) +import SwiftUI +import PhotosUI + +internal struct PhotosPickerView: View { + @Environment(\.backportDismiss) private var dismiss + @Binding var selection: [Backport.PhotosPickerItem] + + let filter: Backport.PHPickerFilter? + let maxSelection: Int? + let selectionBehavior: Backport.PhotosPickerSelectionBehavior + let encoding: Backport.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(@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 diff --git a/Sources/SwiftUIBackports/Shared/Toolbar/Toolbar.swift b/Sources/SwiftUIBackports/Shared/Toolbar/Toolbar.swift new file mode 100644 index 0000000..0a9ac59 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/Toolbar/Toolbar.swift @@ -0,0 +1,122 @@ +#if os(iOS) +import SwiftUI + +public extension Backport { + 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(placement: Backport.ToolbarItemPlacement, @ViewBuilder content: () -> Content) { + self.placement = placement + self.content = AnyView(content()) + } + + public var body: some View { + content + } + } +} + +extension Collection where Element == Backport.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.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.ToolbarItem] { + [Backport.ToolbarItem(placement: .automatic, content: { EmptyView() })] + } + + static func buildBlock(_ content: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [content] + } + + static func buildIf(_ content: Backport.ToolbarItem?) -> [Backport.ToolbarItem?] { + [content].compactMap { $0 } + } + + static func buildEither(first: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [first] + } + + static func buildEither(second: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [second] + } + + static func buildLimitedAvailability(_ content: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [content] + } + + static func buildBlock(_ c0: Backport.ToolbarItem, _ c1: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [c0, c1] + } + + static func buildBlock(_ c0: Backport.ToolbarItem, _ c1: Backport.ToolbarItem, _ c2: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [c0, c1, c2] + } + + static func buildBlock(_ c0: Backport.ToolbarItem, _ c1: Backport.ToolbarItem, _ c2: Backport.ToolbarItem, _ c3: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [c0, c1, c2, c3] + } + + static func buildBlock(_ c0: Backport.ToolbarItem, _ c1: Backport.ToolbarItem, _ c2: Backport.ToolbarItem, _ c3: Backport.ToolbarItem, _ c4: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [c0, c1, c2, c3, c4] + } + + static func buildBlock(_ c0: Backport.ToolbarItem, _ c1: Backport.ToolbarItem, _ c2: Backport.ToolbarItem, _ c3: Backport.ToolbarItem, _ c4: Backport.ToolbarItem, _ c5: Backport.ToolbarItem) -> [Backport.ToolbarItem] { + [c0, c1, c2, c3, c4, c5] + } +} +#endif diff --git a/Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDismiss.swift b/Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDismiss.swift index 26ade0a..931d425 100644 --- a/Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDismiss.swift +++ b/Sources/SwiftUIBackports/iOS/PresentationDetents/InteractiveDismiss.swift @@ -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.Representable(isModal: isDisabled, onAttempt: nil)) - } else { - content - } + content.background(Backport.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.Representable(isModal: isDisabled, onAttempt: onAttempt)) - } else { - content - } + content.background(Backport.Representable(isModal: isDisabled, onAttempt: onAttempt)) #else content #endif