欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用Swift构建带有地理定位功能的App

程序员文章站 2024-03-22 23:01:52
...

英文来源:Building a Geo Targeting iOS App in Swift

作者简介: Eugene Trapeznikov,高级iOS开发工程师,他不断学习新技术,范围从移动开发到web敏捷实践(持续集成和行为驱动开发)。在过去的四年中,发布了超过10款App,其中两款被Apple Store中的“精品推荐(featured)”收录。

地理定位是根据用户所处的地理位置,来显示不同的内容的一种方法。比如根据国家、地区、城市或者其他标准来定位。地理定位应用在很多地方。想象一下,一位客户正去造访你的竞争对手,我们可以给予他特别的优惠,把他吸引过来。再比如,如果用户在过去的几天里,跑了好几家汽车分销商,这就说明他想要买一辆新车。因此我们可以向他展示我们的汽车广告。这样有目标的广告投放总比随机投放要有效得多。

在这篇文章中,我们会教你如何在iOS设备中实现地理定位,会向你介绍苹果标准库CLRegion。并且,我们还会教你如何测试不常见的功能特性。还会看到如何实现复杂的追踪逻辑。最后,我们还会教你如何创建自定义的区域,并向你解释什么情况下自定义的区域要优于CLRegion。你可以使用地理定位功能开发许多带有创新性定位功能的App。

使用Swift构建带有地理定位功能的App

创建工程

假定要追踪用户去饭店。下面开始创建工程,创建以GeoTargeting命名的单视图应用。打开Main.storyboard文件,在View上面拖拽一个Map Kit View。在ViewController.swift中为它创建@IBOutlet。编译的时候会报错,不过先不用管,storyboard就先这样,我们一起去看看ViewController.swift

我们要把MapKitCoreLocation的库加上去。并且在View Controller类中加上它俩的协议:

class ViewController: UIViewController, MKMapDelegate, CLLocationManagerDelegate {
    @IBOutlet weak var mapView: MKMapView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

接下来我们创建mapView和locationManager实例:

//1. create locationManager

    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        // 2. setup locationManager
        locationManager.delegate = self
        locationManager.distanceFilter = kCLLocationAccuracyNearestTenMeters
        locationManager.desiredAccuracy = kCLLocationAccuracyBest

        // 3. setup mapView
        mapView.delegate = self
        mapView.showsUserLocation = true
        mapView.userTrackingMode = .Follow

        // 4. setup test data
        setupData()

在ViewController.swift文件中做了下面这些事情:

  1. 创建了locationManager实例,用来探测用户位置的变化。
  2. 将locationManager的代理指向了ViewController,这样就可以通过View Controller追踪用户的位置和区域范围了。另外,将定位服务的精准度设置成了最佳精准度(kCLLocationAccuracyBest)。
  3. 创建了mapView实例,设置了它的代理,这样就可以使mapView单独绘制图形。让mapView显示用户的位置,使我们能够对其进行追踪。这也让人更容易了解用户是否在某个区域内。也可以在Storyboard中设置MapView的代理,使用showsUserLocation属性显示用户的位置信息。用代码实现更直观。
  4. 创建了测试数据,创建的方法在后面实现。

接下来就要追踪用户的地理位置了。在这之前,我们需要检查是否有权限这样做。

    override func viewDidAppear(animated: Bool) {
        // 1. status is not determined
        if CLLocationManager.authorizationStatus() == .NotDetermined {
            locationManager.requestAlwaysAuthorization()
        }
        // 2. authorization were denied
        else if CLLocationManager.authorizationStatus() == .Denied {
            //showAlert("Location services were previously denied, please enable loaction services")
        }
        // 3. we do have authorization
        else if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
            locationManager.startUpdatingLocation()
        }
    }

我们来仔细分析一下。首先,在viewDidAppear方法中检查了授权状态。用户在设置中将授权状态修改后我们还可以对授权状态再次检查一遍。

因此,可能出现下面几种情况:

  1. 如果用户从未选择或设置过授权状态,那么我们就要设置“Always”权限。在CLRegion使用状态监控,也只能这样设置。
  2. 如果定位权限之前被用户拒绝使用了。我们就要通知用户,允许使用地位服务,App会获得更佳的工作状态。这里使用了showAlert(title: String)方法。这也是UIAlertController来现实信息的便捷做法,还可以带有“Cancel”按钮。我们稍后会在几个地方用到该方法。
  3. 如果获得了定位权限,那么久可以使用startUpdatingLocation()方法了。

