Screens
Every screen is a UIViewController you push onto your own navigation stack.
The factories are async (they read your settings), @MainActor, and each one
starts the SDK if you have not — so all of them can throw
storageUnavailable(reason:).
Each takes an onEvent closure and reports through it. Nothing is presented for
you.
properties
PropertyManagement, on tours.properties.
| Method | Returns |
|---|---|
makePropertyListViewController(subtitle:onEvent:) | The list of properties · PropertyListEvent |
makePropertyViewController(property:onEvent:) | One property's floors and rooms · PropertyEvent |
makeFloorViewController(floor:onEvent:) | One floor · FloorEvent |
makeRoomViewController(room:onEvent:) | One room · RoomEvent |
Plus four non-screen calls:
| Method | Purpose |
|---|---|
createProperty(name:floorCount:metadata:) | Create a property, returns a Property handle |
property(id:) | Look one up; nil if it no longer exists |
status(of:) | One-shot PropertyStatus |
statusUpdates(for:) | AsyncStream<PropertyStatus> |
let list = try await tours.properties.makePropertyListViewController(
subtitle: "Acme Ltd",
onEvent: { event in
switch event {
case .selectProperty(let property): show(property)
case .failed(let error): present(error)
}
}
)
subtitle is an optional line under the title.
structureCapture
StructureCapture, on tours.structureCapture. Builds the floor plan.
func makeViewController(
floor: Floor,
mode: StructureCaptureMode = .newScan,
onEvent: (StructureCaptureEvent) -> Void
) async throws(NoughtyToursError) -> UIViewController
mode says whether the scan starts from nothing or from the plan the floor
already has:
StructureCaptureMode | What it does |
|---|---|
.newScan | Maps the floor from scratch. On confirm the new plan replaces the old one |
.continueScan | Adds rooms to the existing plan. The rooms already on the floor keep their identity and their panoramas |
.continueScan is only for floors that offered itPass it only for a floor that arrived on
continueStructureCapture —
asking for it on any other floor shows an error screen with a way back. The
camera relocalizes into the floor's saved world map first ("Finding position…")
before the user can add rooms, so a plan captured without that saved geometry
has to be remapped once before it can be continued.
The flow owns its own sub-navigation — capture, then review — and pops itself
back on finished. You only handle cancelled and redoRequested:
switch event {
case .finished: break // the flow pops itself
case .cancelled: navigationController.popViewController(animated: true)
case .redoRequested: mapFloor(floor) // build a fresh flow, same floor
}
redoRequested never arrives for .continueScan: the merged plan is already
stored by the time the review is shown, so that review is confirm-only.
A RoomPlan or ARKit failure mid-scan does not end the flow. The active room is
discarded, the session relocalizes, and the screen offers Retry, Build
with N rooms or Go back — the rooms already finished are kept either way,
and nothing reaches onEvent until the user picks one.
panoramaCapture
PanoramaCapture, on tours.panoramaCapture. Two overloads — capture into a
floor, or into a specific room.
func makeViewController(floor: Floor, onEvent: …) async throws(NoughtyToursError) -> UIViewController
func makeViewController(room: Room, onEvent: …) async throws(NoughtyToursError) -> UIViewController
finished reports what this visit added:
case .finished(let floor, let capturedInSession):
// `capturedInSession` counts this visit to the screen, not the floor's total.
log("captured \(capturedInSession) panoramas on \(floor.name ?? floor.id)")
case .cancelled:
// Already popped — do not pop again. Anything captured before that is kept.
cancelled fires after the screen is goneThe back button pops the capture screen itself, so by the time cancelled
reaches you the screen has already left the navigation stack. Popping again
takes the screen that pushed it with it. There is nothing left to do here but
update your own state.
Panoramas the SDK discards
A panorama is graded while it records. Tilt, sweep speed, shake, reversing the sweep and low light each warn on screen first, and a bad enough breach discards the scan: its folder is deleted, nothing reaches local storage or the upload queue, and the user gets the reason and a Try again. In very low light — ISO above 2000, or a shutter slower than 1/15s — the shutter is blocked before recording starts.
A discard is not reported to the host. It shows up only as a smaller
capturedInSession than the user may describe, so count panoramas from that
value or from PropertyStatus.
They know whether a floor is "Ground floor" or "Floor 2". You own the stack — pushing, popping — and any bar-button items or title view you add on top.