Skip to content
macOSDeep Dive Published Updated 8 min readViews unavailable

ImageCaptureCore on macOS: Device Discovery, Camera Sessions, and Import

Build robust ImageCaptureCore imports with hot-plug discovery, device sessions, bounded downloads, progress ownership, and verified photo-library handoff.

ImageCaptureCore lets a macOS application discover digital cameras and scanners and interact with their contents or capabilities. It is not the same API as AVCaptureSession: the latter configures capture pipelines, while ImageCaptureCore browses connected devices, enumerates camera files, requests downloads, and can support tethered capture or scanner workflows.

The framework is asynchronous and device-oriented. A camera may appear, disappear, suspend operations, lose authorization, or reject a session request while the app is importing. Model each device as a changing resource with a session owner and a set of cancellable transfers. Never equate seeing a device in the browser with having an open, authorized session.

Browse for devices as a live inventory

Create an ICDeviceBrowser, assign its delegate before starting it, and retain both for the duration of discovery. The browser reports additions and removals through delegate callbacks. During initial discovery, moreComing and moreGoing help the UI batch changes; they do not mean all devices of every transport have finished discovery. The browser can finish local enumeration before slower network-connected devices appear.

import ImageCaptureCore

final class CaptureBrowser: NSObject, ICDeviceBrowserDelegate {
    private let browser = ICDeviceBrowser()

    override init() {
        super.init()
    }

    func start() {
        browser.delegate = self
        let deviceMaskValue = ICDeviceTypeMask.camera.rawValue | ICDeviceTypeMask.scanner.rawValue
        guard let deviceMask = ICDeviceTypeMask(rawValue: deviceMaskValue) else { return }
        browser.browsedDeviceTypeMask = deviceMask
        browser.start()
    }

    func stop() {
        browser.stop()
    }

    func deviceBrowser(
        _ browser: ICDeviceBrowser,
        didAdd device: ICDevice,
        moreComing: Bool
    ) {
        updateDeviceList(device, present: true, moreComing: moreComing)
    }

    func deviceBrowser(
        _ browser: ICDeviceBrowser,
        didRemove device: ICDevice,
        moreGoing: Bool
    ) {
        updateDeviceList(device, present: false, moreComing: moreGoing)
    }
}

private func updateDeviceList(_ device: ICDevice, present: Bool, moreComing: Bool) {}

Keep browser callbacks short. Update a device model keyed by the identity properties appropriate to the current connection, then let a separate session coordinator open or close a device as the user selects it. Names, serial numbers, transport, and persistent IDs have different stability and privacy characteristics; do not assume that a localized name is a unique database key.

When a device is removed, invalidate in-flight work associated with that connection generation. A delayed metadata or download completion must not update the UI for a newly connected camera that happens to share a display name. Treat additions and removals as state transitions rather than one-time notifications.

Open a session before device operations

Set the device delegate before requesting an open session. ImageCaptureCore reports the outcome through the device delegate or supported completion API. Keep the device object and delegate alive while operations are outstanding. On teardown, cancel work that should not continue and request session closure before discarding the owning state.

Device content authorization and control authorization are distinct. Browsing metadata, downloading existing media, deleting files, and tethering may require different capabilities. Request only what the user’s selected operation needs and handle denial without converting it into a transport failure. If the destination is the user’s Photos library, its Photos entitlement and authorization are separate from downloading into the app’s own container. Depending on how a sandboxed app accesses a USB device, the USB device entitlement may also apply; do not assume it is required for every ImageCaptureCore-only import path. Validate the exact permission and entitlement set for the transport, destination, and distribution target.

Do not treat the device’s hasOpenSession property as permanent truth. The camera can disconnect or become unavailable during the operation. Handle session-open callbacks, capability changes, device suspension, cancellation, and removal separately. A failed session is recoverable; the UI should let the user reconnect or choose a different device without restarting the application.

Enumerate and download safely

Camera contents are not necessarily available the instant a device appears. Wait for cataloging and use the documented camera-file APIs. mediaFiles and contents represent different views: one can ignore storage folder structure while the other reflects the camera’s hierarchy. Preserve the folder and filename context when the product needs to reproduce the source organization.

ICCameraFile.requestDownload returns a Progress object when available and invokes its completion on an available queue, often not the main queue. Retain the progress object while the operation is active, route UI updates to the main actor, and capture the device generation. Do not call completion success until the callback reports no error and the destination has been checked according to product needs.

