Skip to content

Commit 84acbe8

Browse files
author
Chris
authored
Merge pull request #12 from crelies/dev
Further SwiftUI improvements
2 parents 1888e4a + 526e7d7 commit 84acbe8

23 files changed

+530
-130
lines changed

Example/Media-Example/Views/BrowserSection.swift

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,38 @@
66
// Copyright © 2021 Christian Elies. All rights reserved.
77
//
88

9+
import AVKit
10+
import Combine
11+
import Foundation
912
import MediaCore
1013
import MediaSwiftUI
14+
import Photos
1115
import SwiftUI
1216

17+
extension URL: Identifiable {
18+
public var id: String { absoluteString }
19+
}
20+
21+
extension UIImage: Identifiable {
22+
public var id: UIImage { self }
23+
}
24+
25+
extension PHLivePhoto: Identifiable {
26+
public var id: PHLivePhoto { self }
27+
}
28+
29+
struct Garbage {
30+
static var cancellables: [AnyCancellable] = []
31+
}
32+
1333
struct BrowserSection: View {
1434
@State private var isLivePhotoBrowserViewVisible = false
1535
@State private var isMediaBrowserViewVisible = false
1636
@State private var isPhotoBrowserViewVisible = false
1737
@State private var isVideoBrowserViewVisible = false
38+
@State private var playerURL: URL?
39+
@State private var image: UIImage?
40+
@State private var livePhoto: PHLivePhoto?
1841

1942
var body: some View {
2043
Section(header: Label("Browser", systemImage: "photo.on.rectangle.angled")) {
@@ -26,8 +49,16 @@ struct BrowserSection: View {
2649
.fullScreenCover(isPresented: $isLivePhotoBrowserViewVisible, onDismiss: {
2750
isLivePhotoBrowserViewVisible = false
2851
}) {
29-
LivePhoto.browser(selectionLimit: 0) { _ in }
52+
LivePhoto.browser(isPresented: $isLivePhotoBrowserViewVisible, selectionLimit: 0, handleLivePhotoBrowserResult)
3053
}
54+
.background(
55+
EmptyView()
56+
.sheet(item: $livePhoto, onDismiss: {
57+
livePhoto = nil
58+
}) { livePhoto in
59+
PhotosUILivePhotoView(phLivePhoto: livePhoto)
60+
}
61+
)
3162

3263
Button(action: {
3364
isMediaBrowserViewVisible = true
@@ -37,7 +68,7 @@ struct BrowserSection: View {
3768
.fullScreenCover(isPresented: $isMediaBrowserViewVisible, onDismiss: {
3869
isMediaBrowserViewVisible = false
3970
}) {
40-
Media.browser(selectionLimit: 0) { _ in }
71+
Media.browser(isPresented: $isMediaBrowserViewVisible, selectionLimit: 0, handleMediaBrowserResult)
4172
}
4273

4374
Button(action: {
@@ -48,8 +79,18 @@ struct BrowserSection: View {
4879
.fullScreenCover(isPresented: $isPhotoBrowserViewVisible, onDismiss: {
4980
isPhotoBrowserViewVisible = false
5081
}) {
51-
Photo.browser(selectionLimit: 0) { _ in }
82+
Photo.browser(isPresented: $isPhotoBrowserViewVisible, selectionLimit: 0, handlePhotoBrowserResult)
5283
}
84+
.background(
85+
EmptyView()
86+
.sheet(item: $image, onDismiss: {
87+
image = nil
88+
}) { uiImage in
89+
Image(uiImage: uiImage)
90+
.resizable()
91+
.aspectRatio(contentMode: .fit)
92+
}
93+
)
5394

5495
Button(action: {
5596
isVideoBrowserViewVisible = true
@@ -59,8 +100,87 @@ struct BrowserSection: View {
59100
.fullScreenCover(isPresented: $isVideoBrowserViewVisible, onDismiss: {
60101
isVideoBrowserViewVisible = false
61102
}) {
62-
Video.browser(selectionLimit: 0) { _ in }
103+
Video.browser(isPresented: $isVideoBrowserViewVisible, selectionLimit: 0, handleVideoBrowserResult)
104+
}
105+
.background(
106+
EmptyView()
107+
.sheet(item: $playerURL, onDismiss: {
108+
playerURL = nil
109+
}) { url in
110+
VideoPlayer(player: .init(url: url))
111+
}
112+
)
113+
}
114+
}
115+
}
116+
117+
private extension BrowserSection {
118+
func handleVideoBrowserResult(_ result: Result<[BrowserResult<Video, URL>], Swift.Error>) {
119+
switch result {
120+
case let .success(browserResult):
121+
switch browserResult.first {
122+
case let .data(url):
123+
playerURL = url
124+
default: ()
125+
}
126+
default: ()
127+
}
128+
}
129+
130+
func handlePhotoBrowserResult(_ result: Result<[BrowserResult<Photo, UIImage>], Swift.Error>) {
131+
switch result {
132+
case let .success(browserResult):
133+
switch browserResult.first {
134+
case let .data(uiImage):
135+
image = uiImage
136+
default: ()
137+
}
138+
default: ()
139+
}
140+
}
141+
142+
func handleLivePhotoBrowserResult(_ result: Result<[BrowserResult<LivePhoto, PHLivePhoto>], Swift.Error>) {
143+
switch result {
144+
case let .success(browserResult):
145+
switch browserResult.first {
146+
case let .data(phLivePhoto):
147+
livePhoto = phLivePhoto
148+
default: ()
149+
}
150+
default: ()
151+
}
152+
}
153+
154+
func handleMediaBrowserResult(_ result: Result<[BrowserResult<PHAsset, NSItemProvider>], Swift.Error>) {
155+
switch result {
156+
case let .success(browserResult):
157+
switch browserResult.first {
158+
case let .data(itemProvider):
159+
if itemProvider.canLoadObject(ofClass: PHLivePhoto.self) {
160+
itemProvider.loadLivePhoto()
161+
.receive(on: DispatchQueue.main)
162+
.sink(receiveCompletion: { _ in }) { phLivePhoto in
163+
livePhoto = phLivePhoto
164+
}
165+
.store(in: &Garbage.cancellables)
166+
} else if itemProvider.canLoadObject(ofClass: UIImage.self) {
167+
itemProvider.loadImage()
168+
.receive(on: DispatchQueue.main)
169+
.sink(receiveCompletion: { _ in }) { uiImage in
170+
image = uiImage
171+
}
172+
.store(in: &Garbage.cancellables)
173+
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
174+
itemProvider.loadVideo()
175+
.receive(on: DispatchQueue.main)
176+
.sink(receiveCompletion: { _ in }) { url in
177+
playerURL = url
178+
}
179+
.store(in: &Garbage.cancellables)
180+
}
181+
default: ()
63182
}
183+
default: ()
64184
}
65185
}
66186
}

Example/Media-Example/Views/PermissionsSection.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,26 @@
88

99
import AVFoundation
1010
import MediaCore
11+
import Photos
1112
import SwiftUI
1213

1314
struct PermissionsSection: View {
1415
@State private var isLimitedLibraryPickerPresented = false
16+
@State private var cameraPermission: AVAuthorizationStatus = .notDetermined
17+
@State private var mediaPermission: PHAuthorizationStatus = .notDetermined
1518

1619
var requestedPermission: (Result<Void, PermissionError>) -> Void
17-
20+
1821
var body: some View {
1922
Section(header: Text("Permissions")) {
2023
Button(action: {
21-
Media.requestCameraPermission { result in
22-
debugPrint(result)
24+
Media.requestCameraPermission { _ in
25+
cameraPermission = Media.currentCameraPermission
2326
}
2427
}) {
2528
HStack {
2629
Text("Trigger camera permission request")
27-
Toggle("", isOn: .constant(Media.currentCameraPermission == .authorized))
30+
Toggle("", isOn: .constant(cameraPermission == .authorized))
2831
.disabled(true)
2932
}
3033
}
@@ -38,17 +41,24 @@ struct PermissionsSection: View {
3841
}) {
3942
HStack {
4043
Text("Trigger photo library permission request")
41-
Toggle("", isOn: .constant(Media.currentPermission == .authorized))
44+
Toggle("", isOn: .constant(mediaPermission == .authorized))
4245
.disabled(true)
4346
}
4447
}
4548
.background(PHPicker(isPresented: $isLimitedLibraryPickerPresented))
4649
}
50+
.onAppear {
51+
cameraPermission = Media.currentCameraPermission
52+
mediaPermission = Media.currentPermission
53+
}
4754
}
4855
}
4956

5057
private extension PermissionsSection {
5158
func requestPermission() {
52-
Media.requestPermission(requestedPermission)
59+
Media.requestPermission { result in
60+
mediaPermission = Media.currentPermission
61+
requestedPermission(result)
62+
}
5363
}
5464
}

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,14 @@ Use the `LazyVideos` wrapper if you want to fetch videos only on demand (request
406406
- **SwiftUI only**: `video.view` (*some View*)
407407

408408
*Get a ready-to-use **SwiftUI** view for displaying the video in your UI*
409+
410+
- **PHPicker**: SwiftUI port of the `PHPickerViewController`
411+
412+
*Use the `PHPickerViewController` in your `SwiftUI` applications*
413+
414+
- **PhotosUILivePhotoView**: SwiftUI port of the `PHLivePhotoView`
415+
416+
*Use the `PHLivePhotoView` in your `SwiftUI` applications*
409417

410418
### 🚀 `@propertyWrapper`
411419

Sources/MediaCore/API/Media/Media.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public struct Media {
3838

3939
/// Returns the current camera permission.
4040
///
41+
@available(tvOS, unavailable)
4142
public static var currentCameraPermission: AVAuthorizationStatus {
4243
AVCaptureDevice.authorizationStatus(for: .video)
4344
}

Sources/MediaCore/API/PublicAliases.swift renamed to Sources/MediaCore/API/MediaCoreAliases.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
2-
// Aliases.swift
3-
//
2+
// MediaCoreAliases.swift
3+
// MediaCore
44
//
55
// Created by Christian Elies on 30.11.19.
66
//
@@ -18,13 +18,9 @@ public typealias MediaSubtype = PHAssetMediaSubtype
1818
public typealias ResultDataCompletion = (Result<Data, Swift.Error>) -> Void
1919
public typealias ResultGenericCompletion<T> = (Result<T, Swift.Error>) -> Void
2020
public typealias ResultLivePhotoCompletion = (Result<LivePhoto, Error>) -> Void
21-
public typealias ResultLivePhotosCompletion = (Result<[LivePhoto], Error>) -> Void
2221
public typealias RequestLivePhotoResultHandler = (PHLivePhoto?, [AnyHashable : Any]) -> Void
2322
public typealias ResultPHAssetCompletion = (Result<PHAsset, Swift.Error>) -> Void
24-
public typealias ResultPHAssetsCompletion = (Result<[PHAsset], Swift.Error>) -> Void
2523
public typealias ResultPhotoCompletion = (Result<Photo, Swift.Error>) -> Void
26-
public typealias ResultPhotosCompletion = (Result<[Photo], Swift.Error>) -> Void
2724
public typealias ResultURLCompletion = (Result<URL, Swift.Error>) -> Void
2825
public typealias ResultVideoCompletion = (Result<Video, Swift.Error>) -> Void
29-
public typealias ResultVideosCompletion = (Result<[Video], Swift.Error>) -> Void
3026
public typealias ResultVoidCompletion = (Result<Void, Swift.Error>) -> Void

Sources/MediaCore/API/Video/Video.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ public extension Video {
153153

154154
let copyCGImageResult: Result<UniversalImage, Swift.Error> = Result {
155155
let cgImage = try generator.copyCGImage(at: requestedTime, actualTime: nil)
156+
#if os(macOS)
157+
return UniversalImage(cgImage: cgImage, size: .init(width: cgImage.width, height: cgImage.height))
158+
#else
156159
return UniversalImage(cgImage: cgImage)
160+
#endif
157161
}
158162

159163
DispatchQueue.main.async {

Sources/MediaSwiftUI/API/LivePhoto/LivePhoto+SwiftUI.swift

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
#if canImport(SwiftUI)
9+
import Combine
910
import MediaCore
1011
import PhotosUI
1112
import SwiftUI
@@ -50,41 +51,67 @@ public extension LivePhoto {
5051
/// Creates a ready-to-use `SwiftUI` view for browsing `LivePhoto`s in the photo library
5152
/// If an error occurs during initialization a `SwiftUI.Text` with the `localizedDescription` is shown.
5253
///
54+
/// - Parameter isPresented: A binding to whether the underlying picker is presented.
5355
/// - 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`.
5456
/// - Parameter completion: A closure which gets the selected `LivePhoto` on `success` or `Error` on `failure`.
5557
///
5658
/// - Returns: some View
57-
static func browser(selectionLimit: Int = 1, _ completion: @escaping ResultLivePhotosCompletion) -> some View {
58-
browser(selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion)
59+
static func browser(isPresented: Binding<Bool>, selectionLimit: Int = 1, _ completion: @escaping ResultLivePhotosCompletion) -> some View {
60+
browser(isPresented: isPresented, selectionLimit: selectionLimit, errorView: { error in Text(error.localizedDescription) }, completion)
5961
}
6062

6163
/// Creates a ready-to-use `SwiftUI` view for browsing `LivePhoto`s in the photo library
6264
/// If an error occurs during initialization the provided `errorView` closure is used to construct the view to be displayed.
6365
///
66+
/// - Parameter isPresented: A binding to whether the underlying picker is presented.
6467
/// - 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`.
6568
/// - Parameter errorView: A closure that constructs an error view for the given error.
6669
/// - Parameter completion: A closure which gets the selected `LivePhoto` on `success` or `Error` on `failure`.
6770
///
6871
/// - Returns: some View
69-
@ViewBuilder static func browser<ErrorView: View>(selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View {
72+
@ViewBuilder static func browser<ErrorView: View>(isPresented: Binding<Bool>, selectionLimit: Int = 1, @ViewBuilder errorView: (Swift.Error) -> ErrorView, _ completion: @escaping ResultLivePhotosCompletion) -> some View {
7073
if #available(iOS 14, macOS 11, *) {
71-
PHPicker(configuration: {
72-
var configuration = PHPickerConfiguration()
74+
PHPicker(isPresented: isPresented, configuration: {
75+
var configuration = PHPickerConfiguration(photoLibrary: .shared())
7376
configuration.filter = .livePhotos
7477
configuration.selectionLimit = selectionLimit
78+
configuration.preferredAssetRepresentationMode = .current
7579
return configuration
7680
}()) { result in
7781
switch result {
7882
case let .success(result):
79-
let result = Result {
80-
try result.compactMap { object -> LivePhoto? in
81-
guard let assetIdentifier = object.assetIdentifier else {
82-
return nil
83+
if Media.currentPermission == .authorized {
84+
let result = Result {
85+
try result.compactMap { object -> BrowserResult<LivePhoto, PHLivePhoto>? in
86+
guard let assetIdentifier = object.assetIdentifier else {
87+
return nil
88+
}
89+
guard let livePhoto = try LivePhoto.with(identifier: .init(stringLiteral: assetIdentifier)) else {
90+
return nil
91+
}
92+
return .media(livePhoto)
8393
}
84-
return try LivePhoto.with(identifier: .init(stringLiteral: assetIdentifier))
94+
}
95+
completion(result)
96+
} else {
97+
DispatchQueue.global(qos: .userInitiated).async {
98+
let loadLivePhotos = result.map { $0.itemProvider.loadLivePhoto() }
99+
Publishers.MergeMany(loadLivePhotos)
100+
.collect()
101+
.receive(on: DispatchQueue.main)
102+
.sink { result in
103+
switch result {
104+
case let .failure(error):
105+
completion(.failure(error))
106+
case .finished: ()
107+
}
108+
} receiveValue: { urls in
109+
let browserResults = urls.map { BrowserResult<LivePhoto, PHLivePhoto>.data($0) }
110+
completion(.success(browserResults))
111+
}
112+
.store(in: &Garbage.cancellables)
85113
}
86114
}
87-
completion(result)
88115
case let .failure(error): ()
89116
completion(.failure(error))
90117
}
@@ -94,7 +121,7 @@ public extension LivePhoto {
94121
try ViewCreator.browser(mediaTypes: [.image, .livePhoto]) { (result: Result<LivePhoto, Error>) in
95122
switch result {
96123
case let .success(livePhoto):
97-
completion(.success([livePhoto]))
124+
completion(.success([.media(livePhoto)]))
98125
case let .failure(error):
99126
completion(.failure(error))
100127
}

0 commit comments

Comments
 (0)