クラウド Mac で iOS 画像サムネイルの縮小処理を検証する

クラウド Mac で iOS 画像サムネイルの縮小処理を検証する

高解像度の写真は iOS のリストでは小さなセルにしか表示されなくても、高速スクロール中に繰り返しデコードされることがあります。元画像を読み込んでから UIImage を表示サイズに縮小する実装では、画面に見えるのはサムネイルでも、デコード処理では画像全体を扱っている可能性があります。クラウド Mac で回帰テストを実行するなら、リストの滑らかさを目視するだけでなく、「サムネイルの最大辺は指定したピクセル数を超えない」という検証可能な条件を先に定めるほうが確実です。

ファイルサイズではなく出力ピクセル数の上限を決める

この記事では最大辺 320 ピクセルをテスト条件の例として使います。すべての画面に共通する値ではありません。実際のプロジェクトでは、セルのポイント数、対象画面の倍率、切り抜き方法から要求するサイズを導きます。たとえば高倍率の画面に表示する画像なら、セルのポイント数だけを出力ピクセル数にしてはいけません。上限が決まったら画像読み込みの入口でその値を受け取り、リストのコントローラーごとに定数を書かないようにします。

テスト用の JPEG をテストターゲットのリソースに追加し、landscape.jpg と名付けます。元画像の幅と高さは、どちらもテスト上限より大きくしてください。そうしないと、実装が元画像をそのまま返してもテストを通過する可能性があります。画像はネットワークから実行時にダウンロードせず、リポジトリに含めます。縦横比の異なるサンプルも用意し、ひとつのレイアウトだけをテストしないようにします。テストターゲットのリソース設定を確認し、実行時にテスト bundle からファイルを取得できることも確かめます。

出力寸法のアサーションで分かるのは「生成された画像の大きさ」であり、「ピーク時のメモリ使用量」ではありません。この二つは別々に検証します。

デコードの入口でサムネイルを生成する

ImageIO でファイル URL から画像ソースを作り、サムネイルの生成時に最大辺のピクセル数を指定します。例の関数は CGImage を返すため、テストからピクセル単位の幅と高さを直接確認できます。画面側で UIImage が必要なら、呼び出し元で変換します。画像ソースの作成時には即時キャッシュを無効にして、元画像を先にデコードしないようにします。一方、サムネイルの作成時にはデコードを完了させます。

import Foundation
import ImageIO

func makeThumbnail(at url: URL, maxPixelSize: Int) -> CGImage? {
    guard maxPixelSize > 0 else { return nil }

    let sourceOptions: [CFString: Any] = [
        kCGImageSourceShouldCache: false
    ]
    guard let source = CGImageSourceCreateWithURL(
        url as CFURL, sourceOptions as CFDictionary
    ) else {
        return nil
    }

    let thumbnailOptions: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
        kCGImageSourceShouldCacheImmediately: true
    ]
    return CGImageSourceCreateThumbnailAtIndex(
        source, 0, thumbnailOptions as CFDictionary
    )
}

ここでは CreateThumbnailFromImageAlways を使い、サムネイル生成の経路を確実にテストします。CreateThumbnailWithTransform は画像の向きの情報を出力に反映します。向きによっては幅と高さが入れ替わるため、アサーションでは二辺のうち大きいほうを確認し、「幅は高さより大きい」をすべての画像に共通する条件にしないでください。入力が無効、またはデコードできない場合は nil を返します。画面側にも、失敗が分かる表示を用意する必要があります。すべてのファイルが有効だとは仮定できません。

固定サンプルで寸法の回帰テストを書く

本番用の関数をアプリのターゲットに置き、テストターゲットからアクセスできるようにします。次のテストは、リソースがテスト bundle に追加済みであることを前提としています。ファイルがサブディレクトリにある場合は、プロジェクトのリソース構成に合わせて検索方法を調整してください。XCTUnwrap を使うと、サンプルが見つからない場合やデコードに失敗した場合に、オプショナル値を見過ごさず、失敗箇所を特定できます。

