上篇文章說道,審核被拒了,這兩天就忙著把AR的功能加進(jìn)去,再提交一版。中間還嘗試了一下Facebook的Spark AR Studio,還有抖音的像塑,快手的Beyond Effect。三者都是社交媒體的特效制作工具,回頭我再把其他幾個(gè)的使用體驗(yàn)也都寫成文章,敬請(qǐng)期待。
0. ARKit vs RealityKit
ARKit還是RealityKit 其實(shí)描述的不準(zhǔn)確,其實(shí)應(yīng)該在SceneKit 還是 RealityKit之間進(jìn)行選擇,當(dāng)然我們會(huì)選擇未來的版本即RealityKit。就因?yàn)槭俏磥淼陌姹?,所以現(xiàn)在RealityKit的文章很少,文檔也不是很健全,中間不免會(huì)踩不少的坑。沒關(guān)系,早就踩坑踩習(xí)慣了!當(dāng)然這里同時(shí)又激發(fā)了寫RealityKit文檔翻譯的欲望。。
1. 通過UIViewRepresentable自定義View
首先創(chuàng)建一個(gè)文件PosterARView.swift
struct PosterARView: UIViewRepresentable { //1
var arView: ARView! // 2
var imageUrl: String = ""
init(imageUrl url: String) {
self.arView = ARView() // 3
self.arView.addCoaching()
self.imageUrl = url
}
func makeCoordinator() -> ARViewCoordinator {
return ARViewCoordinator(arView: self.arView, imageUrl: self.imageUrl)
} //4
func makeUIView(context: Context) -> ARView { //5
let arConfiguration = ARWorldTrackingConfiguration()
arConfiguration.planeDetection = .vertical
arView.session.run(arConfiguration) // 6
arView.session.delegate = context.coordinator // 7
let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(sender:)))
arView.addGestureRecognizer(tapGesture) // 8
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
UIViewRepresentable是一個(gè)UIKit視圖的包裝器,用于將該視圖集成到SwiftUI的視圖層次中。(參考鏈接1)
我們?cè)谶@里定義了一個(gè)ARView
初始化 ARView,并給這個(gè)ARView添加引導(dǎo)層
coordinator比較關(guān)鍵,是ARView和包裹其的SwiftUI中View通訊的橋梁。我們?cè)谶@里初始化了自定義的ARViewCoordinator,這個(gè)對(duì)象我會(huì)在后面介紹
makeUIView 是 UIViewRepresentable協(xié)議必須要實(shí)現(xiàn)的的方法,我們?cè)谶@個(gè)方法中初始化ARView,并返回ARView。
我們可以看到在這個(gè)項(xiàng)目中我初始化的AR配置是世界追蹤,并設(shè)置了追蹤的屏幕是垂直的,因?yàn)槲抑幌M脩魧⒑?bào)貼在垂直的墻面上。
將ARView 中 session的delegate設(shè)置到context的coodinator,也就是 4 中返回的對(duì)象。
這里給ARView添加一個(gè) 點(diǎn)擊手勢識(shí)別的方法。 (參考鏈接2)
實(shí)現(xiàn)ARCoachingOverlayView
extension ARView: ARCoachingOverlayViewDelegate {
func addCoaching() {
let coachingOverlay = ARCoachingOverlayView()
coachingOverlay.delegate = self
coachingOverlay.session = self.session
coachingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
coachingOverlay.goal = .anyPlane
self.addSubview(coachingOverlay)
}
public func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
//Ready to add entities next?
}
}
這里通過 實(shí)現(xiàn) addCoaching 方法來完成ARView的引導(dǎo)層設(shè)置,這里我們?cè)O(shè)置引導(dǎo)層結(jié)束并退出的目標(biāo)是 識(shí)別到任意屏幕。
2. 定義一個(gè)Poster對(duì)象
class Poster: Entity, HasModel, HasAnchoring, HasCollision { // 1
required init() {
super.init()
}
convenience init(_ imageUrl: String) {
self.init()
let cache = Environment(\.imageCache).wrappedValue // 2
var image: UIImage?
// Create a temporary file URL to store the image at the remote URL.
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
image = cache[URL(string: imageUrl)!]
let data = image?.pngData()
if data == nil {
return
}
// Write the image Data to the file URL.
try! data!.write(to: fileURL) // 3
let mesh = MeshResource.generatePlane(width: 0.4, height: 0.6) // 4
// 不受AR環(huán)境中光的影響
var material = UnlitMaterial()
material.tintColor = UIColor.white.withAlphaComponent(1) // 5
do {
// Create a TextureResource by loading the contents of the file URL.
let texture = try TextureResource.load(contentsOf: fileURL)
material.baseColor = MaterialColorParameter.texture(texture)
self.components[ModelComponent] = ModelComponent(
mesh: mesh,
materials: [material]
) // 6
} catch {
print(error.localizedDescription)
}
}
}
- 主要繼承了Entity 和 HasAnchoring對(duì)象,用于后續(xù)將entity添加到session中,這里參考一下官方文檔的圖

