ARKit教程17_第十三章:位置跟踪和信标

前言

ARKit感兴趣的同学,可以订阅ARKit教程专题
源代码地址在这里

正文

在前三章中,我们学习了如何使用ARKit实现虚拟广告牌,该虚拟广告牌首先是通过扫描矩形显示出来,然后是扫描QR码显示出来。

广告牌的第一个版本使用视图控制器和附加到ARKit平面材料的视图来显示图像轮播和视频播放器。

在第二个修订版中,我们使用故事板替换了视图控制器并添加了Web视图;这使得在ARKit会话中显示网站成为可能。我们还学习了切换全屏模式。

在本章的最后一章中,我们将学习如何使用位置功能,通过在用户靠近感兴趣的地方时自动启用功能来增强用户体验。

具体来说,我们将学习到:

  • 获取用户的位置。
  • 使用地理围栏来检测用户何时进入或离开特定区域。
  • 使用信标进行接近检测。

开始

确定用户位置

虽然我们可能没有使用它,但我们可能听说过Core Location,用于处理设备位置和信标的iOS框架。我们将在本项目中实现它。

启用核心位置

要使用Core Location并遵守用户权限要求,我们需要向Info.plist文件添加一些Key

使用Core Location有两种方法:

  • when in use: Core Location仅在应用程序位于前台时才向应用程序发送位置更新。
  • always: Core Location随时向应用程序发送位置更新,无论应用程序是在前台还是后台。

在本项目中,我们需要使用always

iOS 11中已经发生了一些变化。在iOS 10及更早版本中,要求always模式,我们需要将NSLocationAlwaysUsageDescription键添加到Info.plist,其值应包含提示用户让用户决定是否同意。

另一方面,在iOS 11中,我们需要包含两个Key,要求始终和何时使用权限。

我们首先按照下图作如下操作:

文件以源代码模式打开。在文件中看到的第一个<dict>标签后立即转到第5行,然后插入以下两行:

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Chapter13 requires access to your phones location to notify when you enter a geofence containing ads</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Chapter13 requires access to your phones location to notify when you enter a geofence containing ads</string>

iOS要求访问设备位置的权限时,这些行指定提示给用户的文本。

注意:XML代码已经过格式化,通过在两个<string> </ string>标记中包含的文本中添加几个换行符来提高可读性。当文本显示给用户时会呈现这些换行符,因此在编写代码时忽略它们会更好,如果使用复制和粘贴则删除。

Info.plist中正确设置密钥非常重要。如果Core Location没有找到它所期望的密钥,那么它就不会告诉你它们已经丢失了。虽然它会向Xcode控制台发出警告,但它会默默地拒绝执行任何与位置相关的活动。

打开LocationManager.swift;这实现了LocationManager类。我们将在本章的过程中添加更多功能。但是现在,在初始化程序之后添加此方法:

func initialize() {

// 1 
locationManager.delegate = self 
// 2 
locationManager.activityType = .otherNavigation

// Geofencing requires always authorization 
// 3 
locationManager.requestAlwaysAuthorization() 
// 4 
locationManager.startUpdatingLocation()
}

上面代码作用如下:

  • 1: LocationManagerCore LocationCLLocationManager类的实例。该类公开了一个根据该应用的需求量身定制的界面。
  • 2: Core Location需要知道位置跟踪的目的,以便更好地满足应用程序的需求。像如下应用场景:automotive navigationfitnessother navigationother。你在这个应用程序中使用otherNavigation
  • 3: 这要求用户获得永久授权的许可。如果未授予或拒绝授权,则授权过程通过委托异步执行,因此下一行不会产生任何影响。
  • 4: 此行通过指示Core Location发送位置更新来启动跟踪过程,除非权限被拒绝。

现在我们需要添加下面的方法:

private var locationManager = LocationManager()

找到viewDidLoad()方法。在将场景设置为sceneView之后,添加以下代码行:

// Initialize core location 
locationManager.initialize() 
locationManager.delegate = self

这将调用初始化方法并设置位置管理器委托。

在文件的最后,我们添加一个extension:

// MARK: - LocationManagerDelegate 
extension ViewController: LocationManagerDelegate {

// MARK: Location 
func locationManager(_ locationManager: LocationManager, didEnterRegionId regionId: String) { 
}

func locationManager(_ locationManager: LocationManager, didExitRegionId regionId: String) { 
}

// MARK: Beacons 
func locationManager(_ locationManager: LocationManager, didRangeBeacon beacon: CLBeacon) {
 }

func locationManager(_ locationManager: LocationManager, didLeaveBeacon beacon: CLBeacon) {
     }
}

