Most property and field-service apps still ask users to manually enter room dimensions. Tape measure, pen, back to the app, mistype, repeat. It's tedious and it's lossy.
I've been building a property inspection app and this week I shipped a feature that replaces manual floor-area entry with a LiDAR scan. Walk around a room for 30 seconds, hit Done — the app returns floor area, wall count, door count, window dimensions, and a confidence score for each surface. No internet connection required. No server round-trips. All on-device.
Here's how I wired Apple's RoomPlan framework into a React Native / Expo app as a native module.
The Basic Idea
Apple's RoomPlan framework uses the LiDAR sensor on iPhone 12 Pro and later to build a structured 3D model of a room in real time. It gives you back typed objects: floors, walls, doors, windows — each with dimensions and a confidence rating (high, medium, low).
The challenge is surfacing this in a cross-platform Expo app without hacking around the JS/native boundary.
Building the Module
Expo's native module API makes this cleaner than the old React Native bridge approach. The module exposes two functions:
-
isAvailable()— synchronous check, returnstrueon LiDAR + iOS 17+ -
scanRoom()— async, presents a full-screen capture UI and resolves with structured results
AsyncFunction("scanRoom") { (promise: Promise) in
guard RoomCaptureSession.isSupported else {
promise.reject("UNSUPPORTED", "Requires a LiDAR-equipped device")
return
}
// present RoomCaptureViewController...
}
The view controller runs captureSession.run(configuration:), delegates back through RoomCaptureSessionDelegate, and when the user hits Done it hands raw CapturedRoomData to RoomBuilder. The builder does a cleanup pass (with .beautifyObjects) and returns a CapturedRoom.
What Comes Back
let floorArea = room.floors
.map { Double($0.dimensions.x) * Double($0.dimensions.y) }
.reduce(0, +)
I map the result into a plain dictionary — area in m², per-surface dimensions, confidence string — and resolve the Expo promise. The JS side gets a typed object:
const result = await RoomPlan.scanRoom();
console.log(result.floorAreaM2); // e.g. 18.34
console.log(result.floors[0].confidence); // "high"
Graceful Degradation
Non-LiDAR devices get isAvailable() → false and fall through to a manual input form. No crashes, no confusing errors. The LiDAR path is an enhancement, not a hard dependency.
Why On-Device Matters Here
For property data, sending floor plans to a cloud service introduces latency, cost, and privacy concerns. Every scan happening fully on-device means no API bills per-scan, no waiting on a round-trip, and no floor plan leaving the phone until the user explicitly submits their report.
The practical result: floor area capture went from ~3 minutes (manual) to under 60 seconds with better accuracy than tape-measure entry. On iPhone Pro hardware, LiDAR readings are accurate to within a few centimetres on a normal rectangular room.
The Limitation Worth Knowing
RoomPlan requires a LiDAR sensor — iPhone 12 Pro and later, or iPad Pro. Most fieldwork happens on phones. For a consumer app targeting everyone, you need a fallback. For a professional tool where you control (or specify) the hardware, worth requiring it outright.
If you're building anything involving physical space measurement on iOS, RoomPlan is dramatically underused. Apple ships a full structured-capture pipeline and most apps still ask users to type numbers into a form.
Top comments (0)