1. Swift
  2. Dimensioning (3D Box Measurement)

Swift

Dimensioning (3D Box Measurement)

VisionSDK ships an optional dimensioning module that measures a real-world box's length, width, height, and volume using the device's LiDAR sensor. Use it to verify package dimensions at a shipping or receiving counter, spot-check cartons before palletization, or capture dimensional data for billing, manifests, or catalog entry without a tape measure.

The module is opt-in: it is not in the default Core install. It lives in the source distribution at packagexlabs/vision-sdk-ios.

First shipped in: VisionSDK iOS v2.2.2 (May 2026).

Changed in v2.7.0. The VSDK-prefixed wrapper types were removed and the module now re-exports the underlying engine's API directly - DimensioningView, DimensioningSession, DimensioningConfiguration, DimensioningMeasurement. Code written against 2.6.x will not compile. See Migrating from 2.6.x at the end of this page. The release also adds overlay callbacks, live tracking updates, multi-box capture, and per-capture telemetry, and drops the encrypted-model setup entirely.

R&D status: the dimensioning module is in active development. The accuracy envelope, supported shapes, and device requirements may evolve in future SDK releases. Design your integration with that in mind: surface dimensions to your users as approximate, allow re-captures, and don't auto-act on a single capture without confirmation.

5-Minute Quickstart

If you just want to wire it up, the minimum viable integration is four steps:

  1. Add pod 'VisionSDK/Dimensioning' (or the matching SPM product) to your project.
  2. Set your Podfile platform to '17.0' and add the two Info.plist keys below.
  3. Gate your entry-point on Dimensioning.deviceCapabilities().lidar.
  4. Drop a DimensioningView into your SwiftUI hierarchy and read the measurement out of its onCapture closure.

The rest of this page is the same flow with much more context: hardware support, configuration, capture conditions and accuracy, error handling, lifecycle, and troubleshooting.

What ships with the SDK

All assets the dimensioning module needs at runtime are bundled with the SDK. You do not need to download anything at first launch and the .offline mode works without a network connection. The .online mode (covered below) is a separate cloud-augmented path that you opt into per session.

Linking the dimensioning module adds a meaningful amount to your app's install size; if size is a concern, gate the install behind a device-capability check at build time, or ship dimensioning in a separate App Clip / extension that only LiDAR-capable users download.

Hardware and OS Requirements

Requirement Value Notes
iOS deployment target 17.0+ The dimensioning module raises the Core SDK's iOS 16.0 minimum (13.0 prior to Core v2.6.0).
Device class LiDAR-equipped iPhone 12 Pro / 13 Pro / 14 Pro / 15 Pro / 16 Pro (and successors). iPad Pro 2020 (4th gen) and later.
Simulator Not supported Dimensioning.deviceCapabilities().lidar returns false. Note that DimensioningSession.start() does not throw on simulator - it returns normally and then never produces tracks or captures, so gate on the capability flag rather than expecting an error.
Xcode 15.0+ Required for iOS 17 SDK.
Swift 5.9+

Always check Dimensioning.deviceCapabilities() at runtime before showing any dimensioning entry-point in your UI; do not assume LiDAR from the iOS version or device model.

Installation

The dimensioning module is not included in the binary SPM distribution at packagexlabs/vision-sdk. Install via the source distribution instead.

CocoaPods

In your Podfile:

        platform :ios, '17.0'

target 'MyApp' do
  use_frameworks!
  pod 'VisionSDK/Core'
  pod 'VisionSDK/Dimensioning'
end

      

Then:

        pod install

      

The Dimensioning subspec depends on Core, so listing both is redundant but explicit. The subspec declares its own ios.deployment_target = '17.0' and pulls in ARKit and RealityKit as framework dependencies.

Swift Package Manager

Point at vision-sdk-ios directly (not at vision-sdk) and link both products:

        // Package.swift
let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v17)],
    dependencies: [
        .package(
            url: "https://github.com/packagexlabs/vision-sdk-ios.git",
            from: "2.2.2"
        )
    ],
    targets: [
        .target(
            name: "MyApp",
            dependencies: [
                .product(name: "VisionSDK",             package: "vision-sdk-ios"),
                .product(name: "VisionSDKDimensioning", package: "vision-sdk-ios"),
            ]
        )
    ]
)

      

