基于位置的 iOS 增強(qiáng)現(xiàn)實(shí)教程

本教程翻譯自 Augmented Reality iOS Tutorial: Location Based ,已更新至 Swift 3 和 iOS 10厅缺。


學(xué)習(xí)如何制作基于位置的增強(qiáng)現(xiàn)實(shí) App狰挡,在視頻上顯示興趣點(diǎn)!

增強(qiáng)現(xiàn)實(shí)是一項(xiàng)很酷、很流行的技術(shù)抄课,通過一臺設(shè)備(例如 iPhone 攝像頭或微軟的 HoloLens)來觀察世界稿黄,設(shè)備會在真實(shí)世界視角上覆蓋一些額外的信息喊衫。

我相信你已經(jīng)見過標(biāo)記追逐的 iOS App,把攝像頭指向標(biāo)記杆怕,就會彈出一個 3D 模型族购。

在本篇增強(qiáng)現(xiàn)實(shí)教程中,我們會編寫一個 App陵珍,它可以接收用戶的當(dāng)前位置并識別出附近的興趣點(diǎn)(我們把它們稱之為 POI)寝杖。我們會把這些點(diǎn)添加到 MapView 中,并將它們覆蓋在相機(jī)視圖上互纯。

要查找 POI瑟幕,我們會使用谷歌的 Places API,然后使用 HDAugmentedReality 庫在相機(jī)視圖上顯示 POI 并計算與用戶當(dāng)前位置的距離留潦。

本教程假設(shè)你對 MapKit 有一定基本的了解只盹。如果你完全不了解 MapKit,看看 Introduction to MapKit 這篇教程兔院。

開始

首先下載 起始項(xiàng)目殖卑,熟悉一下里面的內(nèi)容。在項(xiàng)目導(dǎo)航器里選擇 Places 項(xiàng)目坊萝,editing pane 中選擇 Places target孵稽,在 General 標(biāo)簽頁,Signing 區(qū)屹堰,把 Team 設(shè)置為自己的開發(fā)者賬號「匾保現(xiàn)在應(yīng)該可以編譯項(xiàng)目了埋酬。Main.storyboard 包含了已經(jīng)連接 MapViewUIButton 的場景厘贼。還包含了 HDAugmentedReality 庫,有文件 PlacesLoader.swiftPlace.swift旺隙。后面會使用它們以從谷歌 Places API 查詢 POI荣刑,并把結(jié)果映射為容易處理的類馅笙。

在做其他事之前,需要先獲取用戶的當(dāng)前位置厉亏。為此董习,我們會使用 CLLocationManager。打開 ViewController.swift 然后在 mapView outlet 下面添加一個屬性爱只,并命名為 locationManager皿淋。

fileprivate let locationManager = CLLocationManager()

這樣就初始化了一個 CLLocationManager 對象屬性。
下面給 ViewController.swift 添加下面這個類擴(kuò)展。

extension ViewController: CLLocationManagerDelegate {
}

在獲取當(dāng)前位置之前窝趣,必須給 Info.plist 添加一個鍵疯暑。打開該文件,然后添加 NSLocationWhenInUseUsageDescription 鍵哑舒,值為 AR 所需妇拯。首次嘗試訪問位置服務(wù)時,iOS 會向用戶顯示警告洗鸵,要求用戶給予該權(quán)限越锈。

現(xiàn)在萬事俱備,可以獲得位置了膘滨。打開 ViewController.swift 然后將 viewDidLoad() 替換為如下代碼:

override func viewDidLoad() {
  super.viewDidLoad()

  locationManager.delegate = self
  locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
  locationManager.startUpdatingLocation()
  locationManager.requestWhenInUseAuthorization()
}

這是 locationManager 的基礎(chǔ)配置甘凭。manager 需要一個 delegate,以便在 iDevice 更新位置的時候獲得通知吏祸。使用 self 將其設(shè)置為這個視圖控制器对蒲。然后 manager 需要了解位置的精度應(yīng)該是多少钩蚊。我們將其設(shè)置為 kCLLocationAccuracyNearestTenMeters贡翘,對于這個示例項(xiàng)目這已經(jīng)足夠精確了。最后一行啟動了 manager砰逻,詢問用戶以準(zhǔn)許權(quán)限來訪問位置服務(wù)(如果沒有被允許或禁止的話)鸣驱。

