前言
對(duì)ARKit感興趣的同學(xué),可以訂閱ARKit教程專(zhuān)題
源代碼地址在這里
正文
在之前的章節(jié),我們學(xué)習(xí)了:
- 檢測(cè)一個(gè)矩形。
- 檢測(cè)一個(gè)QR碼。
- 在檢測(cè)到的矩形和QR碼上面添加一個(gè)平面。
- 在平面上面顯示內(nèi)容。
在這一章,我們將會(huì)學(xué)到:
- 通過(guò)使用故事板而不是獨(dú)立的視圖控制器來(lái)改善用戶交互。
- 切換全屏模式。
- 檢測(cè)預(yù)定義的圖像。
我們會(huì)采用上一章的代碼繼續(xù)做開(kāi)發(fā)。不過(guò)我們需要移除掉一些無(wú)用的代碼。
首先,我們把BillboardViewController.xib移除掉,然后再把BillboardView.swift.移除掉。
打開(kāi)BillboardViewController.swift并刪除對(duì)剛剛刪除的BillboardView類(lèi)的引用。此外,刪除BillboardViewController類(lèi)中的其余代碼,但保留其定義。你最終會(huì)得到一個(gè)像這樣的空類(lèi):
class BillboardViewController: UIViewController {
}
接下來(lái),刪除實(shí)現(xiàn)UICollectionViewDelegateFlowLayout協(xié)議的擴(kuò)展。
打開(kāi)ViewController.swift并找到setBillboardImages(_ :)。找到后,刪除整個(gè)方法。
此外,我們可以刪除BillboardViewDelegate協(xié)議的擴(kuò)展名。找到以下代碼并將其刪除:
extension ViewController: BillboardViewDelegate {
func billboardViewDidSelectPlayVideo(
_ view: BillboardView) {
createVideo()
}
}
最后,找到addBillboardNode()。刪除以下代碼行:
let images = [
"logo_1", "logo_2", "logo_3", "logo_4", "logo_5"
].map { UIImage(named: $0)! }
setBillboardImages(images)
之后我們需要添加一些新的文件。我們首先創(chuàng)建一個(gè)Billboard.storyboard
請(qǐng)注意,它由導(dǎo)航控制器和具有三個(gè)不同自定義單元的集合視圖控制器組成。

