😎

SwiftUIでRoomPlan使ってみた!

2023/04/27に公開

RoomPlanとは

RoomPlanとはARKitを活用して部屋の3Dモデルを作成するAppleが提供しているSwift APIです。
このAPIはLiDARスキャナを搭載しているiPhone/iPadで使用可能なものになっており、スキャンした3DモデルはUSD/USDZファイル形式で出力することもできます。
またCinema 4D、Shapr3D、AutoCADといった各種USDZ対応ツールにエクスポートするとより細かく調整することができるので、建築やインテリアデザインにおける最初のワークフローで有効的なAPIです。
https://developer.apple.com/jp/augmented-reality/roomplan/

RoomPlanを使用する環境について

使用に適した環境

・最大約9メートル×9メートルの大きさの部屋のシングルルーム
・最低50ルクス(夜間のリビングルームに相当)以上の明るさ

使用に適さない環境

・フルハイトの鏡や窓
・高い天井
・とても暗い表面

高い精度を得るための環境作り

・日中はカーテンを開け、十分な自然光を取り入れる
・ドアを閉めて部屋の外の余計な場所がスキャンされるのを防ぐ

使用する上での注意点

バッテリーの消耗や発熱問題でユーザ体験に影響を与える可能性がある行為
・繰り返しのスキャン
・5分以上のスキャン

RoomPlan対応端末

RoomPlanを使用するにはLiDARスキャナを搭載しているiPhone/iPadを使う必要があります。
以下対応端末一覧になります。

機種名 発売日 モデル番号
iPhone 14 Pro Max 2022年 全モデル
iPhone 14 Pro 2022年 全モデル
iPhone 13 Pro Max 2021年 A2641
iPhone 13 Pro 2021年 A2636
iPhone 12 Pro Max 2020年 A2410
iPhone 12 Pro 2020年 A2406
iPad Pro 12.9 インチ (第 5 世代) 2021年 A2378,A2461
iPad Pro 11 インチ (第 3 世代) 2021年 A2377,A2459
iPad Pro 12.9 インチ (第 4 世代) 2020年 A2229,A2069,A2232
iPad Pro 11 インチ (第 2 世代) 2020年 A2228,A2068,A2230

RoomPlanの活用方法例

・インテリアデザインアプリであれば壁の色の変更をプレビューし、必要な塗料の量を正確に計算できます。
・建築アプリの場合は部屋のレイアウト変更のプレビューや編集がリアルタイムで可能になります。
・不動産アプリの場合は業者が間取りをキャプチャしその3Dモデルを提供できます。

SwiftUIでRoomPlanを使用する

今回はSwiftUIでの実装例を紹介いたします。
実装は以下の3つのクラスによって行っていきます。
・RoomCaptureViewの設定、Delegatenの処理を行うクラス
・RoomCaptureViewをSwitUIで使用できるように変換するUIViewRepresentableのクラス
・RoomCaptureViewをSwiftUIで使用するクラス

またRoomPlanの機能には関係ないですが、補足でActivityViewControllerでのデータ共有についても記載していきたいと思います。

RoomCaptureViewの設定、Delegatenの処理

静的変数にinstanceを定義することでシングルトンでアクセスするようにしておきます。
他の処理は最低限のデリゲートの実装とスキャン開始/停止を実装しておきます。

import Foundation
import RoomPlan

class RoomCaptureController : ObservableObject, RoomCaptureViewDelegate, RoomCaptureSessionDelegate
{
  static var instance = RoomCaptureController()
  
  @Published var roomCaptureView: RoomCaptureView
  @Published var showExportButton = false
  @Published var showShareSheet = false
  @Published var exportUrl: URL?
  
  var sessionConfig: RoomCaptureSession.Configuration
  var finalResult: CapturedRoom?
  
  init() {
    // roomCaptureViewの設定
    roomCaptureView = RoomCaptureView(frame: CGRect(x: 0, y: 0, width: 42, height: 42))
    sessionConfig = RoomCaptureSession.Configuration()
    roomCaptureView.captureSession.delegate = self
    roomCaptureView.delegate = self
  }
  
  // スキャン開始
  func startSession() {
    roomCaptureView.captureSession.run(configuration: sessionConfig)
  }
  
  // スキャン停止
  func stopSession() {
    roomCaptureView.captureSession.stop()
  }
  