注意:關(guān)于 desiredAccuracy,應(yīng)該設(shè)置為足夠使用的最低精度蝠咆。為什么呢踊东?
假設(shè)你只需要數(shù)百米的精度——然后 LocationManager 就可以使用手機(jī)移動網(wǎng)絡(luò)和 WLAN 來獲得位置。這樣就節(jié)省了電量刚操,你知道闸翅,電量是 iDevices 上最大的限制因素之一。但如果你需要更好地確定位置菊霜,LocationManager 就會使用 GPS坚冀,會很快地耗盡電池。這也是為什么有一個可接受的值后鉴逞,就要停止更新位置记某。

現(xiàn)在需要實(shí)現(xiàn)一個 delegate 方法來獲得當(dāng)前位置。在 ViewController.swiftCLLocationManagerDelegate 擴(kuò)展里添加如下代碼:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
  //1
  if locations.count > 0 {
    let location = locations.last!
    print("精度: \(location.horizontalAccuracy)")
    
    //2
    if location.horizontalAccuracy < 100 {
      //3
      manager.stopUpdatingLocation()
      let span = MKCoordinateSpan(latitudeDelta: 0.014, longitudeDelta: 0.014)
      let region = MKCoordinateRegion(center: location.coordinate, span: span)
      mapView.region = region
      // 后面后有更多代碼...
    }
  }
}

一步步解釋此方法:

  1. 每次 LocationManager 更新了位置构捡,就會發(fā)送消息給它的 delegate液南,給出更新后的位置。位置數(shù)組按時間順序包含所有位置勾徽,因此最新的位置就是數(shù)組中的最后一個對象滑凉。首先檢查一下數(shù)據(jù)里有沒有位置,如果至少有一個的話,就用最新的那個畅姊。下一行獲得了水平精度并輸出到控制臺闪幽。這個值是當(dāng)前位置周圍的半徑。如果值為50涡匀,則表示實(shí)際位置就位于 location 中存儲的位置周圍半徑50米的圓內(nèi)盯腌。
  2. If 語句檢查準(zhǔn)確性是否足夠高以達(dá)到我們的需要。100米對于這個例子已經(jīng)足夠好了陨瘩,不需要等太久就會拿到這個精度腕够。在一個真實(shí)的 App 中,你大概會需要小于等于10米的精度舌劳,但這樣可能會需要幾分鐘以獲得該精度(GPS 追蹤需要時間)帚湘。
  3. 第一行停止更新位置以節(jié)省電量。下面三行把 mapView 放大到該位置甚淡。

在你的設(shè)備上構(gòu)建并運(yùn)行大诸,眼睛盯著控制臺,看看如何獲得位置贯卦,以及精度是如何一步步變好的资柔。最終你會看到地圖放大到一個以當(dāng)前位置為中心的區(qū)域上。

注意:還有一個叫做 verticalAccuracy 的屬性撵割,和 horizontalAccuracy 相同贿堰,只不過是針對位置的緯度的。所以值為50就意味著實(shí)際緯度可能大于或小于50米啡彬。對于這兩個屬性來說羹与,負(fù)值都是非法的。

添加谷歌 Places

現(xiàn)在我們有了當(dāng)前位置庶灿,可以加載 POI 列表了纵搁。為了獲得這個列表,我們會使用谷歌的 Places API往踢。

谷歌 Places API 需要注冊才能訪問腾誉。如果以前你已經(jīng)創(chuàng)建過谷歌賬號來訪問類似 Maps 的 API,直接到 這里 然后選擇 Services菲语。然后跳過接下來的步驟妄辩,直接到啟用 Places API 部分。

然而山上,如果你以前從沒使用過谷歌 Places API眼耀,就需要 注冊 一個賬號。

可以跳過第二屏佩憾,然后在第三屏上哮伟,點(diǎn)擊 Back to Developers Consoles干花。

現(xiàn)在在左上角點(diǎn)擊 Project/Create Project 然后輸入項(xiàng)目名。為了啟用 Places API楞黄,搜索到 Google Places API Web Service 這一行然后點(diǎn)擊鏈接池凄。點(diǎn)擊上面的 ENABLE。現(xiàn)在點(diǎn)擊 Credentials 然后接著做下面的步驟來取得 API key鬼廓。

加載興趣點(diǎn)