以上三個(gè)cell用于顯示:
- 視頻播放器
- 圖片
- 網(wǎng)頁(yè)視圖
為此,我們需要?jiǎng)?chuàng)建三個(gè)cell。
-
ImageCell:用于顯示一張圖片。
import UIKit class ImageCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! func show(image: UIImage) { imageView.image = image } } -
WebBrowserCell: 用于展示一個(gè)網(wǎng)頁(yè)視圖。
import UIKit class WebBrowserCell: UICollectionViewCell { @IBOutlet weak var webBrowser: UIWebView! func go(to urlString: String) { guard let url = URL(string: urlString) else { return } let request = URLRequest(url: url) webBrowser.loadRequest(request) } } -
VideoCell:用于播放視頻。
import UIKit import SpriteKit import AVFoundation import ARKit class VideoCell: UICollectionViewCell { @IBOutlet weak var playButton: UIButton! @IBOutlet weak var playerContainer: UIView! func configure(videoUrl: String, sceneView: ARSCNView, billboard: BillboardContainer) { } @IBAction func play() { } }
創(chuàng)建基于故事板的廣告牌
新廣告牌使用故事板,因此它與之前的實(shí)現(xiàn)有很大不同。打開(kāi)ViewController.swift并滾動(dòng)到renderer(_:nodeFor:)。
在上一次迭代中,我們?cè)诶锩鎰?chuàng)建了BillboardViewController
addBillboardNode()。由于實(shí)現(xiàn)現(xiàn)在更復(fù)雜,而不是再次將代碼添加到該方法,最好使用新方法。
在let billboardNode = addBillboardNode()之后添加如下代碼:
createBillboardController()
在上一次迭代中,我們?cè)?strong>BillboardViewController里面創(chuàng)建了
addBillboardNode()方法。由于實(shí)現(xiàn)現(xiàn)在更復(fù)雜,而不是再次將代碼添加到該方法,最好使用新方法。
func createBillboardController() {
// 1
DispatchQueue.main.async {
// 2
let navController = UIStoryboard(name: "Billboard", bundle: nil) .instantiateInitialViewController() as! UINavigationController
// 3
let billboardViewController = navController.visibleViewController as! BillboardViewController
// 4
billboardViewController.sceneView = self.sceneView billboardViewController.billboard = self.billboard
// 5
billboardViewController.willMove( toParentViewController: self) self.addChildViewController(billboardViewController) self.view.addSubview(billboardViewController.view)
// 6
self.show(viewController: billboardViewController)
}
}
上面的代碼作用如下:
- 1: 切換到主線程,以便我們可以處理用戶界面更新。
- 2: 創(chuàng)建Billboard故事板的初始視圖控制器的實(shí)例,它是一個(gè)導(dǎo)航控制器。這里的強(qiáng)制轉(zhuǎn)換是一個(gè)很好的調(diào)試工具:如果應(yīng)用程序崩潰,則意味著存在開(kāi)發(fā)錯(cuò)誤 - 初始視圖控制器很可能不是預(yù)期的導(dǎo)航控制器。
- 3: 導(dǎo)航控制器的根視圖控制器是BillboardViewController。再次強(qiáng)制轉(zhuǎn)換非常方便,以防我們?cè)诠适掳逯懈哪承﹥?nèi)容并忘記更新代碼。
- 4: 廣告牌將使用場(chǎng)景視圖和廣告牌容器,因此最好在這里設(shè)置它們;但是,我們必須添加兩個(gè)相應(yīng)的屬性。
- 5: 要在顯示新視圖控制器之前準(zhǔn)備它,我們需要:
??a:告訴新視圖控制器它將移動(dòng)到ViewController。
??b: 將新視圖控制器添加到ViewController。
??c: 將新視圖控制器的視圖添加到ViewController的視圖中作為子視圖。 - 6: 新視圖控制器已準(zhǔn)備好顯示,因此我們將其傳遞給show(viewController:)。
這樣就完成了廣告牌的創(chuàng)建;但是,show方法尚未實(shí)現(xiàn)。在剛剛創(chuàng)建的方法之后添加如下代碼:
private func show(viewController: BillboardViewController) {
let material = SCNMaterial() material.isDoubleSided = true
material.cullMode = .front
material.diffuse.contents = viewController.view
billboard?.viewController = viewController
billboard?.billboardNode?.geometry?.materials = [material]
}
這與我們?cè)谏弦徽碌?strong>setBillboardImages()中所做的類(lèi)似。
打開(kāi)BillboardViewController,添加如下代碼:
var sceneView: ARSCNView?
var billboard: BillboardContainer?
構(gòu)建并運(yùn)行應(yīng)用程序。注意一旦檢測(cè)到QR碼,應(yīng)用程序就會(huì)崩潰。看看Xcode控制臺(tái)會(huì)發(fā)現(xiàn)一個(gè)無(wú)法識(shí)別的選擇器被發(fā)送到廣告牌視圖控制器:
[RazeAd.BillboardViewController collectionView:numberOfItemsInSection:]: unrecognized selector sent to instance 0x1030e4600
RazeAd[3180:1334506] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RazeAd.BillboardViewController collectionView:numberOfItemsInSection:]:unrecognized selector sent to instance 0x1030e4600'
發(fā)生這種情況是因?yàn)楣适掳逍枰獜V告牌視圖控制器是UICollectionViewController的子類(lèi),當(dāng)我們打開(kāi)故事板文件時(shí)可能會(huì)注意到它。
這是一個(gè)簡(jiǎn)單的方法:修改BillboardViewController類(lèi)并使其繼承自UICollectionViewController而不是UIViewController:
class BillboardViewController: UICollectionViewController
與廣告牌互動(dòng)
加載廣告牌故事板后,就可以專(zhuān)注于在Billboard ViewController類(lèi)中進(jìn)行連接。
最初的步驟是提供集合視圖;這意味著覆蓋UICollectionViewDataSource協(xié)議定義的一些方法。
打開(kāi)BillboardViewController.swift并且在最后添加如下代碼:
// UICollectionViewDataSource
extension BillboardViewController {
}
我們需要告訴集合視圖三件事:每個(gè)部分的部分?jǐn)?shù)量,每個(gè)部分的項(xiàng)目數(shù)以及每個(gè)項(xiàng)目要顯示的單元格。
在擴(kuò)展中添加如下代碼:
override func numberOfSections(
in collectionView: UICollectionView) -> Int {
return 3
}
每個(gè)單元格類(lèi)型都作為單獨(dú)的部分處理。這在處理圖像序列時(shí)很有用。
接下來(lái)是在每個(gè)case下面添加以下方法:
override func collectionView(
_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let currentSection = Section(rawValue: section) else { return 0 }
switch currentSection {
case .images:
return images.count case .video:
return 1 case .webBrowser:
return 1
}
}
只有一個(gè)視頻和一個(gè)Web瀏覽器,因此對(duì)于這些,我們返回1。但是,可能有多個(gè)圖像,因此我們返回images.count以確定需要多少行。
最后,我們必須添加返回給定索引路徑的單元格的方法:
override func collectionView(
_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// 1
guard let currentSection =
Section(rawValue: indexPath.section) else { fatalError("Unexpected collection view section") }
// 2
let cellType: Cell switch currentSection {
case .images:
cellType = .cellImage case .video:
cellType = .cellVideo case .webBrowser:
cellType = .cellWebBrowser }
// 3
let cell = collectionView.dequeueReusableCell( withReuseIdentifier: cellType.rawValue, for: indexPath)
// 4
switch cell {
case let imageCell as ImageCell:
let image = UIImage(named: images[indexPath.row])! imageCell.show(image: image)
case let videoCell as VideoCell:
let videoUrl = "https://www.rmp-streaming.com/media/bbb-360p.mp4" if let sceneView = sceneView, let billboard = billboard { videoCell.configure( videoUrl: videoUrl, sceneView: sceneView, billboard: billboard ) } break
case let webBrowserCell as WebBrowserCell:
webBrowserCell.go(to: "https://www.raywenderlich.com")
default:
fatalError("Unrecognized cell") }
return cell
}
這是一個(gè)很長(zhǎng)的,雖然簡(jiǎn)單的實(shí)現(xiàn):
- 1:Section枚舉用于標(biāo)識(shí)每個(gè)部分。如果其原始值無(wú)法識(shí)別某個(gè)部分,則會(huì)拋出fatalError。
- 2: 另一個(gè)名為Cell的枚舉,用于識(shí)別細(xì)胞。
- 3: Cell枚舉使用String類(lèi)型的原始值來(lái)定義單元標(biāo)識(shí)符。指定索引路徑的單元格已出列。
- 4: 我們可以根據(jù)細(xì)胞類(lèi)型使用適當(dāng)?shù)臄?shù)據(jù)配置細(xì)胞。
我們可能已經(jīng)注意到圖像單元格的代碼引用了不存在的圖像屬性。此屬性將包含要在廣告牌中顯示的圖像名稱(chēng)列表。
在billboard屬性之后,添加以下內(nèi)容:
var images: [String] = [ "logo_1", "logo_2", "logo_3", "logo_4", "logo_5" ]
目前,我們已完成廣告牌視圖控制器。構(gòu)建并運(yùn)行應(yīng)用程序。

