Skip to main content
Version: 0.4.0

Getting started

Providers, one NoughtyTours, start(), and placing the first screen in your own navigation controller.

Do App Attest first

The device attests against your App ID, and nothing syncs until it does. See App Attest.

Five steps, all of them at launch except the last.

1. Implement the two providers​

The SDK reads both live, at the moment it needs them, so a settings change or a re-login is picked up without rebuilding anything. Both are Sendable with async requirements, so you can keep the underlying state on any isolation you like.

extension MyAuthService: OrganizationProviderType {
/// The organisation the SDK scopes its device bearer to. `nil` when nobody is signed in.
func currentOrganizationId() async -> String? { await session?.organizationId }
}

extension MySettingsStore: SettingsProviderType {
func recordingResolution() async -> RecordingResolution { await resolution }
func floorPlanStyle() async -> FloorPlanStyle { await style }
// uiVersion picks the SDK's UI/UX version and defaults to `.v0`;
// panoramaProcessing picks where a panorama is stitched and defaults to `.server`;
// captureMedia / capturePointCloud / captureARData are developer toggles and
// default to `false`. Implement any of them only if you expose them.
}

2. Build the SDK at launch​

One NoughtyTours for the whole process. init(configuration:) cannot fail.

let tours = NoughtyTours(configuration: Configuration(
environment: isReleaseBuild
? .production(publishableKey: "pk_ios_…")
: .development(publishableKey: "pk_ios_…"),
organizationProvider: authService,
settings: settingsStore
))

3. Start it before the first screen​

start() prepares local storage and reconnects to uploads still in flight from a previous launch.

try await tours.start() // idempotent; concurrent calls share one open

Its only failure is storageUnavailable(reason:), and it is terminal — nothing the SDK offers works without local storage. A failed start can be retried. Await it behind your launch or loading screen; after it returns you never have to model a "not ready" state.

4. Establish device auth​

try await tours.prepareDeviceAuth()

See App Attest for when to call it and which failures are worth surfacing.

5. Place the entry screen​

Everything the SDK shows hangs off tours.properties. Ask it for an entry screen, put that screen in your navigation controller, and the SDK takes it from there — opening properties, floors and rooms, and both capture flows, on that same stack. Each entry point reports what happened through a single onEvent closure; the only case you have to act on is failed.

/// The factories are `async` (they read your settings) and `@MainActor`, and each one
/// starts the SDK if you have not — so they throw `.storageUnavailable` when storage is dead.
func showProperties() async throws(NoughtyToursError) {
let list = try await tours.properties.makePropertyListViewController(
subtitle: "Acme Ltd", // optional line under the title
onEvent: { [weak self] event in
switch event {
case .failed(let error):
self?.present(error)
case .structureCaptureFinished(let floor):
analytics.track("floor_mapped", floor.id)
default:
break
}
}
)
navigationController.setViewControllers([list], animated: false)
}

A host with its own "new capture" action skips the list and starts at creating a property. Here you place nothing: the SDK presents the form over your view controller, then the new property in a navigation controller of its own, and dismisses it when the user closes it.

try await tours.properties.presentPropertyCreation(from: self) { event in
if case .failed(let error) = event { present(error) }
}

A host that already knows what it is capturing — a job, a work order — skips asking the user and opens one property:

let property = try await tours.properties.createProperty(name: "12 Oak Street")
let screen = try await tours.properties.makePropertyViewController(property: property) { event in
if case .failed(let error) = event { present(error) }
}
navigationController.setViewControllers([screen], animated: false)

Any of these can also be presented by the SDK, with a Close button and no navigation of yours: presentPropertyList(from:subtitle:onEvent:) and presentProperty(_:from:onEvent:).

A SwiftUI app writes no wrappers. It places a flow with PropertyListFlow or PropertyFlow, and presents one with a modifier:

PropertyListFlow(tours: tours) { event in
if case .failed(let error) = event { failure = error }
}
.failureAlert($failure) // shows over flows the SDK presented, where .alert cannot

Button("New capture") { creating = true }
.propertyCreation(isPresented: $creating, tours: tours) { event in
if case .failed(let error) = event { failure = error }
}
.failureAlert($failure)

See In a SwiftUI app.

6. Forward background upload events​

Uploads continue after your app is suspended, and iOS may relaunch the app to finish them. Forward the delegate call verbatim:

func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
tours.handleEventsForBackgroundURLSession(identifier: identifier, completionHandler: completionHandler)
}

Call it unconditionally, whatever the SDK's state — this is the one entry point that works before start() has finished. It returns true if the session was the SDK's; false means the events are still yours.

Opening a property directly​

For a host that already knows what it wants to capture — a work order, a project, a job — two non-screen calls skip the list. Store the id next to your own record, look it up on the next launch, and create a fresh property when it comes back nil:

let property = if let id = storedPropertyId,
let found = try await tours.properties.property(id: id) {
found
} else {
try await tours.properties.createProperty(name: job.address)
}
let vc = try await tours.properties.makePropertyViewController(property: property, onEvent: handle)
navigationController.setViewControllers([vc], animated: false)

Rules of the road​

  • Handles come from the SDK. Property and Floor arrive in events, or from createProperty(name:floorCount:metadata:) / property(id:). You cannot construct one, by design — a hand-built handle could only name a row that doesn't exist.
  • The SDK controls the navigation of its flows. It pushes and pops its own screens on the navigation controller you placed the entry screen in, and presents and dismisses a flow you asked it to present, so don't push, pop or dismiss around them. It never removes a screen of yours. See Screens.
  • The screens title themselves. They know whether a floor is "Ground floor" or "Floor 2". You own the entry screen and any bar-button items you add to it.
  • Event enums grow. New cases arrive in minor versions — keep a default in exhaustive switches.
  • No alerts are shown for you. The SDK never puts one up of its own; failures are reported through a failed case or thrown. A SwiftUI app shows them with failureAlert. See Errors.