Skip to content

Commit

Permalink
feat: Abstract handling of NSItemProvider
Browse files Browse the repository at this point in the history
feat: Full Integration testing of ItemProviderRepresentation
  • Loading branch information
adrien-coye committed Jul 17, 2023
1 parent 3e858f4 commit 593a5fe
Show file tree
Hide file tree
Showing 13 changed files with 993 additions and 56 deletions.
25 changes: 17 additions & 8 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "78424be314842833c04bc3bef5b72e85fff99204",
"version" : "5.6.4"
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
"version" : "5.7.1"
}
},
{
Expand Down Expand Up @@ -41,26 +41,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-core.git",
"state" : {
"revision" : "dd91f5f967c4ae89c37e24ab2a0315c31106648f",
"version" : "13.6.0"
"revision" : "f1434caadda443b4ed2261b91ea4f43ab1ee2aa5",
"version" : "13.15.1"
}
},
{
"identity" : "realm-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-swift",
"state" : {
"revision" : "8ac6fe1aa5d0fb0100062d80863416a4d70de8ca",
"version" : "10.37.0"
"revision" : "b287dc102036ff425bd8a88483f0a5596871f05e",
"version" : "10.41.0"
}
},
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
"revision" : "2e7899aff930ed3b8d81be1909492f7684bbd481",
"version" : "8.3.1"
"revision" : "e46936ed191c0112cd3276e1c10c0bb7f865268e",
"version" : "8.9.1"
}
},
{
Expand All @@ -71,6 +71,15 @@
"revision" : "32e8d724467f8fe623624570367e3d50c5638e46",
"version" : "1.5.2"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/weichsel/ZIPFoundation.git",
"state" : {
"revision" : "43ec568034b3731101dbf7670765d671c30f54f3",
"version" : "0.9.16"
}
}
],
"version" : 2
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let package = Package(
.package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.0.0")),
.package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.7.0")),
.package(url: "https://github.com/matomo-org/matomo-sdk-ios", .upToNextMajor(from: "7.5.2")),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0"))
],
targets: [
.target(
Expand All @@ -37,7 +38,7 @@ let package = Package(
),
.testTarget(
name: "InfomaniakCoreTests",
dependencies: ["InfomaniakCore"]
dependencies: ["InfomaniakCore","ZIPFoundation"]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Infomaniak Mail - iOS App
Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
*/

#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 {
public enum ErrorDomain: Error, Equatable{
case UTINotFound
case UnableToLoadFile
}

public typealias Success = URL
public typealias Failure = Error

/// Track task progress with internal Combine pipe
private let resultProcessed = PassthroughSubject<Success, Failure>()

/// Internal observation of the Combine progress Pipe
private var resultProcessedObserver: AnyCancellable?

/// Internal Task that wraps the combine result observation
private var computeResultTask: Task<Success, Failure>?

private let fileManager = FileManager.default

public init(from itemProvider: NSItemProvider) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)

super.init()

// Set progress and hook completion closure to a combine pipe
progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileProviderURL, error in
guard let fileProviderURL, error == nil else {
self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.UnableToLoadFile))
return
}

do {
let UTI = UTI(rawValue: typeIdentifier as CFString)
@InjectService var pathProvider: AppGroupPathProvidable
let temporaryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try self.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)
} catch {
self.resultProcessed.send(completion: .failure(error))
}
}

/// Wrap the Combine pipe to a native Swift Async Task for convenience
computeResultTask = Task {
do {
let result: 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 result

} catch {
throw error
}
}
}

// MARK: Public

public var progress: Progress

public var result: Result<URL, Error> {
get async {
guard let computeResultTask else {
fatalError("This never should be nil")
}

return await computeResultTask.result
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
Infomaniak Mail - iOS App
Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
*/

#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 {
public enum ErrorDomain: Error, Equatable {
case UTINotFound
case UTINotSupported
case unknown
}

public typealias Success = URL
public typealias Failure = Error

private static let progressStep: Int64 = 1

/// Track task progress with internal Combine pipe
private let resultProcessed = PassthroughSubject<Success, Failure>()

/// Internal observation of the Combine progress Pipe
private var resultProcessedObserver: AnyCancellable?

/// Internal Task that wraps the combine result observation
private var computeResultTask: Task<Success, Failure>?

private let fileManager = FileManager.default

public init(from itemProvider: NSItemProvider) throws {
guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else {
throw ErrorDomain.UTINotFound
}

// Keep compiler happy
progress = Progress(totalUnitCount: 1)

super.init()

let childProgress = Progress()
progress.addChild(childProgress, withPendingUnitCount: Self.progressStep)

itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { coding, error in
defer {
childProgress.completedUnitCount += Self.progressStep
}

guard error == nil, coding != nil else {
self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.unknown))
return
}

do {
// Build dedicated storage path
@InjectService var pathProvider: AppGroupPathProvidable
let temporaryURL = pathProvider.tmpDirectoryURL
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try self.fileManager.createDirectory(at: temporaryURL, withIntermediateDirectories: true)

// Is String
if let text = coding as? String {
let targetURL = temporaryURL.appendingPathComponent("\(UUID().uuidString).txt")

try text.write(to: targetURL, atomically: true, encoding: .utf8)
self.resultProcessed.send(targetURL)
self.resultProcessed.send(completion: .finished)
}

// Is Data
else if let data = coding as? Data {
guard let uti = UTI(typeIdentifier) else {
self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotFound))
return
}

let targetURL = temporaryURL
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(for: uti)

try data.write(to: targetURL)
self.resultProcessed.send(targetURL)
self.resultProcessed.send(completion: .finished)
}

// Not supported
else {
self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotSupported))
}

} catch {
self.resultProcessed.send(completion: .failure(error))
return
}
}

/// Wrap the Combine pipe to a native Swift Async Task for convenience
computeResultTask = Task {
do {
let result: 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 result

} catch {
throw error
}
}
}

// MARK: Public

public var progress: Progress

public var result: Result<URL, Error> {
get async {
guard let computeResultTask else {
fatalError("This never should be nil")
}

return await computeResultTask.result
}
}
}

#endif
Loading

0 comments on commit 593a5fe

Please sign in to comment.