LocationManagerDelegate是一个类似于CLLocationManager委托的自定义委托协议。我们可以将其视为CLLocationManagerDelegate的包装器,它在LocationManager类中使用。

实际上,LocationManager将实现CLLocationManagerDelegate中定义的总共九种方法,其中四种将通过自定义LocationManagerDelegate转发到ViewController

现在,我们需要在采用CLLocationManagerDelegate的扩展中的LocationManager中实现两个委托方法。这是第一个:

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    print("Authorization status changed to: \(status)")
}

当授权状态从默认的notDetermined更改为任何其他可能的值时,将调用此方法:restricteddeniedauthorizedAlwaysauthorizedWhenInUse

第二个:

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print("Location manager failed with error: " + "\(error.localizedDescription)"
}

如果在尝试检索位置时发生错误,则会调用此方法。实现此方法很重要,或者Core Location在尝试使用位置服务时会抛出异常。

添加这两个委托方法后,构建并运行应用程序。核心位置将要求我们授权该应用访问我们的位置。

标题下方的描述是我们之前添加到Info.plist的文本。请务必选择Always Allow

地理围栏

现在已设置Core Location,我们可以开始跟踪设备位置。不关心实际的设备位置。当我们在三章前介绍时,你会发现它应该能够在用户离目标一定距离时显示出来。此功能称为地理围栏,它包括监视某个位置周围的区域,特别是当设备跨越该区域的边界时。

项目中有一个Geo Fencing的按钮。其目的是启动或停止区域监测。

开始区域监测

ViewController.swift中,找到toggleLocationTracking();这已经连接到Geo Fencing按钮的tap事件。该方法包含注释的打印语句;暂时忽略它们。

在方法的开头添加此代码:

isLocationTrackingActive = !isLocationTrackingActive
if isLocationTrackingActive { 
} else { 
}

isLocationTrackingActive是指示位置跟踪当前是否处于活动状态的标记。你将填充if /else语句。

print语句移动到if分支中并取消注释。它们用于向控制台显示警告,提醒我们RMK位置是硬编码的。

仍在if分支内部,在print语句之前,添加以下代码:

// 1 self.toggleLocationTrackingButton.setImage(#imageLiteral(resourceName: "arKit-fence-on"), for: .normal)
// 2
 let rmkLocation = Constants.razewareMobileKioskLocation
// 3
let rmkCoordinates = rmkLocation.location

当用户点击地理围栏按钮,并且正在激活位置跟踪时:

  • 1: 我们更新按钮图像以指示它已打开。
  • 2: 我们可以读取RMK所在目标点的位置。
  • 3: 我们阅读该位置的坐标。

RMK位置使用包含名称和位置的自定义结构在Constants.swift中进行静态硬编码。

现在你有了目标位置。下一步是开始监视该目标周围的区域。在print语句之后,添加以下代码:

do {
    try locationManager.startMonitoring( 
        location: rmkCoordinates, 
        radius: Constants.geofencingRadius, 
        identifier: Constants.razewareMobileKioskIdentifier)
} catch (let error as LocationManager.GeofencingError) { 
     print( "An error occurred while monitoring a region: \(error)") 
} catch (let error) { 
    print( "Tracking location error: \(error.localizedDescription)") 
}

startMonitoring()开始监视半径为Constants.geofencingRadius米的rmlCoordinates位置周围的区域,当前设置为300,并在Constants.razewareMobileKioskIdentifier中指定一个字符串标识符。

监控一个地区

监视启动在LocationManager类中完成。打开LocationManager.swift。在initialize方法之后,添加以下内容:

func startMonitoring(location: CLLocationCoordinate2D, radius: Double, identifier: String) throws {
// 1
guard CLLocationManager.isMonitoringAvailable( for: CLCircularRegion.self)
else { throw GeofencingError.notSupported }
guard CLLocationManager.authorizationStatus() == .authorizedAlways
else { throw GeofencingError.notAuthorized }
trackedLocation = location
// 2
let region = CLCircularRegion(center: location, radius: radius, identifier: identifier)
// 3
region.notifyOnEntry = true 
region.notifyOnExit = true
// 4
locationManager.startMonitoring(for: region)
// 5
// Delay state request by 1 second due to an old bug 
// http://www.openradar.me/16986842
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(1), qos: .default, flags: []) {
    self.locationManager.requestState(for: region)
    }
}

