iOS Integration (Swift)

The compiled TreedisVPS iOS SDK supplies the AR engine, online and offline localization, occlusion, spatial content, and navigation. It uses ARKit and RealityKit. Your app supplies its UI, authenticated platform requests, and route data. There is no Unity runtime or MultiSet framework to embed.

VPSController supplies the host command/event API, and VPSSession owns the AR engine and its native controllers. They use one coordinate frame and one set of capabilities. Both interfaces work in your own host application.

Install the compiled SDK

Obtain TreedisVPS-iOS-0.3.0.zip and its SHA-256 checksum from Treedis. This distribution contains compiled XCFrameworks, inference models, a host integration example, and third-party licenses. Customers do not need access to the SDK implementation or a source repository.

  1. Verify the supplied archive checksum and extract it into your project's Vendor directory.
  2. In Xcode, add Frameworks/TreedisVPS.xcframework to your app target's Frameworks, Libraries, and Embedded Content and select Embed & Sign.
  3. For an App Clip, also add the core framework to the Clip target and select Embed & Sign there.
  4. Import TreedisVPS. CoreML models are already inside the framework; do not copy them separately.

Alternatively, add the extracted SDK folder as a local Swift package and link its TreedisVPS product. The included Package.swift wraps the compiled frameworks; it does not build SDK implementation sources. Choose one installation method to avoid duplicate symbols or embedded frameworks.

Requirements:

  • iOS 17 or newer and an ARKit-capable iPhone or iPad for live AR. The simulator supports host UI, protocol tests, and photo localization, but has no AR camera.
  • This release was built with Xcode 26.6 / Swift 6.3 and library evolution enabled. Use Xcode 26.6 or a compatible newer toolchain. Device arm64 and simulator arm64/x86_64 slices are included.
  • NSCameraUsageDescription in the host's Info.plist. Add NSLocationWhenInUseUsageDescription only when using GPS hints or georeferencing collection.
  • For GLB/glTF content, also embed the supplied TreedisVPSGLTF.xcframework and GLTFKit2.xcframework, or select the local package's TreedisVPSGLTF product. Ordinary tags and navigation need only the core framework.
  • Keep the VPS key in the host's configuration. Do not put keys in URLs, logs, or content payloads. A distributed app key is not a substitute for server authorization.
  • Retain the supplied license and third-party notice files when redistributing the SDK with your product.

For XcodeGen, a direct framework installation looks like this:

targets:
  YourApp:
    dependencies:
      - framework: Vendor/TreedisVPS-iOS-0.3.0/Frameworks/TreedisVPS.xcframework
        embed: true

Pin the SDK version and checksum in your build process, and replace the complete SDK distribution when upgrading. Published tour/offline packs are separate downloads and are not bundled with the SDK.

Minimal native integration

This is a complete UIKit controller for a map whose published tour ID is already known. Supply the VPS configuration and target from your host app; tour IDs are scoped to that deployment.

import RealityKit
import TreedisVPS
import UIKit

@MainActor
final class TourARViewController: UIViewController {
    let session: VPSSession
    let target: VPSTarget

    init(configuration: VPSConfiguration, tourID: Int) {
        session = VPSSession(configuration: configuration)
        target = .tour(id: tourID)
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { nil }

    override func viewDidLoad() {
        super.viewDidLoad()
        let camera = ARView(frame: view.bounds, cameraMode: .ar,
                            automaticallyConfigureSession: false)
        camera.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(camera)
        session.attach(to: camera)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        session.start(target: target)
        session.setContent([
            VPSContentItem(id: "123", position: [2, 1, -4], title: "Reception")
        ])
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        session.stop()
    }
}

The SDK owns the attached ARView's ARSession and delegate. Create the view with automaticallyConfigureSession: false. Keep one session for an experience and attach its view before starting. All session and controller calls run on the main actor.

VPSSession is observable: SwiftUI can read state, problem, cameraPose, and navigation. Subscribe with for await event in session.events() or session.arEvents() in a cancellable task. stop() pauses while preserving the fix and content for resumption; reset() releases the experience and forces a fresh scan.

The archive includes NativeARViewController.swift, a host integration example demonstrating the controller and an app-supplied platform adapter. It uses only the compiled SDK's public API.

Integrate with your platform

The SDK does not fetch Treedis account data or choose a SaaS/CW environment. Your app:

