Introduces rudimentary Toolbar support for iOS 13

This commit is contained in:
Shaps Benkau 2023-01-30 22:09:25 +00:00
parent 0ad99191d9
commit cce0b59d3d
18 changed files with 1035 additions and 496 deletions

View File

@ -84,6 +84,14 @@ extension View {
func inspect<ViewType: PlatformView>(selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void) -> some View { func inspect<ViewType: PlatformView>(selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void) -> some View {
inject(InspectionView(selector: selector, customize: customize)) inject(InspectionView(selector: selector, customize: customize))
} }
func controller(_ customize: @escaping (UIViewController?) -> Void) -> some View {
inspect { inspector in
inspector.sourceController.view
} customize: { view in
customize(view.parentController)
}
}
} }
private struct InspectionView<ViewType: PlatformView>: View { private struct InspectionView<ViewType: PlatformView>: View {

View File

@ -0,0 +1,40 @@
import Photos
/// Represents a `PHFetchResult` that can be used as a `RandomAccessCollection` in a SwiftUI view such as `List`, `ForEach`, etc...
internal struct MediaResults<Result>: RandomAccessCollection where Result: PHObject {
/// Represents the underlying results
public private(set) var result: PHFetchResult<Result>
/// Instantiates a new instance with the specified result
public init(_ result: PHFetchResult<Result>) {
self.result = result
}
public var startIndex: Int { 0 }
public var endIndex: Int { result.count }
public subscript(position: Int) -> Result { result.object(at: position) }
}
/// An observer used to observe changes on a `PHFetchResult`
internal final class ResultsObserver<Result>: NSObject, ObservableObject, PHPhotoLibraryChangeObserver where Result: PHObject {
@Published
internal var result: PHFetchResult<Result>
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
init(result: PHFetchResult<Result>) {
self.result = result
super.init()
PHPhotoLibrary.shared().register(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
result = changeInstance.changeDetails(for: result)?.fetchResultAfterChanges ?? result
}
}

View File

@ -0,0 +1,63 @@
import Foundation
import Photos
internal extension PHFetchOptions {
/// The maximum number of objects to include in the fetch result.
func fetchLimit(_ fetchLimit: Int) -> Self {
self.fetchLimit = fetchLimit
return self
}
/// The set of source types for which to include assets in the fetch result.
func includeAssetSourceTypes(_ sourceTypes: PHAssetSourceType) -> Self {
self.includeAssetSourceTypes = sourceTypes
return self
}
/// Determines whether the fetch result includes all assets from burst photo sequences.
func includeHiddenAssets(_ includeHiddenAssets: Bool) -> Self {
self.includeHiddenAssets = includeHiddenAssets
return self
}
/// Determines whether the fetch result includes all assets from burst photo sequences.
func includeAllBurstAssets(_ includeAllBurstAssets: Bool) -> Self {
self.includeAllBurstAssets = includeAllBurstAssets
return self
}
/// Appends the specified sort to the current list of descriptors.
/// - Parameters:
/// - keyPath: The keyPath sort by
/// - ascending: If true, the results will be in ascending order
func sort<Root, Value>(by keyPath: KeyPath<Root, Value>, ascending: Bool = true) -> Self {
var descriptors = sortDescriptors ?? []
descriptors.append(NSSortDescriptor(keyPath: keyPath, ascending: ascending))
self.sortDescriptors = descriptors
return self
}
/// Appends the specified predicate to the current list of predicates.
/// - Parameter predicate: The predicate to append
func filter(_ predicate: NSPredicate) -> Self {
let predicates = [predicate, self.predicate].compactMap { $0 }
self.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
return self
}
/// Replaces the sort descriptors.
/// - Parameter sortDescriptors: The descriptors to sort results
func sortDescriptors(_ sortDescriptors: [NSSortDescriptor]) -> Self {
self.sortDescriptors = sortDescriptors
return self
}
/// Replaces the predicate.
/// - Parameter predicate: The predicate to filter results
func predicate(_ predicate: NSPredicate) -> Self {
self.predicate = predicate
return self
}
}

View File

@ -0,0 +1,6 @@
import SwiftUI
import Photos
extension PHObject: Identifiable {
public var id: String { localIdentifier }
}

View File

@ -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
}
}

View File

@ -0,0 +1,95 @@
import Photos
import SwiftUI
/// Fetches a set of asset collections from the `Photos` framework
@propertyWrapper
internal struct FetchAssetCollection<Result>: DynamicProperty where Result: PHAssetCollection {
@ObservedObject
internal private(set) var observer: ResultsObserver<Result>
/// Represents the results of the fetch
public var wrappedValue: MediaResults<Result> {
get { MediaResults(observer.result) }
set { observer.result = newValue.result }
}
}
internal extension FetchAssetCollection {
/// Instantiates a fetch with an existing `PHFetchResult<Result>` instance
init(result: PHFetchResult<Result>) {
self.init(observer: ResultsObserver(result: result))
}
/// Instantiates a fetch with a custom `PHFetchOptions` instance
init(_ options: PHFetchOptions? = nil) {
let result = PHAssetCollection.fetchTopLevelUserCollections(with: options)
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
}
/// Instantiates a fetch with a filter and sort options
/// - Parameters:
/// - filter: The predicate to apply when filtering the results
/// - sort: The keyPaths to apply when sorting the results
init<Value>(filter: NSPredicate? = nil,
sort: [(KeyPath<PHAssetCollection, Value>, ascending: Bool)]) {
let options = PHFetchOptions()
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
options.predicate = filter
self.init(options)
}
}
internal extension FetchAssetCollection {
/// Instantiates a fetch for the specified album type and subtype
/// - Parameters:
/// - album: The album type to fetch
/// - kind: The album subtype to fetch
/// - options: Any additional options to apply to the fetch
init(album: PHAssetCollectionType,
kind: PHAssetCollectionSubtype = .any,
options: PHFetchOptions? = nil) {
let result = PHAssetCollection.fetchAssetCollections(with: album, subtype: kind, options: options)
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
}
/// Instantiates a fetch for the specified album type and subtype
/// - Parameters:
/// - album: The album type to fetch
/// - kind: The album subtype to fetch
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
init(album: PHAssetCollectionType,
kind: PHAssetCollectionSubtype = .any,
fetchLimit: Int = 0,
filter: NSPredicate? = nil) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.predicate = filter
self.init(album: album, kind: kind, options: options)
}
/// Instantiates a fetch for the specified album type and subtype
/// - Parameters:
/// - album: The album type to fetch
/// - kind: The album subtype to fetch
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
/// - sort: The keyPaths to apply when sorting the results
init<Value>(album: PHAssetCollectionType,
kind: PHAssetCollectionSubtype = .any,
fetchLimit: Int = 0,
filter: NSPredicate? = nil,
sort: [(KeyPath<PHAssetCollection, Value>, ascending: Bool)]) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
options.predicate = filter
self.init(album: album, kind: kind, options: options)
}
}

