Test iOS image thumbnail downsampling on a cloud Mac

Test iOS image thumbnail downsampling on a cloud Mac

A high-resolution photo may occupy only a tiny cell in an iOS list, yet trigger repeated decoding during fast scrolling. If the code loads the original image and then scales the UIImage down to its display size, the screen shows a thumbnail, but the decoder may already have processed the full image. When running regression tests on a cloud Mac, it is more reliable to make “the thumbnail’s longest side must not exceed a specified pixel limit” a testable contract than to judge the result by scrolling smoothness alone.

Set a pixel limit instead of guessing from file size

This article uses a 320-pixel limit for the longest side as an example test condition, not as a universal value for every interface. In a real project, derive the requested size from the cell’s dimensions in points, the target screen scale, and how the image is cropped. For example, an image displayed on a high-density screen needs an output pixel size based on more than the cell’s dimensions in points. Once you have determined the limit, pass it to the image-loading entry point rather than defining separate constants in each list controller.

Add a test JPEG named landscape.jpg to the test target’s resources. Both dimensions of the original must exceed the test limit; otherwise, the test might pass even if the implementation returns the original image unchanged. Commit the fixture to the repository instead of downloading it during the test. Keep fixtures with different aspect ratios as well, so the tests do not cover just one layout. Check the test target’s resource configuration to confirm that the file is available from the test bundle at runtime.

An output-size assertion tells you how large the resulting image is. It does not tell you how high peak memory usage gets. Verify those separately.

Create the thumbnail at the decoding entry point

Use ImageIO to create an image source from a file URL, then set the maximum pixel dimension when creating the thumbnail. The example function returns a CGImage so the test can read its pixel width and height directly. If the UI needs a UIImage, convert it at the call site. Disable immediate caching when creating the image source to avoid proactively decoding the original; request immediate decoding when creating the thumbnail.

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 makes the test explicitly exercise the thumbnail-creation path. CreateThumbnailWithTransform makes the output respect image orientation metadata. Width and height may swap for fixtures with different orientations, so assert against the longer side rather than applying a “width must exceed height” rule to every image. The function returns nil for an invalid limit or a file it cannot decode. The UI also needs a visible failure state; it cannot assume every file is valid.

Regression-test the dimensions with a fixed fixture

Put the production function in the app target and make it accessible to the test target. The following test assumes the fixture has been added to the test bundle. If the resource lives in a subdirectory, adjust the lookup to match the project’s resource path. If the fixture is missing or decoding fails, XCTUnwrap reports the failure at that point instead of letting an optional value go unexamined.

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))
    }
}

Replace Sample with the actual module name. Beyond the dimension test, add a corrupt-file fixture to verify that the caller handles nil, and an orientation-tagged fixture to confirm that the image displays as expected. Test images should have a clear provenance and appropriate usage rights; do not commit files from a personal photo library.

Run the test on a cloud Mac and investigate false alarms

First run xcrun simctl list devices available to see which simulators are installed, then choose an existing device for the xcodebuild destination. Replace the project name, scheme, and test target with those in your repository too. Do not copy the example arguments and assume the device is available.

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

If the test reports that it cannot find the image, check whether the resource belongs to the test target before increasing the size limit. If the output exceeds the limit, confirm that the list actually calls makeThumbnail rather than falling back to the old path that creates the original UIImage first. In CI, retain command output for failed tests and keep the same set of fixture files. Changing images without reviewing their pixel dimensions and orientation makes results from separate runs hard to compare.

Keep size checks separate from memory measurements

A dimension test is suitable for every commit, but it does not simulate a real list holding multiple images at once, using an image cache, and scrolling quickly. Before merging, use a representative image set on the target device and repeatedly scroll through the actual list flow, observing peak memory usage and whether images are released. Record the device, OS version, number of images, and steps performed, then compare measurements from before and after the change. Do not treat one simulator reading as a conclusion about every device.

Finally, check three things: whether the requested thumbnail size is derived from display requirements; whether every image-loading path uses the same downsampling function; and whether invalid images have a visible handling path. That gives the cloud Mac automation a clear job—enforcing the pixel contract—while measurements on a device answer the question about real memory usage. Each type of evidence has a defined scope.

Frequently asked questions

Is resizing a UIImage after loading it equivalent to downsampling?

No. Loading may decode the full-resolution source first. Set the pixel limit while ImageIO creates the thumbnail.

Does a passing thumbnail size test prove memory usage is safe?

No. It verifies output dimensions, not peak memory. Measure the actual scrolling workflow with representative images on a target device.

Build on a dedicated physical node

Choose a cloud Mac for your next task

Choose a Mac mini based on memory or build workload. Three models are available to rent by the day, week, month, or quarter. Check real-time node availability in the console.

Rent a cloud Mac now