第一個(gè)單元是視頻播放器 - 它還沒(méi)有實(shí)現(xiàn),所以不要指望它能夠工作。但是,我們可以向右滑動(dòng)以顯示圖像,并且可以訪問(wèn)最后一個(gè)單元格中的Web瀏覽器,該單元格將加載www.raywenderlich.com。
顯示視頻播放器
視頻單元實(shí)現(xiàn)
打開(kāi)VideoCell.swift添加如下代碼:
// 1
var isPlaying = false
// 2
var videoNode: SKVideoNode!
var spriteScene: SKScene!
// 3
var videoUrl: String!
// 4
var player: AVPlayer?
// 5
weak var billboard: BillboardContainer?
weak var sceneView: ARSCNView?
上面代碼作用如下:
- 1: isPlaying是指示視頻當(dāng)前是否正在播放的標(biāo)志。
- 2: 在創(chuàng)建過(guò)程中,將ARKit節(jié)點(diǎn)存儲(chǔ)到videoNode中,將SpriteKit場(chǎng)景存儲(chǔ)到spriteScene中。
- 3: 這包含目標(biāo)視頻的URL。
- 4: 這是負(fù)責(zé)播放視頻的AVPlayer。
- 5: 這些是對(duì)廣告牌容器和ARKit場(chǎng)景視圖的引用。
其中一些屬性可以在configure方法中初始化,該方法當(dāng)前為空。將此內(nèi)容添加到其正文:
self.videoUrl = videoUrl
self.billboard = billboard
self.sceneView = sceneView
要播放視頻,用戶將點(diǎn)按“播放”按鈕;這個(gè)按鈕已經(jīng)連接到空的play()動(dòng)作方法,我們只需要提供實(shí)現(xiàn)。添加以下代碼:
guard let billboard = billboard else { return }
if billboard.isFullScreen {
} else {
// 1
createVideoPlayerAnchor()
// 2
billboard.videoPlayerDelegate?.didStartPlay()
// 3
playButton.isEnabled = false
}
上面代碼作用如下:
- 1: 創(chuàng)建視頻播放器錨點(diǎn)。
- 2: 通知代表視頻開(kāi)始播放。
- 3: 禁用播放按鈕以防止雙擊觸發(fā)相同的操作兩次。
廣告牌容器沒(méi)有isFullScreen屬性,但我們可以快速添加它。打開(kāi)BillboardContainer.swift并添加以下內(nèi)容:
var isFullScreen = false
視頻播放器代理方法
在最后一個(gè)代碼段中,我們通過(guò)委托方法調(diào)用發(fā)送了通知;此委托存儲(chǔ)在廣告牌容器中,因此我們需要添加協(xié)議定義。仍然在BillboardContainer.swift中,在import語(yǔ)句之后添加以下內(nèi)容:
protocol VideoPlayerDelegate: class {
func didStartPlay() func didEndPlay()
}
這兩種方法用于在視頻開(kāi)始或停止時(shí)通知應(yīng)用。
現(xiàn)在,我們需要保留對(duì)要通知的目標(biāo)實(shí)體的引用。將此屬性添加到BillboardContainer:
weak var videoPlayerDelegate: VideoPlayerDelegate?
要使代理有效,必須將此屬性設(shè)置為要通知的實(shí)例。在這種情況下,實(shí)例是ViewController,因?yàn)樗潜仨殞?duì)視頻播放器狀態(tài)的變化作出反應(yīng)的實(shí)體。
打開(kāi)ViewController.swift并找到createBillboard,然后在廣告牌實(shí)例化后添加如下代碼:
billboard?.videoPlayerDelegate = self
除了設(shè)置代理之外,還必須實(shí)現(xiàn)它。在文件的末尾,添加以下代碼以處理視頻播放器狀態(tài)更改:
extension ViewController: VideoPlayerDelegate {
func didStartPlay() {
// 1
billboard?.billboardNode?.isHidden = true
}
func didEndPlay() {
// 2
billboard?.billboardNode?.isHidden = false
}
}
上面代碼作用如下:
- 1: 在視頻播放器啟動(dòng)時(shí)隱藏廣告牌。
- 2: 視頻播放器停止時(shí)再次顯示廣告牌。
回到視頻單元格
在VideoCell的play()處理程序中調(diào)用的createVideoPlayerAnchor()方法仍未實(shí)現(xiàn)。打開(kāi)VideoCell.swift,在configure()方法之后添加以下內(nèi)容:
func createVideoPlayerAnchor() {
guard let billboard = billboard else { return }
guard let sceneView = sceneView else { return }
// 1
let center = billboard.plane.center *matrix_float4x4(SCNMatrix4MakeRotation( Float.pi / 2.0, 0.0, 0.0, 1.0))
let anchor = ARAnchor(transform: center)
// 2
sceneView.session.add(anchor: anchor)
// 3
billboard.videoAnchor = anchor
}
上面代碼作用如下:
- 1: 創(chuàng)建一個(gè)以廣告牌中心為中心的新錨點(diǎn),并圍繞z軸應(yīng)用通常的90度旋轉(zhuǎn)。
- 2: 將新錨添加到ARKit場(chǎng)景。
- 3: 保留對(duì)錨點(diǎn)的引用,因?yàn)樯院髮⑿枰玫剿?/li>
視頻播放器創(chuàng)建邏輯更新
我們需要添加一些代碼來(lái)將新創(chuàng)建的錨與SceneKit節(jié)點(diǎn)相關(guān)聯(lián)。 ARSCNView的委托是ViewController類(lèi),因此我們必須在其中創(chuàng)建新節(jié)點(diǎn)。但是,為了保持一致性,最好將該任務(wù)保留在VideoCell中,最簡(jiǎn)單的方法是通過(guò)委托實(shí)現(xiàn)。
打開(kāi)ViewController.swift并找到renderer(_:nodeFor)方法。在switch語(yǔ)句中,我們將看到已經(jīng)存在處理視頻節(jié)點(diǎn)創(chuàng)建的情況;但是,它使用的是舊實(shí)現(xiàn)。
我們需要?jiǎng)h除包含以下方法的實(shí)現(xiàn):
- addVideoPlayerNode()
- createVideo()
- removeVideo()
找到這三種方法并且刪除它們。
現(xiàn)在,我們?cè)?strong>renderer方法中,找到如下代碼:
node = addVideoPlayerNode()
把這一行代碼替換為:
node = billboard.videoNodeHandler?.createNode()
如前所述,我們將使用代理來(lái)公開(kāi)視頻單元格方法。打開(kāi)BillboardContainer.swift,在videoPlayerDelegate屬性之前添加:
weak var videoNodeHandler: VideoNodeHandler?
要關(guān)閉循環(huán),我們需要定義VideoNodeHandler協(xié)議。在import語(yǔ)句后添加如下代碼:
protocol VideoNodeHandler: class {
func createNode() -> SCNNode?
func removeNode()
}
上面代碼作用如下:
- createNode(): 當(dāng)廣告視圖控制器需要視頻播放器的新SCNNode時(shí),由廣告視圖控制器調(diào)用。
- removeNode(): 必須刪除節(jié)點(diǎn)時(shí)調(diào)用。
打開(kāi)VideController.swift并在touchesBegun(_:with :)中找到有問(wèn)題的代碼行。將其替換為以下內(nèi)容:
billboard?.videoNodeHandler?.removeNode()
刪除廣告牌后,我們需要重置之前添加的videoNodeHandler屬性。
找到removeBillboard()并在最后一行之前插入以下代碼行,其中billboard屬性重置為nil:
billboard?.videoNodeHandler = nil
運(yùn)行應(yīng)用程序。然后掃描QR碼,當(dāng)前的效果是點(diǎn)擊視頻播放器中的播放按鈕......廣告牌消失,但不顯示視頻。
實(shí)現(xiàn)視頻節(jié)點(diǎn)處理程序
打開(kāi)VideoCell.swift。在configure(videoUrl:sceneView:billboard :)結(jié)束時(shí),設(shè)置屬性:
billboard.videoNodeHandler = self
VideoHandlerProtocol分別定義了兩種方法來(lái)為視頻播放器創(chuàng)建和刪除SceneKit節(jié)點(diǎn)。
完成后,構(gòu)建并運(yùn)行應(yīng)用程序。掃描二維碼后,點(diǎn)擊播放按鈕;廣告牌消失,并由顯示視頻的簡(jiǎn)約視頻播放器取代。