View File

@ -0,0 +1,116 @@
import Photos
import SwiftUI
/// Fetches a set of assets from the `Photos` framework
@propertyWrapper
internal struct FetchAssetList<Result>: DynamicProperty where Result: PHAsset {
@ObservedObject
internal private(set) var observer: ResultsObserver<Result>
/// Represents the results of the fetch
public var wrappedValue: MediaResults<Result> {
get { MediaResults(observer.result) }
set { observer.result = newValue.result }
}
}
internal extension FetchAssetList {
/// Instantiates a fetch with an existing `PHFetchResult<Result>` instance
init(_ result: PHFetchResult<PHAsset>) {
observer = ResultsObserver(result: result as! PHFetchResult<Result>)
}
/// Instantiates a fetch with a custom `PHFetchOptions` instance
init(_ options: PHFetchOptions? = nil) {
let result = PHAsset.fetchAssets(with: options)
observer = ResultsObserver(result: result as! PHFetchResult<Result>)
}
/// Instantiates a fetch by applying the specified sort and filter options
/// - Parameters:
/// - filter: The predicate to apply when filtering the results
/// - sort: The keyPaths to apply when sorting the results
/// - sourceTypes: The sourceTypes to include in the results
/// - includeAllBurstAssets: If true, burst assets will be included in the results
/// - includeHiddenAssets: If true, hidden assets will be included in the results
init<Value>(filter: NSPredicate? = nil,
sort: [(KeyPath<PHAsset, Value>, ascending: Bool)],
sourceTypes: PHAssetSourceType = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced],
includeAllBurstAssets: Bool = false,
includeHiddenAssets: Bool = false) {
let options = PHFetchOptions()
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
options.predicate = filter
options.includeAssetSourceTypes = sourceTypes
options.includeHiddenAssets = includeHiddenAssets
options.includeAllBurstAssets = includeAllBurstAssets
self.init(options)
}
}
internal extension FetchAssetList {
/// Fetches all assets in the specified collection
/// - Parameters:
/// - collection: The asset collection to filter by
/// - options: Any additional options to apply to the request
init(in collection: PHAssetCollection,
options: PHFetchOptions? = nil) {
let result = PHAsset.fetchAssets(in: collection, options: options)
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
}
/// Fetches all assets in the specified collection
/// - Parameters:
/// - collection: The asset collection to filter by
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
/// - sourceTypes: The sourceTypes to include in the results
/// - includeAllBurstAssets: If true, burst assets will be included in the results
/// - includeHiddenAssets: If true, hidden assets will be included in the results
init(in collection: PHAssetCollection,
fetchLimit: Int = 0,
filter: NSPredicate? = nil,
sourceTypes: PHAssetSourceType = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced],
includeAllBurstAssets: Bool = false,
includeHiddenAssets: Bool = false) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.predicate = filter
options.includeAssetSourceTypes = sourceTypes
options.includeHiddenAssets = includeHiddenAssets
options.includeAllBurstAssets = includeAllBurstAssets
self.init(in: collection, options: options)
}
/// Fetches all assets in the specified collection
/// - Parameters:
/// - collection: The asset collection to filter by
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
/// - sort: The keyPaths to apply when sorting the results
/// - sourceTypes: The sourceTypes to include in the results
/// - includeAllBurstAssets: If true, burst assets will be included in the results
/// - includeHiddenAssets: If true, hidden assets will be included in the results
init<Value>(in collection: PHAssetCollection,
fetchLimit: Int = 0,
filter: NSPredicate? = nil,
sort: [(KeyPath<PHAsset, Value>, ascending: Bool)],
sourceTypes: PHAssetSourceType = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced],
includeAllBurstAssets: Bool = false,
includeHiddenAssets: Bool = false) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
options.predicate = filter
options.includeAssetSourceTypes = sourceTypes
options.includeHiddenAssets = includeHiddenAssets
options.includeAllBurstAssets = includeAllBurstAssets
self.init(in: collection, options: options)
}
}

View File

