swiftasync-awaitavkitswift-concurrencystructured-concurrency

How do I load metadata from an AVAsset into a SwiftUI View without compiler warnings?


I am doing things, I think, by the book. However, I keep running into compiler warnings/errors.

Here's the affected code, which is in a View's .onChange(of:_:) closure, where currentMediaTitle is just a String, and player is just a AVPlayer:

currentMediaTitle = ""

Task {
    guard let asset = player.currentItem?.asset else {
        currentMediaTitle = nil
        return
    }
    
    let allMetadata = try await asset.load(.metadata) // WARNING: Non-sendable type '[AVMetadataItem]' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary
    
    guard let titleMetadata = allMetadata.first(where: { item in
        item.identifier == AVMetadataIdentifier.commonIdentifierTitle
    })
    else {
        currentMediaTitle = nil
        return
    }
    
    let titleValue = try await titleMetadata.load(.value) // WARNING: Passing argument of non-sendable type 'AVMetadataItem' outside of main actor-isolated context may introduce data races
    
    let title = (titleValue as? NSString) as String?
    
    self.currentMediaTitle = title
}

here's a screenshot version if that's better for you:

A screenshot of the above code in Xcode, with warnings in yellow GUI tags next to the lines in question

So there's a few things that... feel paradoxical to me:

  1. The old ways of using things like .metadata are deprecated in favor of this new structured concurrency .load(.metadata) method, but at the same time the value returned from this new method then also breaks the contracts of structured concurrency
  2. It seems to be treating AVAsset andor AVMetadataItem as actors for some reason, when they're both classes
  3. It says I'm passing an argument of type AVMetadataItem, but I'm calling a method on it. Maybe I can understand how it thinks this because Swift seems to treat methods as static functions which take their self as their first argument, but even then I don't see how this could possibly break structured concurrency since it's self-scoped

I even tried a few things to get this all to happen on the MainActor but it still gave me the exact same warnings so like.. I'm kinda stuck here.

What do I do to resolve these warnings?


Solution

  • The code in the Task { ... } is main-actor isolated, but load is not, so non-sendable types like AVMetadataItem cannot be passed between them, crossing actor boundaries.

    You can use Task.detached to run the code off of the main actor, but this will still capture self (assuming currentMediaTitle is a member of self), which is probably non-sendable. You still get a warning if you turn on strict concurrency checking.

    It seems like you are fetching the title of some AVAsset, so I would write this as an extension on AVAsset:

    extension AVAsset {
        func mediaTitle() async throws -> String? {
            let allMetadata = try await load(.metadata)
            guard let titleMetadata = allMetadata.first(where: { item in
                item.identifier == AVMetadataIdentifier.commonIdentifierTitle
            })
            else {
                return nil
            }
            let titleValue = try await titleMetadata.load(.value)
            let title = (titleValue as? NSString) as String?
            return title
        }
    }
    

    and now you can do:

    Task {
        currentMediaTitle = try await player.currentItem?.asset.mediaTitle()
    }
    

    This again sends an AVAsset across actor boundaries, and you will get a warning if you turn on strict concurrency checking. You can silence this warning using @preconcurrency import AVFoundation, and this will probably not be an issue after region-based actor isolation is implemented.