iOS Apprentice中文版-从0开始学iOS开发-第三十三课

真机测试

当我第一次写这些代码,我只是在模拟器上测试了一下,运行效果不错。当我在我的iPod上测试的时候,你猜发生了什么?效果并不好。

原因是iPod上没有GPS功能,它依赖Wifi来进行定位。但是Wifi不能给你非常精确的位置,无法精确到10米范围,我猜Wifi大概是可以最高精确到100米的范围。

目前,你只是在读取到desiredAccuracy中设置的精度后,才停止位置信息更新,但是在iPod中这一幕根本不会发生。

这就说明了你不能仅仅依赖模拟器来测试app。你需要把app安装到真实的设备上,到户外去实际的测试,特别是涉及定位的app。如果你有多台设备的话,最好都测试一遍。

为了应对刚才我介绍的情况,你需要改进一下didUpdateLocations委托方法。

将locationManager(didUpdateLocations)修改为:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
       ...
        if newLocation.horizontalAccuracy < 0{
            return
        }
        
        //这里是新的代码
        var distance = CLLocationDistance(Float.greatestFiniteMagnitude)
        if let location = location {
            distance = newLocation.distance(from: location)
        }
        
        if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
            ...
        if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
            print("*** We're Done!")
            stopLocationManager()
            configureGetButton()
            
            //这里是新的代码
            if distance > 0 {
                performingReverseGeocoding = false
            }
        }
        
        if !performingReverseGeocoding {
            ...
            //这里是新的代码
        } else if distance < 1 {
            let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp)
            if timeInterval > 10 {
                print("***Force done!")
                stopLocationManager()
                updateLabels()
                configureGetButton()
            }
        }
    }

它现在已经是一个非常长的方法了,我们刚才添加了三段代码,这是第一段:

var distance = CLLocationDistance(Float.greatestFiniteMagnitude)
        if let location = location {
            distance = newLocation.distance(from: location)
        }

这段代码计算了新的位置和上一个位置(如果有的话)之间的距离差值。我们可以使用这个差值来判断我们的位置是不是在继续提高精度。

如果前一个地址不存在,那么distance就是Float.greatestFiniteMagnitude。这是一个内建的常量,代表浮点数可以有的最大值。在首次读取到地址信息时,将距离设置为一个巨大的值是一个技巧。这样做,即使你无法计算真实的距离,之后的计算仍然有效。

在你停止location manager后,你还添加了一个if语句:

if distance > 0 {
                performingReverseGeocoding = false
            }

这段代码会对最后一个地址进行强制地址解析,即使app仍然在执行另一个解析请求。

当最后一个地址是你能找到的精度最高的地址时,它就绝对是你要的这个地址。但是之前的地址也许仍然在执行地址解析,这时就需要把它们都跳过。

通过简单的将performingReverseGeocoding设置为false,你就实现了强制的将最后一个坐标进行地址解析。(当然,如果distance等于0,那么就是说这个地址和上一个一摸一样,你也不需要对它进行地址解析)

最大的改进是在这个方法的底部:

else if distance < 1 {
            let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp)
            if timeInterval > 10 {
                print("***Force done!")
                stopLocationManager()
                updateLabels()
                configureGetButton()
            }
        }

如果当前读取到的坐标和上一个差距不大,并且这种情况维持了10秒,你就强制停止location manager。

如果你无法获得一个更加精准的坐标,那么你就停止获取坐标,这样做是没问题的。

这个改进对iPod是非常必要的,iPod会给你一个大概100米精度的坐标,然后不断的重复这个过程。

我选择10秒是因为测试过程中发现10秒就够多了。

注意一下你不能这样去判断:

} else if distance == 0 {

distance的值不可能刚好为0。它也许会是0.0017623这样的值。比起检查distance是否为0,更好的办法是检查它是不是足够的小,比如说1米以内的差距。

顺便说一下,你有没有注意到在你读取timestamp属性前先试用了感叹号对location进行解包(location!)?当app执行else if内的语句时,location的值可以确保不会为nil,所以这时强制解包是安全的。

运行app,测试一下是否运行正常。在模拟器上很难测出问题,所以最好是使用实际的设备进行测试,并且观察调试区域打印出的信息。

还有一些你可以做的改进,提高一下代码的健壮性,设置一个超出时间。你可以告诉iOS,执行一个方法最多等1分钟。如果1分钟后还是没有找到任何一个位置,你就停止location manager,并且打印报错信息。

首先添加一个新的实例变量:

var timer: Timer?

然后改变一下startLocationManager()方法:

func startLocationManager() {
        if CLLocationManager.locationServicesEnabled() {
           ...
            timer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(didTimeOut), userInfo: nil, repeats: false)
        }
    }