@ -0,0 +1,105 @@
import Photos
import SwiftUI
@propertyWrapper
internal struct FetchCollectionList<Result>: DynamicProperty where Result: PHCollectionList {
@ObservedObject
internal private(set) var observer: ResultsObserver<Result>
public var wrappedValue: MediaResults<Result> {
get { MediaResults(observer.result) }
set { observer.result = newValue.result }
}
}
internal extension FetchCollectionList {
/// Instantiates a fetch with an existing `PHFetchResult<Result>` instance
init(_ result: PHFetchResult<PHAsset>) {
observer = ResultsObserver(result: result as! PHFetchResult<Result>)
}
/// Instantiates a fetch with a custom `PHFetchOptions` instance
init(_ options: PHFetchOptions?) {
let result = PHCollectionList.fetchTopLevelUserCollections(with: options)
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
}
/// Instantiates a fetch by applying the specified sort and filter options
/// - Parameters:
/// - filter: The predicate to apply when filtering the results
init(filter: NSPredicate? = nil) {
let options = PHFetchOptions()
options.predicate = filter
self.init(options)
}
/// Instantiates a fetch by applying the specified sort and filter options
/// - Parameters:
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
/// - sort: The keyPaths to apply when sorting the results
init<Value>(fetchLimit: Int = 0,
filter: NSPredicate? = nil,
sort: [(KeyPath<PHCollectionList, Value>, ascending: Bool)]) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
options.predicate = filter
self.init(options)
}
}
internal extension FetchCollectionList {
/// Fetches all lists of the specified type and subtyle
/// - Parameters:
/// - list: The list type to filter by
/// - kind: The list subtype to filter by
/// - options: Any additional options to apply to the request
init(list: PHCollectionListType,
kind: PHCollectionListSubtype = .any,
options: PHFetchOptions? = nil) {
let result = PHCollectionList.fetchCollectionLists(with: list, subtype: kind, options: options)
self.init(observer: ResultsObserver(result: result as! PHFetchResult<Result>))
}
/// Fetches all lists of the specified type and subtyle
/// - Parameters:
/// - list: The list type to filter by
/// - kind: The list subtype to filter by
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
init(list: PHCollectionListType,
kind: PHCollectionListSubtype = .any,
fetchLimit: Int = 0,
filter: NSPredicate) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.predicate = filter
self.init(list: list, kind: kind, options: options)
}
/// Fetches all lists of the specified type and subtyle
/// - Parameters:
/// - list: The list type to filter by
/// - kind: The list subtype to filter by
/// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results
/// - filter: The predicate to apply when filtering the results
/// - sort: The keyPaths to apply when sorting the results
init<Value>(list: PHCollectionListType,
kind: PHCollectionListSubtype = .any,
fetchLimit: Int = 0,
filter: NSPredicate? = nil,
sort: [(KeyPath<PHCollectionList, Value>, ascending: Bool)]) {
let options = PHFetchOptions()
options.fetchLimit = fetchLimit
options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) }
options.predicate = filter
self.init(list: list, kind: kind, options: options)
}
}

View File

@ -1,231 +0,0 @@
#if os(iOS)
import SwiftUI
import PhotosUI
internal extension View {
@ViewBuilder
func _photoPicker(
isPresented: Binding<Bool>,
selection: Binding<[Backport<Any>.PhotosPickerItem]>,
filter: Backport<Any>.PHPickerFilter?,
maxSelectionCount: Int?,
selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior,
preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy,
library: PHPhotoLibrary
) -> some View {
if #available(iOS 14, *) {
sheet(isPresented: isPresented) {
}
// var config = PHPickerConfiguration(photoLibrary: library)
// config.preferredAssetRepresentationMode = preferredItemEncoding.mode
// config.selectionLimit = maxSelectionCount ?? 0
// // config.filter = filter.filter
//
// if #available(iOS 15, *) {
// config.selection = selectionBehavior.behaviour
// }
//
// PickerViewController(
// isPresented: isPresented,
// selection: selection,
// configuration: config
// )
} else {
backport.background {
LegacyPhotosViewController(isPresented: isPresented, selection: selection, filter: filter)
}
}
}
}
@available(iOS 14, *)
private struct PhotosViewController: UIViewControllerRepresentable {
@Binding var isPresented: Bool
@Binding var selection: [Backport<Any>.PhotosPickerItem]
let configuration: PHPickerConfiguration
func makeUIViewController(context: Context) -> Representable {
Representable(
isPresented: $isPresented,
selection: $selection,
configuration: configuration
)
}
func updateUIViewController(_ controller: Representable, context: Context) {
controller.selection = $selection
controller.configuration = configuration
}
}
@available(iOS 14, *)
private extension PhotosViewController {
final class Representable: UIViewController, PHPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
private weak var controller: PHPickerViewController?
var isPresented: Binding<Bool> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: isPresented.wrappedValue
)
}
}
var selection: Binding<[Backport<Any>.PhotosPickerItem]>
var configuration: PHPickerConfiguration
init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, configuration: PHPickerConfiguration) {
self.isPresented = isPresented
self.selection = selection
self.configuration = configuration
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateControllerLifecycle(from oldValue: Bool, to newValue: Bool) {
switch (oldValue, newValue) {
case (false, true):
presentController()
case (true, false):
dismissController()
case (true, true), (false, false):
break
}
}
private func presentController() {
let controller = PHPickerViewController(configuration: configuration)
controller.presentationController?.delegate = self
controller.delegate = self
present(controller, animated: true)
self.controller = controller
}
private func dismissController() {
isPresented.wrappedValue = false
guard let controller else { return }
controller.presentedViewController?.dismiss(animated: true)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
print(results)
dismissController()
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
dismissController()
}
}
}
@available(iOS 13, *)
private struct LegacyPhotosViewController: UIViewControllerRepresentable {
@Binding var isPresented: Bool
@Binding var selection: [Backport<Any>.PhotosPickerItem]
let filter: Backport<Any>.PHPickerFilter?
func makeUIViewController(context: Context) -> Representable {
Representable(
isPresented: $isPresented,
selection: $selection,
filter: filter
)
}
func updateUIViewController(_ controller: Representable, context: Context) {
controller.selection = $selection
controller.filter = filter
}
}
@available(iOS 13, *)
private extension LegacyPhotosViewController {
final class Representable: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate {
private weak var controller: UIImagePickerController?
var isPresented: Binding<Bool> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: isPresented.wrappedValue
)
}
}
var selection: Binding<[Backport<Any>.PhotosPickerItem]>
var filter: Backport<Any>.PHPickerFilter?
init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, filter: Backport<Any>.PHPickerFilter?) {
self.isPresented = isPresented
self.selection = selection
self.filter = filter
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateControllerLifecycle(from oldValue: Bool, to newValue: Bool) {
switch (oldValue, newValue) {
case (false, true):
presentController()
case (true, false):
dismissController()
case (true, true), (false, false):
break
}
}
private func presentController() {
let controller = UIImagePickerController()
if let filter {
controller.mediaTypes = filter.mediaTypes
} else if let types = UIImagePickerController.availableMediaTypes(for: .photoLibrary) {
controller.mediaTypes = types
}
controller.allowsEditing = false
controller.sourceType = .photoLibrary
controller.videoQuality = .typeHigh
controller.presentationController?.delegate = self
controller.delegate = self
present(controller, animated: true)
self.controller = controller
}
private func dismissController() {
isPresented.wrappedValue = false
guard let controller else { return }
controller.presentedViewController?.dismiss(animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
print("TBD")
print(info)
dismissController()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismissController()
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
dismissController()
}
}
}
#endif

