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

先插播一个小话题——异步操作(Asynchronous operations)

有时app做某件事的时候会花一点时间。在你开始一个操作后,你必须等待它给你返回结果,如果你运气不够好,也许永远都等不到返回结果。

比如Core Location这个例子,它会花费两到三秒得到第一个精度不太高的位置信息,然后再一点点的提高精度。

异步的意思就是,当你执行类似这样的操作后,app不再等待,而是该干嘛干嘛,用户界面依旧可以响应用户的其他操作,新的事件可以立即被处理,用户仍然可以点击app中的其他部分。

异步进程通俗的讲,就是把操作放入后台。一旦这个操作结束,app会通过委托注意到它。

与异步相反的是同步(synchronous)。如果一个操作是同步的,那么app不会再接收其他操作,直到等待这个同步操作完成为止。从效果上看,就是app被不会动了。

假如CLLocationManager是同步操作的话,那么它会带来一些大问题:当你得到一个位置信息前,app会有几秒钟时间完全失去响应。类似于这种假死,对用户是非常不友好的。

例如:MyLocation app的底部有一个tab bar。如果在获取位置信息时,app假死了,那么你点击tab bar切换到其他页面就不会有响应。对用户而言,应该在任何时候都可以随意切换页面才是良好的使用体验,但是现在你的app却假死了,甚至挂了。

iOS的设计师决定了类似这种行为是无法接受的,因此所有需要花费一点时间执行的东西都以异步的方式管理。

在下一个课程中,我们会讲一些对于网络的操作,比如从internet上下载点东西,那时我们会更加深入的讨论异步进程的内容。

顺便说一下,iOS有一种叫做“看门狗定时器”的东西。如果你的app假死太长时间,看门狗计时器会杀掉你的app。

结论就是:任何会耗费一定时间的东西都应该用异步的方式处理,把它们放到后台去。

将坐标展示在界面上

locationManager(didUpdateLocations)委托方法会给你返回一个数组,里面是包含着当前经纬度的CLLocation对象列表。(这些对象还有些额外信息,比如高度和速度,但是在我们这个app中用不到这些。)

你要把数组中最后一条CLLocation对象取出来,因为这是最近的一条,并且将它展示在之前你在界面上添加好的label中。

打开CurrentLocationViewController.swift,添加一个新的实例变量:

var location: CLLocation?

你会把用户目前的位置存到这个变量里。

这个变量必须是可选型,因为有可能获取位置信息会失败,比如用户目前身处撒哈拉沙漠这种既没有GPS也没有基站和Wifi的地方。

将locationManager(didUpdateLocations)方法中的内容改变为:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let newLocation = locations.last!
        print("didUpdateLocations \(newLocation)")
        location = newLocation
        updateLabels()
    }

你将从location manager中得到的CLLocation对象保存到了实例变量中,并且调用了一个新的updateLabels()方法。

保留print语句,因为后面我们还会用到它。

现在我们来添加updateLabels():

func updateLabels() {
        if let location = location {
            latitudeLabel.text = String(format: "%.8f",location.coordinate.latitude)
            longitudeLabel.text = String(format: "%.8f", location.coordinate.longitude)
            tagButton.isHidden = false
            messageLabel.text = ""
        } else {
            latitudeLabel.text = ""
            longitudeLabel.text = ""
            addressLabel.text = ""
            tagButton.isHidden = true
            messageLabel.text = "Tap 'Get My Location' to start"
        }
    }

因为location实例变量是可选型的,所以你使用if let语句来对它进行解包。

注意一下,解包后的变量名称和可选型变量名称一样,是ok的,这里它们都叫做location。在if语句内,location引用一个实际的CLLocation对象,它不会为nil。

如果location对象是有效的,那么你就将latitude和longitude从Double转换为String,然后将它们放入标签中。

之前我们可以通过字符串插值的方式将值放入字符串,所以为什么我们在这里没有使用字符串插值呢?比如像下面这样:

latitudeLabel.text = "\(location.coordinate.latitude)"

这样做确实可以,但是有个坏处是你无法控制latitude的精度。对于我们这个app,我们希望经纬度都保留8位小数就可以了。

为了达到这个目的,你需要使用所谓的字符串格式化。

和字符串插值一样,字符串格式化中的占位符会在运行时被实际的值替代。这些占位符,或者说格式说明符,有点复杂。

