From c7e91aa72cedee362c2b6accfafa42523c7205d9 Mon Sep 17 00:00:00 2001 From: Lucian Cerbu Date: Fri, 8 Dec 2023 13:37:43 +0200 Subject: [PATCH] Added new custom photo picker view. --- Permanent.xcodeproj/project.pbxproj | 60 ++++++++ .../UIViewController/PhotoLibraryPicker.swift | 49 +++++++ .../CustomPhotoLibraryViewModel.swift | 17 +++ .../ViewModels/FetchAlbumsViewModel.swift | 109 +++++++++++++++ .../Views/CustomPhotoLibraryView.swift | 131 ++++++++++++++++++ .../Views/FetchAlbumsView.swift | 56 ++++++++ .../Views/PhotoDetailView.swift | 61 ++++++++ .../Views/PhotoThumbnailView.swift | 65 +++++++++ .../ViewController/MainViewController.swift | 69 +++++++-- .../Contents.json | 12 ++ .../checkmarkSelectItem.svg | 4 + 11 files changed, 625 insertions(+), 8 deletions(-) create mode 100644 Permanent/Modules/CustomPhotoLibrary/UIViewController/PhotoLibraryPicker.swift create mode 100644 Permanent/Modules/CustomPhotoLibrary/ViewModels/CustomPhotoLibraryViewModel.swift create mode 100644 Permanent/Modules/CustomPhotoLibrary/ViewModels/FetchAlbumsViewModel.swift create mode 100644 Permanent/Modules/CustomPhotoLibrary/Views/CustomPhotoLibraryView.swift create mode 100644 Permanent/Modules/CustomPhotoLibrary/Views/FetchAlbumsView.swift create mode 100644 Permanent/Modules/CustomPhotoLibrary/Views/PhotoDetailView.swift create mode 100644 Permanent/Modules/CustomPhotoLibrary/Views/PhotoThumbnailView.swift create mode 100644 Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/Contents.json create mode 100644 Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/checkmarkSelectItem.svg diff --git a/Permanent.xcodeproj/project.pbxproj b/Permanent.xcodeproj/project.pbxproj index 2c488cc3..897b1543 100644 --- a/Permanent.xcodeproj/project.pbxproj +++ b/Permanent.xcodeproj/project.pbxproj @@ -125,6 +125,8 @@ 5E3E12452A41F16500682DE5 /* EmptyFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE3C94E25385FB500EC3A66 /* EmptyFolderView.swift */; }; 5E3E12462A41F3B700682DE5 /* FileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C37D4628AD62060062D9EB /* FileCollectionViewCell.swift */; }; 5E3E124B2A431F9600682DE5 /* EnvVars.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3E124A2A431F9600682DE5 /* EnvVars.generated.swift */; }; + 5E41FAF52B2335C5000B79FD /* FetchAlbumsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E41FAF42B2335C5000B79FD /* FetchAlbumsView.swift */; }; + 5E41FAF72B2335F2000B79FD /* PhotoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E41FAF62B2335F2000B79FD /* PhotoDetailView.swift */; }; 5E4455D32A08F0BB00A56235 /* TrustedStewardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4455D22A08F0BB00A56235 /* TrustedStewardViewController.swift */; }; 5E4455D42A08F1F100A56235 /* TrustedStewardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4455D22A08F0BB00A56235 /* TrustedStewardViewController.swift */; }; 5E46217225C17C5A007642BE /* AccountInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E46217125C17C5A007642BE /* AccountInfoViewController.swift */; }; @@ -288,6 +290,11 @@ 5EA88C482ABAF95100876251 /* EditFileNamesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA88C472ABAF95100876251 /* EditFileNamesViewModelTests.swift */; }; 5EA88C4A2ABAF96D00876251 /* EditLocationViewModelTets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA88C492ABAF96D00876251 /* EditLocationViewModelTets.swift */; }; 5EA88C4C2ABAF98400876251 /* EditTagsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA88C4B2ABAF98400876251 /* EditTagsViewModelTests.swift */; }; + 5EADAD962B1F9E150099D3B5 /* CustomPhotoLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD952B1F9E150099D3B5 /* CustomPhotoLibraryView.swift */; }; + 5EADAD982B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD972B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift */; }; + 5EADAD9A2B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD992B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift */; }; + 5EADAD9C2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD9B2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift */; }; + 5EADAD9E2B1FCAB30099D3B5 /* PhotoThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD9D2B1FCAB30099D3B5 /* PhotoThumbnailView.swift */; }; 5EADF803262475D500D14E9C /* TagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADF801262475D500D14E9C /* TagCollectionViewCell.swift */; }; 5EADF804262475D500D14E9C /* TagCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5EADF802262475D500D14E9C /* TagCollectionViewCell.xib */; }; 5EADF8082625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADF8062625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift */; }; @@ -999,6 +1006,8 @@ 5E3E12222A41906700682DE5 /* ShareManagementRemoteDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareManagementRemoteDataSource.swift; path = Permanent/Services/Repositories/Datasources/Remote/ShareManagementRemoteDataSource.swift; sourceTree = SOURCE_ROOT; }; 5E3E12492A42EEA900682DE5 /* ShareExtensionDEV-Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "ShareExtensionDEV-Release.entitlements"; sourceTree = ""; }; 5E3E124A2A431F9600682DE5 /* EnvVars.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvVars.generated.swift; sourceTree = ""; }; + 5E41FAF42B2335C5000B79FD /* FetchAlbumsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAlbumsView.swift; sourceTree = ""; }; + 5E41FAF62B2335F2000B79FD /* PhotoDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoDetailView.swift; sourceTree = ""; }; 5E4455D22A08F0BB00A56235 /* TrustedStewardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedStewardViewController.swift; sourceTree = ""; }; 5E46217125C17C5A007642BE /* AccountInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfoViewController.swift; sourceTree = ""; }; 5E46217425C17CE2007642BE /* InfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewModel.swift; sourceTree = ""; }; @@ -1112,6 +1121,11 @@ 5EA88C472ABAF95100876251 /* EditFileNamesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFileNamesViewModelTests.swift; sourceTree = ""; }; 5EA88C492ABAF96D00876251 /* EditLocationViewModelTets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationViewModelTets.swift; sourceTree = ""; }; 5EA88C4B2ABAF98400876251 /* EditTagsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTagsViewModelTests.swift; sourceTree = ""; }; + 5EADAD952B1F9E150099D3B5 /* CustomPhotoLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPhotoLibraryView.swift; sourceTree = ""; }; + 5EADAD972B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPhotoLibraryViewModel.swift; sourceTree = ""; }; + 5EADAD992B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAlbumsViewModel.swift; sourceTree = ""; }; + 5EADAD9B2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; + 5EADAD9D2B1FCAB30099D3B5 /* PhotoThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoThumbnailView.swift; sourceTree = ""; }; 5EADF801262475D500D14E9C /* TagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCollectionViewCell.swift; sourceTree = ""; }; 5EADF802262475D500D14E9C /* TagCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TagCollectionViewCell.xib; sourceTree = ""; }; 5EADF8062625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsNamesCollectionViewCell.swift; sourceTree = ""; }; @@ -2262,6 +2276,14 @@ path = ViewModel; sourceTree = ""; }; + 5E41FAF82B233622000B79FD /* UIViewController */ = { + isa = PBXGroup; + children = ( + 5EADAD9B2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift */, + ); + path = UIViewController; + sourceTree = ""; + }; 5E46217725C194E8007642BE /* View */ = { isa = PBXGroup; children = ( @@ -2296,6 +2318,7 @@ 5E4739C32A41020A00A20D85 /* Storage */, 5E0B8EE62ACC1E990077A862 /* UploadManager */, 5E4739A12A40EC8300A20D85 /* WelcomePage */, + 5EADAD922B1F9DB80099D3B5 /* CustomPhotoLibrary */, ); path = Modules; sourceTree = ""; @@ -2918,6 +2941,36 @@ path = EditDate; sourceTree = ""; }; + 5EADAD922B1F9DB80099D3B5 /* CustomPhotoLibrary */ = { + isa = PBXGroup; + children = ( + 5EADAD932B1F9DE00099D3B5 /* Views */, + 5EADAD942B1F9DE80099D3B5 /* ViewModels */, + 5E41FAF82B233622000B79FD /* UIViewController */, + ); + path = CustomPhotoLibrary; + sourceTree = ""; + }; + 5EADAD932B1F9DE00099D3B5 /* Views */ = { + isa = PBXGroup; + children = ( + 5EADAD952B1F9E150099D3B5 /* CustomPhotoLibraryView.swift */, + 5EADAD9D2B1FCAB30099D3B5 /* PhotoThumbnailView.swift */, + 5E41FAF42B2335C5000B79FD /* FetchAlbumsView.swift */, + 5E41FAF62B2335F2000B79FD /* PhotoDetailView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 5EADAD942B1F9DE80099D3B5 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 5EADAD972B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift */, + 5EADAD992B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; 5ECBAF9F2A1B5F0200FACFDF /* LegacyPlanning */ = { isa = PBXGroup; children = ( @@ -4303,6 +4356,7 @@ 5EADF8082625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift in Sources */, 5E4E4CCF29826F4000FEF292 /* ArchiveSettingsTagsHeaderCollectionView.swift in Sources */, 5E31B62F292FA9BC00934408 /* ShareManagementAccessRolesCollectionViewCell.swift in Sources */, + 5EADAD9C2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift in Sources */, 5E6673132A79B4E3001C49CC /* CustomTextFieldStyle.swift in Sources */, F557A64927A1C81600C061D4 /* OnlinePresenceTableViewCell.swift in Sources */, BC62D580254181BD00E84DA9 /* DataExtension.swift in Sources */, @@ -4315,6 +4369,7 @@ BCF4E5DB255C3782003505BA /* AttachmentRecordVO.swift in Sources */, 06644A8B24EBF4CD003CD359 /* CustomView.swift in Sources */, BC6D3B4D2514E57500390927 /* NetworkSessionProtocol.swift in Sources */, + 5EADAD982B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift in Sources */, 5E5AB60F2A6494170030BF61 /* AddTagsView.swift in Sources */, F54596C325FF737200E0BC5F /* FilePreviewNavigationController.swift in Sources */, 927AE0CD2A2F290E00BDF26A /* BannerView.swift in Sources */, @@ -4345,6 +4400,7 @@ BC59BACE25C2B8BD005A45D3 /* ActivityFeedViewModelDelegate.swift in Sources */, BC04DAC425669AC4009D9C0C /* FolderDestVOPayload.swift in Sources */, BC6AF9A625922CBA00483BBA /* AccountVOPayload.swift in Sources */, + 5EADAD962B1F9E150099D3B5 /* CustomPhotoLibraryView.swift in Sources */, BC0D99B0256C08F000D29041 /* SideMenuViewController.swift in Sources */, 5ECBAF9D2A1B5EEE00FACFDF /* ArchiveSteward.swift in Sources */, 5E46217225C17C5A007642BE /* AccountInfoViewController.swift in Sources */, @@ -4361,6 +4417,7 @@ BD96908F25D17E3700E49AB3 /* RegisterRecordResponse.swift in Sources */, BC6AF9A32590E6EC00483BBA /* UILabelExtension.swift in Sources */, 5ED73B252613C30E002F9861 /* TagEndpoint.swift in Sources */, + 5E41FAF52B2335C5000B79FD /* FetchAlbumsView.swift in Sources */, 5E2CFA27275116480055941C /* PublicProfileAboutPageViewController.swift in Sources */, BC326AB32527393400A69597 /* AccountUpdateVO.swift in Sources */, 06644A8724EA772D003CD359 /* CustomButton.swift in Sources */, @@ -4572,6 +4629,7 @@ F52D2B88292E3CA40008D047 /* ShareManagementHeaderCollectionReusableView.swift in Sources */, BCD414E4257FCEB50019548F /* SharebyURLVOPayload.swift in Sources */, BC6D3B512514EF3300390927 /* OperationProtocol.swift in Sources */, + 5EADAD9E2B1FCAB30099D3B5 /* PhotoThumbnailView.swift in Sources */, F559F87528F990F20015A522 /* FolderContentViewModel.swift in Sources */, F58B8B9C2757F67A00D43606 /* PublicArchiveViewController.swift in Sources */, 927AE0CB2A2634CA00BDF26A /* GradientView.swift in Sources */, @@ -4669,6 +4727,7 @@ 5ED3B3BC29FAB2BE000CFF48 /* LegacyPlanningSaveButton.swift in Sources */, 5E559EC829BF438200F129BF /* IntExtension.swift in Sources */, 5EF0B6E927F43D62000CBAF6 /* PublicGalleryViewModel.swift in Sources */, + 5EADAD9A2B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift in Sources */, 5E3E121D2A41902C00682DE5 /* AccountRemoteDataSource.swift in Sources */, BC04DACA2567D040009D9C0C /* RelocateRecordPayload.swift in Sources */, 92430A732B078EEE0098597D /* GiftStorageView.swift in Sources */, @@ -4722,6 +4781,7 @@ BC6D3B4B2514E46E00390927 /* APIEnvironment.swift in Sources */, F502C48C26D6132F00657D37 /* AlbumsViewController.swift in Sources */, F509D6FE2742E2FA007E4594 /* SearchFilesViewModel.swift in Sources */, + 5E41FAF72B2335F2000B79FD /* PhotoDetailView.swift in Sources */, BC59BACD25C2B8BD005A45D3 /* ActivityFeedViewModel.swift in Sources */, BC4526E8251CACDF00E24A51 /* CodableHelper.swift in Sources */, 92C73E402A13BDC8000EF633 /* LegacyAccountStatusCell.swift in Sources */, diff --git a/Permanent/Modules/CustomPhotoLibrary/UIViewController/PhotoLibraryPicker.swift b/Permanent/Modules/CustomPhotoLibrary/UIViewController/PhotoLibraryPicker.swift new file mode 100644 index 00000000..54118971 --- /dev/null +++ b/Permanent/Modules/CustomPhotoLibrary/UIViewController/PhotoLibraryPicker.swift @@ -0,0 +1,49 @@ +// +// PhotoLibraryPicker.swift +// Permanent +// +// Created by Lucian Cerbu on 05.12.2023. + +import SwiftUI +import PhotosUI + +struct PhotoLibraryPicker: UIViewControllerRepresentable { + @Binding var selectedAssets: [PHAsset] + @Environment(\.presentationMode) var presentationMode + + func makeUIViewController(context: Context) -> PHPickerViewController { + var config = PHPickerConfiguration(photoLibrary: .shared()) + config.selectionLimit = 0 // 0 for unlimited selection + config.filter = .images + + let picker = PHPickerViewController(configuration: config) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { + // No update needed here + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, PHPickerViewControllerDelegate { + let parent: PhotoLibraryPicker + + init(_ parent: PhotoLibraryPicker) { + self.parent = parent + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + parent.presentationMode.wrappedValue.dismiss() + + let identifiers = results.compactMap(\.assetIdentifier) + let assets = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil) + DispatchQueue.main.async { + self.parent.selectedAssets = assets.objects(at: IndexSet(0.. = [] + @Published var imagesInPhotos: [PHAsset] = [] + @Published var imagesInAlbums: [PHAsset] = [] + @Published var selectedSegment = 0 +} diff --git a/Permanent/Modules/CustomPhotoLibrary/ViewModels/FetchAlbumsViewModel.swift b/Permanent/Modules/CustomPhotoLibrary/ViewModels/FetchAlbumsViewModel.swift new file mode 100644 index 00000000..0147b504 --- /dev/null +++ b/Permanent/Modules/CustomPhotoLibrary/ViewModels/FetchAlbumsViewModel.swift @@ -0,0 +1,109 @@ +// +// FetchAlbumsViewModel.swift +// Permanent +// +// Created by Lucian Cerbu on 05.12.2023. + +import Foundation +import Photos +import UIKit +import SwiftUI + +struct PHFetchResultCollection: RandomAccessCollection, Equatable { + + typealias Element = PHAsset + typealias Index = Int + + let fetchResult: PHFetchResult + + var endIndex: Int { fetchResult.count } + var startIndex: Int { 0 } + + subscript(position: Int) -> PHAsset { + fetchResult.object(at: fetchResult.count - position - 1) + } +} + +enum QueryError: Error { + case NotFound +} + +class FetchAlbumsViewModel: ObservableObject { + + @Published var assets: [PHAsset] = [] + @Published var isLoadingPhotos: Bool = false + + var imageCachingManager = PHCachingImageManager() + + init() { + PHPhotoLibrary.requestAuthorization(for: .readWrite) {[weak self] status in + switch status { + case .authorized: + self?.loadImagesForSegment(segment: 0) + default: + break + } + } + } + + func loadImagesForSegment(segment: Int) { + isLoadingPhotos = true + if segment == 0 { + imageCachingManager.allowsCachingHighQualityImages = true + + let fetchOptions = PHFetchOptions() + fetchOptions.includeHiddenAssets = false + fetchOptions.predicate = NSPredicate(format: "mediaType = %d || mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue) + fetchOptions.sortDescriptors = [ + NSSortDescriptor(key: "creationDate", ascending: false) + ] + + DispatchQueue.main.async { + let result = PHAsset.fetchAssets(with: fetchOptions) + var assets: [PHAsset] = [] + result.enumerateObjects { object, index, stop in + assets.append(object) + } + self.assets = assets + } + } else { + var assets: [PHAsset] = [] + self.assets = assets + } + isLoadingPhotos = false + } + + func fetchImage( + byLocalIdentifier localId: String, + targetSize: CGSize = CGSize(width: 150, height: 150), + contentMode: PHImageContentMode = .aspectFill + ) async throws -> Image? { + guard let asset = assets.first(where: { $0.localIdentifier == localId }) else { + throw QueryError.NotFound + } + + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + options.resizeMode = .fast + options.isNetworkAccessAllowed = false + options.isSynchronous = false + + return try await withCheckedThrowingContinuation { [weak self] continuation in + self?.imageCachingManager.requestImage( + for: asset, + targetSize: targetSize, + contentMode: contentMode, + options: options, + resultHandler: { image, info in + if let error = info?[PHImageErrorKey] as? Error { + continuation.resume(throwing: error) + return + } + if let image = image { + continuation.resume(returning: Image(uiImage: (image))) + } + } + ) + } + } +} diff --git a/Permanent/Modules/CustomPhotoLibrary/Views/CustomPhotoLibraryView.swift b/Permanent/Modules/CustomPhotoLibrary/Views/CustomPhotoLibraryView.swift new file mode 100644 index 00000000..7ae98e54 --- /dev/null +++ b/Permanent/Modules/CustomPhotoLibrary/Views/CustomPhotoLibraryView.swift @@ -0,0 +1,131 @@ +// +// CustomPhotoLibraryView.swift +// Permanent +// +// Created by Lucian Cerbu on 05.12.2023. + +import SwiftUI +import PhotosUI + +struct CustomPhotoLibraryView: View { + @StateObject var viewModel: CustomPhotoLibraryViewModel + @Environment(\.presentationMode) var presentationMode + var dismissAction: ((Bool) -> Void)? + let photoLibraryService = FetchAlbumsViewModel() + + private var displayedImages: [PHAsset] { + return viewModel.selectedSegment == 0 ? viewModel.imagesInPhotos : viewModel.imagesInAlbums + } + + var body: some View { + ZStack { + Color.whiteGray + .frame(maxWidth: .infinity, maxHeight: .infinity) + .edgesIgnoringSafeArea(.all) + VStack { + // Custom navigation bar + HStack { + Button("Cancel") { + dismiss() + } + Spacer() + Picker("Select an option", selection: $viewModel.selectedSegment) { + Text("Photos").tag(0) + Text("Albums").tag(1) + } + .pickerStyle(SegmentedPickerStyle()) + .frame(width: 132, height: 28) + Spacer() + Button("Upload") { + // Handle the upload action + } + .disabled(viewModel.selectedPhotos.count == .zero) + } + .padding(.horizontal) + .padding(.top) + + // Search bar + TextField("Search photos, people, places", text: .constant("")) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding([.horizontal, .bottom]) + // Photos grid + FetchAlbumsView(viewModel: viewModel, selectedSegment: $viewModel.selectedSegment) + .environmentObject(photoLibraryService) + + Spacer() + } + .environmentObject(photoLibraryService) + .edgesIgnoringSafeArea(.bottom) + + VStack { + // Toolbar + Spacer() + floatingActionButton + .scaleEffect(viewModel.selectedPhotos.isEmpty ? 0 : 1) + .animation(.spring(), value: !viewModel.selectedPhotos.isEmpty) + } + .edgesIgnoringSafeArea(.bottom) + } + } + + private var floatingActionButton: some View { + HStack(spacing: 8) { + Button(action: actionAll) { + Text(viewModel.selectedPhotos.count > 1 ? "Deselect All" : "Select All") + .multilineTextAlignment(.leading) + } + .padding(.vertical) + .padding(.leading) + .clipShape(Capsule()) + + Spacer() + + Button(action: viewSelected) { + Text("View Selected") + .multilineTextAlignment(.trailing) + } + .padding(.leading, 10) + .clipShape(Capsule()) + + Text(" \(viewModel.selectedPhotos.count) ") + .frame(height: 24) + .background(Color.lightGray) + .foregroundColor(Color.black) + .clipShape(Capsule()) + } + .padding(.horizontal) + .background(BlurView(style: .systemMaterial)) + .cornerRadius(30) + .padding(.horizontal, 30) + .shadow(radius: 10) + .frame(height: 48, alignment: .center) + .padding(.bottom, 20) + } + + private func dismiss() { + dismissAction?(viewModel.hasUpdates) + self.presentationMode.wrappedValue.dismiss() + } + + func actionAll() { + } + + func viewSelected() { + } + + struct BlurView: UIViewRepresentable { + var style: UIBlurEffect.Style + + func makeUIView(context: Context) -> UIVisualEffectView { + return UIVisualEffectView(effect: UIBlurEffect(style: style)) + } + + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + uiView.effect = UIBlurEffect(style: style) + } + } +} + +#Preview { + CustomPhotoLibraryView(viewModel: CustomPhotoLibraryViewModel()) +} diff --git a/Permanent/Modules/CustomPhotoLibrary/Views/FetchAlbumsView.swift b/Permanent/Modules/CustomPhotoLibrary/Views/FetchAlbumsView.swift new file mode 100644 index 00000000..7b424f25 --- /dev/null +++ b/Permanent/Modules/CustomPhotoLibrary/Views/FetchAlbumsView.swift @@ -0,0 +1,56 @@ +// +// FetchAlbumsView.swift +// Permanent +// +// Created by Lucian Cerbu on 08.12.2023. + +import SwiftUI + +struct FetchAlbumsView: View { + @StateObject var viewModel: CustomPhotoLibraryViewModel + @EnvironmentObject var service: FetchAlbumsViewModel + @Binding var selectedSegment: Int + + var body: some View { + VStack { + libraryView + .onAppear { + service.loadImagesForSegment(segment: selectedSegment) + } + .onChange(of: selectedSegment) { newValue in + service.loadImagesForSegment(segment: newValue) + } + } + } + + var libraryView: some View { + ScrollView(showsIndicators: false) { + LazyVGrid( + columns: Array( + repeating: .init(.adaptive(minimum: 100), spacing: 1), + count: 3 + ), + spacing: 1 + ) { + ForEach(service.assets, id: \.localIdentifier) { asset in + Button { + if viewModel.selectedPhotos.contains(asset) { + viewModel.selectedPhotos.remove(asset) + } else { + viewModel.selectedPhotos.insert(asset) + } + } label: { + ZStack(alignment: .bottomTrailing) { + PhotoThumbnailView(assetLocalId: asset.localIdentifier) + if viewModel.selectedPhotos.contains(asset) { + Image(.checkmarkSelectItem) + .frame(width: 19, height: 19) + .padding(5) + } + } + } + } + } + } + } +} diff --git a/Permanent/Modules/CustomPhotoLibrary/Views/PhotoDetailView.swift b/Permanent/Modules/CustomPhotoLibrary/Views/PhotoDetailView.swift new file mode 100644 index 00000000..a58bd971 --- /dev/null +++ b/Permanent/Modules/CustomPhotoLibrary/Views/PhotoDetailView.swift @@ -0,0 +1,61 @@ +// +// PhotoDetailView.swift +// Permanent +// +// Created by Lucian Cerbu on 08.12.2023. + +import SwiftUI + +struct PhotoDetailView: View { + @EnvironmentObject var service: FetchAlbumsViewModel + + @State private var image: Image? + @State private var loadImageTask: Task? + + private var assetLocalId: String + + init(assetLocalId: String) { + self.assetLocalId = assetLocalId + } + + var body: some View { + ZStack { + Color.black + .ignoresSafeArea() + + if let _ = image { + photoView + } else { + ProgressView() + } + } + .onAppear { + loadImageTask = Task { + await loadImageAsset() + } + } + .onDisappear { + image = nil + } + } + + var photoView: some View { + GeometryReader { proxy in + image? + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: proxy.size.width) + .frame(maxHeight: .infinity) + } + } + + func loadImageAsset() async { + guard let uiImage = try? await service.fetchImage( + byLocalIdentifier: assetLocalId + ) else { + image = nil + return + } + image = uiImage + } +} diff --git a/Permanent/Modules/CustomPhotoLibrary/Views/PhotoThumbnailView.swift b/Permanent/Modules/CustomPhotoLibrary/Views/PhotoThumbnailView.swift new file mode 100644 index 00000000..1b027d18 --- /dev/null +++ b/Permanent/Modules/CustomPhotoLibrary/Views/PhotoThumbnailView.swift @@ -0,0 +1,65 @@ +// +// PhotoThumbnailView.swift +// Permanent +// +// Created by Lucian Cerbu on 05.12.2023. + +import SwiftUI +import Photos +import UIKit + +struct PhotoThumbnailView: View { + + private var assetLocalId: String + @State private var image: Image? + @State private var loadImageTask: Task? + + @EnvironmentObject var service: FetchAlbumsViewModel + + init(assetLocalId: String) { + self.assetLocalId = assetLocalId + } + + var body: some View { + ZStack { + if let image = image { + GeometryReader { proxy in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame( + width: proxy.size.width, + height: proxy.size.width + ) + .clipped() + } + .aspectRatio(1, contentMode: .fit) + } else { + Rectangle() + .foregroundColor(.gray) + .aspectRatio(1, contentMode: .fit) + ProgressView() + } + } + .onAppear { + loadImageTask = Task { + await loadImageAsset(targetSize: CGSize(width: 150, height: 150)) + } + } + .onDisappear { + image = nil + } + } + + func loadImageAsset(targetSize: CGSize = PHImageManagerMaximumSize) async { + guard let uiImage = try? await service + .fetchImage( + byLocalIdentifier: assetLocalId, + targetSize: targetSize + ) else { + image = nil + return + } + image = uiImage + } +} diff --git a/Permanent/Modules/Main/ViewController/MainViewController.swift b/Permanent/Modules/Main/ViewController/MainViewController.swift index f4c5c4a6..322e0ea1 100644 --- a/Permanent/Modules/Main/ViewController/MainViewController.swift +++ b/Permanent/Modules/Main/ViewController/MainViewController.swift @@ -1301,16 +1301,51 @@ extension MainViewController: FABActionSheetDelegate { mediaRecorder.present() } +// func openPhotoLibrary() { +// PHPhotoLibrary.requestAuthorization { (authStatus) in +// switch authStatus { +// case .authorized, .limited: +// DispatchQueue.main.async { +// let storyboard = UIStoryboard(name: "PhotoPicker", bundle: nil) +// let imagePicker = storyboard.instantiateInitialViewController() as! PhotoTabBarViewController +// imagePicker.pickerDelegate = self +// +// self.present(imagePicker, animated: true, completion: nil) +// } +// +// case .denied: +// let alertController = UIAlertController(title: "Photos permission required".localized(), message: "Please go to Settings and turn on the permissions.".localized(), preferredStyle: .alert) +// +// let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in +// guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { +// return +// } +// if UIApplication.shared.canOpenURL(settingsUrl) { +// UIApplication.shared.open(settingsUrl, completionHandler: { (success) in }) +// } +// } +// let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil) +// +// alertController.addAction(cancelAction) +// alertController.addAction(settingsAction) +// +// DispatchQueue.main.async { +// self.present(alertController, animated: true, completion: nil) +// } +// +// default: break +// } +// } +// } + func openPhotoLibrary() { - PHPhotoLibrary.requestAuthorization { (authStatus) in + PHPhotoLibrary.requestAuthorization { [weak self] (authStatus) in switch authStatus { case .authorized, .limited: - DispatchQueue.main.async { - let storyboard = UIStoryboard(name: "PhotoPicker", bundle: nil) - let imagePicker = storyboard.instantiateInitialViewController() as! PhotoTabBarViewController - imagePicker.pickerDelegate = self - - self.present(imagePicker, animated: true, completion: nil) + self?.presentPhotoLibraryView { hasUpdates in + if hasUpdates { + self?.refreshCurrentFolder() + } } case .denied: @@ -1330,7 +1365,7 @@ extension MainViewController: FABActionSheetDelegate { alertController.addAction(settingsAction) DispatchQueue.main.async { - self.present(alertController, animated: true, completion: nil) + self?.present(alertController, animated: true, completion: nil) } default: break @@ -1338,6 +1373,24 @@ extension MainViewController: FABActionSheetDelegate { } } + func presentPhotoLibraryView(completion: @escaping (Bool) -> Void) { + DispatchQueue.main.async { + let hostingController = UIHostingController(rootView: CustomPhotoLibraryView(viewModel: CustomPhotoLibraryViewModel())) + hostingController.modalPresentationStyle = .pageSheet + + self.present(hostingController, animated: true, completion: nil) + + self.fabView.isHidden = false + self.clearButtonWasPressed(UIButton()) + + hostingController.rootView.dismissAction = { hasUpdates in + hostingController.dismiss(animated: true, completion: { + completion(hasUpdates) + }) + } + } + } + func openFileBrowser() { let docPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeItem as String, kUTTypeContent as String], in: .import) diff --git a/Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/Contents.json b/Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/Contents.json new file mode 100644 index 00000000..4761da4e --- /dev/null +++ b/Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "checkmarkSelectItem.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/checkmarkSelectItem.svg b/Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/checkmarkSelectItem.svg new file mode 100644 index 00000000..70aa7a20 --- /dev/null +++ b/Permanent/Resources/Assets/Assets.xcassets/Images/enhancedUpload/checkmarkSelectItem.imageset/checkmarkSelectItem.svg @@ -0,0 +1,4 @@ + + + +