View File

@ -1,96 +1,80 @@
//#if os(iOS) #if os(iOS)
//import SwiftUI import SwiftUI
//import PhotosUI import PhotosUI
//
//@available(iOS, deprecated: 16.0) @available(iOS, deprecated: 16.0)
//extension Backport where Wrapped: View { public extension Backport where Wrapped: View {
// /// Presents a Photos picker that selects a `PhotosPickerItem`. /// Presents a Photos picker that selects a `PhotosPickerItem` from a given photo library.
// /// ///
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed. /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
// /// ///
// /// - Parameters: /// - Parameters:
// /// - isPresented: The binding to whether the Photos picker should be shown. /// - isPresented: The binding to whether the Photos picker should be shown.
// /// - selection: The item being shown and selected in the Photos picker. /// - selection: The item being shown and selected in the Photos picker.
// /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown. /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown.
// /// - preferredItemEncoding: The encoding disambiguation policy of the selected item. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used. /// - preferredItemEncoding: The encoding disambiguation policy of the selected item. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used.
// public func photosPicker( /// - photoLibrary: The photo library to choose from.
// isPresented: Binding<Bool>, func photosPicker(
// selection: Binding<PhotosPickerItem?>, isPresented: Binding<Bool>,
// matching filter: PHPickerFilter? = nil, selection: Binding<Backport<Any>.PhotosPickerItem?>,
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic matching filter: Backport<Any>.PHPickerFilter? = nil,
// ) -> some View { preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
// photoLibrary: PHPhotoLibrary = .shared()
// } ) -> some View {
// let binding = Binding(
// get: {
// /// Presents a Photos picker that selects a collection of `PhotosPickerItem`. [selection.wrappedValue].compactMap { $0 }
// /// },
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed. set: { newValue in
// /// selection.wrappedValue = newValue.first
// /// - Parameters: }
// /// - isPresented: The binding to whether the Photos picker should be shown. )
// /// - selection: All items being shown and selected in the Photos picker.
// /// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system. return content.sheet(isPresented: isPresented) {
// /// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`. PhotosPickerView(
// /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown. selection: binding,
// /// - preferredItemEncoding: The encoding disambiguation policy of selected items. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used. filter: filter,
// public func photosPicker( maxSelection: 1,
// isPresented: Binding<Bool>, selectionBehavior: .default,
// selection: Binding<[PhotosPickerItem]>, encoding: preferredItemEncoding,
// maxSelectionCount: Int? = nil, library: photoLibrary
// selectionBehavior: PhotosPickerSelectionBehavior = .default, )
// matching filter: PHPickerFilter? = nil, }
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic }
// ) -> some View {
//
// } /// Presents a Photos picker that selects a collection of `PhotosPickerItem` from a given photo library.
// ///
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed.
// /// Presents a Photos picker that selects a `PhotosPickerItem` from a given photo library. ///
// /// /// - Parameters:
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed. /// - isPresented: The binding to whether the Photos picker should be shown.
// /// /// - selection: All items being shown and selected in the Photos picker.
// /// - Parameters: /// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system.
// /// - isPresented: The binding to whether the Photos picker should be shown. /// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`.
// /// - selection: The item being shown and selected in the Photos picker. /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown.
// /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown. /// - preferredItemEncoding: The encoding disambiguation policy of selected items. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used.
// /// - preferredItemEncoding: The encoding disambiguation policy of the selected item. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used. /// - photoLibrary: The photo library to choose from.
// /// - photoLibrary: The photo library to choose from. func photosPicker(
// public func photosPicker( isPresented: Binding<Bool>,
// isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>,
// selection: Binding<PhotosPickerItem?>, maxSelectionCount: Int? = nil,
// matching filter: PHPickerFilter? = nil, selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior = .default,
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic, matching filter: Backport<Any>.PHPickerFilter? = nil,
// photoLibrary: PHPhotoLibrary preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
// ) -> some View { photoLibrary: PHPhotoLibrary = .shared()
// ) -> some View {
// } content.sheet(isPresented: isPresented) {
// PhotosPickerView(
// selection: selection,
// /// Presents a Photos picker that selects a collection of `PhotosPickerItem` from a given photo library. filter: filter,
// /// maxSelection: maxSelectionCount,
// /// The user explicitly grants access only to items they choose, so photo library access authorization is not needed. selectionBehavior: selectionBehavior,
// /// encoding: preferredItemEncoding,
// /// - Parameters: library: photoLibrary
// /// - isPresented: The binding to whether the Photos picker should be shown. )
// /// - selection: All items being shown and selected in the Photos picker. }
// /// - maxSelectionCount: The maximum number of items that can be selected. Default is `nil`. Setting it to `nil` means maximum supported by the system. }
// /// - selectionBehavior: The selection behavior of the Photos picker. Default is `.default`. }
// /// - filter: Types of items that can be shown. Default is `nil`. Setting it to `nil` means all supported types can be shown.
// /// - preferredItemEncoding: The encoding disambiguation policy of selected items. Default is `.automatic`. Setting it to `.automatic` means the best encoding determined by the system will be used. #endif
// /// - photoLibrary: The photo library to choose from.
// public func photosPicker(
// isPresented: Binding<Bool>,
// selection: Binding<[PhotosPickerItem]>,
// maxSelectionCount: Int? = nil,
// selectionBehavior: PhotosPickerSelectionBehavior = .default,
// matching filter: PHPickerFilter? = nil,
// preferredItemEncoding: PhotosPickerItem.EncodingDisambiguationPolicy = .automatic,
// photoLibrary: PHPhotoLibrary
// ) -> some View {
//
// }
//
//}
//
//#endif

