美文网首页使用SwiftUI开发一个APP
使用SwiftUI开发一个APP - 增加AR海报功能

使用SwiftUI开发一个APP - 增加AR海报功能

作者: LazyGunner | 来源:发表于2021-09-04 17:11 被阅读0次

    上篇文章说道,审核被拒了,这两天就忙着把AR的功能加进去,再提交一版。中间还尝试了一下Facebook的Spark AR Studio,还有抖音的像塑,快手的Beyond Effect。三者都是社交媒体的特效制作工具,回头我再把其他几个的使用体验也都写成文章,敬请期待。

    0. ARKit vs RealityKit

    ARKit还是RealityKit 其实描述的不准确,其实应该在SceneKit 还是 RealityKit之间进行选择,当然我们会选择未来的版本即RealityKit。就因为是未来的版本,所以现在RealityKit的文章很少,文档也不是很健全,中间不免会踩不少的坑。没关系,早就踩坑踩习惯了!当然这里同时又激发了写RealityKit文档翻译的欲望。。

    1. 通过UIViewRepresentable自定义View

    首先创建一个文件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是一个UIKit视图的包装器,用于将该视图集成到SwiftUI的视图层次中。(参考链接1)

    2. 我们在这里定义了一个ARView

    3. 初始化 ARView,并给这个ARView添加引导层

    4. coordinator比较关键,是ARView和包裹其的SwiftUI中View通讯的桥梁。我们在这里初始化了自定义的ARViewCoordinator,这个对象我会在后面介绍

    5. makeUIView 是 UIViewRepresentable协议必须要实现的的方法,我们在这个方法中初始化ARView,并返回ARView。

    6. 我们可以看到在这个项目中我初始化的AR配置是世界追踪,并设置了追踪的屏幕是垂直的,因为我只希望用户将海报贴在垂直的墙面上。

    7. 将ARView 中 session的delegate设置到context的coodinator,也就是 4 中返回的对象。

    8. 这里给ARView添加一个 点击手势识别的方法。 (参考链接2)

    实现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?
    
        }
    }
    

    这里通过 实现 addCoaching 方法来完成ARView的引导层设置,这里我们设置引导层结束并退出的目标是 识别到任意屏幕。

    2. 定义一个Poster对象

    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环境中光的影响
            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对象,用于后续将entity添加到session中,这里参考一下官方文档的图
    image
    1. 这里我使用了前面文章提到的 AsyncImage 中定义的图片缓存,也就是图片下载过之后,在这里就不需要重复下载了,只需要根据URL去缓存中找图片对象就可以了。

    2. 这里需要将内存中的数据保存在磁盘上,才能供后续texture调用。

    3. 因为我们这里要将海报贴在墙上,所以要创建一个 plane类型的mesh,并设置mesh的宽和高为0.4米和0.6米。这里参数的单位就是“米”,因为这个单位,我还找了半天,最终在文档中找到了。(参考链接3)

    4. 如果使用参考链接中的方法使用 SimpleMaterial 会使得图片很暗,所以这里使用 UnlitMaterial 使得texture不受AR环境光影响

    5. 最终生成海报图片的Entity。参考链接4

    3. 协调器-实现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
    
            // 根据raycast 设置位置
            poster.setTransformMatrix(rayCast.worldTransform, relativeTo: nil)
            // 根据X轴旋转 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的处理函数

    2. 获取到点击手势的屏幕位置点

    3. 通过Reality Kit的raycast方法,获取屏幕点击位置发送射线所到达的真实世界的点。(参考链接5)

    4. 判断如果射线没有返回任何值,则返回。

    5. 判断如果射线到达的结果不是一个平面,则返回。

    6. 判断如果射线锁到达的平面不是垂直的,则返回。

    7. 通过从资源详情页带过来的imageUrl,创建海报对象,这个对象是一个Entity

    8. 当我们添加Entity到ARView的Scene中的时候,平面默认是面向下的,因此我们需要对屏幕进行翻转。这里同时可以看一下ARView中坐标系,如下图。(参考链接6)

    image
    1. 最终将海报Entity添加到ARView的Scene中。

    4. 创建SwiftUI View包裹 ARView

    因为ARView是一个 UIViewRepresentable 的实现,无法直接继承到 SwiftUI 的 View中,因此我们需要先创建一个 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海报")
        }
    }
    
    struct ResourceDetailPosterView_Previews: PreviewProvider {
        static var previews: some View {
            ResourceDetailPosterView(imageUrl: "https://cdn1.paranoidsqd.com/2021-08-11-151936.png")
        }
    }
    
    

    5. 在原有详情页跳转

    我们在原有详情页的海报图片上包裹一层 NavigationLink 来实现 页面跳转

    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. 再次被拒

    心想这次增加了这个屌炸天的功能之后,不会再说我功能少了吧?结果还是被拒了,被拒的原因如下:

    image

    GuideLine 2.3 - Performance - Accurate Metadata

    意思大概就是,在详情页里面提到了可以追剧,但是实际功能并不能追剧。所以要么增加追剧的功能,要么把详情页的介绍去掉。

    OK,那我先简单点做,把详情页改一下吧。

    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

    相关文章

      网友评论

        本文标题:使用SwiftUI开发一个APP - 增加AR海报功能

        本文链接:https://www.haomeiwen.com/subject/zzbrwltx.html