From dd75211a5fc5ca1ecc35691fdfc5b757d101847d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 20 Jan 2025 09:11:40 +0100 Subject: [PATCH 1/6] refactor(UploadQueueViewController): Using DifferenceKit to prevent a crash --- .../Upload/UploadQueueViewController.swift | 81 ++++++++++--------- .../Data/Models/Upload/UploadFile.swift | 21 +++++ 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift index 42d95ddba..cfba67a15 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift @@ -17,6 +17,7 @@ */ import CocoaLumberjackSwift +import DifferenceKit import InfomaniakCore import InfomaniakDI import kDriveCore @@ -33,7 +34,8 @@ final class UploadQueueViewController: UIViewController { @LazyInjectService var uploadQueue: UploadQueue var currentDirectory: File! - private var uploadingFiles = AnyRealmCollection(List()) + private var liveUploadingFiles = [UploadFile]() + private var observedFiles: AnyRealmCollection = AnyRealmCollection(List()) private var notificationToken: NotificationToken? override func viewDidLoad() { @@ -70,42 +72,45 @@ final class UploadQueueViewController: UIViewController { } notificationToken?.invalidate() - notificationToken = uploadQueue.getUploadingFiles(withParent: currentDirectory.id, - userId: accountManager.currentUserId, - driveId: currentDirectory.driveId) - .observe(keyPaths: UploadFile.observedProperties, on: .main) { [weak self] change in - guard let self else { - return - } - - switch change { - case .initial(let results): - uploadingFiles = AnyRealmCollection(results) - tableView.reloadData() - if results.isEmpty { - navigationController?.popViewController(animated: true) - } - case .update(let results, deletions: let deletions, insertions: let insertions, modifications: let modifications): - uploadingFiles = AnyRealmCollection(results) - - guard !results.isEmpty else { - navigationController?.popViewController(animated: true) - return - } - - tableView.performBatchUpdates { - // Always apply updates in the following order: deletions, insertions, then modifications. - // Handling insertions before deletions may result in unexpected behavior. - self.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic) - self.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic) - self.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic) - } - // Update cell corners - tableView.reloadCorners(insertions: insertions, deletions: deletions, count: results.count) - case .error(let error): - DDLogError("Realm observer error: \(error)") - } + + observedFiles = AnyRealmCollection(uploadQueue.getUploadingFiles(withParent: currentDirectory.id, + userId: accountManager.currentUserId, + driveId: currentDirectory.driveId)) + notificationToken = observedFiles.observe(keyPaths: UploadFile.observedProperties, on: .main) { [weak self] change in + guard let self else { + return + } + + let newResults: AnyRealmCollection? + switch change { + case .initial(let results): + newResults = results + case .update(let results, _, _, _): + newResults = results + case .error(let error): + newResults = nil + DDLogError("Realm observer error: \(error)") } + + guard let newResults else { + reloadCollectionViewWith(files: []) + return + } + + reloadCollectionViewWith(files: Array(newResults)) + } + } + + func reloadCollectionViewWith(files: [UploadFile]) { + let changeSet = StagedChangeset(source: liveUploadingFiles, target: files) + tableView.reload(using: changeSet, + with: UITableView.RowAnimation.automatic, + interrupt: { $0.changeCount > Endpoint.itemsPerPage }, + setData: { self.liveUploadingFiles = $0 }) + + if files.isEmpty { + navigationController?.popViewController(animated: true) + } } @IBAction func cancelButtonPressed(_ sender: UIBarButtonItem) { @@ -130,7 +135,7 @@ final class UploadQueueViewController: UIViewController { extension UploadQueueViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return uploadingFiles.count + return liveUploadingFiles.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -142,7 +147,7 @@ extension UploadQueueViewController: UITableViewDataSource { ) - 1) /// Make sure the file is valid - let file = uploadingFiles[indexPath.row] + let file = liveUploadingFiles[indexPath.row] if !file.isInvalidated { let progress: CGFloat? = (file.progress != nil) ? CGFloat(file.progress!) : nil cell.configureWith(uploadFile: file, progress: progress) diff --git a/kDriveCore/Data/Models/Upload/UploadFile.swift b/kDriveCore/Data/Models/Upload/UploadFile.swift index 83b08c1be..c7744651b 100644 --- a/kDriveCore/Data/Models/Upload/UploadFile.swift +++ b/kDriveCore/Data/Models/Upload/UploadFile.swift @@ -389,3 +389,24 @@ public extension [UploadFile] { return file } } + +extension UploadFile: Differentiable { + public var differenceIdentifier: Int { + return id.hashValue + } + + public func isContentEqual(to source: UploadFile) -> Bool { + autoreleasepool { + name == source.name + && parentDirectoryId == source.parentDirectoryId + && name == source.name + && userId == source.userId + && driveId == source.driveId + && uploadDate == source.uploadDate + && creationDate == source.creationDate + && modificationDate == source.modificationDate + && taskCreationDate == source.taskCreationDate + && maxRetryCount == source.maxRetryCount + } + } +} From 5e8ef52e500c271b1017d0980f22825aa596c082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 20 Jan 2025 14:02:09 +0100 Subject: [PATCH 2/6] refactor(UploadQueueViewController): Simplify then add lightness --- .../Files/Upload/UploadQueueViewController.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift index cfba67a15..9282e1d1e 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift @@ -35,7 +35,6 @@ final class UploadQueueViewController: UIViewController { var currentDirectory: File! private var liveUploadingFiles = [UploadFile]() - private var observedFiles: AnyRealmCollection = AnyRealmCollection(List()) private var notificationToken: NotificationToken? override func viewDidLoad() { @@ -73,9 +72,9 @@ final class UploadQueueViewController: UIViewController { notificationToken?.invalidate() - observedFiles = AnyRealmCollection(uploadQueue.getUploadingFiles(withParent: currentDirectory.id, - userId: accountManager.currentUserId, - driveId: currentDirectory.driveId)) + let observedFiles = AnyRealmCollection(uploadQueue.getUploadingFiles(withParent: currentDirectory.id, + userId: accountManager.currentUserId, + driveId: currentDirectory.driveId)) notificationToken = observedFiles.observe(keyPaths: UploadFile.observedProperties, on: .main) { [weak self] change in guard let self else { return From d42f193e1917aa0acf575d7e61b7ccecf65c09ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 09:46:36 +0100 Subject: [PATCH 3/6] feat(UploadQueueViewController): Diffing tracks top and bottom cells for rounding them --- .../Upload/UploadQueueViewController.swift | 30 ++++++++----- .../Models/Upload/CornerCellContainer.swift | 44 +++++++++++++++++++ .../Data/Models/Upload/UploadFile.swift | 8 +--- 3 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 kDriveCore/Data/Models/Upload/CornerCellContainer.swift diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift index 9282e1d1e..cb00c70fd 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift @@ -25,6 +25,8 @@ import kDriveResources import RealmSwift import UIKit +typealias UploadFileDisplayed = CornerCellContainer + final class UploadQueueViewController: UIViewController { @IBOutlet var tableView: UITableView! @IBOutlet var retryButton: UIBarButtonItem! @@ -34,7 +36,7 @@ final class UploadQueueViewController: UIViewController { @LazyInjectService var uploadQueue: UploadQueue var currentDirectory: File! - private var liveUploadingFiles = [UploadFile]() + private var liveUploadingFiles = [UploadFileDisplayed]() private var notificationToken: NotificationToken? override func viewDidLoad() { @@ -92,15 +94,21 @@ final class UploadQueueViewController: UIViewController { } guard let newResults else { - reloadCollectionViewWith(files: []) + reloadCollectionViewWith([]) return } - reloadCollectionViewWith(files: Array(newResults)) + let wrappedFiles = newResults.enumerated().map { index, item in + UploadFileDisplayed(isFirstInList: index == 0, + isLastInList: index == newResults.count - 1, + content: item) + } + + reloadCollectionViewWith(wrappedFiles) } } - func reloadCollectionViewWith(files: [UploadFile]) { + func reloadCollectionViewWith(_ files: [UploadFileDisplayed]) { let changeSet = StagedChangeset(source: liveUploadingFiles, target: files) tableView.reload(using: changeSet, with: UITableView.RowAnimation.automatic, @@ -139,14 +147,12 @@ extension UploadQueueViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(type: UploadTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, - isLast: indexPath.row == self.tableView( - tableView, - numberOfRowsInSection: indexPath.section - ) - 1) - - /// Make sure the file is valid - let file = liveUploadingFiles[indexPath.row] + let fileWrapper = liveUploadingFiles[indexPath.row] + let file = fileWrapper.content + + cell.initWithPositionAndShadow(isFirst: fileWrapper.isFirstInList, + isLast: fileWrapper.isLastInList) + if !file.isInvalidated { let progress: CGFloat? = (file.progress != nil) ? CGFloat(file.progress!) : nil cell.configureWith(uploadFile: file, progress: progress) diff --git a/kDriveCore/Data/Models/Upload/CornerCellContainer.swift b/kDriveCore/Data/Models/Upload/CornerCellContainer.swift new file mode 100644 index 000000000..645553aa1 --- /dev/null +++ b/kDriveCore/Data/Models/Upload/CornerCellContainer.swift @@ -0,0 +1,44 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2025 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import DifferenceKit +import Foundation + +public struct CornerCellContainer: Differentiable { + public let isFirstInList: Bool + public let isLastInList: Bool + public let content: Content + + public init(isFirstInList: Bool, isLastInList: Bool, content: Content) { + self.isFirstInList = isFirstInList + self.isLastInList = isLastInList + self.content = content + } + + public var differenceIdentifier: some Hashable { + return content.differenceIdentifier + } + + public func isContentEqual(to source: CornerCellContainer) -> Bool { + autoreleasepool { + isFirstInList == source.isFirstInList + && isLastInList == source.isLastInList + && content.isContentEqual(to: source.content) + } + } +} diff --git a/kDriveCore/Data/Models/Upload/UploadFile.swift b/kDriveCore/Data/Models/Upload/UploadFile.swift index c7744651b..3f1957015 100644 --- a/kDriveCore/Data/Models/Upload/UploadFile.swift +++ b/kDriveCore/Data/Models/Upload/UploadFile.swift @@ -398,15 +398,9 @@ extension UploadFile: Differentiable { public func isContentEqual(to source: UploadFile) -> Bool { autoreleasepool { name == source.name - && parentDirectoryId == source.parentDirectoryId - && name == source.name - && userId == source.userId - && driveId == source.driveId && uploadDate == source.uploadDate - && creationDate == source.creationDate && modificationDate == source.modificationDate - && taskCreationDate == source.taskCreationDate - && maxRetryCount == source.maxRetryCount + && error == source.error } } } From 6e9a6bd031af12094be187aa210f9eeb80d77feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 17:36:55 +0100 Subject: [PATCH 4/6] feat(UploadFile): Optimised isContentEqual given the content displayed --- kDriveCore/Data/Models/Upload/UploadFile.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/kDriveCore/Data/Models/Upload/UploadFile.swift b/kDriveCore/Data/Models/Upload/UploadFile.swift index 3f1957015..07280d14b 100644 --- a/kDriveCore/Data/Models/Upload/UploadFile.swift +++ b/kDriveCore/Data/Models/Upload/UploadFile.swift @@ -397,9 +397,7 @@ extension UploadFile: Differentiable { public func isContentEqual(to source: UploadFile) -> Bool { autoreleasepool { - name == source.name - && uploadDate == source.uploadDate - && modificationDate == source.modificationDate + id == source.id && error == source.error } } From fbf5e63251b0671d3fd545a8cf19eb8d5c8edf19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 20 Jan 2025 14:34:54 +0100 Subject: [PATCH 5/6] refactor(UploadQueueFoldersViewController): Using DifferenceKit to prevent a crash --- .../UploadQueueFoldersViewController.swift | 89 ++++++++----------- 1 file changed, 35 insertions(+), 54 deletions(-) diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift index 45b348bbd..5f3e9f539 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift @@ -17,25 +17,26 @@ */ import CocoaLumberjackSwift +import DifferenceKit +import InfomaniakCore import InfomaniakDI import kDriveCore import RealmSwift import UIKit final class UploadQueueFoldersViewController: UITableViewController { - var driveFileManager: DriveFileManager! + @LazyInjectService private var accountManager: AccountManageable + @LazyInjectService private var driveInfosManager: DriveInfosManager + @LazyInjectService private var uploadQueue: UploadQueue - @LazyInjectService var accountManager: AccountManageable - @LazyInjectService var driveInfosManager: DriveInfosManager - @LazyInjectService var uploadQueue: UploadQueue + private var frozenUploadingFolders = [File]() + private var notificationToken: NotificationToken? + private var driveFileManager: DriveFileManager! private var userId: Int { return driveFileManager.drive.userId } - private var folders: [File] = [] - private var notificationToken: NotificationToken? - override func viewDidLoad() { super.viewDidLoad() @@ -57,53 +58,30 @@ final class UploadQueueFoldersViewController: UITableViewController { private func setUpObserver() { guard driveFileManager != nil else { return } - // Get the drives (current + shared with me) let driveIds = [driveFileManager.drive.id] + driveInfosManager.getDrives(for: userId, sharedWithMe: true) .map(\.id) - - // Observe uploading files - notificationToken = uploadQueue.getUploadingFiles(userId: userId, driveIds: driveIds) + let uploadingFiles = uploadQueue.getUploadingFiles(userId: userId, driveIds: driveIds) .distinct(by: [\.parentDirectoryId]) - .observe(keyPaths: UploadFile.observedProperties, on: .main) { [weak self] change in - guard let self else { - return - } - - switch change { - case .initial(let results): - updateFolders(from: results) - tableView.reloadData() - if results.isEmpty { - navigationController?.popViewController(animated: true) - } - case .update(let results, deletions: let deletions, insertions: let insertions, modifications: let modifications): - guard !results.isEmpty else { - navigationController?.popViewController(animated: true) - return - } - - // No animation on updating the same lines without changes - if deletions == insertions, modifications.isEmpty { - return - } - - tableView.performBatchUpdates { - self.updateFolders(from: results) - // Always apply updates in the following order: deletions, insertions, then modifications. - // Handling insertions before deletions may result in unexpected behavior. - self.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic) - self.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic) - self.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic) - } - case .error(let error): - DDLogError("Realm observer error: \(error)") - } + + notificationToken = uploadingFiles.observe(keyPaths: UploadFile.observedProperties, on: .main) { [weak self] change in + guard let self else { + return } + + switch change { + case .initial(let results): + updateFolders(from: results) + case .update(let results, _, _, _): + updateFolders(from: results) + case .error(let error): + DDLogError("Realm observer error: \(error)") + } + } } private func updateFolders(from results: Results) { let files = results.map { (driveId: $0.driveId, parentId: $0.parentDirectoryId) } - folders = files.compactMap { tuple in + let folders: [File] = files.compactMap { tuple in let parentId = tuple.parentId let driveId = tuple.driveId @@ -123,11 +101,14 @@ final class UploadQueueFoldersViewController: UITableViewController { return folder } - // (Pop view controller if nothing to show) + let changeSet = StagedChangeset(source: frozenUploadingFolders, target: folders) + tableView.reload(using: changeSet, + with: UITableView.RowAnimation.automatic, + interrupt: { $0.changeCount > Endpoint.itemsPerPage }, + setData: { self.frozenUploadingFolders = $0 }) + if folders.isEmpty { - Task { @MainActor in - self.navigationController?.popViewController(animated: true) - } + navigationController?.popViewController(animated: true) } } @@ -141,14 +122,14 @@ final class UploadQueueFoldersViewController: UITableViewController { // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return folders.count + return frozenUploadingFolders.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(type: UploadFolderTableViewCell.self, for: indexPath) - let folder = folders[indexPath.row] - cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == folders.count - 1) + let folder = frozenUploadingFolders[indexPath.row] + cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == frozenUploadingFolders.count - 1) cell.configure(with: folder, drive: driveFileManager.drive) return cell @@ -158,7 +139,7 @@ final class UploadQueueFoldersViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let uploadViewController = UploadQueueViewController.instantiate() - uploadViewController.currentDirectory = folders[indexPath.row] + uploadViewController.currentDirectory = frozenUploadingFolders[indexPath.row] navigationController?.pushViewController(uploadViewController, animated: true) } } From 81159557e9f943a70284378087754fde4db60ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 23 Jan 2025 09:31:03 +0100 Subject: [PATCH 6/6] refactor(UploadQueueFoldersViewController): Reworked to work with CornerCellContainer --- .../UploadQueueFoldersViewController.swift | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift index 5f3e9f539..7b3bce61e 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueFoldersViewController.swift @@ -24,12 +24,14 @@ import kDriveCore import RealmSwift import UIKit +typealias FileDisplayed = CornerCellContainer + final class UploadQueueFoldersViewController: UITableViewController { @LazyInjectService private var accountManager: AccountManageable @LazyInjectService private var driveInfosManager: DriveInfosManager @LazyInjectService private var uploadQueue: UploadQueue - private var frozenUploadingFolders = [File]() + private var frozenUploadingFolders = [FileDisplayed]() private var notificationToken: NotificationToken? private var driveFileManager: DriveFileManager! @@ -81,7 +83,8 @@ final class UploadQueueFoldersViewController: UITableViewController { private func updateFolders(from results: Results) { let files = results.map { (driveId: $0.driveId, parentId: $0.parentDirectoryId) } - let folders: [File] = files.compactMap { tuple in + let filesCount = files.count + let folders: [FileDisplayed] = files.enumerated().compactMap { index, tuple in let parentId = tuple.parentId let driveId = tuple.driveId @@ -98,7 +101,9 @@ final class UploadQueueFoldersViewController: UITableViewController { return nil } - return folder + return FileDisplayed(isFirstInList: index == 0, + isLastInList: index == filesCount - 1, + content: folder) } let changeSet = StagedChangeset(source: frozenUploadingFolders, target: folders) @@ -128,9 +133,10 @@ final class UploadQueueFoldersViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(type: UploadFolderTableViewCell.self, for: indexPath) - let folder = frozenUploadingFolders[indexPath.row] - cell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == frozenUploadingFolders.count - 1) - cell.configure(with: folder, drive: driveFileManager.drive) + let folderDisplayed = frozenUploadingFolders[indexPath.row] + cell.initWithPositionAndShadow(isFirst: folderDisplayed.isFirstInList, + isLast: folderDisplayed.isLastInList) + cell.configure(with: folderDisplayed.content, drive: driveFileManager.drive) return cell } @@ -139,7 +145,7 @@ final class UploadQueueFoldersViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let uploadViewController = UploadQueueViewController.instantiate() - uploadViewController.currentDirectory = frozenUploadingFolders[indexPath.row] + uploadViewController.currentDirectory = frozenUploadingFolders[indexPath.row].content navigationController?.pushViewController(uploadViewController, animated: true) } }