Skip to main content
Version: 0.4.0

Release notes

What changed in each release of the iOS SDK, newest first.

0.4.0​

Released 2026-09-29

Added​

  • SwiftUI apps start every flow without wrappers. PropertyListFlow and PropertyFlow are views that place the list or one property wherever a screen of yours goes — a scene's root, a tab — with the flow's own navigation inside. The propertyList(isPresented:), property(item:) and propertyCreation(isPresented:) modifiers present a flow the way sheet(isPresented:) presents a sheet: set the binding, and the SDK presents over whatever is on screen, with a Close button, and sets the binding back when nothing of the flow is left. The UIViewControllerRepresentable and presenter anchor the screens guide used to have you write are gone.

    PropertyListFlow(tours: tours) { event in
    if case .failed(let error) = event { failure = error }
    }

    Button("New capture") { creating = true }
    .propertyCreation(isPresented: $creating, tours: tours) { event in
    if case .failed(let error) = event { show(error) }
    }
  • SwiftUI apps can show a failure over a flow the SDK presented. SwiftUI presents an alert from the view it is attached to, so while a flow is presented over that view the alert never shows. failureAlert(_:) takes a NoughtyToursError? binding and shows your alert over whatever is on screen instead — titled with the error's description, with its recovery suggestion as the message — and clears the binding when it is dismissed. Pass actions: for buttons of your own, such as a Retry when the error isRetryable.

    .propertyCreation(isPresented: $creating, tours: tours) { event in
    if case .failed(let error) = event { failure = error }
    }
    .failureAlert($failure)
  • The SDK presents the list or a property, too. presentPropertyList(from:subtitle:onEvent:) and presentProperty(_:from:onEvent:) present either full screen over your view controller, in a navigation controller of the SDK's own with a Close button, as presentPropertyCreation presents a new property — so a host that opens the flow from a screen of its own builds no navigation controller and adds no button. PropertyFlowEvent.closedPropertyList reports the list closed; closedProperty now also reports a presented property closed.

  • A flow can start at creating a property, and the SDK presents it. presentPropertyCreation(from:metadata:onEvent:) is the entry point for a host with its own "new capture" action and no use for the property list. You hand it a view controller and place nothing: the SDK presents the creation form over it as a sheet — name and floor count in .v0, the name alone in .v1 — dismisses the form on Create, and presents the new property full screen in a navigation controller of its own. The property screen carries a Close button that dismisses the flow. metadata is optional and attaches your own record to the property, as createProperty(name:floorCount:metadata:) does.

    try await tours.properties.presentPropertyCreation(
    from: self,
    metadata: JobRef(jobId: "WO-4471", priority: 2)
    ) { event in
    if case .failed(let error) = event { present(error) }
    }
  • The property screens take your colours. Configuration.appearance is an Appearance: a background, a card, two text tiers, a border, a primary colour with the colour of text on it, and destructive, success and warning — plus the interface style the screens' system controls render in. The .v0 property, floor and room screens, the property list, the edit cards and the panorama viewer are drawn in it, and so is what capture asks you to read: its alerts and tips, the sensor and position-lost screens, the manual placement picker and the floor-plan review. The rest of the capture screens — top bar, pills, shutter — keeps its white-on-dark chrome over the camera and takes only the primary and status colours. Leaving it out draws them in Appearance.system, UIKit's system colours; start from it to change a few.

    var appearance = Appearance.system
    appearance.primary = UIColor(named: "Brand")!

    let tours = NoughtyTours(configuration: Configuration(
    environment: .production(publishableKey: "pk_ios_…"),
    organizationProvider: authService,
    settings: settingsStore,
    appearance: appearance
    ))
  • PropertyFlowEvent.closedProperty reports that the user closed a property the SDK presented. openedProperty is raised by the presented flow too, with the handle of the property the user created.

Changed​

  • The .v0 screens follow light and dark mode. They were always white on black; they now take Appearance.system unless you pass an appearance, and are light in light mode — or in whichever style your own screens around them are pinned to. Capture keeps its dark chrome over the camera. To keep a dark look, pass an appearance with interfaceStyle = .dark.
  • The SDK may present over your UI when you ask it to. Until now it only pushed below an entry screen you placed. The SDK still never pops, dismisses or replaces a screen of yours.
  • A property created from the list opens over the list. + on the property list starts the flow presentPropertyCreation(from:metadata:onEvent:) presents: the new property is presented full screen in a navigation controller of the SDK's own, and its Close button returns to the list. It was pushed onto your navigation controller. openedProperty is reported as before, and closedProperty when the user closes it. A property selected on the list is still pushed.
  • Capture hides a tab bar. With the flow placed in a tab of a UITabBarController, the capture screens and the floor-plan review hide the tab bar while they are showing. It stayed over them. A SwiftUI TabView does not hide its bar for a pushed screen; the Screens guide says how to place the flow in a SwiftUI app.

