Skip to content

Commit

Permalink
Merge pull request #12 from crelies/dev
Browse files Browse the repository at this point in the history
Further SwiftUI improvements
  • Loading branch information
Chris authored May 4, 2021
2 parents 1888e4a + 526e7d7 commit 84acbe8
Show file tree
Hide file tree
Showing 23 changed files with 530 additions and 130 deletions.
128 changes: 124 additions & 4 deletions Example/Media-Example/Views/BrowserSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,38 @@
// Copyright © 2021 Christian Elies. All rights reserved.
//

import AVKit
import Combine
import Foundation
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 Garbage {
static var cancellables: [AnyCancellable] = []
}

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")) {
Expand All @@ -26,8 +49,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
Expand All @@ -37,7 +68,7 @@ struct BrowserSection: View {
.fullScreenCover(isPresented: $isMediaBrowserViewVisible, onDismiss: {
isMediaBrowserViewVisible = false
}) {
Media.browser(selectionLimit: 0) { _ in }
Media.browser(isPresented: $isMediaBrowserViewVisible, selectionLimit: 0, handleMediaBrowserResult)
}

Button(action: {
Expand All @@ -48,8 +79,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
Expand All @@ -59,8 +100,87 @@ struct BrowserSection: View {
.fullScreenCover(isPresented: $isVideoBrowserViewVisible, onDismiss: {
isVideoBrowserViewVisible = false
}) {
Video.browser(selectionLimit: 0) { _ in }
Video.browser(isPresented: $isVideoBrowserViewVisible, 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<Video, URL>], Swift.Error>) {
switch result {
case let .success(browserResult):
switch browserResult.first {
case let .data(url):
playerURL = url
default: ()
}
default: ()
}
}

func handlePhotoBrowserResult(_ result: Result<[BrowserResult<Photo, UIImage>], Swift.Error>) {
switch result {
case let .success(browserResult):
switch browserResult.first {
case let .data(uiImage):
image = uiImage
default: ()
}
default: ()
}
}

func handleLivePhotoBrowserResult(_ result: Result<[BrowserResult<LivePhoto, PHLivePhoto>], Swift.Error>) {
switch result {
case let .success(browserResult):
switch browserResult.first {
case let .data(phLivePhoto):
livePhoto = phLivePhoto
default: ()
}
default: ()
}
}

func handleMediaBrowserResult(_ result: Result<[BrowserResult<PHAsset, NSItemProvider>], 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: ()
}
}
}
22 changes: 16 additions & 6 deletions Example/Media-Example/Views/PermissionsSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, PermissionError>) -> 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)
}
}
Expand All @@ -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)
}
}
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
1 change: 1 addition & 0 deletions Sources/MediaCore/API/Media/Media.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public struct Media {

/// Returns the current camera permission.
///
@available(tvOS, unavailable)
public static var currentCameraPermission: AVAuthorizationStatus {
AVCaptureDevice.authorizationStatus(for: .video)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Aliases.swift
//
// MediaCoreAliases.swift
// MediaCore
//
// Created by Christian Elies on 30.11.19.
//
Expand All @@ -18,13 +18,9 @@ public typealias MediaSubtype = PHAssetMediaSubtype
public typealias ResultDataCompletion = (Result<Data, Swift.Error>) -> Void
public typealias ResultGenericCompletion<T> = (Result<T, Swift.Error>) -> Void
public typealias ResultLivePhotoCompletion = (Result<LivePhoto, Error>) -> Void
public typealias ResultLivePhotosCompletion = (Result<[LivePhoto], Error>) -> Void
public typealias RequestLivePhotoResultHandler = (PHLivePhoto?, [AnyHashable : Any]) -> Void
public typealias ResultPHAssetCompletion = (Result<PHAsset, Swift.Error>) -> Void
public typealias ResultPHAssetsCompletion = (Result<[PHAsset], Swift.Error>) -> Void
public typealias ResultPhotoCompletion = (Result<Photo, Swift.Error>) -> Void
public typealias ResultPhotosCompletion = (Result<[Photo], Swift.Error>) -> Void
public typealias ResultURLCompletion = (Result<URL, Swift.Error>) -> Void
public typealias ResultVideoCompletion = (Result<Video, Swift.Error>) -> Void
public typealias ResultVideosCompletion = (Result<[Video], Swift.Error>) -> Void
public typealias ResultVoidCompletion = (Result<Void, Swift.Error>) -> Void
4 changes: 4 additions & 0 deletions Sources/MediaCore/API/Video/Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ public extension Video {

let copyCGImageResult: Result<UniversalImage, Swift.Error> = 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 {
Expand Down
51 changes: 39 additions & 12 deletions Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

#if canImport(SwiftUI)
import Combine
import MediaCore
import PhotosUI
import SwiftUI
Expand Down Expand Up @@ -50,41 +51,67 @@ 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<Bool>, 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<ErrorView: View>(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View {
@ViewBuilder static func browser<ErrorView: View>(isPresented: Binding<Bool>, selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View {
if #available(iOS 14, macOS 11, *) {
PHPicker(configuration: {
var configuration = PHPickerConfiguration()
PHPicker(isPresented: isPresented, configuration: {
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<LivePhoto, PHLivePhoto>? 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 loadLivePhotos = result.map { $0.itemProvider.loadLivePhoto() }
Publishers.MergeMany(loadLivePhotos)
.collect()
.receive(on: DispatchQueue.main)
.sink { result in
switch result {
case let .failure(error):
completion(.failure(error))
case .finished: ()
}
} receiveValue: { urls in
let browserResults = urls.map { BrowserResult<LivePhoto, PHLivePhoto>.data($0) }
completion(.success(browserResults))
}
.store(in: &Garbage.cancellables)
}
}
completion(result)
case let .failure(error): ()
completion(.failure(error))
}
Expand All @@ -94,7 +121,7 @@ public extension LivePhoto {
try ViewCreator.browser(mediaTypes: [.image, .livePhoto]) { (result: Result<LivePhoto, Error>) in
switch result {
case let .success(livePhoto):
completion(.success([livePhoto]))
completion(.success([.media(livePhoto)]))
case let .failure(error):
completion(.failure(error))
}
Expand Down
Loading

0 comments on commit 84acbe8

Please sign in to comment.