SwiftUIとARKitでUSDZ表示アプリ
SwiftUIを使ったARKitアプリの情報があまりないので備忘録的に書いていきます。
作ったものはこんな感じ。
ソースコード
これだけコピペしても動きません。
ContentView.swift
import SwiftUI
import RealityKit
import ARKit
import FocusEntity
struct ContentView : View {
@State private var isPlacementEnabled = false
@State private var selectedModel: Model?
@State private var modelConfirmedForPlacement: Model?
private var models: [Model] = {
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath, let files = try?
filemanager.contentsOfDirectory(atPath: path) else {
return []
}
var availableModeles: [Model] = []
for filename in files where
filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
let model = Model(modelName: modelName)
availableModeles.append(model)
}
return availableModeles
}()
var body: some View {
ZStack(alignment: .bottom) {
ARViewContainer(modelConfirmedForPlacement: self.$modelConfirmedForPlacement)
if self.isPlacementEnabled {
PlacementButtonsView(isPlacementEnabled: self.$isPlacementEnabled, selectedModel: self.$selectedModel, modelConfirmedForPlacement: self.$modelConfirmedForPlacement)
} else {
ModelPickerView(isPlacementEnabled: self.$isPlacementEnabled, selectedModel: self.$selectedModel, models: self.models)
}
}
}
}
struct ARViewContainer: UIViewRepresentable {
@Binding var modelConfirmedForPlacement: Model?
func makeUIView(context: Context) -> ARView {
let arView = CustomARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
if let model = self.modelConfirmedForPlacement {
if let modelEntity = model.modelEntity {
print("DEBUG: adding model to scene - \(model.modelName)")
let anchorEntity = AnchorEntity(plane: .any)
anchorEntity.addChild(modelEntity.clone(recursive: true))
uiView.scene.addAnchor(anchorEntity)
} else {
print("DEBUG: Unable to load modelEntity for \(model.modelName)")
}
DispatchQueue.main.async {
self.modelConfirmedForPlacement = nil
}
}
}
}
class CustomARView: ARView {
let focusSquare = FESquare()
required init(frame frameRect: CGRect) {
super.init(frame: frameRect)
focusSquare.viewDelegate = self
focusSquare.delegate = self
focusSquare.setAutoUpdate(to: true)
self.setupARView()
}
@objc required dynamic init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupARView() {
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical]
config.environmentTexturing = .automatic
if
ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
config.sceneReconstruction = .mesh
}
self.session.run(config)
}
}
extension CustomARView: FEDelegate {
func toTrackingState() {
print("tracking")
}
func toInitializingState() {
print("initializing")
}
}
struct ModelPickerView: View {
@Binding var isPlacementEnabled: Bool
@Binding var selectedModel: Model?
var models: [Model]
var body: some View{
ScrollView(.horizontal,
showsIndicators: false) {
HStack(spacing: 30) {
ForEach(0 ..< self.models.count) {
index in
Button(action: {
print("DEBUG: selected model with name: \(self.models[index].modelName)")
self.selectedModel = self.models[index]
self.isPlacementEnabled = true
}) {
Image(uiImage: self.models[index].image)
.resizable()
.frame(height: 80)
.aspectRatio(1/1, contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle(PlainButtonStyle())
}
}
}
.padding(20).background(Color.black.opacity(0.5))
}
}
struct PlacementButtonsView: View {
@Binding var isPlacementEnabled: Bool
@Binding var selectedModel: Model?
@Binding var modelConfirmedForPlacement: Model?
var body: some View {
HStack{
// Cancel Button
Button(action: {print("DEBUG: model placement canceled.")
self.resetPlacementParameters()
}) {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
// Confirm Button
Button(action: {print("DEBUG: model placement confirmed.")
self.modelConfirmedForPlacement = self.selectedModel
self.resetPlacementParameters()
}) {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
}
}
func resetPlacementParameters() {
self.isPlacementEnabled = false
self.selectedModel = nil
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Model.swift
import UIKit
import RealityKit
import Combine
class Model {
var modelName: String
var image: UIImage
var modelEntity: ModelEntity?
private var cnacellable: AnyCancellable? = nil
init(modelName: String) {
self.modelName = modelName
self.image = UIImage(named: modelName)!
let filename = modelName + ".usdz"
self.cnacellable = ModelEntity.loadModelAsync(named: filename)
.sink(receiveCompletion: { loadCompletion in
print("DEBUG: Unable to load modelEntity for modelName: \(self.modelName)")
}, receiveValue: { modelEntity in
self.modelEntity = modelEntity
print("DEBUG: Successfully loaded modelEntity for modelName: \(self.modelName)")
})
}
}
プロジェクト作成
Augmented Reality Appを選びます。
content TechnologyはRealityKit
InterfaceはSwiftUI
新規フォルダを作成して、その中にUSDZファイルを入れます。
Assets.xcassetsにサムネイルを入れます。
コード概説
ContentView
メインコンテンツ部分です。
モデルの読み込みとARview, ButtonView, PickerViewを行います。
ARViewContainer
CustomARViewを呼び出す構造体です。
CustomARView
FocusEntityというサードパーティライブラリを呼び出す構造体です。
FocusEntityはARViewにモデル設置場所を矩形で表示するライブラリです。FocusEntityだけでなく、ARWorldTrackingConfigurationもここで定義しています。
ModelPickerView
USDZモデルを選択するScrollViewで構成された構造体です。
PlacementButtonsView
モデルの設置、キャンセルボタンを定義した構造体です。
Model.swift
Modelクラスを定義したファイルです。
状態の管理
ContentViewにおいて以下のコードで状態管理を定義しています。
@State private var isPlacementEnabled = false
@State private var selectedModel: Model?
@State private var modelConfirmedForPlacement: Model?
モデルを選択したらisPlacementEnabledがtrueになりPlacementButtonsViewが呼び出されます。
同時に、ModelPickerViewのself.selectedModel = self.models[index]により、どのモデルが選択されたかをselectedModelに定義します。
続いてmodelConfirmedForPlacementにselectedModelが定義され、modelEntityとなります。
modelEntityはanchorEntityに.addChildされ、ARKitで検出された平面上にモデルが表示されます。
所管
RealityKitとARKit自体はそれほど難しいことはありませんが、SwiftUIとの組み合わせた情報が基本英語しかないので苦労しました。