  1. Resolves the tour slug to the correct tour ID and Matterport model for its environment.
  2. Fetches AR settings, tags, rich content, and the navigation graph or route from its API.
  3. Converts that data to VPSExperience, VPSContentItem, VPSARObject, and VPSRoute.
  4. Supplies the matching VPS server URL and authorization. Verify environment, tour ID, model, and provider before starting; IDs can overlap between deployments.

For the controller, configure loadTour and the route provider before sending commands:

let sdk = VPSController(session: session)
// Your host's loaders return SDK values, using its own API and authentication.
sdk.loadTour = { slug, matterID in
    try await platform.experience(slug: slug, matterID: matterID)
}
session.navigation.routeProvider = { request in
    try await platform.route(request)
}
sdk.start(in: camera)
sdk.setTourInfo(slug: "your-tour-slug")
sdk.startLocalization()

Here platform is your app's adapter, not another SDK dependency. Its experience returns VPSExperience(target:settings:tags:objects:); its route returns VPSRoute. See the example's NativeARPlatform protocol for a complete contract.

setEnvironment is forwarded to hostCommand: the host updates its API/CDN clients and session.configuration. That update completes before the next loadTour call. Missing loaders or host handlers produce onError with a command and message. A handler must perform the action or throw; accepting a command alone does not implement it.

The host route provider receives VPSRouteRequest: the live start position, destination ID/title/position, and shortest, noElevators, or noStairs. Call your route endpoint and return the ordered nodes in public tour coordinates. The SDK follows the route, draws path dots and turn/landing markers, measures progress, detects deviation, and emits arrival and floor-change events.

try await session.navigation.start(to: VPSDestination(
    id: "123", title: "Conference room", position: [2, 1, -4]),
    type: .shortest)

Localize before starting navigation. routeProvider also handles automatic rerouting and recompute(). A failed recomputation preserves the previous usable path. To provide a path directly, call session.navigation.setPath(VPSRoute(nodes:destination:)); this disables automatic rerouting. The wire setPath command is a single-floor path without transitions.

A floor-change entry node carries VPSFloorTransition(kind:direction:name:exitPosition:). On .waitingForFloorChange, show the user's stairs/elevator confirmation. Call confirmTeleport() before they move between floors, then endTeleport() after they physically arrive. The SDK hides content during the change, applies a landing hint, and requires a new localization before resuming. Do not send both commands back to back.

Observe session.navigation.events() and its observable state, remainingMeters, instruction, bearingDegrees, isWrongDirection, and nextManeuver. Path appearance is configurable through navigation.style.

Localization settings and offline mode

The default engine is .server. A slow or failed server request never silently switches to the phone.

Offline use is explicit:

  1. Ask for user approval in your host's Settings before downloading.
  2. Inspect session.offline.offer(for:) for download size and version.
  3. Call session.offline.download(_:attachments:progress:). Supply host files such as tags.json, arSettings.json, identity, and navigation data as attachments.
  4. Start with VPSLocalizationSettings.engine = .onDevice only while the host's offline toggle is enabled and that target has a complete pack.
  5. Load host content from offline.attachment(named:for:); avoid host API calls and remote media in this mode. Disabling offline mode should remove its downloaded data.

Downloads verify file hashes, commit atomically, exclude data from iCloud backups, and are scoped by server and tour. A missing pack fails with notDownloaded; the on-device localization and map-data clients cannot make HTTP requests. The host is responsible for caching its own route/content assets too. Map sets localize on the server; download member tours separately for offline use.

session.update(settings:) changes confidence gating, relocalization time/distance, search radius/floor band/depth, query mode, multi-frame requests, geo settings, occlusion, and mesh display. Defaults include confidence 0.4, background checks every 30 seconds or 10 metres. An engine change starts a new scan. rescan() discards the current fix; relocalize() requests a fresh check.

session.setLocalizationHint(position:radius:floorBand:expiresAfter:) uses ordinary tour coordinates and clears after the next accepted localization. Server localization applies these hints; the current on-device engine searches its downloaded pack without hint filtering.

For georeferenced tours, GPS hints and geographic pose output are optional. session.server exposes georeference, map-set, map information, and analytics APIs. VPSTarget.mapSet(primaryTourId:) uses the primary tour's coordinate frame.

Spatial content, queries, and threads