If you're using an Xcode .xcodeproj instead of Package.swift, add the package URL via File > Add Package Dependencies, then in your target's Frameworks, Libraries, and Embedded Content section add both VisionSDK and VisionSDKDimensioning.

Verifying the install

A clean build that succeeds plus a successful import VisionSDKDimensioning is enough confirmation.

Info.plist

Add the following keys to the Info.plist of any target that links the dimensioning module:

        <key>NSCameraUsageDescription</key>
<string>Required for box dimensioning</string>
<key>Privacy - LiDAR Usage Description</key>
<string>Required for accurate 3D measurement</string>

      

Replace the strings with copy that's appropriate for your app; users see this string verbatim in the iOS permission prompt. The Core SDK already requires the camera key for scanning, so you only need to add the LiDAR key when you add dimensioning.

The first time your app shows a DimensioningView, iOS prompts the user for camera permission. If the user denies, subsequent sessions fail with DimensioningError.arSessionFailed(reason:); there is no second-chance prompt without a Settings round-trip.

App-Launch Setup

There is no required setup call. As of v2.7.0 the CoreML models ship unencrypted inside the framework, so there is no decryption-key fetch, no first-launch network round-trip, and no Apple Developer Team requirement - the module works offline from first launch for any signing team.

What remains is a capability check before you show any dimensioning UI:

        import VisionSDK
import VisionSDKDimensioning

@main
struct MyApp: App {
    init() {
        // Only needed if you use a cloud segmentation backend (see Configuration).
        VSDKConstants.apiKey = "YOUR_API_KEY"

        let caps = Dimensioning.deviceCapabilities()
        if !caps.lidar {
            // On non-LiDAR devices, hide the dimensioning button/screen entirely.
            // Do not let the user navigate to it.
        }
    }

    var body: some Scene {
        WindowGroup { RootView() }
    }
}

      

Warming the models (optional)

prefetchModels() was removed along with the wrapper; the engine exposes no prefetch entry point of its own. The first capture still pays a one-time CoreML compile cost, so if first-capture latency matters you can warm the bundled models yourself at launch:

        import CoreML

func warmDimensioningModels() async {
    let bundleURL = Bundle(for: DimensioningSession.self).bundleURL
        .appendingPathComponent("MVDimensioningCore_MVDimensioningCore.bundle")
    guard let contents = try? FileManager.default.contentsOfDirectory(
        at: bundleURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]
    ) else { return }

    await withTaskGroup(of: Void.self) { group in
        for url in contents where url.pathExtension == "mlmodelc" {
            group.addTask { _ = try? await MLModel.load(contentsOf: url) }
        }
    }
}

      

Fire-and-forget; it's idempotent and skipping it is non-fatal.

Capability check

Dimensioning.deviceCapabilities() returns a DimensioningCapabilities value with three booleans:

Field What true means
lidar Device has a usable LiDAR sensor
arWorldTracking ARKit world-tracking is available (always true on supported devices)
sceneReconstruction Scene reconstruction is supported (used internally)

All three are false on simulators. The most important one to check is lidar - gate the entry-point on it.

Dimensioning.version returns the bundled engine build (a YYYY.MM.DD string), which is worth logging in diagnostics.

SwiftUI Integration

DimensioningView is the drop-in camera. It owns its own AR session and reports results through closures.

        import SwiftUI
import VisionSDKDimensioning

struct DimensioningCameraScreen: View {
    var onMeasured: (DimensioningMeasurement) -> Void

    var body: some View {
        DimensioningView(
            configuration: DimensioningConfiguration(
                segmentationBackend: .localOnly,
                measurementUnit: .centimeters,
                maximumTrackCount: 3
            ),
            onCapture: onMeasured
        )
        .ignoresSafeArea()
    }
}

      