點(diǎn)按屏幕上的任意位置即可關(guān)閉視頻播放器并返回廣告牌。
全屏
觸發(fā)全屏模式
我們可以通過(guò)多種方式使用全屏模式,例如使用按鈕或放大/縮小手勢(shì)。也可以使用雙擊,這個(gè)項(xiàng)目中,我們就采用雙擊手勢(shì)。
打開(kāi)BillboardViewController.swift并且在images屬性之后添加如下代碼:
// 1
let doubleTapGesture = UITapGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
// 2
doubleTapGesture.numberOfTapsRequired = 2 doubleTapGesture.addTarget( self, action: #selector(didDoubleTap)) view.addGestureRecognizer(doubleTapGesture)
}
// 3
@objc func didDoubleTap() {
guard let billboard = billboard else { return }
if billboard.isFullScreen {
restoreFromFullScreen()
} else {
showFullScreen()
}
}
如果之前使用過(guò)手勢(shì),那么這些代碼可能看起來(lái)很熟悉。我們正在使用點(diǎn)擊手勢(shì)識(shí)別器來(lái)檢測(cè)廣告牌視圖控制器上的雙擊:
- 1: 此屬性用于保留對(duì)手勢(shì)的引用,以防我們要禁用或以其他方式更改手勢(shì)。
- 2: 我們?cè)趘iewDidLoad()中配置并激活點(diǎn)擊手勢(shì)。 numberOfTapsRequired設(shè)置為2**表示我們要捕獲雙擊。
- 3: 在雙擊處理程序中,切換全屏模式。這兩種情況都是由接下來(lái)要實(shí)施的各種方法處理的。
進(jìn)入全屏
當(dāng)廣告牌以ARKit模式顯示時(shí),其導(dǎo)航控制器 - 以及整個(gè)堆棧 - 是ViewController的子節(jié)點(diǎn),因?yàn)檫@是顯示ARKit的視圖控制器。要進(jìn)入全屏模式,我們必須將廣告牌的視圖與其超級(jí)視圖分離,并將其附加到父視圖控制器的視圖中。
創(chuàng)建廣告牌視圖控制器時(shí),我們將其視圖添加到ViewController的show(viewController :)方法中的SCNMaterial:
private func show(viewController: BillboardViewController) {
let material = SCNMaterial()
...
material.diffuse.contents = viewController.view ...
}
在BillboardViewController的末尾,添加以下代碼:
extension BillboardViewController {
func showFullScreen() {
guard let billboard = billboard else { return }
guard billboard.isFullScreen == false else { return }
// 1
guard let mainViewController = parent as? ViewController else { return }
self.mainViewController = mainViewController mainView = view.superview
// 2
willMove(toParentViewController: nil)
view.removeFromSuperview()
removeFromParentViewController()
// 3
willMove(toParentViewController: mainViewController)
mainViewController.view.addSubview(view)
mainViewController.addChildViewController(self)
// 4
billboard.isFullScreen = true
}
}
上面作用如下:
- 1; 保持對(duì)父視圖控制器和超級(jí)視圖的引用。退出全屏模式時(shí),我們需要它們才能恢復(fù)視圖。
- 2: 從各自的父母中刪除視圖控制器及其視圖。
- 3: 再次將視圖控制器添加到其上一個(gè)父級(jí),并將其視圖添加到父級(jí)視圖(而不是SCNMaterial)。
- 4: 設(shè)置標(biāo)記以跟蹤全屏模式是打開(kāi)還是關(guān)閉。
現(xiàn)在,我們需要添加用于跟蹤原始父視圖控制器和父視圖的兩個(gè)屬性。在廣告牌屬性后添加它們:
weak var mainViewController: AdViewController?
weak var mainView: UIView?
退出全屏
要退出全屏模式,必須執(zhí)行相反操作:從ViewController視圖中刪除視圖,并在進(jìn)入全屏模式之前將其放回視圖所具有的任何父類(lèi)。
回想一下,我們?cè)?strong>mainView屬性中保存了這個(gè)值。
打開(kāi)BillboardViewController.swift文件,在showFullScreen()之后添加此代碼:
func restoreFromFullScreen() {
guard let billboard = billboard else { return }
guard billboard.isFullScreen == true else { return }
guard let mainViewController = mainViewController else { return }
guard let mainView = mainView else { return }
// 1
willMove(toParentViewController: nil)
view.removeFromSuperview()
removeFromParentViewController()
// 2
willMove(toParentViewController: mainViewController)
mainView.addSubview(view) mainViewController.addChildViewController(self)
// 3
billboard.isFullScreen = false
self.mainViewController = nil
self.mainView = nil
}
上面的代碼作用如下:
- 1: 從父視圖中刪除BillboardViewController的視圖,在本例中是ViewController的視圖。
- 2:將視圖放回到原始視圖中,該視圖在全屏顯示時(shí)存儲(chǔ)在mainView屬性中。
- 3: 重置全屏圖像,以及對(duì)主視圖控制器和主視圖的引用。
運(yùn)行應(yīng)用程序并執(zhí)行常規(guī)的QR檢測(cè)。然后,雙擊廣告牌以轉(zhuǎn)換到全屏。

