MPMediaItemPropertyArtwork crashes on Swift 6

Hey all!

in my personal quest to make future proof apps moving to Swift 6, one of my app has a problem when setting an artwork image in MPNowPlayingInfoCenter

Here's what I'm using to set the metadata

func setMetadata(title: String? = nil, artist: String? = nil, artwork: String? = nil) async throws {
        let defaultArtwork = UIImage(named: "logo")!
        
        var nowPlayingInfo = [
            MPMediaItemPropertyTitle: title ?? "***",
            MPMediaItemPropertyArtist: artist ?? "***",
            MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: defaultArtwork.size) { _ in
                defaultArtwork
            }
        ] as [String: Any]
        
        if let artwork = artwork {
            guard let url = URL(string: artwork) else { return }
            
            let (data, response) = try await URLSession.shared.data(from: url)
            
            guard (response as? HTTPURLResponse)?.statusCode == 200 else { return }
            
            guard let image = UIImage(data: data) else { return }
            
            nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in
                image
            }
        }
        
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

the app crashes when hitting

MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: defaultArtwork.size) { _ in
     defaultArtwork
}

or

 nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in
    image
}

commenting out these two make the app work again.

Again, no clue on why.

Thanks in advance

Answered by DTS Engineer in 806545022

I was gonna say that this looks remarkably like an issue I’ve been working on a different thread, but then I noticed that you’re the OP on that thread too.

So, yeah, this is a similar issue and, as I mentioned over there, I’m still not 100% sure I fully understand all of these cases. However, I do have a suggestion for you.

Consider this snippet:

@MainActor class AudioPlayerProvider: ObservableObject {

    func setMetadataQ() {
        let defaultArtwork = UIImage(systemName: "fireworks")!
        let nowPlayingInfo = [
            MPMediaItemPropertyTitle: "Hello",
            MPMediaItemPropertyArtist: "Cruel World!",
            MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: defaultArtwork.size) { _ in
                defaultArtwork
            },
        ] as [String: Any]
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
}

@MainActor
func test() {
    let o = AudioPlayerProvider()
    o.setMetadataQ()
}

I put this into a small test project, built it with Xcode 16.0 in Swift 6 mode, and ran on an iOS 18.0 device. It crashed exactly like your code. I resolved the crash by changing { _ in to { @Sendable _ in.

In this case the @Sendable really does make sense. MPMediaItemArtwork holds on to the closure you pass in and it calls in that ‘promise’ on an arbitrary thread. Thus, the closure has to be sendable. The only annoying thing is that the compiler doesn’t tell you that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Here' also the crash report

https://drive.google.com/file/d/1knyD5oMX46gnsxNY3RjFWH5iuXkeRlsg/view?usp=sharing

Thanks for the crash report.

Consider the crashing thread backtrace:

Thread 1 Crashed::  Dispatch queue: */accessQueue
0 libdispatch.dylib          … _dispatch_assert_queue_fail + 116
1 libdispatch.dylib          … dispatch_assert_queue + 188
2 libswift_Concurrency.dylib … swift_task_isCurrentExecutorImpl(swift::SerialExecutorRef) + 284
3 RadioPNR.debug.dylib       … closure #1 in AudioPlayerProvider.setMetadata(title:artist:artwork:) + 100
4 RadioPNR.debug.dylib       … thunk for @escaping @callee_guaranteed (@unowned CGSize) -> (@owned UIImage) + 80
5 MediaPlayer                … -[MPMediaItemArtwork jpegDataWithSize:] + 16

Frame 5 a routine internal to the Media Player framework. I believe it’s part of the request handler chain for the init(boundsSize:requestHandler:) initialiser that you’re calling. It’s called your code in frame 4. Well, not your code, but code inserted by the Swift compiler. That’s checking that the code is running on the queue that it expects to be running on. It’s discovered that it isn’t, and thus trapped.

Is your setMetadata(…) isolated to the main actor?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello Quinn!

setMetadata(...) is set inside the

@MainActor class AudioPlayerProvider: ObservableObject

along AVPlayer and some @Published properties.

The function itself can be called inside the action of a SwiftUI button with:

 Task {
     try? await self.audioPlayerProvider.setMetadata(title: ..., artist: ..., artwork: ...)
}

or inside a .onChange event triggered by a metadata ObservableObject class, when needed.

So always on the thread the enclosing view runs.

Thanks again

Accepted Answer

I was gonna say that this looks remarkably like an issue I’ve been working on a different thread, but then I noticed that you’re the OP on that thread too.

So, yeah, this is a similar issue and, as I mentioned over there, I’m still not 100% sure I fully understand all of these cases. However, I do have a suggestion for you.

Consider this snippet:

@MainActor class AudioPlayerProvider: ObservableObject {

    func setMetadataQ() {
        let defaultArtwork = UIImage(systemName: "fireworks")!
        let nowPlayingInfo = [
            MPMediaItemPropertyTitle: "Hello",
            MPMediaItemPropertyArtist: "Cruel World!",
            MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: defaultArtwork.size) { _ in
                defaultArtwork
            },
        ] as [String: Any]
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
}

@MainActor
func test() {
    let o = AudioPlayerProvider()
    o.setMetadataQ()
}

I put this into a small test project, built it with Xcode 16.0 in Swift 6 mode, and ran on an iOS 18.0 device. It crashed exactly like your code. I resolved the crash by changing { _ in to { @Sendable _ in.

In this case the @Sendable really does make sense. MPMediaItemArtwork holds on to the closure you pass in and it calls in that ‘promise’ on an arbitrary thread. Thus, the closure has to be sendable. The only annoying thing is that the compiler doesn’t tell you that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

After you explained it, it makes sense. Thanks Quinn!

Wow, this was as simple as adding a @Sendable keyword!

I posted about this last month here: https://forums.developer.apple.com/forums/thread/763790

I also submitted a bug report #FB15145734 as I assumed the block was being run on the incorrect thread which was out of our control.

Happy to have this working now, but I do wonder is this still an issue with the function, or simply a Swift 6 compiler issue?

MPMediaItemPropertyArtwork crashes on Swift 6
 
 
Q