Use it from a parent:

        struct ContentView: View {
    @State private var showCamera = false
    @State private var lastMeasurement: DimensioningMeasurement?

    var body: some View {
        VStack {
            if let m = lastMeasurement {
                Text("\(m.length.value) x \(m.width.value) x \(m.height.value) \(m.length.unit.symbol)")
            }
            Button("Measure box") { showCamera = true }
        }
        .fullScreenCover(isPresented: $showCamera) {
            DimensioningCameraScreen { measurement in
                lastMeasurement = measurement
                showCamera = false
            }
        }
    }
}

      

UIKit Integration

There is no UIKit view as of v2.7.0 - VSDKDimensioningView and its delegate protocol were removed, and the engine ships a SwiftUI view only. Host it in a UIHostingController:

        import UIKit
import SwiftUI
import VisionSDKDimensioning

final class DimensioningViewController: UIViewController {

    private var host: UIHostingController<DimensioningView>?

    override func viewDidLoad() {
        super.viewDidLoad()

        guard Dimensioning.deviceCapabilities().lidar else {
            // Hide the entry-point upstream; this is a backstop.
            return
        }

        let dimensioningView = DimensioningView(
            configuration: DimensioningConfiguration(measurementUnit: .centimeters,
                                                     maximumTrackCount: 3),
            onCapture: { [weak self] measurement in
                let l = measurement.length.value
                let w = measurement.width.value
                let h = measurement.height.value
                print("Captured \(l) x \(w) x \(h), confidence \(measurement.confidence)")
                self?.navigationController?.popViewController(animated: true)
            }
        )

        let controller = UIHostingController(rootView: dimensioningView)
        addChild(controller)
        controller.view.frame = view.bounds
        controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(controller.view)
        controller.didMove(toParent: self)
        host = controller
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Releases the ARSession and the camera. Always tear down when leaving
        // the screen; AVCaptureSession will fight ARSession otherwise.
        host?.willMove(toParent: nil)
        host?.view.removeFromSuperview()
        host?.removeFromParent()
        host = nil
    }
}

      

Objective-C is no longer supported for dimensioning. The removed wrapper was what provided the @objc surface. The engine's API is Swift-only (structs, enums with associated values, async methods), so it cannot be bridged. Objective-C callers need a small Swift shim of their own.

Live guidance - onMeasurementUpdate

DimensioningView streams the tracking state and the in-progress dimensions of every tracked box, which is what you need for a "hold steady… ready" HUD:

        DimensioningView(
    configuration: config,
    onCapture: { m in save(m) },
    onMeasurementUpdate: { update in
        // update.trackingState: .searching -> .groundFound -> .boxDetected -> .stable
        // update.tracks:        live per-box measurements, `isStable`, screen rect
        // update.primaryTrackId: the box the engine considers primary
        status = update.trackingState
    }
)

      

DimensioningTrack.measurement is nil until the pipeline has a reading for that box.

Custom overlays - onOverlayUpdate

Set overlayMode = .callback to suppress the built-in graphics entirely and receive every overlay primitive each frame. All geometry arrives in view-space points, so it maps 1:1 onto the view:

        var config = DimensioningConfiguration()
config.overlayMode = .callback          // .builtIn (default) | .none | .callback

DimensioningView(configuration: config, onOverlayUpdate: { frame in
    frame.boxes    // [DimensioningOverlay]: boxVertices2D, contour2D, boundingBox, isStable
    frame.planes   // [DimensioningPlaneOverlay]: boundary2D, center2D
    frame.hud      // statusText, guidanceText, isCapturing, groundPlanePrompt
})

      

Drawing them with a Canvas is straightforward - the 8 box vertices are ordered base face 0–3, top face 4–7, with corner k under corner k+4:

        Canvas { context, _ in
    for box in frame.boxes where box.boxVertices2D.count >= 8 {
        var path = Path()
        for face in [0, 4] {
            path.move(to: box.boxVertices2D[face])
            for offset in 1..<4 { path.addLine(to: box.boxVertices2D[face + offset]) }
            path.closeSubpath()
        }
        for corner in 0..<4 {
            path.move(to: box.boxVertices2D[corner])
            path.addLine(to: box.boxVertices2D[corner + 4])
        }
        context.stroke(path, with: .color(box.isStable ? .green : .yellow), lineWidth: 2)
    }
}

      