這里我使用了前面文章提到的 AsyncImage 中定義的圖片緩存,也就是圖片下載過之后,在這里就不需要重復(fù)下載了,只需要根據(jù)URL去緩存中找圖片對(duì)象就可以了。
這里需要將內(nèi)存中的數(shù)據(jù)保存在磁盤上,才能供后續(xù)texture調(diào)用。
因?yàn)槲覀冞@里要將海報(bào)貼在墻上,所以要?jiǎng)?chuàng)建一個(gè) plane類型的mesh,并設(shè)置mesh的寬和高為0.4米和0.6米。這里參數(shù)的單位就是“米”,因?yàn)檫@個(gè)單位,我還找了半天,最終在文檔中找到了。(參考鏈接3)
如果使用參考鏈接中的方法使用 SimpleMaterial 會(huì)使得圖片很暗,所以這里使用 UnlitMaterial 使得texture不受AR環(huán)境光影響
最終生成海報(bào)圖片的Entity。參考鏈接4
3. 協(xié)調(diào)器-實(shí)現(xiàn)ARViewCoordinator
class ARViewCoordinator: NSObject, ARSessionDelegate {
var arView: ARView
var imageUrl: String = ""
init(arView: ARView, imageUrl url: String) {
self.arView = arView
self.imageUrl = url
super.init()
}
@objc func handleTap(sender: UITapGestureRecognizer) { // 1
let tapLocation: CGPoint = sender.location(in: arView) // 2
let estimatedPlane: ARRaycastQuery.Target = .existingPlaneInfinite
let alignment: ARRaycastQuery.TargetAlignment = .vertical
let result: [ARRaycastResult] = arView.raycast(from: tapLocation, allowing: estimatedPlane, alignment: alignment) // 3
guard let rayCast: ARRaycastResult = result.first
else {
print("no result")
return
} // 4
guard let planeAnchor = rayCast.anchor as? ARPlaneAnchor
else {
print("not a plane")
return
}
if planeAnchor.alignment != .vertical {
print("not a vertical plane")
return
}
let poster = Poster(imageUrl) // 7
let radians = -90.0 * Float.pi / 180.0
// 根據(jù)raycast 設(shè)置位置
poster.setTransformMatrix(rayCast.worldTransform, relativeTo: nil)
// 根據(jù)X軸旋轉(zhuǎn) 90°
poster.transform.rotation *= simd_quatf(angle: radians, axis: SIMD3<Float>(1,0,0)) // 8
// 在場景中添加該entity
arView.scene.addAnchor(poster) // 9
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
}
}
在上文我們提到了,為ARView添加手勢,這里是手勢action的處理函數(shù)
獲取到點(diǎn)擊手勢的屏幕位置點(diǎn)
通過Reality Kit的raycast方法,獲取屏幕點(diǎn)擊位置發(fā)送射線所到達(dá)的真實(shí)世界的點(diǎn)。(參考鏈接5)
判斷如果射線沒有返回任何值,則返回。
判斷如果射線到達(dá)的結(jié)果不是一個(gè)平面,則返回。
判斷如果射線鎖到達(dá)的平面不是垂直的,則返回。
通過從資源詳情頁帶過來的imageUrl,創(chuàng)建海報(bào)對(duì)象,這個(gè)對(duì)象是一個(gè)Entity
當(dāng)我們添加Entity到ARView的Scene中的時(shí)候,平面默認(rèn)是面向下的,因此我們需要對(duì)屏幕進(jìn)行翻轉(zhuǎn)。這里同時(shí)可以看一下ARView中坐標(biāo)系,如下圖。(參考鏈接6)

- 最終將海報(bào)Entity添加到ARView的Scene中。
4. 創(chuàng)建SwiftUI View包裹 ARView
因?yàn)锳RView是一個(gè) UIViewRepresentable 的實(shí)現(xiàn),無法直接繼承到 SwiftUI 的 View中,因此我們需要先創(chuàng)建一個(gè) View將其包裹起來,再供其他View使用
//
// ResourceDetailPosterView.swift
// FoloPro
//
// Created by GUNNER on 2021/8/31.
//
import SwiftUI
struct ResourceDetailPosterView: View {
var imageUrl: String = ""
var body: some View {
PosterARView(imageUrl: imageUrl).navigationTitle("AR海報(bào)")
}
}
struct ResourceDetailPosterView_Previews: PreviewProvider {
static var previews: some View {
ResourceDetailPosterView(imageUrl: "https://cdn1.paranoidsqd.com/2021-08-11-151936.png")
}
}
5. 在原有詳情頁跳轉(zhuǎn)
我們?cè)谠性斍轫摰暮?bào)圖片上包裹一層 NavigationLink 來實(shí)現(xiàn) 頁面跳轉(zhuǎn)
VStack (alignment:.center) {
HStack(alignment:.top) {
NavigationLink(
destination: ResourceDetailPosterView(imageUrl: resource.poster)) {
AsyncImage(url: URL(string: resource.poster)!,
placeholder: { Text("Loading ...") },
image: {
Image(uiImage: $0).resizable()
})
.scaledToFit()
.frame(width: 156, height: 240)
}
....
6. 再次被拒
心想這次增加了這個(gè)屌炸天的功能之后,不會(huì)再說我功能少了吧?結(jié)果還是被拒了,被拒的原因如下:

GuideLine 2.3 - Performance - Accurate Metadata
意思大概就是,在詳情頁里面提到了可以追劇,但是實(shí)際功能并不能追劇。所以要么增加追劇的功能,要么把詳情頁的介紹去掉。
OK,那我先簡單點(diǎn)做,把詳情頁改一下吧。

Guideline 4.0 - Design
主要是提到了頁面的UI的問題,有的頁面太擁擠了,一些文字看不全。
我大概知道一些地方了,主要是類目頁和詳情頁。那我就先改一版UI再提交吧。 等我的好消息!
參考鏈接:
https://betterprogramming.pub/how-to-use-uiviewrepresentable-in-swiftui-1b9a0a7c1358
https://developer.apple.com/documentation/realitykit/meshresource/3244420-generateplane
https://stackoverflow.com/questions/61854324/add-uiimage-as-texture-to-a-plane-in-realitykit
https://developer.apple.com/documentation/realitykit/arview/3282007-raycast