View File

@ -25,89 +25,19 @@ public extension Backport<Any> {
} label: { } label: {
label label
} }
._photoPicker( .backport.photosPicker(
isPresented: $isPresented, isPresented: $isPresented,
selection: $selection, selection: $selection,
filter: filter,
maxSelectionCount: maxSelection, maxSelectionCount: maxSelection,
selectionBehavior: selectionBehavior, selectionBehavior: selectionBehavior,
matching: filter,
preferredItemEncoding: encoding, preferredItemEncoding: encoding,
library: library photoLibrary: library
) )
} }
} }
} }
@available(iOS, introduced: 13, deprecated: 14)
public extension Backport<Any>.PhotosPicker {
init(
selection: Binding<Backport.PhotosPickerItem?>,
matching filter: Backport.PHPickerFilter? = nil,
@ViewBuilder label: () -> Label
) {
_selection = .init(
get: {
[selection.wrappedValue].compactMap { $0 }
},
set: { newValue in
selection.wrappedValue = newValue.first
}
)
self.filter = filter
self.maxSelection = 1
self.selectionBehavior = .default
self.encoding = .automatic
self.library = .shared()
self.label = label()
}
}
@available(iOS, introduced: 13, deprecated: 14)
public extension Backport<Any>.PhotosPicker<Text> {
init(
_ titleKey: LocalizedStringKey,
selection: Binding<Backport.PhotosPickerItem?>,
matching filter: Backport.PHPickerFilter? = nil
) {
_selection = .init(
get: {
[selection.wrappedValue].compactMap { $0 }
},
set: { newValue in
selection.wrappedValue = newValue.first
}
)
self.filter = filter
self.maxSelection = 1
self.selectionBehavior = .default
self.encoding = .automatic
self.library = .shared()
self.label = Text(titleKey)
}
init<S>(
_ title: S,
selection: Binding<Backport.PhotosPickerItem?>,
matching filter: Backport.PHPickerFilter? = nil
) where S: StringProtocol {
_selection = .init(
get: {
[selection.wrappedValue].compactMap { $0 }
},
set: { newValue in
selection.wrappedValue = newValue.first
}
)
self.filter = filter
self.maxSelection = 1
self.selectionBehavior = .default
self.encoding = .automatic
self.library = .shared()
self.label = Text(title)
}
}
@available(iOS 14, *)
public extension Backport<Any>.PhotosPicker { public extension Backport<Any>.PhotosPicker {
/// Creates a Photos picker that selects a `PhotosPickerItem`. /// Creates a Photos picker that selects a `PhotosPickerItem`.
/// ///
@ -176,7 +106,6 @@ public extension Backport<Any>.PhotosPicker {
// MARK: Single selection // MARK: Single selection
@available(iOS 14, *)
public extension Backport<Any>.PhotosPicker<Text> { public extension Backport<Any>.PhotosPicker<Text> {
/// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`. /// Creates a Photos picker with its label generated from a localized string key that selects a `PhotosPickerItem`.
/// ///
@ -309,7 +238,6 @@ public extension Backport<Any>.PhotosPicker<Text> {
// MARK: Multiple selection // MARK: Multiple selection
@available(iOS 15, *)
public extension Backport<Any>.PhotosPicker { public extension Backport<Any>.PhotosPicker {
/// Creates a Photos picker that selects a collection of `PhotosPickerItem`. /// Creates a Photos picker that selects a collection of `PhotosPickerItem`.
/// ///
@ -370,7 +298,6 @@ public extension Backport<Any>.PhotosPicker {
} }
} }
@available(iOS 15, *)
public extension Backport<Any>.PhotosPicker<Text> { public extension Backport<Any>.PhotosPicker<Text> {
/// Creates a Photos picker with its label generated from a localized string key that selects a collection of `PhotosPickerItem`. /// Creates a Photos picker with its label generated from a localized string key that selects a collection of `PhotosPickerItem`.
/// ///

View File

@ -36,14 +36,6 @@ public extension Backport where Wrapped == Any {
/// All supported content types of the item, in order of most preferred to least preferred. /// All supported content types of the item, in order of most preferred to least preferred.
public let supportedContentTypes: [String] public let supportedContentTypes: [String]
// internal let provider: NSItemProvider?
//
// internal init(itemIdentifier: String, provider: NSItemProvider) {
// self.itemIdentifier = itemIdentifier
// self.provider = provider
// supportedContentTypes = provider.registeredTypeIdentifiers
// }
/// Creates an item without any representation using an identifier. /// Creates an item without any representation using an identifier.
/// ///
/// - Parameters: /// - Parameters:

View File

@ -8,17 +8,9 @@ public extension Backport where Wrapped == Any {
/// A filter that restricts which types of assets to show /// A filter that restricts which types of assets to show
struct PHPickerFilter: Equatable, Hashable { struct PHPickerFilter: Equatable, Hashable {
internal let predicate: NSPredicate internal let predicate: NSPredicate
// this enables us to support iOS 13 for images vs videos
internal let mediaTypes: [String]
internal init(mediaTypes: [String]) {
self.predicate = .init(value: true)
self.mediaTypes = mediaTypes
}
internal init(predicate: NSPredicate) { internal init(predicate: NSPredicate) {
self.predicate = predicate self.predicate = predicate
self.mediaTypes = []
} }
} }
} }
@ -27,34 +19,19 @@ public extension Backport where Wrapped == Any {
public extension Backport<Any>.PHPickerFilter { public extension Backport<Any>.PHPickerFilter {
/// The filter for images. /// The filter for images.
static var images: Self { static var images: Self {
if #available(iOS 14, *) { .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image]))
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.image]))
} else {
return .init(mediaTypes: [String(kUTTypeImage)])
}
} }
/// The filter for videos. /// The filter for videos.
static var videos: Self { static var videos: Self {
if #available(iOS 14, *) { .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video]))
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaType.video]))
} else {
return .init(mediaTypes: [String(kUTTypeMovie)])
}
} }
/// The filter for live photos. /// The filter for live photos.
static var livePhotos: Self { static var livePhotos: Self {
if #available(iOS 14, *) { .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive]))
return .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoLive]))
} else {
return .init(mediaTypes: [String(kUTTypeMovie), String(kUTTypeLivePhoto)])
}
} }
}
@available(iOS 14, *)
public extension Backport<Any>.PHPickerFilter {
/// The filter for Depth Effect photos. /// The filter for Depth Effect photos.
static var depthEffectPhotos: Self { static var depthEffectPhotos: Self {
.init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoDepthEffect])) .init(predicate: NSPredicate(format: "(mediaSubtypes & %d) != 0", argumentArray: [PHAssetMediaSubtype.photoDepthEffect]))
@ -81,7 +58,6 @@ public extension Backport<Any>.PHPickerFilter {
} }
/// Returns a new filter based on the asset playback style. /// Returns a new filter based on the asset playback style.
#warning("NEEDS TESTING!")
static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self { static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self {
.init(predicate: NSPredicate(format: "(playbackStyle & %d) != 0", argumentArray: [playbackStyle.rawValue])) .init(predicate: NSPredicate(format: "(playbackStyle & %d) != 0", argumentArray: [playbackStyle.rawValue]))
} }

