たごもりすメモ

コードとかその他の話とか。

iOS17/iPhone15でPhotosPickerItem#loadTransferable(type: Data.self)が動かなくなっていた

まーいけるだろ、と自作アプリが動いていたiPhone14 Pro w/ iOS16をiPhone15 Pro w/ iOS17に移行したところ、ビルドしなおしてインストールしたら普通に動いてたっぽいから油断してたら、タイトルの通りPhotosPickerを使って写真データを取り出してるところが動かなくなってた。 具体的にはこのエントリで書いてたコード。

tagomoris.hatenablog.com

これの、以下のようなコード。

fileprivate extension PhotosPickerItem {
    func pictureData() async -> Data? {
        var transferable: Data? = nil
        do {
            transferable = try await self.loadTransferable(type: Data.self)
        }
        catch {
            // log the error
            return nil
        }

これを実行するとcatch節で以下のようなエラーが報告された。iOS16 / iPhone14では起きなかったもの*1

CoreTransferable.TransferableSupportError error 0

で、これ、iPhone15Proのシミュレーターでも起きないんだよね。今のところ実機でしか再現できてない。なんなんでしょう。PhotosPickerの使いかたみたいなのをググると同じようなコードで直接Dataに変換している例が大量に見付かるんだけど、みなさんどうなってるんですかね?

解法?: Transferableな変換先を自分で実装する

よく(Appleのドキュメントでも)見るloadTransferableの変換先といえばImageなんだけど、これだとデータ本体を取り出すことができないので却下。困ったんだけど、自分でTransferableな変換先を実装するしかないかな、ということになった。Appleの以下のドキュメントにある通り。

developer.apple.com

自分の場合は対応フォーマットとしてJPEG/PNGのデータが取り出せればよかったので、それぞれJPEGの場合とPNGの場合用のTransferableなstructを作って変換を試みたところ、これでiPhone15の実機でも正常に動くようになった。コードは下に載せておく。

直接Dataを取り出すことがなぜ失敗するのかがマジで意味不明だが*2、まあ解決した上にコードも綺麗になったような気がするので良しとする。しっかし何故なんだろう、他の人のは動いてるのかな……。

import PhotosUI

fileprivate enum TransferError: Error {
    case importJpeg
    case importPng
    case emptyJpeg
    case emptyPng
}

struct PickedJpegImage: Transferable {
    let data: Data

    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(importedContentType: .image) { data in
            guard let uiImage = UIImage(data: data) else {
                throw TransferError.importJpeg
            }
            guard let data = uiImage.jpegData(compressionQuality: 1.0) else {
                throw TransferError.emptyPng
            }
            return PickedJpegImage(data: data)
        }
    }
}

struct PickedPngImage: Transferable {
    let data: Data

    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(importedContentType: .image) { data in
            guard let uiImage = UIImage(data: data) else {
                throw TransferError.importPng
            }
            guard let data = uiImage.pngData() else {
                throw TransferError.emptyPng
            }
            return PickedPngImage(data: data)
        }
    }
}


fileprivate extension PhotosPickerItem {
    func pictureData() async -> Data? {
        do {
            if self.supportedContentTypes.contains(UTType.jpeg) {
                guard let p = try await self.loadTransferable(type: PickedJpegImage.self) else {
                    // error log
                    return nil
                }
                return p.data
            } else if self.supportedContentTypes.contains(UTType.png) {
                guard let p = try await self.loadTransferable(type: PickedPngImage.self) else {
                    // error log
                    return nil
                }
                return p.data
            } else {
                // unknown type: error log
                return nil
            }
        } catch {
            // error log
        }
        return nil
    }
}

*1:iPhone14でちゃんとiOS17に上げておくべきだった……。いやちょっと他の優先順位が、ゴニョゴニョ

*2:だって独自TransferableだってDataを経由して変換してるじゃん