見出し画像

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

画像1

新規フォルダを作成して、その中にUSDZファイルを入れます。

スクリーンショット 2021-07-23 22.53.38

Assets.xcassetsにサムネイルを入れます。

画像3

コード概説

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?

モデルを選択したらisPlacementEnabledtrueになりPlacementButtonsViewが呼び出されます。
同時に、ModelPickerViewのself.selectedModel = self.models[index]により、どのモデルが選択されたかをselectedModelに定義します。
続いてmodelConfirmedForPlacementselectedModelが定義され、modelEntityとなります。
modelEntityanchorEntity.addChildされ、ARKitで検出された平面上にモデルが表示されます。

所管

RealityKitとARKit自体はそれほど難しいことはありませんが、SwiftUIとの組み合わせた情報が基本英語しかないので苦労しました。

いいなと思ったら応援しよう!