到此為止,視頻播放器還不能用。
修復(fù)視頻播放器
打開(kāi)VideoCell.swift。在play()中,將if billboard.isFullScreen分支留空,插入以下代碼:
if isPlaying == false {
// 1
createVideoPlayerView()
playButton.setImage( #imageLiteral(resourceName: "arKit-pause"), for: .normal)
} else {
// 2
stopVideo()
playButton.setImage( #imageLiteral(resourceName: "arKit-play"), for: .normal)
}
// 3
isPlaying = !isPlaying
上面的代碼作用如下:
1: 如果沒(méi)有播放視頻,請(qǐng)創(chuàng)建視頻播放器視圖并更改按鈕的圖標(biāo)以顯示paused圖像。
2: 如果當(dāng)前正在播放視頻,請(qǐng)將其停止并將按鈕的圖標(biāo)恢復(fù)為play圖像。
3: 切換isPlaying標(biāo)志,以跟蹤視頻播放狀態(tài)。
接下來(lái)需要做的是創(chuàng)建視頻播放器。在createVideoPlayerAnchor()之后添加如下代碼:
func createVideoPlayerView() {
if player == nil {
guard let url = URL(string: videoUrl) else { return }
player = AVPlayer(url: url)
let layer = AVPlayerLayer(player: player)
layer.frame = playerContainer.bounds
playerContainer.layer.addSublayer(layer)
}
player?.play()
}
如果不存在視頻播放器我們會(huì)創(chuàng)建一個(gè)視頻播放器并且開(kāi)始播放。接下來(lái),我們還需要一個(gè)停止播放的方法:
func stopVideo() {
player?.pause()
}
上面的這個(gè)方法會(huì)讓播放器暫停播放。
構(gòu)建并運(yùn)行應(yīng)用程序。掃描QR碼并雙擊廣告牌進(jìn)入全屏模式。然后,點(diǎn)擊play按鈕播放視頻;再次點(diǎn)擊它可暫停視頻。

檢測(cè)參考圖像
ARKit 1.5引入了一些新功能:能夠檢測(cè)自定義圖像。因此,我們可能希望讓?xiě)?yīng)用程序識(shí)別一個(gè)或多個(gè)預(yù)定義圖像,而不是檢測(cè)白色矩形或QR碼。
在下一節(jié)中,我們將通過(guò)使用自定義圖像檢測(cè)替換QR代碼檢測(cè)。但是,要實(shí)現(xiàn)此目的,有兩個(gè)先決條件:
- Xcode版本大于或等于9.3。
- iOS 系統(tǒng)版本大于或等于11.3。
我們添加一個(gè)或多個(gè)參考圖像。
選擇參考圖像
第一步是讓?xiě)?yīng)用知道需要檢測(cè)哪些圖像。在Xcode中,打開(kāi)Assets.xcassets目錄,然后單擊位于窗口右下角的+按鈕。將顯示一個(gè)彈出菜單,其中包含一長(zhǎng)串選項(xiàng)。找到這兩個(gè):New AR Resource Group和New AR Reference Image。