Fixed​

  • The form a new property is asked for in closes once. A second Create or Cancel while it was closing could create the property twice, and dismiss a screen of yours the flow was presented in.

0.3.0​

Released 2026-09-28

Added​

  • Panoramas can be stitched on the device. SettingsProviderType.panoramaProcessing() chooses, per capture session, where a panorama is stitched, with PanoramaProcessing as its value. .server, the default, records the sweep as a movie and uploads it for the server to stitch. .onDevice stitches it on the phone, in the background, while the user captures the next one, and uploads the finished panorama. Stitching runs only while the app is in the foreground and carries on where it stopped when the app comes back. In .v0, the Room screen shows a thumbnail and the status of each panorama, opens it in a full-screen viewer on tap, and offers "Retry stitch" on one that failed.
  • The SDK's UI comes in two versions. SettingsProviderType.uiVersion() chooses which one is presented, with UIVersion as its value. .v0, the default, lists a property's floors and opens floors and rooms on screens of their own. .v1 is a redesign in which the property screen is one project tree: every floor, its rooms, each room's floor plan and panoramas, and their sync state on a single scrolling screen, in light or dark following the system appearance. Floors and rooms are added and edited in sheets, rooms can be added to a floor before its floor plan is captured, and creating a property asks only for its name.
  • Both settings have a default, so a settings provider written for 0.2.0 keeps compiling.

Changed​

  • The SDK navigates inside its flows; the host only places the entry screen. Put the screen from makePropertyListViewController(subtitle:onEvent:) or makePropertyViewController(property:onEvent:) in your UINavigationController, and the SDK pushes and pops everything below it on that same controller: the property opened from the list, floors and rooms, and both capture flows. Both entry points report one enum, PropertyFlowEvent, whose cases say what happened — openedProperty, structureCaptureFinished, panoramaCaptureFinished, failed — and never ask you to navigate. A screen the flow cannot open, such as capture on a device without LiDAR, arrives as failed and the user stays where they were. The entry screen must be inside a navigation controller. A host coordinator reduces to its failure handler:

    let list = try await tours.properties.makePropertyListViewController { event in
    if case .failed(let error) = event { present(error) }
    }
    navigationController.setViewControllers([list], animated: false)
  • FloorPlanStylePreview is replaced by FloorPlanStyle.previewImage(size:), which returns a UIImage of a sample plan drawn in that style, for you to frame and lay out yourself. It returns nil for an empty size or if the sample is unavailable.

  • AuthError.serverError is now serverError(statusCode:message:). message carries the backend's explanation, and the error's description includes it — for example Auth server error 401: Device not registered.

  • No two floors of a property can share a level.

  • The SDK bundles the Geist and JetBrains Mono typefaces its .v1 screens use, and registers them itself; the host app adds nothing.

  • The binary now includes libwebp, with its symbols hidden as GRDB's and Zip's are, so an app that links its own copy is unaffected. Its licence is in NOTICES.

Removed​

  • Every screen a host built and pushed below the entry points; the SDK opens them itself: PropertyManagement.makeFloorViewController(floor:onEvent:), PropertyManagement.makeRoomViewController(room:onEvent:), NoughtyTours.structureCapture (StructureCapture, StructureCaptureMode, StructureCaptureEvent) and NoughtyTours.panoramaCapture (PanoramaCapture, PanoramaCaptureEvent).
  • PropertyListEvent, PropertyEvent, FloorEvent and RoomEvent, replaced by PropertyFlowEvent.
  • The Room handle. Nothing public takes or returns one any more.

Fixed​

  • Capture, sync and upload failures are shown in the user's language (en, de, ro, ru) and worded for the person holding the phone. They were English sentences whatever the device language. Failures the SDK does not classify — database, network, the backend's own message — still carry the system's wording.
  • Device auth recovers on its own when the backend no longer recognises a device it attested before, and after the app is reinstalled on a device that was never attested. Both failed every session until the app's data was cleared.
  • Uploads no longer use up their automatic retries while the device is offline or device auth is failing. The queue pauses and resumes on the next launch or background run.
  • A request rejected because the device session expired is always refreshed and retried. A sync or upload could fail with a 401 until it was retried by hand.