This fires every frame - keep the handler cheap.

Full-control path: DimensioningSession

DimensioningSession is a @MainActor ObservableObject exposing @Published lifecycle state, plus imperative capture control. It runs headless - it publishes state but renders nothing, so use DimensioningView when you need the preview.

        import VisionSDKDimensioning

@MainActor
final class DimensioningSessionModel: ObservableObject {

    let session = DimensioningSession(
        configuration: DimensioningConfiguration(
            segmentationBackend: .localOnly,
            measurementUnit: .centimeters,
            maximumTrackCount: 5
        )
    )

    @Published var lastMeasurements: [DimensioningMeasurement] = []
    @Published var lastError: String?

    func start() async {
        do { try await session.start() }
        catch { lastError = describe(error) }
    }

    /// Single primary box.
    func capture() async {
        do { lastMeasurements = [try await session.capture()] }
        catch { lastError = describe(error) }
    }

    /// One result per tracked box, each tagged with its `trackId`.
    func captureAll() async {
        do { lastMeasurements = try await session.captureAll() }
        catch { lastError = describe(error) }
    }

    /// Fully releases the camera and resolves once ARKit has let go of it -
    /// a reliable handoff point before starting an AVCaptureSession.
    func shutdown() async {
        await session.shutdown()
    }
}

      
Member Notes
start() async throws. Begins the AR session.
capture() async throws -> DimensioningMeasurement. Primary box.
captureAll() async throws -> [DimensioningMeasurement]. One per tracked box.
pause() / resume() Suspends and restores processing without tearing down.
shutdown() async. Releases the camera; resolves once ARKit has actually let go.
clearCaptured() Drops the captured-state so the session can capture again.
phase @Published DimensioningPhase.
tracks @Published [DimensioningTrack].
trackingState @Published DimensioningTrackingState.
onMeasurementUpdate / onOverlayUpdate Closure properties, same payloads as the view's.

Phase lifecycle

session.phase walks through:

        idle  ->  initializing  ->  scanning  ->  detected(trackCount:)  ->  capturing  ->  captured(DimensioningMeasurement)

      

Bind directly to it in SwiftUI for a live status indicator - drive a "Capture" button that only enables once phase == .detected and disables while .capturing.

session.tracks gives live per-box overlay data (isStable, normalizedScreenRect) for drawing on top of the AR view, and session.trackingState is the coarser searching → groundFound → boxDetected → stable progression.

Telemetry

Pass a DimensioningTelemetrySink to receive per-capture diagnostics. Events are delivered only when enableTelemetry is true.

        final class MySink: DimensioningTelemetrySink {
    func receive(_ event: DimensioningEvent) {
        switch event {
        case .measurementCaptured(let p):
            log("captured", p.lengthCm, p.confidence, p.durationMs, p.consensusLevel as Any)
        case .measurementAborted(let p):
            log("aborted", p.reason, p.durationMs)   // "timeout" | "user_cancel" | "no_dimensions"
        @unknown default:
            break
        }
    }
}

var config = DimensioningConfiguration()
config.enableTelemetry = true
DimensioningView(configuration: config, telemetry: sink, onCapture: {  })

      

.measurementAborted is the only signal for a capture that gave up - worth surfacing as a retry hint. The SDK has no analytics backend of its own and makes no telemetry network calls; events go only to your sink.

The session holds telemetry weakly. Retain your sink for the lifetime of the session or you will silently stop receiving events.

Configuration

All knobs live on DimensioningConfiguration:

Field Type Default Notes
segmentationBackend SegmentationBackend .localOnly See "On-device vs cloud" below.
measurementUnit UnitLength .centimeters Any Foundation.UnitLength. Pass .inches, .millimeters, etc.
maximumTrackCount Int 5 Cap on simultaneously tracked boxes. Lower values reduce GPU load on older devices (helpful on iPhone 12 Pro / 13 Pro).
overlayMode OverlayMode .builtIn .none hides the built-in graphics; .callback suppresses them and streams geometry to onOverlayUpdate.
enableTelemetry Bool false Gates delivery to your DimensioningTelemetrySink.
        var config = DimensioningConfiguration(
    segmentationBackend: .localOnly,
    measurementUnit: .inches,
    maximumTrackCount: 1   // single-box workflows: keep the capture deterministic
)
config.overlayMode = .callback

      