  • session.setContent places tag labels, stems, and anchors. session.setObjects/upsertObject handle TAG, THREAD, VS_IMAGE, VS_VIDEO, VS_OBJECT, VS_HTML, and POLYGON, including visibility, range, transform, and completion state.
  • Native model content supports USDZ and Reality files. For GLB/glTF, link TreedisVPSGLTF and import TreedisVPSGLTF and call TreedisVPSGLTF.install(on: session).
  • HTML content is a static snapshot with JavaScript disabled. Present interactive web content in the host UI after a tap.
  • Read object taps through session.arEvents(); native AR touches work directly. Use click(at:) or click only when a host overlay intercepts the touch.
  • raycastTag() finds a tag along the camera's forward cone. captureCameraFrame(uploadTo:) returns camera-only JPEG data, dimensions, and matching intrinsics, optionally sent with an HTTPS signed PUT URL. The VPS key is not attached to that upload.
  • Thread placement supports device or surface mode, confirmation, cancellation, nearest sweep, completion, removal, and upsert. The SDK returns the placement; the host persists threads and messages to its own backend.

Commands

VPSController accepts both send(type, data) and receive(json). Wire data is a JSON object; optional requestId belongs inside it. Native queries and web replies preserve the same request ID.

Typed and wire commands queue until start(in:) attaches an ARView. Queued commands execute in order before onReady; asynchronous environment/tour loading still completes later through loading events. stop() clears queued commands and cancels work. A late delegate receives readiness again. reset() clears the experience, content, route, and hints, and cancels an unfinished tour load.

Commands Data / typed Swift API
setEnvironment environment, customApiUrl?, customCdnUrl?; setEnvironment(_:customApiUrl:customCdnUrl:), host adapter
setTourInfo slug, matterId?; setTourInfo(slug:matterId:)
startLocalization, relocalize, reset Methods with the same names
setLocalizationHint x,y,z, radius?, floorMinY?, floorMaxY?; setLocalizationHint(x:y:z:radiusMeters:floorMinY:floorMaxY:)
clearLocalizationHint clearLocalizationHint()
showARObject, hideARObject String id, type; showARObject(id:type:), hideARObject(id:type:)
setMeshVisible Boolean visible; setMeshVisible(_:)
click x,y in top-left-origin view points; click(x:y:)
setWindowSize Integer width,height of input coordinate space; setWindowSize(width:height:)
startNavigation destinationName,x,y,z,navigationType?; startNavigation(destinationName:x:y:z:navigationType:)
stopNavigation, recomputePath Methods with the same names
setPath destinationName, nodes:[{x,y,z}]; setPath(destinationName:nodes:)
confirmTeleport, endTeleport confirmTeleport(), endTeleport()
getPosition requestId?; getPosition(requestId:)
raycastTag requestId?; raycastTag(requestId:)
captureCameraFrame requestId?, uploadUrl?; captureCameraFrame(requestId:uploadTo:)
startThreadCreation, setThreadPlacementMode Boolean useDevicePosition; typed methods with that argument
confirmThreadPlacement, cancelThreadCreation Methods with the same names
spawnThread, updateThread spawnThread(_:), updateThread(_:); thread data has id, title, position fields and isCompleted
removeThread String threadId; removeThread(threadId:)
setThreadCompleted threadId,isCompleted; setThreadCompleted(threadId:isCompleted:)
loadUI, unloadUI url for load; host adapter; also openTreedisUI / closeTreedisUI
requestPushToken, requestDeviceIntegrity Host adapter
reportSessionOrigin, signOut, log, logWebView Host adapter; web/account/diagnostic services

Unknown commands and malformed fields emit onError. Position fields must be finite numbers, object IDs strings (integer JSON IDs are normalized to strings), flags booleans. Hint radius is clamped to 1–100 metres, default 25; supply both floor bounds in ascending order.

All public positions, including controller, wire and session hints, use the Treedis/Showcase Y-up metre frame. Hint radius defaults to 25 metres and is clamped to 1–100, including fractional values. Supply both floor bounds in ascending order. Convert any external coordinate format at the host boundary before calling the SDK. Position-query rotation is the camera forward vector; heading is degrees in [0,360).

Events

Set sdk.onEvent = { name, args in ... }, or retain a delegate conforming to VPSControllerDelegate. Delegate methods have no-op defaults. The catch-all receives every event, including those with typed callbacks. Event arguments are JSON-serializable and match the {event,args} envelope.

Event Arguments
onReady []; attached and accepting commands, not a successful localization
onLoadingStarted, onLoadingComplete []
onLoadingProgress [step, progress], progress 0–1
onLocalizationStatusChanged [localized]; scanning/re-establishing false, accepted localization true
onLocalizationResult [success, confidence] for each attempt
onARObjectClick [id, type]
onARObjectReady [id, type], additional native content-load event
onMeshReady [true] when the scan mesh is ready
onNavigationDistance [metres]
onNavigationTeleport ["up" or "down", name, "stairs" or "elevator"]
onNavigationComplete []
onPose [x,y,z,headingDegrees], while navigating
onPosition [{requestId, localized, pose?, rotation?}]; pose has x,y,z,confidence, rotation has forward x,y,z
onRaycast [{requestId, tagId, distance, confidence}]; tagId is null when absent
onCameraFrame [{requestId, ok, mime?, width?, height?, image?, uploadUrl?, error?}]; image is JPEG base64
onThreadPlacementStarted ["device" or "raycast"]
onThreadPlaced [x,y,z,sweepID]
onThreadPlacementCancelled []
onThreadPlacementFailed, onThreadCreationError [message]
onMessage [{type,data}]; structured diagnostic/scanning message
onError [{command?, objectId?, message}]
onPushToken [token, platform], reported by the host
onPushTap, onPushMessage [data], reported by the host
onDeviceIntegrity [report], reported by the host

Typed delegate callbacks cover ready, loading, localization status, object taps, navigation distance/pose/floor-change/arrival, and VPSPositionResult. Other events use vpsController(_:didReceiveEvent:args:).

onMessage types include log, localization_status (Searching/Success), localizationSuccess, localizationWarning, warning, and info. After a first accepted localization, the soft warning can fire after ten seconds without 0.3 metres of movement or a failed background attempt, at most once every ten seconds. It does not mean the AR anchor has been lost. Tracking warnings use actual ARKit tracking state. The host supplies the scanning prompt and animation.

Hosted web UI and app services

For an existing page, retain VPSWebBridge(webView:sdk:allowedOrigins:) before loading the WKWebView. It provides TreedisVPS.send(type, data) and named event callbacks over the native engine. Only the main frame of explicitly allowed HTTPS origins can send commands. Disconnect it before removing the page. The host still manages page navigation and permissions.

The bridge calls window.onNavigationComplete(), window.onPose(...), and other named functions directly. onMessage receives a structured {type,data} object. Native callbacks and web callbacks use the same structured values; no callback requires JSON parsing.

Handle loadUI, unloadUI, environment changes, push registration, device-integrity policy, sign-out, cookies, and session diagnostics in sdk.hostCommand. Return real service results through:

sdk.reportHostEvent(.pushToken(token: token, platform: "ios"))
sdk.reportHostEvent(.pushTap(notificationData))
sdk.reportHostEvent(.pushMessage(notificationData))
sdk.reportHostEvent(.deviceIntegrity(report))

Notification data is [String:String]; the integrity report uses the report fields (platform, issues, flags, and host-provided device/app metadata). The SDK does not manufacture a push token or assess your security policy. These adapters allow navigation apps, Connected Workers apps, and independent hosts to use the same AR engine.

App Clip integration

Create your own full-app and App Clip identifiers and signing configuration. Add an App Clip target, embed it in the parent app, set its parent-application entitlement, associated domains, and NSAppClip configuration. Link the same compiled core SDK version into both targets, with Embed & Sign enabled for each.

Handle browsing-web user activities in both targets; parse the invocation URL in the host, resolve the tour/destination/hint, and pass SDK values or commands to the engine. The SDK itself does not parse a particular customer's QR format. Both targets can share your host's navigation UI and platform adapters.

Keep the Clip small: link the core product, use server localization, and leave large media/offline downloads out of the initial Clip. Verify the built embedded Clip's size and entitlements for your chosen distribution experience.

A locally built or simulator-invoked Clip does not publish an App Clip experience. Public launch also requires your website's AASA association and the corresponding App Store Connect configuration. Test physical QR invocation, camera localization, and walking navigation on a real device with a published map before release.

Independent host applications

Retain your VPSController and its session/delegate, supply tour and route adapters, and end the experience on dismissal. All native and web commands share one contract. Applications use the same compiled SDK with their own account API and UI.