現(xiàn)在我們有了 API key肿仑,打開 PlacesLoader.swift 然后找到 let apiKey = "Your API key" 這一行,把值替換為你的 API key碎税。

這是再測試一次的好機(jī)會尤慰,但構(gòu)建和編譯之前,打開 ViewController.swift 然后在 locationManager 屬性之前添加兩個新屬性雷蹂。

fileprivate var startedLoadingPOIs = false
fileprivate var places = [Place]()

startedLoadingPOIs追蹤當(dāng)前是否有正在進(jìn)行的請求伟端,CLLocationManagerDelegate 方法可能會被調(diào)用多次,哪怕在我們停止了更新位置之后匪煌。為了避免多次請求责蝠,我們使用了這個標(biāo)志。places 會存儲接收到的 POI萎庭。

現(xiàn)在找到 locationManager(manager: didUpdateLocations:)霜医。在 if 語句里面,添加下面的代碼擎椰,就在 "后面后有更多代碼..." 注釋后面:

//1
if !startedLoadingPOIs {
  startedLoadingPOIs = true
  //2
  let loader = PlacesLoader()
  loader.loadPOIS(location: location, radius: 1000) { placesDict, error in
    //3
    if let dict = placesDict {
      print(dict)
    }
  }
}

這樣就開始加載用戶當(dāng)前位置方圓1000米內(nèi)的 POI 列表支子,然后打印到控制臺创肥。

構(gòu)建并運(yùn)行达舒,看看控制臺的輸出。應(yīng)該看起來像這樣叹侄,但是是不同的 POI:

{
    "html_attributions" =     (
    );
    "next_page_token" = "CpQCAgEAAJWpTe34EHADqMuEIXEUvbWnzJ3fQ0bs1AlHgK2SdpungTLOeK21xMPoi04rkJrdUUFRtFX1niVKCrz49_MLOFqazbOOV0H7qbrtKCrn61Lgm--DTBc_3Nh9UBeL8h-kDig59HmWwj5N-gPeki8KE4dM6EGMdZsY1xEkt0glaLt9ScuRj_w2G8d2tyKMXtm8oheiGFohz4SnB9d36MgKAjjftQBc31pH1SpnyX2wKVInea7ZvbNFj5I8ooFOatXlp3DD9K6ZaxXdJujXJGzm0pqAsrEyuSg3Dnh3UfXPLdY2gpXBLpHCiMPh90-bzYDMX4SOy2cQOk2FYQVR5UUmLtnrRR9ylIaxQH85RmNmusrtEhDhgRxcCZthJHG4ktJk37sGGhSL3YHgptN2UExsnhzABwmP_6L_mg";
    results =     (
                {
            geometry =             {
                location =                 {
                    lat = "50.5145334";
                    lng = "8.3931416";
                };
                viewport =                 {
                    northeast =                     {
                        lat = "50.51476485000001";
                        lng = "8.393168700000002";
                    };
                    southwest =                     {
                        lat = "50.51445624999999";
                        lng = "8.3930603";
                    };
                };
            };
            icon = "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png";
            id = c64c6c1abd02f4764d00a72c4bd504ab6d152a2b;
            name = "Schlo\U00df-Hotel Braunfels";
            photos =             (
                                {
                    height = 4160;
                    "html_attributions" =                     (
                        "<a href=\"https://maps.google.com/maps/contrib/113263673214221090182/photos\">Ralph Peters</a>"
                    );
                    "photo_reference" = "CoQBdwAAABZT7LYlGHmdep61gMOtwpZsYtVeHRWch0PcUZQOuICYHEWnZhKsSkVdMLx3RBTFIz9ymN10osdlqrPcxhxn-vv3iSsg6YyM18A51e3Sy0--jO2u4kCC05zeMyFp-k7C6ygsDsiOK4Dn3gsu_Bf5D-SZt_SrJqkO0Ys6CwTJ75EPEhDcRLUGnYt2tSODqn_XwxKWGhRMrOG9BojlDHFSoktoup1OsbCpkA";
                    width = 3120;
                }
            );
            "place_id" = ChIJdadOzRdPvEcRkItOT1FMzdI;
            rating = "3.8";
            reference = "CmRSAAAAgvVO1e988IpXI7_u0IsRFCD1U1IUoSXlW7KfXvLb0DDtToodrGbiVtGZApSKAahnClm-_o-Nuixca_azt22lrT6VGwlJ1m6P0s2TqHAEmnD2QasXW6dCaDjKxesXCpLmEhAOanf32ZUsfX7JNLfNuuUXGhRrzQg-vvkQ0pGT-iSOczT5dG_7yg";
            scope = GOOGLE;
            types =             (
                lodging,
                "point_of_interest",
                establishment
            );
            vicinity = "Hubertusstra\U00dfe 2, Braunfels";
        },

Pardon my french! :]

如果響應(yīng)返回 NULL巩搏,嘗試增加半徑。

到目前為止趾代,我們的 App 可以確定用戶的位置并加載當(dāng)前區(qū)域的 POI 列表贯底。我們有一個類可以存儲列表里的地方,雖然我們目前并沒使用它撒强。真正缺少的能力是在地圖上顯示 POI禽捆!

顯示興趣點(diǎn)

要在 mapView 上進(jìn)行注釋,需要另一個類飘哨。選擇 File\New\File…胚想,選擇 iOS\Swift File 然后點(diǎn)擊 Next。將文件命名為 PlaceAnnotation.swift 然乎點(diǎn)擊 Create芽隆。

打開 PlaceAnnotation.swift 將內(nèi)容替換為下面的代碼:

import Foundation
import MapKit

class PlaceAnnotation: NSObject, MKAnnotation {
  let coordinate: CLLocationCoordinate2D
  let title: String?
  
  init(location: CLLocationCoordinate2D, title: String) {
    self.coordinate = location
    self.title = title
    
    super.init()
  }
}

現(xiàn)在我們做好了實(shí)現(xiàn) MKAnnotation協(xié)議的類浊服,然后定義了兩個屬性和一個自定義初始化方法统屈。

現(xiàn)在萬事俱備,只需要在地圖上顯示 POI 了牙躺!

回到 ViewController.swift 然后完成 locationManager(manager: didUpdateLocations:) 方法愁憔。找到 print(dict) 行然后替換為如下代碼:

//1
guard let placesArray = dict.object(forKey: "results") as? [NSDictionary]  else { return }
//2
for placeDict in placesArray {
  //3
  let latitude = placeDict.value(forKeyPath: "geometry.location.lat") as! CLLocationDegrees
  let longitude = placeDict.value(forKeyPath: "geometry.location.lng") as! CLLocationDegrees
  let reference = placeDict.object(forKey: "reference") as! String
  let name = placeDict.object(forKey: "name") as! String
  let address = placeDict.object(forKey: "vicinity") as! String
              
  let location = CLLocation(latitude: latitude, longitude: longitude)
  //4
  let place = Place(location: location, reference: reference, name: name, address: address)              
  self.places.append(place)
  //5
  let annotation = PlaceAnnotation(location: place.location!.coordinate, title: place.placeName)
  //6
  DispatchQueue.main.async {
    self.mapView.addAnnotation(annotation)
  }
}

仔細(xì)看看上面發(fā)生了什么:

  1. guard 語句檢查響應(yīng)是否是我們期待的格式
  2. 這行遍歷了接收到的 POI
  3. 這幾行從字典中獲得了所需的信息。響應(yīng)還包含了很多此 App 不需要的信息孽拷。
  4. 使用提取出來的信息吨掌,創(chuàng)建 Place 對象并將其附到 places 數(shù)組的末尾。
  5. 下一行創(chuàng)建了 PlaceAnnotation用于在 map view 上顯示注釋脓恕。
  6. 終于注釋被添加到了 map view 上思犁。因?yàn)樾薷牧?UI,所以必須在主線程上執(zhí)行代碼进肯。

構(gòu)建并運(yùn)行激蹲。這次,一些注釋出現(xiàn)在了地圖上江掩,點(diǎn)擊一個学辱,會看到地方的名字。這個 App 現(xiàn)在看起來棒棒的环形,但增強(qiáng)現(xiàn)實(shí)在哪里策泣?

介紹 HDAugmentedReality

我們目前已經(jīng)做了很多工作,已經(jīng)為接下來的事情做好了準(zhǔn)備:是時候帶入增強(qiáng)現(xiàn)實(shí)了抬吟。