上面代码作用如下:

  • 1:我们首先需要检查:
     ̯a: 可以使用区域监视,因为某些设备可能不支持它。
     ̯b: 用户授权始终开启位置跟踪。
  • 2: 这是我们要监控的区域。它以位置为中心,并具有由radius参数指定的半径。
  • 3: 我们可以启用这两个文件来指示我们有兴趣在设备进入和退出受监控区域时通知的Core Location
  • 4: 这开始了区域监测。
  • 5: 在这里,我们可以请求通过Core Location委托异步传送的区域的立即状态。

注意:区域是共享的应用程序资源,这意味着如果我们有多个CLLocationManager实例,则每个实例都会在进入或退出任何受监视区域时收到通知。

此外,对状态的请求包含在延迟调用中,因为旧的错误导致该方法在同步调用时不执行任何操作。

现在,我们需要提供在设备进入和退出受监控区域时由Core Location调用的两个委托方法。

LocationManager中,转到底部。在最终扩展中,添加进入区域时调用的委托方法:

func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    if let region = region as? CLBeaconRegion { } else {
    print("Entered in region: \(region)")
   delegate?.locationManager(
   self, didEnterRegionId: region.identifier) }
}

我们现在可以忽略if分支;它将在以后使用。 else分支是区域输入事件被转发到LocationManagerDelegate中定义的委托作为locationManager(_:didEnterRegionId :)并在ViewController中实现。虽然它现在已经空了,但我们很快就会照顾到它。我们将区域标识符传递给该方法。

didEnterRegion之后,添加其镜面反射didExitRegion方法:

func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { 
    print("Left region \(region)") 
    delegate?.locationManager( self, didExitRegionId: region.identifier) 
}

这类似于通过调用locationManager(_:didExitRegionId :)来通知何时设备离开受监视区域。

注意:我们可以在一台设备上监控多达20个区域。

当我们对requestState做一个延迟调用(for :)时,请记住startMonitoring(location:radius:identifier :)吗?结果通过另一个CLLocationManagerDelegate方法异步传递。在locationManager(_:didFailWithError :)之后添加它:

func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
    switch state { 
    // 1 
    case .inside:
    locationManager(manager, didEnterRegion: region)
    // 2 
    case .outside, .unknown:
        break 
    }
}

上面代码作用如下:

  • 1: 在这里,我们有兴趣了解设备何时已经位于受监控区域内,在这种情况下,我们将委托给locationManager(_:didEnterRegion :),就好像它是从受监控区域外部到内部的正常过渡一样。
  • 2: 在这里,你说你对其他案件并不感兴趣。

你在开始监控之后立即调用requestState(for :)的原因是我们想知道 - 在那个特定的时刻 - 如果设备已经在被监控区域内。区域监视仅在状态发生变化时触发事件,即从外部转换到内部,反之亦然。如果我们已经在室内或室外,它将不会触发任何事件。

如果设备已在内部,那么我们需要对信标执行某些操作。稍后会详细介绍!

还有一个委托方法可以实现,至少在调试方面是有用的。在locationManager之后添加它(_:didDetermineState:for :)

func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
    print("Geofencing monitoring failed for region " + "\(String(describing: region?.identifier))," + "error: \(error.localizedDescription)")
}

startMonitoring(for :)失败时调用此方法。将错误消息打印到控制台可能有助于我们了解问题何时发生以及发生的原因。

对区域变化做出反应

当设备穿过受监控区域时,Core Location会通知LocationManager。反过来,LocationManager使用LocationManagerDelegate协议的两种方法将通知转发给ViewController。我们将使用这两个通知来玩信标。但是,我们可以在用户进入某个区域时提醒用户。

ViewController.swift中,滚动到locationManager(_:didEnterRegionId :)并添加以下代码:

// Notify the user that he’s entered the geofenced zone 
let distance = String(format: "%0.0f", Constants.geofencingRadius)
let message = "\(regionId) is less than \(distance) meters. " + "Come say hi and interact with our e-billboard"
let title = regionId
showAlert(with: "geofencing-notification", title: title, message: message)

这会提示警报并通知用户RMK在附近。

停止区域监测