0.2.0​

Released 2026-09-14

Changed​

  • NoughtyToursSDK.xcframework.zip is 10.6 MB, down from 13.0 MB. The binary no longer carries the linker's debug map, which only named files on our build machine. SDK frames in your crash reports still symbolicate to function names, and your linked app is the same size.
  • The documentation is now at docs.noughtyreality.com, with a version for each release.

0.1.0​

Initial binary release.

Added​

  • Closed-source Swift Package Manager distribution of the NoughtyTours SDK as a prebuilt, cross-Xcode static xcframework, exposed through a single NoughtyToursSDK product and a single import (import NoughtyToursSDK).
  • NoughtyTours — one instance per process, built from a Configuration (environment, publishable key, organisation provider, settings provider). init cannot fail; start() prepares local storage and reconnects to in-flight uploads, and is idempotent.
  • Device-level auth. The SDK authenticates itself: every capture/data request carries a bearer minted from an Apple App Attest assertion, scoped to the organisation the host supplies. prepareDeviceAuth() establishes it up front; revokeDeviceSession() tears it down. There is no user login in the SDK — the host owns that and supplies only the selected organisation via OrganizationProviderType.
  • Screens vended from the core in three namespaces — core.properties, core.structureCapture, core.panoramaCapture — each reporting through one typed onEvent closure whose events carry the handle for the next screen. PanoramaCaptureEvent matches its structure-capture neighbour: a .cancelled case, and a .finished that carries the floor and how many panoramas the visit added.
  • iPhone and iPad. The capture screens and the modal cards lay out correctly on iPad, where the cards stay dialog-sized rather than stretching to the full width.
  • Property/floor/room handles without the list screen: createProperty(name:floorCount:metadata:) and property(id:), for hosts that already know what they are capturing. Both are async: they start the core themselves, so a host that builds a screen before awaiting start() gets an error rather than a trap.
  • Host metadata on a tour. createProperty takes an optional metadata — any Encodable that encodes to a JSON object — serialized once at the boundary and sent when the tour is created, so a work order or job id travels to the backend with the capture. Set once, at creation: the update request carries no such field, and a property created without it never gets any. The only check is that the value is a JSON object; anything else is NoughtyToursError.invalidMetadata, which is not retryable.
  • Reading what a capture produced. status(of:) for a point-in-time read and statusUpdates(for:) for an AsyncStream driven by the database's own change notifications; both run the same query, so a poll and a stream cannot disagree. PropertyStatus carries the backend tour id (nil until the first sync), the sync state, and floor/room/panorama counts including uploaded and failed.
  • Typed errors — NoughtyToursError and AuthError, both LocalizedError with an isRetryable flag and a stable code (lowercase dot-separated slugs, one per case, tabulated on each property). Unlike description and the reason payloads, which are diagnostics whose wording may change, a code is API: safe to log, to forward, and to branch on.
  • Offline-first storage and sync: capture never blocks on connectivity, and uploads continue in the background, reconnecting to transfers still in flight across launches.
  • The SDK logs for itself (Log). Its lines are printed to the console prefixed [NoughtyToursSDK], filtered by Log.level. Every line, down to debug, is written to the SDK's on-device log file (Documents/Logs/capture-runtime.log) whatever the level says, so a bug report stays complete at any verbosity.
  • NoughtyTours.isDeviceSupported for gating the capture flow on LiDAR availability.
  • Bundled assets and localized strings resolved automatically at runtime. A missing resource bundle degrades to raw keys on screen rather than taking the host app down.
  • Every property has a name. createProperty(name:) takes a non-optional String, trimmed before it reaches the API; a blank one is rejected with NoughtyToursError.invalidName rather than silently becoming an English placeholder. Floor and room names stay optional — the backend models an unnamed floor or room, and a floor titles itself from its number.
  • Third-party NOTICES and LICENSE.

Notes​

  • Requires iOS 17.0+ and Xcode 16+.
  • arm64 only (device + Apple-silicon simulator); Intel-Mac simulator unsupported.
  • A real device with a LiDAR scanner is required for anything that captures or syncs; App Attest is additionally unavailable in the simulator.
  • Requires the App Attest capability on your own App ID and a pk_ios_* publishable key issued by The Noughty Fox for the matching environment.
  • The package resolves no dependencies of its own — everything the SDK needs is inside the binary.
  • Localized in en, de, ro and ru. The SDK's screens localize themselves; there is no string-override hook.