見出し画像

「写真を取り込む」徹底解説

割引あり

Playgrounds 4.1 で新しい App教材 が追加になりました。
今回は「写真を取り込む」を解説します。
シンプルですがフロント・バックカメラ切り替えや写真ライブラリー表示、お気に入り設定や写真の削除機能が使える完全なカメラアプリです。
Playgrounds の教材としては最もむずかしいもののひとつです。

この記事はこれらをじっくり解説しています。
初学者を混乱させる部分には「‼️」マークを付けました(検索してください)。

【2023年4月:概要ページの情報を更新しました】
【2022年9月9日:一部加筆しました】

この記事は『Playgrounds 4.1徹底解説』マガジンで読むことができます。
ほかの追加になった App教材の解説はすでに公開しています。



・画像クリックで拡大表示できます
・画像を拡大表示中は画像の左右をクリックで画像だけを順に表示できます
・ソースコード部分は左右にスクロールできます
・リンクしているドキュメントは英文が多いですが、翻訳機能を活用してください
・この記事は Mac用 Playgrounds と iPad用 Playgrounds 共通です

画面はライトモードの iPad を使い、操作説明も「タップ」と書いています、Macでは「クリック」に読み替えてください。

WWDC のセッションビデオは日本語字幕が付いています。



🟢 写真を取り込む

サブタイトルは「カメラと写真ライブラリへのアクセス」です。

中級から上級のプロジェクトと明記されています。

300行を超えるソースコードと200行を超えるソースコードがあります。
内容を理解するには中級以上のスキルが必要ですが、アプリとしてはダウンロードするだけで実行可能です。

教材の説明は日本語ですが、すべての説明があるわけではありません。
フロントとバックのカメラ切り替えやお気に入りの設定・表示、写真の削除などは実装されていますが詳しい説明は教材にはありません
この記事の後半に教材で説明されていないソースコードと、「フロントカメラに切り替え」「お気に入りと写真の削除」を解説しています。

カメラのほかにグリッドでフォトギャラリーの表示もおこなうアプリです。
少なくとも「グリッドを使った整理」と「イメージギャラリー」をやった後に取り組むのがおすすめです。

「写真を取り込む」のダウンロード画面


概要

Playgrounds 4.1 で
に追加された App教材です。

バージョン 1.0.0 
リリース:2021年12月15日 2022年5月17日
Swift5.5 5.8版
プロジェクトの容量は5.6 6MBです。
【2023年4月現在、バージョン番号は同じですが3点変更されていました】

バージョン番号とリリース日は Playgrounds 4.0 で利用可能だった App教材と同じです。
Apple社としては最初から提供予定だったが 4.0 には間に合わなかったのかも知れません。
実際にダウンロード可能になったのは Playgrounds 4 から 4.1 へアップデートのタイミングです。


カメラを利用します

この教材は実行するデバイスのカメラを利用します。
このためデバイスにより操作可能な機能に差が出ます。
iPhone や iPad ではバックカメラとフロントカメラを切り替えて利用できます。


🟢 基本の予習

使用するフレームワークが実際のカメラアプリと同じなので、このApp教材は中級以上のスキルが必要になります。
翻訳機能を活用して予習するために関連するページURLを示します。

各ページは情報量が多いですが、すべてを理解する必要はないので心配しないでください。
多機能を区別するために型やメソッド名なども長いものが多いです。
まずは詳しく知りたくなった時のスタート地点として押さえてもらえたらと思います。

AVFoundationフレームワーク

https://developer.apple.com/documentation/avfoundation

AVFoundationは、Appleプラットフォームで視聴覚メディアを検査、再生、キャプチャ、処理するための幅広いタスクを含むいくつかの主要な技術分野を組み合わせています。


iOS や macOS では文字や写真だけでなく音声や動画も扱えます。
これらオーディオ(A)とビデオ(V)関連の基本的な部分を扱うフレームワークが AVFoundation です。

多機能で上記ページからもたくさんのドキュメントにリンクしています。
さらに詳しく知りたくなってから参照してください。

PhotoKitフレームワーク

https://developer.apple.com/documentation/photokit
iCloud写真や Live Photos など、写真アプリが管理する画像とビデオのアセットを操作します。

iOS と macOS では、PhotoKit は写真アプリの写真編集拡張機能の構築をサポートするクラスを提供します。
iOS、macOS、tvOS では、PhotoKit は写真アプリが管理する写真やビデオのアセットに直接アクセスできます。

import Photos で利用します。

os.log

OS機能で実行中の記録(ログ)出力を組み込み、後から参照しアプリの動作解析やバグの原因調査のためにも利用可能です。
WWDC20のビデオ「Swiftにおけるロギング」を参照してください。
https://developer.apple.com/wwdc20/10168
個人情報はハッシュ値を使うなどの詳しい説明があります。