为latitude标签创建文本时,你使用了:

String(format: "%.8f",location.coordinate.latitude)

这行代码创建了一个新的字符串对象,使用"%.8f"格式说明符,字符串中插入的值就是location.coordinate.latitude。

占位符一定是由一个百分号%开始。比如常见的%d用于整数,%f用于浮点数,%@用于任意对象。

字符串格式化在Object-C中非常常见,但是在Swift中就非常少,因为字符串插值非常简单,而且足以应对大多数情况,但是字符串插值存在死角,比如这里,就必须用字符串格式化。

%.8f格式说明符的作用和%f一样,都是将浮点数放入字符串,.8的意思是该浮点数仅保留8位小数。

运行app,点击Get My Location按钮,然后你就会看到经纬度已经展示在界面上了。

当这个app启动时,location对象并不存在(location为nil)因此我们应该在app的顶部展示“Tap ‘Get My Location’ to Start”信息来提示用户。但是现在app并没有展示这个信息,那是因为你在没有接受到坐标信息时,都没有调用updateLabels()方法。

你应该在viewDidLoad()中调用updateLabels():

override func viewDidLoad() {
        super.viewDidLoad()
        updateLabels()
    }

运行app。这时在app一开始运行,你就可以看到“Tap ‘Get My Location’ to Start”信息了,此时latitude和longitude仍然是空的。

处理错误

获取GPS坐标,是非常容易出错的。也许你在一些场所比如高楼大厦中,信号不好,无法接受GPS信号。也许你的附近没有Wifi,情况太多了。

能成功的接受GPS信号和设备的关系也很大,比如iPhone在这方面的表现就比iPod要强的多。

所以你的app比如在接受GPS信号时,具备很强的容错性能,否则任何一个细节都会让你的app挂掉,这肯定不是你和你的用户想看到的结果。并且没有任何保证说你可以顺利接受GPS信号。

这正是真实的软件开发所遭遇的问题。你必须提供容错能力,来处理GPS信息获取失败的情况。

在CurrentLocationViewController.swift中添加两个实例变量:

var updatingLocation = false
var lastLocationError: Error?

将locationManager(didFailWithError)修改为:

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("didFailWithError \(error)")
        
        if (error as NSError).code == CLError.locationUnknown.rawValue {
            return
        }
        
        lastLocationError = error
        
        stopLocationManager()
        updateLabels()
    }

location manager可以在多种情况下提供错误信息,你可以从Error对象中的code属性看到它们,并且从而得知错误的详细情况。(首先你要扮演NSError角色,它是Error的子类,其中包含具体的错误信息)

Core Location发生错误的一些情况:

CLError.locationUnknown:目前的位置信息未知,但是Core Location还在努力搜索。

CLError.denied:用户拒绝了app访问位置信息。

CLError.network:找不到可用的网络。

还有其他的一些情况,就不一一例举了,你要明白的是很多原因都会导致Core Location报错。

⚠️:所有的报错信息都在CLError的枚举中有定义。回忆一下,一个枚举(an enumeration, or enum),是值和名称的一个列表。
Core Location使用整数来表示错误代码以外,还使用CLError枚举给所有的错误都给了一个名称,使得错误的信息比较容易理解。
将这些名称转换为整数时,你需要使用rawValue。

在你的locationManager(didFailWithError)方法中,你使用了如下代码:

if (error as NSError).code == CLError.locationUnknown.rawValue {
            return
        }

CLError.locationUnknown错误意味着location manager无法立即获取一个位置信息,但是还在努力搜索,并不是说获取失败了。

当你遇到这种错误,你需要保持搜索状态,直到你获得一个地址信息,或者获得其他的报错信息。

对于其他报错信息,你将error对象存储到新的实例变量lastLocationError中:

lastLocationError = error

这样,你就可以得知你面对的是什么情况了。在这里使用updateLabels()非常必要。你要向用户展示这些错误的信息,因为用户得不到反馈信息的话,会立刻卸载掉你的app。

练习:你能解释为什么lastLocationError是个可选型吗?

答案:当没有报错时,lastLocationError不会有值。也就是说它可能会为nil,而只有可选型变量才可以将nil作为其值。

最后,你调用了两个方法:

stopLocationManager()
updateLabels()

