Skip to content

Commit 7689519

Browse files
Merge pull request #404 from PermanentOrg/feature/VSP-1291-CustomImagePicker
VSP-1291 [IOS] Implement custom image picker
2 parents 1d3566a + 1fc5d05 commit 7689519

File tree

11 files changed

+625
-8
lines changed

11 files changed

+625
-8
lines changed

Permanent.xcodeproj/project.pbxproj

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123
5E3E12452A41F16500682DE5 /* EmptyFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE3C94E25385FB500EC3A66 /* EmptyFolderView.swift */; };
124124
5E3E12462A41F3B700682DE5 /* FileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C37D4628AD62060062D9EB /* FileCollectionViewCell.swift */; };
125125
5E3E124B2A431F9600682DE5 /* EnvVars.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E3E124A2A431F9600682DE5 /* EnvVars.generated.swift */; };
126+
5E41FAF52B2335C5000B79FD /* FetchAlbumsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E41FAF42B2335C5000B79FD /* FetchAlbumsView.swift */; };
127+
5E41FAF72B2335F2000B79FD /* PhotoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E41FAF62B2335F2000B79FD /* PhotoDetailView.swift */; };
126128
5E4455D32A08F0BB00A56235 /* TrustedStewardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4455D22A08F0BB00A56235 /* TrustedStewardViewController.swift */; };
127129
5E4455D42A08F1F100A56235 /* TrustedStewardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4455D22A08F0BB00A56235 /* TrustedStewardViewController.swift */; };
128130
5E46217225C17C5A007642BE /* AccountInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E46217125C17C5A007642BE /* AccountInfoViewController.swift */; };
@@ -286,6 +288,11 @@
286288
5EA88C482ABAF95100876251 /* EditFileNamesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA88C472ABAF95100876251 /* EditFileNamesViewModelTests.swift */; };
287289
5EA88C4A2ABAF96D00876251 /* EditLocationViewModelTets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA88C492ABAF96D00876251 /* EditLocationViewModelTets.swift */; };
288290
5EA88C4C2ABAF98400876251 /* EditTagsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA88C4B2ABAF98400876251 /* EditTagsViewModelTests.swift */; };
291+
5EADAD962B1F9E150099D3B5 /* CustomPhotoLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD952B1F9E150099D3B5 /* CustomPhotoLibraryView.swift */; };
292+
5EADAD982B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD972B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift */; };
293+
5EADAD9A2B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD992B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift */; };
294+
5EADAD9C2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD9B2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift */; };
295+
5EADAD9E2B1FCAB30099D3B5 /* PhotoThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADAD9D2B1FCAB30099D3B5 /* PhotoThumbnailView.swift */; };
289296
5EADF803262475D500D14E9C /* TagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADF801262475D500D14E9C /* TagCollectionViewCell.swift */; };
290297
5EADF804262475D500D14E9C /* TagCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5EADF802262475D500D14E9C /* TagCollectionViewCell.xib */; };
291298
5EADF8082625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EADF8062625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift */; };
@@ -1006,6 +1013,8 @@
10061013
5E3E12222A41906700682DE5 /* ShareManagementRemoteDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareManagementRemoteDataSource.swift; path = Permanent/Services/Repositories/Datasources/Remote/ShareManagementRemoteDataSource.swift; sourceTree = SOURCE_ROOT; };
10071014
5E3E12492A42EEA900682DE5 /* ShareExtensionDEV-Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "ShareExtensionDEV-Release.entitlements"; sourceTree = "<group>"; };
10081015
5E3E124A2A431F9600682DE5 /* EnvVars.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvVars.generated.swift; sourceTree = "<group>"; };
1016+
5E41FAF42B2335C5000B79FD /* FetchAlbumsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAlbumsView.swift; sourceTree = "<group>"; };
1017+
5E41FAF62B2335F2000B79FD /* PhotoDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoDetailView.swift; sourceTree = "<group>"; };
10091018
5E4455D22A08F0BB00A56235 /* TrustedStewardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedStewardViewController.swift; sourceTree = "<group>"; };
10101019
5E46217125C17C5A007642BE /* AccountInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfoViewController.swift; sourceTree = "<group>"; };
10111020
5E46217425C17CE2007642BE /* InfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewModel.swift; sourceTree = "<group>"; };
@@ -1119,6 +1128,11 @@
11191128
5EA88C472ABAF95100876251 /* EditFileNamesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFileNamesViewModelTests.swift; sourceTree = "<group>"; };
11201129
5EA88C492ABAF96D00876251 /* EditLocationViewModelTets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationViewModelTets.swift; sourceTree = "<group>"; };
11211130
5EA88C4B2ABAF98400876251 /* EditTagsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTagsViewModelTests.swift; sourceTree = "<group>"; };
1131+
5EADAD952B1F9E150099D3B5 /* CustomPhotoLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPhotoLibraryView.swift; sourceTree = "<group>"; };
1132+
5EADAD972B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPhotoLibraryViewModel.swift; sourceTree = "<group>"; };
1133+
5EADAD992B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAlbumsViewModel.swift; sourceTree = "<group>"; };
1134+
5EADAD9B2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = "<group>"; };
1135+
5EADAD9D2B1FCAB30099D3B5 /* PhotoThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoThumbnailView.swift; sourceTree = "<group>"; };
11221136
5EADF801262475D500D14E9C /* TagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCollectionViewCell.swift; sourceTree = "<group>"; };
11231137
5EADF802262475D500D14E9C /* TagCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TagCollectionViewCell.xib; sourceTree = "<group>"; };
11241138
5EADF8062625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsNamesCollectionViewCell.swift; sourceTree = "<group>"; };
@@ -2275,6 +2289,14 @@
22752289
path = ViewModel;
22762290
sourceTree = "<group>";
22772291
};
2292+
5E41FAF82B233622000B79FD /* UIViewController */ = {
2293+
isa = PBXGroup;
2294+
children = (
2295+
5EADAD9B2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift */,
2296+
);
2297+
path = UIViewController;
2298+
sourceTree = "<group>";
2299+
};
22782300
5E46217725C194E8007642BE /* View */ = {
22792301
isa = PBXGroup;
22802302
children = (
@@ -2309,6 +2331,7 @@
23092331
5E4739C32A41020A00A20D85 /* Storage */,
23102332
5E0B8EE62ACC1E990077A862 /* UploadManager */,
23112333
5E4739A12A40EC8300A20D85 /* WelcomePage */,
2334+
5EADAD922B1F9DB80099D3B5 /* CustomPhotoLibrary */,
23122335
);
23132336
path = Modules;
23142337
sourceTree = "<group>";
@@ -2931,6 +2954,36 @@
29312954
path = EditDate;
29322955
sourceTree = "<group>";
29332956
};
2957+
5EADAD922B1F9DB80099D3B5 /* CustomPhotoLibrary */ = {
2958+
isa = PBXGroup;
2959+
children = (
2960+
5EADAD932B1F9DE00099D3B5 /* Views */,
2961+
5EADAD942B1F9DE80099D3B5 /* ViewModels */,
2962+
5E41FAF82B233622000B79FD /* UIViewController */,
2963+
);
2964+
path = CustomPhotoLibrary;
2965+
sourceTree = "<group>";
2966+
};
2967+
5EADAD932B1F9DE00099D3B5 /* Views */ = {
2968+
isa = PBXGroup;
2969+
children = (
2970+
5EADAD952B1F9E150099D3B5 /* CustomPhotoLibraryView.swift */,
2971+
5EADAD9D2B1FCAB30099D3B5 /* PhotoThumbnailView.swift */,
2972+
5E41FAF42B2335C5000B79FD /* FetchAlbumsView.swift */,
2973+
5E41FAF62B2335F2000B79FD /* PhotoDetailView.swift */,
2974+
);
2975+
path = Views;
2976+
sourceTree = "<group>";
2977+
};
2978+
5EADAD942B1F9DE80099D3B5 /* ViewModels */ = {
2979+
isa = PBXGroup;
2980+
children = (
2981+
5EADAD972B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift */,
2982+
5EADAD992B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift */,
2983+
);
2984+
path = ViewModels;
2985+
sourceTree = "<group>";
2986+
};
29342987
5ECBAF9F2A1B5F0200FACFDF /* LegacyPlanning */ = {
29352988
isa = PBXGroup;
29362989
children = (
@@ -4326,6 +4379,7 @@
43264379
5EADF8082625BCCD00D14E9C /* TagsNamesCollectionViewCell.swift in Sources */,
43274380
5E4E4CCF29826F4000FEF292 /* ArchiveSettingsTagsHeaderCollectionView.swift in Sources */,
43284381
5E31B62F292FA9BC00934408 /* ShareManagementAccessRolesCollectionViewCell.swift in Sources */,
4382+
5EADAD9C2B1FCA3E0099D3B5 /* PhotoLibraryPicker.swift in Sources */,
43294383
5E6673132A79B4E3001C49CC /* CustomTextFieldStyle.swift in Sources */,
43304384
F557A64927A1C81600C061D4 /* OnlinePresenceTableViewCell.swift in Sources */,
43314385
BC62D580254181BD00E84DA9 /* DataExtension.swift in Sources */,
@@ -4338,6 +4392,7 @@
43384392
BCF4E5DB255C3782003505BA /* AttachmentRecordVO.swift in Sources */,
43394393
06644A8B24EBF4CD003CD359 /* CustomView.swift in Sources */,
43404394
BC6D3B4D2514E57500390927 /* NetworkSessionProtocol.swift in Sources */,
4395+
5EADAD982B1F9E280099D3B5 /* CustomPhotoLibraryViewModel.swift in Sources */,
43414396
5E5AB60F2A6494170030BF61 /* AddTagsView.swift in Sources */,
43424397
F54596C325FF737200E0BC5F /* FilePreviewNavigationController.swift in Sources */,
43434398
927AE0CD2A2F290E00BDF26A /* BannerView.swift in Sources */,
@@ -4368,6 +4423,7 @@
43684423
BC59BACE25C2B8BD005A45D3 /* ActivityFeedViewModelDelegate.swift in Sources */,
43694424
BC04DAC425669AC4009D9C0C /* FolderDestVOPayload.swift in Sources */,
43704425
BC6AF9A625922CBA00483BBA /* AccountVOPayload.swift in Sources */,
4426+
5EADAD962B1F9E150099D3B5 /* CustomPhotoLibraryView.swift in Sources */,
43714427
BC0D99B0256C08F000D29041 /* SideMenuViewController.swift in Sources */,
43724428
5ECBAF9D2A1B5EEE00FACFDF /* ArchiveSteward.swift in Sources */,
43734429
5E46217225C17C5A007642BE /* AccountInfoViewController.swift in Sources */,
@@ -4384,6 +4440,7 @@
43844440
BD96908F25D17E3700E49AB3 /* RegisterRecordResponse.swift in Sources */,
43854441
BC6AF9A32590E6EC00483BBA /* UILabelExtension.swift in Sources */,
43864442
5ED73B252613C30E002F9861 /* TagEndpoint.swift in Sources */,
4443+
5E41FAF52B2335C5000B79FD /* FetchAlbumsView.swift in Sources */,
43874444
5E2CFA27275116480055941C /* PublicProfileAboutPageViewController.swift in Sources */,
43884445
BC326AB32527393400A69597 /* AccountUpdateVO.swift in Sources */,
43894446
06644A8724EA772D003CD359 /* CustomButton.swift in Sources */,
@@ -4596,6 +4653,7 @@
45964653
F52D2B88292E3CA40008D047 /* ShareManagementHeaderCollectionReusableView.swift in Sources */,
45974654
BCD414E4257FCEB50019548F /* SharebyURLVOPayload.swift in Sources */,
45984655
BC6D3B512514EF3300390927 /* OperationProtocol.swift in Sources */,
4656+
5EADAD9E2B1FCAB30099D3B5 /* PhotoThumbnailView.swift in Sources */,
45994657
F559F87528F990F20015A522 /* FolderContentViewModel.swift in Sources */,
46004658
F58B8B9C2757F67A00D43606 /* PublicArchiveViewController.swift in Sources */,
46014659
927AE0CB2A2634CA00BDF26A /* GradientView.swift in Sources */,
@@ -4694,6 +4752,7 @@
46944752
5ED3B3BC29FAB2BE000CFF48 /* LegacyPlanningSaveButton.swift in Sources */,
46954753
5E559EC829BF438200F129BF /* IntExtension.swift in Sources */,
46964754
5EF0B6E927F43D62000CBAF6 /* PublicGalleryViewModel.swift in Sources */,
4755+
5EADAD9A2B1FCA1E0099D3B5 /* FetchAlbumsViewModel.swift in Sources */,
46974756
5E3E121D2A41902C00682DE5 /* AccountRemoteDataSource.swift in Sources */,
46984757
BC04DACA2567D040009D9C0C /* RelocateRecordPayload.swift in Sources */,
46994758
92430A732B078EEE0098597D /* GiftStorageView.swift in Sources */,
@@ -4748,6 +4807,7 @@
47484807
BC6D3B4B2514E46E00390927 /* APIEnvironment.swift in Sources */,
47494808
F502C48C26D6132F00657D37 /* AlbumsViewController.swift in Sources */,
47504809
F509D6FE2742E2FA007E4594 /* SearchFilesViewModel.swift in Sources */,
4810+
5E41FAF72B2335F2000B79FD /* PhotoDetailView.swift in Sources */,
47514811
BC59BACD25C2B8BD005A45D3 /* ActivityFeedViewModel.swift in Sources */,
47524812
BC4526E8251CACDF00E24A51 /* CodableHelper.swift in Sources */,
47534813
92C73E402A13BDC8000EF633 /* LegacyAccountStatusCell.swift in Sources */,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// PhotoLibraryPicker.swift
3+
// Permanent
4+
//
5+
// Created by Lucian Cerbu on 05.12.2023.
6+
7+
import SwiftUI
8+
import PhotosUI
9+
10+
struct PhotoLibraryPicker: UIViewControllerRepresentable {
11+
@Binding var selectedAssets: [PHAsset]
12+
@Environment(\.presentationMode) var presentationMode
13+
14+
func makeUIViewController(context: Context) -> PHPickerViewController {
15+
var config = PHPickerConfiguration(photoLibrary: .shared())
16+
config.selectionLimit = 0 // 0 for unlimited selection
17+
config.filter = .images
18+
19+
let picker = PHPickerViewController(configuration: config)
20+
picker.delegate = context.coordinator
21+
return picker
22+
}
23+
24+
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
25+
// No update needed here
26+
}
27+
28+
func makeCoordinator() -> Coordinator {
29+
Coordinator(self)
30+
}
31+
32+
class Coordinator: NSObject, PHPickerViewControllerDelegate {
33+
let parent: PhotoLibraryPicker
34+
35+
init(_ parent: PhotoLibraryPicker) {
36+
self.parent = parent
37+
}
38+
39+
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
40+
parent.presentationMode.wrappedValue.dismiss()
41+
42+
let identifiers = results.compactMap(\.assetIdentifier)
43+
let assets = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)
44+
DispatchQueue.main.async {
45+
self.parent.selectedAssets = assets.objects(at: IndexSet(0..<assets.count))
46+
}
47+
}
48+
}
49+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// CustomPhotoLibraryViewModel.swift
3+
// Permanent
4+
//
5+
// Created by Lucian Cerbu on 05.12.2023.
6+
7+
import Foundation
8+
import PhotosUI
9+
10+
class CustomPhotoLibraryViewModel: ObservableObject {
11+
@Published var hasUpdates: Bool = false
12+
13+
@Published var selectedPhotos: Set<PHAsset> = []
14+
@Published var imagesInPhotos: [PHAsset] = []
15+
@Published var imagesInAlbums: [PHAsset] = []
16+
@Published var selectedSegment = 0
17+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//
2+
// FetchAlbumsViewModel.swift
3+
// Permanent
4+
//
5+
// Created by Lucian Cerbu on 05.12.2023.
6+
7+
import Foundation
8+
import Photos
9+
import UIKit
10+
import SwiftUI
11+
12+
struct PHFetchResultCollection: RandomAccessCollection, Equatable {
13+
14+
typealias Element = PHAsset
15+
typealias Index = Int
16+
17+
let fetchResult: PHFetchResult<PHAsset>
18+
19+
var endIndex: Int { fetchResult.count }
20+
var startIndex: Int { 0 }
21+
22+
subscript(position: Int) -> PHAsset {
23+
fetchResult.object(at: fetchResult.count - position - 1)
24+
}
25+
}
26+
27+
enum QueryError: Error {
28+
case NotFound
29+
}
30+
31+
class FetchAlbumsViewModel: ObservableObject {
32+
33+
@Published var assets: [PHAsset] = []
34+
@Published var isLoadingPhotos: Bool = false
35+
36+
var imageCachingManager = PHCachingImageManager()
37+
38+
init() {
39+
PHPhotoLibrary.requestAuthorization(for: .readWrite) {[weak self] status in
40+
switch status {
41+
case .authorized:
42+
self?.loadImagesForSegment(segment: 0)
43+
default:
44+
break
45+
}
46+
}
47+
}
48+
49+
func loadImagesForSegment(segment: Int) {
50+
isLoadingPhotos = true
51+
if segment == 0 {
52+
imageCachingManager.allowsCachingHighQualityImages = true
53+
54+
let fetchOptions = PHFetchOptions()
55+
fetchOptions.includeHiddenAssets = false
56+
fetchOptions.predicate = NSPredicate(format: "mediaType = %d || mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue)
57+
fetchOptions.sortDescriptors = [
58+
NSSortDescriptor(key: "creationDate", ascending: false)
59+
]
60+
61+
DispatchQueue.main.async {
62+
let result = PHAsset.fetchAssets(with: fetchOptions)
63+
var assets: [PHAsset] = []
64+
result.enumerateObjects { object, index, stop in
65+
assets.append(object)
66+
}
67+
self.assets = assets
68+
}
69+
} else {
70+
var assets: [PHAsset] = []
71+
self.assets = assets
72+
}
73+
isLoadingPhotos = false
74+
}
75+
76+
func fetchImage(
77+
byLocalIdentifier localId: String,
78+
targetSize: CGSize = CGSize(width: 150, height: 150),
79+
contentMode: PHImageContentMode = .aspectFill
80+
) async throws -> Image? {
81+
guard let asset = assets.first(where: { $0.localIdentifier == localId }) else {
82+
throw QueryError.NotFound
83+
}
84+
85+
let options = PHImageRequestOptions()
86+
options.deliveryMode = .highQualityFormat
87+
options.resizeMode = .fast
88+
options.isNetworkAccessAllowed = false
89+
options.isSynchronous = false
90+
91+
return try await withCheckedThrowingContinuation { [weak self] continuation in
92+
self?.imageCachingManager.requestImage(
93+
for: asset,
94+
targetSize: targetSize,
95+
contentMode: contentMode,
96+
options: options,
97+
resultHandler: { image, info in
98+
if let error = info?[PHImageErrorKey] as? Error {
99+
continuation.resume(throwing: error)
100+
return
101+
}
102+
if let image = image {
103+
continuation.resume(returning: Image(uiImage: (image)))
104+
}
105+
}
106+
)
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)