我們將使用前者創(chuàng)建組,后者將新圖像添加到AR資源組。 AR資源組是為ARKit創(chuàng)建的特殊組類(lèi)型。它在運(yùn)行時(shí)加載,ARKit*使用它來(lái)確定它應(yīng)該檢測(cè)哪些圖像。
創(chuàng)建一個(gè)新的AR資源組并將其命名為RMK-ARKit-triggers。然后,將圖像添加到新創(chuàng)建的組:
- 1: 右鍵選擇logo_3圖片
- 2: 選擇Show in Finder。
- 3: 在Xcode打開(kāi)的Finder窗口中,拖動(dòng)圖像文件并將其放入RMK-ARKit-triggers組。
- 4: 對(duì)logo_4圖像重復(fù)步驟1-3。
選擇組中兩個(gè)圖像之一,然后查看“屬性”檢查器。除了圖像名稱(chēng),它還包含兩個(gè)可編輯的參數(shù):大小和度量單位。
尺寸和度量單位屬性用于為現(xiàn)實(shí)世界中的圖像的物理尺寸提供ARKit。 ARKit使用這些測(cè)量值來(lái)確定相機(jī)的正確視角和距離。
為寬度和高度輸入0.2,并驗(yàn)證Meters是選定的度量單位,如下頁(yè)所示。如果在A4或Letter紙張上最大化打印,這大致是圖像的大小。重復(fù)其他圖像。