import XCTest
import ImageIO
@testable import Sample

final class ThumbnailTests: XCTestCase {
    func testThumbnailRespectsPixelLimit() throws {
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "landscape",
                withExtension: "jpg"
            )
        )
        let image = try XCTUnwrap(
            makeThumbnail(at: url, maxPixelSize: 320)
        )

        XCTAssertGreaterThan(image.width, 0)
        XCTAssertGreaterThan(image.height, 0)
        XCTAssertLessThanOrEqual(
            max(image.width, image.height), 320
        )
    }

    func testRejectsInvalidLimit() throws {
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "landscape",
                withExtension: "jpg"
            )
        )
        XCTAssertNil(makeThumbnail(at: url, maxPixelSize: 0))
    }
}

Sample は実際のモジュール名に置き換えます。寸法テストに加え、破損したファイルのサンプルで呼び出し元が nil を処理できることを確認し、向きの情報を持つサンプルで表示方向が想定どおりかも確かめます。テスト画像は出所と利用許諾を明確にし、個人の写真ライブラリにあるファイルをリポジトリへ追加しないでください。

クラウド Mac で実行し、誤検知を調べる

まず xcrun simctl list devices available でインストール済みのシミュレータを確認し、実在するデバイスを xcodebuild の destination に指定します。プロジェクト名、scheme、テストターゲットもリポジトリ内の名前に置き換えてください。例の引数をそのままコピーして、デバイスが使えると決めつけないようにします。

xcrun simctl list devices available
xcodebuild \
  -project Sample.xcodeproj \
  -scheme Sample \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:SampleTests/ThumbnailTests \
  test

「画像が見つからない」というエラーが出たら、寸法の上限を引き上げる前に、ファイルがテストターゲットのリソースに含まれているか確認します。出力が上限を超えた場合は、リストが実際に makeThumbnail を呼んでおり、先に元画像の UIImage を作る古い経路に戻っていないか確かめます。CI では失敗したテストのコマンド出力を保存し、同じサンプル群を使い続けます。画像を差し替える際にピクセル寸法と向きを確認しないと、変更前後の結果を比較できなくなります。

寸法の検証とメモリの観察を分ける

寸法テストはコミットごとに実行できますが、実際のリストが複数の画像を同時に保持する状況や、画像キャッシュ、高速スクロールの挙動までは再現しません。変更をマージする前に、対象デバイスで代表的な画像群を使い、実際のリストの読み込み経路で繰り返しスクロールして、メモリのピーク値と画像が解放される様子を観察します。デバイス、OS のバージョン、画像の枚数、操作手順を記録して変更前後を比較してください。シミュレータで一度得た数値を、すべてのデバイスに当てはまる結論にはしないでください。

最後に三点を確認します。サムネイルの要求サイズは表示要件から導いているか。画像読み込みの入口はすべて同じダウンサンプリング関数を通るか。無効な画像に対して、画面上で分かる処理経路があるか。こうして、クラウド Mac 上の自動テストではピクセル数の条件を守り、デバイス上の測定では実際のメモリ使用量を確認します。それぞれの結果が示せる範囲を明確にしておきます。

よくある質問

読み込んだ UIImage を縮小するだけでも十分ですか?

十分とは限りません。先に元画像全体をデコードする可能性があるため、ImageIO がサムネイルを生成する段階で最大ピクセル数を指定します。

寸法テストに通ればメモリ使用量も保証できますか?

いいえ。テストが確認するのは出力寸法です。メモリのピークは、実際のスクロール操作と代表的な画像を使い、対象の実機で別途計測します。

専用物理ノードで試す

次のタスクに合わせてクラウドMacを用意

モデルのメモリ要件やビルド負荷に合わせてMac miniをお選びください。3つの構成を、日・週・月・四半期単位でご利用いただけます。ノードの空き状況は管理画面にリアルタイムで表示されます。

クラウドMacを借りる