由於開啟 App 時,加載 AR 都要花費一些時間,AR 鏡頭才會由卡頓變成順暢。
因此從使用者的角度,我想先加一些提示畫面,轉移注意力,才不會覺得等很久。
目前開啟 App 之後的流程:
AR 開機 → 先顯示提示卡牌 → 使用者選擇 → 決定是否把魔杖加進場景
用 ZStack 疊兩層:
ZStack {
RealityView { ... } update: { ... }
.ignoresSafeArea()
if vm.phase != .done {
OnboardingCard(onChoose: { summon in ... })
.transition(.opacity)
}
}
使用 ZStack 可以讓我們不動 AR 場景就能替換前景 UI,互不干擾。
@MainActor
final class AROnboardingVM: ObservableObject {
enum Phase {
case loadingAR // AR 載入中
case onboarding // 顯示卡牌互動
case done // 卡牌收起,進入正式體驗
}
enum Choice {
case undecided // 尚未決定
case summonWand // 召喚魔杖
case noWand // 不要魔杖
}
@Published var phase: Phase = .loadingAR
@Published var choice: Choice = .undecided
}
分別記錄畫面階段 Phase 與使用者對於是否要魔杖的選擇 Choice。
private final class SceneStore {
weak var cameraAnchor: AnchorEntity?
weak var wandEntity: ModelEntity?
}
原本是把 RealityKit 的物件放在 RealityView 中,結果在 app 運行期間出現警告:
Modifying state during view update, this will cause undefined behavior.
為何有這警告?
SwiftUI 在更新畫面的時候去改 @State/@Published 等狀態,會造成未定義行為。
把 RealityKit 實體的增刪移交由非 @Published 的物件管理,就不會碰到渲染期改狀態的問題。
RealityView { content in
content.camera = .spatialTracking
if scene.cameraAnchor == nil {
let anchor = AnchorEntity(.camera)
content.add(anchor)
scene.cameraAnchor = anchor
}
} update: { _ in
syncWandIfNeeded() // 只讀 vm.choice,操作 RealityKit
}
.onAppear {
if vm.phase == .loadingAR { vm.phase = .onboarding }
}
使用者還在卡牌場景時,AR 畫面中先不要有魔杖。
struct OnboardingCard: View {
var onChoose: (_ summonWand: Bool) -> Void
var body: some View {
VStack(spacing: 16) {
Text("準備好了嗎?").font(.title2).bold()
Text("請把手放到鏡頭前。\n拇指與食指捏合可以抓住魔杖,放開即可鬆手。")
.multilineTextAlignment(.center)
.font(.callout)
.foregroundStyle(.secondary)
Divider().opacity(0.2)
Text("要使用召喚咒召喚魔杖嗎?").font(.headline)
HStack(spacing: 12) {
Button("稍後再說") { onChoose(false) }.buttonStyle(.bordered)
Button("速速前魔杖!") { onChoose(true) }.buttonStyle(.borderedProminent)
}
}
.padding(20)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
.padding(.horizontal, 24)
.shadow(radius: 12)
}
}
重點:讀 vm.choice,用 scene 存取 RealityKit 物件,只動場景、不動 SwiftUI 狀態。
private func syncWandIfNeeded() {
guard let cameraAnchor = scene.cameraAnchor else { return }
switch vm.choice {
case .summonWand:
if scene.wandEntity == nil {
let wand = makeWandPlaceholder()
wand.position = [0, 0, -0.3]
cameraAnchor.addChild(wand)
scene.wandEntity = wand
Task { await applyWandTexture(to: wand) }
}
case .noWand, .undecided:
scene.wandEntity?.removeFromParent()
scene.wandEntity = nil
}
}
func makeWandPlaceholder() -> ModelEntity {
let mesh = MeshResource.generatePlane(width: 0.04, height: 0.25)
var mat = UnlitMaterial()
mat.blending = .transparent(opacity: 0.0)
return ModelEntity(mesh: mesh, materials: [mat])
}
@MainActor
func applyWandTexture(to entity: ModelEntity) async {
do {
let texture = try await TextureResource(named: "Wand")
var material = UnlitMaterial()
material.color = .init(tint: .white, texture: .init(texture))
material.blending = .transparent(opacity: 1.0)
entity.model?.materials = [material]
} catch {
print("Failed to load Wand texture: \(error)")
}
}
後面兩區都是屬於前幾天的範疇,只是時機的調動。
現在一打開 App 呈現的樣子:
AR 相機運行起來之後:
點擊速速前魔杖,魔杖就會出現在畫面中
如果點稍後再說...那恭喜獲得滿版相機鏡頭!