你應(yīng)該已經(jīng)看到了右下角的 Camera 按鈕∪荆現(xiàn)在按這個按鈕不會有任何反應(yīng)。在本節(jié)中火本,我們會給這個按鈕添加一些動作危队,并顯示攝像頭的實(shí)時預(yù)覽,而且?guī)в幸恍┰鰪?qiáng)實(shí)現(xiàn)元素钙畔。

為了讓人生不要那么艱難茫陆,我們會使用 HDAugmentedReality庫。它已經(jīng)被包含在你早前下載的啟動項(xiàng)目中了擎析,如果想要最新版本的簿盅,可以在 Github 上找到,但用這個 lib 也不會把你怎么樣揍魂,是吧桨醋?

首先,HDAugmentedReality會為你處理相機(jī)上的字幕现斋,這樣顯示實(shí)時視頻就很容易了喜最。其次,它為你添加 POI 的疊加層并處理它們的位置步责。

馬上就會看到返顺,最后一點(diǎn)真是我們最大的福音禀苦,因?yàn)檫@樣我們就不用做一些復(fù)雜的數(shù)學(xué)了!如果你想更多地了解 HDAugmentedReality 背后的數(shù)學(xué)知識遂鹊,繼續(xù)讀吧振乏。

如果不想的話,希望立即處理代碼秉扑,完全可以跳過下面兩節(jié)慧邮,直接到開始寫代碼部分。

警告舟陆,這里有數(shù)學(xué)误澳!

你還在這里,說明你想多多了解 HDAugmentedReality 的數(shù)學(xué)秦躯。很棒棒忆谓!但還是要警告一下,這會比標(biāo)準(zhǔn)的算術(shù)復(fù)雜一點(diǎn)踱承。在下面的例子中倡缠,我們假設(shè)有兩個給定的點(diǎn) A 和 B,存儲地球上特定點(diǎn)的坐標(biāo)茎活。

點(diǎn)的坐標(biāo)由兩個值組成:經(jīng)度和緯度昙沦。這些是 2D 笛卡爾系統(tǒng)中 x 和 y 值的地理名稱。

  • 經(jīng)度指定了點(diǎn)位于英格蘭格林威治參考點(diǎn)的以西還是以東载荔。取值范圍是 +180° 到 -180°盾饮。
  • 緯度指定了點(diǎn)位于赤道以北還是以南。取值范圍是北極的 90° 到南極的 -90°懒熙。

如果看一個標(biāo)準(zhǔn)的地球儀丘损,就能看到從極點(diǎn)到極點(diǎn)的經(jīng)度線——也被稱為經(jīng)線。還會看到全球各地的緯度線煌珊,也稱為平行線号俐。地理書里會講兩條平行線間的距離約為 111 公里,兩條經(jīng)線之間的距離也為約 111 公里定庵。

有 360 條子午線,360 度的每一度踪危,以及 180 條平行線蔬浙。把這個記在腦子里,就可以使用以下公式計算地球上兩點(diǎn)間的距離:


這樣就得到了緯度和經(jīng)度的距離贞远,它們是直角三角形的兩邊畴博。使用畢達(dá)哥拉斯定理,現(xiàn)在可以計算三角形的斜邊以找出兩點(diǎn)間的距離:

這太簡單了蓝仲,但很不幸俱病,這是錯的官疲。

如果再看一眼你的地球儀,你會看到平行線之間的距離幾乎相等亮隙,但經(jīng)線在兩極相交途凫。所以越靠近極點(diǎn),子午線之間的距離就會縮小溢吻,而在極點(diǎn)上是零维费。這意味著以上公式僅適用于赤道附近的點(diǎn)。點(diǎn)越接近極點(diǎn)促王,誤差越大犀盟。

要更準(zhǔn)確地計算距離,可以使用大圓距離蝇狼。這是球體上兩點(diǎn)之間的距離阅畴,眾所周知,地球是一個球體迅耘。好吧恶阴,她近似是一個球體,但這種方法會計算出很好的結(jié)果豹障。已知兩個點(diǎn)的緯度和經(jīng)度冯事,可以使用下面的公式來計算大圓距離。

這個公式給出了兩點(diǎn)之間的距離血公,精度約為 60 公里昵仅,如果你想知道東京和紐約的距離,這樣就相當(dāng)好了累魔。但對于更近的點(diǎn)摔笤,結(jié)果將會更好。

唉——這太難了垦写!好消息是吕世,CLLocation 有一個方法,distanceFromLocation:梯投,它會為你做這個計算命辖。HDAugmentedReality 也是用這個方法。

