🌄

iOSの写真選択画面の変遷から見るアップデートの大切さ

2024/10/17に公開

アルダグラムで、モバイルアプリを開発している長尾です。

ところで、スマホアプリで、写真選択画面ってあるじゃないですか。iOSの写真選択UI
あれ。またの名をそれ。(多分違う)

あれです。あれ。そうそれ。それです。

実は、iOSにおいては、写真選択画面は、同じUIでも、使えるClassが複数存在しています。

本稿は、使えるClassが複数あるだけならまだしも、、という与太話です。

写真選択画面を構成する3つのClass

2024/10/17現在、iOSにおいて、写真選択画面を構成できるClassは、3つ存在しています。

PhotosPickerは、iOS16からサポートされているswiftUIの写真選択画面です。

PHPickerControllerは、iOS14からサポートされているUIViewControllerベースの写真選択画面です。swiftUIで画面を作成するには、UIViewControllerRepresentableを利用するのが良いでしょう。

そして最後にUIImagePickerController。こちらはなんとiOS2からサポートされている写真選択画面です。なんとまだ(完全には)deprecatedになっていません。驚きです。

これらをswiftUIを用いて画面に組み込んだサンプルをご紹介します。

まずは、PhotosPickerから。


import SwiftUI
import PhotosUI

struct PhotoPickerView: View {
    @State private(set) var photoPickerItems: [PhotosPickerItem] = []
    @State private(set) var selectedImage: Image? = nil
  
    var body: some View {
        PhotosPicker(selection: $photoPickerItems,
            maxSelectionCount: 0,
            selectionBehavior: .continuousAndOrdered,
            matching: .images,
            preferredItemEncoding: .automatic,
            photoLibrary: .shared()) {
            VStack {
                selectedImage?.resizable().scaledToFit()
                Text("Photo Picker")
            }
        }
        .onChange(of: photoPickerItems) { _, newValue in
            guard let first = newValue.first else { return }
            loadTransferable(from: first)
        }
    }

    private func loadTransferable(from imageSelection: PhotosPickerItem) {
        imageSelection.loadTransferable(type: Data.self) { result in
            Task { @MainActor in
                switch result {
                case .success(let data):
                    guard let data else { return }
                    guard let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return }
                    guard let properties = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nil) else { return }
                    print(properties)
          
                    let uiImage = UIImage(data: data)
                    let image = Image(uiImage: uiImage)
                    self.selectedImage = image
                case .failure:
                    print("handle error")
                }
            }
        }
    }
}

iOS16というswiftUIがこなれてきた時代にリリースされたClassだけあって、インターフェースもswiftUIで使いやすいものになっています。

続いて、PHPickerControllerのサンプルコード。

import SwiftUI
import PhotosUI

struct PhPhotoPickerView: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    @Binding var seledtedImage: Image?
  
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
        configuration.filter = .images
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }
  
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        // do nothing.
    }
  
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
  
    class Coordinator: PHPickerViewControllerDelegate {
        private let parent: PhPhotoPickerView
      
        init(_ parent: PhPhotoPickerView) {
            self.parent = parent
        }
      
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            parent.isPresented = false
      
            let itemProvider = results.first?.itemProvider
            itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in
                if let url {
                    let rawData = try Data(contentsOf: url)
                    guard let cgImageSource = CGImageSourceCreateWithData(rawData as CFData, nil) else { return }
                    guard let properties = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nil) else { return }
                    print(properties)
  
                    guard let uiImage = UIImage(data: rawData) else { return }
                    self.parent.seledtedImage = Image(uiImage: uiImage)
                }
            }
        }
    }
}

UIViewContorollerRepresentableを利用してswiftUIに組み込んでいますが、PhotosPickerと比較するとどうしてもコード量が増えたり、直感的ではなかったりします。

最後に、UIImagePickerControllerを使ったサンプルもご紹介します。

import SwiftUI
import UIKit
import Photos

struct UIPickerView: View {
    @State var isPresented: Bool = false
    @State var selectedImage: Image? = nil
    
    var body: some View {
        VStack {
            selectedImage?.resizable().scaledToFit()
            Button {
                isPresented.toggle()
            } label: {
                Text("Photo Picker")
            }
        }
        .fullScreenCover(isPresented: $isPresented, onDismiss: nil) {
            UIImagePickerView(selectedImage: $selectedImage, isPresented: $isPresented)
        }
    }
}