View File

@ -1,48 +0,0 @@
#if os(iOS)
import SwiftUI
import PhotosUI
/// An observer used to observe changes on a `PHFetchResult`
internal final class PickerObserver<Result>: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
@Published
internal var result: PHFetchResult<PHAsset>
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
init(maxSelectionCount: Int?, filter: Backport<Any>.PHPickerFilter) {
let options = PHFetchOptions()
options.includeAssetSourceTypes = [.typeCloudShared, .typeUserLibrary, .typeiTunesSynced]
result = PHAsset.fetchAssets(with: options)
super.init()
PHPhotoLibrary.shared().register(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
result = changeInstance.changeDetails(for: result)?.fetchResultAfterChanges ?? result
}
}
/// Represents a `PHFetchResult` that can be used as a `RandomAccessCollection` in a SwiftUI view such as `List`, `ForEach`, etc...
internal struct MediaResults: RandomAccessCollection {
/// Represents the underlying results
private let result: PHFetchResult<PHAsset>
private let playbackStyle: PHAsset.PlaybackStyle
public var element: [Backport<Any>.PhotosPickerItem] {
filter { $0.playbackStyle == playbackStyle }
.map { Backport<Any>.PhotosPickerItem(itemIdentifier: $0.localIdentifier) }
}
/// Instantiates a new instance with the specified result
public init(_ result: PHFetchResult<PHAsset>, playbackStyle: PHAsset.PlaybackStyle) {
self.result = result
self.playbackStyle = playbackStyle
}
public var startIndex: Int { 0 }
public var endIndex: Int { result.count }
public subscript(position: Int) -> PHAsset { result.object(at: position) }
}
#endif

View File