為什么是 HDAugmentedReality

你可能在琢磨“嗯分蓖,我還是不明白我為什么應(yīng)該使用 HDAugmentedReality尔艇。”使用框架并且顯示它們真的沒那么難么鹤,可以在它的網(wǎng)站上閱讀有關(guān)信息终娃。我們可以無痛使用 CLLocation 的方法來計算點(diǎn)之間的距離。

那我為什么要介紹這個庫呢蒸甜?當(dāng)你需要計算屏幕上顯示的疊加層的位置時棠耕,問題就來了余佛。假設(shè)有一個 POI 在你的北邊,然后你的設(shè)備正朝向東北窍荧。應(yīng)該把 POI 顯示在哪里呢——中心還是左側(cè)辉巡?頂部還是底部?

這一切都取決于設(shè)備在房間中的當(dāng)前位置搅荞。如果設(shè)備指向地面上的一個標(biāo)題红氯,附近的 POI 就應(yīng)該顯示在頂部。如果指向南邊咕痛,就不應(yīng)該顯示所有 POI痢甘。這樣很快就會變的相當(dāng)復(fù)雜!

這就是 HDAugmentedReality 最有用的地方茉贡。它從陀螺儀和羅盤獲取所需的所有信息塞栅,并計算設(shè)備指向的位置及其傾斜程度。借助這個知識腔丧,它決定是否和如何把 POI 顯示在屏幕上放椰。

另外,無需再為顯示實(shí)時視頻并進(jìn)行復(fù)雜和容易出錯的數(shù)學(xué)而擔(dān)憂愉粤,我們可以集中精力編寫一個用戶喜愛的 App砾医。

開始寫代碼

現(xiàn)在快速瀏覽一下 HDAugmentedReality\Classes 組里面的文件:

  • ARAnnotation:此類用于定義 POI。
  • ARAnnotationView:用于提供 POI 視圖衣厘。
  • ARConfiguration:提供一些基礎(chǔ)的配置和幫助方法如蚜。
  • ARTrackingManager:艱苦的工作就是在這里完成的。幸運(yùn)的是影暴,我們并不需要處理它错邦。
  • ARViewController:這個控制器替我們完成視覺上的事情。顯示了實(shí)時視頻以及為視圖添加標(biāo)記型宙。

設(shè)置 AR 視圖

打開 ViewController.swift 然后在 places 屬性下面添加另一個屬性:

fileprivate var arViewController: ARViewController!

現(xiàn)在找到 @IBAction func showARController(_ sender: Any) 然后把下面的代碼添加到方法體內(nèi):

arViewController = ARViewController()
//1
arViewController.dataSource = self
//2
arViewController.maxVisibleAnnotations = 30
arViewController.headingSmoothingFactor = 0.05
//3
arViewController.setAnnotations(places)
    
self.present(arViewController, animated: true, completion: nil)
  1. 首先設(shè)置了 arViewController 的 dataSource撬呢。dataSource 為視圖提供可視化 POI。
  2. 這是對 arViewController 的一些微調(diào)妆兑。maxVisibleAnnotations 定義了同時顯示的視圖數(shù)量魂拦。為了保持一切順滑,值使用了 30箭跳,但這也意味著晨另,如果你住在一個令人興奮的地區(qū),周圍有很多 POI谱姓,但可能并不會全部都顯示出來。
  3. headingSmoothingFactor 用于移動屏幕相關(guān)的 POI 視圖刨晴。值為1意味著沒有平滑屉来,如果你的 iPhone 繞著視圖轉(zhuǎn)動路翻,視圖可能會從一個位置跳到另一個位置。較低的值表示移動是動畫茄靠,但是視圖可能會有一點(diǎn)滯后于“移動”茂契。你應(yīng)該多嘗試這個值,以很好地平衡順滑移動和速度慨绳。
  4. 顯示 arViewController掉冶。

你還應(yīng)該看看 ARViewController.swift 里面,找到更多類似 maxDistance 的屬性脐雪,這個屬性定義了以米計算的范圍厌小,以在其中顯示視圖。所以任何大于這個值的東西都不會被顯示战秋。

實(shí)現(xiàn) Datasource 方法

