1000枚の写真を10秒で閲覧するフォトビューアアプリを作った

## 「写真いっぱい撮ってるけど、一生振り返る暇ないな…」

という課題意識から、**100枚/秒で写真を閲覧**できる「Somato」というiOSアプリをつくった。

1,000枚の写真であれば10秒で閲覧できる。

https://www.youtube.com/watch?v=OA48IaUYL9I

まぁ正直100枚/秒はインパクト重視で謳っているところはあるが、意外と20枚/秒ぐらいで高速表示していても不思議と「懐かしいなぁ」と十分に思い出に浸れる。無料なので興味のある人はぜひダウンロードして試してみてほしい。

https://apps.apple.com/jp/app/id1566199421

（M1 Macでも利用可能。MacのApp Storeで"Somato"で検索してください）

本記事ではこのアプリの実装について書く。

## 概論

実は、Somatoは**技術的に尖った部分は特にない**。

ざっくりいうと、**Photos（PhotoKit）で画像を取得し、UICollectionViewで表示しているだけ**。

最初のプロトは1時間もかからずできた。特に工夫もなく、素直な実装をやってみたら、「あれ、100分の1秒以下で表示できてるぞ...！」っていう。（[約10年前に同様のコンセプトでアプリをつくった](https://shu223.hatenablog.com/entry/20120525/1337902839)ときはデバイス性能の限界もありこうはいかなかった）

しかし、やはり当然ながら、**プロダクトとしてちゃんと出すとなるとそう甘くはなかった**...

## メモリの問題

まず最初にさくっとつくったプロトでは、僕がiPhoneに入れている約500枚の画像を、最初に（表示開始前に）全部UIImageとして読み込んでいた。

3列表示に十分なサイズ（iPhone 12 miniで`375x375`）で取得すればいいので、それでもメモリ使用量は大したことはない。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/2fd4e601-9455-123e-8f2b-fd86afa735d4.png" width="250">

しかしこれが、iPhoneに1万枚以上の画像が入っているとなると話は違う。実際に一緒にやったデザイナーさんのiPhoneには16,000枚の写真・ビデオが入っていた。**どんなにサイズが小さくとも、1万枚の画像をUIImageとしてメモリに置いておくことはできない**。

さらに、1列表示だとより高い解像度の画像データが必要となる。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3180/1ec3bf61-0059-8414-ff9e-1d1ed295f61d.png" width="250">

かといって、「大量の写真を高速に振り返ることができる」というのがアプリのコンセプトなので、「1,000枚までのアルバムが対象です」なんていう制限を設けるわけいにはいかない。**1万枚だろうと100万枚だろうと、メモリを食わずに高速表示**できる必要がある。


## マルチスレッドの問題

上記の使用メモリの制約から、**一定枚数だけをメモリにキャッシュして表示しつつ、古いキャッシュを捨てつつ、バックグラウンドスレッドで先読みする**、という処理を1/100秒ペースで実行する必要がある。

で、画面描画のメインスレッドの他に、メモリキャッシュを管理するスレッド、ローカルやiCloudから画像を取得してくるスレッドetc.のマルチスレッド処理が必要になってくる。

キャッシュはアクセスを一本化するために**Serial Queue**で、画像取得は並列に処理したいので**Concurrent Queue**で、という点も注意が必要。


## 動画のフレーム抽出処理の問題

コンセプトとしては「大量にたまった写真を高速に閲覧する」というものなので、同じくたまっている動画も高速にふりかえりたい。Somatoでは**動画からn秒おきにフレームを抽出**して、それを1枚の写真と同様に高速に流す、という実装を行っている。（これがなかなかにエモい）

この動画処理がさらに鬼門で、**フレームを抜き出す処理は動画のデコードが入る**ので1/100秒表示しながらだといくら先読みしていても間に合わない。

なのでいったんフレーム抽出したものをファイルキャッシュして、そこから必要な分だけメモリに読み込みつつ捨てつつ、みたいなことをする必要がある。

## iCloudの問題

iCloudがさらにさらに鬼門だった。問題が大きく分けて2つ、

1. **iCloudからの読み込み速度**の問題

2. **PhotoKit**の**Internal Error**の問題

があった。

1の方は、ネットワーク経由でのダウンロードとなる場合はローカルからの読み込みと比較して当然ながら相当時間がかかるので、 `PHImageManager.default().requestImage〜`する際に

```swift
let options = PHImageRequestOptions()
options.deliveryMode = .fastFormat
```

でいったん**サムネレベルの解像度の低い画像を取得してお茶を濁しつつiCloudから必要サイズの画像を取得**し、ファイルキャッシュする、というのを100枚/秒で再生しつつバックグラウンドスレッドで行っている。（実は一度iCloudから取得しておけばPhotoKit/Photos自体がローカルキャッシュしてくれるようだが、そちらはいつキャッシュが解放されるのかが不明）

しかしそれよりもややこしく、そして**サードパーティデベロッパーにとってどうしようもない問題**は2だ。これはシステムのiCloud同期の状態によって引き起こされる。試しに「設定」アプリの「写真」 > 「iCloud写真」をオフにし、5分ぐらい待ってから（データが削除されてから）、またオンにして、標準の写真アプリを見てほしい。iCloudの共有アルバムその他の写真がなかなか出てこないと思う。

iCloud同期を有効化した直後、あるいは標準の写真アプリで長いこと当該iCloudアルバム（写真）を閲覧していない状態だと、OS側でまだサムネ解像度の画像取得すらできておらず、このときにPhotosフレームワークでその写真にアクセスしようとすると、2 に書いたInternal Errorを引き起こしてしまう。

> [PhotoKit] Error: Unknown internal error

これについては問題がニッチすぎるので対処法はローカルメモに留めておく。


## まとめ

写真を高速表示するビューアアプリの実装について書いた。

500枚ぐらいのデータで「作ってみたらサクッとできた」んだけど、16,000枚ぐらいのケースや動画サポート、iCloudサポート等を考慮しはじめたら意外と大変だった、という話。

画像を1/100秒で表示したいというケースはあまりないかもしれないが、

- 画像をPhotosで高速に取得する
- それらを省メモリで行う（画像枚数にメモリ使用量を比例させない）
- 動画から高速にフレーム抽出する

こういった実装自体は実はわりと汎用的に役立つのではと思う。