新的这一行设置了一个timer对象,用于60秒后发送“didTimeOut”信息给它自己;didTimeOut是一个方法,你稍后要自己提供这个方法。

selector选择器,是OC中的一个术语,用于描述方法的名称,#selector()符号就是Swift中使用选择器的方法。

改变一下stopLocationManager()方法:

func stopLocationManager() {
        if updatingLocation {
            ...
            if let timer = timer {
                timer.invalidate()
            }
        }
    }

当location manager在超出时间前就被停止了的情况下,你必须取消掉timer。这个情况发生在一分钟内已经获得了精度足够的位置的情况下,或者用户在不到一分钟的时候点击了Stop按钮。

最后,我们来添加didTimeOut()方法:

func didTimeOut() {
        print("*** Time Out")
        
        if location == nil {
            stopLocationManager()
            lastLocationError = NSError(domain: "MyLocationErrorDomain", code: 1, userInfo: nil)
            updateLabels()
            configureGetButton()
        }
    }

didTimeOut()方法总是在一分钟后被调用,无论你是否获取到了有效的位置信息,除非在1分钟内,stopLocationManager()方法被执行,取消了timer的作用。

如果一分钟后没有获取到有效的位置信息,或者用户主动停止了location manager,你创建了一个错误代码,并且更新屏幕上的标签。

通过创建自己的NSError对象,并且把它放入到lastLocationError实例变量中,你可以不用改动updateLabels()方法。

然而,你必须明确这个错误的范围,不能是kCLErrorDomain,因为这个错误不是来自Core Location,而是来自你自己的app内。

一个错误范围(error domain)就是一个字符串,就像“MyLocationsErrorDomain”。错误代码我选择了1。错误代码的值是多少并不重要,因为这是你自定义的报错,你只需要自己明白就好。你可以想象一下,如果是一个非常庞大的app,那么很可能你要定义大量的自定义报错,这时你需要用不同的code将它们区分开,但是你自己要明白每一个code代表什么。

注意一下,你并不需要总是使用NSError对象;这里有其他的方法可以让你其他部分的代码知道有错误发生了。在updateLabels()中,你一直在使用NSError,所以你有必要定义自己的error对象。

运行app,将模拟器的区域设置为None,并且点击Get My Location按钮。

一分钟后,调试区域会显示“*** Time Out”,并且Stop按钮转换为了Get My Location按钮。

屏幕上应该可以看到一条报错信息:

iOS Apprentice中文版-从0开始学iOS开发-第三十三课_第1张图片

仅仅是简单的从Core Location中获得一个位置信息,并且得到相应的街道信息是无法应对复杂的情况的。这其中存在着多种场景需要处理。没有东西可以保证这中间一帆风顺。(iOS开发有时需要钢铁般的意志力)

我们来复习一下各种场景:

1、可以找到一个精度符合要求的位置信息

2、找到的位置信息不符合要求,并且无法获取到更高精度的位置信息

3、根本无法获取到位置信息

4、搜索时间过长

现在你的代码可以处理以上所有情况,但是我确定这样还是不算完美。不要怀疑,我们还会对这些逻辑进行调整,这就是我们这个课程的意义。

我希望你已经能够理解了,如果你要发布一款基于定位功能的app,你需要做大量的测试。

设备需求

Info.plist中有一个字段,Required device capabilities,这里列出了你的app需要什么样的硬件设备支持。这个字段是App store用于决定一个用户是否可以下载你的app的。

默认值为armv7,这是代表iPhone 3GS和之后版本的CPU结构。如果你的app需要其他的功能,比如Core Location,你就需要在这里写清楚它。

打开Info.plist,添加值location-service到Required device capabilities字段中:

iOS Apprentice中文版-从0开始学iOS开发-第三十三课_第2张图片

你也可以把gps添加进去,但是一旦你添加了gps需求,那么iPod和某些版本的iPad就无法下载你的app了。

如何确定合适的硬件支持,可以在苹果的开发者网站上参考App Programming Guide手册。

PS:你现在可以把print语句都删掉或者注释掉了。个人而言,我喜欢把它们留着,以便日后调试app方便。如果你要上传app到App store的话,那么你必须移除掉print语句。