Xcode 會在將 self 賦值為 dataSource 的那一行抱怨璧亚,為了讓它開心,ViewController 必須采用 ARDataSource 協(xié)議脂信。這個協(xié)議只有一個必須的方法癣蟋,需要為 POI 返回視圖。在大部分情況下狰闪,也可以在此處提供自定義視圖疯搅。按 [cmd] + [n] 添加一個新文件。選擇 iOS\Swift File 然后存儲為 AnnotationView.swift埋泵。

將內(nèi)容替換如下:

import UIKit

//1
protocol AnnotationViewDelegate {
  func didTouch(annotationView: AnnotationView)
}

//2
class AnnotationView: ARAnnotationView {
  //3
  var titleLabel: UILabel?
  var distanceLabel: UILabel?
  var delegate: AnnotationViewDelegate?
  
  override func didMoveToSuperview() {
    super.didMoveToSuperview()
    
    loadUI()
  }
  
  //4
  func loadUI() {
    titleLabel?.removeFromSuperview()
    distanceLabel?.removeFromSuperview()
    
    let label = UILabel(frame: CGRect(x: 10, y: 0, width: self.frame.size.width, height: 30))
    label.font = UIFont.systemFont(ofSize: 16)
    label.numberOfLines = 0
    label.backgroundColor = UIColor(white: 0.3, alpha: 0.7)
    label.textColor = UIColor.white
    self.addSubview(label)
    self.titleLabel = label
    
    distanceLabel = UILabel(frame: CGRect(x: 10, y: 30, width: self.frame.size.width, height: 20))
    distanceLabel?.backgroundColor = UIColor(white: 0.3, alpha: 0.7)
    distanceLabel?.textColor = UIColor.green
    distanceLabel?.font = UIFont.systemFont(ofSize: 12)
    self.addSubview(distanceLabel!)
    
    if let annotation = annotation as? Place {
      titleLabel?.text = annotation.placeName
      distanceLabel?.text = String(format: "%.2f km", annotation.distanceFromUser / 1000)
    }
  }
}
  1. 首先添加了一個代理協(xié)議幔欧,后面會用。
  2. 創(chuàng)建了 ARAnnotationView 的子類秋泄,用于顯示一個 POI 視圖琐馆。
  3. 該 App 中的視圖只顯示一個帶有 POI 名稱的 label 和一個帶有距離的 label。這些行聲明了所需的屬性恒序,第三個也是我們后面會用到的瘦麸。
  4. loadUI() 添加并配置了 label。

要完成這個類歧胁,還需要兩個方法

//1
override func layoutSubviews() {
  super.layoutSubviews()
  titleLabel?.frame = CGRect(x: 10, y: 0, width: self.frame.size.width, height: 30)
  distanceLabel?.frame = CGRect(x: 10, y: 30, width: self.frame.size.width, height: 20)
}
  
//2
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  delegate?.didTouch(annotationView: self)
}
  1. 每次需要重新繪制視圖時滋饲,都會調(diào)用次方法,只要確保 labe 有正確的 frame 值喊巍,重置它們即可屠缭。
  2. 這里我們告訴 delegate 視圖被觸摸了,所以 delegate 可以決定如何以及哪個動作需要被執(zhí)行崭参。

現(xiàn)在回到 ViewController.swift 并添加如下擴(kuò)展:

extension ViewController: ARDataSource {
  func ar(_ arViewController: ARViewController, viewForAnnotation: ARAnnotation) -> ARAnnotationView {
    let annotationView = AnnotationView()
    annotationView.annotation = viewForAnnotation
    annotationView.delegate = self
    annotationView.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
    
    return annotationView
  }
}

這里我們創(chuàng)建了一個新的 AnnotaionView 并在 return 它之前為其設(shè)置 delegate呵曹。

真正可以測試視圖之前,還需要另一個擴(kuò)展。

extension ViewController: AnnotationViewDelegate {
  func didTouch(annotationView: AnnotationView) {
    print("Tapped view for POI: \(annotationView.titleLabel?.text)")
  }
}

激活相機(jī)之前奄喂,必須向 Info.plist 添加一個鍵铐殃。打開該文件并添加一個值為“AR 所需”的鍵 NSCameraUsageDescription,就像我們訪問位置信息時做的一樣跨新。

構(gòu)建并運(yùn)行富腊,點(diǎn)擊地圖視圖上的相機(jī)按鈕進(jìn)入 ar 視圖。第一次這樣做時域帐,系統(tǒng)將在訪問相機(jī)之前彈出權(quán)限對話框赘被。點(diǎn)擊一個 POI 然后查看控制臺。