现在可以运行App了。你可能会问,并没有弹出授权定位的提示框呀?这需要打开...-Info.plist文件,在文件中添加一行,NSLocationAlwaysUsageDescription,然后把它作为key,添加value。例如:“Regions needs to always be able to access your location.”。当使用requestAlwaysAuthorization()方法时会用到上面的Plist信息。否则系统就会无视定位请求。这个键值对描述了系统使用定位功能的原因。

在Plist里面添加了上述信息后,再次运行App就可以弹出设置的描述信息了。

当然,App中,对定位授权的情况可能有很多种,我们这里所选择的是最基本的一项。

接下来我们就要对地理位置进行监控了。

func setupData() {
        // 1. check if system can monitor regions
        if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion) {
            // 2. region data
            let title = "Lorrenzillo' s"
            let coordinate = CLLocationCoordinate2DMake(37.703026, -121.759735)
            let regionRadius = 300.0

            // 3. setup region
            let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude), radius: regionRadius, identifier: title)

            // 4. setup annotation
            let restaurantAnnotation = MKPointAnnotation()
            restaurantAnnotation.coordinate = coordinate;
            restaurantAnnotation.title = "\(title)";
            self.mapView.addAnnotation(restaurantAnnotation)

            // 5. setup circle
            let circle = MKCircle(centerCoordinate: coordinate, radius: resionRadius)
            self.mapView.addOverlay(circle)
        }
        else {
            print("System can't track regions")
        }

        // 6. draw circle
        func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
            let circleRenderer = MKCircleRenderer(overlay: overlay)
            circleRenderer.strokeColor = UIColor.redColor()
            circleRenderer.lineWidth = 1.0
            return circleRenderer
        }
    }

我们一步步地来看,setupData()里面都做了哪些事情:

  1. 无论如何都要检查当前设备是否支持对制定区域进行位置监控。如果用户拒绝了来自系统的定位请求,那么isMonitoringAvailableForClass就会返回false,包括把“后台应用刷新”的功能禁掉,或者在飞行模式的情况下。
  2. 我们在这里创建了一个restaurant用来测试,本文中这样做无可厚非,但在实际项目中,你需要为被监控对象创建单独的类。
  3. 我们还创建了监控区域,使用restaurant的名称作为被监控区域的ID。由于这事探测用户所到区域的唯一方法,我们需要这样做。不要把强引用存入CLRegion,而可能只要记下区域的ID,以备后用。
  4. 为了视觉效果更好,我们在区域的中心位置添加了说明性的信息。
  5. 同样,我们在地图上也添加了圆环,表示区域的边界。
  6. 绘制圆环调用的是MKMapViewDelegate的代理方法。

这些都是工程里的准备工作,接下来,我们就要对区域进行监控了。

苹果的CLRegion类

本文中,我们在对地理位置进行监控。苹果的CLRegion类是针对已知区域特定半径的圆形区域。所以只能使用CLRegion监控圆形区域。

    // 1. user enter region
    func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
        showAlert("enter" \(region.identifier)")
    }
    // 2. user exit region
    func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
        showAlert("exit \(region.identifier)")
    }

添加这两个方法是为了通知用户进入或是离开了被监控的区域。使用简单的提醒方式只是为了方便查看。给圆形区域周边加个过渡区域,用户在边缘移动的时候,就不会收到大量进出边界的消息,这种情况下,给出进出消息的这两个方法不会被调用。

还要注意同时监控多个位置的情况。一个App最多只能监控20个位置,这些位置在用户所在位置的周边。当用户所在位置发生变化时,可以去掉较远的位置,把用户移动轨迹上的位置添加进来。如果监控的位置数量到达了上限,LocationManager会调用monitoringDidFailForRegion方法。这样做可以获得更好的用户体验。

刚才提到的两个方法,还有一些限制,都是需要注意的。

基本的方法都已经创建好了。现在你可以驾车去被监控的区域了吧!呵呵,开玩笑的,有一种便捷方法来测试功能。

如何测试

在Xcode当中,有一种简便的方法,那就是使用GPX文件。在开发过程中,GPX是描述GPS的常见数据格式,是XML文件。看看再说:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
    <wpt lat="37.702801" lon="-121.751862"></wpt>
    <wpt lat="37.702821" lon="-121.752321"></wpt>
    <wpt lat="37.702840" lon="-121.752780"></wpt>
</gpx>

这个GPX文件中,在我们所在区域附近有三个定位点。Xcode会逐一扫描并移动用户的位置。每秒移动一个位置。在Xcode中可以自行创建GPX文件。