import は「os」でも大丈夫ですが、このApp教材では import os.log で使われています。
実用的アプリでは内容も複雑になるのでログの活用は基本テクニックです。

Core Image

https://developer.apple.com/documentation/coreimage

「Camera」で利用している「CIImage型」のために import しています。

CIImage は CoreImage フレームワークの画像データのための型です。

通常は SwiftUI や UIKit の機能で十分ですが、Core Image を使うと多様で細かな処理を高速に実行できます。

非同期処理

通常アプリはユーザー操作を処理し続けています。
実感がないかもしれませんが何もしていない状態でも、いつでもタッチなどを処理するため操作などを監視し続ける処理をしています。

アプリで通信データを送受信したり、カメラで撮影した画像の保存などの処理は短時間であったとしても、時間がかかります。
データ受信中などに操作ができないと操作性の悪いアプリになります。
(例えばスクロールが引っかかるような影響が出ます)

このような問題に対して、非同期処理は有効な手法です。
非同期処理は複数の処理を、見た目はは同時に実行可能にするためのしくみです。
実際には非常に短時間で処理を切り替えながらどちらも処理することで実現しています。

また時間切れや保存失敗などのエラー対応などの対策がいろいろ必要になります。
開発中もどこまでが正常でどこに問題があるかを見極めるのもスキルが必要です。
このため同期処理よりも非同期処理プログラミングは難しいです。

非同期処理で使い勝手は向上しますが、コードは複雑になります
非同期関連ではそのため開発を支援する機能やキーワードがたくさんあります
キーワードも Swift言語のものと、フレームワークを使うものなどいろいろです。

WWDC21の『Swiftの新機能』ビデオが参考になります。(日本語字幕付き)
非同期と並行プログラミング関連はここから(21:00付近)
https://developer.apple.com/wwdc21/10192?time=1254

さらに深く知るための別のセッションも複数紹介されています。


アクセス許可

写真などの個人情報にアクセスするアプリは、利用者に許可を取らなければなりません。
iOS や macOS アプリでは許可を取らずに個人情報にアクセスするコードを実行することはできません。

写真アクセスの App教材を実行すると Playgrounds 4.x でも許可を求めます。

Playgrounds では Appプロジェクトを開くと次のような確認が求められます。(アプリ実行時と同様の確認が必要です)

今回はカメラと写真データそれぞれで許可を求めています。

カメラへのアクセス許可表示
写真データへのアクセス許可表示


アプリ実行時
保存したアプリを実行する場合にも同じく許可を求めます。

カメラへのアクセス許可表示
写真データへのアクセス許可表示


App Store提出用アプリに表示する説明文は App設定 > 機能 で設定します。
Playgrounds で実行する場合の説明文は固定です。


🟢 全体構成

ガイドの構成

アプリの解説が中心ですが、各タスクグループ最後のタスクはアプリに変更を加えるチュートリアルになっています(コードを学ぼうのBluのアイコンです)。

アプリ機能は完成した状態です。
ダウンロードしてすぐに実行しても機能します。

コードが複雑で行数が多いこともあり、同じコードを別の角度から何度も解説しているものがあります。

Playgrounds で開いた時の最初の表示
ガイド画面(縦にスクロールしたものを合成)
ガイド画面(縦にスクロールしたものを合成 その2)


カメラ出力をプレビューする
カメラを使う
プレビューストリームを探求する
プレビューストリームを処理する
プレビューストリームを表示する
ファインダーを回転する/ぼかす

写真を取り込んで保存する
シャッターボタンに応答する
写真を取り込む
写真を処理して保存する
少し遅れて作動するシャッターを追加する

写真をブラウズする
フォトコレクションを使う
フォト素材を取得する
フォトギャラリーに移動する
フォトギャラリーを作成する
写真を表示する
固定グリッドを使う


プロジェクトの構成

プロジェクト内の全14コードのうち8コードはガイドでの説明がありません。
ガイドに説明のないコードの解説は『解説なしのコード』に書きました。

ソースコードの構成


ここからタスクのステップ別説明です。


🟢 カメラ出力をプレビューする


タスク:カメラを使う

ソースコードは「DataModel」です。

ステップ 1/3

camera プロパティはこの App教材内の Camera 型インスタンスです。

このように Appプレビュー にカメラ画像を表示します。

プレビューに表示されれるライブ画像

※ほかの画面キャプチャーは iPad を机に置いた状態のためプレビューは黒くなっています。

ステップ 2/3

リンクしているドキュメントの「Cameras and Media Capture」は 2022年7月のwebドキュメントでは
Capture Setup」などに分かれています。

Photo Capture

Audio and Video Capture

Additional Data Capture

ステップ 3/3