結(jié)束觸摸

我們現(xiàn)在有了一個能完整工作的 AR App肖揣,我們可以在攝像頭視圖上顯示 POI 并檢測 POI 上的點(diǎn)擊民假,為了徹底完成這個 App 我們現(xiàn)在需要添加一些觸摸的處理邏輯。

打開 ViewController.swift(如果關(guān)掉了的話)然后將采用 AnnotationViewDelegate 協(xié)議的擴(kuò)展替換為如下代碼:

extension ViewController: AnnotationViewDelegate {
  func didTouch(annotationView: AnnotationView) {
  //1
    if let annotation = annotationView.annotation as? Place {
    //2
      let placesLoader = PlacesLoader()
      placesLoader.loadDetailInformation(forPlace: annotation) { resultDict, error in
      
      //3  
      if let infoDict = resultDict?.object(forKey: "result") as? NSDictionary {
          annotation.phoneNumber = infoDict.object(forKey: "formatted_phone_number") as? String
          annotation.website = infoDict.object(forKey: "website") as? String
          
          //4
          self.showInfoView(forPlace: annotation)
        }
      }
    }
  }
}
  1. 首先把 annotationViews 的 annotation 轉(zhuǎn)為 Place许饿。
  2. 加載此地的附加信息阳欲。
  3. 將其分配給合適的屬性。
  4. 我們現(xiàn)在來實(shí)現(xiàn) showInfoView(forPlace:) 這個方法陋率。

showARController(sender:) 下面添加這個方法

func showInfoView(forPlace place: Place) {
  //1
  let alert = UIAlertController(title: place.placeName , message: place.infoText, preferredStyle: UIAlertControllerStyle.alert)
  alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
   //2 
  arViewController.present(alert, animated: true, completion: nil)
}
  1. 我們創(chuàng)建了一個 alert 視圖以顯示附加信息球化,POI 的名字是標(biāo)題,info 文本是消息瓦糟。
  2. 由于 ViewController 現(xiàn)在還不是視圖層級的一部分筒愚,可以使用 arViewController 來顯示 alert。

再次構(gòu)建并運(yùn)行菩浙,就可以看到最終的 App 了巢掺。

下一步?

這里可以下載 最終項(xiàng)目 劲蜻,帶有上面全部的代碼陆淀。

恭喜,你現(xiàn)在已經(jīng)知道如何制作自己的基于位置的增強(qiáng)現(xiàn)實(shí) App先嬉!另外轧苫,還看到了 Google Places API 的簡單介紹。

如果有任何意見或問題疫蔓,直接在下面評論含懊!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市衅胀,隨后出現(xiàn)的幾起案子岔乔,更是在濱河造成了極大的恐慌,老刑警劉巖滚躯,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雏门,死亡現(xiàn)場離奇詭異嘿歌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)剿配,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門搅幅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阅束,“玉大人呼胚,你說我怎么就攤上這事∠⒙悖” “怎么了蝇更?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呼盆。 經(jīng)常有香客問我年扩,道長,這世上最難降的妖魔是什么访圃? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任厨幻,我火速辦了婚禮,結(jié)果婚禮上腿时,老公的妹妹穿的比我還像新娘况脆。我一直安慰自己,他們只是感情好批糟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布格了。 她就那樣靜靜地躺著,像睡著了一般徽鼎。 火紅的嫁衣襯著肌膚如雪盛末。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天否淤,我揣著相機(jī)與錄音悄但,去河邊找鬼。 笑死石抡,一個胖子當(dāng)著我的面吹牛檐嚣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播汁雷,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼净嘀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侠讯?” 一聲冷哼從身側(cè)響起挖藏,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厢漩,沒想到半個月后膜眠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年宵膨,在試婚紗的時候發(fā)現(xiàn)自己被綠了架谎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡辟躏,死狀恐怖谷扣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捎琐,我是刑警寧澤会涎,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站瑞凑,受9級特大地震影響末秃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜籽御,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一练慕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧技掏,春花似錦铃将、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至涧衙,卻和暖如春哪工,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弧哎。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工雁比, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撤嫩。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓偎捎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親序攘。 傳聞我的和親對象是個殘疾皇子茴她,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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