From 8888bd8bc3bbf61e6b0d6e0501fb80ceeba88427 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Sun, 2 May 2021 00:13:51 +0200 Subject: [PATCH 01/11] fix(tvOS): added missing unavailable annotation --- Sources/MediaCore/API/Media/Media.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/MediaCore/API/Media/Media.swift b/Sources/MediaCore/API/Media/Media.swift index 705856e..aef0f1e 100644 --- a/Sources/MediaCore/API/Media/Media.swift +++ b/Sources/MediaCore/API/Media/Media.swift @@ -38,6 +38,7 @@ public struct Media { /// Returns the current camera permission. /// + @available(tvOS, unavailable) public static var currentCameraPermission: AVAuthorizationStatus { AVCaptureDevice.authorizationStatus(for: .video) } From 835e4a9bec79c836b7bb8dbba355b8a1cb715ad1 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Sun, 2 May 2021 12:29:02 +0200 Subject: [PATCH 02/11] fix(macOS): fixed some issues --- Sources/MediaCore/API/Video/Video.swift | 4 ++++ .../MediaSwiftUI/internal/Views/ActivityIndicatorView.swift | 2 ++ .../MediaSwiftUI/internal/Views/UniversalProgressView.swift | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/MediaCore/API/Video/Video.swift b/Sources/MediaCore/API/Video/Video.swift index c9b2838..993dc9a 100644 --- a/Sources/MediaCore/API/Video/Video.swift +++ b/Sources/MediaCore/API/Video/Video.swift @@ -153,7 +153,11 @@ public extension Video { let copyCGImageResult: Result = Result { let cgImage = try generator.copyCGImage(at: requestedTime, actualTime: nil) + #if os(macOS) + return UniversalImage(cgImage: cgImage, size: .init(width: cgImage.width, height: cgImage.height)) + #else return UniversalImage(cgImage: cgImage) + #endif } DispatchQueue.main.async { diff --git a/Sources/MediaSwiftUI/internal/Views/ActivityIndicatorView.swift b/Sources/MediaSwiftUI/internal/Views/ActivityIndicatorView.swift index 52f65eb..8476dd4 100644 --- a/Sources/MediaSwiftUI/internal/Views/ActivityIndicatorView.swift +++ b/Sources/MediaSwiftUI/internal/Views/ActivityIndicatorView.swift @@ -5,6 +5,7 @@ // Created by Christian Elies on 15.02.21. // +#if canImport(UIKit) import SwiftUI import UIKit @@ -15,3 +16,4 @@ struct ActivityIndicatorView: UIViewRepresentable { func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {} } +#endif diff --git a/Sources/MediaSwiftUI/internal/Views/UniversalProgressView.swift b/Sources/MediaSwiftUI/internal/Views/UniversalProgressView.swift index 8cc0c85..367b2f9 100644 --- a/Sources/MediaSwiftUI/internal/Views/UniversalProgressView.swift +++ b/Sources/MediaSwiftUI/internal/Views/UniversalProgressView.swift @@ -7,17 +7,21 @@ import SwiftUI +@available(macOS 11, *) struct UniversalProgressView: View { var body: some View { - if #available(iOS 14, macOS 11, tvOS 14, *) { + if #available(iOS 14, tvOS 14, *) { ProgressView() } else { + #if canImport(UIKit) ActivityIndicatorView() + #endif } } } #if DEBUG +@available(macOS 11, *) struct UniversalProgressView_Previews: PreviewProvider { static var previews: some View { UniversalProgressView() From efc42c12a1759d4ab8ae9eb5f2c5f18a2c5b1de5 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Sun, 2 May 2021 12:42:48 +0200 Subject: [PATCH 03/11] chore(example): improved permissions section --- .../Views/PermissionsSection.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Example/Media-Example/Views/PermissionsSection.swift b/Example/Media-Example/Views/PermissionsSection.swift index cfc024a..cc19206 100644 --- a/Example/Media-Example/Views/PermissionsSection.swift +++ b/Example/Media-Example/Views/PermissionsSection.swift @@ -8,23 +8,26 @@ import AVFoundation import MediaCore +import Photos import SwiftUI struct PermissionsSection: View { @State private var isLimitedLibraryPickerPresented = false + @State private var cameraPermission: AVAuthorizationStatus = .notDetermined + @State private var mediaPermission: PHAuthorizationStatus = .notDetermined var requestedPermission: (Result) -> Void - + var body: some View { Section(header: Text("Permissions")) { Button(action: { - Media.requestCameraPermission { result in - debugPrint(result) + Media.requestCameraPermission { _ in + cameraPermission = Media.currentCameraPermission } }) { HStack { Text("Trigger camera permission request") - Toggle("", isOn: .constant(Media.currentCameraPermission == .authorized)) + Toggle("", isOn: .constant(cameraPermission == .authorized)) .disabled(true) } } @@ -38,17 +41,24 @@ struct PermissionsSection: View { }) { HStack { Text("Trigger photo library permission request") - Toggle("", isOn: .constant(Media.currentPermission == .authorized)) + Toggle("", isOn: .constant(mediaPermission == .authorized)) .disabled(true) } } .background(PHPicker(isPresented: $isLimitedLibraryPickerPresented)) } + .onAppear { + cameraPermission = Media.currentCameraPermission + mediaPermission = Media.currentPermission + } } } private extension PermissionsSection { func requestPermission() { - Media.requestPermission(requestedPermission) + Media.requestPermission { result in + mediaPermission = Media.currentPermission + requestedPermission(result) + } } } From a8e52cff2052f8318cf67d7f95cb5624012225e7 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 12:30:01 +0200 Subject: [PATCH 04/11] fix(swiftui): browser views now correctly return a result on completion --- .../MediaCore/API/Models/BrowserResult.swift | 14 ++++++ Sources/MediaCore/API/PublicAliases.swift | 8 ++-- .../API/LivePhoto/LivePhoto+SwiftUI.swift | 44 +++++++++++++---- .../API/Media/Media+SwiftUI.swift | 23 +++++---- .../PHPicker/PHPickerResult+loadImage.swift | 38 +++++++++------ .../PHPickerResult+loadLivePhoto.swift | 36 ++++++++++++++ .../PHPicker/PHPickerResult+loadVideo.swift | 48 +++++++++++++++++++ .../API/Photo/Photo+SwiftUI.swift | 44 +++++++++++++---- .../API/Video/Video+SwiftUI.swift | 44 +++++++++++++---- .../internal/Models/Garbage.swift | 12 +++++ 10 files changed, 260 insertions(+), 51 deletions(-) create mode 100644 Sources/MediaCore/API/Models/BrowserResult.swift create mode 100644 Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift create mode 100644 Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift create mode 100644 Sources/MediaSwiftUI/internal/Models/Garbage.swift diff --git a/Sources/MediaCore/API/Models/BrowserResult.swift b/Sources/MediaCore/API/Models/BrowserResult.swift new file mode 100644 index 0000000..a660c32 --- /dev/null +++ b/Sources/MediaCore/API/Models/BrowserResult.swift @@ -0,0 +1,14 @@ +// +// BrowserResult.swift +// MediaCore +// +// Created by Christian Elies on 03.05.21. +// + +/// Represents the result of a media browser view. +public enum BrowserResult { + /// The result is a concrete media type, like `LivePhoto`, `Photo` or `Video`. + case media(_ value: T) + /// The result is a data representation, like `URL` or `Data`. + case data(_ data: U) +} diff --git a/Sources/MediaCore/API/PublicAliases.swift b/Sources/MediaCore/API/PublicAliases.swift index 2c6babe..ca4aa9c 100644 --- a/Sources/MediaCore/API/PublicAliases.swift +++ b/Sources/MediaCore/API/PublicAliases.swift @@ -18,13 +18,13 @@ public typealias MediaSubtype = PHAssetMediaSubtype public typealias ResultDataCompletion = (Result) -> Void public typealias ResultGenericCompletion = (Result) -> Void public typealias ResultLivePhotoCompletion = (Result) -> Void -public typealias ResultLivePhotosCompletion = (Result<[LivePhoto], Error>) -> Void +public typealias ResultLivePhotosCompletion = (Result<[BrowserResult], Error>) -> Void public typealias RequestLivePhotoResultHandler = (PHLivePhoto?, [AnyHashable : Any]) -> Void public typealias ResultPHAssetCompletion = (Result) -> Void -public typealias ResultPHAssetsCompletion = (Result<[PHAsset], Swift.Error>) -> Void +public typealias ResultPHAssetsCompletion = (Result<[BrowserResult], Swift.Error>) -> Void public typealias ResultPhotoCompletion = (Result) -> Void -public typealias ResultPhotosCompletion = (Result<[Photo], Swift.Error>) -> Void +public typealias ResultPhotosCompletion = (Result<[BrowserResult], Swift.Error>) -> Void public typealias ResultURLCompletion = (Result) -> Void public typealias ResultVideoCompletion = (Result) -> Void -public typealias ResultVideosCompletion = (Result<[Video], Swift.Error>) -> Void +public typealias ResultVideosCompletion = (Result<[BrowserResult], Swift.Error>) -> Void public typealias ResultVoidCompletion = (Result) -> Void diff --git a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift index dd02b39..561d68a 100644 --- a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift @@ -6,6 +6,7 @@ // #if canImport(SwiftUI) +import Combine import MediaCore import PhotosUI import SwiftUI @@ -69,22 +70,49 @@ public extension LivePhoto { @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View { if #available(iOS 14, macOS 11, *) { PHPicker(configuration: { - var configuration = PHPickerConfiguration() + var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .livePhotos configuration.selectionLimit = selectionLimit + configuration.preferredAssetRepresentationMode = .current return configuration }()) { result in switch result { case let .success(result): - let result = Result { - try result.compactMap { object -> LivePhoto? in - guard let assetIdentifier = object.assetIdentifier else { - return nil + if Media.currentPermission == .authorized { + let result = Result { + try result.compactMap { object -> BrowserResult? in + guard let assetIdentifier = object.assetIdentifier else { + return nil + } + guard let livePhoto = try LivePhoto.with(identifier: .init(stringLiteral: assetIdentifier)) else { + return nil + } + return .media(livePhoto) } - return try LivePhoto.with(identifier: .init(stringLiteral: assetIdentifier)) + } + completion(result) + } else { + DispatchQueue.global(qos: .userInitiated).async { + let loadVideos = result.map { $0.loadLivePhoto() } + Publishers.MergeMany(loadVideos) + .collect() + .sink { result in + switch result { + case let .failure(error): + DispatchQueue.main.async { + completion(.failure(error)) + } + case .finished: () + } + } receiveValue: { urls in + let browserResults = urls.map { BrowserResult.data($0) } + DispatchQueue.main.async { + completion(.success(browserResults)) + } + } + .store(in: &Garbage.cancellables) } } - completion(result) case let .failure(error): () completion(.failure(error)) } @@ -94,7 +122,7 @@ public extension LivePhoto { try ViewCreator.browser(mediaTypes: [.image, .livePhoto]) { (result: Result) in switch result { case let .success(livePhoto): - completion(.success([livePhoto])) + completion(.success([.media(livePhoto)])) case let .failure(error): completion(.failure(error)) } diff --git a/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift b/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift index 2f3af38..45120f5 100644 --- a/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift @@ -35,19 +35,26 @@ public extension Media { @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultPHAssetsCompletion) -> some View { if #available(iOS 14, macOS 11, *) { PHPicker(configuration: { - var configuration = PHPickerConfiguration() + var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.selectionLimit = selectionLimit + configuration.preferredAssetRepresentationMode = .current return configuration }()) { result in switch result { case let .success(result): - let identifiers = result.compactMap { $0.assetIdentifier } - let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil) - var assets: [PHAsset] = [] - fetchResult.enumerateObjects { asset, _, _ in - assets.append(asset) + if Media.currentPermission == .authorized { + let identifiers = result.compactMap { $0.assetIdentifier } + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil) + var assets: [PHAsset] = [] + fetchResult.enumerateObjects { asset, _, _ in + assets.append(asset) + } + let browserResults = assets.map { BrowserResult.media($0) } + completion(.success(browserResults)) + } else { + let browserResults = result.map { BrowserResult.data($0.itemProvider) } + completion(.success(browserResults)) } - completion(.success(assets)) case let .failure(error): () completion(.failure(error)) } @@ -59,7 +66,7 @@ public extension Media { completion(.failure(MediaPicker.Error.unsupportedValue)) return } - completion(.success([phAsset])) + completion(.success([.media(phAsset)])) }, onFailure: { error in completion(.failure(error)) }) diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift b/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift index aaf30b2..fa3555c 100644 --- a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift +++ b/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift @@ -5,33 +5,41 @@ // Created by Christian Elies on 14.10.20. // -#if !os(tvOS) && !os(macOS) +#if !os(tvOS) && (!os(macOS) || targetEnvironment(macCatalyst)) +import Combine +import MediaCore import PhotosUI @available(iOS 14, macCatalyst 14, *) extension PHPickerResult { + /// <#Description#> public enum Error: Swift.Error { + /// case couldNotLoadObject(underlying: Swift.Error) + /// case unknown } - public func loadImage(_ completion: @escaping (Result) -> Void) { - guard itemProvider.canLoadObject(ofClass: UIImage.self) else { - completion(.failure(Error.couldNotLoadObject(underlying: Error.unknown))) - return - } + /// <#Description#> + /// + /// - Returns: <#description#> + public func loadImage() -> AnyPublisher { + Future { promise in + guard itemProvider.canLoadObject(ofClass: UniversalImage.self) else { + promise(.failure(Error.couldNotLoadObject(underlying: Error.unknown))) + return + } - itemProvider.loadObject(ofClass: UIImage.self) { newImage, error in - if let error = error { - DispatchQueue.main.async { - completion(.failure(Error.couldNotLoadObject(underlying: error))) - } - } else if let newImage = newImage { - DispatchQueue.main.async { - completion(.success(newImage as! UIImage)) + itemProvider.loadObject(ofClass: UniversalImage.self) { newImage, error in + if let error = error { + promise(.failure(Error.couldNotLoadObject(underlying: error))) + } else if let newImage = newImage { + promise(.success(newImage as! UniversalImage)) + } else { + promise(.failure(Error.unknown)) } } - } + }.eraseToAnyPublisher() } } #endif diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift b/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift new file mode 100644 index 0000000..5b47b4a --- /dev/null +++ b/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift @@ -0,0 +1,36 @@ +// +// PHPickerResult+loadLivePhoto.swift +// MediaSwiftUI +// +// Created by Christian Elies on 03.05.21. +// + +#if !os(tvOS) && (!os(macOS) || targetEnvironment(macCatalyst)) +import Combine +import PhotosUI + +@available(iOS 14, macCatalyst 14, *) +extension PHPickerResult { + /// <#Description#> + /// + /// - Returns: <#description#> + public func loadLivePhoto() -> AnyPublisher { + Future { promise in + guard itemProvider.canLoadObject(ofClass: PHLivePhoto.self) else { + promise(.failure(Error.couldNotLoadObject(underlying: Error.unknown))) + return + } + + itemProvider.loadObject(ofClass: PHLivePhoto.self) { livePhoto, error in + if let error = error { + promise(.failure(Error.couldNotLoadObject(underlying: error))) + } else if let livePhoto = livePhoto { + promise(.success(livePhoto as! PHLivePhoto)) + } else { + promise(.failure(Error.unknown)) + } + } + }.eraseToAnyPublisher() + } +} +#endif diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift b/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift new file mode 100644 index 0000000..c39480c --- /dev/null +++ b/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift @@ -0,0 +1,48 @@ +// +// PHPickerResult+loadVideo.swift +// MediaSwiftUI +// +// Created by Christian Elies on 03.05.21. +// + +#if !os(tvOS) && !os(macOS) +import Combine +import PhotosUI + +@available(iOS 14, macCatalyst 14, *) +extension PHPickerResult { + /// <#Description#> + /// + /// - Returns: <#description#> + public func loadVideo() -> AnyPublisher { + Future { promise in + let typeIdentifier = UTType.movie.identifier + guard itemProvider.hasItemConformingToTypeIdentifier(typeIdentifier) else { + promise(.failure(Error.unknown)) + return + } + + itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in + if let url = url { + let fileManager: FileManager = .default + let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! + let targetLocation = cachesDirectory.appendingPathComponent(url.lastPathComponent) + let result: Result = Result { + if fileManager.fileExists(atPath: targetLocation.path) { + let newItemURL = try fileManager.replaceItemAt(targetLocation, withItemAt: url) + return newItemURL ?? targetLocation + } else { + try fileManager.copyItem(at: url, to: targetLocation) + } + return targetLocation + } + promise(result) + } else { + let error = error ?? Error.unknown + promise(.failure(error)) + } + } + }.eraseToAnyPublisher() + } +} +#endif diff --git a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift index 42a7344..c72090c 100644 --- a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift @@ -6,6 +6,7 @@ // #if canImport(SwiftUI) && (!os(macOS) || targetEnvironment(macCatalyst)) +import Combine import MediaCore import Photos import PhotosUI @@ -79,22 +80,49 @@ public extension Photo { @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultPhotosCompletion) -> some View { if #available(iOS 14, macOS 11, *) { PHPicker(configuration: { - var configuration = PHPickerConfiguration() + var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .images configuration.selectionLimit = selectionLimit + configuration.preferredAssetRepresentationMode = .current return configuration }()) { result in switch result { case let .success(result): - let result = Result { - try result.compactMap { object -> Photo? in - guard let assetIdentifier = object.assetIdentifier else { - return nil + if Media.currentPermission == .authorized { + let result = Result { + try result.compactMap { object -> BrowserResult? in + guard let assetIdentifier = object.assetIdentifier else { + return nil + } + guard let photo = try Photo.with(identifier: .init(stringLiteral: assetIdentifier)) else { + return nil + } + return .media(photo) } - return try Photo.with(identifier: .init(stringLiteral: assetIdentifier)) + } + completion(result) + } else { + DispatchQueue.global(qos: .userInitiated).async { + let loadVideos = result.map { $0.loadImage() } + Publishers.MergeMany(loadVideos) + .collect() + .sink { result in + switch result { + case let .failure(error): + DispatchQueue.main.async { + completion(.failure(error)) + } + case .finished: () + } + } receiveValue: { urls in + let browserResults = urls.map { BrowserResult.data($0) } + DispatchQueue.main.async { + completion(.success(browserResults)) + } + } + .store(in: &Garbage.cancellables) } } - completion(result) case let .failure(error): () completion(.failure(error)) } @@ -104,7 +132,7 @@ public extension Photo { try ViewCreator.browser(mediaTypes: [.image]) { (result: Result) in switch result { case let .success(photo): - completion(.success([photo])) + completion(.success([.media(photo)])) case let .failure(error): completion(.failure(error)) } diff --git a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift index eb0c6a4..831f3d7 100644 --- a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift @@ -6,6 +6,7 @@ // #if canImport(SwiftUI) && (!os(macOS) || targetEnvironment(macCatalyst)) +import Combine import MediaCore import PhotosUI import SwiftUI @@ -95,22 +96,49 @@ public extension Video { @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultVideosCompletion) -> some View { if #available(iOS 14, macOS 11, *) { PHPicker(configuration: { - var configuration = PHPickerConfiguration() + var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .videos configuration.selectionLimit = selectionLimit + configuration.preferredAssetRepresentationMode = .current return configuration }()) { result in switch result { case let .success(result): - let result = Result { - try result.compactMap { object -> Video? in - guard let assetIdentifier = object.assetIdentifier else { - return nil + if Media.currentPermission == .authorized { + let browserResult = Result { + try result.compactMap { object -> BrowserResult? in + guard let assetIdentifier = object.assetIdentifier else { + return nil + } + guard let video = try Video.with(identifier: .init(stringLiteral: assetIdentifier)) else { + return nil + } + return .media(video) } - return try Video.with(identifier: .init(stringLiteral: assetIdentifier)) + } + completion(browserResult) + } else { + DispatchQueue.global(qos: .userInitiated).async { + let loadVideos = result.map { $0.loadVideo() } + Publishers.MergeMany(loadVideos) + .collect() + .sink { result in + switch result { + case let .failure(error): + DispatchQueue.main.async { + completion(.failure(error)) + } + case .finished: () + } + } receiveValue: { urls in + let browserResults = urls.map { BrowserResult.data($0) } + DispatchQueue.main.async { + completion(.success(browserResults)) + } + } + .store(in: &Garbage.cancellables) } } - completion(result) case let .failure(error): () completion(.failure(error)) } @@ -120,7 +148,7 @@ public extension Video { try ViewCreator.browser(mediaTypes: [.movie]) { (result: Result) in switch result { case let .success(video): - completion(.success([video])) + completion(.success([.media(video)])) case let .failure(error): completion(.failure(error)) } diff --git a/Sources/MediaSwiftUI/internal/Models/Garbage.swift b/Sources/MediaSwiftUI/internal/Models/Garbage.swift new file mode 100644 index 0000000..4d8fd1f --- /dev/null +++ b/Sources/MediaSwiftUI/internal/Models/Garbage.swift @@ -0,0 +1,12 @@ +// +// Garbage.swift +// MediaSwiftUI +// +// Created by Christian Elies on 03.05.21. +// + +import Combine + +struct Garbage { + static var cancellables: [AnyCancellable] = [] +} From ab15da702a8aa7ce255342fcd36859669186ca3f Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 13:14:30 +0200 Subject: [PATCH 05/11] chore(example): added selected video in browser example --- .../Media-Example/Views/BrowserSection.swift | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Example/Media-Example/Views/BrowserSection.swift b/Example/Media-Example/Views/BrowserSection.swift index 55b1e5a..a49f799 100644 --- a/Example/Media-Example/Views/BrowserSection.swift +++ b/Example/Media-Example/Views/BrowserSection.swift @@ -6,15 +6,21 @@ // Copyright © 2021 Christian Elies. All rights reserved. // +import AVKit import MediaCore import MediaSwiftUI import SwiftUI +extension URL: Identifiable { + public var id: String { absoluteString } +} + struct BrowserSection: View { @State private var isLivePhotoBrowserViewVisible = false @State private var isMediaBrowserViewVisible = false @State private var isPhotoBrowserViewVisible = false @State private var isVideoBrowserViewVisible = false + @State private var playerURL: URL? var body: some View { Section(header: Label("Browser", systemImage: "photo.on.rectangle.angled")) { @@ -59,8 +65,34 @@ struct BrowserSection: View { .fullScreenCover(isPresented: $isVideoBrowserViewVisible, onDismiss: { isVideoBrowserViewVisible = false }) { - Video.browser(selectionLimit: 0) { _ in } + Video.browser(selectionLimit: 0, handleVideoBrowserResult) + } + .background( + EmptyView() + .sheet(item: $playerURL, onDismiss: { + playerURL = nil + }) { url in + VideoPlayer(player: .init(url: url)) + } + ) + } + } +} + +private extension BrowserSection { + func handleVideoBrowserResult(_ result: Result<[BrowserResult], Swift.Error>) { + switch result { + case let .success(browserResult): + switch browserResult.first { + case let .data(url): + isVideoBrowserViewVisible = false + // TODO: improve this + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + playerURL = url + } + default: () } + default: () } } } From 7211b0a11e7d203110f611c6cb5d29ca2727f8a1 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 22:57:01 +0200 Subject: [PATCH 06/11] fix(swiftui): added isPresented binding to browser views; feat(swiftui): made the photosUILivePhotoView public --- .../API/LivePhoto/LivePhoto+SwiftUI.swift | 10 +++--- .../API/LivePhoto/PhotosUILivePhotoView.swift | 32 +++++++++++++++++++ .../API/Media/Media+SwiftUI.swift | 10 +++--- .../MediaSwiftUI/API/PHPicker/PHPicker.swift | 21 ++++++++++-- .../API/Photo/Photo+SwiftUI.swift | 10 +++--- .../API/Video/Video+SwiftUI.swift | 10 +++--- .../LivePhoto/PhotosUILivePhotoView.swift | 26 --------------- 7 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 Sources/MediaSwiftUI/API/LivePhoto/PhotosUILivePhotoView.swift delete mode 100644 Sources/MediaSwiftUI/internal/Views/LivePhoto/PhotosUILivePhotoView.swift diff --git a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift index 561d68a..c23de56 100644 --- a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift @@ -51,25 +51,27 @@ public extension LivePhoto { /// Creates a ready-to-use `SwiftUI` view for browsing `LivePhoto`s in the photo library /// If an error occurs during initialization a `SwiftUI.Text` with the `localizedDescription` is shown. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter completion: A closure which gets the selected `LivePhoto` on `success` or `Error` on `failure`. /// /// - Returns: some View - static func browser(selectionLimit: Int = 1, _ completion: @escaping ResultLivePhotosCompletion) -> some View { - browser(selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) + static func browser(isPresented: Binding, selectionLimit: Int = 1, _ completion: @escaping ResultLivePhotosCompletion) -> some View { + browser(isPresented: isPresented, selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) } /// Creates a ready-to-use `SwiftUI` view for browsing `LivePhoto`s in the photo library /// If an error occurs during initialization the provided `errorView` closure is used to construct the view to be displayed. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter errorView: A closure that constructs an error view for the given error. /// - Parameter completion: A closure which gets the selected `LivePhoto` on `success` or `Error` on `failure`. /// /// - Returns: some View - @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View { + @ViewBuilder static func browser(isPresented: Binding, selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View { if #available(iOS 14, macOS 11, *) { - PHPicker(configuration: { + PHPicker(isPresented: isPresented, configuration: { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .livePhotos configuration.selectionLimit = selectionLimit diff --git a/Sources/MediaSwiftUI/API/LivePhoto/PhotosUILivePhotoView.swift b/Sources/MediaSwiftUI/API/LivePhoto/PhotosUILivePhotoView.swift new file mode 100644 index 0000000..d473998 --- /dev/null +++ b/Sources/MediaSwiftUI/API/LivePhoto/PhotosUILivePhotoView.swift @@ -0,0 +1,32 @@ +// +// PhotosUILivePhotoView.swift +// MediaSwiftUI +// +// Created by Christian Elies on 28.11.19. +// + +#if canImport(SwiftUI) && !os(macOS) && !targetEnvironment(macCatalyst) +import PhotosUI +import SwiftUI + +@available(iOS 13, tvOS 13, *) +/// `SwiftUI` port of the `PHLivePhotoView`. +public struct PhotosUILivePhotoView: UIViewRepresentable { + let phLivePhoto: PHLivePhoto + + /// Initializes the view with the given live photo. + /// + /// - Parameter phLivePhoto: The live photo which should be displayed. + public init(phLivePhoto: PHLivePhoto) { + self.phLivePhoto = phLivePhoto + } + + public func makeUIView(context: UIViewRepresentableContext) -> PHLivePhotoView { + let livePhotoView = PHLivePhotoView() + livePhotoView.livePhoto = phLivePhoto + return livePhotoView + } + + public func updateUIView(_ uiView: PHLivePhotoView, context: UIViewRepresentableContext) {} +} +#endif diff --git a/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift b/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift index 45120f5..c882375 100644 --- a/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Media/Media+SwiftUI.swift @@ -16,25 +16,27 @@ public extension Media { /// Creates a ready-to-use `SwiftUI` view for browsing the photo library /// If an error occurs during initialization a `SwiftUI.Text` with the `localizedDescription` is shown. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter completion: A closure which gets the selected `PHAsset` on `success` or `Error ` on `failure`. /// /// - Returns: some View - static func browser(selectionLimit: Int = 1, _ completion: @escaping ResultPHAssetsCompletion) -> some View { - browser(selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) + static func browser(isPresented: Binding, selectionLimit: Int = 1, _ completion: @escaping ResultPHAssetsCompletion) -> some View { + browser(isPresented: isPresented, selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) } /// Creates a ready-to-use `SwiftUI` view for browsing the photo library /// If an error occurs during initialization the provided `errorView` closure is used to construct the view to be displayed. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter errorView: A closure that constructs an error view for the given error. /// - Parameter completion: A closure which gets the selected `PHAsset` on `success` or `Error ` on `failure`. /// /// - Returns: some View - @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultPHAssetsCompletion) -> some View { + @ViewBuilder static func browser(isPresented: Binding, selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultPHAssetsCompletion) -> some View { if #available(iOS 14, macOS 11, *) { - PHPicker(configuration: { + PHPicker(isPresented: isPresented, configuration: { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.selectionLimit = selectionLimit configuration.preferredAssetRepresentationMode = .current diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift b/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift index 1af5442..3df87ea 100644 --- a/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift +++ b/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift @@ -11,7 +11,9 @@ import PhotosUI import SwiftUI @available(iOS 14, macCatalyst 14, *) +/// `SwiftUI` port of the `PHPicker`. public struct PHPicker: UIViewControllerRepresentable { + /// The coordinator of the view. Mainly it's the delegate of the underlying `PHPickerViewController`. public final class Coordinator: NSObject, PHPickerViewControllerDelegate { private let picker: PHPicker @@ -22,16 +24,26 @@ public struct PHPicker: UIViewControllerRepresentable { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { self.picker.completion(.success(results)) picker.dismiss(animated: true, completion: nil) + self.picker.isPresented = false } } + @Binding var isPresented: Bool let configuration: PHPickerConfiguration let completion: ResultGenericCompletion<[PHPickerResult]> + /// Initializes the picker. + /// + /// - Parameters: + /// - isPresented: A binding to whether the picker is presented. + /// - configuration: The configuration for the picker. + /// - completion: A closure called on completion with a result - an array of `PHPickerResult` on `success` or an `Error` on `failure`. public init( + isPresented: Binding, configuration: PHPickerConfiguration, _ completion: @escaping ResultGenericCompletion<[PHPickerResult]> ) { + _isPresented = isPresented self.configuration = configuration self.completion = completion } @@ -45,7 +57,12 @@ public struct PHPicker: UIViewControllerRepresentable { public func updateUIViewController( _ uiViewController: PHPickerViewController, context: Context - ) {} + ) { + guard !isPresented else { + return + } + uiViewController.dismiss(animated: true, completion: nil) + } public func makeCoordinator() -> Coordinator { Coordinator(picker: self) @@ -56,7 +73,7 @@ public struct PHPicker: UIViewControllerRepresentable { @available(iOS 14, macCatalyst 14, *) struct PHPicker_Previews: PreviewProvider { static var previews: some View { - PHPicker(configuration: .init(), { _ in }) + PHPicker(isPresented: .constant(true), configuration: .init(), { _ in }) } } #endif diff --git a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift index c72090c..050caa1 100644 --- a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift @@ -61,25 +61,27 @@ public extension Photo { /// Creates a ready-to-use `SwiftUI` view for browsing the photo library /// If an error occurs during initialization a `SwiftUI.Text` with the `localizedDescription` is shown. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter completion: A closure which gets a `Result` (`Photo` on `success` or `Error` on `failure`). /// /// - Returns: some View - static func browser(selectionLimit: Int = 1, _ completion: @escaping ResultPhotosCompletion) -> some View { - browser(selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) + static func browser(isPresented: Binding, selectionLimit: Int = 1, _ completion: @escaping ResultPhotosCompletion) -> some View { + browser(isPresented: isPresented, selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) } /// Creates a ready-to-use `SwiftUI` view for browsing the photo library /// If an error occurs during initialization the provided `errorView` closure is used to construct the view to be displayed. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter errorView: A closure that constructs an error view for the given error. /// - Parameter completion: A closure which gets a `Result` (`Photo` on `success` or `Error` on `failure`). /// /// - Returns: some View - @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultPhotosCompletion) -> some View { + @ViewBuilder static func browser(isPresented: Binding, selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultPhotosCompletion) -> some View { if #available(iOS 14, macOS 11, *) { - PHPicker(configuration: { + PHPicker(isPresented: isPresented, configuration: { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .images configuration.selectionLimit = selectionLimit diff --git a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift index 831f3d7..1dce909 100644 --- a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift @@ -77,25 +77,27 @@ public extension Video { /// Creates a ready-to-use `SwiftUI` view for browsing `Video`s in the photo library /// If an error occurs during initialization a `SwiftUI.Text` with the `localizedDescription` is shown. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter completion: A closure wich gets `Video` on `success` or `Error` on `failure`. /// /// - Returns: some View - static func browser(selectionLimit: Int = 1, _ completion: @escaping ResultVideosCompletion) -> some View { - browser(selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) + static func browser(isPresented: Binding, selectionLimit: Int = 1, _ completion: @escaping ResultVideosCompletion) -> some View { + browser(isPresented: isPresented, selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion) } /// Creates a ready-to-use `SwiftUI` view for browsing `Video`s in the photo library /// If an error occurs during initialization the provided `errorView` closure is used to construct the view to be displayed. /// + /// - Parameter isPresented: A binding to whether the underlying picker is presented. /// - Parameter selectionLimit: Specifies the number of items which can be selected. Works only on iOS 14 and macOS 11 where the `PHPicker` is used under the hood. Defaults to `1`. /// - Parameter errorView: A closure that constructs an error view for the given error. /// - Parameter completion: A closure wich gets `Video` on `success` or `Error` on `failure`. /// /// - Returns: some View - @ViewBuilder static func browser(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultVideosCompletion) -> some View { + @ViewBuilder static func browser(isPresented: Binding, selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultVideosCompletion) -> some View { if #available(iOS 14, macOS 11, *) { - PHPicker(configuration: { + PHPicker(isPresented: isPresented, configuration: { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.filter = .videos configuration.selectionLimit = selectionLimit diff --git a/Sources/MediaSwiftUI/internal/Views/LivePhoto/PhotosUILivePhotoView.swift b/Sources/MediaSwiftUI/internal/Views/LivePhoto/PhotosUILivePhotoView.swift deleted file mode 100644 index 1ff4e4f..0000000 --- a/Sources/MediaSwiftUI/internal/Views/LivePhoto/PhotosUILivePhotoView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// PhotosUILivePhotoView.swift -// -// -// Created by Christian Elies on 28.11.19. -// - -#if canImport(SwiftUI) && !os(macOS) && !targetEnvironment(macCatalyst) -import PhotosUI -import SwiftUI - -@available(iOS 13, tvOS 13, *) -struct PhotosUILivePhotoView: UIViewRepresentable { - let phLivePhoto: PHLivePhoto - - func makeUIView(context: UIViewRepresentableContext) -> PHLivePhotoView { - let livePhotoView = PHLivePhotoView() - livePhotoView.livePhoto = phLivePhoto - return livePhotoView - } - - func updateUIView(_ uiView: PHLivePhotoView, context: UIViewRepresentableContext) { - - } -} -#endif From 975031d6f2f1c8ae1e26bd834282c85586f50bb6 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 22:58:03 +0200 Subject: [PATCH 07/11] chore(example): improved browser view examples --- .../Media-Example/Views/BrowserSection.swift | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/Example/Media-Example/Views/BrowserSection.swift b/Example/Media-Example/Views/BrowserSection.swift index a49f799..80582b4 100644 --- a/Example/Media-Example/Views/BrowserSection.swift +++ b/Example/Media-Example/Views/BrowserSection.swift @@ -9,18 +9,29 @@ import AVKit import MediaCore import MediaSwiftUI +import Photos import SwiftUI extension URL: Identifiable { public var id: String { absoluteString } } +extension UIImage: Identifiable { + public var id: UIImage { self } +} + +extension PHLivePhoto: Identifiable { + public var id: PHLivePhoto { self } +} + struct BrowserSection: View { @State private var isLivePhotoBrowserViewVisible = false @State private var isMediaBrowserViewVisible = false @State private var isPhotoBrowserViewVisible = false @State private var isVideoBrowserViewVisible = false @State private var playerURL: URL? + @State private var image: UIImage? + @State private var livePhoto: PHLivePhoto? var body: some View { Section(header: Label("Browser", systemImage: "photo.on.rectangle.angled")) { @@ -32,8 +43,16 @@ struct BrowserSection: View { .fullScreenCover(isPresented: $isLivePhotoBrowserViewVisible, onDismiss: { isLivePhotoBrowserViewVisible = false }) { - LivePhoto.browser(selectionLimit: 0) { _ in } + LivePhoto.browser(isPresented: $isLivePhotoBrowserViewVisible, selectionLimit: 0, handleLivePhotoBrowserResult) } + .background( + EmptyView() + .sheet(item: $livePhoto, onDismiss: { + livePhoto = nil + }) { livePhoto in + PhotosUILivePhotoView(phLivePhoto: livePhoto) + } + ) Button(action: { isMediaBrowserViewVisible = true @@ -43,7 +62,9 @@ struct BrowserSection: View { .fullScreenCover(isPresented: $isMediaBrowserViewVisible, onDismiss: { isMediaBrowserViewVisible = false }) { - Media.browser(selectionLimit: 0) { _ in } + Media.browser(isPresented: $isMediaBrowserViewVisible, selectionLimit: 0) { result in + debugPrint(result) + } } Button(action: { @@ -54,8 +75,18 @@ struct BrowserSection: View { .fullScreenCover(isPresented: $isPhotoBrowserViewVisible, onDismiss: { isPhotoBrowserViewVisible = false }) { - Photo.browser(selectionLimit: 0) { _ in } + Photo.browser(isPresented: $isPhotoBrowserViewVisible, selectionLimit: 0, handlePhotoBrowserResult) } + .background( + EmptyView() + .sheet(item: $image, onDismiss: { + image = nil + }) { uiImage in + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + } + ) Button(action: { isVideoBrowserViewVisible = true @@ -65,7 +96,7 @@ struct BrowserSection: View { .fullScreenCover(isPresented: $isVideoBrowserViewVisible, onDismiss: { isVideoBrowserViewVisible = false }) { - Video.browser(selectionLimit: 0, handleVideoBrowserResult) + Video.browser(isPresented: $isVideoBrowserViewVisible, selectionLimit: 0, handleVideoBrowserResult) } .background( EmptyView() @@ -85,11 +116,31 @@ private extension BrowserSection { case let .success(browserResult): switch browserResult.first { case let .data(url): - isVideoBrowserViewVisible = false - // TODO: improve this - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - playerURL = url - } + playerURL = url + default: () + } + default: () + } + } + + func handlePhotoBrowserResult(_ result: Result<[BrowserResult], Swift.Error>) { + switch result { + case let .success(browserResult): + switch browserResult.first { + case let .data(uiImage): + image = uiImage + default: () + } + default: () + } + } + + func handleLivePhotoBrowserResult(_ result: Result<[BrowserResult], Swift.Error>) { + switch result { + case let .success(browserResult): + switch browserResult.first { + case let .data(phLivePhoto): + livePhoto = phLivePhoto default: () } default: () From b15a65024cb84549b13d05d570f453b0b35e8fef Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 23:03:25 +0200 Subject: [PATCH 08/11] refactor(): renaming, moved types and typealiases to swiftui product --- ...{PublicAliases.swift => MediaCoreAliases.swift} | 8 ++------ Sources/MediaSwiftUI/API/MediaSwiftUIAliases.swift | 14 ++++++++++++++ .../API/Models/BrowserResult.swift | 0 3 files changed, 16 insertions(+), 6 deletions(-) rename Sources/MediaCore/API/{PublicAliases.swift => MediaCoreAliases.swift} (69%) create mode 100644 Sources/MediaSwiftUI/API/MediaSwiftUIAliases.swift rename Sources/{MediaCore => MediaSwiftUI}/API/Models/BrowserResult.swift (100%) diff --git a/Sources/MediaCore/API/PublicAliases.swift b/Sources/MediaCore/API/MediaCoreAliases.swift similarity index 69% rename from Sources/MediaCore/API/PublicAliases.swift rename to Sources/MediaCore/API/MediaCoreAliases.swift index ca4aa9c..91cac82 100644 --- a/Sources/MediaCore/API/PublicAliases.swift +++ b/Sources/MediaCore/API/MediaCoreAliases.swift @@ -1,6 +1,6 @@ // -// Aliases.swift -// +// MediaCoreAliases.swift +// MediaCore // // Created by Christian Elies on 30.11.19. // @@ -18,13 +18,9 @@ public typealias MediaSubtype = PHAssetMediaSubtype public typealias ResultDataCompletion = (Result) -> Void public typealias ResultGenericCompletion = (Result) -> Void public typealias ResultLivePhotoCompletion = (Result) -> Void -public typealias ResultLivePhotosCompletion = (Result<[BrowserResult], Error>) -> Void public typealias RequestLivePhotoResultHandler = (PHLivePhoto?, [AnyHashable : Any]) -> Void public typealias ResultPHAssetCompletion = (Result) -> Void -public typealias ResultPHAssetsCompletion = (Result<[BrowserResult], Swift.Error>) -> Void public typealias ResultPhotoCompletion = (Result) -> Void -public typealias ResultPhotosCompletion = (Result<[BrowserResult], Swift.Error>) -> Void public typealias ResultURLCompletion = (Result) -> Void public typealias ResultVideoCompletion = (Result) -> Void -public typealias ResultVideosCompletion = (Result<[BrowserResult], Swift.Error>) -> Void public typealias ResultVoidCompletion = (Result) -> Void diff --git a/Sources/MediaSwiftUI/API/MediaSwiftUIAliases.swift b/Sources/MediaSwiftUI/API/MediaSwiftUIAliases.swift new file mode 100644 index 0000000..312a5c9 --- /dev/null +++ b/Sources/MediaSwiftUI/API/MediaSwiftUIAliases.swift @@ -0,0 +1,14 @@ +// +// MediaSwiftUIAliases.swift +// MediaSwiftUI +// +// Created by Christian Elies on 03.05.21. +// + +import MediaCore +import Photos + +public typealias ResultLivePhotosCompletion = (Result<[BrowserResult], Error>) -> Void +public typealias ResultPHAssetsCompletion = (Result<[BrowserResult], Swift.Error>) -> Void +public typealias ResultPhotosCompletion = (Result<[BrowserResult], Swift.Error>) -> Void +public typealias ResultVideosCompletion = (Result<[BrowserResult], Swift.Error>) -> Void diff --git a/Sources/MediaCore/API/Models/BrowserResult.swift b/Sources/MediaSwiftUI/API/Models/BrowserResult.swift similarity index 100% rename from Sources/MediaCore/API/Models/BrowserResult.swift rename to Sources/MediaSwiftUI/API/Models/BrowserResult.swift From 270bff8418d0f1c2a808598e60eb7190eb198a70 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 23:26:18 +0200 Subject: [PATCH 09/11] refactor(swiftui): moved extension from phpickerresult to nsItemProvider --- .../API/LivePhoto/LivePhoto+SwiftUI.swift | 2 +- .../API/PHPicker/NSItemProvider+Error.swift | 18 +++++++++++++ ...e.swift => NSItemProvider+loadImage.swift} | 25 ++++++------------- ...ift => NSItemProvider+loadLivePhoto.swift} | 20 +++++++-------- ...o.swift => NSItemProvider+loadVideo.swift} | 23 +++++++++-------- .../API/Photo/Photo+SwiftUI.swift | 2 +- .../API/Video/Video+SwiftUI.swift | 2 +- 7 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+Error.swift rename Sources/MediaSwiftUI/API/PHPicker/{PHPickerResult+loadImage.swift => NSItemProvider+loadImage.swift} (56%) rename Sources/MediaSwiftUI/API/PHPicker/{PHPickerResult+loadLivePhoto.swift => NSItemProvider+loadLivePhoto.swift} (62%) rename Sources/MediaSwiftUI/API/PHPicker/{PHPickerResult+loadVideo.swift => NSItemProvider+loadVideo.swift} (67%) diff --git a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift index c23de56..423a12e 100644 --- a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift @@ -95,7 +95,7 @@ public extension LivePhoto { completion(result) } else { DispatchQueue.global(qos: .userInitiated).async { - let loadVideos = result.map { $0.loadLivePhoto() } + let loadVideos = result.map { $0.itemProvider.loadLivePhoto() } Publishers.MergeMany(loadVideos) .collect() .sink { result in diff --git a/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+Error.swift b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+Error.swift new file mode 100644 index 0000000..0fff17b --- /dev/null +++ b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+Error.swift @@ -0,0 +1,18 @@ +// +// NSItemProvider+Error.swift +// MediaSwiftUI +// +// Created by Christian Elies on 03.05.21. +// + +import Foundation + +extension NSItemProvider { + /// Represents the errors thrown if loading data from the receiving item provider fails. + public enum Error: Swift.Error { + /// The requested object could not be loaded. + case couldNotLoadObject(underlying: Swift.Error) + /// An unknown error occurred. + case unknown + } +} diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadImage.swift similarity index 56% rename from Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift rename to Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadImage.swift index fa3555c..b9ddf91 100644 --- a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadImage.swift +++ b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadImage.swift @@ -1,36 +1,27 @@ // -// PHPickerResult+loadImage.swift +// NSItemProvider+loadImage.swift // MediaSwiftUI // // Created by Christian Elies on 14.10.20. // -#if !os(tvOS) && (!os(macOS) || targetEnvironment(macCatalyst)) +#if !os(macOS) || targetEnvironment(macCatalyst) import Combine +import Foundation import MediaCore -import PhotosUI -@available(iOS 14, macCatalyst 14, *) -extension PHPickerResult { - /// <#Description#> - public enum Error: Swift.Error { - /// - case couldNotLoadObject(underlying: Swift.Error) - /// - case unknown - } - - /// <#Description#> +extension NSItemProvider { + /// Loads an image from the receiving item provider if one is available. /// - /// - Returns: <#description#> + /// - Returns: A publisher which provides an `UniversalImage` on `success`. public func loadImage() -> AnyPublisher { Future { promise in - guard itemProvider.canLoadObject(ofClass: UniversalImage.self) else { + guard self.canLoadObject(ofClass: UniversalImage.self) else { promise(.failure(Error.couldNotLoadObject(underlying: Error.unknown))) return } - itemProvider.loadObject(ofClass: UniversalImage.self) { newImage, error in + self.loadObject(ofClass: UniversalImage.self) { newImage, error in if let error = error { promise(.failure(Error.couldNotLoadObject(underlying: error))) } else if let newImage = newImage { diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadLivePhoto.swift similarity index 62% rename from Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift rename to Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadLivePhoto.swift index 5b47b4a..48f01f3 100644 --- a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadLivePhoto.swift +++ b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadLivePhoto.swift @@ -1,27 +1,26 @@ // -// PHPickerResult+loadLivePhoto.swift +// NSItemProvider+loadLivePhoto.swift // MediaSwiftUI // // Created by Christian Elies on 03.05.21. // -#if !os(tvOS) && (!os(macOS) || targetEnvironment(macCatalyst)) import Combine -import PhotosUI +import Foundation +import Photos -@available(iOS 14, macCatalyst 14, *) -extension PHPickerResult { - /// <#Description#> - /// - /// - Returns: <#description#> +extension NSItemProvider { + /// Loads a live photo from the receiving item provider if one is available. + /// + /// - Returns: A publisher which provides a `PHLivePhoto` on `success`. public func loadLivePhoto() -> AnyPublisher { Future { promise in - guard itemProvider.canLoadObject(ofClass: PHLivePhoto.self) else { + guard self.canLoadObject(ofClass: PHLivePhoto.self) else { promise(.failure(Error.couldNotLoadObject(underlying: Error.unknown))) return } - itemProvider.loadObject(ofClass: PHLivePhoto.self) { livePhoto, error in + self.loadObject(ofClass: PHLivePhoto.self) { livePhoto, error in if let error = error { promise(.failure(Error.couldNotLoadObject(underlying: error))) } else if let livePhoto = livePhoto { @@ -33,4 +32,3 @@ extension PHPickerResult { }.eraseToAnyPublisher() } } -#endif diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadVideo.swift similarity index 67% rename from Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift rename to Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadVideo.swift index c39480c..b2256d7 100644 --- a/Sources/MediaSwiftUI/API/PHPicker/PHPickerResult+loadVideo.swift +++ b/Sources/MediaSwiftUI/API/PHPicker/NSItemProvider+loadVideo.swift @@ -1,28 +1,32 @@ // -// PHPickerResult+loadVideo.swift +// NSItemProvider+loadVideo.swift // MediaSwiftUI // // Created by Christian Elies on 03.05.21. // -#if !os(tvOS) && !os(macOS) import Combine +import Foundation import PhotosUI -@available(iOS 14, macCatalyst 14, *) -extension PHPickerResult { - /// <#Description#> +extension NSItemProvider { + /// Loads a video from the receiving item provider if one is available. /// - /// - Returns: <#description#> + /// - Returns: A publisher which provides a `URL` of the video on `success`. public func loadVideo() -> AnyPublisher { Future { promise in - let typeIdentifier = UTType.movie.identifier - guard itemProvider.hasItemConformingToTypeIdentifier(typeIdentifier) else { + let typeIdentifier: String + if #available(iOS 14, macCatalyst 14, macOS 11, tvOS 14, *) { + typeIdentifier = UTType.movie.identifier + } else { + typeIdentifier = "public.movie" + } + guard self.hasItemConformingToTypeIdentifier(typeIdentifier) else { promise(.failure(Error.unknown)) return } - itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in + self.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in if let url = url { let fileManager: FileManager = .default let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! @@ -45,4 +49,3 @@ extension PHPickerResult { }.eraseToAnyPublisher() } } -#endif diff --git a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift index 050caa1..28489b9 100644 --- a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift @@ -105,7 +105,7 @@ public extension Photo { completion(result) } else { DispatchQueue.global(qos: .userInitiated).async { - let loadVideos = result.map { $0.loadImage() } + let loadVideos = result.map { $0.itemProvider.loadImage() } Publishers.MergeMany(loadVideos) .collect() .sink { result in diff --git a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift index 1dce909..86a14d4 100644 --- a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift @@ -121,7 +121,7 @@ public extension Video { completion(browserResult) } else { DispatchQueue.global(qos: .userInitiated).async { - let loadVideos = result.map { $0.loadVideo() } + let loadVideos = result.map { $0.itemProvider.loadVideo() } Publishers.MergeMany(loadVideos) .collect() .sink { result in From 0d494b1f4a04f709e22c339955dd1ef576ac6c0e Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Mon, 3 May 2021 23:29:03 +0200 Subject: [PATCH 10/11] chore(example): added present media browser selection example --- .../Media-Example/Views/BrowserSection.swift | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/Example/Media-Example/Views/BrowserSection.swift b/Example/Media-Example/Views/BrowserSection.swift index 80582b4..d8f3136 100644 --- a/Example/Media-Example/Views/BrowserSection.swift +++ b/Example/Media-Example/Views/BrowserSection.swift @@ -7,6 +7,8 @@ // import AVKit +import Combine +import Foundation import MediaCore import MediaSwiftUI import Photos @@ -24,6 +26,10 @@ extension PHLivePhoto: Identifiable { public var id: PHLivePhoto { self } } +struct Garbage { + static var cancellables: [AnyCancellable] = [] +} + struct BrowserSection: View { @State private var isLivePhotoBrowserViewVisible = false @State private var isMediaBrowserViewVisible = false @@ -62,9 +68,7 @@ struct BrowserSection: View { .fullScreenCover(isPresented: $isMediaBrowserViewVisible, onDismiss: { isMediaBrowserViewVisible = false }) { - Media.browser(isPresented: $isMediaBrowserViewVisible, selectionLimit: 0) { result in - debugPrint(result) - } + Media.browser(isPresented: $isMediaBrowserViewVisible, selectionLimit: 0, handleMediaBrowserResult) } Button(action: { @@ -146,4 +150,37 @@ private extension BrowserSection { default: () } } + + func handleMediaBrowserResult(_ result: Result<[BrowserResult], Swift.Error>) { + switch result { + case let .success(browserResult): + switch browserResult.first { + case let .data(itemProvider): + if itemProvider.canLoadObject(ofClass: PHLivePhoto.self) { + itemProvider.loadLivePhoto() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { phLivePhoto in + livePhoto = phLivePhoto + } + .store(in: &Garbage.cancellables) + } else if itemProvider.canLoadObject(ofClass: UIImage.self) { + itemProvider.loadImage() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { uiImage in + image = uiImage + } + .store(in: &Garbage.cancellables) + } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { + itemProvider.loadVideo() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { url in + playerURL = url + } + .store(in: &Garbage.cancellables) + } + default: () + } + default: () + } + } } From 526e7d75f0b654719f62f0f3493d717d906dc491 Mon Sep 17 00:00:00 2001 From: Christian Elies Date: Tue, 4 May 2021 23:34:38 +0200 Subject: [PATCH 11/11] refactor(): small improvements: docs(readme): added some swiftui views --- README.md | 8 ++++++++ .../API/LivePhoto/LivePhoto+SwiftUI.swift | 13 +++++-------- Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift | 2 +- Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift | 13 +++++-------- Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift | 9 +++------ 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b81bee3..b3c6e2b 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,14 @@ Use the `LazyVideos` wrapper if you want to fetch videos only on demand (request - **SwiftUI only**: `video.view` (*some View*) *Get a ready-to-use **SwiftUI** view for displaying the video in your UI* + +- **PHPicker**: SwiftUI port of the `PHPickerViewController` + + *Use the `PHPickerViewController` in your `SwiftUI` applications* + +- **PhotosUILivePhotoView**: SwiftUI port of the `PHLivePhotoView` + + *Use the `PHLivePhotoView` in your `SwiftUI` applications* ### 🚀 `@propertyWrapper` diff --git a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift index 423a12e..314b40f 100644 --- a/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift @@ -95,22 +95,19 @@ public extension LivePhoto { completion(result) } else { DispatchQueue.global(qos: .userInitiated).async { - let loadVideos = result.map { $0.itemProvider.loadLivePhoto() } - Publishers.MergeMany(loadVideos) + let loadLivePhotos = result.map { $0.itemProvider.loadLivePhoto() } + Publishers.MergeMany(loadLivePhotos) .collect() + .receive(on: DispatchQueue.main) .sink { result in switch result { case let .failure(error): - DispatchQueue.main.async { - completion(.failure(error)) - } + completion(.failure(error)) case .finished: () } } receiveValue: { urls in let browserResults = urls.map { BrowserResult.data($0) } - DispatchQueue.main.async { - completion(.success(browserResults)) - } + completion(.success(browserResults)) } .store(in: &Garbage.cancellables) } diff --git a/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift b/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift index 3df87ea..9d88dc5 100644 --- a/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift +++ b/Sources/MediaSwiftUI/API/PHPicker/PHPicker.swift @@ -11,7 +11,7 @@ import PhotosUI import SwiftUI @available(iOS 14, macCatalyst 14, *) -/// `SwiftUI` port of the `PHPicker`. +/// `SwiftUI` port of the `PHPickerViewController`. public struct PHPicker: UIViewControllerRepresentable { /// The coordinator of the view. Mainly it's the delegate of the underlying `PHPickerViewController`. public final class Coordinator: NSObject, PHPickerViewControllerDelegate { diff --git a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift index 28489b9..74c1841 100644 --- a/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Photo/Photo+SwiftUI.swift @@ -105,22 +105,19 @@ public extension Photo { completion(result) } else { DispatchQueue.global(qos: .userInitiated).async { - let loadVideos = result.map { $0.itemProvider.loadImage() } - Publishers.MergeMany(loadVideos) + let loadImages = result.map { $0.itemProvider.loadImage() } + Publishers.MergeMany(loadImages) .collect() + .receive(on: DispatchQueue.main) .sink { result in switch result { case let .failure(error): - DispatchQueue.main.async { - completion(.failure(error)) - } + completion(.failure(error)) case .finished: () } } receiveValue: { urls in let browserResults = urls.map { BrowserResult.data($0) } - DispatchQueue.main.async { - completion(.success(browserResults)) - } + completion(.success(browserResults)) } .store(in: &Garbage.cancellables) } diff --git a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift index 86a14d4..c9d00cb 100644 --- a/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift +++ b/Sources/MediaSwiftUI/API/Video/Video+SwiftUI.swift @@ -124,19 +124,16 @@ public extension Video { let loadVideos = result.map { $0.itemProvider.loadVideo() } Publishers.MergeMany(loadVideos) .collect() + .receive(on: DispatchQueue.main) .sink { result in switch result { case let .failure(error): - DispatchQueue.main.async { - completion(.failure(error)) - } + completion(.failure(error)) case .finished: () } } receiveValue: { urls in let browserResults = urls.map { BrowserResult.data($0) } - DispatchQueue.main.async { - completion(.success(browserResults)) - } + completion(.success(browserResults)) } .store(in: &Garbage.cancellables) }