Bound concurrent transfers. A camera may expose large RAW images, videos, sidecars, or bursts. Estimate disk and memory use from metadata, cap queued files and parallel downloads, and provide cancellation. Never buffer an entire card’s contents in memory. For a large import, stream or let the framework write to a controlled destination and process each completed asset independently.

Use original filenames as metadata, not as trusted filesystem paths. Sanitize path components and prevent collisions before publication. Download into an import staging directory; after completion, validate file readability, extension/type, size, and any required metadata, then move it into the destination library. Preserve paired RAW/JPEG or sidecar relationships when required. Do not delete source files from a camera until the user explicitly requests it and the imported copy has passed verification.

Keep Photos and application storage boundaries clear

Importing bytes to an app container and adding an item to the user’s Photos library are different operations. The Photos entitlement and its authorization policy apply when the app writes to that library. If the application uses a file picker or export directory instead, the user-selected destination has its own access lifetime. Avoid creating an unrequested duplicate in Photos just because an asset was downloaded successfully.

If metadata contains GPS coordinates, creation time, camera serials, or other identifying details, preserve them only when the feature requires it. Make clear whether an import preserves original metadata, transforms orientation, or creates a derivative. Keep a record of the original filename and import transaction so a failed batch can resume or report exactly which assets were imported.

Scanner and tethered workflows

Scanner devices follow a different capability model from camera storage. Inspect the functional units and supported settings instead of assuming every scanner can use the same resolution, color mode, or feeder. Treat each scan as a bounded image-production job, and allow the user to cancel or recover from paper jams and transport errors.

Tethered capture is available only for devices that advertise the relevant capability. Configure camera state deliberately, handle remote shutter and PTP command failures, and do not block the UI while awaiting the device. Keep camera control actions separate from passive file import so a user can understand when the application is changing hardware state.

Validation and diagnostics

Test camera and scanner hot-plug, multiple devices with identical names, local and network transports, incomplete initial enumeration, session refusal, authorization denial, removal during import, cancellation, duplicate filenames, paired files, malformed metadata, disk full, sleep/wake, and application termination. Exercise the import pipeline with synthetic fixtures even when physical devices are unavailable.

Log operation IDs, transport type, device class, file byte count, duration, and error codes. Avoid logging serial numbers, GPS, or image data. Measure discovery time, catalog time, transfer throughput, peak concurrent bytes, cancellation latency, and successful post-import verification rate. A downloaded file should not be counted as a successful user-visible import until the destination transaction completes.

ImageCaptureCore handles the device protocol boundary; the app still owns authorization, session lifetime, bounded transfer, naming, validation, and publication. Keeping those stages separate makes camera imports recoverable when devices or cables are unreliable.

Make import batches resumable

Persist an import manifest outside the camera with the device session generation, selected source-file identities, intended destination, completed transfer list, and current status. If the Mac sleeps or the camera is unplugged, mark unfinished transfers as interrupted instead of completed. On reconnect, re-enumerate the device and reconcile the manifest with the currently visible source files; never assume the camera’s contents array is identical to the previous session.

Use an idempotent destination name strategy. Two cameras can contain files with the same base filename, and a filename may be reused after a card is reformatted. Pair the source device context with a content fingerprint or import transaction identifier where needed. If a transfer produced a file but the app crashed before writing its manifest entry, validate the staging file and either adopt it into the transaction or remove it safely. Do not create duplicates on every retry.

For a multi-file import, publish each validated asset individually or commit a manifest describing the whole batch only after every requested item succeeds. The product should clearly tell the user whether the batch is partial, complete, or cancelled. Maintain an explicit mapping between source item and destination item so retry logic never guesses from sorted order.

A camera can advertise operations that the current app cannot perform because of its entitlement, authorization state, device firmware, or transport. Check supported capabilities before exposing a button, then handle the operation’s final result anyway because capabilities can change. Do not repeatedly request control authorization as a workaround for a failed session.

When a physical device is absent, use fixture-based unit tests for metadata normalization, filename safety, manifest recovery, and the staging-to-library transition. Reserve end-to-end claims about tethering, scanner behavior, USB permissions, and transfer throughput for the exact physical hardware and signed app configuration that were tested. Simulator or compiler checks cannot validate camera firmware behavior.

Related:

Sources:

Comments