@MainActor
struct UIImagePickerView: UIViewControllerRepresentable {
    @Binding var selectedImage: Image?
    @Binding var isPresented: Bool
    
    func makeCoordinator() -> UIImagePickerViewCoordinator {
        UIImagePickerViewCoordinator(parent: self)
    }
    
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let pickerController = UIImagePickerController()
        pickerController.sourceType = .photoLibrary
        pickerController.delegate = context.coordinator
        return pickerController
    }
    
    func updateUIViewController(_ uiView: UIImagePickerController, context: Context) {
        // nothing
    }
  
    class UIImagePickerViewCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: UIImagePickerView
        
        init(parent: UIImagePickerView) {
            self.parent = parent
        }
    
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            guard let url = info[UIImagePickerController.InfoKey.imageURL] as? URL else { return }
            let rawData = try! Data(contentsOf: url)
            guard let cgImageSource = CGImageSourceCreateWithData(rawData as CFData, nil) else { return }
            guard let properties = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nil) else { return }
            print(properties)

            guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
            parent.selectedImage = Image(uiImage: image)
            parent.isPresented = false
        }
    
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.isPresented = false
        }
    }
}

こちらも、UIViewControllerRepresentableを利用してswiftUIの画面を構成するので、どうしても記述量が増えてしまいます。

どれを使うべきか?

写真選択画面を実装する際、同じ機能のClassが3つも存在していて、どれを利用すれば良いのか?

悩みますよね?いや悩まないですか??

私は色々悩みます。

私が、どのClassを利用するかを判断する際に、まず考えることは、そのClassがアプリで利用できるかどうか?です。

現在、弊社がリリースしているKANNAアプリのサポートバージョンは、iOS15以上としています。

それぞれのClassのサポートは

  • UIImagePickerController: iOS2〜
  • PHPickerController: iOS14〜
  • PhotosPicker: iOS16〜

となっているので、サポートしているバージョンだけを考慮すると、PhotosPicker以外は、利用できそうです。

PhotosPickerを利用すると、他のClassと併用する必要があるOSバージョンが存在するため、OSバージョンによって処理を分岐させる必要があり、コードが煩雑になることを考えると、積極的には導入しづらいかなと考えます。早めに導入しておいて、deprecatedに備えるという判断も場合によってはできると思います。

古いClassを利用し続けること

古いClassなら、サポートバージョンも広いし、一度書いて終わりになるので、書き直すのも面倒だし、ずっと利用し続ければいいじゃないですか、という話かというと、そうは問屋がおろしません。[1]

諸般の事情で、写真を選択した際に、画像情報に加えて、EXIF情報も利用することになりました。EXIFには、撮影日時や、場合によっては撮影場所など様々な情報が記録されています。画像とともにEXIF情報も利用されているアプリも多くあるでしょう。

今回のサンプルコードは、EXIF情報を利用するコードを検証するために書いたものでした。

そこで私は驚きました。

なんと、UIImagePickerControllerでは、EXIF情報の全てを取得することができませんでした。。

ネット上では、UIImagePickerControllerを利用して、写真情報と、EXIF情報を取り出しているサンプルコードがいくつも見つかります。

なのですがその通りに書いてみても、なぜかEXIF情報の一部が取れませんでした。

サンプルコードということもあり[2]、ざっくりとしか確認していないので、設定ミス等の考慮もれがある可能性は排除しきれませんが[3]、同一プロジェクト上に組み込んだ、PHPickerControllerやPhotosPickerからはEXIF情報は取り出せているのに、UIImagePickerControllerからは取り出せませんでした。

過去にはUIImagePickerControllerからもEXIF情報は取り出せていたはずです。

しかし今現在、UIImagePickerControllerから、EXIF情報が取り出せない。

使い方が間違っているのかなーと思い、コードの修正点がないか、よく見ていると、、
Xcodeがコメントを

何やらXcodeがコメントを。。

UIImagePickerController.SourceTypeの定義を見にいくと、、、
UIImagePickerController.SourceType

will be removed in a future release, use PHPicker.

「今は」消えてないけど、将来的には消えるので、PHPickerを使えって書かれている?

実はこのコメント、UIImagePickerControllerをカメラ設定とするもの(case camera = 1)には付けられておらず、写真を選択する設定の項目にのみ付けられています。

