阅读 158

iOS开发 · iOS音视频开发 - ARKit 2.0 教学:储存并恢复世界地图数据 建立更连贯的 AR 体验

以下就是你即将开始建立的 App。 arkit-20-demo-1

你可以从图片了解,我所说的储存世界地图数据 (Data),指的是你可以先储存 AR 世界地图,然后即使关闭了 app,仍可稍后回复地图数据;这是 iOS 11 无法做到的。现在只要把世界地图的数据储存起来,你就能让使用者回到之前 AR 体验的时间点。

先决条件

在开始实作之前,你需要对之前的 ARKit 教学 内容有基本了解。如果你对 ARKit 还很陌生,请先看看我们的 ARKit 系列教程.

为配合本次教学,你需要准备 Xcode 10 beta 或更新版本,而 Apple 的装置也需要 iOS 12 beta 或更新版本。

好,现在让我们开始吧!

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

开始制作

开始实作

首先,在此下载起始专案,这专案已有预先写好的 UI 元件及方法 (Method),这样我们就能够专注在储存 ARKit 世界地图的核心元件开发。当你下载完成之后,在 iOS 装置上建置并执行专案。过程中,一个视窗会弹出,询问你是否允许此 App 使用相机,点选 OK 来允许。

arkit-20-starter-proj

好了! 现在让我们谈一下甚麽是 ARWorldMap 物件,以及如何运用 ARWorldMap 物件来取得 ARKit 世界地图数据。

使用 ARWorldMap

ARWorldMap 物件包含所有空间地图信息的快照,在真实世界中,ARKit 也是使用这些信息来定位装置的所在位置。

ARWorldMap 物件所做的一如其名,它代表著现实世界中的一个地理位置。当你使用 ARWorldMap时,你可以将 ARWorldMap 物件转换成 Data 物件并储存,然后储存于装置目录裡。之后,你可以到当初储存世界地图 Data 物件的目录,将它解压转换回 ARWorldMap 物件。要恢复这地图,你需要将世界追踪配置 (Configuration) 的初始世界地图,设置为之前储存的 ARWorldMap 物件。

这就是我们使用 ARWorldMap 物件的方法。现在,让我们来实作一个范例 App。

设定世界地图本机文件目录 (Local Document Directory)

首先,宣告一个 URL 型别的变数,它为我们提供文件目录的存取路径,用于写入与读取世界地图数据。然后,为 ViewController 类别新增属性:

var worldMapURL: URL = {
    do {
        return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("worldMapURL")
    } catch {
        fatalError("Error getting world map URL from document directory.")
    }
}()
复制代码

指定好世界地图的 URL 后,让我们建立一个世界地图数据储存器 (Archiver) 方法,并写入我们本机文件目录中。

储存 AR 世界地图为数据

现在你将创建一个储存器方法,来储存你的 ARWorldMap 物件。插入下列程式码到 ViewController类别:

func archive(worldMap: ARWorldMap) throws {
    let data = try NSKeyedArchiver.archivedData(withRootObject: worldMap, requiringSecureCoding: true)
    try data.write(to: self.worldMapURL, options: [.atomic])
}
复制代码

储存了世界地图为数据物件后,把这数据物件写到本机目录下。我们使用 .atomic 选项 (Option),这样就能确定文档是否已完全写入到装置内。这方法包含签名 (Signature) 的 throws 叙述,因为在数据写入本机文件目录时,有可能因空间不足或其他缘故,而回传错误讯息。

完成世界地图储存器方法后,让我们实作从 Scene View 中储存世界地图。

将 AR 世界地图数据储存于本机文件目录

Apple 提供了一个方便的方法,协助我们取得当前 Session 的世界地图。利用下列的方式来更新 saveBarButtonItemDidTouch(_:)

@IBAction func saveBarButtonItemDidTouch(_ sender: UIBarButtonItem) {
    
    sceneView.session.getCurrentWorldMap { (worldMap, error) in
        guard let worldMap = worldMap else {
            return self.setLabel(text: "Error getting current world map.")
        }
        
        do {
            try self.archive(worldMap: worldMap)
            DispatchQueue.main.async {
                self.setLabel(text: "World map is saved.")
            }
        } catch {
            fatalError("Error saving world map: \(error.localizedDescription)")
        }
    }
}
复制代码