請(qǐng)注意,兩個(gè)圖像現(xiàn)在在其各自的右下角顯示警告圖標(biāo)。將鼠標(biāo)懸停在警告圖標(biāo)上,然后單擊以顯示更多信息:

上面警告包含的意思如下:
- 1: 圖像彼此太相似了。
- 2: 圖像顏色直方圖不足夠,這意味著沒(méi)有均勻的顏色分布。
- 3: 圖像具有相同顏色的大部分。
看看你添加的圖像,它們并不完全符合第二和第三個(gè)要求 - 但不要擔(dān)心,這些圖片能用。
注冊(cè)參考圖像
此時(shí),應(yīng)用程序不知道這些圖像的存在 - 你必須告訴它們。這是在ARKit配置期間完成的。
打開(kāi)ViewController.swift并導(dǎo)航到viewWillAppear(_ :)。使用以下代碼行創(chuàng)建配置:
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
configuration.worldAlignment = .camera
// Run the view's session
sceneView.session.run(configuration)
ARWorldTrackingConfiguration繼承的ARConfiguration類(lèi)具有名為detectionImages的屬性;我們可以使用此屬性來(lái)指定希望ARKit識(shí)別的一組圖像。
可檢測(cè)圖像捆綁在ARReferenceImage的實(shí)例中,該實(shí)例添加了一些元數(shù)據(jù),即:
- name。
-
physicalSize。
如前所述,在創(chuàng)建AR資源組時(shí),可以分配兩者。
在上面顯示的代碼段中,在運(yùn)行會(huì)話的最后一行之前,插入此代碼以加載和設(shè)置參考圖像:
// 1
var triggerImages = ARReferenceImage.referenceImages( inGroupNamed: "RMK-ARKit-triggers", bundle: nil)
// 2
configuration.detectionImages = triggerImages
第一行加載之前添加到RMK-ARKit-triggers AR資源組的圖像。第二行將這些圖像分配給配置。
需要注意的是referenceImages(inGroupNamed:bundle :)如何返回Set <ARReferenceImage>而不是數(shù)組。
提供一個(gè)hook
現(xiàn)在,我們需要ARKit在識(shí)別參考圖像時(shí)通知應(yīng)用程序。識(shí)別后,ARKit會(huì)自動(dòng)為會(huì)話添加錨點(diǎn)。
添加錨點(diǎn)時(shí),有三種可能的方法可以通知:
ARSessionDelegate委托的session(_:didAdd :)。
查看(_:didAdd:for :) ARSKViewDelegate委托。
-
ARSCNViewDelegate委托的rendered(_:didAdd:for:)。
sceneView.session.delegate = self
向下滾動(dòng)以找到ARSessionDelegate擴(kuò)展,并在最后添加:
// 1
func session(_ session: ARSession,
didAdd anchors: [ARAnchor]) {
// 2
if let imageAnchor = anchors.compactMap({ $0 as? ARImageAnchor }).first {
// 3
self.createBillboard(center: imageAnchor.transform,size: imageAnchor.referenceImage.physicalSize)
}
}
上面代碼作用如下:
- 1: 當(dāng)錨點(diǎn)添加到會(huì)話時(shí),ARKit調(diào)用此委托方法;識(shí)別參考圖像時(shí)創(chuàng)建新錨點(diǎn)。
- 2: 由于該方法接收到一組錨點(diǎn),因此我們首先按類(lèi)型過(guò)濾它們并僅保留ARImageAnchor的實(shí)例;然后你拿第一個(gè)。
- 3: 我們可以使用該錨點(diǎn)創(chuàng)建廣告牌。
打開(kāi)AdViewController.swift文件,找到createBillboard()的當(dāng)前實(shí)現(xiàn),并在它之后添加此重載:
func createBillboard(center: matrix_float4x4, size: CGSize) {
// 1
let plane = RectangularPlane(center: center, size: size)
// 2
let rotation = SCNMatrix4MakeRotation(Float.pi / 2, -1.0, 0.0, 0.0)
// 3
let rotatedCenter = plane.center * matrix_float4x4(rotation)
let anchor = ARAnchor(transform: rotatedCenter)
billboard = BillboardContainer( billboardAnchor: anchor, plane: plane)
billboard?.videoPlayerDelegate = self
sceneView.session.add(anchor: anchor)
print("New billboard created")
}
上面代碼作用如下:
- 1: 我們不必使用4個(gè)矩形頂點(diǎn),而是具有參考圖像的中心,因此我們可以使用RectangularPlane初始化程序的正確重載。
- 2: 旋轉(zhuǎn)矩陣是不同的。而不是圍繞z軸旋轉(zhuǎn)90度,我們需要旋轉(zhuǎn)相同的度數(shù),但圍繞x軸,以及相反的方向。這是SceneKit節(jié)點(diǎn)通常需要的調(diào)整。
- 3: 方法的其余部分與另一個(gè)相同。
注意:如果參考圖像的大小不正確,ARKit無(wú)論如何都會(huì)識(shí)別它。但是,觀點(diǎn)可能會(huì)發(fā)生變化。如果圖像較大,ARKit可能認(rèn)為它比它更接近,而如果它更小,它可以看得更遠(yuǎn)。
在運(yùn)行時(shí)添加參考圖像
能夠檢測(cè)預(yù)定義圖像非常棒,但是必須將其與應(yīng)用程序捆綁在一起可能是一個(gè)相當(dāng)大的限制,因?yàn)橐滤?,我們必須發(fā)布新版本的應(yīng)用程序。
在ViewController.swift中,向上滾動(dòng)到viewWillAppear(_ :)并找到創(chuàng)建triggerImages變量的行。在該行之后添加此代碼:
// 1
let image = UIImage(named: "logo_2")!
// 2
let referenceImage = ARReferenceImage(image.cgImage!, orientation: .up, physicalWidth: 0.2)
// 3
triggerImages?.insert(referenceImage)
上面的代碼作用如下:
- 1: 從圖像資源中找到的logo_2圖像創(chuàng)建UIImage。
- 2: 為它創(chuàng)建一個(gè)參考圖像,通過(guò)0.2米的物理寬度。系統(tǒng)計(jì)算物理高度。
- 3: 將新創(chuàng)建的參考圖像添加到加載到triggerImages變量中的組。
參考圖像檢測(cè)是不同的用例,并不代替QR碼檢測(cè)。使用QR碼,我們可以存儲(chǔ)附加值的自定義數(shù)據(jù)。
| 上一章 | 目錄 | 下一章 |
|---|