当我们点击Geo Fencing按钮时,到目前为止编写的代码负责启动区域监视并通知设备何时穿过区域。要关闭圆圈,我们必须在相反的方向上工作:停止区域监控。

转到toggleLocationTracking()点击处理程序。早些时候,如果未实现,你就离开了最外层的else分支。使用以下代码填充该空白区域:

// 1 
self.toggleLocationTrackingButton.setImage( #imageLiteral(resourceName: "arKit-fence-off"), for: .normal) 
// 2 
locationManager.stopMonitoringRegions()

上面代码作用如下:

  • 1: 恢复地理围栏按钮的关闭图标。
  • 2: 停止区域监控。

但是,该停止方法尚不存在。打开LocationManager.swift并在startMonitoring()之后添加它:

func stopMonitoringRegions() { 
    trackedLocation = nil
    for region in locationManager.monitoredRegions { 
       locationManager.stopMonitoring(for: region) }
}

我们无需跟踪已开始监控的区域;它们列在CLLocationManagermonitoredRegions属性中 - 我们只需遍历它们并停止每个属性。

距离计算

到目前为止,我们已经忽略了获取实际的设备位置。但是,这是通过实现单个委托方法完成的。

打开LocationManager并在扩展的开头添加此方法以实现CLLocationManagerDelegate

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    // 1 
    guard let currentLocation = locations.last else { return }
    // 2 
    guard let trackedLocation = trackedLocation else { return }
    let location = CLLocation(latitude: trackedLocation.latitude, longitude: trackedLocation.longitude)
    // 3 
    let distance = currentLocation.distance(from: location)
    print("Distance: \(distance)")
}

上面代码作用如下:

  • 1: 委托方法接收一个数组,该数组包含自上次调用以来的位置列表,按从最旧到最新的时间戳排序。你拿最后一个;即最近的。

  • 2: 我们获得包含目标位置的CLLocation - RMK。由于它们存储为CLLocationCoordinate2D结构,因此我们将转换为CLLocation的实例。

  • 3: 我们计算两个位置之间的距离并将其打印到Xcode控制台。

测试

我们写了很多代码。现在是时候运行应用程序来查看它的全部动态。为方便起见,选择模拟器,然后构建并运行应用程序。然后,点击Geo Fencing按钮。

除了按钮图标改变颜色,显然没有任何反应。如果尚未显示,请打开Xcode控制台。在执行toggleLocationTracking()时遇到的打印语句会打印出警告:

WARNING: ensure that the Razeware Mobile Kiosk is at a location near you, otherwise geofencing and beacon detection won't work The current location is: Pisa (43.7153187, 10.4019739)

如前所述,RMK位置已经静态地设置在比萨斜塔附近。我们可能想知道:我是否必须去那里测试应用程序?当然,请随意去,但是在这个项目中测试地理围栏的方法比较简单一些。

Xcode中,打开位于res组中的TestLocations.gpx。它应该如下所示:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
      <wpt lat="43.72281" lon="10.3958585">
            <name>Lungarno, Pisa</name>
            <time>2010-01-01T00:00:00Z</time>
      </wpt>
      <wpt lat="43.7153187" lon="10.4019739">
        <name>Piazza dei Miracoli, Pisa</name>
        <time>2010-01-01T00:00:30Z</time>
       </wpt>
<wpt lat="43.72281" lon="10.3958585">
  <name>Lungarno, Pisa</name>
  <time>2010-01-01T00:01:00Z</time>
    </wpt>
</gpx>

该文件包含由三个航点定义的路线:起点,目的地和返回起点。选择初始位置距离目的地约1,000米的距离,即RMK位置。

除了坐标和名称,每个航路点也有时间。绝对值的重要性并不重要,但相对差异表示从一个位置到下一个位置所需的时间。如果你仔细观察,你会注意到一个航点到下一个航点的差异为30秒。

Xcode中,我们可以通过调试区域中的“模拟位置”按钮选择此文件。这使得Xcode模拟.gpx文件中定义的路由。


如果你看一下Xcode中的控制台面板,你应该开始看到距离更新,如下所示:

Distance: 935.436896038748 
Distance: 903.575340351579 
Distance: 871.696596559219 
Distance: 839.800076468077 
Distance: 807.885154208941 
Distance: 775.951162495185 
Distance: 743.997388360152 
Distance: 712.023068265481

每次Core Location为刚刚实现的方法locationManager(_:didUpdateLocations)提供新位置时,都会更新。

