使用SwiftUI開發(fā)一個(gè)APP - 增加AR海報(bào)功能

上篇文章說道,審核被拒了,這兩天就忙著把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) {

    }
}
  1. UIViewRepresentable是一個(gè)UIKit視圖的包裝器,用于將該視圖集成到SwiftUI的視圖層次中。(參考鏈接1)

  2. 我們?cè)谶@里定義了一個(gè)ARView

  3. 初始化 ARView,并給這個(gè)ARView添加引導(dǎo)層

  4. coordinator比較關(guān)鍵,是ARView和包裹其的SwiftUI中View通訊的橋梁。我們?cè)谶@里初始化了自定義的ARViewCoordinator,這個(gè)對(duì)象我會(huì)在后面介紹

  5. makeUIView 是 UIViewRepresentable協(xié)議必須要實(shí)現(xiàn)的的方法,我們?cè)谶@個(gè)方法中初始化ARView,并返回ARView。

  6. 我們可以看到在這個(gè)項(xiàng)目中我初始化的AR配置是世界追蹤,并設(shè)置了追蹤的屏幕是垂直的,因?yàn)槲抑幌M脩魧⒑?bào)貼在垂直的墻面上。

  7. 將ARView 中 session的delegate設(shè)置到context的coodinator,也就是 4 中返回的對(duì)象。

  8. 這里給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)
        }
    }

}
  1. 主要繼承了Entity 和 HasAnchoring對(duì)象,用于后續(xù)將entity添加到session中,這里參考一下官方文檔的圖
image
  1. 這里我使用了前面文章提到的 AsyncImage 中定義的圖片緩存,也就是圖片下載過之后,在這里就不需要重復(fù)下載了,只需要根據(jù)URL去緩存中找圖片對(duì)象就可以了。

  2. 這里需要將內(nèi)存中的數(shù)據(jù)保存在磁盤上,才能供后續(xù)texture調(diào)用。

  3. 因?yàn)槲覀冞@里要將海報(bào)貼在墻上,所以要?jiǎng)?chuàng)建一個(gè) plane類型的mesh,并設(shè)置mesh的寬和高為0.4米和0.6米。這里參數(shù)的單位就是“米”,因?yàn)檫@個(gè)單位,我還找了半天,最終在文檔中找到了。(參考鏈接3)

  4. 如果使用參考鏈接中的方法使用 SimpleMaterial 會(huì)使得圖片很暗,所以這里使用 UnlitMaterial 使得texture不受AR環(huán)境光影響

  5. 最終生成海報(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) {

    }
}
  1. 在上文我們提到了,為ARView添加手勢,這里是手勢action的處理函數(shù)

  2. 獲取到點(diǎn)擊手勢的屏幕位置點(diǎn)

  3. 通過Reality Kit的raycast方法,獲取屏幕點(diǎn)擊位置發(fā)送射線所到達(dá)的真實(shí)世界的點(diǎn)。(參考鏈接5)

  4. 判斷如果射線沒有返回任何值,則返回。

  5. 判斷如果射線到達(dá)的結(jié)果不是一個(gè)平面,則返回。

  6. 判斷如果射線鎖到達(dá)的平面不是垂直的,則返回。

  7. 通過從資源詳情頁帶過來的imageUrl,創(chuàng)建海報(bào)對(duì)象,這個(gè)對(duì)象是一個(gè)Entity

  8. 當(dāng)我們添加Entity到ARView的Scene中的時(shí)候,平面默認(rèn)是面向下的,因此我們需要對(duì)屏幕進(jìn)行翻轉(zhuǎn)。這里同時(shí)可以看一下ARView中坐標(biāo)系,如下圖。(參考鏈接6)

image
  1. 最終將海報(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é)果還是被拒了,被拒的原因如下:

image

GuideLine 2.3 - Performance - Accurate Metadata

意思大概就是,在詳情頁里面提到了可以追劇,但是實(shí)際功能并不能追劇。所以要么增加追劇的功能,要么把詳情頁的介紹去掉。

OK,那我先簡單點(diǎn)做,把詳情頁改一下吧。

image

Guideline 4.0 - Design

主要是提到了頁面的UI的問題,有的頁面太擁擠了,一些文字看不全。

我大概知道一些地方了,主要是類目頁和詳情頁。那我就先改一版UI再提交吧。 等我的好消息!

參考鏈接:

  1. https://heartbeat.fritz.ai/introduction-to-realitykit-on-ios-entities-gestures-and-ray-casting-8f6633c11877

  2. https://betterprogramming.pub/how-to-use-uiviewrepresentable-in-swiftui-1b9a0a7c1358

  3. https://developer.apple.com/documentation/realitykit/meshresource/3244420-generateplane

  4. https://stackoverflow.com/questions/61854324/add-uiimage-as-texture-to-a-plane-in-realitykit

  5. https://developer.apple.com/documentation/realitykit/arview/3282007-raycast

  6. https://developer.apple.com/forums/thread/658620

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 之前的文章介紹了列表視圖和無限加載瀑布流,今天的主要內(nèi)容是講在列表視圖上方增加一個(gè)搜索輸入框,并跳轉(zhuǎn)到搜索頁面完成...
    LazyGunner閱讀 2,650評(píng)論 0 0
  • SwiftUI 是一種非常簡單的創(chuàng)新方法,可以利用 Swift 的強(qiáng)大能力在所有蘋果設(shè)備平臺(tái)上構(gòu)建用戶界面。通過 ...
    騰飛Tenfay閱讀 2,276評(píng)論 0 1
  • SwiftUI 是一種非常簡單的創(chuàng)新方法,可以利用 Swift 的強(qiáng)大能力在所有蘋果設(shè)備平臺(tái)上構(gòu)建用戶界面。通過 ...
    王大媽啊閱讀 82,830評(píng)論 26 51
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,834評(píng)論 28 54
  • 人工智能是什么?什么是人工智能?人工智能是未來發(fā)展的必然趨勢嗎?以后人工智能技術(shù)真的能達(dá)到電影里機(jī)器人的智能水平嗎...
    ZLLZ閱讀 4,098評(píng)論 0 5

友情鏈接更多精彩內(nèi)容