From 13abe141428c07b020e83937b5e25420ce58a1c2 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Thu, 25 Sep 2025 03:57:03 +0530 Subject: [PATCH] async ios --- mobile/ios/Runner.xcodeproj/project.pbxproj | 19 ++++- .../Background/BackgroundWorker.g.swift | 43 +++++++----- .../Background/BackgroundWorkerApiImpl.swift | 26 ++++--- .../Runner/Connectivity/Connectivity.g.swift | 15 ++-- .../Connectivity/ConnectivityApiImpl.swift | 4 +- mobile/ios/Runner/Core/ImmichPlugin.swift | 12 ++++ .../{Sync => Core}/PHAssetExtensions.swift | 0 .../PHAssetResourceExtensions.swift | 0 mobile/ios/Runner/Sync/Messages.g.swift | 70 +++++++++++-------- mobile/ios/Runner/Sync/MessagesImpl.swift | 36 ++++++++-- 10 files changed, 150 insertions(+), 75 deletions(-) create mode 100644 mobile/ios/Runner/Core/ImmichPlugin.swift rename mobile/ios/Runner/{Sync => Core}/PHAssetExtensions.swift (100%) rename mobile/ios/Runner/{Sync => Core}/PHAssetResourceExtensions.swift (100%) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index cb9dbc60bd..867925b4f3 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -133,11 +133,14 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; + B2D27ABE2E84A0FF004DD55B /* Core */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Core; + sourceTree = ""; + }; F0B57D3D2DF764BD00DC5BCC /* WidgetExtension */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -247,6 +250,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B2D27ABE2E84A0FF004DD55B /* Core */, B25D37792E72CA15008B6CA7 /* Connectivity */, B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, @@ -332,6 +336,7 @@ ); fileSystemSynchronizedGroups = ( B2CF7F8C2DDE4EBB00744BF6 /* Sync */, + B2D27ABE2E84A0FF004DD55B /* Core */, ); name = Runner; productName = Runner; @@ -521,10 +526,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -553,10 +562,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index ece5cd5f64..aeeb45ca88 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -179,11 +179,12 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter()) } + /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerFgHostApi { - func enable() throws - func configure(settings: BackgroundWorkerSettings) throws - func disable() throws + func enable(completion: @escaping (Result) -> Void) + func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result) -> Void) + func disable(completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -195,11 +196,13 @@ class BackgroundWorkerFgHostApiSetup { let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { enableChannel.setMessageHandler { _, reply in - do { - try api.enable() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) + api.enable { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { @@ -210,11 +213,13 @@ class BackgroundWorkerFgHostApiSetup { configureChannel.setMessageHandler { message, reply in let args = message as! [Any?] let settingsArg = args[0] as! BackgroundWorkerSettings - do { - try api.configure(settings: settingsArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) + api.configure(settings: settingsArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { @@ -223,11 +228,13 @@ class BackgroundWorkerFgHostApiSetup { let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { disableChannel.setMessageHandler { _, reply in - do { - try api.disable() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) + api.disable { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { diff --git a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift index f7f8f69989..b85bff8bc0 100644 --- a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -1,23 +1,27 @@ import BackgroundTasks class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { - - func enable() throws { - BackgroundWorkerApiImpl.scheduleRefreshWorker() - BackgroundWorkerApiImpl.scheduleProcessingWorker() - print("BackgroundWorkerApiImpl:enable Background worker scheduled") + func enable(completion: @escaping (Result) -> Void) { + dispatch(completion: completion, block: { + BackgroundWorkerApiImpl.scheduleRefreshWorker() + BackgroundWorkerApiImpl.scheduleProcessingWorker() + print("BackgroundWorkerApiImpl:enable Background worker scheduled") + }); } - func configure(settings: BackgroundWorkerSettings) throws { + func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result) -> Void) { // Android only + completion(Result.success(Void())) } - func disable() throws { - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); - print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers") + func disable(completion: @escaping (Result) -> Void) { + dispatch(completion: completion, block: { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); + print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers") + }); } - + private static let refreshTaskID = "app.alextran.immich.background.refreshUpload" private static let processingTaskID = "app.alextran.immich.background.processingUpload" private static let taskSemaphore = DispatchSemaphore(value: 1) diff --git a/mobile/ios/Runner/Connectivity/Connectivity.g.swift b/mobile/ios/Runner/Connectivity/Connectivity.g.swift index 45333f03d8..78c0b4f654 100644 --- a/mobile/ios/Runner/Connectivity/Connectivity.g.swift +++ b/mobile/ios/Runner/Connectivity/Connectivity.g.swift @@ -94,9 +94,10 @@ class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter()) } + /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol ConnectivityApi { - func getCapabilities() throws -> [NetworkCapability] + func getCapabilities(completion: @escaping (Result<[NetworkCapability], Error>) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -115,11 +116,13 @@ class ConnectivityApiSetup { : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) if let api = api { getCapabilitiesChannel.setMessageHandler { _, reply in - do { - let result = try api.getCapabilities() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) + api.getCapabilities { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { diff --git a/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift index 0261cb26fb..96739ebadc 100644 --- a/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift +++ b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift @@ -1,6 +1,6 @@ class ConnectivityApiImpl: ConnectivityApi { - func getCapabilities() throws -> [NetworkCapability] { - [] + func getCapabilities(completion: @escaping (Result<[NetworkCapability], any Error>) -> Void) { + completion(Result.success([])) } } diff --git a/mobile/ios/Runner/Core/ImmichPlugin.swift b/mobile/ios/Runner/Core/ImmichPlugin.swift new file mode 100644 index 0000000000..ee6292581b --- /dev/null +++ b/mobile/ios/Runner/Core/ImmichPlugin.swift @@ -0,0 +1,12 @@ +func dispatch( + qos: DispatchQoS.QoSClass = .default, + completion: @escaping (Result) -> Void, + block: @escaping () throws -> T +) { + DispatchQueue.global(qos: qos).async { + let result = Result { try block() } + DispatchQueue.main.async { + completion(result) + } + } +} diff --git a/mobile/ios/Runner/Sync/PHAssetExtensions.swift b/mobile/ios/Runner/Core/PHAssetExtensions.swift similarity index 100% rename from mobile/ios/Runner/Sync/PHAssetExtensions.swift rename to mobile/ios/Runner/Core/PHAssetExtensions.swift diff --git a/mobile/ios/Runner/Sync/PHAssetResourceExtensions.swift b/mobile/ios/Runner/Core/PHAssetResourceExtensions.swift similarity index 100% rename from mobile/ios/Runner/Sync/PHAssetResourceExtensions.swift rename to mobile/ios/Runner/Core/PHAssetResourceExtensions.swift diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index 305aca5266..518f475daf 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -355,13 +355,13 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol NativeSyncApi { func shouldFullSync() throws -> Bool - func getMediaChanges() throws -> SyncDelta + func getMediaChanges(completion: @escaping (Result) -> Void) func checkpointSync() throws func clearSyncCheckpoint() throws - func getAssetIdsForAlbum(albumId: String) throws -> [String] - func getAlbums() throws -> [PlatformAlbum] - func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 - func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] + func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) + func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void) + func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result) -> Void) + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void) func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) func cancelHashing() throws } @@ -395,11 +395,13 @@ class NativeSyncApiSetup { : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) if let api = api { getMediaChangesChannel.setMessageHandler { _, reply in - do { - let result = try api.getMediaChanges() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) + api.getMediaChanges { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { @@ -438,11 +440,13 @@ class NativeSyncApiSetup { getAssetIdsForAlbumChannel.setMessageHandler { message, reply in let args = message as! [Any?] let albumIdArg = args[0] as! String - do { - let result = try api.getAssetIdsForAlbum(albumId: albumIdArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) + api.getAssetIdsForAlbum(albumId: albumIdArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { @@ -453,11 +457,13 @@ class NativeSyncApiSetup { : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) if let api = api { getAlbumsChannel.setMessageHandler { _, reply in - do { - let result = try api.getAlbums() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) + api.getAlbums { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { @@ -471,11 +477,13 @@ class NativeSyncApiSetup { let args = message as! [Any?] let albumIdArg = args[0] as! String let timestampArg = args[1] as! Int64 - do { - let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) + api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { @@ -489,11 +497,13 @@ class NativeSyncApiSetup { let args = message as! [Any?] let albumIdArg = args[0] as! String let updatedTimeCondArg: Int64? = nilOrValue(args[1]) - do { - let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) + api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } } } } else { diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index bb23bae6b6..6a7b7dc5f8 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -18,6 +18,7 @@ struct AssetWrapper: Hashable, Equatable { } class NativeSyncApiImpl: NativeSyncApi { + private let defaults: UserDefaults private let changeTokenKey = "immich:changeToken" private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] @@ -75,7 +76,12 @@ class NativeSyncApiImpl: NativeSyncApi { return false } - func getAlbums() throws -> [PlatformAlbum] { + + func getAlbums(completion: @escaping (Result<[PlatformAlbum], any Error>) -> Void) { + dispatch(qos: .userInitiated, completion: completion, block: getAlbums) + } + + private func getAlbums() throws -> [PlatformAlbum] { var albums: [PlatformAlbum] = [] albumTypes.forEach { type in @@ -112,7 +118,11 @@ class NativeSyncApiImpl: NativeSyncApi { return albums.sorted { $0.id < $1.id } } - func getMediaChanges() throws -> SyncDelta { + func getMediaChanges(completion: @escaping (Result) -> Void) { + dispatch(qos: .userInitiated, completion: completion, block: getMediaChanges) + } + + private func getMediaChanges() throws -> SyncDelta { guard #available(iOS 16, *) else { throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil) } @@ -198,7 +208,11 @@ class NativeSyncApiImpl: NativeSyncApi { return albumAssets } - func getAssetIdsForAlbum(albumId: String) throws -> [String] { + func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) { + dispatch(qos: .userInitiated, completion: completion, block: { try self.getAssetIdsForAlbum(albumId: albumId) }) + } + + private func getAssetIdsForAlbum(albumId: String) throws -> [String] { let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) guard let album = collections.firstObject else { return [] @@ -214,7 +228,13 @@ class NativeSyncApiImpl: NativeSyncApi { return ids } - func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { + func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result) -> Void) { + dispatch(qos: .userInitiated, completion: completion, block: { + try self.getAssetsCountSince(albumId: albumId, timestamp: timestamp) + }) + } + + private func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) guard let album = collections.firstObject else { return 0 @@ -228,7 +248,13 @@ class NativeSyncApiImpl: NativeSyncApi { return Int64(assets.count) } - func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], any Error>) -> Void) { + dispatch(qos: .userInitiated, completion: completion, block: { + try self.getAssetsForAlbum(albumId: albumId, updatedTimeCond: updatedTimeCond) + }) + } + + private func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) guard let album = collections.firstObject else { return []