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からデータを取り出す
実際にデータを取り出すとき、JPEGかPNGのどちらかが欲しいとする。しかしiPhoneのカメラで撮影した画像はHEICがプライマリの画像形式になっている(ことが多い)ため、JPEGやPNGが欲しい、と指定する必要がある。選択した写真が対応している形式は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) } }
もうちょっといいやりかた無いもんかなあ。