On-device vs cloud segmentation

segmentationBackend replaced the old mode enum in v2.7.0, and now carries its credentials directly instead of reading them from VSDKConstants:

        .localOnly                                    // entirely on-device, no network, no key
.cloud(url:apiKey:sdkID:)                     // cloud segmentation only
.localFirstThenCloud(url:apiKey:sdkID:)       // local result, refined by the cloud

      

A cloud-side segmentation step can improve accuracy on harder surfaces, at the cost of a per-capture HTTP request. measurement.usedCloudSAM is true on results where the cloud path ran.

.localOnly (default) .cloud / .localFirstThenCloud
Network required No Yes
Credentials required No Yes - passed at the call site
measurement.usedCloudSAM false true when the cloud result landed

There is no longer a VSDKConstants.apiKey fallback - the credential resolver that read it was part of the removed wrapper. If you want the old behaviour, read the constants yourself:

        let creds = VSDKDimensioningCredentials.current   // still public in VisionSDK core
let backend: DimensioningConfiguration.SegmentationBackend =
    creds.apiKey.isEmpty
        ? .localOnly
        : .cloud(url: creds.cloudURL, apiKey: creds.apiKey, sdkID: creds.sdkID)

      

Default to .localOnly. Reach for a cloud backend only when you specifically need the extra accuracy.

Capture Conditions and Accuracy

The dimensioning pipeline reaches its typical accuracy only inside an operational envelope. Design your in-app guidance around these limits and re-prompt the user if a capture falls outside them.

Accuracy envelope

  • Typical variance: roughly ±3-5 cm per dimension, per capture, on the same box. This comes from LiDAR depth noise, pose drift, and lighting. Treat the result as approximate; surface it to your users as such.
  • Minimum box size: ~10 cm on the smallest side. Smaller objects are below the SDK's current tuning floor.
  • Shape: cuboids only. The pipeline is designed for rectangular boxes. Tubes, polybags, envelopes, soft bags that sag, and other non-cuboidal shapes are not supported and will fail to capture or return unreliable values.

Geometry and framing

  • The whole box must be in view, with the top face visible. Length and width are derived primarily from the top face.
  • The box must rest on a flat horizontal surface (table, floor, conveyor) - the SDK uses that surface as the height reference.
  • One box at a time. Multiple boxes in frame, or stacked boxes, can confuse the segmentation and may include neighbor edges in the measurement.

Distance and motion

  • Optimal capture distance: 40-90 cm from the box. Closer than ~30 cm or farther than ~1.5-2 m degrades accuracy meaningfully.
  • Hold steady for 1-2 seconds before and during capture. The pipeline averages multiple frames; motion within the capture window translates directly into error.
  • Tilt slightly downward so the camera sees the top face plus at least one side face. Pure top-down or pure side-on shots give the pipeline less geometric information.

Surface and lighting

  • Box surfaces: prefer flat, opaque, matte cardboard. Highly reflective wrap, transparent film, and very dark matte surfaces reduce LiDAR signal quality.
  • Lighting: ambient indoor light or diffuse warehouse lighting is ideal. Very dim environments hurt detection. Direct sunlight on glossy boxes can introduce false depth readings.
  • Background: cluttered backgrounds (boxes stacked behind the target, shelving directly behind) can cause the SDK to include background edges. A contrasting empty surface behind the box helps.

If you build a customer-facing dimensioning flow, the following patterns hold up well:

  • Show a live framing hint that asks the user to move closer if the box occupies less than ~30% of the screen, or farther if it occupies more than ~80%.
  • Require a stable detection before enabling the capture button (use phase == .detected and track.isStable from the session path).
  • Offer a one-tap "Re-capture" - because of the ±3-5 cm variance, averaging two or three captures noticeably tightens the result.
  • Confirm with the user before persisting a measurement to a downstream system (billing, manifest, catalog).