‼️「await model.camera.start()」には「コピー」ボタンがありますが、ペースト先が書かれていません。
(組み込み済みなのでペーストの必要はありません。)

検索すると「CameraView」で使われています。


タスク:プレビューストリームを探求する

ソースコードは「Camera」です。

ステップ 1/2

ここで『カメラを使う』ステップ 3/3の「start()」メソッドを表示します。

「async」はSwift言語のキーワードです。

checkAuthorization() は「Camera」ソースコード内にあるプライベートな関数です。


ステップ 2/2

非同期ストリーム AsyncStream で CIImage型データを非同期に受け取ります。


タスク:プレビューストリームを処理する

ソースコードは「DataModel」(DataModel の二回目)です。

ステップ 1/5

説明文は一般的な用語の「データモデル」にリンクしています。
このコードは「データモデル」である型の型名を「DataModel」としています。

まぎらわしいですね。
App教材では型名の文字数が多くなることを避けるために、このようにしているのかもしれません。
一般にデータモデルの型名は「CameraDataModel」など役割がわかるようにすることも多いです。

Task」は Swift Standard Library に含まれる非同期を実現するためのものです。

Taskのインスタンスを作成するときは、そのタスクが実行する作業を含むクロージャを提供します。
タスクは作成後すぐに実行を開始できます。
明示的に開始またはスケジュールすることはできません。

ステップ 2/5

handleCameraPreviews は最初のタスクで実行する処理です。


ステップ 3/5

handleCameraPreviews の imageStream は map(_:) で CIImage型配列(ストリーム)になります。

ステップ 4/5

swift.org のドキュメント「The Swift Programming Language」は日本語のものがあります。
非同期シーケンス(Asynchronous Sequences)

ステップ 5/5

viewfinderImage は Published 属性のプロパティでそこに画像を非同期で更新しています。

Published 属性なので SwiftUI のビューで参照していると再表示されます。


タスク:プレビューストリームを表示する

ソースコードは「CameraView」です。

ステップ 1/2

CameraView は ViewfinderView を全面表示しています。
overlay で buttonsView を下(.bottom)に配置しています。


ステップ 2/2

model は StateObject属性の DataModel型インスタンスです。
ViewfinderView に model.viewfinderImage を渡しています。


タスク:ファインダーを回転する/ぼかす

このタスクはアプリを修正します。
アプリの機能が変わってしまうので、復習を兼ねて最後に取り組むのがおすすめです。
「🟢 アプリを修正するタスク」で説明します。


🟢 写真を取り込んで保存する


タスク:シャッターボタンに応答する

ソースコードは「CameraView」です。(二回目です)

ステップ 1/2

buttonsView は some View を返す関数で実装されています。
全体は HStack で横に三つのボタンが並びます。

このステップでは中央のシャッターボタンのコードが強調されています。

シャッターボタンのコード


ステップ 2/2

「model.camera.takePhoto()」の部分は model は StateObject属性の DataModel型インスタンスなので、「camera」はDataModel型のソース内で定義されたプロパティです。
「camera」はCamera型のインスタンスです。
takePhoto() は Camera型のソース内で定義されたメソッドです。


タスク:写真を取り込む

ソースコードは「Camera」です。(二回目です)

ステップ 1/5

takePhoto() メソッドの説明です。

カメラのファインダー表示(プレビュー)は解像度が低いですが、撮影する写真は高解像度で保存したいですよね。

ステップ 2/5

実際の写真撮影の要求部分のコードが強調されています。

photoOutput.capturePhoto(with: photoSettings, delegate: self)

最後の引数で自分自身をデリゲートに指定しています。

ステップ 3/5

写真はシャッターを押すUI操作よりも遅れて到着すると説明があります。

ステップ 4/5

