From 8479862afba635e0135d2b42a3e12e3097010a81 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 30 Jul 2026 08:37:25 +0200 Subject: [PATCH 1/4] perf: prefetch collection view images and avatars Cache previews and avatars in memory to avoid repeated disk access and asynchronous image loading during cell configuration. Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 + .../Collection Common/Cell/NCCellMain.swift | 98 +++++++------------ .../Collection Common/Cell/NCListCell.swift | 66 +++++++------ .../Collection Common/Cell/NCPhotoCell.swift | 55 ++--------- ...nViewCommon+CollectionViewDataSource.swift | 8 +- ...+CollectionViewDataSourcePrefetching.swift | 45 +++++++++ .../NCCollectionViewCommon+Search.swift | 10 +- .../NCCollectionViewCommon.swift | 1 + .../NCSectionFirstHeader.swift | 2 +- iOSClient/NCImageCache.swift | 8 ++ 10 files changed, 150 insertions(+), 147 deletions(-) create mode 100644 iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 0a35fe2a21..097542984f 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -215,6 +215,7 @@ F70B86752642CE3B00ED5349 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F70B86742642CE3B00ED5349 /* FirebaseCrashlytics */; }; F70BFC7420E0FA7D00C67599 /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; F70BFC7520E0FA7D00C67599 /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; + F70C8742301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70C8741301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */; }; F70CAE3A1F8CF31A008125FD /* NCEndToEndEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F70CAE391F8CF31A008125FD /* NCEndToEndEncryption.m */; }; F70CEF5623E9C7E50007035B /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; F70D7C3725FFBF82002B9E34 /* NCCollectionViewCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70D7C3525FFBF81002B9E34 /* NCCollectionViewCommon.swift */; }; @@ -1402,6 +1403,7 @@ F70968A324212C4E00ED60E5 /* NCLivePhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCLivePhoto.swift; sourceTree = ""; }; F70A07C8205285FB00DC1231 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; F70BFC7320E0FA7C00C67599 /* NCUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUtility.swift; sourceTree = ""; }; + F70C8741301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift"; sourceTree = ""; }; F70CAE381F8CF31A008125FD /* NCEndToEndEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCEndToEndEncryption.h; sourceTree = ""; }; F70CAE391F8CF31A008125FD /* NCEndToEndEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCEndToEndEncryption.m; sourceTree = ""; }; F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; @@ -2694,6 +2696,7 @@ F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */, F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */, F747EB0C2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift */, + F70C8741301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */, F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */, F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */, F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */, @@ -4894,6 +4897,7 @@ F78ACD52219046DC0088454D /* NCSectionFirstHeader.swift in Sources */, F743C89E2E5B25A1000173A9 /* UIScene+Extension.swift in Sources */, F72944F52A8424F800246839 /* NCEndToEndMetadataV1.swift in Sources */, + F70C8742301B1E5600170B1F /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */, F710D2022405826100A6033D /* NCContextMenuViewer.swift in Sources */, F7EDBB582FA8D00200098C42 /* NCMediaViewerAppearance.swift in Sources */, F74E3EEB2FB0AD8500252FA0 /* Notification+Extension.swift in Sources */, diff --git a/iOSClient/Main/Collection Common/Cell/NCCellMain.swift b/iOSClient/Main/Collection Common/Cell/NCCellMain.swift index 1a91f6e4c5..d5a53739aa 100644 --- a/iOSClient/Main/Collection Common/Cell/NCCellMain.swift +++ b/iOSClient/Main/Collection Common/Cell/NCCellMain.swift @@ -139,46 +139,14 @@ extension NCCollectionViewCommon { } if metadata.name == global.appName { - let ext = global.getSizeExtension(column: numberOfColumns) - let ocId = metadata.ocId - - if metadata.iconName.isEmpty { - cell.previewImg?.image = imageCache.getImageFile() - } else { - cell.previewImg?.image = utility.loadImage( - named: metadata.iconName, - useTypeIconFile: true, - account: metadata.account - ) - } - - cell.previewImg?.contentMode = .scaleAspectFit - - DispatchQueue.global(qos: .userInitiated).async { [weak self, weak cell] in - guard let self else { - return - } - - let image = self.utility.getImage( - ocId: metadata.ocId, - etag: metadata.etag, - ext: ext, - userId: metadata.userId, - urlBase: metadata.urlBase - ) - - guard let image else { - return - } - - DispatchQueue.main.async { - guard let cell, - cell.metadata?.ocId == ocId else { - return - } - - cell.previewImg?.image = image - cell.previewImg?.contentMode = .scaleAspectFill + let ext = global.getSizeExtension(column: self.numberOfColumns) + if let image = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + cell.previewImg?.image = image + } else if cell.previewImg?.image == nil { + if metadata.iconName.isEmpty { + cell.previewImg?.image = imageCache.getImageFile() + } else { + cell.previewImg?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } } } else { @@ -207,29 +175,35 @@ extension NCCollectionViewCommon { if let user = getAvatarFromIconUrl(metadata: metadata) { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: user) let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - let account = metadata.account - let ocId = metadata.ocId - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard cell.metadata?.ocId == ocId else { - return + + if let image = NCImageCache.shared.getImageCache(key: fileName) { + cell.previewImg?.image = image + } else { + let account = metadata.account + let ocId = metadata.ocId + + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + imageCache.addImageCache(image: image, key: fileName) + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard cell.metadata?.ocId == ocId else { + return + } + cell.previewImg?.image = image } - cell.previewImg?.image = image } } } diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index fde6c658dc..99624cb201 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -501,38 +501,44 @@ extension NCCollectionViewCommon { // AVATAR if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - if let image = UIImage(contentsOfFile: fileNameLocalPath) { + + if let image = NCImageCache.shared.getImageCache(key: fileName) { cell.setSharedAvatarImage(image) - } - let user = metadata.ownerId - let ocId = metadata.ocId - let account = metadata.account - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard - let cell = self.collectionView.cellForItem(at: indexPath) as? NCListCell, - cell.metadata?.ocId == ocId else { - return + } else { + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { + cell.setSharedAvatarImage(image) + } + let user = metadata.ownerId + let ocId = metadata.ocId + let account = metadata.account + + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + self.imageCache.addImageCache(image: image, key: fileName) + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard + let cell = self.collectionView.cellForItem(at: indexPath) as? NCListCell, + cell.metadata?.ocId == ocId else { + return + } + cell.setSharedAvatarImage(image) } - cell.setSharedAvatarImage(image) } } } diff --git a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift index 903070bf0e..2d27a1c00c 100644 --- a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift @@ -65,54 +65,15 @@ extension NCCollectionViewCommon { cell.metadata = metadata - // Image - // - if isPinchGestureActive || ext == global.previewExt512 || ext == global.previewExt1024 { - cell.previewImg?.image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) - } - - let ocId = metadata.ocId - let etag = metadata.etag - let userId = metadata.userId - let urlBase = metadata.urlBase - let iconName = metadata.iconName - let account = metadata.account - - DispatchQueue.global(qos: .userInitiated).async { [weak self, weak cell] in - guard let self else { - return - } - - let image = self.utility.getImage( - ocId: ocId, - etag: etag, - ext: ext, - userId: userId, - urlBase: urlBase - ) - - let fallbackImage: UIImage? - - if let image { - fallbackImage = image - } else if iconName.isEmpty { - fallbackImage = NCImageCache.shared.getImageFile() + if let image = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + cell.previewImg?.image = image + cell.previewImg?.contentMode = .scaleAspectFill + } else { + cell.previewImg?.contentMode = .scaleAspectFit + if metadata.iconName.isEmpty { + cell.previewImg?.image = imageCache.getImageFile() } else { - fallbackImage = self.utility.loadImage( - named: iconName, - useTypeIconFile: true, - account: account - ) - } - - DispatchQueue.main.async { - guard let cell, - cell.metadata?.ocId == ocId else { - return - } - - cell.previewImg?.image = fallbackImage - cell.previewImg?.contentMode = image == nil ? .scaleAspectFit : .scaleAspectFill + cell.previewImg?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 05bb80eb97..24e0293e32 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -75,7 +75,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { return } - let image = await NCUtility().createImageFileFrom( + let image = await self.utility.createImageFileFrom( data: data, ocId: ocId, etag: etag, @@ -83,6 +83,10 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { userId: self.session.userId, urlBase: self.session.urlBase) + if let image { + self.imageCache.addImageCache(ocId: ocId, etag: etag, image: image, ext: ext) + } + await MainActor.run { guard let visibleIndexPath = self.collectionView.indexPathsForVisibleItems.first(where: { self.dataSource.getMetadata(indexPath: $0)?.ocId == ocId @@ -104,7 +108,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } } else { cell.previewImg?.contentMode = .scaleAspectFit - cell.previewImg?.image = NCUtility().loadImage( + cell.previewImg?.image = self.utility.loadImage( named: iconName, useTypeIconFile: true, account: account diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift new file mode 100644 index 0000000000..c83d80b201 --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit + +extension NCCollectionViewCommon: UICollectionViewDataSourcePrefetching { + func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { + let ext = global.getSizeExtension(column: self.numberOfColumns) + guard !isSearchingMode else { + return + } + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self else { + return + } + + for indexPath in indexPaths { + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { + continue + } + + // PREVIEW IMAGE + if let metadata = dataSource.getMetadata(indexPath: indexPath), + imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) == nil, + let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) { + imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext) + } + + // AVATAR + if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) + let fileNameLocalPath = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + + if imageCache.getImageCache(key: fileName) == nil, + let image = UIImage(contentsOfFile: fileNameLocalPath) { + imageCache.addImageCache(image: image, key: fileName) + } + } + } + } + } +} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift index d748b07ac6..a192f32a3f 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+Search.swift @@ -55,12 +55,12 @@ extension NCCollectionViewCommon { account: self.session.account ) { task in Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier( + let identifier = await self.networking.networkingTasks.createIdentifier( account: self.session.account, path: urlBase, name: "searchLiteral" ) - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + await self.networking.networkingTasks.track(identifier: identifier, task: task) self.searchTask = task } } @@ -68,8 +68,8 @@ extension NCCollectionViewCommon { if results.error == .success, let files = results.files { let (_, metadatas) = await NCManageDatabaseCreateMetadata().convertFilesToMetadatasAsync(files) - NCManageDatabase.shared.addMetadatas(metadatas) - self.dataSource = NCCollectionViewDataSource( + database.addMetadatas(metadatas) + dataSource = NCCollectionViewDataSource( metadatas: metadatas, layoutForView: self.layoutForView, account: self.session.account @@ -80,7 +80,7 @@ extension NCCollectionViewCommon { errorCode: results.error.errorCode) } - self.collectionView.reloadData() + collectionView.reloadData() } // MARK: - Unifield Search diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 2933f68633..7333784f8e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -256,6 +256,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, longPressedGesture.delaysTouchesBegan = true collectionView.addGestureRecognizer(longPressedGesture) + collectionView.prefetchDataSource = self collectionView.dragInteractionEnabled = true collectionView.dragDelegate = self collectionView.dropDelegate = self diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift index 853289b08e..24670288e1 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift @@ -294,7 +294,7 @@ extension NCSectionFirstHeader: UICollectionViewDataSource { guard result.error == .success, let data = result.responseData?.data, - let image = NCUtility().createImageFileFrom( + let image = utility.createImageFileFrom( data: data, ocId: metadata.ocId, etag: metadata.etag, diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 205d14ba3f..643e77de41 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -53,10 +53,18 @@ final class NCImageCache: @unchecked Sendable { cache.setObject(image, forKey: cacheKey(ocId: ocId, etag: etag, ext: ext)) } + func addImageCache(image: UIImage, key: String) { + cache.setObject(image, forKey: key as NSString) + } + func getImageCache(ocId: String, etag: String, ext: String) -> UIImage? { cache.object(forKey: cacheKey(ocId: ocId, etag: etag, ext: ext)) } + func getImageCache(key: String) -> UIImage? { + return cache.object(forKey: key as NSString) + } + func removeAll() { cache.removeAllObjects() From 64cec93b95a4fd21016e5b8100777054fb03fe16 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 30 Jul 2026 10:04:35 +0200 Subject: [PATCH 2/4] fix: improve collection preview and avatar loading Signed-off-by: Marino Faggiana --- .../Collection Common/Cell/NCCellMain.swift | 34 +++----------- .../Collection Common/Cell/NCListCell.swift | 39 +--------------- ...nViewCommon+CollectionViewDataSource.swift | 46 +++++++++++++++++-- ...+CollectionViewDataSourcePrefetching.swift | 40 ++++++++-------- 4 files changed, 69 insertions(+), 90 deletions(-) diff --git a/iOSClient/Main/Collection Common/Cell/NCCellMain.swift b/iOSClient/Main/Collection Common/Cell/NCCellMain.swift index d5a53739aa..621debc7bb 100644 --- a/iOSClient/Main/Collection Common/Cell/NCCellMain.swift +++ b/iOSClient/Main/Collection Common/Cell/NCCellMain.swift @@ -142,6 +142,9 @@ extension NCCollectionViewCommon { let ext = global.getSizeExtension(column: self.numberOfColumns) if let image = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { cell.previewImg?.image = image + } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) { + imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext) + cell.previewImg?.image = image } else if cell.previewImg?.image == nil { if metadata.iconName.isEmpty { cell.previewImg?.image = imageCache.getImageFile() @@ -174,37 +177,14 @@ extension NCCollectionViewCommon { if !metadata.iconUrl.isEmpty { if let user = getAvatarFromIconUrl(metadata: metadata) { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: user) - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) if let image = NCImageCache.shared.getImageCache(key: fileName) { cell.previewImg?.image = image } else { - let account = metadata.account - let ocId = metadata.ocId - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - imageCache.addImageCache(image: image, key: fileName) - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard cell.metadata?.ocId == ocId else { - return - } - cell.previewImg?.image = image - } - } + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { + cell.previewImg?.image = image + imageCache.addImageCache(image: image, key: fileName) } } } diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 99624cb201..adf2080139 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -433,10 +433,6 @@ extension NCCollectionViewCommon { cell.previewImg?.contentMode = .scaleAspectFit } - guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { - return cell - } - if let metadataFolder { isShare = metadata.permissions.contains(NCMetadataPermissions.permissionShared) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionShared) isMounted = metadata.permissions.contains(NCMetadataPermissions.permissionMounted) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionMounted) @@ -506,41 +502,10 @@ extension NCCollectionViewCommon { cell.setSharedAvatarImage(image) } else { let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { cell.setSharedAvatarImage(image) - } - let user = metadata.ownerId - let ocId = metadata.ocId - let account = metadata.account - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - self.imageCache.addImageCache(image: image, key: fileName) - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard - let cell = self.collectionView.cellForItem(at: indexPath) as? NCListCell, - cell.metadata?.ocId == ocId else { - return - } - cell.setSharedAvatarImage(image) - } - } - } + imageCache.addImageCache(image: image, key: fileName) } } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 24e0293e32..dce3b180a9 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -51,6 +51,48 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { let iconName = metadata.iconName let account = metadata.account + // AVATAR + // + if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + + if UIImage(contentsOfFile: fileNameLocalPath) == nil, + let user = getAvatarFromIconUrl(metadata: metadata) { + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + self.imageCache.addImageCache(image: image, key: fileName) + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard + let cell = self.collectionView.cellForItem(at: indexPath) as? NCListCell, + cell.metadata?.ocId == ocId else { + return + } + cell.setSharedAvatarImage(image) + } + } + } + } + } + } + + // PREVIEW IMAGE + // let ext = self.global.getSizeExtension(column: self.numberOfColumns) let imageExists = self.utilityFileSystem.fileProviderStorageImageExists(ocId, etag: metadata.etag, userId: metadata.userId, urlBase: metadata.urlBase) @@ -83,10 +125,6 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { userId: self.session.userId, urlBase: self.session.urlBase) - if let image { - self.imageCache.addImageCache(ocId: ocId, etag: etag, image: image, ext: ext) - } - await MainActor.run { guard let visibleIndexPath = self.collectionView.indexPathsForVisibleItems.first(where: { self.dataSource.getMetadata(indexPath: $0)?.ocId == ocId diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift index c83d80b201..9246dff4b9 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift @@ -7,39 +7,35 @@ import UIKit extension NCCollectionViewCommon: UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { + /* let ext = global.getSizeExtension(column: self.numberOfColumns) guard !isSearchingMode else { return } - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self else { - return + for indexPath in indexPaths { + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { + continue } - for indexPath in indexPaths { - guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { - continue - } - - // PREVIEW IMAGE - if let metadata = dataSource.getMetadata(indexPath: indexPath), - imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) == nil, - let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) { - imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext) - } + // PREVIEW IMAGE + if let metadata = dataSource.getMetadata(indexPath: indexPath), + imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) == nil, + let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) { + imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext) + } - // AVATAR - if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { - let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) - let fileNameLocalPath = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + // AVATAR + if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) + let fileNameLocalPath = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - if imageCache.getImageCache(key: fileName) == nil, - let image = UIImage(contentsOfFile: fileNameLocalPath) { - imageCache.addImageCache(image: image, key: fileName) - } + if imageCache.getImageCache(key: fileName) == nil, + let image = UIImage(contentsOfFile: fileNameLocalPath) { + imageCache.addImageCache(image: image, key: fileName) } } } + */ } } From 2eebdb7c907dd4c485738e0311351f6c8c129b8d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 30 Jul 2026 10:50:41 +0200 Subject: [PATCH 3/4] perf: reuse image preview availability check Signed-off-by: Marino Faggiana --- iOSClient/Main/Collection Common/Cell/NCGridCell.swift | 3 +-- iOSClient/Main/Collection Common/Cell/NCListCell.swift | 3 +-- ...NCCollectionViewCommon+CollectionViewDataSource.swift | 9 +++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index 57831aa19b..030829e01b 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -240,11 +240,10 @@ class NCGridLayout: UICollectionViewFlowLayout { } extension NCCollectionViewCommon { - func gridCell(cell: NCGridCell, indexPath: IndexPath, metadata: tableMetadata) -> NCGridCell { + func gridCell(cell: NCGridCell, indexPath: IndexPath, metadata: tableMetadata, existsImagePreview: Bool) -> NCGridCell { var isShare = false var isMounted = false var a11yValues: [String] = [] - let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag, userId: metadata.userId, urlBase: metadata.urlBase) // CONTENT MODE cell.previewImg?.layer.borderWidth = 0 diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index adf2080139..6c221d267b 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -412,7 +412,7 @@ class NCListLayout: UICollectionViewFlowLayout { #if !EXTENSION extension NCCollectionViewCommon { - func listCell(cell: NCListCell, indexPath: IndexPath, metadata: tableMetadata) -> NCListCell { + func listCell(cell: NCListCell, indexPath: IndexPath, metadata: tableMetadata, existsImagePreview: Bool) -> NCListCell { defer { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() if !metadata.isSharable() || (!capabilities.fileSharingApiEnabled && !capabilities.filesComments && capabilities.activity.isEmpty) { @@ -422,7 +422,6 @@ extension NCCollectionViewCommon { var isShare = false var isMounted = false var a11yValues: [String] = [] - let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag, userId: metadata.userId, urlBase: metadata.urlBase) // CONTENT MODE cell.previewImg?.layer.borderWidth = 0 diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index dce3b180a9..c7a1bdcb13 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -159,11 +159,12 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let metadata = self.dataSource.getMetadata(indexPath: indexPath) ?? tableMetadata() + let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag, userId: metadata.userId, urlBase: metadata.urlBase) // E2EE create preview if self.isCurrentDirectoryE2EE, metadata.isImageOrVideo, - !utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag, userId: metadata.userId, urlBase: metadata.urlBase) { + !existsImagePreview { utility.createImageFileFrom(metadata: metadata) } @@ -175,18 +176,18 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } else { let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell)! gridCell.delegate = self - return self.gridCell(cell: gridCell, indexPath: indexPath, metadata: metadata) + return self.gridCell(cell: gridCell, indexPath: indexPath, metadata: metadata, existsImagePreview: existsImagePreview) } } else if isLayoutGrid { // LAYOUT GRID let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell)! gridCell.delegate = self - return self.gridCell(cell: gridCell, indexPath: indexPath, metadata: metadata) + return self.gridCell(cell: gridCell, indexPath: indexPath, metadata: metadata, existsImagePreview: existsImagePreview) } else { // LAYOUT LIST let listCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)! listCell.delegate = self - return self.listCell(cell: listCell, indexPath: indexPath, metadata: metadata) + return self.listCell(cell: listCell, indexPath: indexPath, metadata: metadata, existsImagePreview: existsImagePreview) } } From 4a2c812e5739ad43c9ed04f3065e7ec48c6b0fad Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 30 Jul 2026 11:01:38 +0200 Subject: [PATCH 4/4] fix: load photo previews on cache miss Signed-off-by: Marino Faggiana --- iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift index 2d27a1c00c..d00ee10fde 100644 --- a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift @@ -68,6 +68,9 @@ extension NCCollectionViewCommon { if let image = imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { cell.previewImg?.image = image cell.previewImg?.contentMode = .scaleAspectFill + } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) { + imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext) + cell.previewImg?.image = image } else { cell.previewImg?.contentMode = .scaleAspectFit if metadata.iconName.isEmpty {