一旦距离低于我们正在监视的区域的半径,我们应该看到如下日志:

Entered in region: CLCircularRegion (identifier:'The Razeware Mobile Kiosk', center:<+43.71531870,+10.40197390>, radius:300.00m)

这证实了设备的模拟位置已进入地理围栏区域。如果稍等一下,当设备退出该区域时,我们会看到类似的消息。

Left region CLCircularRegion (identifier:'The Razeware Mobile Kiosk', center:<+43.71531870,+10.40197390>, radius:300.00m)

作为旁注,如果我们想使用iPhone和真实位置,我们可以这样做。只有一个建议:我们可能希望查看数据/ Constants.swift并在其中设置新位置,而不是要求Razeware移动信息亭移动到我们当前位置附近。

信标

既然我们可以检测到设备何时进入或退出某个区域,那么下一步是什么?我们可能已经注意到在进入时会向用户提示警报,但仅此一点不应该证明这么多工作是正确的。

这是信标发挥作用的地方。但他们是什么?信标是一小块硬件,可定期通过蓝牙LE(低能耗)发出信号。在最简单的化身中,信号由三个静态数据字段组成:Proximity UUIDMajorMinor

就这么简单。但这对于接近检测也很有用:我们的手机可以拦截信号,识别信标并触发向注册信标的应用程序发出通知。

注意:如上所述,ProximityMajorMinor是静态数据;然而,它们可以(并且应该)在购买信标后进行更改。更新数据的过程取决于供应商。
数据可用于任何有助于特定目的的方式;然而,一个典型的模式是使用邻近度来识别我们的公司或应用程序,一个部分的主要部分,如房间,建筑物,商店和未成年人,以区分同一部分中的信标。

信标是低能耗设备;出于这个原因,他们可以在一个电池上运行几年,不间断地每天24小时,365天 - 有时甚至366天 - 广播他们的数据。由于能量低,它们的信号范围有限。因此,它们通常用于近距离检测。

检测广播信号的设备可以用一致的近似来确定它与信标的距离。距离分为三个不同的范围:

  • Far:超过10米远。
  • Near: 在几米之内。
  • Immediate: 几厘米远。

检测信标

在本项目中,我们将使用信标来检测何时接近RMK,并触发自动QR码检测。在iOS中,信标由Core Location处理,这是一个我们应该已经相当熟悉的框架。

注意:要调试和测试信标, 我们需要一个信标。可以使用备用iPhone作为信标或真正的信标; App Store中有一些应用程序可让我们将iPhone用作灯塔。我们还需要另一部iPhone来测试应用程序。

信标检测不是发现过程。当任何信标出现在雷达上时,我们不会要求核心位置通知。

相反,我们要求Core Location告知我们何时检测到特定信标,由其接近,主要和次要标识,或者我们也可以在相同的邻近UUID或接近度和主要信号下请求一系列信标。关键是我们要求Core Location监控你已经知道的信标。

核心位置定义CLBeaconRegion类,用于指定要监视的信标。只有一个信标的区域已经在data/Constants.swift中定义,打包成一个名为razeadBeacons的数组。

我们应该将beacon标识替换为我们自己的数据。最好不要重复使用我们在代码中找到的相同身份,因此至少应该为我们的信标生成一个新的邻近UUID

管理信标监控

打开LocationManager.swift。在使用MARK: - Beacons标识的扩展中,添加以下两种方法:

// 1 
func startMonitoring(beacons: [CLBeaconRegion]) {
    for beacon in beacons {
        startMonitoring(beacon: beacon)
    } 
}
// 2 
func startMonitoring(beacon: CLBeaconRegion) {
    guard CLLocationManager.isRangingAvailable() else { 
        print("[ERROR] Beacon ranging is not available") 
        return 
    }
    locationManager.startMonitoring(for: beacon)
}

上面的代码作用如下:

  • 1: 注册信标区域阵列。这循环遍历数组并将注册委托给单区域方法。
  • 2: 在检查该应用程序运行的设备上是否有该功能后,注册单个信标区域。

在此处,添加用于停止监视的方法:

func stopMonitoring(beacons: [CLBeaconRegion]) { 
    for beacon in beacons { 
        stopMonitoring(beacon: beacon) 
    } 
}

func stopMonitoring(beacon: CLBeaconRegion) { 
    locationManager.stopMonitoring(for: beacon) 
}