@ -0,0 +1,239 @@
//#if os(iOS)
//import SwiftUI
//import PhotosUI
//
//internal extension View {
// @ViewBuilder
// func _photoPicker(
// isPresented: Binding<Bool>,
// selection: Binding<[Backport<Any>.PhotosPickerItem]>,
// filter: Backport<Any>.PHPickerFilter?,
// maxSelectionCount: Int?,
// selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior,
// preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy,
// library: PHPhotoLibrary
// ) -> some View {
// if #available(iOS 14, *) {
// backport.background {
// PhotosViewController(
// isPresented: isPresented,
// selection: selection,
// filter: filter,
// maxSelectionCount: maxSelectionCount,
// selectionBehavior: selectionBehavior,
// preferredItemEncoding: preferredItemEncoding,
// library: library
// )
// }
// } else {
// backport.background {
// LegacyPhotosViewController(isPresented: isPresented, selection: selection, filter: filter)
// }
// }
// }
//}
//
//@available(iOS 14, *)
//private struct PhotosViewController: UIViewControllerRepresentable {
// @Binding var isPresented: Bool
// @Binding var selection: [Backport<Any>.PhotosPickerItem]
//
// let options: PHFetchOptions
//
// init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, filter: Backport<Any>.PHPickerFilter?, maxSelectionCount: Int?, selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior, preferredItemEncoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy, library: PHPhotoLibrary) {
// _isPresented = isPresented
// _selection = selection
//
// options = PHFetchOptions()
// options.predicate = filter?.predicate
//
// if #available(iOS 15, *) {
// configuration.selection = selectionBehavior.behaviour
// }
//
// self.configuration = configuration
// }
//
// func makeUIViewController(context: Context) -> Representable {
// Representable(
// isPresented: $isPresented,
// selection: $selection,
// configuration: configuration
// )
// }
//
//
// func updateUIViewController(_ controller: Representable, context: Context) {
// controller.selection = $selection
// controller.configuration = configuration
// }
//}
//
//@available(iOS 14, *)
//private extension PhotosViewController {
// final class Representable: UIViewController, PHPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
// private weak var controller: PHPickerViewController?
//
// var isPresented: Binding<Bool> {
// didSet {
// updateControllerLifecycle(
// from: oldValue.wrappedValue,
// to: isPresented.wrappedValue
// )
// }
// }
//
// var selection: Binding<[Backport<Any>.PhotosPickerItem]>
// var configuration: PHPickerConfiguration
//
// init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, configuration: PHPickerConfiguration) {
// self.isPresented = isPresented
// self.selection = selection
// self.configuration = configuration
//
// super.init(nibName: nil, bundle: nil)
// }
//
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// private func updateControllerLifecycle(from oldValue: Bool, to newValue: Bool) {
// switch (oldValue, newValue) {
// case (false, true):
// presentController()
// case (true, false):
// dismissController()
// case (true, true), (false, false):
// break
// }
// }
//
// private func presentController() {
// let controller = PHPickerViewController(configuration: configuration)
// controller.presentationController?.delegate = self
// controller.delegate = self
// present(controller, animated: true)
// self.controller = controller
// }
//
// private func dismissController() {
// isPresented.wrappedValue = false
// guard let controller else { return }
// controller.presentedViewController?.dismiss(animated: true)
// }
//
// func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
// print(results)
// dismissController()
// }
//
// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// dismissController()
// }
// }
//}
//
//@available(iOS 13, *)
//private struct LegacyPhotosViewController: UIViewControllerRepresentable {
// @Binding var isPresented: Bool
// @Binding var selection: [Backport<Any>.PhotosPickerItem]
//
// let filter: Backport<Any>.PHPickerFilter?
//
// func makeUIViewController(context: Context) -> Representable {
// Representable(
// isPresented: $isPresented,
// selection: $selection,
// filter: filter
// )
// }
//
//
// func updateUIViewController(_ controller: Representable, context: Context) {
// controller.selection = $selection
// controller.filter = filter
// }
//}
//
//@available(iOS 13, *)
//private extension LegacyPhotosViewController {
// final class Representable: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate {
// private weak var controller: UIImagePickerController?
//
// var isPresented: Binding<Bool> {
// didSet {
// updateControllerLifecycle(
// from: oldValue.wrappedValue,
// to: isPresented.wrappedValue
// )
// }
// }
//
// var selection: Binding<[Backport<Any>.PhotosPickerItem]>
// var filter: Backport<Any>.PHPickerFilter?
//
// init(isPresented: Binding<Bool>, selection: Binding<[Backport<Any>.PhotosPickerItem]>, filter: Backport<Any>.PHPickerFilter?) {
// self.isPresented = isPresented
// self.selection = selection
// self.filter = filter
//
// super.init(nibName: nil, bundle: nil)
// }
//
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// private func updateControllerLifecycle(from oldValue: Bool, to newValue: Bool) {
// switch (oldValue, newValue) {
// case (false, true):
// presentController()
// case (true, false):
// dismissController()
// case (true, true), (false, false):
// break
// }
// }
//
// private func presentController() {
// let controller = UIImagePickerController()
//
// if let filter {
// controller.mediaTypes = filter.mediaTypes
// } else if let types = UIImagePickerController.availableMediaTypes(for: .photoLibrary) {
// controller.mediaTypes = types
// }
//
// controller.allowsEditing = false
// controller.sourceType = .photoLibrary
// controller.videoQuality = .typeHigh
// controller.presentationController?.delegate = self
// controller.delegate = self
//
// present(controller, animated: true)
// self.controller = controller
// }
//
// private func dismissController() {
// isPresented.wrappedValue = false
// guard let controller else { return }
// controller.presentedViewController?.dismiss(animated: true)
// }
//
// func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
// print("TBD")
// print(info)
// dismissController()
// }
//
// func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
// dismissController()
// }
//
// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// dismissController()
// }
// }
//}
//#endif

View File