我们 Scene View 的 Session 包含一个方法,让我们轻易取得当前的世界地图。在 getCurrentWorldMap 闭包 (Closure) 中,我们安全地解开回传的可选性 ARWorldMap 物件。在 ARWorldMap 物件不再存在的情况下,我们将简单地回传并设置显示错误讯息的标籤文本 (Label Text)。

在安全解开 ARWorldMap 物件后,我们宣告一个 do-catch 叙述。如果错误讯息是由 do 中的程式码产生,我们会在 catch clause 中处理这个错误。此时,我们会中断执行程式码,印出错误讯息,以进行除错 (Debug)。

建置并执行 App,扫描你的执行环境,然后在装置上点击新增一个或多个球体到 Scene。点击 Save 按钮时,请确认你的标籤显示 “World map is saved” 字样。现在当前的 AR 世界地图已经储存好了。

从你的文件目录中下载 AR 世界地图数据

成功储存一个 ARWorldMap 物件后,现在我们要创建一个方法,来解开文件目录中的 ARWorldMap 数据,并将它下载到我们的 Scene 上。

在我们解压转换 DataARWorldMap 物件前,我们先要从文件目录中取得这世界地图的数据。

将下列方法新增到你的 ViewController 类别:

func retrieveWorldMapData(from url: URL) -> Data? {
    do {
        return try Data(contentsOf: self.worldMapURL)
    } catch {
        self.setLabel(text: "Error retrieving world map data.")
        return nil
    }
}
复制代码

上面的程式码宣告了一个 do-catch 叙述,以尝试从世界地图 URL 中取回 Data 物件。在程式码出现在 catch 句时,我们只需使用错误讯息设置标籤。

我们已写好了一个方法,帮助我们藉著世界地图 URL,从文件目录中读取这些数据。现在是时候试著去把回传的 Data 物件解压转换回 ARWorldMap 物件了。

首先,将下列方法新增到 ViewController 类别:

func unarchive(worldMapData data: Data) -> ARWorldMap? {
    guard let unarchievedObject = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data),
        let worldMap = unarchievedObject else { return nil }
    return worldMap
}
复制代码

我们用 NSKeyedUnarchiver 去尝试解开这数据物件的封包,然后传给 unarchive(worldMapData:)方法。如果封包的解压过程顺利,而且被解压的 ARWorldMap 物件不是 Nil,我们就可以安全地回传解压了的 ARWorldMap 物件。

现在解压方法已经准备好,让我们来以下列程式码更新 loadBarButtonItemDidTouch(_:) 吧!

@IBAction func loadBarButtonItemDidTouch(_ sender: UIBarButtonItem) {
    guard let worldMapData = retrieveWorldMapData(from: worldMapURL),
        let worldMap = unarchive(worldMapData: worldMapData) else { return }
    resetTrackingConfiguration(with: worldMap)
}
复制代码

现在无论我们何时点击 Load 按钮,我们都呼叫 retrieveWorldMapData 方法,来根据指定的 URL 位置取回世界地图数据。成功取回后,我们就解开世界地图数据的封包,将它转换为 ARWorldMap 物件。之后,我们呼叫 resetTrackingConfiguration 方法,把下载好的数据恢复成原来的 AR 世界地图。

设定 Scene View 配置中的初始世界地图

在宣告 options 常数后面,新增下列程式码到 resetTrackingConfiguration(with:) 中:  

    if let worldMap = worldMap {
        configuration.initialWorldMap = worldMap
        setLabel(text: "Found saved world map.")
    } else {
        setLabel(text: "Move camera around to map your surrounding space.")
    }
复制代码

上述程式码将 Scene View 配置的初始世界地图设置为世界地图参数 (Parameter)。 然后我们更新文字标籤,显示世界地图已经被找到了;反之,我们就会设置一个文字标籤,引导使用者用相机拍下周遭环境,建立图档。

完成了! 让我们展示范例吧!

演示

在展示范例的视频中,使用者点击了萤幕来新增一个球体到 Scene View。 然后,使用者按下 Save 按钮来储存 Scene View 当前的世界地图。成功储存后,文字标籤会显示 “World map is saved”。 使用者点击 Load 按钮后,这被储存的世界地图就会成功地被载到 Scene View 上。

很有趣吧?

总结

恭喜你读完了本次教学! 希望你在阅读本篇教程中获得乐趣,并有所收穫。 欢迎将教学分享到社交平台上,让朋友圈也能获得有用的知识!

如果有兴趣了解更多,可以在 GitHub 下载完整的 Xcode 专案。

文末推荐:iOS热门文集

文章分类
iOS