这里的stopLocationManager()是新添加的。如果无法获取用户的位置信息,那么你应该停止location manager。为了节省电池的电量,应该在不需要GPS功能时,立刻将其中断掉。

如果是那种需要持续获取用户位置信息的app,则需要保持location manager持续工作,说不定走几公里就有信号了呢。

但是对于我们这个app而言,用户如果发生了位置变动,那么他只需要再点击一次Get My Location按钮就可以了,无须持续保持location manager工作。

我们现在来添加具体的stopLocationManager()方法:

func stopLocationManager() {
        if updatingLocation {
            locationManager.stopUpdatingLocation()
            locationManager.delegate = nil
            updatingLocation = false
        }
    }

这里有一个if语句,用来检查bool型变量updatingLocation是true还是false。如果为false,那么就是说location manager没有工作,也没有必要去中断它。

updatingLocation作用是,在获取位置信息时,用它来改变Get My Location按钮的状态,以及message标签的信息,这样用户就可以知道app的工作状态,而不是一无所知。

改变一下updateLabels()方法,添加展示错误信息的部分:

func updateLabels() {
        if let location = location {
            ...
        } else {
            latitudeLabel.text = ""
            longitudeLabel.text = ""
            addressLabel.text = ""
            tagButton.isHidden = true
            
            //新代码从这里开始
            let statusMessage: String
            if let error = lastLocationError as? NSError {
                if error.domain == kCLErrorDomain && error.code == CLError.denied.rawValue {
                    statusMessage = "Location Services Disabled"
                } else {
                    statusMessage = "Error Getting Location"
                }
            } else if !CLLocationManager.locationServicesEnabled() {
                statusMessage = "Location Services Disabled"
            } else if updatingLocation {
                statusMessage = "Searching..."
            } else {
                statusMessage = "Tap 'Get My Location' to Start"
            }
            messageLabel.text = statusMessage
        }
    }

新增的代码决定展示在屏幕上的信息是什么。它使用了一大堆if来指出目前app的状态。

如果location manager报错了,那么message标签会显示出报错信息。

首先我们检查CLError.denied,看看用户是否授权你的app可以使用位置信息。这是最要紧的一点,所以你要优先检查它。

如果报错为其他信息,你则展示“Error Getting Location”来表示无法获取到用户的位置信息。

然后你要检查的是,用户是直接关闭了位置服务,并不是针对某个app,而是在设置中直接关闭了位置服务,你通过locationServicesEnabled()方法来检查这个状态。

假设没有报错,一切都正常进行,那么标签应该现实“Searching...(搜索中)”。

没人喜欢等待,所以最好每一步都向用户展示出来,这样用户就会知道app此时正在做什么,而不是急于卸载它。

⚠️:这里我们将所有的逻辑都放入了一个方法中,是因为便于统一管理,以后界面上要展示什么新的东西,直接改这一个地方就可以了,当获取到位置信息时,或者获得一个报错时,都是调用updateLabels()。

同时添加一个startLocationManager()方法。我建议你就将这个方法写在stopLocationManager()上面,将相关功能的方法放在一起,便于阅读。

func startLocationManager() {
        if CLLocationManager.locationServicesEnabled() {
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
            locationManager.startUpdatingLocation()
            updatingLocation = true
        }
    }

之前我们使用getLocation()方法来启动location manager。因为你现在有了一个stopLocationManager()方法,所以我们最好将相关的功能都分别归类,添加这个startLocationManager()是为了保持逻辑上的平衡。

改动一下getLocation()方法:

@IBAction func getLocation() {
        let authStatus = CLLocationManager.authorizationStatus()
        
        if authStatus == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
            return
        }
        
        if authStatus == .denied || authStatus == .restricted {
            showLocationServicesDeniedAlert()
            return
        }
        
        startLocationManager()
        updateLabels()
    }

还有一点微小的细节需要注意。假设有一个报错并且没能获取到一个位置信息,于是你四处移动了一下,突然就可以获得位置信息了,这种情况下,最好把之前的报错信息清除掉。

在locationManager(didUpdateLocations)方法的底部,仅仅是在调用updateLabels()前,增加一行代码:

lastLocationError = nil

这样就可以在获取到一个位置信息时,把之前的错误信息全部清除掉。

运行app,当app等待获取位置信息时,顶部标签会显示“Searching...”,直到获取到一个位置信息或者一个报错信息为止。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容