@ -0,0 +1,93 @@
#if os(iOS)
import SwiftUI
import PhotosUI
internal struct PhotosPickerView: View {
@Environment(\.backportDismiss) private var dismiss
@Binding var selection: [Backport<Any>.PhotosPickerItem]
let filter: Backport<Any>.PHPickerFilter?
let maxSelection: Int?
let selectionBehavior: Backport<Any>.PhotosPickerSelectionBehavior
let encoding: Backport<Any>.PhotosPickerItem.EncodingDisambiguationPolicy
let library: PHPhotoLibrary
private enum Source: String, CaseIterable, Identifiable {
var id: Self { self }
case photos = "Photos"
case albums = "Albums"
}
@State private var source: Source = .photos
var body: some View {
NavigationView {
List {
}
.navigationBarTitle(Text("Photos"), displayMode: .inline)
.backport.toolbar {
Backport.ToolbarItem(placement: .primaryAction) {
Text("Done")
}
Backport.ToolbarItem(placement: .cancellationAction) {
Text("Cancel")
}
Backport.ToolbarItem(placement: .principal) {
Text("Principal")
}
}
// .backport.toolbar {
// picker
// } leading: {
// Button("Cancel") {
// dismiss()
// }
// } trailing: {
// Button("Add") {
//
// }
// .font(.body.weight(.semibold))
// .disabled(selection.isEmpty)
// .opacity(maxSelection == 1 ? 0 : 1)
// }
// .controller { controller in
// if #available(iOS 14, *) { } else {
// guard controller?.navigationItem.titleView == nil else { return }
// controller?.navigationItem.titleView = UIHostingController(rootView: picker, ignoreSafeArea: false).view
// }
// }
}
.backport.interactiveDismissDisabled()
}
private var picker: some View {
Picker("", selection: $source) {
ForEach(Source.allCases) { source in
Text(source.rawValue)
.tag(source)
}
}
.pickerStyle(.segmented)
.fixedSize()
}
}
//private extension Backport where Wrapped: View {
// @ViewBuilder
// func toolbar<Leading: View, Trailing: View, Principal: View>(@ViewBuilder principal: () -> Principal, @ViewBuilder leading: () -> Leading, @ViewBuilder trailing: () -> Trailing) -> some View {
// if #available(iOS 14, *) {
// content.toolbar {
//// ToolbarItem(placement: .navigationBarLeading, content: leading)
//// ToolbarItem(placement: .navigationBarTrailing, content: trailing)
//// ToolbarItem(placement: .principal, content: principal)
// }
// } else {
// content
// .navigationBarItems(leading: leading(), trailing: trailing())
// }
// }
//}
#endif

View File

@ -0,0 +1,122 @@
#if os(iOS)
import SwiftUI
public extension Backport<Any> {
enum ToolbarItemPlacement {
case automatic
case primaryAction
case confirmationAction
case cancellationAction
case destructiveAction
case principal
var isLeading: Bool {
switch self {
case .cancellationAction:
return true
default:
return false
}
}
var isTrailing: Bool {
switch self {
case .automatic, .primaryAction, .confirmationAction, .destructiveAction:
return true
default:
return false
}
}
}
struct ToolbarItem: View {
let placement: Backport.ToolbarItemPlacement
let content: AnyView
public init<Content: View>(placement: Backport.ToolbarItemPlacement, @ViewBuilder content: () -> Content) {
self.placement = placement
self.content = AnyView(content())
}
public var body: some View {
content
}
}
}
extension Collection where Element == Backport<Any>.ToolbarItem, Indices: RandomAccessCollection, Indices.Index: Hashable {
@ViewBuilder var content: some View {
if !isEmpty {
HStack {
ForEach(indices, id: \.self) { index in
self[index].content
}
}
}
}
}
@available(iOS, introduced: 13, deprecated: 14)
public extension Backport where Wrapped: View {
@ViewBuilder
func toolbar(@BackportToolbarContentBuilder _ items: () -> [Backport<Any>.ToolbarItem]) -> some View {
let items = items()
content
.navigationBarItems(leading: items.filter { $0.placement.isLeading }.content, trailing: items.filter { $0.placement.isTrailing }.content)
.controller { controller in
controller?.navigationItem.titleView = UIHostingController(
rootView: items.filter { $0.placement == .principal }.content,
ignoreSafeArea: false
).view
}
}
}
@resultBuilder
public struct BackportToolbarContentBuilder { }
public extension BackportToolbarContentBuilder {
static func buildBlock() -> [Backport<Any>.ToolbarItem] {
[Backport<Any>.ToolbarItem(placement: .automatic, content: { EmptyView() })]
}
static func buildBlock(_ content: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[content]
}
static func buildIf(_ content: Backport<Any>.ToolbarItem?) -> [Backport<Any>.ToolbarItem?] {
[content].compactMap { $0 }
}
static func buildEither(first: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[first]
}
static func buildEither(second: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[second]
}
static func buildLimitedAvailability(_ content: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[content]
}
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[c0, c1]
}
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[c0, c1, c2]
}
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem, _ c3: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[c0, c1, c2, c3]
}
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem, _ c3: Backport<Any>.ToolbarItem, _ c4: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[c0, c1, c2, c3, c4]
}
static func buildBlock(_ c0: Backport<Any>.ToolbarItem, _ c1: Backport<Any>.ToolbarItem, _ c2: Backport<Any>.ToolbarItem, _ c3: Backport<Any>.ToolbarItem, _ c4: Backport<Any>.ToolbarItem, _ c5: Backport<Any>.ToolbarItem) -> [Backport<Any>.ToolbarItem] {
[c0, c1, c2, c3, c4, c5]
}
}
#endif

View File

@ -74,11 +74,7 @@ public extension Backport where Wrapped: View {
@available(watchOS, deprecated: 9) @available(watchOS, deprecated: 9)
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View { func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
#if os(iOS) #if os(iOS)
if #available(iOS 15, *) { content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
} else {
content
}
#else #else
content content
#endif #endif
@ -154,11 +150,7 @@ public extension Backport where Wrapped: View {
@ViewBuilder @ViewBuilder
func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View { func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View {
#if os(iOS) #if os(iOS)
if #available(iOS 15, *) { content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
} else {
content
}
#else #else
content content
#endif #endif