diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift index 0ba00d2..5061bf0 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker+View.swift @@ -2,7 +2,7 @@ import SwiftUI import PhotosUI -@available(iOS, deprecated: 16.0) +@available(iOS, introduced: 14, deprecated: 16.0) public extension Backport where Wrapped: View { /// Presents a Photos picker that selects a `PhotosPickerItem` from a given photo library. /// @@ -30,16 +30,13 @@ public extension Backport where Wrapped: View { } ) - return content.sheet(isPresented: isPresented) { - PhotosPickerView( - selection: binding, - filter: filter, - maxSelection: 1, - selectionBehavior: .default, - encoding: preferredItemEncoding, - library: photoLibrary - ) - } + return photosPicker( + isPresented: isPresented, + selection: binding, + matching: filter, + preferredItemEncoding: preferredItemEncoding, + photoLibrary: photoLibrary + ) } @@ -64,16 +61,82 @@ public extension Backport where Wrapped: View { 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 - ) - } + content._photoPicker( + isPresented: isPresented, + selection: selection, + filter: filter, + maxSelectionCount: maxSelectionCount, + selectionBehavior: selectionBehavior, + preferredItemEncoding: preferredItemEncoding, + library: photoLibrary + ) + } +} + +@available(iOS, introduced: 13, deprecated: 14.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. + func photosPicker( + isPresented: Binding, + selection: Binding.PhotosPickerItem?>, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic + ) -> some View { + let binding = Binding( + get: { + [selection.wrappedValue].compactMap { $0 } + }, + set: { newValue in + selection.wrappedValue = newValue.first + } + ) + + return content._photoPicker( + isPresented: isPresented, + selection: binding, + filter: nil, + maxSelectionCount: 1, + selectionBehavior: .default, + preferredItemEncoding: preferredItemEncoding, + library: .shared() + ) + } + + + /// 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. + /// - 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. + func photosPicker( + isPresented: Binding, + selection: Binding<[Backport.PhotosPickerItem]>, + maxSelectionCount: Int? = nil, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic + ) -> some View { + content._photoPicker( + isPresented: isPresented, + selection: selection, + filter: filter, + maxSelectionCount: maxSelectionCount, + selectionBehavior: .default, + preferredItemEncoding: preferredItemEncoding, + library: .shared() + ) } } diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift index 4e25a2b..cacfdac 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPicker.swift @@ -25,19 +25,20 @@ public extension Backport { } label: { label } - .backport.photosPicker( + ._photoPicker( isPresented: $isPresented, selection: $selection, + filter: filter, maxSelectionCount: maxSelection, selectionBehavior: selectionBehavior, - matching: filter, preferredItemEncoding: encoding, - photoLibrary: library + library: library ) } } } +@available(iOS, introduced: 13, deprecated: 16) public extension Backport.PhotosPicker { /// Creates a Photos picker that selects a `PhotosPickerItem`. /// @@ -69,7 +70,10 @@ public extension Backport.PhotosPicker { self.library = .shared() self.label = label() } +} +@available(iOS, introduced: 14, deprecated: 16) +public extension Backport.PhotosPicker { /// Creates 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. @@ -80,6 +84,7 @@ public extension Backport.PhotosPicker { /// - 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. /// - label: The view that describes the action of choosing an item from the photo library. + @available(iOS 14, *) init( selection: Binding, matching filter: Backport.PHPickerFilter? = nil, @@ -106,6 +111,7 @@ public extension Backport.PhotosPicker { // MARK: Single selection +@available(iOS, introduced: 13, deprecated: 16) public extension Backport.PhotosPicker { /// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`. /// @@ -179,6 +185,7 @@ public extension Backport.PhotosPicker { /// - 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. + @available(iOS 14, *) init( _ titleKey: LocalizedStringKey, selection: Binding, @@ -212,6 +219,7 @@ public extension Backport.PhotosPicker { /// - 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. + @available(iOS 14, *) init( _ title: S, selection: Binding, @@ -236,8 +244,9 @@ public extension Backport.PhotosPicker { } } -// MARK: Multiple selection +// MARK: Multiple selection (iOS 15+) +@available(iOS 15, *) public extension Backport.PhotosPicker { /// Creates a Photos picker that selects a collection of `PhotosPickerItem`. /// @@ -298,6 +307,7 @@ 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`. /// @@ -415,4 +425,90 @@ public extension Backport.PhotosPicker { self.label = Text(title) } } + +// MARK: Multiple selection (iOS 13+) + +@available(iOS 13, *) +public extension Backport.PhotosPicker { + /// Creates 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: + /// - 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. + /// - 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. + /// - label: The view that describes the action of choosing items from the photo library. + init( + selection: Binding<[Backport.PhotosPickerItem]>, + maxSelectionCount: Int? = nil, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic, + @ViewBuilder label: () -> Label + ) { + _selection = selection + self.filter = filter + self.maxSelection = maxSelectionCount + self.selectionBehavior = .default + self.encoding = preferredItemEncoding + self.library = .shared() + self.label = label() + } +} + +@available(iOS 13, *) +public extension Backport.PhotosPicker { + /// Creates a Photos picker with its label generated from a localized string key 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: + /// - titleKey: A localized string key that describes the purpose of showing the picker. + /// - 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. + /// - 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. + init( + _ titleKey: LocalizedStringKey, + selection: Binding<[Backport.PhotosPickerItem]>, + maxSelectionCount: Int? = nil, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic + ) { + _selection = selection + self.filter = filter + self.maxSelection = maxSelectionCount + self.selectionBehavior = .default + self.encoding = preferredItemEncoding + self.library = .shared() + self.label = Text(titleKey) + } + + /// Creates a Photos picker with its label generated from a string 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: + /// - title: A string that describes the purpose of showing the picker. + /// - 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. + /// - 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. + init( + _ title: S, + selection: Binding<[Backport.PhotosPickerItem]>, + maxSelectionCount: Int? = nil, + matching filter: Backport.PHPickerFilter? = nil, + preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic + ) where S: StringProtocol { + _selection = selection + self.filter = filter + self.maxSelection = maxSelectionCount + self.selectionBehavior = .default + self.encoding = preferredItemEncoding + self.library = .shared() + self.label = Text(title) + } +} #endif diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift index 4fe7cbf..32d7ba0 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift @@ -3,7 +3,7 @@ import SwiftUI import PhotosUI @available(iOS, deprecated: 16) -public extension Backport where Wrapped == Any { +public extension Backport { /// An item can contain multiple representations. Each representation has a corresponding content type. struct PhotosPickerItem: Equatable, Hashable { /// A policy that decides the encoding to use given a content type, if multiple encodings are available. @@ -19,10 +19,9 @@ public extension Backport where Wrapped == Any { /// Uses the most compatible encoding if possible, even if transcoding is required. public static let compatible: Self = .init(rawValue: 2) - @available(iOS, introduced: 14) - public var mode: PHPickerConfiguration.AssetRepresentationMode { + @available(iOS 14, *) + internal var mode: PHPickerConfiguration.AssetRepresentationMode { switch self { - case .automatic: return .automatic case .current: return .current case .compatible: return .compatible default: return .automatic diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift index 7da88ff..592e5a9 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift @@ -6,11 +6,25 @@ import CoreServices @available(iOS, introduced: 13, deprecated: 16) public extension Backport where Wrapped == Any { /// A filter that restricts which types of assets to show - struct PHPickerFilter: Equatable, Hashable { - internal let predicate: NSPredicate + struct PHPickerFilter { + internal let mediaTypes: [CFString] + internal let filter: Any? - internal init(predicate: NSPredicate) { - self.predicate = predicate + @available(iOS 13, *) + internal init(mediaTypes: [CFString]) { + self.mediaTypes = mediaTypes + self.filter = nil + } + + @available(iOS 14, *) + internal init(filter: PhotosUI.PHPickerFilter) { + self.filter = filter + self.mediaTypes = [] + } + + @available(iOS 14, *) + var photosFilter: PhotosUI.PHPickerFilter { + filter as! PhotosUI.PHPickerFilter } } } @@ -19,71 +33,90 @@ public extension Backport where Wrapped == Any { public extension Backport.PHPickerFilter { /// The filter for images. static var images: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image])) + if #available(iOS 14, *) { + return .init(filter: .images) + } else { + return .init(mediaTypes: [kUTTypeImage]) + } } /// The filter for videos. static var videos: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video])) + if #available(iOS 14, *) { + return .init(filter: .videos) + } else { + return .init(mediaTypes: [kUTTypeMovie]) + } } /// The filter for live photos. static var livePhotos: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive])) + if #available(iOS 14, *) { + return .init(filter: .livePhotos) + } else { + return .init(mediaTypes: [kUTTypeLivePhoto, kUTTypeMovie]) + } } +} - /// The filter for Depth Effect photos. - static var depthEffectPhotos: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoDepthEffect])) +@available(iOS 14, *) +public extension Backport.PHPickerFilter { + /// Returns a new filter formed by AND-ing the filters in a given array. + static func any(of subfilters: [Self]) -> Self { + .init(filter: .any(of: subfilters.map { $0.photosFilter })) } +} +@available(iOS 15, *) +public extension Backport.PHPickerFilter { /// The filter for panorama photos. static var panoramas: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoPanorama])) + .init(filter: .panoramas) } /// The filter for screenshots. static var screenshots: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoScreenshot])) + .init(filter: .screenshots) } /// The filter for Slow-Mo videos. static var slomoVideos: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.videoHighFrameRate])) + .init(filter: .slomoVideos) } /// The filter for time-lapse videos. static var timelapseVideos: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.videoTimelapse])) + .init(filter: .timelapseVideos) } /// Returns a new filter based on the asset playback style. static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self { - .init(predicate: NSPredicate(format: "(playbackStyle & %d) != 0", argumentArray: [playbackStyle.rawValue])) - } - - /// Returns a new filter formed by OR-ing the filters in a given array. - static func any(of subfilters: [Self]) -> Self { - .init(predicate: NSCompoundPredicate(orPredicateWithSubpredicates: subfilters.map { $0.predicate })) + .init(filter: .playbackStyle(playbackStyle)) } /// Returns a new filter formed by AND-ing the filters in a given array. static func all(of subfilters: [Self]) -> Self { - .init(predicate: NSCompoundPredicate(andPredicateWithSubpredicates: subfilters.map { $0.predicate })) + .init(filter: .all(of: subfilters.map { $0.photosFilter })) } /// Returns a new filter formed by negating the given filter. static func not(_ filter: Self) -> Self { - .init(predicate: NSCompoundPredicate(notPredicateWithSubpredicate: filter.predicate)) + .init(filter: .not(filter.photosFilter)) } } -@available(iOS 15.0, *) +@available(iOS 16, *) public extension Backport.PHPickerFilter { + /// The filter for Depth Effect photos. + static var depthEffectPhotos: Self { + .init(filter: .depthEffectPhotos) + } + /// The filter for Cinematic videos. static var cinematicVideos: Self { - .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.videoCinematic])) + .init(filter: .cinematicVideos) } + } #endif diff --git a/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift b/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift index 9bd674d..6f4ba79 100644 --- a/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift +++ b/Sources/SwiftUIBackports/Shared/PhotosPicker/UI/Hosts.swift @@ -1,239 +1,189 @@ -//#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 +#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) { + PhotosViewController( + isPresented: isPresented, + selection: selection, + filter: filter, + maxSelectionCount: maxSelectionCount, + selectionBehavior: selectionBehavior, + preferredItemEncoding: preferredItemEncoding, + library: library + ) + .ignoresSafeArea() + } + } else { + sheet(isPresented: isPresented) { + LegacyPhotosViewController( + isPresented: isPresented, + selection: selection, + filter: filter, + preferredItemEncoding: preferredItemEncoding + ) + .edgesIgnoringSafeArea(.all) + } + } + } +} + +@available(iOS 14, *) +private struct PhotosViewController: UIViewControllerRepresentable { + @Binding var isPresented: Bool + @Binding var selection: [Backport.PhotosPickerItem] + let configuration: PHPickerConfiguration + + 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 + + var configuration = PHPickerConfiguration(photoLibrary: library) + configuration.preferredAssetRepresentationMode = preferredItemEncoding.mode + configuration.selectionLimit = maxSelectionCount ?? 0 + configuration.filter = filter?.photosFilter + + if #available(iOS 15, *) { + configuration.selection = selectionBehavior.behaviour + } + + self.configuration = configuration + } + + func makeCoordinator() -> Coordinator { + Coordinator(isPresented: $isPresented, selection: $selection, configuration: configuration) + } + + func makeUIViewController(context: Context) -> UIViewController { + context.coordinator.controller + } + + func updateUIViewController(_ controller: UIViewController, context: Context) { + context.coordinator.isPresented = $isPresented + context.coordinator.selection = $selection + context.coordinator.configuration = configuration + } +} + +@available(iOS 14, *) +private extension PhotosViewController { + final class Coordinator: NSObject, PHPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate { + var isPresented: Binding + var selection: Binding<[Backport.PhotosPickerItem]> + var configuration: PHPickerConfiguration + + lazy var controller: PHPickerViewController = { + let controller = PHPickerViewController(configuration: configuration) + controller.presentationController?.delegate = self + controller.delegate = self + return controller + }() + + init(isPresented: Binding, selection: Binding<[Backport.PhotosPickerItem]>, configuration: PHPickerConfiguration) { + self.isPresented = isPresented + self.selection = selection + self.configuration = configuration + super.init() + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + isPresented.wrappedValue = false + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + isPresented.wrappedValue = false + } + } +} + +@available(iOS 13, *) +private struct LegacyPhotosViewController: UIViewControllerRepresentable { + @Binding var isPresented: Bool + @Binding var selection: [Backport.PhotosPickerItem] + let filter: Backport.PHPickerFilter? + var preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy + + func makeCoordinator() -> Coordinator { + Coordinator(isPresented: $isPresented, selection: $selection, filter: filter, preferredItemEncoding: preferredItemEncoding) + } + + func makeUIViewController(context: Context) -> UIViewController { + context.coordinator.controller + } + + + func updateUIViewController(_ controller: UIViewController, context: Context) { + context.coordinator.isPresented = $isPresented + context.coordinator.selection = $selection + context.coordinator.filter = filter + context.coordinator.preferredItemEncoding = preferredItemEncoding + } +} + +private extension LegacyPhotosViewController { + final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIAdaptivePresentationControllerDelegate { + var isPresented: Binding + var selection: Binding<[Backport.PhotosPickerItem]> + var filter: Backport.PHPickerFilter? + var preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy + + lazy var controller: UIImagePickerController = { + let controller = UIImagePickerController() + + if let filter { + controller.mediaTypes = filter.mediaTypes.map { String($0) } + } 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 + + switch preferredItemEncoding { + case .compatible: + controller.imageExportPreset = .compatible + controller.videoExportPreset = AVAssetExportPresetHighestQuality + case .current: + controller.imageExportPreset = .current + controller.videoExportPreset = AVAssetExportPresetPassthrough + default: + controller.imageExportPreset = .compatible + controller.videoExportPreset = AVAssetExportPresetHEVCHighestQuality + } + + return controller + }() + + init(isPresented: Binding, selection: Binding<[Backport.PhotosPickerItem]>, filter: Backport.PHPickerFilter?, preferredItemEncoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy) { + self.isPresented = isPresented + self.selection = selection + self.filter = filter + self.preferredItemEncoding = preferredItemEncoding + super.init() + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + isPresented.wrappedValue = false + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + isPresented.wrappedValue = false + } + } +} +#endif