Result Type

DimensioningMeasurement is a Sendable, Hashable, Identifiable struct (it was an @objc NSObject class before v2.7.0, and its dimensions were NSMeasurement):

        public struct DimensioningMeasurement: Sendable, Hashable, Identifiable {
    public let id: UUID
    public let trackId: UUID                        // stable id of the physical box
    public let timestamp: Date

    // Unit matches `configuration.measurementUnit`
    public let length: Measurement<UnitLength>
    public let width:  Measurement<UnitLength>
    public let height: Measurement<UnitLength>

    public let distanceFromCamera: Measurement<UnitLength>
    public let confidence: Float                    // 0...1
    public let usedCloudSAM: Bool

    public let imageData: Data?                     // captured frame, portrait-up JPEG
    public let imagePixelSize: CGSize
    public let boxVertices2D: [CGPoint]             // 8 corners, image pixel space

    public var volume: Measurement<UnitVolume> { get }   // cubic meters, derived
    public var image: UIImage? { get }
}

      

Reading values

        // Same unit as your configuration's measurementUnit (cm by default)
let lengthCm = measurement.length.value

// Convert on the fly
let widthIn = measurement.width.converted(to: .inches).value

// Volume is always cubic meters; convert as needed
let volCm3 = measurement.volume.converted(to: .cubicCentimeters).value

      

Note the API change: Measurement uses .value and .converted(to:), where the old NSMeasurement used .doubleValue and .converting(to:).

The captured frame

trackId, image / imageData, imagePixelSize and boxVertices2D are new in v2.7.0. boxVertices2D holds the 8 projected box corners in the captured image's pixel space - indices 0–3 the base face, 4–7 the top face, corner k under corner k+4 - so you can draw the measured box over the photo:

        if let image = measurement.image {
    // scale measurement.boxVertices2D from measurement.imagePixelSize
    // into your displayed image's frame, then stroke the 12 edges
}

      

All three are empty/zero when the engine kept no frame.

Confidence

measurement.confidence is a Float in [0, 1]. Use it to gate auto-save: a typical threshold is 0.85 for unattended capture. Below that, prompt the user to retry from a better angle.

Live tracks

session.tracks (and update.tracks from onMeasurementUpdate) is an array of DimensioningTrack:

        public struct DimensioningTrack: Sendable, Identifiable {
    public let id: UUID
    public let measurement: DimensioningMeasurement?   // nil until the pipeline has a reading
    public let isStable: Bool
    public let normalizedScreenRect: CGRect            // [0,1] x [0,1]
}

      