再次运行App,在Xcode中打开“Debug Area”(界面下方),选择“Simulate Location”,选择刚刚添加的GPX文件。再看看模拟器,用户的位置应该发生了变化。

使用Swift构建带有地理定位功能的App

几秒钟后,你就会看到“enter Lorrenzillo’s”这样的提示信息。然后再过几秒钟,会提示“exit Lorrenzillo’s”。所以,位置监控功能已经工作了,恭喜!

使用Swift构建带有地理定位功能的App

给被监控区域添加复杂的业务逻辑

对于一些App来说,监控进入或离开事件已经足够了。但如果需要更复杂的业务逻辑,又该如何呢?也许你会关注用户在某个区域的停留时间,或者在该区域内移动的平均速度。在我们的示例中,会检查用户是否在饭店中停留了足够长的时间,这意味着用户是否造访了饭店,我们可以从用户那里收集反馈信息。下面对代码来做一些改进:

    var monitoredRegions: Dictionary<String, NSDate> = [:]
    // 1. user enter region
    func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
        //showAlert("enter" \(region.identifier)")

        // 2.1 Add entrance time
        monitoredRegions[region.identifier] = NSDate()
    }

    // 2. user exit region
    func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
        //showAlert("exit \(region.identifier)")

        // 2.2 Remove entrance time
        monitoredRegions.removeValueForKey(region.identifier)
    }

    // 3. Update resions logic
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        updateRegions()
    }
  1. 在字典中存储用户进入监控区的时间。
  2. 在这里实现了时间的添加和删除功能。
  3. LocationManager的代理方法didUpdateLocations会帮助我们检查用户在监控区域内是否停留了足够的时间。
func updateRegions() {
        // 1. 
        let regionMaxVisiting = 10.0
        var regionsToDelete: [String] = []
        //2.
        for regionIdentifier in monitoredRegions.keys {
            //3.
            if NSDate().timeIntervalSinceDate(monitoredRegions[restorationIdentifier!]!) > regionMaxVisiting {
                //showAlert("Thanks for visiting our restaurant")    
                regionsToDelete.append((regionIdentifier))
            }
            // 4.
            for regionIdentifier in regionsToDelete {
  monitoredRegions.removeValueForKey(regionIdentifier)
            }
        }
    }
  1. 假定10秒钟对用户来说已经足够了。我们也需要变量来存储用户的位置,就是用户停留足够长时间的区域,将来会删除掉,因为我们已经达到了目的。
  2. 遍历所有当前被监控的区域。
  3. 如果用户在被监控区域停留了足够长的时间,我们就会向用户显示特定的消息,并把该区域标记为待删除区域。
  4. 删除所有待删除区域。

现在你就可以完全用updateRegions方法来实现任意复杂度的监控方法了。

自定义监控区域

令人遗憾的是,苹果的CLRegion有很多限制。其中之一就是用户如果对App设置了“While in Use”访问权限,你就无法监控用户所在位置了。我们都知道,很多用户担心它们手机的电池寿命,从而不想让App一直开启监控。真的很难向用户解释说,一直开启位置监控会有对他们有益处。这也是为什么有时你只在用户使用App的时候才监控用户的位置。我建议你创建自定义的区域类,像CLRegion那样带有类似接口和回调方法的类。这样就会容易理解自定义类的功能,就像其它开发人员所熟悉的CLRegion类一样。

protocol RegionProtocol {
    var coordinate: CLLocation {get}
    var radius: CLLocationDistance {get}
    var identifier: String {get}

    func updateRegion()
}
protocol RegionDelegateProtocol {
    func didEnterRegion()
    func didExitRegion()
}

你可以使用上面的协议,轻松地替换掉CLRegion。

CLRegion另外一项限制是我们只能监控圆形区域。有时我们需要监控多边形(四边形、五边形等)或者椭圆形区域。这种情况依旧可以使用自定义的类。只要使用RegionDelegateProtocol中的didEnterRegion方法,检查是否进入了被监控区域即可。

另外,也没有必要立刻就向用户显示进出警告信息。我们要为今后的数据分析存储这些数据,这里有很多用例可以去实现。

你可以从这里获得完整的Xcode project。

译者简介:白云鹏,移动应用开发者,个人博客:http://baiyunpeng.com

审校/责任编辑:唐小引(@唐门教主),欢迎技术投稿、约稿,给文章纠错,请发送邮件tangxy#csdn.net(请将#更换为@)。

第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。

使用Swift构建带有地理定位功能的App