この記事ははてなエンジニア Advent Calendar 2023の2024年1月18日の記事です。
iOSアプリ開発で、ユーザーが過去に撮影した写真をアプリから使いたいケースはよくあると思います。例えばメッセージング機能を持つアプリでは必須の機能でしょう。メッセージアプリ以外にも、様々なアプリで写真ライブラリへのアクセスは必要とされます。
Appleはプライバシーを重視していて、アプリから、写真ライブラリへの無制限のアクセスは行えないようになっています。写真ライブラリへアクセスが必要なときは、以下のようなアラートでアプリごとにユーザーに許可しても良いか尋ねます。
iOS 14以降のすべてのアプリで、写真ライブラリへのパーミッションは、ユーザーが「フルアクセスを許可」と「アクセスを制限」を選べるようになりました。「フルアクセスを許可」とすると、すべての写真に対してアプリがアクセス可能です。
「アクセスを制限」を選択すると、アプリごとにユーザーが許可した写真だけがアプリから読み取り可能になります。以下のようなUIで、写真を選択し、ユーザーが選んだ写真だけ、アプリからアクセスできるようになります。iOSを普段から使っている方は一度は見たことがあるのではないでしょうか。
写真を読み込んで表示したり、送信したいだけのアプリでは、このパーミッションを回避して写真を取得できます。一番単純なパターンでは「アクセスを制限」のパターンのUIを表示し、ユーザーが選択した画像のみをアプリで利用できます。このAPIでもプライバシーは重視されていて、ユーザーが選択していない写真についてはアプリ側からは存在を知ることさえできません。
実装では、PhotosPickerを使います。
struct WholePicker: View { @State private var photosPickerItems: [PhotosPickerItem] = [] @State private var messages: [Message] = [] var body: some View { VStack { MessagesView(messages: messages) PhotosPicker("送信する画像を選択", selection: $photosPickerItems, matching: .images ).padding() } .onChange(of: photosPickerItems) { if !photosPickerItems.isEmpty { messages.append( Message(pickerItems: photosPickerItems) ) photosPickerItems.removeAll() } } } }
この方法を使うことで、パーミッションアラートでユーザーからのパーミッションを取得せずとも画像を取得できます。
ただし、この方法ではモーダルで画像を選択させなければいけないため、ユーザーの自然な行動を妨げる可能性があります。モーダルは意識を奪うものなので、例えばメッセージアプリなどの場合、テキスト入力の流れの中で直接写真を選択できる方が使い勝手が良いでしょう。実際に多くのメッセージアプリでは、ソフトウェアキーボードのあたりで写真を選択できるようなユーザーインターフェースを提供しています。
このインターフェースの提供のために、冒頭に説明した方法で写真を取得しているアプリが多く見られます。フルアクセスを許可しないと、大きくユーザービリティが落ちてしまうため、フルアクセスを許可している人も多いのではないでしょうか。
プライバシーを保ちつつ、パーミッションも必要とせず、尚且つユーザービリティも落とさない形で、このような機能を実装できるととても良いですよね。なんと、iOS 17からは三方良しのこの実装が可能になりました。
iOS 17においては、PhotosPickerに新たなスタイルが追加され、パーミッションを要求せずにプライバシーを維持したまま写真にアクセスすることが可能になりました。画面を覆うモーダル形式のpresentationに加え、inlineおよびcompactといったスタイルが導入されています。これらはアプリ内のビューとして利用可能です。このビューはアプリ本体とは別プロセスで動くため、モーダル表示のピッカーと同様、アプリ側から選択していない写真を取得することはできない仕組みになっています。
inlineでは縦スクロールのビューが、compactでは横に一列で並ぶ形のビューとして使えます。この機能を使ってメッセージアプリ風の写真送信UIを作ってみたのがこちらです。
実装には、PhotosPicker
を通常のSwiftUIのビューのように使うだけです。PickerStyleとして.compact
を指定していることと、selectionBehaviorをcontinuous
にしていることが大切です。
struct InlinePicker: View { @State private var photosPickerItems: [PhotosPickerItem] = [] @State private var messages: [Message] = [] var body: some View { VStack { MessagesView(messages: messages) HStack { if photosPickerItems.isEmpty { Text("写真を選択してください") } else { Text("\(photosPickerItems.count)個の写真を選択中です") Button("選択解除") { photosPickerItems.removeAll() } } Spacer() Button("送信") { messages.append(Message(pickerItems: photosPickerItems)) photosPickerItems.removeAll() } .disabled(photosPickerItems.isEmpty) }.padding(.horizontal) PhotosPicker("Picker", selection: $photosPickerItems, selectionBehavior: .continuous, matching: .images ) .photosPickerStyle(.compact) .photosPickerAccessoryVisibility(.hidden) .frame(height: 150) } } }
PickerStyleをcompactにすることで、横一列に画像を並べて表示されます。また、photosPickerAccessoryVisibility(.hidden)
を追加することで、画像以外の要素を非表示にすることができます。
デフォルトのPhotosPickerでは、右上に表示されている追加ボタンを押したタイミングでアプリ側が画像を取得できるようになります。しかし、photosPickerAccessoryVisibility(.hidden)
を適用すると追加ボタンが表示されません。selectionBehavior
を .continuous
にすることで、追加ボタンを押さずともアプリ側に最新の選択状態を取得できます。compact表示では、selectionBehavior
も合わせて指定しましょう。
今回作ったサンプルプロジェクトはこちらです!
はてなエンジニア Advent Calendar 2023はいつまで続くのでしょうか!明日は id:yutailang0119 です!誕生日おめでとうございます!