ARKit教程16_第十二章:高級(jí)用戶(hù)交互

前言

對(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)改善用戶(hù)交互。
  • 切換全屏模式季蚂。
  • 檢測(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: 切換到主線(xiàn)程,以便我們可以處理用戶(hù)界面更新惨险。
  • 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)以顯示圖像,并且可以訪(fǎ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

要播放視頻膀斋,用戶(hù)將點(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í)再次顯示廣告牌。

回到視頻單元格

VideoCellplay()處理程序中調(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í)吵瞻,我們將其視圖添加到ViewControllershow(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 GroupNew 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è)所示蛀蜜。如果在A4Letter紙張上最大化打印,這大致是圖像的大小增蹭。重復(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ì)話(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ì)話(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ì)話(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)橐滤牵覀儽仨毎l(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ù)阳准。

上一章 目錄 下一章
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馏臭,隨后出現(xiàn)的幾起案子野蝇,更是在濱河造成了極大的恐慌,老刑警劉巖括儒,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绕沈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡帮寻,警方通過(guò)查閱死者的電腦和手機(jī)乍狐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)固逗,“玉大人浅蚪,你說(shuō)我怎么就攤上這事√陶郑” “怎么了惜傲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)贝攒。 經(jīng)常有香客問(wèn)我盗誊,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任哈踱,我火速辦了婚禮荒适,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘开镣。我一直安慰自己吻贿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布哑子。 她就那樣靜靜地躺著舅列,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卧蜓。 梳的紋絲不亂的頭發(fā)上帐要,一...
    開(kāi)封第一講書(shū)人閱讀 48,954評(píng)論 1 283
  • 那天,我揣著相機(jī)與錄音弥奸,去河邊找鬼榨惠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盛霎,可吹牛的內(nèi)容都是我干的赠橙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼愤炸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼期揪!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起规个,我...
    開(kāi)封第一講書(shū)人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤凤薛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后诞仓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體缤苫,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年墅拭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了活玲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谍婉,死狀恐怖舒憾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屡萤,我是刑警寧澤珍剑,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布掸宛,位于F島的核電站死陆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜措译,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一别凤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧领虹,春花似錦规哪、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至最疆,卻和暖如春杯巨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背努酸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工服爷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人获诈。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓仍源,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親舔涎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子笼踩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 生平第一次素描戳表,而目還是頭,亂紅老師的講解和一步步人物頭像的立體活現(xiàn)昼伴,我感覺(jué)到的是驚訝和感動(dòng)匾旭,就一支小小的鉛筆和一...
    謙玥兒閱讀 748評(píng)論 4 9
  • 風(fēng)。風(fēng)而后是葉而后是熟紅的棗子圃郊,而后是泥土與黏在一旁亮白的混凝土地面上干癟的蚯蚓价涝。 鴉片白與霉斑青的路,路上印著一...
    反光物閱讀 256評(píng)論 0 1
  • 我愛(ài)上你了 是持舆, 這一次色瘩, 無(wú)法自拔 喜歡看你的眼睛 喜歡看你調(diào)皮的表情 喜歡你打哈欠時(shí)撐大的鼻孔 喜歡看你寬敞的...
    尚靈心閱讀 277評(píng)論 0 3
  • 1.我是因?yàn)槭裁粗档帽粣?ài)? 雖然有那么多人愛(ài)我逸寓,但我心里不安定居兆! 爛故事又在起作用了。 (1)我會(huì)覺(jué)得愛(ài)我的人是因...
    weeklybright閱讀 234評(píng)論 0 1