Use normalizedScreenRect to draw an overlay (multiply by your view's bounds). isStable flips to true once the box has been tracked long enough for the engine to attempt a capture. track.id matches measurement.trackId, so you can correlate a capture back to the box it came from.

Errors

DimensioningError is a plain Swift enum thrown by DimensioningSession. Catch it with catch let err as DimensioningError.

Case Trigger What to do
.lidarUnavailable Non-LiDAR device Hide the dimensioning entry-point; gate via deviceCapabilities().lidar upstream.
.arSessionFailed(reason:) ARKit failure (camera denied, app backgrounded mid-session, hardware fault) Surface the underlying reason. Camera-permission denial requires a Settings round-trip.
.noGroundPlane Could not anchor a horizontal floor plane Ask the user to point the camera at the floor for a moment before aiming at the box.
.captureTimedOut capture() never reached a stable measurement Common when the box is too far, partially out of frame, or moving. Retry with better framing.
.userCancelled Cancellation propagated from the session Treat as a soft cancel, not an error.

Two cases from 2.6.x are gone: .missingCredentials (credentials are now passed at the call site, so there is nothing to resolve and fail) and .notConfigured (there is no separate configure() step).

DimensioningView reports no errors. Only DimensioningSession throws. The view has no error callback, so if you need to react to in-session failures you must either use the session directly or infer them from telemetry's .measurementAborted.

Example: handling the full error set

        do {
    let m = try await session.capture()
    save(m)
} catch let error as DimensioningError {
    switch error {
    case .lidarUnavailable:
        showAlert("This device doesn't support dimensioning.")
    case .arSessionFailed(let reason):
        showAlert("Camera error: \(reason)")
    case .noGroundPlane:
        showHint("Point the camera at the floor first.")
    case .captureTimedOut:
        showHint("Move closer or center the box in the frame.")
    case .userCancelled:
        break   // soft cancel
    @unknown default:
        showAlert("Dimensioning failed.")
    }
}

      

Important: Capture Session Conflict

DimensioningView and DimensioningSession own an ARSession internally. ARKit and AVCaptureSession cannot share the camera, so:

  • Before showing the dimensioning view, stop any existing CodeScannerView (call its scanning teardown), then present the dimensioning UI.
  • Before returning to the barcode/OCR scanner, tear the dimensioning UI down. From the session path call await session.shutdown() - it resolves once ARKit has actually released the camera, which makes it a reliable handoff point. From the view path, remove the view (or its hosting controller) from the hierarchy; DimensioningView owns its session privately and exposes no handle, so there is nothing to await.

If both are alive simultaneously, one of them will fail to start with arSessionFailed. There is no automatic handoff - your navigation code must serialize them.

A common pattern in a single-screen flow with both:

        @State private var showScanner = true
@State private var showDimensioning = false

var body: some View {
    ZStack {
        if showScanner { ScannerView() }
        if showDimensioning { DimensioningSessionView() }
    }
    .onChange(of: showDimensioning) { newValue in
        // Mutually exclusive: ScannerView teardown happens via `showScanner = false`
        if newValue { showScanner = false }
    }
}

      

Troubleshooting

Build succeeds but the first session fails on launch

Confirm pod install or SPM resolution completed cleanly and that VisionSDKDimensioning is linked in your target's Frameworks, Libraries, and Embedded Content. If you still see issues, do a clean build (Cmd-Shift-K) and reset the Xcode package cache.

Camera opens, AR session starts, but no boxes are detected

This is most often a cold-start race: the SDK has not finished warming up. Either:

  • Warm the bundled models at app launch (see "Warming the models" above), or
  • Wait a few seconds in the view before expecting detections, or
  • Filter Xcode console logs for [VSDK-DIM] - the SDK prints diagnostic progress messages.

If you've warmed up and still see no detection, check the capture conditions: framing, distance, surface, and lighting. Highly reflective wrap, transparent film, very dark matte surfaces, or boxes smaller than ~10 cm are below the pipeline's tuning floor.

"Scanning Environment..." or initialization phase doesn't progress

ARKit world-tracking is still establishing. Pan the device slowly side to side and downward to give it more visual texture to anchor on. Ensure the scene has enough light. If phase stays at .initializing for more than a few seconds, the user is likely pointing the camera at a textureless surface (a blank wall) - re-prompt them to point at the floor / their workspace.

Dimensions look wildly wrong (e.g. 1 cm height)

The SDK locked onto the box's own top face as the floor plane. This usually happens when the user starts the session already aiming at the box without giving ARKit a chance to see the floor first. Workaround: re-capture after panning the camera over the supporting surface (floor / table) for a second, then back to the box.

Capture / measure path stays disabled

The pipeline has not reached a stable detection. Wait 1-2 seconds with the box centered, the whole box in frame, and the device steady. If this persists, the capture conditions are out of envelope - see "Capture Conditions and Accuracy" above.

lidarUnavailable on what should be a LiDAR device

Devices in some lighting conditions return spurious false from ARKit's capability probe. Confirm by running Apple's "Object Capture" or "Measure" apps. If those work but the SDK says no LiDAR, file an issue with the device model, iOS version, and the SDK version.

noGroundPlane

The dimensioning pipeline expects a flat floor plane before locking onto a box. Pan the camera at the floor for ~1 second before pointing at the box. This is also a hint to surface to your user via UI ("Point at the floor...").

Measurements jitter / change a lot

Typical capture variance is roughly ±3-5 cm per dimension; if you're seeing more, three things help:

  1. Keep the device steady for a full 1-2 seconds before and during capture. The pipeline averages frames - any motion in the capture window translates directly into error.
  2. Ensure all four top corners of the box are in frame.
  3. Avoid extreme lighting (direct sunlight on glossy tape; near-dark warehouses).
  4. Move the device into the 40-90 cm sweet spot. Very close (<30 cm) or very far (>1.5 m) captures degrade meaningfully.
  5. Confirm the box is on a flat horizontal surface and there are no neighboring boxes within ~10 cm of its edges.

Online mode is slow or unreliable

.online makes a per-capture HTTP request to PackageX cloud. On poor connections, prefer .offline and pick .online only for a manual "high-accuracy retry" button.

Both barcode scanning and dimensioning fail to start

Almost always the capture-session conflict (see "Capture Session Conflict" above). Tear down one before starting the other.

FAQ

Do I need a separate API key for dimensioning? No. Only a cloud segmentation backend needs credentials, and as of v2.7.0 you pass them directly to .cloud(url:apiKey:sdkID:). .localOnly - the default - needs no key and no network.

Will the dimensioning feature work on iPad? Yes, on LiDAR-equipped iPads (iPad Pro 2020 / 4th gen and newer). The same deviceCapabilities().lidar check applies.

Can I use ARKit elsewhere in my app while dimensioning is open? Not for the same camera. Two ARSessions cannot share the rear camera. Tear down one before starting the other.

Can the SDK measure cylindrical or irregular shapes? The dimensioning module is designed for rectangular boxes (cuboids). Tubes, polybags, envelopes, and soft bags that sag are not supported and will either fail to capture or return unreliable values.

What's the smallest box the SDK can measure? The pipeline is currently tuned for parcels larger than about 10 cm on the smallest side. Smaller items are below the tuning floor and produce unreliable measurements.

How accurate is a single measurement? Plan around roughly ±3-5 cm of variance per dimension. The variance is centered around the true value, so averaging two or three captures of the same box tightens the result. See "Capture Conditions and Accuracy" above for the full operational envelope.

What units does the volume property return? volume is always a Measurement<UnitVolume> in .cubicMeters, derived from length * width * height. Convert as needed (measurement.volume.converted(to: .cubicCentimeters)).

Do the CoreML models still need a decryption key or a specific signing team? No. Before v2.7.0 the models were encrypted and scoped to PackageX's Apple Developer Team, which meant third-party builds silently degraded to LiDAR-only measurement. They now ship unencrypted inside the framework and work offline from first launch for any team.

Can I still use the module from Objective-C? No. The @objc surface lived in the removed wrapper. The engine's API is Swift-only, so Objective-C callers need their own Swift shim.

Migrating from 2.6.x

import VisionSDKDimensioning now re-exports the engine's API directly. Type-for-type:

2.6.x 2.7.0
VSDKDimensioning.deviceCapabilities() Dimensioning.deviceCapabilities()
VSDKDimensioning.prefetchModels() removed - models ship unencrypted; warm them yourself if needed
VSDKDimensioningSwiftUIView DimensioningView
VSDKDimensioningView (UIKit) + delegate DimensioningView in a UIHostingController
VSDKDimensioningSession DimensioningSession
VSDKDimensioningConfiguration DimensioningConfiguration
VSDKDimensioningMeasurement DimensioningMeasurement (struct; Measurement, not NSMeasurement)
VSDKDimensioningTrack DimensioningTrack
VSDKDimensioningPhase DimensioningPhase
VSDKDimensioningError DimensioningError (no .missingCredentials / .notConfigured)
mode: .offline segmentationBackend: .localOnly
mode: .online (read VSDKConstants.apiKey) segmentationBackend: .cloud(url:apiKey:sdkID:)

Behavioural changes worth checking:

  • Measurement values. .doubleValue.value, .converting(to:).converted(to:).
  • Credentials. No VSDKConstants.apiKey fallback; pass them at the call site (VSDKDimensioningCredentials.current still exists if you want the old behaviour).
  • Telemetry. Events route to a DimensioningTelemetrySink you supply, not through VSDKAnalyticsManager.
  • Objective-C. No longer supported.
  • Simulator. start() no longer throws .lidarUnavailable; it returns and then produces nothing. Gate on deviceCapabilities().lidar.