たごもりすメモ

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

SwiftUI PhotosPickerで選択した項目からJPEG/PNGを取り出す

SwiftUIで作るiOSアプリで、画像を選択し、その画像をどこかにアップロードしたい。アップロード先がHEICに対応してないのでJPEG/PNGあたりのフォーマットでやりたい。

これを考えたとき、iOS 16.0+ ならPhotosPickerが使える。が、実際に選択したあとでどうやってJPEG/PNGのデータを取り出すかにだいぶ悩んだのでメモっておく。他にもっとマトモな方法ないの? と思っている。

PhotosPickerを使う

これは既存のビューでボタンを押すとシートで下から出てくる感じにしたかったので、こんなコードをざっと書けばよい。これは1枚だけ選ばせたい場合で、複数枚選ばせたい場合は呼び出しのシグネチャが少し変わるのに注意。

@State var isPickingPhoto: Bool = false
@State var selectedPhotoItem: PhotosPickerItem? = nil

var body: some View {
    AnyView()
    .sheet(isPresented: $isPickingPhoto) {
        PhotosPicker(
            selection: $selectedPhotoItem,
            matching: .images,
            preferredItemEncoding: .current,
            label: { Text("Choose your profile image") }
        )
        .onChange(of: selectedPhotoItem) { photoItem in
            guard let photoItem else {
                return
            }
            uploadSelectedPhoto(photoItem)
        }
        .presentationDetents([.height(280)])
        .presentationDragIndicator(.visible)
    }

んで実際のデータ取り出しおよびアップロードはuploadSelectedPhoto()で行う。

PhotosPickerItemからデータを取り出す

実際にデータを取り出すとき、JPEGPNGのどちらかが欲しいとする。しかしiPhoneのカメラで撮影した画像はHEICがプライマリの画像形式になっている(ことが多い)ため、JPEGPNGが欲しい、と指定する必要がある。選択した写真が対応している形式はsupportedContentTypesで取得できる、が、これはUTTypeを返す。

photoItem.supportedContentTypes //=> [UTType.heic, UTType.jpeg]

ところで、PhotosPickerItemから実際に扱うデータの取り出しはloadTransferable(type:)関数で行うが、ここでtypeに指定するのはSwiftの型であってUTTypeでもMIME Typeでもない。

let transferable = try await photoItem.loadTransferable(type: Data.self)

えー、このDataの中身はなんなんだよ、対応しているUTTypeの形式で取り出させてくれよ、と思うが、そのようなAPIがなさそうに見える。本当に?

で、現状どうしているかというと、しょうがないからいちどUIImageを経由している。これ、画像変換が行われちゃってないかなあ、大丈夫? と思うものの、他に方法が見付かっていない。

// JPEGの場合
let data = UIImage(data: transferable)?.jpegData(compressionQuality: 1.0)
// PNGの場合
let data = UIImage(data: transferable)?.pngData()

このへんをまとめて、必要な場所で次のようなextensionを書いておいた。Swiftのfileprivate便利だよね。

typealias UploadPictureData = (data: Data, mimeType: String)

let PERMITTED_IMAGE_TYPES = [
    UTType.jpeg,
    UTType.png,
]

fileprivate extension PhotosPickerItem {
    func pictureData() async -> UploadPictureData? {
        var transferable: Data? = nil
        do {
            transferable = try await self.loadTransferable(type: Data.self)
        }
        catch {
            // logging
            return nil
        }
        guard let transferable else {
            // logging
            return nil
        }
        var data: Data? = nil
        var mimeType: String? = nil
        if self.supportedContentTypes.contains(UTType.jpeg) {
            data = UIImage(data: transferable)?.jpegData(compressionQuality: 1.0)
            mimeType = UTType.jpeg.preferredMIMEType
        } else if self.supportedContentTypes.contains(UTType.png) {
            data = UIImage(data: transferable)?.pngData()
            mimeType = UTType.png.preferredMIMEType
        } else {
            // logging
            return nil
        }
        guard let data, let mimeType else {
            // logging
            return nil
        }
        return (data, mimeType)
    }
}

もうちょっといいやりかた無いもんかなあ。