支持其他的iPhone类型

目前为止,你都是在4英寸的iPhone SE上进行测试的。

我们之前讨论过,比较旧的iPhone,比如3.5英寸的iPhone设备,不支持iPhone 10,但是你的用户也许会在iPad上运行app,这时app会强制以3.5英寸模式运行。

还有,如果你像一个专业的iOS开发者那样工作的话,你还需要让你的app支持iOS 9甚至iOS 8,这就意味着你的app必须能够完美的适应3.5英寸屏幕。

想要观察app在iPhone 4S上是如何工作的,可以打开Main.storyboard并且使用View as面板,将设备类型选择为最小的那个。

iOS Apprentice中文版-从0开始学iOS开发-第三十三课_第3张图片
在3.5英寸屏幕上运行

这样去试试是非常有必要的,因为在小屏幕上,Get My Location按钮已经不存在了。在4英寸屏幕上这个按钮已经非常靠近底部了,所以在更小的屏幕上它就无法显示了。你需要对此作出调整,否则就等着1星差评吧!

在之前的课程中,你使用过自动布局给app的用户界面添加了自适应功能。你使用的是Pin和Align菜单,通过创建约束来锁定视图的位置,效果拨群,但是你问问你能找到的任何一个iOS开发者,他们都会说管理约束实在太麻烦了,尤其是当界面元素比较复杂的时候。

幸运的是,有一个简单的办法可以使你不用去创建约束,它叫做autoresizing(自动尺寸调整)。

在有自动布局之前,自动尺寸调整被称为“springs & struts(这个不知道怎么翻译合适了)”它是用于构建用户界面自适应的主要工具。它非常简单,但是使用条件有限。然而,大多数场景中使用它已经足够了。

它的工作原理是:每个视图都有自己的自适应尺寸设置,用于决定当这个视图的父级视图或者视图容器的尺寸发生改变的时候,这个视图的尺寸和位置应该如何变化。你可以把这个视图的和它父级视图的四个边中的任何一个粘在一起,并且调整这个视图的水平线和垂直线,使它仍然可以填满整个父级视图。

从iOS 10开始,你就可以轻易的合并使用自动尺寸调整和自动布局两个功能。比起创建约束,你可以简单的设置自适应尺寸选项,并且UIKit会自动为这个设置添加约束。

我们来实际的练习一下。你将要使用自动尺寸调整功能来将Get My Location按钮固定在屏幕的底部,无论屏幕的大小。

首先在View as面板中将设备切换会iPhone SE,这样你就可以重新看到Get My Location按钮了。

选定Get My Location按钮,然后打开尺寸检查器,按照下图所示,改变自动尺寸调整的设置:

iOS Apprentice中文版-从0开始学iOS开发-第三十三课_第4张图片

你可以看到右边有一个动画示例,里边的红色方块就代表你选定的Get My Location按钮,这个红色的方块现在总是在它父级视图的底部。

使用View as,将设备选择为iPhone 4S(或者打开辅助编辑器内的Preview面板)。现在在3.5英寸屏幕上,这个按钮也可见了。

如果你觉得现在Tag Location按钮和Get My Location按钮靠的太近了,那么你可以把Tag Location按钮向上移动一点,大概Y=250的位置就可以了。

现在再使用View as面板,选择设备类型为iPhone 6S Plus(或者把模拟器选择为iPhone 7 plus 运行app):

iOS Apprentice中文版-从0开始学iOS开发-第三十三课_第5张图片

标签与屏幕的右边缘已经不是对齐状态了,Tag Location按钮也不再居中了,看起来乱糟糟的。让我们来抢救一下。

先从View as中把设备选择为iPhone SE。

把Message Label和Tag Location的自动尺寸设置调整为下图所示:

现在这个标签和按钮总是在主视图中水平居中了。

然后把Latitude goes here和longitude goes here标签调整为
![Uploading 屏幕快照 2017-09-18 上午12.02.47_194282.png . . .]:

iOS Apprentice中文版-从0开始学iOS开发-第三十三课_第6张图片

这样就可以保持着两个标签和屏幕上的右侧边缘始终对齐。

最后把Address goes here标签调整为:

这样Address goes here的宽度始终和屏幕宽度保持一致。现在运行app,在各种类型的设备上试试效果。

你可能感兴趣的:(iOS Apprentice中文版-从0开始学iOS开发-第三十三课)