I am working on an app that uses modern TCA (ReducerProtocol and async code). In one of my action handlers in reducer, I have this:
return .run { send in
let result = await environment.mediaManager.storePhotoOrVideo(
fromItemProvider: itemProvider,
mediaSessionId: mediaSessionId,
configuration: .default
)
await send(.mediaManagerStoredPhoto(result))
}
This is nice and expressive and shows exactly what I want to do. Return type of the environment object function call is Result<URL, MediaManagerError>. I receive itemProvider and process it, receiving the result, and pass it on to another action.
Here is the problem. NSItemProvider is an old type that is not Sendable, and most likely will never be. So I get this warning. Which will be an error further down the road, as Swift keeps strengthening Sendable conformance checking.
Is it the answer though? NSItemProvider is a class, so all ActorIsolated does is just protects access to the NSItemProvider instance itself. It doesn't protect from any potential asynchronous internal changes inside of NSItemProvider. In other words, it doesn't actually make it Sendable.
It silences the warning, but is it not the same as
I think you're correct, but in some cases there may not be much of an alternative, as the internals of NSItemProvider may not be able to be affected by the end user, and some APIs require its usage. That said, if there's a way to not use NSItemProvider and instead use simpler, natively-Sendable data types, that would probably be safer.
Now that I look at ActorIsolated I'm realizing that value should not be public. You should be forced to go through withValue to access the underlying value. That's how LockIsolated was designed, so I think it was just an oversight for ActorIsolated.
It really does look like there is no good solution to use non-Sendable types in TCA reducers, and what I should do is isolate NSItemProvider handling to an outside service, and interact with it through dependencies (formely known as environment). I can still manage the control flow with the reducer, but I cannot cleanly move the actual NSItemProvider objects (or any other non-Sendable types) through it.
Fair enough. I was able to create a workaround (basically creating a Sendable wrapper which clones the original). I'll see if I can dig up the code for posterity.
Hi David, I am currently dealing with the whole NSItemProvider / Sendable issue. Were you able to find your workaround code?. Very interested to see how you implemented this.
Sure thing. This is the implementation in using, heavily based on a solution given to me in the Slack group.
import Foundation
import FCPXMLDocument
import UniformTypeIdentifiers
/// NSItemProvider is not Sendable, so we need to wrap it in a Sendable type.
public struct ItemProvider: @unchecked Sendable, Equatable {
public var value: NSItemProvider { _value.clone() }
private var _value: NSItemProvider
/// Initialises an ``ItemProvider`` based on a provided `NSItemProvider`.
///
/// - Parameter value: the original provider.
public init(value: NSItemProvider) {
self._value = value.clone()
}
}
extension NSItemProvider {
/// Clones itself and returns the result.
///
/// - Returns the clone.
fileprivate func clone() -> NSItemProvider {
// swiftlint:disable:next force_cast
copy() as! NSItemProvider
}
}
extension ItemProvider {
/// Checks if there is an item confirming to
/// - Parameter type: The type to search for.
/// - Returns: `true` if one is available.
func hasCompatibleContentType(to type: UTType) -> Bool {
findCompatibleContentType(to: type) != nil
}
/// Finds the first matching compatible content type, if available. If it's a perfect match, it is returned.
/// Othewise, if it's a dynamic type, it will match if the `preferredFilenameExtension` is set and matches.
///
/// - Parameter type: The type to search for.
/// - Returns: The matching type, if found, or `nil`.
public func findCompatibleContentType(to type: UTType) -> UTType? {
return value.registeredContentTypes.first(where: type.isCompatible(with:))
}
/// Asynchronously loads and returns the URL for an item conforming to the specified `UTType`.
/// - Parameter type: The `UTType` to which the item is expected to conform.
/// - Returns: The `URL` of the item if it exists and conforms to the specified `UTType`, or `nil` if not.
public func loadFileURL(forType type: UTType) async throws -> URL? {
guard let registeredType = findCompatibleContentType(to: type) else { return nil }
return try await withCheckedThrowingContinuation { continuation in
value.loadItem(forTypeIdentifier: registeredType.identifier, options: nil) { (item, error) in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let url = item as? URL else {
continuation.resume(returning: nil)
return
}
// If the URL is a security-scoped resource, make sure to handle it appropriately outside of this function.
continuation.resume(returning: url)
}
}
}
}