From ab538bb55131227f66705d17e1d7c92aa14ff8cd Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Mon, 22 Jul 2024 21:35:27 +0400 Subject: [PATCH] [ios] refactor icloud sync to prevent syncing when some errors occur - throws an exeption when the metadata cannot be initialized from the url or nsmetadataitem - add 2 new sync errors cases to clarify errors reasons - stop sync on the all errors except ubiquity errors (uploading/downloading) - subscribe the settings screen on the sync state notification to update the relates cell properly from the cloud manager - show the alert with an error if cloud sync fails with proposal to the user to send a bugreport Signed-off-by: Kiryl Kaveryn --- .../Core/iCloud/CloudStorageManager.swift | 104 +++++++++++------- .../iCloud/DefaultLocalDirectoryMonitor.swift | 41 ++----- iphone/Maps/Core/iCloud/MetadataItem.swift | 25 +++-- .../Core/iCloud/SynchronizationError.swift | 12 +- .../iCloud/SynchronizationStateManager.swift | 4 +- .../iCloudDocumentsDirectoryMonitor.swift | 97 ++++++---------- .../SettingsTableViewiCloudSwitchCell.swift | 23 ++-- .../UI/Settings/MWMSettingsViewController.mm | 8 +- 8 files changed, 148 insertions(+), 166 deletions(-) diff --git a/iphone/Maps/Core/iCloud/CloudStorageManager.swift b/iphone/Maps/Core/iCloud/CloudStorageManager.swift index 64c06c586b..806ff74568 100644 --- a/iphone/Maps/Core/iCloud/CloudStorageManager.swift +++ b/iphone/Maps/Core/iCloud/CloudStorageManager.swift @@ -18,11 +18,23 @@ private let kBookmarksDirectoryName = "bookmarks" private let kICloudSynchronizationDidChangeEnabledStateNotificationName = "iCloudSynchronizationDidChangeEnabledStateNotification" private let kUDDidFinishInitialCloudSynchronization = "kUDDidFinishInitialCloudSynchronization" +final class CloudStorageSynchronizationState: NSObject { + let isAvailable: Bool + let isOn: Bool + let error: NSError? + + init(isAvailable: Bool, isOn: Bool, error: NSError?) { + self.isAvailable = isAvailable + self.isOn = isOn + self.error = error + } +} + @objc @objcMembers final class CloudStorageManager: NSObject { fileprivate struct Observation { weak var observer: AnyObject? - var onErrorCompletionHandler: ((NSError?) -> Void)? + var onSynchronizationStateDidChangeHandler: ((CloudStorageSynchronizationState) -> Void)? } let fileManager: FileManager @@ -33,7 +45,7 @@ private let kUDDidFinishInitialCloudSynchronization = "kUDDidFinishInitialCloudS private let synchronizationStateManager: SynchronizationStateManager private var fileWriter: SynchronizationFileWriter? private var observers = [ObjectIdentifier: CloudStorageManager.Observation]() - private var synchronizationError: SynchronizationError? { + private var synchronizationError: Error? { didSet { notifyObserversOnSynchronizationError(synchronizationError) } } @@ -104,13 +116,11 @@ private extension CloudStorageManager { guard let self else { return } switch result { case .failure(let error): - self.stopSynchronization() self.processError(error) case .success(let cloudDirectoryUrl): self.localDirectoryMonitor.start { result in switch result { case .failure(let error): - self.stopSynchronization() self.processError(error) case .success(let localDirectoryUrl): self.fileWriter = SynchronizationFileWriter(fileManager: self.fileManager, @@ -124,13 +134,17 @@ private extension CloudStorageManager { } } - func stopSynchronization() { + func stopSynchronization(withError error: Error? = nil) { LOG(.debug, "Stop synchronization") localDirectoryMonitor.stop() cloudDirectoryMonitor.stop() - synchronizationError = nil fileWriter = nil synchronizationStateManager.resetState() + + guard let error else { return } + settings.setICLoudSynchronizationEnabled(false) + synchronizationError = error + MWMAlertViewController.activeAlert().presentBugReportAlert(withTitle: L("icloud_synchronization_error_alert_title")) } func pauseSynchronization() { @@ -228,53 +242,60 @@ private extension CloudStorageManager { func writingResultHandler(for event: OutgoingEvent) -> WritingResultCompletionHandler { return { [weak self] result in guard let self else { return } - DispatchQueue.main.async { - switch result { - case .success: - // Mark that initial synchronization is finished. - if case .didFinishInitialSynchronization = event { - UserDefaults.standard.set(true, forKey: kUDDidFinishInitialCloudSynchronization) - } - case .reloadCategoriesAtURLs(let urls): - urls.forEach { self.bookmarksManager.reloadCategory(atFilePath: $0.path) } - case .deleteCategoriesAtURLs(let urls): - urls.forEach { self.bookmarksManager.deleteCategory(atFilePath: $0.path) } - case .failure(let error): - self.processError(error) + switch result { + case .success: + // Mark that initial synchronization is finished. + if case .didFinishInitialSynchronization = event { + UserDefaults.standard.set(true, forKey: kUDDidFinishInitialCloudSynchronization) } + case .reloadCategoriesAtURLs(let urls): + DispatchQueue.main.async { + urls.forEach { self.bookmarksManager.reloadCategory(atFilePath: $0.path) } + } + case .deleteCategoriesAtURLs(let urls): + DispatchQueue.main.async { + urls.forEach { self.bookmarksManager.deleteCategory(atFilePath: $0.path) } + } + case .failure(let error): + self.processError(error) } } } // MARK: - Error handling func processError(_ error: Error) { - if let synchronizationError = error as? SynchronizationError { - LOG(.debug, "Synchronization error: \(error.localizedDescription)") - switch synchronizationError { - case .fileUnavailable: break - case .fileNotUploadedDueToQuota: break - case .ubiquityServerNotAvailable: break - case .iCloudIsNotAvailable: fallthrough - case .failedToOpenLocalDirectoryFileDescriptor: fallthrough - case .failedToRetrieveLocalDirectoryContent: fallthrough - case .containerNotFound: + switch error { + case let syncError as SynchronizationError: + switch syncError { + case .fileUnavailable, + .fileNotUploadedDueToQuota, + .ubiquityServerNotAvailable: + LOG(.warning, "Synchronization Warning: \(syncError.localizedDescription)") + synchronizationError = syncError + case .iCloudIsNotAvailable: + LOG(.warning, "Synchronization Warning: \(error.localizedDescription)") stopSynchronization() + case .failedToOpenLocalDirectoryFileDescriptor, + .failedToRetrieveLocalDirectoryContent, + .containerNotFound, + .failedToCreateMetadataItem, + .failedToRetrieveMetadataQueryContent: + LOG(.error, "Synchronization Error: \(error.localizedDescription)") + stopSynchronization(withError: error) } - self.synchronizationError = synchronizationError - } else { - // TODO: Handle non-synchronization errors - LOG(.debug, "Non-synchronization error: \(error.localizedDescription)") + default: + LOG(.debug, "Non-synchronization Error: \(error.localizedDescription)") + stopSynchronization(withError: error) } } } // MARK: - CloudStorageManger Observing extension CloudStorageManager { - func addObserver(_ observer: AnyObject, onErrorCompletionHandler: @escaping (NSError?) -> Void) { + func addObserver(_ observer: AnyObject, synchronizationStateDidChangeHandler: @escaping (CloudStorageSynchronizationState) -> Void) { let id = ObjectIdentifier(observer) - observers[id] = Observation(observer: observer, onErrorCompletionHandler:onErrorCompletionHandler) - // Notify the new observer immediately to handle initial state. - observers[id]?.onErrorCompletionHandler?(synchronizationError as NSError?) + observers[id] = Observation(observer: observer, onSynchronizationStateDidChangeHandler: synchronizationStateDidChangeHandler) + notifyObserversOnSynchronizationError(synchronizationError) } func removeObserver(_ observer: AnyObject) { @@ -282,10 +303,13 @@ extension CloudStorageManager { observers.removeValue(forKey: id) } - private func notifyObserversOnSynchronizationError(_ error: SynchronizationError?) { - self.observers.removeUnreachable().forEach { _, observable in + private func notifyObserversOnSynchronizationError(_ error: Error?) { + let state = CloudStorageSynchronizationState(isAvailable: cloudDirectoryMonitor.isCloudAvailable(), + isOn: settings.iCLoudSynchronizationEnabled(), + error: error as? NSError) + observers.removeUnreachable().forEach { _, observable in DispatchQueue.main.async { - observable.onErrorCompletionHandler?(error as NSError?) + observable.onSynchronizationStateDidChangeHandler?(state) } } } diff --git a/iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift b/iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift index 1dc855c94d..250425b279 100644 --- a/iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift +++ b/iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift @@ -53,7 +53,9 @@ final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { self.fileManager = fileManager self.directory = directory self.fileType = fileType - try fileManager.createDirectoryIfNeeded(at: directory) + if !fileManager.fileExists(atPath: directory.path) { + try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + } } // MARK: - Public methods @@ -143,31 +145,23 @@ final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { dispatchSourceDebounceState = .started(source: source) do { - let files = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [], options: [.skipsHiddenFiles], fileExtension: fileType.fileExtension) - let contents = files.compactMap { url in - do { - let metadataItem = try LocalMetadataItem(fileUrl: url) - return metadataItem - } catch { - delegate?.didReceiveLocalMonitorError(error) - return nil - } - } - let contentMetadataItems = LocalContents(contents) + let files = try fileManager + .contentsOfDirectory(at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles]) + .filter { $0.pathExtension == fileType.fileExtension } + let contents: LocalContents = try files.map { try LocalMetadataItem(fileUrl: $0) } if !didFinishGatheringIsCalled { didFinishGatheringIsCalled = true LOG(.debug, "LocalMonitor: didFinishGathering called.") - LOG(.debug, "LocalMonitor: contentMetadataItems count: \(contentMetadataItems.count)") - delegate?.didFinishGathering(contents: contentMetadataItems) + LOG(.debug, "LocalMonitor: contentMetadataItems count: \(contents.count)") + delegate?.didFinishGathering(contents: contents) } else { LOG(.debug, "LocalMonitor: didUpdate called.") - LOG(.debug, "LocalMonitor: contentMetadataItems count: \(contentMetadataItems.count)") - delegate?.didUpdate(contents: contentMetadataItems) + LOG(.debug, "LocalMonitor: contentMetadataItems count: \(contents.count)") + delegate?.didUpdate(contents: contents) } } catch { - LOG(.debug, "\(error)") - delegate?.didReceiveLocalMonitorError(SynchronizationError.failedToRetrieveLocalDirectoryContent) + delegate?.didReceiveLocalMonitorError(error) } } @@ -202,15 +196,4 @@ private extension FileManager { } return dispatchSource } - - func createDirectoryIfNeeded(at url: URL) throws { - if !fileExists(atPath: url.path) { - try createDirectory(at: url, withIntermediateDirectories: true) - } - } - - func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options: FileManager.DirectoryEnumerationOptions, fileExtension: String) throws -> [URL] { - let files = try contentsOfDirectory(at: url, includingPropertiesForKeys: keys, options: options) - return files.filter { $0.pathExtension == fileExtension } - } } diff --git a/iphone/Maps/Core/iCloud/MetadataItem.swift b/iphone/Maps/Core/iCloud/MetadataItem.swift index f2ac2cfee1..0eda9cdfa0 100644 --- a/iphone/Maps/Core/iCloud/MetadataItem.swift +++ b/iphone/Maps/Core/iCloud/MetadataItem.swift @@ -23,9 +23,10 @@ struct CloudMetadataItem: MetadataItem { extension LocalMetadataItem { init(fileUrl: URL) throws { - throw NSError(domain: "LocalMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize LocalMetadataItem from URL"]) let resources = try fileUrl.resourceValues(forKeys: [.contentModificationDateKey]) guard let lastModificationDate = resources.contentModificationDate?.roundedTime else { + LOG(.error, "Failed to initialize LocalMetadataItem from URL's resources: \(resources)") + throw SynchronizationError.failedToCreateMetadataItem } self.fileName = fileUrl.lastPathComponent self.fileUrl = fileUrl @@ -38,36 +39,44 @@ extension LocalMetadataItem { } extension CloudMetadataItem { - init(metadataItem: NSMetadataItem) throws { + init(metadataItem: NSMetadataItem, isRemoved: Bool = false) throws { guard let fileName = metadataItem.value(forAttribute: NSMetadataItemFSNameKey) as? String, let fileUrl = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL, let downloadStatus = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String, let lastModificationDate = (metadataItem.value(forAttribute: NSMetadataItemFSContentChangeDateKey) as? Date)?.roundedTime, let hasUnresolvedConflicts = metadataItem.value(forAttribute: NSMetadataUbiquitousItemHasUnresolvedConflictsKey) as? Bool else { - throw NSError(domain: "CloudMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize CloudMetadataItem from NSMetadataItem"]) + let allAttributes = metadataItem.values(forAttributes: metadataItem.attributes) + LOG(.error, "Failed to initialize CloudMetadataItem from NSMetadataItem: \(allAttributes.debugDescription)") + throw SynchronizationError.failedToCreateMetadataItem } self.fileName = fileName self.fileUrl = fileUrl self.isDownloaded = downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent self.lastModificationDate = lastModificationDate - self.isRemoved = CloudMetadataItem.isInTrash(fileUrl) + self.isRemoved = isRemoved || CloudMetadataItem.isInTrash(fileUrl) self.hasUnresolvedConflicts = hasUnresolvedConflicts self.downloadingError = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingErrorKey) as? NSError self.uploadingError = metadataItem.value(forAttribute: NSMetadataUbiquitousItemUploadingErrorKey) as? NSError } - init(fileUrl: URL) throws { - let resources = try fileUrl.resourceValues(forKeys: [.nameKey, .contentModificationDateKey, .ubiquitousItemDownloadingStatusKey, .ubiquitousItemHasUnresolvedConflictsKey, .ubiquitousItemDownloadingErrorKey, .ubiquitousItemUploadingErrorKey]) + init(fileUrl: URL, isRemoved: Bool = false) throws { + let resources = try fileUrl.resourceValues(forKeys: [.nameKey, + .contentModificationDateKey, + .ubiquitousItemDownloadingStatusKey, + .ubiquitousItemHasUnresolvedConflictsKey, + .ubiquitousItemDownloadingErrorKey, + .ubiquitousItemUploadingErrorKey]) guard let downloadStatus = resources.ubiquitousItemDownloadingStatus, let lastModificationDate = resources.contentModificationDate?.roundedTime, let hasUnresolvedConflicts = resources.ubiquitousItemHasUnresolvedConflicts else { - throw NSError(domain: "CloudMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize CloudMetadataItem from NSMetadataItem"]) + LOG(.error, "Failed to initialize CloudMetadataItem from \(fileUrl) resources: \(resources.allValues)") + throw SynchronizationError.failedToCreateMetadataItem } self.fileName = fileUrl.lastPathComponent self.fileUrl = fileUrl self.isDownloaded = downloadStatus.rawValue == NSMetadataUbiquitousItemDownloadingStatusCurrent self.lastModificationDate = lastModificationDate - self.isRemoved = CloudMetadataItem.isInTrash(fileUrl) + self.isRemoved = isRemoved || CloudMetadataItem.isInTrash(fileUrl) self.hasUnresolvedConflicts = hasUnresolvedConflicts self.downloadingError = resources.ubiquitousItemDownloadingError self.uploadingError = resources.ubiquitousItemUploadingError diff --git a/iphone/Maps/Core/iCloud/SynchronizationError.swift b/iphone/Maps/Core/iCloud/SynchronizationError.swift index 2c6b0e04a9..45b530843b 100644 --- a/iphone/Maps/Core/iCloud/SynchronizationError.swift +++ b/iphone/Maps/Core/iCloud/SynchronizationError.swift @@ -6,6 +6,8 @@ case containerNotFound case failedToOpenLocalDirectoryFileDescriptor case failedToRetrieveLocalDirectoryContent + case failedToCreateMetadataItem + case failedToRetrieveMetadataQueryContent } extension SynchronizationError: LocalizedError { @@ -21,13 +23,17 @@ extension SynchronizationError: LocalizedError { return "Failed to open local directory file descriptor" case .failedToRetrieveLocalDirectoryContent: return "Failed to retrieve local directory content" + case .failedToCreateMetadataItem: + return "Failed to create metadata item." + case .failedToRetrieveMetadataQueryContent: + return "Failed to retrieve NSMetadataQuery content." } } } -extension SynchronizationError { - static func fromError(_ error: Error) -> SynchronizationError? { - let nsError = error as NSError +extension Error { + var ubiquitousError: SynchronizationError? { + let nsError = self as NSError switch nsError.code { // NSURLUbiquitousItemDownloadingErrorKey contains an error with this code when the item has not been uploaded to iCloud by the other devices yet case NSUbiquitousFileUnavailableError: diff --git a/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift b/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift index 8d6e06c21b..b803422f23 100644 --- a/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift +++ b/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift @@ -215,10 +215,10 @@ final class DefaultSynchronizationStateManager: SynchronizationStateManager { private static func getItemsWithErrors(_ cloudContents: CloudContents) -> [SynchronizationError] { cloudContents.reduce(into: [SynchronizationError](), { partialResult, cloudItem in - if let downloadingError = cloudItem.downloadingError, let synchronizationError = SynchronizationError.fromError(downloadingError) { + if let downloadingError = cloudItem.downloadingError, let synchronizationError = downloadingError.ubiquitousError { partialResult.append(synchronizationError) } - if let uploadingError = cloudItem.uploadingError, let synchronizationError = SynchronizationError.fromError(uploadingError) { + if let uploadingError = cloudItem.uploadingError, let synchronizationError = uploadingError.ubiquitousError { partialResult.append(synchronizationError) } }) diff --git a/iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift b/iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift index dcc93afa20..f8d2551933 100644 --- a/iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift +++ b/iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift @@ -141,65 +141,34 @@ class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor { return metadataQuery } - class func getContentsFromNotification(_ notification: Notification, _ onError: (Error) -> Void) -> CloudContents { + class func getContentsFromNotification(_ notification: Notification) throws -> CloudContents { guard let metadataQuery = notification.object as? NSMetadataQuery, let metadataItems = metadataQuery.results as? [NSMetadataItem] else { - return [] + throw SynchronizationError.failedToRetrieveMetadataQueryContent } - - let cloudMetadataItems = CloudContents(metadataItems.compactMap { item in - do { - return try CloudMetadataItem(metadataItem: item) - } catch { - onError(error) - return nil - } - }) - return cloudMetadataItems + return try metadataItems.map { try CloudMetadataItem(metadataItem: $0) } } // There are no ways to retrieve the content of iCloud's .Trash directory on the macOS because it uses different file system and place trashed content in the /Users//.Trash which cannot be observed without access. // When we get a new notification and retrieve the metadata from the object the actual list of items in iOS contains both current and deleted files (which is in .Trash/ directory now) but on macOS we only have absence of the file. So there are no way to get list of deleted items on macOS on didFinishGathering state. // Due to didUpdate state we can get the list of deleted items on macOS from the userInfo property but cannot get their new url. - class func getTrashContentsFromNotification(_ notification: Notification, _ onError: (Error) -> Void) -> CloudContents { - guard let removedItems = notification.userInfo?[NSMetadataQueryUpdateRemovedItemsKey] as? [NSMetadataItem] else { return [] } - return CloudContents(removedItems.compactMap { metadataItem in - do { - var item = try CloudMetadataItem(metadataItem: metadataItem) - // on macOS deleted file will not be in the ./Trash directory, but it doesn't mean that it is not removed because it is placed in the NSMetadataQueryUpdateRemovedItems array. - item.isRemoved = true - return item - } catch { - onError(error) - return nil - } - }) + class func getTrashContentsFromNotification(_ notification: Notification) throws -> CloudContents { + guard let removedItems = notification.userInfo?[NSMetadataQueryUpdateRemovedItemsKey] as? [NSMetadataItem] else { + return [] + } + return try removedItems.map { try CloudMetadataItem(metadataItem: $0, isRemoved: true) } } - class func getTrashedContentsFromTrashDirectory(fileManager: FileManager, ubiquitousDocumentsDirectory: URL?, onError: (Error) -> Void) -> CloudContents { + class func getTrashedContentsFromTrashDirectory(fileManager: FileManager, ubiquitousDocumentsDirectory: URL) throws -> CloudContents { // There are no ways to retrieve the content of iCloud's .Trash directory on macOS. if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac { return [] } - // On iOS we can get the list of deleted items from the .Trash directory but only when iCloud is enabled. - guard let ubiquitousDocumentsDirectory, - let trashDirectoryUrl = try? fileManager.trashDirectoryUrl(for: ubiquitousDocumentsDirectory), - let removedItems = try? fileManager.contentsOfDirectory(at: trashDirectoryUrl, - includingPropertiesForKeys: [.isDirectoryKey], - options: [.skipsPackageDescendants, .skipsSubdirectoryDescendants]) else { - return [] - } - let removedCloudMetadataItems = CloudContents(removedItems.compactMap { url in - do { - var item = try CloudMetadataItem(fileUrl: url) - item.isRemoved = true - return item - } catch { - onError(error) - return nil - } - }) - return removedCloudMetadataItems + let trashDirectoryUrl = try fileManager.trashDirectoryUrl(for: ubiquitousDocumentsDirectory) + let removedItems = try fileManager.contentsOfDirectory(at: trashDirectoryUrl, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsPackageDescendants, .skipsSubdirectoryDescendants]) + return try removedItems.map { try CloudMetadataItem(fileUrl: $0, isRemoved: true) } } } @@ -236,16 +205,18 @@ private extension iCloudDocumentsDirectoryMonitor { } @objc func queryDidFinishGathering(_ notification: Notification) { - guard isCloudAvailable() else { return } + guard isCloudAvailable(), let ubiquitousDocumentsDirectory else { return } metadataQuery?.disableUpdates() LOG(.debug, "iCloudMonitor: Query did finish gathering") - let contents = Self.getContentsFromNotification(notification, metadataQueryErrorHandler) - let trashedContents = Self.getTrashedContentsFromTrashDirectory(fileManager: fileManager, - ubiquitousDocumentsDirectory: ubiquitousDocumentsDirectory, - onError: metadataQueryErrorHandler) - LOG(.debug, "iCloudMonitor: Cloud contents count: \(contents.count)") - LOG(.debug, "iCloudMonitor: Trashed contents count: \(trashedContents.count)") - delegate?.didFinishGathering(contents: contents + trashedContents) + do { + let contents = try Self.getContentsFromNotification(notification) + let trashedContents = try Self.getTrashedContentsFromTrashDirectory(fileManager: fileManager, ubiquitousDocumentsDirectory: ubiquitousDocumentsDirectory) + LOG(.debug, "iCloudMonitor: Cloud contents count: \(contents.count)") + LOG(.debug, "iCloudMonitor: Trashed contents count: \(trashedContents.count)") + delegate?.didFinishGathering(contents: contents + trashedContents) + } catch { + delegate?.didReceiveCloudMonitorError(error) + } metadataQuery?.enableUpdates() } @@ -253,17 +224,15 @@ private extension iCloudDocumentsDirectoryMonitor { guard isCloudAvailable() else { return } metadataQuery?.disableUpdates() LOG(.debug, "iCloudMonitor: Query did update") - let contents = Self.getContentsFromNotification(notification, metadataQueryErrorHandler) - let trashedContents = Self.getTrashContentsFromNotification(notification, metadataQueryErrorHandler) - LOG(.debug, "iCloudMonitor: Cloud contents count: \(contents.count)") - LOG(.debug, "iCloudMonitor: Trashed contents count: \(trashedContents.count)") - delegate?.didUpdate(contents: contents + trashedContents) + do { + let contents = try Self.getContentsFromNotification(notification) + let trashedContents = try Self.getTrashContentsFromNotification(notification) + LOG(.debug, "iCloudMonitor: Cloud contents count: \(contents.count)") + LOG(.debug, "iCloudMonitor: Trashed contents count: \(trashedContents.count)") + delegate?.didUpdate(contents: contents + trashedContents) + } catch { + delegate?.didReceiveCloudMonitorError(error) + } metadataQuery?.enableUpdates() } - - private var metadataQueryErrorHandler: (Error) -> Void { - { [weak self] error in - self?.delegate?.didReceiveCloudMonitorError(error) - } - } } diff --git a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift index a9c211a5f8..c25efc9824 100644 --- a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift +++ b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift @@ -1,22 +1,15 @@ final class SettingsTableViewiCloudSwitchCell: SettingsTableViewDetailedSwitchCell { @objc - func updateWithError(_ error: NSError?) { - if let error = error as? SynchronizationError { - switch error { - case .fileUnavailable, .fileNotUploadedDueToQuota, .ubiquityServerNotAvailable: - accessoryView = switchButton - case .iCloudIsNotAvailable, .containerNotFound: - accessoryView = nil - accessoryType = .detailButton - default: - break - } - detailTextLabel?.text = error.localizedDescription - } else { - accessoryView = switchButton - detailTextLabel?.text?.removeAll() + func updateWithSynchronizationState(_ state: CloudStorageSynchronizationState) { + guard state.isAvailable else { + accessoryView = nil + accessoryType = .detailButton + return } + accessoryView = switchButton + detailTextLabel?.text = state.error?.localizedDescription + setOn(state.isOn, animated: true) setNeedsLayout() } } diff --git a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm index 83efc3cd22..d1d06354c9 100644 --- a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm +++ b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm @@ -185,15 +185,14 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS } [self.nightModeCell configWithTitle:L(@"pref_appearance_title") info:nightMode]; - BOOL isICLoudSynchronizationEnabled = [MWMSettings iCLoudSynchronizationEnabled]; [self.iCloudSynchronizationCell configWithDelegate:self title:@"iCloud Synchronization (Beta)" - isOn:isICLoudSynchronizationEnabled]; + isOn:[MWMSettings iCLoudSynchronizationEnabled]]; __weak __typeof(self) weakSelf = self; - [CloudStorageManager.shared addObserver:self onErrorCompletionHandler:^(NSError * _Nullable error) { + [CloudStorageManager.shared addObserver:self synchronizationStateDidChangeHandler:^(CloudStorageSynchronizationState * state) { __strong auto strongSelf = weakSelf; - [strongSelf.iCloudSynchronizationCell updateWithError:error]; + [strongSelf.iCloudSynchronizationCell updateWithSynchronizationState:state]; }]; [self.enableLoggingCell configWithDelegate:self title:L(@"enable_logging") isOn:MWMSettings.isFileLoggingEnabled]; @@ -330,7 +329,6 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS } else if (cell == self.iCloudSynchronizationCell) { if (![NSUserDefaults.standardUserDefaults boolForKey:kUDDidShowICloudSynchronizationEnablingAlert]) { [self showICloudSynchronizationEnablingAlert:^(BOOL isEnabled) { - [self.iCloudSynchronizationCell setOn:isEnabled animated:YES]; [MWMSettings setICLoudSynchronizationEnabled:isEnabled]; }]; } else {