我们必须在ViewController.swift中调用这些方法来分别启动和停止信标监视。我们将在下一部分中执行此操作。

当检测信标时,核心位置语言被称为测距,实际上有两个不同的阶段:

  • Entering the region: 当检测到属于该区域ID的第一个信标时发生。
  • Ranging a single beacon: 在单个信标范围内发生。

第一种情况是通过之前实现的locationManager(_:didEnterRegion :)委托方法处理的。如果还记得,你把if分支留空了。现在用以下代码填写:

print("Entered in beacon region: \(region)") 
locationManager.startRangingBeacons(in: region)

这会将消息打印到控制台并启动属于我们输入的区域的测距信标。

现在,当信标在范围内或发生错误时,剩下的就是通知。在最后一个LocationManager扩展的末尾,添加以下两个委托方法:

// MARK: Beacons 
// 1 
func locationManager(_ manager: CLLocationManager,didRangeBeacons beacons: [CLBeacon],in region: CLBeaconRegion) {
    for beacon in beacons { 
        delegate?.locationManager(self, didRangeBeacon: beacon) 
    }
}
// 2 
func locationManager(_ manager: CLLocationManager,rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error) {
    print("Beacon ranging failed for region \(region) " + "with error: \(error.localizedDescription)")
}

上面代码如下:

  • 1: 当信标数组中的一个或多个信标被测距时,调用此方法。每个信标都通过LocationManagerDelegate转发到ViewController
  • 2: 如果信标范围失败,则调用此方法。要保持一致,请向控制台输出错误消息。

最后一个修复的细节:在stopMonitoring(beacon :)中,我们调用了stopMonitoring(for :)。最好还停止属于该区域的测距信标,因此在方法开头添加此行代码,就在stopMonitoring之前:

locationManager.stopRangingBeacons(in: beacon)

手动停止区域和信标监控

还记得UI中的按钮以启动地理围栏吗?我们可以使用相同的按钮来停止它 - 这已经在其处理程序方法中完成。但是,需要进行一些清理。

ViewController.swift中,滚动到toggleLocationTracking()。在最顶层的if语句的else分支中,在locationManager.stopMonitoringRegions()之后,添加:

// 1 
locationManager.stopMonitoring(beacons: Constants.razeadBeacons) 
// 2 
beaconStatusImage.isHidden = true 
beaconStatusLabel.isHidden = true

上面的代码作用如下:

  • 1: 如果激活,则停止信标监视。
  • 2: 隐藏信标状态图标和标签。

消耗信标检测

现在,信标检测是一件事,我们可以触发自动QR扫描模式。目前,在ViewController中,当用户点击屏幕时,会运行QR代码扫描尝试。是时候修改这种行为了。这是你需要做的:

  • 保留点击以关闭视频播放器功能。
  • 将方法的其余部分(负责QR码检测)移动到新的scanQRCode()方法中。

touchesBegan(_:with)开头的if语句的右括号之后,添加以下两行:

// 1 
} 
// 2 
private func scanQRCode() {

上述代码作用如下:

  • 1: 关闭touchesBegun方法。
  • 2: 开始一个新的scanQRCode方法,它“继承”以前属于touchesBegun的代码。

编译项目以确保我们在正确的位置输入代码。

响应信标通知

在响应信标通知之前,我们应确保已开始测距。无需检查代码 - 我们尚未完成此操作。设备进入受监控区域后,本应用应启动测距信标,这将在ViewController中调用locationManager(_:didEnterRegionId :)委托方法时发生。

在该方法的末尾添加此行:

locationManager.startMonitoring( beacons: Constants.razeadBeacons)

要执行自动QR扫描,我们需要重复调​​用之前创建的scanQRCode()方法,并在后续调用之间保持一定的时间间隔。这个要求闻起来像一个计时器 - 这实际上就是你要使用的。 ViewController已经包含了处理计时器的大部分代码。我们需要将其与信标范围连接起来。

找到locationManager(_:didRangeBeacon :)委托方法;当新信标在范围内时调用它。添加此实现:

// 1 
beaconStatusImage.isHidden = false 
beaconStatusLabel.isHidden = false
// 2 
switch beacon.proximity { 
case .immediate:
    beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-1") 
case .near:
    beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-2") 
case .far:
    beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-3") 
case .unknown:
    beaconStatusImage.image = #imageLiteral(resourceName: "arKit-marker-4") }
// Start auto scan, but only if the app is in the foreground 
// and there is no active billboard 
// 3 
if UIApplication.shared.applicationState == .active && (billboard == nil || billboard?.hasBillboardNode == false) {
    // 4 
    startAutoscanTimer()
}

上面的代码作用如下:

  • 1: 默认情况下隐藏有一个图标和标签,用于报告信标测距状态。因为在这种委托方法中,信标已被范围化,使它们可见。
  • 2: 为图标选择合适的图像。
  • 3: 如果应用程序在前台,并且当前没有显示广告牌......
  • 4: ...然后启动计时器,启用自动QR检测。

相反,当灯塔离开时,我们需要做相反的事情。将以下实现添加到locationManager(_:didLeaveBeacon)

// 1 
stopAutoscanTimer() 
// 2 
beaconStatusImage.isHidden = true 
beaconStatusLabel.isHidden = true

上面代码作用如下:

  • 1: 这会停止计时器,禁用自动QR扫描。
  • 2: 这会隐藏信标状态图标和标签。

当用户退出该区域时,我们希望执行相同的操作。将相同的代码添加到locationManager(_:didExitRegionId :)

stopAutoscanTimer() 
beaconStatusImage.isHidden = true 
beaconStatusLabel.isHidden = true

最后一个位置应该取消定时器,当我们手动点击Geo Fencing按钮停止检测时。这由toggleLocationTracking()方法处理。找到它,并在else分支的底部,隐藏图标和标签后,添加以下代码行:

stopAutoscanTimer()

如果计时器处于活动状态,则会停止计时

定时器和QR码检测

当计时器为红色时,我们想要开始QR码检测尝试。找到didFireTimer(timer :)方法,只需添加:

scanQRCode()

测试

要测试信标,我们需要使用物理设备。我们可以使用启动项目附带的.gpx文件来模拟路径,从而可以测试设备何时进入和离开受监控区域。

我们还需要一个信标或备用iPhone,我们可以在其上安装将其变为信标的应用程序。在App Store中有几个这样的应用程序。

准备好后,运行应用程序,然后通过Xcode调试区域中的Simulate Locations按钮加载.gpx文件。等待虚拟位置距离RMK 300米范围内。

如果不想运行模拟路线,那么有两种选择:

  • 选项#1:手动重新配置应用以使用我们附近的真实位置:
    1: 我们可以使用Google地图找到坐标,这些坐标在我们将地图置于当前位置或至少靠近它之后显示在网址中。
    2: 获得坐标后,打开Constants.swift,然后将坐标替换为实际坐标。
  • 选项#2:通过.gpx文件避免模拟路由,但改为使用静态虚拟位置进行配置:
    1: 在Xcode中,找到res组中包含的TestLocations.gpx
    2: 右键单击它,然后选择在Finder中显示。
    3: 弹出时从Finder中选择.gpx文件,然后复制并粘贴,或右键单击文件并选择复制。
    4: 将重复的文件重命名为StaticLocation.gpx,或我们选择的任何其他名称。
    5: 在Xcode中,右键单击res组,选择Add files to Chapter13”,然后选择新创建的文件,并单击 Add 按钮。
    6: 打开文件并删除第一个和最后一个 <wpt> ... </ wpt>标签。
    7: 现在,我们可以在使用
    Xcode的模拟位置功能时选择此文件。这会将我们的设备放在RMK**旁边。

无论我们选择哪种方法,当运行应用程序,并且设备位于受监控区域内时,都会激活信标监控。

只要受监控的信标有范围,就应该看到启用自动检测按钮:

这表示正在进行自动QR扫描。此外,工具栏中央会显示一个图标,通知信标在范围内:

  • 绿色: 非常近
  • 橙色: 近
  • 红色: 远
  • 灰色: 未知

我们可以使用它来手动关闭广告牌。请注意,创建广告牌时,应用会自动停止地理围栏和自动检测。

如果无法对信标进行测距,请参阅以下列表:

  • 验证信标是否已打开。
  • 确认我们的iPhone已启用蓝牙。
  • 确认我们的iPhone已启用蓝牙。验证iPhone是否位于受监控区域内,即距离Constants.swift文件中指定的位置最多300米。
  • Xcode的控制台中,检查错误消息或警告。
  • 使用我们的信标供应商的应用程序(如果有)来验证信标是否在范围内。
  • 验证信标接近度,主要和次要数据是否与Constants.swift文件中指定的数据匹配。
上一章 目录 下一章
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容