UIImagePickerControllerは、端末にある写真を選択できたり、カメラを起動して、その場で撮った写真も選択できるようなClassです。それに対して、PHPickerControllerや、PhotosPickerには写真を撮影して選択した写真とする機能はありません。

UIImagePickerControllerは、まだ明確にdeprecatedとなったわけではないのですが、deprecatedとなっていないのは、写真撮影機能の箇所が存在しているからで[4][5]、こと写真選択画面の機能においては、Appleがdeprecatedとしていきたいようです。

皆さんもご経験があると思いますが、新しく書き起こしたモジュールが存在するような古いモジュール、互換性の問題で、即座には削除できないけれども、動かしておかなくてはいけない、、できればメンテナンスしたくないモジュール、ありますよね。

コードの互換性を保ちつつ機能開発を続けていくことはとても大変です。

deprecatedとしたい意思が見えるClassの振る舞いが、開発者も意図せず、いつの間にか変更になってしまっていてもおかしくはありません[6]

もし、UIImagePickerControlleを利用して、EXIFを利用するようなコードがプロダクションのコードで動いていたら、昔は正常に動いていたのに、コードを変更していないにもかかわらず、いつの間にか不具合が発生するようになった、ということが起こってしまったかもしれません。

結び

古いClass全てがUIImagePickerControllerのように振る舞いが変わっていく、ということではありません。昔から存在しているClassで、振る舞いがずっと変わっていないClassもたくさんあります。

それでも、自身が書いたコードだけでプロダクトが動いているわけではないのです。

自分が書いていないコードが変更されることで[7]、過去と同じ振る舞いではなくなることは、往々にしてあり、その結果、悲しいことに、自身で書いたコードは何もせずとも壊れていきます。
「何もしてないのに壊れた」のではなく、「何もしてないから壊れる」のです[8]

そのため、プロダクトを正常な動作をさせ続けるために、何かをしていく、今回の場合であれば、Classは更新していく必要があるというのが私の考えです。

同一機能のClassが存在するのであれば、新しいClassを利用していくことがベターな判断になるのではないかと思います。ある程度stableになった状態で新しいclassを利用すると、プロダクトの振る舞いも安定するでしょう。新機能をいち早く導入するためには、リリースされたばかりのClassを安定性を度外視してでも導入してみることも必要になるかと思います。

ただ、既存機能を新しいClassへリプレイスするには、工数が足りない、といったケースや、コードを修正することによって不具合を埋め込んでしまうリスクも存在していると思います。

なので、状況により判断は分かれるでしょう、ということになるのですが、昔のままで良い、ということはなく、それをいつ行うか?ということなんだろうと思います。世界はそれを技術負債と呼ぶんだぜ[9][10]

弊社でも、Frameworkを長い間更新できなかったがために苦労したお話があります[11]が、日々の業務に少しづつメンテナンスを行なっていくことで、メンテナンスの苦労は軽減されるはずです。

ということで、技術負債、少しづつ返済して、利息に押しつぶされないようにやっていきまっしょい。

脚注
  1. この慣用句初めて使った気がする ↩︎

  2. エラーハンドリングや状態管理もだいぶ甘めです ↩︎

  3. 新しいClassは必要な設定項目がわかりやすくなっていて、意図せず全て設定できているが、古いClassだと設定項目がわかりにくくなっていて、全ての設定を満たしきれていないなど ↩︎

  4. その場で撮った写真データを簡単に利用できるようなClassは、現状UIImagePickerController以外に存在しないため ↩︎

  5. カメラを使う機能は、AVFoundationを利用すればカスタム実装することができます ↩︎

  6. しつこく言いますが、私が単純にミスをしている可能性も十分あります ↩︎

  7. OSやライブラリがアップデートされていくことで、自分が書いていないコードの変更が自身が書いたコードのモジュールに影響を与えることは十分に考えられます ↩︎

  8. これは、t-wadaさんのお言葉です ↩︎

  9. 言いたかっただけです。サンボマスター。 ↩︎

  10. https://open.spotify.com/intl-ja/track/3DqDcoihfqU7b9kBh9PqXj ↩︎

  11. https://zenn.dev/aldagram_tech/articles/62e648459c505e ↩︎

アルダグラム Tech Blog

Discussion