diff --git a/Sources/InfomaniakCore/Asynchronous/FlowToAsyncResult.swift b/Sources/InfomaniakCore/Asynchronous/FlowToAsyncResult.swift
new file mode 100644
index 0000000..2ae43dd
--- /dev/null
+++ b/Sources/InfomaniakCore/Asynchronous/FlowToAsyncResult.swift
@@ -0,0 +1,82 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2023 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 Combine
+import Foundation
+
+/// Encapsulate a simple asynchronous event into a Combine flow in order to provide a nice swift `async Result<>`.
+///
+/// Useful when dealing with old xOS APIs that do not work well with swift native structured concurrency.
+@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+public final class FlowToAsyncResult {
+ // MARK: Private
+
+ /// Internal observation of the Combine progress Pipe
+ private var flowObserver: AnyCancellable?
+
+ /// Internal Task that wraps the combine result observation
+ private lazy var resultTask: Task = Task {
+ let result: Success = try await withCheckedThrowingContinuation { continuation in
+ self.flowObserver = flow.sink { result in
+ switch result {
+ case .finished:
+ break
+ case .failure(let error):
+ continuation.resume(throwing: error)
+ }
+ self.flowObserver?.cancel()
+ } receiveValue: { value in
+ continuation.resume(with: .success(value))
+ }
+ }
+
+ return result
+ }
+
+ // MARK: Public
+
+ /// Track task progress with internal Combine pipe.
+ ///
+ /// Public entry point, send result threw this pipe.
+ public let flow = PassthroughSubject()
+
+ /// Provides a nice `async Result` public API
+ public var result: Result {
+ get async {
+ return await resultTask.result
+ }
+ }
+
+ // MARK: Init
+
+ public init() {
+ // META keep SonarCloud happy
+ }
+}
+
+/// Shorthand to access underlying flow
+@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+public extension FlowToAsyncResult {
+ func send(_ input: Success) {
+ flow.send(input)
+ }
+
+ func send(completion: Subscribers.Completion) {
+ flow.send(completion: completion)
+ }
+}
diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift
index b3984a6..9386c2b 100644
--- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift
+++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderFileRepresentation.swift
@@ -18,13 +18,19 @@
#if canImport(MobileCoreServices)
-import Combine
import Foundation
import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to load an url from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderFileRepresentation: NSObject, ProgressResultable {
+ /// Something to transform events to a nice `async Result`
+ private let flowToAsync = FlowToAsyncResult()
+
+ /// Shorthand for default FileManager
+ private let fileManager = FileManager.default
+
+ /// Domain specific errors
public enum ErrorDomain: Error, Equatable{
case UTINotFound
case UnableToLoadFile
@@ -33,17 +39,6 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable
public typealias Success = URL
public typealias Failure = Error
- /// Track task progress with internal Combine pipe
- private let resultProcessed = PassthroughSubject()
-
- /// Internal observation of the Combine progress Pipe
- private var resultProcessedObserver: AnyCancellable?
-
- /// Internal Task that wraps the combine result observation
- private var computeResultTask: Task?
-
- private let fileManager = FileManager.default
-
public init(from itemProvider: NSItemProvider) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
@@ -55,9 +50,9 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable
super.init()
// Set progress and hook completion closure to a combine pipe
- progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileProviderURL, error in
+ progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [self] fileProviderURL, error in
guard let fileProviderURL, error == nil else {
- self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.UnableToLoadFile))
+ flowToAsync.send(completion: .failure(error ?? ErrorDomain.UnableToLoadFile))
return
}
@@ -66,49 +61,27 @@ public final class ItemProviderFileRepresentation: NSObject, ProgressResultable
@InjectService var pathProvider: AppGroupPathProvidable
let temporaryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
- try self.fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)
+ try fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)
let fileName = fileProviderURL.appendingPathExtension(for: UTI).lastPathComponent
let temporaryFileURL = temporaryURL.appendingPathComponent(fileName)
- try self.fileManager.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path)
- self.resultProcessed.send(temporaryFileURL)
- self.resultProcessed.send(completion: .finished)
+ try fileManager.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path)
+
+ flowToAsync.send(temporaryFileURL)
+ flowToAsync.send(completion: .finished)
} catch {
- self.resultProcessed.send(completion: .failure(error))
+ flowToAsync.send(completion: .failure(error))
}
}
-
- /// Wrap the Combine pipe to a native Swift Async Task for convenience
- computeResultTask = Task {
- let resultURL: URL = try await withCheckedThrowingContinuation { continuation in
- self.resultProcessedObserver = resultProcessed.sink { result in
- switch result {
- case .finished:
- break
- case .failure(let error):
- continuation.resume(throwing: error)
- }
- self.resultProcessedObserver?.cancel()
- } receiveValue: { value in
- continuation.resume(with: .success(value))
- }
- }
-
- return resultURL
- }
}
- // MARK: Public
+ // MARK: ProgressResultable
public var progress: Progress
public var result: Result {
get async {
- guard let computeResultTask else {
- fatalError("This never should be nil")
- }
-
- return await computeResultTask.result
+ await self.flowToAsync.result
}
}
}
diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift
index 12c8902..ba72902 100644
--- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift
+++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderTextRepresentation.swift
@@ -18,13 +18,19 @@
#if canImport(MobileCoreServices)
-import Combine
import Foundation
import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a raw text file from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderTextRepresentation: NSObject, ProgressResultable {
+ /// Something to transform events to a nice `async Result`
+ private let flowToAsync = FlowToAsyncResult()
+
+ /// Shorthand for default FileManager
+ private let fileManager = FileManager.default
+
+ /// Domain specific errors
public enum ErrorDomain: Error, Equatable {
case UTINotFound
case UTINotSupported
@@ -36,17 +42,6 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
private static let progressStep: Int64 = 1
- /// Track task progress with internal Combine pipe
- private let resultProcessed = PassthroughSubject()
-
- /// Internal observation of the Combine progress Pipe
- private var resultProcessedObserver: AnyCancellable?
-
- /// Internal Task that wraps the combine result observation
- private var computeResultTask: Task?
-
- private let fileManager = FileManager.default
-
public init(from itemProvider: NSItemProvider) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
@@ -60,14 +55,14 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
let childProgress = Progress()
progress.addChild(childProgress, withPendingUnitCount: Self.progressStep)
- itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { coding, error in
+ itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { [self] coding, error in
defer {
childProgress.completedUnitCount += Self.progressStep
}
guard error == nil,
let coding else {
- self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.unknown))
+ flowToAsync.send(completion: .failure(error ?? ErrorDomain.unknown))
return
}
@@ -76,45 +71,26 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
@InjectService var pathProvider: AppGroupPathProvidable
let temporaryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
- try self.fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)
+ try fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)
// Is String
- guard try !self.stringHandling(coding, temporaryURL: temporaryURL) else {
+ guard try !stringHandling(coding, temporaryURL: temporaryURL) else {
return
}
// Is Data
- guard try !self.dataHandling(coding, typeIdentifier: typeIdentifier, temporaryURL: temporaryURL) else {
+ guard try !dataHandling(coding, typeIdentifier: typeIdentifier, temporaryURL: temporaryURL) else {
return
}
// Not supported
- self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotSupported))
+ flowToAsync.send(completion: .failure(ErrorDomain.UTINotSupported))
} catch {
- self.resultProcessed.send(completion: .failure(error))
+ flowToAsync.send(completion: .failure(error))
return
}
}
-
- /// Wrap the Combine pipe to a native Swift Async Task for convenience
- computeResultTask = Task {
- let resultURL: URL = try await withCheckedThrowingContinuation { continuation in
- self.resultProcessedObserver = resultProcessed.sink { result in
- switch result {
- case .finished:
- break
- case .failure(let error):
- continuation.resume(throwing: error)
- }
- self.resultProcessedObserver?.cancel()
- } receiveValue: { value in
- continuation.resume(with: .success(value))
- }
- }
-
- return resultURL
- }
}
private func stringHandling(_ coding: NSSecureCoding, temporaryURL: URL) throws -> Bool {
@@ -124,8 +100,8 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
let targetURL = temporaryURL.appendingPathComponent("\(UUID().uuidString).txt")
try text.write(to: targetURL, atomically: true, encoding: .utf8)
- resultProcessed.send(targetURL)
- resultProcessed.send(completion: .finished)
+ flowToAsync.send(targetURL)
+ flowToAsync.send(completion: .finished)
return true
}
@@ -136,7 +112,7 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
}
guard let uti = UTI(typeIdentifier) else {
- resultProcessed.send(completion: .failure(ErrorDomain.UTINotFound))
+ flowToAsync.send(completion: .failure(ErrorDomain.UTINotFound))
return false
}
@@ -145,23 +121,19 @@ public final class ItemProviderTextRepresentation: NSObject, ProgressResultable
.appendingPathExtension(for: uti)
try data.write(to: targetURL)
- resultProcessed.send(targetURL)
- resultProcessed.send(completion: .finished)
+ flowToAsync.send(targetURL)
+ flowToAsync.send(completion: .finished)
return true
}
- // MARK: Public
+ // MARK: ProgressResultable
public var progress: Progress
public var result: Result {
get async {
- guard let computeResultTask else {
- fatalError("This never should be nil")
- }
-
- return await computeResultTask.result
+ return await flowToAsync.result
}
}
}
diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderWeblocRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderWeblocRepresentation.swift
index bd0baff..2757648 100644
--- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderWeblocRepresentation.swift
+++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderWeblocRepresentation.swift
@@ -18,13 +18,19 @@
#if canImport(MobileCoreServices)
-import Combine
import Foundation
import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a webloc plist from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultable {
+ /// Something to transform events to a nice `async Result`
+ private let flowToAsync = FlowToAsyncResult()
+
+ /// Shorthand for default FileManager
+ private let fileManager = FileManager.default
+
+ /// Domain specific errors
public enum ErrorDomain: Error, Equatable {
case unableToLoadURLForObject
}
@@ -32,27 +38,16 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
public typealias Success = URL
public typealias Failure = Error
- /// Track task progress with internal Combine pipe
- private let resultProcessed = PassthroughSubject()
-
- /// Internal observation of the Combine progress Pipe
- private var resultProcessedObserver: AnyCancellable?
-
- /// Internal Task that wraps the combine result observation
- private var computeResultTask: Task?
-
- private let fileManager = FileManager.default
-
public init(from itemProvider: NSItemProvider) throws {
// Keep compiler happy
progress = Progress(totalUnitCount: 1)
super.init()
- progress = itemProvider.loadObject(ofClass: URL.self) { path, error in
+ progress = itemProvider.loadObject(ofClass: URL.self) { [self] path, error in
guard error == nil, let path: URL = path else {
let error: Error = error ?? ErrorDomain.unableToLoadURLForObject
- self.resultProcessed.send(completion: .failure(error))
+ flowToAsync.send(completion: .failure(error))
return
}
@@ -63,7 +58,7 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
@InjectService var pathProvider: AppGroupPathProvidable
let tmpDirectoryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
- try self.fileManager.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true)
+ try fileManager.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true)
let fileName = path.lastPathComponent
let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).webloc")
@@ -71,44 +66,21 @@ public final class ItemProviderWeblocRepresentation: NSObject, ProgressResultabl
let data = try encoder.encode(content)
try data.write(to: targetURL)
- self.resultProcessed.send(targetURL)
- self.resultProcessed.send(completion: .finished)
+ flowToAsync.send(targetURL)
+ flowToAsync.send(completion: .finished)
} catch {
- self.resultProcessed.send(completion: .failure(error))
+ flowToAsync.send(completion: .failure(error))
}
}
-
- /// Wrap the Combine pipe to a native Swift Async Task for convenience
- computeResultTask = Task {
- let resultURL: URL = try await withCheckedThrowingContinuation { continuation in
- self.resultProcessedObserver = resultProcessed.sink { result in
- switch result {
- case .finished:
- break
- case .failure(let error):
- continuation.resume(throwing: error)
- }
- self.resultProcessedObserver?.cancel()
- } receiveValue: { value in
- continuation.resume(with: .success(value))
- }
- }
-
- return resultURL
- }
}
- // MARK: Public
+ // MARK: ProgressResultable
public var progress: Progress
public var result: Result {
get async {
- guard let computeResultTask else {
- fatalError("This never should be nil")
- }
-
- return await computeResultTask.result
+ return await flowToAsync.result
}
}
}
diff --git a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift
index 34ad389..1e693f7 100644
--- a/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift
+++ b/Sources/InfomaniakCore/ItemProviderRepresentation/ItemProviderZipRepresentation.swift
@@ -25,6 +25,13 @@ import InfomaniakDI
/// Something that can provide a `Progress` and an async `Result` in order to make a zip from a `NSItemProvider`
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
+ /// Something to transform events to a nice `async Result`
+ private let flowToAsync = FlowToAsyncResult()
+
+ /// Shorthand for default FileManager
+ private let fileManager = FileManager.default
+
+ /// Domain specific errors
public enum ErrorDomain: Error, Equatable {
case unableToLoadURLForObject
case notADirectory
@@ -44,8 +51,6 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
/// Internal Task that wraps the combine result observation
private var computeResultTask: Task?
- private let fileManager = FileManager.default
-
public init(from itemProvider: NSItemProvider) throws {
// It must be a directory for the OS to zip it for us, a file returns a file
guard itemProvider.underlyingType == .isDirectory else {
@@ -77,57 +82,34 @@ public final class ItemProviderZipRepresentation: NSObject, ProgressResultable {
// compress content of folder and move it somewhere we can safely store it for upload
var error: NSError?
- coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { zipURL in
+ coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { [self] zipURL in
do {
@InjectService var pathProvider: AppGroupPathProvidable
let tmpDirectoryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
- try self.fileManager.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true)
+ try fileManager.createDirectory(at: tmpDirectoryURL, withIntermediateDirectories: true)
let fileName = path.lastPathComponent
let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).zip")
- try self.fileManager.moveItem(at: zipURL, to: targetURL)
- self.resultProcessed.send(targetURL)
- self.resultProcessed.send(completion: .finished)
+ try fileManager.moveItem(at: zipURL, to: targetURL)
+ flowToAsync.send(targetURL)
+ flowToAsync.send(completion: .finished)
} catch {
- self.resultProcessed.send(completion: .failure(error))
+ flowToAsync.send(completion: .failure(error))
}
childProgress.completedUnitCount += Self.progressStep
}
}
-
- /// Wrap the Combine pipe to a native Swift Async Task for convenience
- computeResultTask = Task {
- let resultURL: URL = try await withCheckedThrowingContinuation { continuation in
- self.resultProcessedObserver = resultProcessed.sink { result in
- switch result {
- case .finished:
- break
- case .failure(let error):
- continuation.resume(throwing: error)
- }
- self.resultProcessedObserver?.cancel()
- } receiveValue: { value in
- continuation.resume(with: .success(value))
- }
- }
-
- return resultURL
- }
}
- // MARK: Public
+ // MARK: ProgressResultable
public var progress: Progress
public var result: Result {
get async {
- guard let computeResultTask else {
- fatalError("This never should be nil")
- }
-
- return await computeResultTask.result
+ return await flowToAsync.result
}
}
}