  func captureView(shouldPresent roomDataForProcessing: CapturedRoomData, error: Error?) -> Bool {
    return true
  }
  
  // スキャン結果受け取り
  func captureView(didPresent processedResult: CapturedRoom, error: Error?) {
    finalResult = processedResult
  }
  
  // スキャンデータの出力
  func export() {
    exportUrl = FileManager.default.temporaryDirectory.appending(path: "scan.usdz")
    do {
      try finalResult?.export(to: exportUrl!)
    } catch {
      print("Error exporting usdz scan.")
      return
    }
    showShareSheet = true
  }
  
  required init?(coder: NSCoder) {
    fatalError("Not needed.")
  }
  
  func encode(with coder: NSCoder) {
    fatalError("Not needed.")
  }
}

RoomCaptureViewをSwitUIで使用できるように変換

ここでの処理はUIViewをUIViewRepresentableに変換するだけになります。

import SwiftUI

struct RoomCaptureViewRep : UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        RoomCaptureController.instance.roomCaptureView
    }
  
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

RoomCaptureViewをSwiftUIで使用

NavigationLinkでRoomCaptureViewを使用する想定での実装コードになります。
実装しているアクションは
・キャンセル(スキャン停止+画面閉じ)
・Done(スキャン停止+出力ボタン表示)
・onAppear(スキャン開始+出力ボタン非表示)
・データ共有(データの共有+画面閉じ)
の4つになります。

ここまでの処理でRoomPlanによる部屋のスキャン、3Dモデル作成、共有ができます。

import SwiftUI
import RoomPlan

struct ScanningView: View {
  @Environment(\.dismiss) private var dismiss
  @StateObject var captureController = RoomCaptureController.instance
  
  var body: some View {
    ZStack(alignment: .bottom) {
        RoomCaptureViewRep()
            .navigationBarBackButtonHidden(true)
            .navigationBarItems(leading: Button("Cancel") {
		// キャンセル(スキャン停止+画面閉じ)
                captureController.stopSession()
                dismiss()
            })
            .navigationBarItems(trailing: Button("Done") {
	        // Done(スキャン停止+出力ボタン表示)
                captureController.stopSession()
                captureController.showExportButton = true
            }.opacity(captureController.showExportButton ? 0 : 1)).onAppear() {
	        // onAppear(スキャン開始+出力ボタン非表示)
                captureController.showExportButton = false
                captureController.startSession()
            }
        Button(action: {
	    // データ共有(データの共有+画面閉じ)
            captureController.export()
            dismiss()
        }, label: {
            Text("Export").font(.title2)
        })
        .buttonStyle(.borderedProminent).cornerRadius(40).opacity(captureController.showExportButton ? 1 : 0).padding().sheet(isPresented: $captureController.showShareSheet, content:{
            ActivityViewControllerRep(items: [captureController.exportUrl!])
      })
    }
  }
}

関連処理

最後にRoomPlanに直接的には関係ないですが、今回の実装コードに関連がある処理を記載いたします。

ScanningViewの呼び出し処理

NavigationLinkでScanViewへ遷移する処理になります。

import SwiftUI

struct IntroductionView: View {
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("RoomPlan Demo")
                NavigationLink(destination: ScanningView(), label: {Text("Start Scan")}).buttonStyle(.borderedProminent).cornerRadius(40).font(.title2)
            }
        }
    }
}

ActivityViewControllerのSwiftUIでの利用処理

RoomPlanでスキャンしたデータを共有するためのActivityViewControllerをSwiftUIでの利用するための処理になります。

import SwiftUI

struct ActivityViewControllerRep: UIViewControllerRepresentable {
  var items: [Any]
  var activities: [UIActivity]? = nil
  
  func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewControllerRep>) -> UIActivityViewController {
    let controller = UIActivityViewController(activityItems: items, applicationActivities: activities)
    return controller
  }
  
  func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewControllerRep>) {}
}

感想

AR周りの開発は普段触らないのですが、思ったより簡単に実装できてびっくりしました!
ARKitの座標の概念とか難しそうという人は、その辺りを気にしなくていいRoomPlanで遊んでみるのもいいかもしれませんね!
最後まで読んでいただきありがとうございました!

参考

https://developer.apple.com/jp/augmented-reality/roomplan/
https://github.com/deurell/roomscanner

Arsaga Developers Blog

Discussion