Skip to main content
Version: 0.2.0

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.

MethodReturns
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:

MethodPurpose
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:

StructureCaptureModeWhat it does
.newScanMaps the floor from scratch. On confirm the new plan replaces the old one
.continueScanAdds rooms to the existing plan. The rooms already on the floor keep their identity and their panoramas
.continueScan is only for floors that offered it

Pass 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 gone

The 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.

The screens title themselves

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.