IIIF Image API に対応した高解像度画像をARKitで床面に実寸配置し、カメラを近づけると細部が動的に読み込まれるiOSアプリ「IIIF AR」を開発しました。開発にはAnthropicのClaude Codeを活用しています。
アプリの概要
IIIF ARは、IIIF Image APIに対応した画像をiPhoneのカメラで検出した床面に実寸で配置するARアプリです。
主な機能は以下のとおりです。
- 実寸配置: 画像のピクセルサイズと実際の寸法(cm)から、AR空間に正確なスケールで配置
- タイルベース深zoom: カメラを近づけるとIIIF Image APIのregionパラメータを利用して、視野内の領域のみ高解像度タイルを動的に取得
- 配置プレビュー: タップ前に赤いコーナーマーカーで配置予定位置を表示
- コーナーポール: 四隅に黄色いポールを立てて、障害物がある環境でもサイズを把握可能
サンプル画像
琉球関連の3つの歴史史料をサンプルとして組み込みました。
| 名称 | 所蔵 | サイズ |
|---|---|---|
| 海東諸国紀 | 東京大学史料編纂所 | 32.6×21.2 cm |
| 琉球国之図 | 沖縄美ら島財団 | 96.8×47.3 cm |
| 琉球国図 | 沖縄県立博物館・美術館 | 87.8×175.8 cm |
「琉球国図」は縦175.8cmの掛幅で、AR空間に配置すると人の身長に近いサイズになります。
技術スタック
- 言語: Swift 5.9 / SwiftUI
- AR: ARKit + RealityKit
- 画像配信: IIIF Image API 2.0
- 課金: StoreKit 2(Tip Jar)
- プロジェクト管理: XcodeGen
- 開発ツール: Claude Code (Anthropic)
アーキテクチャ
AR配置パイプライン
床面検出 → 配置プレビュー → タップで配置 → タイル読み込み
ARWorldTrackingConfigurationで水平面を検出します- 検出した
ARPlaneAnchorに画像を紐づけて安定したトラッキングを実現しています - 低解像度の全体画像(1024px)をベースレイヤーとして常時表示します
- カメラ距離に応じて高解像度タイルを上に重ねていきます
IIIFタイルシステム
タイルベースの深zoomは、以下の7ファイルで構成しています。
Tile/
TileKey.swift — タイル識別子(scaleFactor, tileX, tileY)
TileGrid.swift — IIIFタイル座標系の幾何計算
ZoomLevelMapper.swift — カメラ距離→スケールファクタのマッピング
FrustumCuller.swift — カメラ視錐台とタイルの可視判定
TileTextureCache.swift — NSCacheベースのテクスチャキャッシュ
TileFetcher.swift — 非同期タイルダウンロード(最大4並行)
TileManager.swift — オーケストレーター
距離とスケールファクタの対応
| 距離 | スケールファクタ | 概要 |
|---|---|---|
| > 3.0m | 128 | 全体が1タイル |
| 1.0〜1.5m | 16 | 立って見る距離 |
| 0.5〜0.7m | 4 | しゃがんで見る |
| ≤ 0.3m | 1 | 最高解像度 |
IIIF URLの構築例
sho.tif(25,167×12,483px)のスケールファクタ8、タイル(0,0)の場合は以下のようになります。
{baseURL}/0,0,4096,4096/512,512/0/default.jpg
エッジタイル(右端)の場合はリージョンがクランプされます。
{baseURL}/24576,0,591,4096/74,512/0/default.jpg
ベースレイヤーの常時表示
開発中、タイルの読み込み中にベース画像が消えてしまう問題がありました。原因は2つありました。
透明タイルプレースホルダーによる遮蔽: 透明度0のタイルエンティティがRealityKitの深度バッファに書き込まれ、下のベース画像を遮蔽していました。テクスチャが準備できたタイルのみエンティティを作成することで解決しています。
ポール切替時のタイル削除: デバッグ用コーナーポールのON/OFF切替で「imageEntity以外の全子要素を削除」するコードが、タイルエンティティまで巻き込んでいました。
poleEntitiesとedgeEntitiesを個別に追跡することで解決しています。
Claude Codeによる開発
Claude Codeの「エージェント」機能を活用して開発しました。
並行開発チーム
最大20のエージェントを同時に起動し、以下のような役割分担で並行作業を行いました。
実装チーム:
- アプリアイコン生成
- IIIF画像キャッシュ・並列化・リトライ
- 配置プレビュー(赤コーナーマーカー)
- ハプティックフィードバック
- カメラ権限チェック
- Tip Jar(StoreKit 2)
- ローカライゼーション(日英)
レビューチーム:
- TileGrid/TileKey正確性検証
- FrustumCuller数学検証
- IIIF タイルURL実動作検証
- ARViewContainerコード品質
- プロジェクト設定レビュー
レビューチームが発見した主なバグ
| 問題 | 原因 | 修正 |
|---|---|---|
| タイルが画面を覆うと非表示になる | FrustumCullerが4隅のみテストし、タイルが画面を包む場合を検出できない | 中心点テスト+逆テスト追加 |
| ポール切替でタイルが消える | 全子要素削除がタイルを巻き込む | edgeEntities個別追跡 |
| TileFetcherのCPU浪費 | スピンループ(busy wait) | 即座にnil返却方式 |
| 透明タイルがベース画像を遮蔽 | 深度バッファへの書き込み | テクスチャ準備後にのみエンティティ作成 |
アイコンデザインの試行錯誤
Claude Codeはプログラムによる画像生成(Pillow, SVG)はできますが、デザイン性の高いアイコン作成には向いていないようでした。最終的にはGeminiの画像生成で作成したアイコンを採用しています。Apple HIGに従い、以下の点に注意しました。
- 正方形で提供(iOSが自動で角丸マスク)
- 四隅まで背景色で埋める(白い隙間を作らない)
- テキストなし
- シンプルな要素数
ソースコード
ソースコードは以下で公開しています。
- GitHub: nakamura196/iiif-ar-ios