extension Camera: AVCapturePhotoCaptureDelegate {
extension で Camera型を AVCapturePhotoCaptureDelegateプロトコル準拠に拡張しています。
Swift言語はこのような書き方が可能なのでこのプロトコルのメソッドだけをまとめて記述できます。
関連するコードをひとまとめに記述できるため、コードの内容が明確になるメリットがあります。

AVCapturePhotoCaptureDelegate

photoOutput(_:didFinishProcessingPhoto:error:)

optional func photoOutput(
    _ output: AVCapturePhotoOutput,
    didFinishProcessingPhoto photo: AVCapturePhoto,
    error: Error?
)

AVCapturePhotoOutput

AVCapturePhoto

ステップ 5/5

addToPhotoStream は Camera型のプライベートプロパティで

private var addToPhotoStream: ((AVCapturePhoto) -> Void)?

引数が一つのクロージャーです。
lazy var photoStream: で addToPhotoStream にクロージャーが設定されています。

addToPhotoStream?(photo)

は photo を引数として渡してクロージャーを実行しています


タスク:写真を処理して保存する

ソースコードは「DataModel」です。(DataModel の三回目)

ステップ 1/8

Task {
    await handleCameraPhotos()
}

ここでは handleCameraPhotos のタスクです。

ステップ 2/8

photoStream からイメージとメタデータを取り出す部分と説明があります。

AVCapturePhoto は「写真を取り込む」ステップ4/5 です。

unpackPhoto は DataModel 型のプライベートメソッドです。

private func unpackPhoto(_ photo: AVCapturePhoto) -> PhotoData?


ステップ 3/8

PhotoData は ファイルプライベートな型として DataModel 内で宣言されています。

fileprivate struct PhotoData {
    var thumbnailImage: Image
    var thumbnailSize: (width: Int, height: Int)
    var imageData: Data
    var imageSize: (width: Int, height: Int)
}


ステップ 4/8

Sequence プロトコルのドキュメント

compactMap内の unpackPhoto(_:) はこのソースコード内のにあるプライベートメソッドです。

ステップ 5/8

非同期動作の for ループで撮影した写真を保存します。

ステップ 6/8

thumbnailImage は画面に表示するので @MainActor をつけています。
Task はクロージャーで渡された処理を非同期で処理するため、 @MainActor は結果を画面に正しく表示するのに必要です。

ステップ 7/8

savePhoto(imageData: Data) メソッドで写真を保存します。

ステップ 8/8

savePhoto(imageData: Data) では保存するタスクがあります。

保存も時間がかかるので Task を使って非同期で処理します。


タスク:少し遅れて作動するシャッターを追加する

このタスクはアプリを修正します。
アプリの機能が変わってしまうので、復習を兼ねて最後に取り組むのがおすすめです。
「🟢 アプリを修正するタスク」で説明します。


🟢 写真をブラウズする

「写真」アプリと同じように、デバイスに保存済みの写真を表示する部分の説明です。


タスク:フォトコレクションを使う

ソースコードは「DataModel」です。(DataModel の四回目)
ステップはひとつだけです。

ステップ 1/1

PhotoCollection(smartAlbum: .smartAlbumUserLibrary)

smartAlbumUserLibrary


タスク:フォト素材を取得する

ソースコードは「PhotoCollection」です。

ステップ 1/5

PhotoCollection はこの App教材独自の class でデータモデル用です。

PhotoAssetCollection型の photoAssets プロパティがあります。

ステップ 2/5

インデックス指定で写真を取得する場合のコードが示されています。

let asset = photoCollection.photoAssets[4]

「コピー」ボタンがありますが、どこかにペーストする指示はありません。

ステップ 3/5

写真の枚数を取得する場合のコードが示されています。

let count = photoCollection.photoAssets.count

同じく「コピー」ボタンがありますが、どこかにペーストする指示はありません。

ステップ 4/5

photoAssets は PhotoAssetCollection 型インスタンスなので「反復処理」ですべての写真を一覧表示できます。

ギャラリーとは PhotoCollectionView が表示する写真の一覧画面を指しています。

ステップ 5/5

SwiftUI のオブザーバブルオブジェクトの説明です。

@Published var photoAssets: PhotoAssetCollection = PhotoAssetCollection(PHFetchResult<PHAsset>())

このように Published 属性にすることで写真の追加やお気に入りの変更などがあった場合に表示を更新できます。
フォトライブラリーは「写真」アプリなどと共通なので、「写真」アプリでフォトライブラリーを変更があり得ます。
実際に「写真」アプリで変更してみると、表示が更新されることを確認できます。


タスク:フォトギャラリーに移動する

ソースコードは「CameraView」です。(三回目です)

ステップ 1/4

NavigationLink を使ってフォトギャラリーに移動するボタンを実現しています。

‼️説明文では「シャッターボタンの左側にこのボタンを配置しましょう」とありますが、既にそのコードが実装済みで強調されています。


ステップ 2/4

CameraView の body は NavigationView を利用しています。
このステップで強調しているコード

PhotoCollectionView(photoCollection: model.photoCollection)

で PhotoCollectionView に photoCollection を渡しています。

ステップ 3/4

PhotoCollectionView(photoCollection: model.photoCollection)

は PhotoCollectionView のイニシャライザーです。

ステップ 4/4

PhotoCollectionView を表示すると CameraView の上に PhotoCollectionView を表示するためファインダーの常時表示は必要なくなります。

.navigationViewStyle(.stack) を指定しているので iPhone だけでなく iPad でもこの動作になります

.navigationViewStyle(.stack) 指定は CameraView のコードで指定しています、確認してください。

常時表示を停止・再開するコードは CameraView に記述します。

.onAppear {
    model.camera.isPreviewPaused = true
}
.onDisappear {
    model.camera.isPreviewPaused = false
}

ここから先は

18,755字 / 22画像
この記事のみ ¥ 200〜

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。