iOS组件与框架——iOS SDK高级特性剖析

978-7-115-36553-8
作者: 【美】Kyle Richter Joe Keeley
译者: 袁国忠
编辑: 傅道坤

图书目录:

详情

本书讲解了如何使用iOS的强大组件和框架来开发杰出的app,使其具备优异的性能、可靠性、交互性,并对媒体提供支持。本书清晰地向读者介绍了一些开发app所用到的复杂中高级技术,并通过大量实例项目来显示,将这些技术集成到iOS应用中的方法。本书为想要开发高性能iOS app的人员提供了实用的技巧、可重用的代码以及专家级的建议。

图书摘要

PEARSON

iOS组件与框架——iOS SDK高级特性剖析

iOS Components and Frameworks Understanding the Advanced Features of the iOS SDK

[美]Kyle Richter Joe keeley 著

袁国忠 译

人民邮电出版社

北京

图书在版编目(CIP)数据

iOS组件与框架:iOS SDK高级特性剖析/(美)里克特(Richter,K.),(美)基利(Keeley,J.)著;袁国忠译.--北京:人民邮电出版社,2014.9

ISBN 978-7-115-36553-8

Ⅰ.①i… Ⅱ.①里…②基…③袁… Ⅲ.①移动终端—应用程序—程序设计 Ⅳ.①TN929.53

中国版本图书馆CIP数据核字(2014)第167886号

版权声明

Kyle Richter,Joe Keeley:iOS Components and Frameworks:Understanding the Advanced Features of the iOS SDK

Copyright © 2014 Pearson Education,Inc.

ISBN:0321856716

All rights reserved.No part of this publication may be reproduced,stored in a retrieval system,or transmitted in any form or by any means,electronic,mechanical,photocopying,recording,or otherwise without the prior consent of Addison Wesley.

版权所有。未经出版者书面许可,对本书任何部分不得以任何方式或任何手段复制和传播。

本书中文简体字版由人民邮电出版社经Pearson Education,Inc.授权出版。版权所有,侵权必究。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。

◆著 [美]Kyle Richter Joe Keeley

译 袁国忠

责任编辑 傅道坤

责任印制 彭志环 焦志炜

◆人民邮电出版社  北京市丰台区成寿寺路11号

邮编 100164  电子邮件  315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京艺辉印刷有限公司印刷

◆开本:800×1000 1/16

印张:29.75

字数:622千字  2014年9月第1版

印数:1-2500册  2014年9月北京第1次印刷

著作权合同登记号 图字:01-2014-4925号

定价:89.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

内容提要

本书讲解了如何使用功能强大的iOS组件和框架来开发杰出的应用,使之具备优异的性能、可靠性、交互性,并对媒体提供支持。本书清晰地介绍了一些开发应用所用到的复杂中高级技术,并通过大量实例项目来演示将这些技术集成到iOS应用中的方法。

本书分为25章,其内容涵盖了使用UIKit Dynamics提供基于物理现象的动画效果;充分利用Core Location、MapKit和地理围栏;利用排行榜和成就等Game Center功能;让用户能够在应用中访问地址簿和媒体库;使用轻量级 JSON 在服务器、应用和网站之间传输数据;使用UIDocument和键/值存储同步通过iCloud同步应用;使用钥匙串保护数据;使用通知通告用户与应用相关的重要事件;使用Core Data在本地存储和检索持久化数据;使用Objective-C高级特性编写更简洁、更易于管理的应用;使用GCD提供的并发功能提高应用的响应速度;使用TextKit进行发杂的文本处理和显示;使用Xcode 5和Instruments卓有成效地调试应用;使用PassKit创建凭证等内容。

本书为想要开发高性能 iOS 应用的人员提供了实用的技巧、可重用的代码以及专家级的建议。本书适合具有一定iOS开发经验,要想进一步提升其开发技能的从业人员阅读。

2008年,iOS SDK的前身iPhone SDK Beta版面世,当时我主要致力于编写Mac桌面应用,没有过多地考虑移动应用开发,但从那时起我一直在使用iOS SDK。

既然选择了做早期使用者,就得自力更生。那时 Apple 提供的文档很少,而要使用 SDK,就必须签署保密协议(NDA,它俨然成了解码魔戒),这使得早期使用者更只能依靠自己:通过Google搜索不到相关内容;无法前往StackOverflow寻求帮助;根本就没有介绍SDK的图书。

Apple推出iPhone 6年(确实只有6年)后的今天,情况有了很大改观:iPhone SDK变成了iOS SDK;探讨iOS开发的图书、博客、播客和会议随处可见。然而,从2009起,WWDC入场券始终一票难求,开发人员要想了解即将推出的新功能更难了——无论对新手还是老手来说都如此。对iOS开发人员来说,需要学的东西实在太多。

身为iOS开发人员,及时地掌握所有组件和框架是我面临的最大挑战之一。iOS人机界面设计指南(Human Interface Guideline,HIG)原本能够提供这方面的帮助,但其深度和广度都很有限。当前,通过Google搜索或浏览StackOverflow,确实能够找到一些答案,但这些答案通常只告诉您如何做,很少说出个所以然,更别说您亟需知道的细节了。

在本书中,Kyle和Joe填补了这种空白,他们提供了丰富的细节,可帮助您全面认识重要的iOS SDK组件。

我有并认识 Kyle和Joe 多年。他们是我见过的最聪明的开发人员,多年来开发了不少出类拔萃的应用,还一如既往地与iOS 开发社区分享其掌握的知识:在会议上发表演讲、编写iOS开发著作。如果您遇到iOS开发方面的问题,Kyle和Joe很可能能提供答案。

然而,Kyle和Joe之如此杰出,凭借的不仅仅是百科全书式的iOS知识,还有乐意与任何人分享知识的品质。在Kyle和Joe的字典里,没有竞争对手,只有朋友。

本书充分展露了Kyle和Joe渊博的iOS SDK知识,这也是我喜欢本书的原因之一。本书详细介绍了涉及的每个组件,其详尽程度之高绝非网上资源可比。

我也喜欢本书的组织结构。本书并不适合从头到尾地阅读;您只需在遇到搞不定的问题(如实现集合视图或在后台线程中执行任务)时求助于它即可。您在需要时拿起它,找到解决方案幵编写实现代码后可将其束之高阁,直到再次需要它。对任何iOS开发人员来说,无论其水平高低,本书都是必备的参考资料。您可能认为自己掌握了Core Location和MapKit,但我敢肯定您将通过本书学到以前根本不懂的知识。

Kyle和Joe不自负、不自夸,从不表现得比任何开发人员都优秀。他们身上浸透着Mac和iOS开发人员社区的精神,这种精神让Mac和iOS开发人员社区成了计算行业最友好、最有帮助的社区之一。本书再次展示了Kyle和Joe渴望与人分享知识的品质。

我总是将本书与Marks、LaMarche、Sadun的开天辟地之作一起,放在触手可及的地方。2008年我开始学习iOS应用开发时,如果能够参考本书,那该多好!拥有本书对您来说真是并事。

——Kirby Turner

White Peak Software首席程序猿;著有Learning iPad Programming,A Hands on Guide to Building Apps for the iPad,Second Edition;Cocoa开发人员社区的组织者,热衷于参加各种会议

于2013年8月28日

关于作者

Kyle Richter是备受赞誉的iOS和Mac开发公司Dragon Forged Software的创始人,还是iOS应用开发咨询公司Empirical Development的联合创始人。Kyle于20世纪90年代初涉足编程,一直致力于Mac平台的开发。他著有多部iOS开发方面的著作,还在众多深受欢迎的开发人员博客和网站上撰写文章。他管理着一个由20多名专职iOS开发人员组成的团队,还负责三家程序开发公司的日常运营工作。Kyle 在世界各地就程序开发和创业发表演讲,当前与博德牧羊犬Landis居住在基韦斯特。

Joe Keeley是Dragon Forged Software公司的CTO,还是Empirical Development的项目负责人,当前正致力于开发Resolve和Slender,还领导大家为客户完成了众多项目。从接触到Apple II起,Joe 就爱上了编码;在整个职业生涯中,他使用过各种技术,参与开发了大量系统项目。在美国各地召开的iOS和Mac会议上,他就多个不同的技术主题发表过演讲。Joe现与妻子和两个女儿居住在科罗拉多州丹佛市,并渴望重拾竞技击剑这项业余爱好。

献辞

谨将本书献给我的合著者,是他驱使着我对本书稿一再修改完善。

——Kyle Richter

谨将本书献给我的妻子Irene和两个女儿Audrey、Scarlet。你们无限的活力和爱是我每天前进的动力。

——Joe Keeley

致谢

如果没有众多幕后人员的巨大努力,本书就不可能付梓;虽然封面上只有两个人的名字,但本书是数十个人劳动的结晶。这里首先要感谢Trina MacDonald,如果没有她督促我们按时交稿,本书就不会有完工的一天。Pearson的编辑提供了极大帮助,他们指出了每一页中可能出现的错别字和技术问题。本书是各位编辑专心致志的结晶,他们是Dave Wood、Olivia Basegio、Collin Ruffenach、Sheri Cain、Tom Cirtin、Elaine Wiley和Cheri Clark。

这里还要感谢Langille Design(http://jordanlangille.com)的Jordan Langille,感谢他设计了第3章和第4章的示例游戏“打仙人掌”,他的努力让Game Center示例项目更加引人入胜。

为本书付出大量时间的不仅有我们两位作者,还有我们的家人和同事。这里要感谢与我们日常生活相关的每个人,感谢他们从我们的肩上接过重担,并理解著书立说需要付出大量劳动。

最后,要感谢iOS开发社区。为获得反馈或问题答案,我们常常求助于开发人员论坛、博客和同侪。如果没有iOS开发社区各位成员的艰苦努力,本书也不可能付梓。

前言

欢迎读者阅读本书。

iOS入门级图书汗牛充栋,探讨特定主题(如Core Data和安全)的专著也不少,但衔接初级和高级的著作却少得可怜。

本书介绍一些中高级开发主题,这些主题不值得出专著,这并非是因为它们平淡无奇或乏善可陈,而只是不够庞大。这包括其他图书通常不介绍的主题——从使用JSON到访问照片库,但专业iOS开发人员每天都要用到这些框架。

另外,本书还简要地介绍了一些高级主题,让开发人员能够快速上手。500 页的Core Data专著令人望而生畏,但本书第13章简要地介绍了这个主题,让您能够马上着手使用它。本书简要介绍的其他高级主题还包括调试与Instruments、TextKit、语言特性以及iCloud。

对于诸如Game Center排行榜和成就、AirPrint、音乐库、地址簿和Passbook等主题,本书也做了全面介绍。无论您是刚开发第一个iOS项目还是一位经验丰富的开发人员,都能在本书中找到感兴趣的内容。

无论您发现了错误或bug,还是有什么建议,都请通过icf@dragonforged.com与我们联系。我们期待您的改进建议,并将为提高本书的质量不断努力。

必须具备的知识

我们尽了最大努力来确保本书的示例和解释简单易懂,但这毕竟是部中高级著作,要读懂它,必须对iOS开发、Objective-C和C语言有基本了解,并熟悉Xcode、开发人员门户(Developer Portal)、iTunes Connect和Instruments等工具。要学习Objective-C和iOS基本技能,请参阅Stephen G.Kochan的著作Programming in Objective-C以及Maurice Sharp、Rod Strougo和Erica Sadun合著的Learning iOS Development。

需要的软件和硬件

虽然使用iOS模拟器也能开发iOS应用,但建议至少有一台用于测试的iOS设备。

Apple iOS开发人员账户:Xcode和iOS SDK等iOS开发工具的最新版本都可从Apple开发人员门户(http://developer.apple.com/ios)下载。要将应用发布到App Store或安装到个人设备上进行测试,还需要有付费的开发人员账户,这种账户的年费为99美元。

Macintosh 计算机:要使用Xcode进行iOS开发,需要一台运行最新版OS X的Mac计算机。

Internet 连接:很多 iOS 开发功能都要求用于开发的Mac 计算机和目标 iOS 设备有Internet连接。

组织结构

几乎每章都自成一体,只有介绍Game Center和Core Data的几章例外。读者可从头到尾地阅读本书,也可根据兴趣和需求阅读相关的主题;本书是为方便查阅而编写的,可助您完成众多常见的iOS开发任务。

对各章的内容概述如下。

第1章,“UIKit Dynamics”:iOS 7引入了UIKit Dynamics,可用于给UIView添加类似于现实世界的动画和行为。在本章中,您将学习如何给标准对象添加力学动画、物理属性和行为。本章按从易到难的顺序演示了7种行为——首先是重力,最后是物体属性。

第2章,“Core Location、MapKit和地理围栏”:在iOS 6中,Apple引入了自己提供的地图和地图数据。本章介绍如何使用Core Location来确定设备的位置、如何在应用中显示地图以及如何使用标注、覆盖层和说明来定制地图;还介绍了如何配置区域监视(地理围栏),在设备进入或离开指定区域时通知应用。

第3章,“排行榜”:Game Center排行榜为给iOS游戏或应用添加社交功能提供了简易途径。本章简要地介绍一款功能齐备的iPad游戏——“打仙人掌”,循序渐进地引导读者添加排行榜功能。您将学习实现Game Center排行榜所需的全部步骤,并对如何使用自定义界面显示排行榜有大致认识。

第4章,“成就”:本章改进第3章介绍的游戏“打仙人掌”,您将学习如何在功能齐备的iPad游戏中实现Game Center成就。本章首先介绍如何使用iTunes Connect显示成就进度,提供了马上着手实现成就所需的全部知识。

第5章,“地址簿”:对众多现代项目来说,集成用户的通讯录是至关重要的一步。Address Book是最古老的iOS框架之一,本章将介绍如何使用这个框架。您将学习如何使用联系人选择器、如何访问地址簿原始数据以及如何修改和保存这些数据。

第 6 章,“使用音乐库”:本章介绍如何在应用中访问用户收藏的音乐,包含如何使用音乐的元数据以及如何选择并播放音乐。

第7章,“使用和分析JSON”:JSON(JavaScript Object Notation)是一种在不同计算平台和架构之间传输数据的轻量级方式,已成为 iOS 客户端应用同服务器交换复杂数据集的首选方式。本章描述如何根据既有iOS对象创建JSON以及如何将JSON转换为iOS对象。

第8章,“iCloud”:本章介绍如何使用iCloud在设备之间同步键/值存储和文档,包括如何配置应用以支持 iCloud、如何实现键/值存储和基于文档的存储,以及如何检测并解决冲突。

第9章,“通知”:iOS支持两种通知。一是本地通知,它能够在设备上独立地运行,不需要联网;一是远程通知,这需要一台服务器,它通过网络将推送通知经Apple推送通知服务发送给设备。本章阐述这两种通知之间的差别,并演示如何在应用中实现它们。

第10章,“使用Game Kit蓝牙联网技术”:本章详尽地介绍如何创建基于蓝牙的实时聊天客户端,让您能够使用蓝牙连接到朋友的设备,并相互发送文本消息。您将学习如何使用Game Kit蓝牙功能,查找要连接的对等设备并发送数据。

第11章,“AirPrint”:AirPrint是常被低估的iOS功能之一,它让用户能够将文档和多媒体打印到任何与AirPrint兼容的无线打印机。本章介绍如何在应用中快速而轻松地支持AirPrint,让用户能够打印视图、图像、PDF乃至渲染的HTML。

第12章,“Core Data简介”:Core Data是个庞大而令人生畏的主题,本章尝试从门外汉的角度出发,指出在什么情况下Core Data可能是不错的选择,在什么情况下使用它犹如牛刀杀鸡,还以简单易懂的方式阐述一些基本的Core Data概念。

第13章,“使用Core Data”:本章演示如何配置应用以使用Core Data、如何建立Core Data数据模型以及如何在应用中实现众多常用的Core Data工具。如果您只想马上着手使用Core Data,又不想去啃500页的专著,那么本章非常适合您阅读。

第14章,“语言特性”:自iOS面世后,Objective-C一直在不断演化。本章介绍一些语言和编译器层面的变化,指出开发人员如何以及为何要使用这些新特性。这包括用于数字、数组和字典的字面量语法;块、ARC、属性声明;一些古老而优秀的特性,如句点表示法、快速枚举和方法替换。

第15章,“使用Social Framework集成Twitter和Facebook”:集成社交功能是未来的发展趋势,几乎所有应用都应这样做。本章引领您使用Social Framework在应用中添加Facebook和Twitter功能。您将学习如何使用内置的书写器创建Twitter和Facebook消息;您还将学习如何从这两种服务获取并分析摘要信息;最后,您将学习如何使用这个框架通过自定义界面发送消息。阅读本章后,您将牢固地掌握Social Framework,能够使用Twitter和Facebook给应用添加社交功能。

第16章,“执行后台任务”:iOS 4引入了一种新功能,让应用即便不在前台也能执行任务,随后又添加了其他后台功能。本章阐述如何在应用从前台进入后台后执行任务以及如何执行iOS允许的后台活动。

第17章,“使用GCD改善性能”:如果在主线程中执行资源密集型操作,可能导致应用反应迟缓。本章阐述GCD提供的多种技术,使用这些技术可并行地执行繁重的任务,而不影响主线程的性能。

第18章,“使用钥匙串保护数据”:保护数据是应用开发中重要的一步,但常常被忽视。最近几年常常爆出新闻,说著名大型公司以明文方式存储用户的信用卡信息。本章简要地介绍如何使用钥匙串来保护用户数据,并将安全视为整个开发过程中不可或缺的一部分。阅读本章后,您将能够使用钥匙串保护用户设备上的少量数据,让他们把心放在肚子里。

第19章,“使用图像和滤镜”:本章首先介绍一些基本的图像处理技巧,然后探讨一些使用Core Image对图像应用滤镜的高级技巧。本章的示例应用让您能够探索Core Image提供的各种滤镜,还能以交互方式实时串接多个滤镜。

第20章,“集合视图”:集合视图是iOS 6新增的一个功能强大的API,它提供的工具让开发人员能够灵活地排列基于单元格的可滚动内容。除新的内容布局选项外,集合视图还提供了激动人心的动画功能,使得可以以动画方式在集合视图中增删内容以及在不同集合视图布局之间切换。本章的示例应用演示了如何创建基本的集合视图、自定义的流式集合视图以及自定义的非线性集合视图布局。

第21章,“TextKit简介”:iOS 7引入了TextKit,它比Core Text易用,同时极大地扩展了Core Text的功能。TextKit让开发人员能够在应用中提供交互式富文本,虽然这是个极其庞大的主题,但本章为您实现多种常见任务(从文本绕图到设置自定义字体属性)打下了坚实的基础。阅读本章后,您将对TextKit有深入认识,为进一步探索TextKit打下坚实的基础。

第22章,“手势识别器”:本章阐述如何在应用中使用手势识别器。手势识别器为您识别并响应手势提供了简单而清晰的途径,让您无需直接处理和解读触摸数据。另外,您还可使用手势识别器定义和识别自定义手势。

第23章,“访问照片库”:大家上传到Flickr等网站的照片数量表明,iPhone实际上已成为深受欢迎的相机。本章阐述如何在应用中访问用户的照片库以及如何处理照片和视频。本章的示例应用演示了如何重建iOS 6应用“照片”。

第24章,“Passbook和PassKit”:在iOS 6中,Apple新增了Passbook,这个独立的应用可用于存储机票、优惠券、会员卡、音乐会门票等凭证。本章阐述如何设计凭证,如何创建和分发凭证以及如何在应用中与凭证交互。

第25章,“调试和Instruments”:在应用开发中,最重要的一个方面是有能力调试和剖析应用。这个主题很少有人介绍,哪怕是只言片语。本章简要地介绍如何在Xcode中进行调试以及如何使用Instruments分析性能。首先简要地介绍了计算机bug的历史以及常用的调试技巧;然后,简要地介绍了断点和调试器命令;最后,介绍了如何使用Time Profiler剖析应用以及如何使用Leaks分析内存使用情况。阅读本章后,您将对如何诊断和调试iOS应用有清晰认识,无论是在模拟器还是设备上诊断和调试。

示例代码说明

本书的每章都自成一体,因此除第25章、第12章和第14章外,每章都有一个示例项目。第13章和第14章虽然使用的是同一个基本项目,但以不同的方式对这个项目进行了扩展。在每章开头,都简要地介绍了示例项目,并详细阐述了其中与当前主题无关但比较复杂的部分。

我们竭尽全力让示例代码简单易懂,这常常导致代码并非最优或并非解决问题的最佳方式。在这种情况下,我们将指出那些对实际应用来说不合适的做法。这些示例项目并非是最终的应用,设计它们旨在方便讨论相关的功能。示例项目没有考虑具体情况,这是有意为之的,旨在避免不相关的代码干扰读者,让读者能够将注意力集中在相关的代码上。我们竭尽所能地删除不必要的代码,将代码压缩到尽可能最少。

示例项目没有使用自动引用计数(ARC),很多读者发现这一点后大为惊讶,这也是有意为之的,因为放下内存管理的包袱比背上它容易。为满足读者的不同需求,提供了两个版本的示例代码:ARC版和非ARC版。在示例代码中,类名都使用了前缀ICF,而大多数示例项目都是根据章名命名的。

在第3章和第4章,束ID与真实应用相关联,并使用了笔者的Apple账户,这旨在确保这些示例能够正常运行;另外,与这个示例项目交互时,填写多名用户的数据可能会有所帮助。在第8章、第9章和第25章,详细介绍了必须对应用所做的配置,要让这些项目能够正常运行,您必须使用自己的开发账户配置新App ID。

下载示例代码

要下载本书源代码的最新版本,可前往https://github.com/dfsw/icf。这些代码是开源的,任何人都可下载。下载包中有两个文件夹,分别包含ARC代码和非ARC代码。每章都有一个独立的文件夹,其中有且只有一个项目。欢迎读者就这些源代码提供反馈和建议,以便我们不断地改进和优化。

安装Git及使用GitHub

Git 是一种版本控制系统,近年来日益受到大家欢迎。要克隆(clone)并使用 GitHub 上的代码,需要安装Git;当前,可从http://code.google.com/p/git-osx-installer下载Git安装程序。另外,还有多个Git GUI前端,它们都是GitHub开发的,适合不喜欢命令行界面的开发人员使用。如果您不想安装Git,也可从GitHub下载Zip格式的源代码文件。

您可前往https://github.com/signup/free免费注册GitHub账户。安装Git后,从终端命令行执行命令$git clone git@github.com:dfsw/icf.git,将源代码副本下载到当前工作目录。欢迎您提交代码,为改进示例项目做贡献。

与作者联系

如果您在阅读本书时遇到问题或者有什么建议,请给我们发电子邮件(邮件地址为icf@dragonforged.com),也可通过Twitter(@kylerichter和@jwkeeley)与我们联系。

第2章 Core Location、MapKit和地理围栏

地图和位置信息是最有用的iOS 功能,让应用能够提供相关的本地信息,帮助用户找到前进的方向。当前,有帮助用户根据具体需求查找地点的应用,有帮助用户确定行车路线的应用,有帮助用户使用特殊通勤服务的应用,还有让反复前往同一个地点变得趣味盎然的应用。Apple 推出新地图的同时,新增了一些功能强大的特性,开发人员可利用它们让应用更上一层楼。

iOS提供了两个支持定位和地图的框架。框架Core Location包含的类可帮助设备确定位置和航向以及使用基于位置的信息;框架 MapKit 为定位提供了用户界面方面的支持。Apple 地图提供了地图视图、卫星视图以及2D和3D混合视图。框架MapKit让开发人员能够管理地图标注和地图覆盖层,其中前者类似于大头针,而后者可用于突出位置、线路等。

2.1 示例应用

本章的示例应用名为FavoritePlaces,它显示设备的当前位置,让用户能够搜集喜欢的地方并在地图上查看。用户可使用Core Location对地址进行地理编码,即确定经度和维度。这个应用在用户离喜欢的地方不远时发出通知。它还在地图上显示一个绿色箭头,用户可通过拖曳这个箭头来指定下一个目的地;用户松开箭头后,应用将自动进行反向地理编码,以显示指定位置的名称和地址。

2.2 获取用户的位置

要使用 Core Location 获取设备的当前位置,需要执行多个步骤。只有在得到用户许可的情况下,应用才能获取设备的当前位置;获取设备位置前,应用还必须确保设备启用了定位服务。满足这些条件后,应用便可启动位置请求,并对Core Location提供的结果进行分析和使用。本节将详细介绍这些步骤。

2.2.1 需求和许可

要在应用中使用 Core Location,需要将框架 CoreLocation 加入项目目标,并根据需要导入CoreLocation头文件:

要在应用中使用 MapKit,需要将框架 MapKit 加入项目目标,并在所有使用它的类中导入MapKit头文件:

Core Location尊重用户隐私,仅在用户许可时获取设备的当前位置。在应用“设置”的“隐私”部分,可在设备上关闭或开启定位服务,还可禁止或允许特定应用获取位置信息,如图2.1所示。

要请求用户允许使用定位服务,应用需要让 CLLocationManager 开始更新当前位置或将MKMapView 实例的属性ShowsUserLocation 设置为 YES。如果设备关闭了定位服务,Core Location 将提醒用户在应用“设置”中开启定位服务,让应用能够获取当前位置,如图2.2所示。

如果位置管理器以前未请求用户允许获取设备的位置,它将显示一个提醒框,请求用户许可,如图2.3所示。

如果用户轻按OK按钮,说明得到了用户的许可,位置管理器将获取当前位置。如果用户轻按按钮Don’t Allow禁止获取当前位置,将调用CLLocationManager的委托ICFLocationManager中响应授权状态变化的方法。

在这个示例应用中,ICFLocationManager 存储其他地方的位置请求结束块,以便能够轻松地处理多个位置请求。位置可用或发生错误时,方法getLocationWithCompletionBlock:将执行存储的所有结束块,让调用者能够根据当前的情况以合适的方式使用位置或处理错误。用户禁止获取当前位置时,调用者显示一个提醒框,指出发生了位置获取请求遭拒错误,如图2.4所示。

可实现一个委托方法,在用户修改了定位服务授权状态(针对整台设备或具体应用,如图2.1所示)时做出合适的响应。

在示例应用中,这个方法是在ICFLocationManager中实现的,它在未获得用户许可时显示错误提示,在得到用户许可时重启位置更新并清除以前的错误。

2.2.2 检查定位服务是否已开启

要直接检查设备是否开启了定位服务,可使用 CLLocationManager的类方法 location ServicesEnabled。

使用这个类方法,可让应用根据能否获取当前位置采取不同的措施。在使用位置的应用中,应在用户禁止获取当前位置时采取妥善的措施,并清楚地告诉用户,如果他想让应用获取当前位置,该如何做。

2.2.3 开始位置请求

获得使用定位服务的许可后,便可使用CLLocationManager实例来获取当前位置了。在示例应用中,ICFLocationManager负责集中管理位置功能,它为应用管理着一个CLLocationManager实例。在ICFLocationManager类的方法init中,创建了一个CLLocationManager实例,并根据定位需求对其进行了定制。

可设置 CLLocationManager的多个属性,以指定它如何管理当前位置。通过设置精度属性desiredAccuracy,应用可告诉 CLLocationManager,该以缩短电池续航时间为代价尽可能提高精度,还是为延长电池续航时间而使用较低的精度。使用较低的精度时,还可缩短获取当前位置所需的时间。通过设置属性distanceFilter,可告诉 CLLocationManager,移动多长距离后才触发新的位置事件;这对微调基于位置变化的功能很有帮助。最后,给CLLocationManager指定了委托,这使得可以独特的方式响应位置事件和授权状态变化。为获取位置做好准备后,让位置管理器开始更新位置。

CLLocationManager将根据指定的参数在需要时利用GPS和/或Wi-Fi确定当前位置。应实现两个委托方法,它们分别处理如下情形:位置管理器更新了当前位置或无法更新当前位置。获取位置后,将调用方法locationManager:didUpdateLocations:。

位置管理器可能通过数组locations提供多个位置,其中最后一个对象是最新的位置。位置管理器还可能在没有开始获取位置时,就快速返回 GPS 检测到的最后位置;在这种情况下,如果GPS已关闭且设备发生了移动,返回的位置将很不准确。这个方法检查位置的精度,如果精度值为负,就忽略返回的位置。如果返回的位置相当准确,这个方法就存储它并执行结束块。请注意,在逐步获取准确位置期间,位置管理器可能调用这个方法多次,编写这个方法时必须考虑到这一点。

如果位置管理器未能获取位置,它将调用方法 locationManager:didFailWithError:。导致错误的原因可能是未得到用户的许可,也可能是由于 GPS 或 Wi-Fi 不可用(例如,设备处于飞行模式)。发生错误时,示例应用命令位置管理器停止更新当前位置、捕获错误并执行结束块(让请求当前位置的代码能够妥善地处理错误)。

位置管理器委托可监视航向变化,这很有用。例如,可使用这些信息在地图上标出用户的前进路线相对于正确路线的偏差。要获取航向信息,需要让位置管理器对其进行监视。还可设置一个可选的筛选器,这样航向变化小于指定的度数时就不会获取更新。

发生航向变化事件时,将调用委托方法locationManager:didUpdateHeading:。

参数newHeading提供了多项重要信息,其中包括相对于磁北的航向和相对于真北的航向,它们的单位都是度。它还提供了精度,这指出了磁北航向可能偏离多少度。这个值为越少的正数,航向就越准确;如果为负数,就说明航向无效,这可能是因为存在磁场干扰。时间戳指出了航向是什么时候获取的,应通过检查它来避免使用过时的航向。

2.2.4 分析和理解位置数据

位置管理器返回的位置是用CLLocation实例表示的。CLLocation包含多项有关位置的重要信息,首先是用CLLocationCoordinate2D表示的经度和维度。

维度指的是位于赤道以北或以南多少度,其中赤道为0度,北极为90度,而南极为-90度。经度指的是位于本初子午线以东或以西多少度;本初子午线是一条虚构的线条,它从北极出发,经英国格林尼治天文台到达南极。位于本初子午线以西时,经度为负数,最高可达-180度;而位于本初子午线以东时,经度为正数,最高可达180度。

作为经度和维度坐标的补充,还有水平精度,它用CLLocationDistance或米数表示。水平精度指的是实际位置与返回的坐标之间的距离在指定米数内。

CLLocation还提供了当前位置的海拔和垂直精度(单位为米)——如果设备装备有GPS。如果设备没有GPS,返回的海拔将为零,而垂直精度将为-1。

CLLocation包含时间戳,它指出了这个位置是位置管理器在何时确定的。这可用于判断位置是否已过时(进而忽略它),还可用于比较不同位置的时间。

最后,CLLocation提供了速度(单位为米每秒)和航向(相对于正北多少度)。

2.2.5 重大变化通知

Apple强烈建议应用在获取位置后停止位置更新,以延长电池的续航时间。如果应用不要求位置非常准确,可监视重大位置变化,这是一种高效的方式,既让应用获悉设备的位置发生了重大变化,又可避免让GPS和Wi-Fi不断监视当前位置,从而极大地节省电量。

通常,在设备位置变化超过500米或更换了连接的基站时,将发出通知。另外,仅当最后一次通知是在5 分钟之前时,才会发送新的通知。位置更新时间被交给委托方法 locationManager:didUpdateLocations:进行处理。

2.2.6 使用GPX文件进行位置测试

测试基于位置的应用令人望而却步,需要对不方便的位置进行测试时尤其如此。所幸Xcode使用GPX文件提供了强大的位置测试支持。GPX文件是GPS交换格式(Exchange Format)文档,它使用XML格式,可用于在设备和GPS之间交换信息。在调试模式下,Xcode可使用GPX文件定义的“航点”来设置iOS模拟器或设备的当前位置。

在示例应用中,使用文件DMNS.gpx将当前位置设置为丹佛自然科学博物馆(Denver Museum of Nature and Science)。

要让Xcode使用GPX文件进行调试,可从项目窗口左上角的下拉列表Scheme中选择Edit Scheme,再选择标签Options,并选中复选框Allow Location Simulation,如图2.5所示。选择复选框Allow Location Simulation,便可从下拉列表 Default Location中选择一个位置。这个下拉列表包含一些内置位置,还包含添加到项目中的GPX文件指定的位置。

应用在调试模式下运行时,Core Location将返回GPX文件指定的位置,将其作为设备或模拟器的当前位置。要在调试期间改变模拟位置,可从Xcode菜单中选择Debug>Simulate Location,再选择一个位置,如图2.6所示。Core Location 将把模拟位置改为选定的位置,导致委托方法locationManager:didUpdateLocations:被调用。

2.3 显示地图

MapKit框架为iOS提供了地图用户界面功能,其中的基本类是MKMapView,它显示地图、处理用户与地图的交互以及管理标注(像大头针)和覆盖层(如线路图或突出区域)。要更深入地了解iOS中地图的工作原理,必须明白坐标系。

2.3.1 理解坐标系

在MapKit中,有两个坐标系:地图坐标系和视图坐标系。地图使用墨卡托投影,将3D世界地图投影到2D坐标系。坐标可使用经度和纬度指定。地图视图表示显示在屏幕上的地图部分,它使用标准的UIKit视图坐标,并负责决定在什么地方显示地图坐标指定的点。

2.3.2 配置和定制MKMapKit

在示例应用中,ICFMainViewController包含一个地图视图,它在地图上显示用户的位置,并允许用户滚动和缩放;在Interface Builder中,将这个地图视图配置成了标准类型。ICFMainViewController有一个分段控件,让用户能够调整地图类型。

除设置地图类型外,另一种常见的定制是设置地图显示的区域。在ICFMainViewController中,有一个名为zoomMapToFitAnnotations方法,它检查用户当前喜欢的地点,调整地图的大小和中心位置,以覆盖所有这些地点。这个方法首先设置默认的最大和最小坐标。

接下来,这个方法获取地图上所有的标注(这将在2.4 节更详细地介绍),并找出这些标注中最大和最小的经度和纬度。

然后,这个方法根据最大和最小经度和纬度坐标计算中心坐标。

接下来,这个方法计算显示所有标注需要的跨度(span)。计算每个方向的跨度时,都乘以放大系数1.2,以便在最偏远的标注和视图边缘之间留出一定的边距。

计算中心点和跨度后,便可创建一个地图区域,并使用它来显示地图视图的显示区域。

如果将参数 animated:设置为 YES,可以动画方式放大地图,就像放大操作是用户执行的那样;如果将它设置为NO,将瞬间放大地图,而没有动画效果。

2.3.3 响应用户交互

可给MKMapView指定委托,以便对用户与地图交互做出响应。用户与地图的交互包括平移和缩放、拖曳注释(annotation)以及用户轻按标注(callout)时进行响应。

用户平移或缩放地图时,将调用委托方法 mapView:regionWillChangeAnimated:和mapView:regionDidChangeAnimated:。在示例应用中,不需要采取额外的措施来缩放地图和调整注释。然而,如果应用在地图上显示了大量信息或显示的信息随缩放等级而异,就可使用这些委托方法来删除不可见的地图注释以及添加新出现的注释。在示例应用中,这个委托方法演示了如何获取新的地图区域,这可用来查询要在地图上显示的内容。

如何在用户拖曳注释或轻按标注时做出响应,这将下一节介绍。

2.4 地图注释和覆盖层

地图视图(MKMapView)是一种可滚动的视图,行为独特;以标准方式在其中添加子视图时,子视图不会随地图视图滚动,而是静止的,其相对于地图视图框架的位置始终不变。对悬停按钮或标签来说,这种特点也许不错,但在地图上标出点和细节至关重要。要标出地图视图中感兴趣的点或区域,可使用地图注释和覆盖层。地图滚动或缩放时,注释和覆盖层在地图上的位置保持不变。地图注释是使用地图上的单个坐标点定义的,而地图覆盖层可以是线段、多边形或复杂形状。MapKit 将注释(覆盖层)同其关联的视图区分开来。注释和覆盖层是数据,指定了相关联的视图应显示在地图的什么地方,这些数据被直接添加到地图视图中。在需要显示注释或覆盖层时,地图视图将请求相关联的视图,就像表视图根据需要为索引路径请求单元格一样。

2.4.1 添加注释

任何对象都可用作地图视图中的注释,前提条件是它实现了协议MKAnnotation。Apple建议注释对象应是轻量级的,因为对于添加的每个注释,地图视图都将包含一个指向它的引用;另外,如果注释太多,可能影响地图的滚动和缩放性能。如果注释非常简单,可使用MKPointAnnotation类。在示例应用中,ICFFavoritePlace实现了协议MKAnnotation,它是NSManagedObject的子类,因此可使用Core Data进行持久化。有关如何使用Core Data和NSManagedObject子类的更详细信息,请参阅第13章。

要实现协议MKAnnotation,类必须实现属性coordinate,地图视图将使用这个属性确定将注释放在地图的什么地方。属性coordinate的获取方法返回一个CLLocationCoordinate2D。

由于ICFFavoritePlace类存储了地点的经度和纬度,因此属性coordinate的获取方法根据经度和纬度创建一个CLLocationCoordinate2D,这是使用Core Location提供的函数CLLocationCoordinate2DMake完成的。在属性coordinate的设置方法中,ICFFavoritePlace存储CLLocationCoordinate2D参数中的经度和纬度。

协议MKAnnotation还定义了另外两个可选属性:title和subtitle。在用户轻按注释视图时,地图视图可使用这两个属性来显示标注(callout),如图2.7所示。

标注的上面那行为属性title,而下面那行为属性subtitle。

在ICFMainViewController的方法viewDidLoad:中,调用了方法updateMapAnnotations来填充初始地图注释,在地点详情编辑视图被关闭时,也调用了这个方法。这个方法首先将地图视图的注释都删除。虽然注释不多时这种方式完全可行,但注释很多时,必须设计更智能的方式,以高效地删除不需要的注释并添加新的注释。

接下来,这个方法执行Core Data检索请求以获取一个NSArray(其中包含存储的地点),再将这个数组加入到地图视图的属性annotations中。

将添加的注释显示在地图上的工作将由地图视图负责。

2.4.2 显示标准和自定义的注释视图

注释视图在地图上表示注释。MapKit 提供了两种标准注释视图:大头针(表示搜索到的位置)和蓝点(表示当前位置)。可使用静态图像定制注释视图,还可通过创建MKAnnotationView子类来全面定制注释视图。在示例应用中,在用户喜欢的地方显示标准大头针,在当前位置显示标准蓝点,而可拖曳的注释则使用绿色箭头表示,如图2.8所示。

为让地图视图显示注释视图,地图视图委托需要实现方法 mapView:viewForAnnotation。在示例应用中,方法mapView:viewForAnnotation是在ICFMainViewController中实现的,它首先检查注释是否是当前位置。

如果是当前位置,就返回 nil,这让地图视图使用标准蓝点。接下来,这个方法检查注释的类型。如果注释表示的位置为下一个目的地(goingNext),就返回一个自定义注释视图,否则返回标准的大头针注释视图。

为返回标准的大头针注释视图,这个方法首先尝试从队列中取出一个未用的注释视图。如果没有这样的注释视图,就创建一个MKPinAnnotationView实例。

创建大头针注释视图后,就可对其进行定制了:设置大头针的颜色(可设置为红色、绿色或紫色)、指定用户轻按注释视图时是否显示说明、指定注释视图是否是可拖曳的。

轻按注释视图显示的说明包含可定制的左扩展视图(accessory view)和右扩展视图。左扩展视图被设置为一幅自定义图像,而右扩展视图被设置为标准的展开按钮。如果左扩展视图或右扩展视图是从 UIControl 派生而来的对象,用户轻按它们时将调用委托方法 mapView:annotation View:calloutAccessoryControlTapped:。

否则,开发人员应对这些对象进行配置,使其对轻按做出响应。请注意,Apple规定扩展视图的高度不能超过32像素。

为返回自定义的注释视图,这个方法视图从队列中取出一个未用的注释视图(其类型由字符串标识符指定)。如果没有这样的注释视图,这个方法将创建一个MKAnnotationView实例。

可像标准大头针注释视图那样定制该注释视图:指定用户轻按注释视图时是否显示标注以及注释视图是否是可拖曳的。主要差别在于,可直接使用方法 setImage:设置这种注释视图的图像。

这种注释视图将显示为绿色箭头,而不是标准大头针,如图2.8所示。

2.4.3 可拖曳的注释视图

可拖曳的注释视图很有用,它让用户能够在地图上指定地点。在示例应用中,有个特殊的地点,它是用户的下一个目的地,用绿色箭头表示。可让注释视图是可拖曳的,为此可将其属性draggable设置为YES。

这样,用户就可将它拖曳到地图的任何地方。为更详细地了解用户是如何拖曳注释视图的,地图视图委托实现了方法mapView:annotationView:didChangeDragState:fromOldState:。每当可拖曳注释视图的拖曳状态发生变化时,都将调用这个方法,并指出拖曳状态为无(MKAnnotationViewDragStateNone)、即将开始、正在拖曳、即将撤销还是即将结束。通过查看新的拖曳状态和旧的拖曳状态,可编写自定义逻辑来处理众多不同的拖曳情形。

在示例应用中,当用户停止拖曳绿色箭头时,将对箭头指定的新位置进行反向地理编码(这将在2.5 节更详细地介绍),以获取新位置的名称和地址。为此,这个方法需要检查拖曳是否已结束。

如果拖曳已结束,这个方法就获取与注释视图相关联的注释,以确定需要进行反向地理编码的新坐标。

这个方法将说明的左扩展视图设置为标准的活动指示视图,让用户知道正在更新说明,再调用对新地点进行反向地理编码的方法,这将在2.5节介绍。

2.4.4 使用地图覆盖层

地图覆盖层类似于地图注释,可以是任何实现了协议 MKOverlay的对象,而地图视图委托将负责提供与地图覆盖层相关联的视图。地图覆盖层不同于注释的地方在于,它们不仅可以表示点,还可以表示线段和形状,因此非常适合用于在地图上表示线路或感兴趣的区域。为演示地图覆盖层,示例应用提供了给喜欢的地点添加地理围栏的功能(这将在后面的2.6节更详细地介绍),围栏的半径由用户指定。给喜欢的地点添加地理围栏后,将在地图上显示一个圆,其半径是用户指定的值,圆心为喜欢的地点,如图2.9所示。

前面的2.4.1节说过,方法updateMapAnnotations给地图添加注释;这个方法还给地图添加覆盖层。为此,它清除地图视图中的所有覆盖层。

由于仅当地点启用了地理围栏功能时,才需要为它显示覆盖层,因为这个方法迭代所有的地点,并为启用了地理围栏功能的地点添加覆盖层。

当地图需要显示地图覆盖层时,地图视图将调用委托方法mapView:viewForOverlay。这个方法创建一个覆盖层视图,供地图进行显示。MapKit 提供了三种方式:圆圈、多边形和折线;如果MapKit提供的方式不能满足需求,也可创建自定义形状和覆盖层。示例应用根据覆盖层的半径以及地点的地图坐标创建一个环绕地点的圆圈。

MKCircle 对象准备就绪后,这个方法创建一个圆形视图,并定制其描边色、填充色和线条宽度。然而,返回这个圆形视图,而地图将显示它。

2.5 地理编码和反向地理编码

地理编码指的是找出人类能够看懂的地址对应的经度和纬度坐标;反向地理编码指的是根据坐标做出人类能够看懂的地址。在iOS 5.0中,Core Location支持这两种功能,且不像以前的版本那样存在特殊条款或限制。

2.5.1 对地址进行地理编码

示例应用让用户能够添加喜欢的地方:在ICFFavoritePlaceViewController中输入地址。然后,用户可轻按按钮Geocode Location Now,以获取该地址的经度和纬度,如图2.10所示。

用户轻按按钮Geocode Location Now时,将调用方法geocodeLocationTouched:。这个方法首先将用户提供的地址信息合并成一个字符串,如“2100 York St,Denver,CO 80205”,以便将其提供给地理编码器。

接下来,这个方法禁用按钮Geocode Location Now,以防因用户多次轻按而再发起请求。Apple明确地指出,地理编码器应每次只处理一个请求。这个方法还更新文本框和按钮,以指出正在进行地理编码。

这个方法获取一个指向CLGeocoder实例的引用。

然后,让geocoder对地址字符串进行地理编码,并提供一个将在主队列中调用的结束处理程序块。这个结束处理程序块首先重新启用按钮,让用户能够再次轻按它,然后检查地理编码过程中发生了错误还是成功地完成了地理编码。

如果在地理编码过程中发生错误,就使用Not found填充经度和纬度文本框,并显示一个提醒框,其中包含经过本地化的错误描述。如果没有Internet连接,或者地址的格式不对或找不到,地理编码过程将以失败告终。

如果地理编码成功,将向结束处理程序传递数组placemarks。这个数组包含一些CLPlacemark实例,其中每个都包含潜在匹配结果的信息。CLPlacemark包含经度和纬度坐标以及地址信息。

如果返回了多个CLPlacemark,可让通过用户界面让用户选择最满意的结果(应用“地图”就是这样做的)。出于简化考虑,示例应用选择数组中的最后一个CLPlacemark,并使用其中的坐标信息更新用户界面。

2.5.2 对位置进行反向地理编码

示例应用允许用户拖曳绿色箭头,以指定下一个目的地,如图2.11所示。

用户拖曳绿色箭头时,将调用ICFMainViewController中的地图视图委托方法mapView:annotation View:didChange DragState:fromOldState:。这个方法按2.4.3节介绍的那样检查拖曳状态,如果用户已停止拖曳绿色箭头,就使用活动指示视图更新说明视图,并开始反向地理编码。

方法reverseGeocodeDraggedAnnotation:forAnnotationView:获取一个CLGeocoder引用。

根据绿色箭头的当前坐标创建了一个CLLocation实例,供地理编码器使用。

接下来,让地理编码器对这个CLLocation 实例进行反向地理编码,并提供一个将在主队列中调用的结束处理程序块。这个结束处理程序块将说明视图中的活动指示视图替换为绿色箭头,再检查反向地理编码过程成功完成了还是发生了错误。

如果地理编码器遇到了错误,就显示一个提醒框,其中包含经过本地化的错误描述。如果没有Internet连接,反向地理编码将以失败告终。

如果成功地完成了反向地理编码过程,将向结束处理程序传递一个CLPlacemark数组。示例应用使用最后一个CLPlacemark实例来更新下一个目的地的名称和地址。

CLPlacemark包含使用国际通行说法的详细地址信息,例如,街道地址用数字(subThoroughfare)和街道(thoroughfare)表示,而城市和州为subAdministrativeArea和administrativeArea。

提示:

地理编码器提供的CLPlacemark实例包含一个addressDictionary属性,可轻松地将其插入到地址簿(更详细的信息请参阅第5章)。

接下来,使用Core Data存储这个地点,使其在应用重启后依然可用。现在,如果用户轻按显示为绿色箭头的注释视图,将显示箭头所处位置的名称和地址,如图2.12所示。

2.6 地理围栏

地理围栏(Geofencing)也叫区域监视(regional monitoring),指的是能够知道设备已进入或离开指定的地图区域。iOS在Siri中充分利用了这项功能,使其能够完成类似于下面的任务:“在我离开办公室时提醒我带上面包”;“在我回到家时提醒我将烤肉放进烤箱”。iOS 还在Passbook中使用了地理围栏功能,让用户能够在主屏幕上看到相关的凭证(更详细的信息请参阅第24章)。

2.6.1 检查区域监视功能

CLLocationManager有一个类方法,指出设备是否支持区域监视。应用可使用它来决定是否执行区域监视任务,例如,在示例应用的ICFFavoritePlaceViewController中,根据情况决定是否显示一个开关,用于对喜欢的地点启用地理围栏。

2.6.2 定义边界

可使用 Core Location 位置管理器(CLLocationManager)存储一组应用要监视的区域。在ICFMainViewController中,方法updateMapAnnotations:清除这些被监视的区域。

接下来,它迭代用户喜欢的地点,确定用户对哪些地点设置了地理围栏。对于每个设置了地理围栏的地点,这个方法都像前一节描述的那样给它添加覆盖层视图,再让位置管理器开始监视该区域。要监视的区域必须有中心坐标、半径以及供应用对区域进行跟踪的标识符。示例应用将Core Data通用资源ID用作区域标识符,这样发生区域监视事件时可快速检索相关的地点。

请注意,当前只能监视圆形区域。

2.6.3 监视变化

设备进入或离开监视区域后,位置管理器将这一点告诉其委托:调用方法 locationManager:didEnterRegion:或locationManager:didExitRegion:。

方法locationManager:didEnterRegion:首先获取监视区域的标识符。这个标识符是在让位置管理器对区域进行监视时指定的,它是保存的地点的Core Data URI。使用这个URI来获取托管对象的ID,再使用托管对象ID从托管对象上下文检索喜欢的地点。

接下来,这个方法获取地点的详细信息,并使用提醒框显示这些信息。

要在示例应用中测试这一点,在调试模式下运行这个应用,并使用包含丹佛自然科学博物馆(DMNS)地址的GPX文件,这在2.2.6节介绍过。确保对地点Denver Art Museum启用了地理围栏,如图2.9所示。应用运行后,使用Xcode将位置改为Denver Art Museum,为此可从Default Location下拉列表中选择DMNS,如图2.6所示。这将触发地理围栏事件,进而显示如图2.13所示的提醒框。

用户离开监视区域时,将调用方法 locationManager:didExitRegion:。这个方法也获取监视区域的Core Data标识符,使用Core Data获取托管对象ID,找出喜欢的地点,再显示一个提醒框,指出用户离开了监视区域。要在示例应用中测试这一点,在图2.13所示的Favorite Nearby提醒框中轻按OK按钮,再在iOS模拟器中选择菜单Debug>Location>Apple。几秒钟后,模拟器将改变模拟位置,并显示一个提醒框,如图2.14所示。

位置管理器有意识地推迟调用委托方法,等到穿越缓冲地带20秒后才这样做,这旨在避免设备接近监视区域时发送虚假消息。

2.7 获取线路

iOS 6改进了标准应用“地图”,使其除提供线路外还进行分步导航;另外,还可在其他应用中启动“地图”,并指定要显示的内容。应用可请求“地图”显示一系列内容、显示两个位置之间的线路或显示从当前位置出发的线路。还可对应用“地图”进行配置:指定中心、跨度和地图类型(标准地图、卫星地图或混合地图)。在iOS 7中,MapKit包含MKDirectionRequest类,这个类提供可在应用中直接使用的线路。使用MKDirectionRequest可获取一个数组,其中包含表示线路的折线以及可显示在表视图中的具体步骤。示例应用演示了如何显示折线和具体步骤。

要打开应用“地图”,可使用 MKMapItem的类方法 openMapsWithItems:launchOptions:,也可使用其实例方法openInMapsWithlaunchOptions:。在示例应用中,ICFFavoritePlaceViewController包含一个按钮,可用于获取前往喜欢地点的线路。用户轻按这个按钮时,将调用方法getDirectionsButtonTouched:。在这个方法中,根据喜欢的地点创建一个MKMapItem实例。

接下来,创建一个启动选项数组,用于告诉应用“地图”启动时如何配置自己。

然后,创建一个数组,其中包含前面创建的MKMapItem实例,再将这个数组与启动选项数组一起传递给应用“地图”。如果随启动选项一起传递的数组包含两个MKMapItem,“地图”将显示从第一个地点前往第二个地点的线路。

应用“地图”将启动,并显示前往喜欢的地点的线路。如果发生错误,openMapsWithItems:launchOptions:将返回NO。

要获取可在应用中显示的线路,需要实例化一个MKDirectionsRequest 对象,并指定用MKMapItem实例表示的起点和终点,再使用这个MKDirectionsRequest实例化一个MKDirections对象。

然后调用方法calculateDirectionsWithCompletionHandler:,并指定一个结束块。这个结束块应处理可能发生的错误,并查看提供给它的MKDirectionsResponse。在这里,这个方法确保至少返回了一条线路(MKRoute实例),再选择使用第一条线路。这个方法迭代第一条线路的属性steps (这个属性包含一系列 MKRouteStep 实例),并以字符串的方式显示每个步骤的距离和说明。然后,这个方法调用委托的方法将线路添加到地图中。

在委托的方法中,将表示线路的折线添加到地图覆盖层中,再关闭对话框。

由于折线是以覆盖层的方式添加的,因此对返回覆盖层视图的地图委托方法来说,除处理地理围栏覆盖层外,还必须知道如何处理折线。

方法 mapView:viewForOverlay:检查覆盖层的类型,并据此为覆盖层创建相应类型的覆盖层视图。对于折线覆盖层,这个方法将其转换为折线,再根据这条折线创建一个MKPolylineRenderer实例,并设置线宽以及填充色和描边色(蓝色),这将在地图上使用折线显示从起点到终点的线路,如图2.15所示。

2.8 小结

本章介绍了Core Location和MapKit。首先,介绍了如何导入Core Location,如何检查服务是否可用,如何处理授权状态变更以及如何获取设备的当前位置。

接下来,本章阐述了如何使用 MapKit:使用标准注释和自定义注释在地图上显示位置,这包括如何在说明中显示注释的详细信息以及如何在用户轻按标注或拖曳注释时做出响应;如何添加覆盖层以突出地图的内容。

然后,本章描述了如何使用地理编码器:根据街道地址获取经度和纬度;根据经度和纬度坐标获取地址信息。

接下来,本章介绍了地理围栏,并通过示例应用演示了如何对用户进出指定地图区域进行监视。

最后,本章演示了两种提供线路的方式:使用应用“地图”提供线路;使用线路请求获取信息,并直接在应用的用户界面中显示它们。

2.9 练习

1.应用处于后台时也能检测到地理围栏事件,但应用处于后台时不能显示提醒框。请改进本章的示例应用,使其在后台检测到地理围栏事件时发送本地通知,而在前台时显示提醒框。提示:有关通知和后台处理的更详细信息,请参阅第9章和第16章。

2.在本章的示例应用中,用户轻按表示喜欢地点的大头针时,将显示标注;如果用户再轻按标注右边的展开按钮,将显示一个模态视图,让用户能够编辑这个地点的详细信息。请修改这种方式,在弹出框中显示地点详情视图,并以大头针为锚点。另外,弹出框出现后,相应的大头针应从红色变成绿色。

第6章 使用音乐库

Steve Jobs于2007年推出iPhone时,宣称说它集电话、iPod和上网利器于一身。多年后,iPhone已远不止提供这三项核心功能,这要部分归功于第三方开发人员的艰苦努力。然而,最初的广告词仍然适用,iPhone依然主要集电话、iPod和上网利器于一体。用户不是将iPhone加入到他们每天都携带的设备中,而是用它取代了电话和iPod。

Apple于2004年计划推出iPhone时,正是出于播放音乐的考虑;iPhone始终是iPod的扩展。推出iPod的灵感来自音乐,可以说是iPod让Apple死而复生。音乐深受大家的喜爱,让人们走到一起,让人们表达自我。虽然 iPhone 用户可能从未将其视为音乐播放设备,但大多数用户都会不由自主地使用它来欣赏喜爱的歌曲。

本章讨论如何在iOS应用中访问用户的音乐库,让您能够打造功能齐备的音乐播放器或让用户玩游戏的同时播放背景乐。

6.1 示例应用简介

本章的示例应用名为Player,如图6.1所示。这是一个功能齐备的iPhone音乐播放器,让用户能够通过多媒体选择器选择要播放的歌曲、随机地播放歌曲以及播放特定艺术家的歌曲。它还提供了暂停、重放、播放下一曲、播放前一曲、调整音量、显示播放时间以及前进30秒和后退30秒等功能。这个应用还显示当前播放的音轨的专辑封面(如果有的话)。

鉴于Xcode自带的iOS模拟器没有应用“音乐”,也没有将音乐加入其文件系统的简单途径,因此这个示例应用只能在设备上运行。在模拟器上运行这个应用时,将出现大量的错误。

在模拟器上,试图访问媒体库将导致应用崩溃,并显示如下错误消息。

6.2 打造播放引擎

如果对播放控制没有深刻认识,获取音频数据将毫无意义。要在应用中播放音乐,需要创建一个MPMusicPlayerController 实例。在头文件 ICFViewController.h中,声明了一个名为 player的MPMusicPlayerController变量,在整个示例应用中,都将使用它来控制播放以及获取当前播放的曲目的信息。

在方法 viewDidLoad中,初始化了 MPMusicPlayerController 变量 player,这是使用MPMusicPlayerController的一个类方法完成的。创建MPMusicPlayerController实例的方式有两种:一是使用applicationMusicPlayer,这将在应用内播放音乐,不影响iPod的状态,并在应用退出后停止播放;二是使用iPodMusicPlayer,这将控制应用iPod,从iPod播放头的位置开始继续播放,应用进入后台后也不会停止播放。本章的示例应用使用的是applicationMusicPlayer;但可轻松地转而使用iPodMusicPlayer,而无需对其他代码做任何修改。

6.2.1 注册播放通知

要有效地播放音乐,必须知道音乐播放器的状态。处理音乐播放器时,需要监视3种通知:当前播放的乐曲变了、音量变了,以及播放状态发生了变化。要监视这些状态,可使用NSNotificationCenter 来订阅前述事件。为确保代码整洁易懂,示例应用使用了辅助方法registerMediaPlayerNotifications。将观察者加入NSNotificationCenter后,需要对对象player调用beginGeneratingPlaybackNotifications。

注册通知后,确保在清理内存和视图期间将它们注销,这很重要,否则将导致应用崩溃以及其他意外行为。另外,在方法viewWillDisapper中还调用了endGeneratingPlayback Notifications。

除注册音乐播放器回调方法外,还将创建一个NSTimer,用于更新播放进度和播放头时间标签。在示例应用中,这个NSTimer名为playbackTimer。对于通知回调方法和NSTimer,暂时就介绍这些,后面的6.2.3节将详细讨论它们。

6.2.2 播放控制

示例应用提供了多个让用户能够与音乐播放器交互的按钮,如播放、暂停、下一曲、前一曲以及前进和后退30秒。首先需要实现的是播放和暂停方法。这是一个简单的切换按钮:如果正在播放,就暂停;如果已暂停或停止,就继续播放。至于将按钮文本在Play和Pause之间切换的代码,将在6.2.3节讨论状态变化通知回调方法时进行讨论。

用户欣赏音乐时,还应能够跳到下一曲或前一曲。这是通过对对象player调用另外两个方法实现的。

用户还可前进或后退30秒。在下面的代码中,如果超过了当前曲目末尾,就跳到下一曲目开头;同样,如果超过了当前曲目开头,就重新播放当前曲目。这两个方法都利用了对象player的属性currentPlaybackTime,这个属性可用于调整播放头的位置,还可用于确定当前播放时间。

除这些标准的播放控制方式外,示例应用还让用户能够调整音量。对象player使用一个0.0 (静音)~1.0(最高音量)的浮点值来设置音量;为让用户能够调整音量,示例应用使用了一个UISlider,因此可将UISlider的值直接传递给player。

6.2.3 响应状态变化

前面给3个通知注册了回调方法。这些通知让应用能够获悉MPMusicPlayerController的当前状态和行为。当前播放的曲目变了时,将调用第一个方法。这个方法包含两部分,第一部分更新专辑封面,第二部分更新显示艺术家、曲目名和专辑的标签。

MPMusicPlayerController 当前播放的音频或视频由一个MPMediaItem 对象表示,要获取这个对象,可对MPMusicPlayerController实例调用方法nowPlayingItem。

创建了一个用于表示专辑封面的UIImage,并将其初始化为一个占位符,在MPMediaItem没有专辑封面时将显示该占位符。MPMediaItem使用键值属性来表示存储的数据,表6.1列出了所有的键值属性。创建了一个MPMediaItemArtwork,并将其设置为专辑封面数据。Apple文档指出,如果没有专辑封面,获取属性MPMediaItemPropertyArtwork时将返回nil,但实际情况并非如此。为解决这个问题,将专辑封面加载到一个UIImage中,并检查结果。如果结果为 nil,就认为没有专辑封面,进而加载前面指定的占位符。即便MPMediaItemPropertyArtwork在没有专辑封面时返回nil,示例应用也能正常运行。

方法nowPlayingItemChanged:的第二部分更新歌曲名、艺术家信息和专辑名,如图6.1所示。如果获取这些属性时返回nil,就使用占位符字符串。表6.1列出了MPMediaItem的所有可访问属性。请注意,媒体项为播客时,则除表 6.1所示的属性外,还有其他一些属性,详情请参阅 Apple的MPMediaItem文档。在这个表中,还指出了以编程方式查找媒体项时,属性键是否可用于谓词搜索。

监视音乐播放器的状态很重要,在状态可能受应用无法控制的外部输入的影响时尤其如此。如果状态发生变化,将调用方法playbackStateChanged:。在这个方法中,创建了变量playbackState,用于存储播放器的当前状态。这个方法执行了多项重要任务,其中第一项任务是更新按钮play/pause的文本,以反映当前状态。另外,创建和拆除了6.2.1节提到的NSTimer。如果应用正在播放音频,就将该定时器设置为每隔0.3秒触发一次,以便更新播放时间标签以及指出播放头位置的UIProgressIndicator。这个定时器触发的方法updateCurrentPlaybackTimer将在下一小节讨论。

除这个示例应用演示的状态外,还有其他 3 种状态。第一种状态是 MPMusicPlaybackState Interrupted,表示音频播放中断,如来电导致中断。其他两种状态是 MPMusicPlaybackState SeekingForward和MPMusicPlaybackStateSeekingBackward,表示音乐播放器正为查找指定播放位置而前进或后退。

如果音量发生变化,也必须通过应用的音量滑块反映出来。这是由通知回调方法 volume Changed:完成的。在这个方法中,获取了播放器的当前音量,并相应地设置volumeSlider。

6.2.4 时长和定时器

在大多数情况下,用户都想获悉当前播放的歌曲的信息,如已播放多长时间以及还有多长时间。示例应用包含两个显示这些数据的方法。第一个是 updateSongDuration,在当前播放的歌曲变了或应用启动时被调用。它创建一个指向当前播放歌曲的引用,再使用属性键playbackDuration获取该歌曲的时长(单位为秒)。然后,将这项数据转换为小时、分钟和秒数,并将结果显示在UIProgressIndicator旁边的标签上。

第二个方法是updateCurrentPlaybackTime,由NSTimer每隔0.3秒调用一次,而这个NSTimer由6.2.3节讨论的方法playbackStateChanged:控制。与方法updateSongDuration一样,将已播放的时间转换为小时、分钟和秒数;另外,还根据前面确定的歌曲时长计算percentagePlayed,并使用它来更新playbackProgressIndicator。由于currentPlaybackTime只能精确到秒,因此没有必要过于频繁地调用这个方法。然而,调用这个方法的频率越高,显示的结果就越精确。

6.2.5 随机播放和重复播放

除前面提到的属性和控制外,还可指定MPMusicPlayerController的属性repeatMode和shuffleMode。虽然示例应用没有实现设置这两个属性的功能,但实现起来非常容易。

重复播放模式包括 MPMusicRepeatModeDefault(用户的预定义首选模式)、MPMusicRepeat ModeNone、MPMusicRepeatModeOne和MPMusicRepeatModeAll。

随机播放模式包括MPMusicShuffleModeDefault(用户的预定义首选模式)、MPMusicShuffle ModeOff、MPMusicShuffleModeSongs和MPMusicShuffleModeAlbums,其中 MPMusicShuffle ModeDefault表示用户的预定义首选模式。

6.3 媒体选择器

要让用户能够选择要欣赏的歌曲,最简单的方式是让他能够访问MPMediaPickerController,如图6.2所示。MPMediaPickerController让用户能够浏览艺术家、歌曲、播放列表和专辑,以指定要播放的歌曲。要使用 MPMediaPickerController,必须遵守委托协议 MPMediaPicker ControllerDelegate,它定义了两个必须实现的方法。第一个是 mediaPicker:didPickMediaItems:,在用户选择了要欣赏的歌曲时被调用。将通过一个MPMediaItemCollection对象返回选定的歌曲,MPMusicPlayerController 可直接将这个对象作为参数来调用 setQueueWith ItemCollection:。为MPMusicPlayerController 设置新队列后,就可开始播放选定歌曲了。用户选择歌曲后,MPMediaPickerController 不会自动关闭,您必须显式地调用 dismissViewControllerAnimated:completion:来关闭它。

如果用户在MPMediaPickerController中取消了选择,将调用委托方法mediaPickerDidCancel:。在这个方法中,您必须关闭MPMediaPickerController。

实现委托方法后,便可创建MPMediaPickerController实例了。分配和初始化MPMediaPicker Controller时,必须指定一个表示媒体类型的参数。表6.2列出了这个参数的所有可能取值。请注意,同一个媒体项可能归属于多种媒体类型。可给MPMediaPickerController实例指定一些属性,如支持多选及显示提示,如图6.2所示。还有一个Boolean属性,它指定是否显示iCloud媒体项,默认为YES。

使用MPMediaPickerController让用户选择要播放的歌曲时,这些就是需要完成的所有步骤。然而,在很多情况下,都必须提供自定义的用户界面或在没有界面的情况下选择歌曲,这将在下一节介绍。

6.4 以编程方式选择媒体

经常需要以定制方式让用户选择音乐,这包括创建自定义音乐选择界面或自动搜索艺术家或专辑。本节讨论以编程方式选择音乐所需的步骤。

要在不使用 MPMediaPickerController的情况下获取歌曲,需要分配并初始化一个MPMediaQuery实例。MPMediaQuery相当于一个存储器,包含大量指向MPMediaItem的引用,而每个MPMediaItem都表示一个要播放的歌曲或音轨。

示例应用包含两个实现MPMediaQuery的方法。第一个是playRandomSongAction:,它在用户的音乐库中随机选择一个音轨,并使用现成的MPMusicPlayerController播放它。要以编程方式选择音乐,首先需要分配并初始化一个MPMediaQuery实例。

6.4.1 随机选择歌曲

没有提供任何谓词参数时,MPMediaQuery 将包含音乐库中所有的媒体项。创建了一个NSArray,用于存储这些媒体项;为获取这些媒体项,调用了MPMediaQuery的方法items。每个媒体项都由一个MPMediaItem 表示。示例应用每次随机选择一首歌曲并播放它。如果查询没有返回任何歌曲,就向用户显示一个UIAlert;如果返回了多首歌曲,就随机地选择一首。

选择要播放的歌曲后,使用它创建一个MPMediaItem 数组,再使用这个数组创建一个MPMediaItemCollection。这个MPMediaItemCollection将用作MPMusicPlayerController的播放列表。创建MPMediaItemCollection后,使用setQueueWithItemCollection将其传递给对象player。至此,播放器知道了用户要欣赏哪些歌曲。接下来,对对象player调用play以播放MPMediaItem Collection,这将依次播放用于创建MPMediaItemCollection的数组中的歌曲。

注意:

arc4random()位于标准C语言库中,可在Objective-C项目中使用它来生成随机数。不同于大多数随机数生成函数,arc4random在首次被调用时自动生成种子。

6.4.2 使用谓词选择歌曲

应用常常需要执行更复杂的歌曲搜索,而不仅仅是随机选择音轨,为此可使用谓词。下面的示例使用谓词在音乐库中查找属性MPMediaItemPropertyArtist为Bob Dylan的歌曲,如图6.3所示。这个方法的工作原理与前面的随机选择示例很像,但使用addFilterPredicate给MPMediaQuery添加了筛选器。另外,不是只选择一首歌曲,而给播放器传递一个数组,其中包含所有符合条件的歌曲。有关创建谓词时可使用哪些属性常量,请参阅表6.1的第二列。要使用多个谓词,可对MPMediaQuery调用方法addFilterPredicate多次。

6.5 小结

本章介绍了如何访问和使用用户的音乐库。首先,介绍了如何打造播放引擎,让用户能够控制播放,如暂停、继续播放、控制音量以及前进和后退;接下来,介绍了如何访问和选择音乐库中的歌曲;最后,演示了如何使用内置的媒体选择器让用户选择歌曲,以及如何使用谓词查找和搜索歌曲。

本章的示例应用演示了如何打造iOS音乐播放器,这个播放器虽然经过了简化,但功能齐备。利用本章介绍的知识,您可创建功能齐备的音乐播放器,也可将用户的音乐库作为应用的背景乐。

6.6 练习

1.在示例应用的播放指示器中添加一个刮擦条(scrubber),让用户能够通过拖曳播放头实时地指定播放位置。

2.给示例应用添加这样的功能:用户输入艺术家或歌曲名后,返回所有符合条件的歌曲供用户欣赏。

第8章 iCloud

iCloud是Apple提供的一组云服务。iCloud是iOS 5引入的,旨在替代MobileMe,以提供云存储以及在iOS设备、OS X设备和Web之间自动同步。iCloud提供了如下功能:邮件、通讯录和日历同步;iOS设备自动备份和恢复;“查找我的iPhone”,用于定位和/或禁用丢弃的设备;“查找我的朋友”,用于同家人和朋友分享位置信息;“我的照片流”,自动将照片发送到其他设备;“Back to My Mac”,让用户无需配置就能通过网络访问自己的Mac;“iTunes Match”,无需上传和同步就能访问用户的音乐库。另外,iCloud还让应用能够将其专用的数据存储到云端,并自动在设备之间同步这些数据。编写本书时,iCloud 提供 5GB的免费存储空间,并提供了使用更多存储空间的付费套餐。

在应用数据存储和同步方面,iCloud 提供了 3 种方式:基于文档的存储和同步(基于NSDocument或UIDocument)、键值存储和同步(类似于NSUserDefaults)以及Core Data同步。本章介绍如何配置应用,以便使用UIDocument和键值存储同步通过iCloud进行同步。本章不介绍Core Data同步;Core Data同步的配置和初步实现虽然比较简单,但存在一些棘手的实现问题,使得使用Core Data同步的风险极大。具体地说,迁移既有数据集时,初步数据的同步非常复杂;用户关闭iCloud或删除iCloud数据后,处理起来非常棘手;还有其他几个相当复杂的问题。要更详细地了解这些问题以及一些推荐的处理方式,请参阅Marcus Zarra编写的《Core Data:Data Storage and Management for iOS,OS X,and iCloud》(第二版)。

8.1 示例应用

本章的示例应用名为 MyNotes,这是一个简单的备忘录编辑器。备忘录是使用一个UIDocument子类创建的,并使用iCloud在设备间同步。这个示例应用还使用基于iCloud的键值存储来记录最后一次编辑的是哪个备忘录,并在设备之间同步这种信息。

在实际应用中,通常并非必须支持 iCloud,因为并非所有用户都有 iCloud 账户。出于简化考虑,这个示例应用假设开启了 iCloud,且没有 iCloud 账户时不支持在本地存储。本地存储和云存储的主要差别是表示文件存储位置的URL。只支持本地存储时,用户生成的文件将存储在应用沙箱中的目录Documents下;使用iCloud时,文档存储在一个特殊目录下,该目录的URL由系统提供(详情请参阅8.2节和8.4节)。

8.2 配置应用以支持iCloud

要配置应用以使用iCloud,以前必须执行多个步骤:给应用指定权限(Entitlement),并前往iOS供应配置文件门户(iOS Provisioning Portal)配置应用。iCloud功能只能在设备上测试,因此要让iCloud应用能够正确运行,必须先完成与供应配置文件相关的工作。Xcode 5推出后,这个过程极大地简化了,在Xcode 5中就能完成。

8.2.1 设置账户

为连接到Member Center并代表开发人员执行所需的iCloud配置工作,Xcode 5需要iOS开发人员信息。选择菜单Xcode>Preferences,再选择选项卡Accounts,如图8.1所示。

要添加新账户,单击Accounts选项卡左下角的加号并选择Apple ID。输入账户凭证,再单击Add按钮。Xcode将验证凭证,并在凭证有效时收集账户信息。可单击View Details按钮,以查看账户凭证以及为账户配置的供应配置文件,如图8.2所示。

8.2.2 启用iCloud功能

有了账户凭证后,Xcode 5便可使用这个账户给应用配置功能。它可根据需要设置App ID、权限和供应配置文件。为配置iCloud功能,在Xcode中选择Targets下的MyNotes,单击标签Capabilities,并找到iCloud部分。将iCloud开关设置为On,Xcode将自动为项目创建一个权限文件。选中复选框Use key-value store,为应用启用键值存储功能。Xcode将在Ubiquity Containers表中自动添加一项,其值为应用的束ID。就这个示例应用而言,这就够了;如果应用更复杂,需要与Mac OS X应用共享数据并支持多个无处不在的容器(ubiquity container),可在这里添加其他的名称。Xcode将向开发人员门户核实,确定为iCloud配置的App ID是否正确。如果不正确,Xcode将指出问题,如图8.3所示。如果您轻按按钮Fix Issue,Xcode将与开发人员门户联系,并修复所有的应用设置问题。

8.2.3 初始化iCloud

应用每次运行时,都必须调用NSFileManager的方法URLForUbiquityContainerIdentifier,以获取用于存储和同步文件的iCloud 容器的URL。为此,在应用委托的方法 application:didFinishLaunchingWithOptions:中,调用了访问setupiCloud。

首次调用时,方法 URLForUbiquityContainerIdentifier 将为应用设置目录;以后调用时,它将核实URL是否还在。如果为应用启用了iCloud,将返回一个有效的URL;如果禁用了iCloud,将返回nil。用户完全有可能对应用(甚至整个设备)禁用iCloud,导致iCloud不再可用。在实际应用中,应在iCloud不可用时通知用户,并将所有文件操作都重定向到本地的Documents目录。

需要注意的一个重要细节是,对 URLForUbiquityContainer:的调用是在一个后台队列中进行的。这样做是因为这个方法返回的时间不确定。如果调用它时还没有任何 iCloud 数据,它将很快执行完毕并返回;然而,如果调用它时有多个文档需要下载,系统可能花一段时间来设置目录并使用云端数据填充文件。必须考虑这种延迟问题,以免影响用户界面的响应速度。

8.3 UIDocument简介

应用的功能围绕着以用户为中心的文档式数据展开时,创建 UIDocument 子类很合适。UIDocument 用于自动完成与文档交互时所需的众多典型功能。例如,UIDocument 支持自动保存,还支持后台加载和保存,以免影响主队列,进而降低用户界面的响应速度。另外,UIDocument 将加载和保存逻辑抽象为简单的方法调用,让开发人员只需编写在文档数据和NSData之间进行转换的简单逻辑。对本章来说最重要的是,UIDocument还自动完成与iCloud的交互。

8.3.1 创建UIDocument子类

在示例应用中,用户创建的每条备忘录都是UIDocument子类ICFMyNoteDocument的实例。UIDocument子类应实现方法contentsForType:error:和loadFromContents:ofType:error:,并实现修改跟踪以启用UIDocument的自动保存功能。在示例应用中,由于备忘录只在一个字符串变量中存储备忘录内容,因此这些方法实现起来很容易。

在方法contentsForType:error:中,首先检查是否给myNoteText指定了值,如果没有,就提供一个默认值以防应用崩溃。接下来,将myNoteText转换为NSData并返回转换结果。这个NSData就是UIDocument将保存的内容。方法loadFromContents:ofType:error:执行的操作与此相反。

UIDocument以NSData格式返回保存的内容,而这个方法将其转换为子类的属性。它检查内容的长度,如果没有内容,就将myNoteText设置为空字符串,否则将NSData转换为NSString,并使用转换结果填充属性myNoteText。最后,这个方法告诉委托文档发生了变化,让它能够采取合适的措施,如使用新加载的数据更新用户界面。

为启用 UIDocument 提供的自动保存功能,子类需要实现变更跟踪。变更跟踪是使用UIDocument提供的undoManager启用的。

注意到不需要执行额外的操作就能自动保存:应用不需要实际实现撤销功能;给undoManager指定操作(action)后,自动保存功能便启用了。

8.3.2 与UIDocument交互

要创建UIDocument子类的实例,首先需要确定表示文件保存位置的URL。用户轻按示例应用中的加号按钮时,将调用主视图控制器的方法newMyNoteName,由它决定新备忘录将使用的文件名。这个方法在MyNote后面加上一个数字来生成文件名,并检查它是否存在;如果存在,就将数字加1,直到生成的文件名未被使用。接下来,将这个文件名附加到iCloud目录URL的后面,得到文件的完整路径。然后,将这个URL传递给详细视图控制器。在详细视图控制器中,方法configureView使用NSFileManager判断这个文件是否存在,并据此创建或加载它。

为创建文档,调用了UIDocument的方法saveToURL:forSaveOperation:completionHandler:,并将保存操作指定为 UIDocumentSaveForCreating。为打开既有文档,调用了方法 openWith CompletionHandler,并指定了一个结束块。在这里,结束块将更新用户界面,并将文本视图设置为第一响应者以便开始编辑备忘录。请注意,这里检查了文档的documentState;这个属性指出文档可编辑还是处于需要解决的冲突状态。冲突状态将在本章后面介绍,详情请参阅8.4.2节。

8.4 与iCloud交互

添加iCloud功能后,应用需要处理一些额外的复杂问题。应用未使用iCloud时,列出其文档很简单,但使用 iCloud 后,可用文档清单随时会变,甚至在清单生成和显示期间都会改变。另外,由于可能在多台设备上同时编辑同一个文档,可能导致不使用 iCloud 时根本不会出现的冲突。本节介绍如何妥善地处理这些问题。

8.4.1 列出iCloud中的文档

要显示可用的备忘录清单,示例应用需要查询 iCloud 目录,以确定那里都有哪些文件。这是使用NSMetadataQuery完成的。

将NSMetadataQuery的搜索范围设置成了 NSMetadataQueryUbiquitousDocumentsScope,它表示应用的iCloud 目录。接下来,将谓词指定为一个文件模式字符串,这个字符串与文件扩展名为.icfnote的文档都匹配。元数据查询是在ICFMasterViewController的viewDidLoad方法中执行的,这个视图控制器还接收来自NSMetadataQuery的通知。

这两个通知分别在如下两种情况下调用方法 processFiles::NSMetadataQuery 收集了与谓词匹配的信息;其他设备修改了 iCloud中的文档。iCloud 同步来自其他设备的新文件时,将更新iCloud目录。

调用processFiles:后,在处理结果期间让NSMetadataQuery停止收集信息很重要。这可避免这个方法在更新期间被再次调用,以免应用崩溃或获得错误的结果。然后,可迭代查询结果并创建新的备忘录清单。创建备忘录清单后,再更新表视图,如图8.4所示。接下来,让NSMetadataQuery继续获取文件系统更新。

NSMetadataQuery 生成的文件清单是一个NSURL 数组。在方法 tableView:cell ForRowAtIndexPath:中,从这些NSURL中提取了文件名(不包括扩展名),以便将其显示在表单元格中。

其中的if逻辑检查备忘录是不是最后更新的备忘录,这种信息是使用iCloud键值存储维护的,这将在本章后面介绍(参见8.6节)。如果是,就在文件名前面加上一个星号。

打开文档和新建文档在8.3.2节介绍过。用户选择一个表行或轻按加号按钮时,将把新文档或既有文档的URL传递给详细视图控制器,后者打开或创建指定的文档,在文本视图中显示该文档的文本,并将文本视图设置为第一响应者,让用户马上能够进行编辑。

关闭文档很简单。在详细视图控制器的方法 viewWillDisappear:中,使用文本视图中的文本更新文档,再关闭文档。由于启用了自动保存功能,因此保存是自动完成的。

8.4.2 检测iCloud冲突

所有的同步技术都可能发生冲突。冲突指的是文档在多台设备上被同时编辑,导致 iCloud根据同步规则无法确定哪个版本是最新的。

要引发冲突,可在两台设备上同时运行示例应用。将一台设备切换到飞行模式,编辑并保存一份备忘录;再在另一台设备上编辑并保存这份备忘录。然后,在第一台设备上关闭飞行模式,并尝试在第二台设备上再次编辑那份备忘录。第二台设备将发生冲突。

UIDocument类有一个文档状态属性,指出了文档发生了冲突还是可正常编辑。UIDocument对象还在文档状态发生变化时发出通知,这很有用,因为编辑文档期间发生冲突时,如果能够马上获悉并解决冲突,用户体验将好得多。为检测冲突,详细视图控制器需要通过注册从文档那里收到文档状态变化通知,这是在方法viewWillAppear:中完成的。

这里指定由方法 documentStateChanged 来处理文档状态变化。这个方法检查当前的文档状态,并据此对用户界面做必要的调整。

如果文档状态为 UIDocumentStateEditingDisabled,这个方法就让文本视图放弃第一响应者地位,导致编辑马上结束。如果文档状态为UIDocumentStateInConflict,就更新用户界面,显示一个让用户能够通过轻按来解决冲突的按钮(如图8.5所示),否则就让用户界面返回到正常编辑状态。

8.5 解决冲突

文档处于冲突状态时,可通过NSFileVersion类获悉文档的各种版本。用户轻按按钮Resolve Conflict时,将调用方法resolveConflict-Tapped:。这个方法收集处于冲突状态的文档的版本信息,并实例化一个自定义的页面视图控制器,让用户能够浏览不同的冲突版本并选择最终版本。

这个方法首先获取冲突版本,即不能合并到本地版本的远程版本,并将它们放到一个数组中,因为可能存在多个冲突版本。接下来,这个方法获取文档的当前版本,即本地编辑的版本,并将它和冲突版本一起加入到一个可变数组中。这个可变数组提供了所有版本,供用户进行评估以选择正确的版本。然后,这个方法创建一个ICFConflictResolutionViewController实例,这是一个自定义页面视图控制器,让用户能够浏览冲突版本并选择正确版本。接下来,设置这个页面视图控制器的属性,让它知道有哪些冲突版本、当前版本是哪个(这对后面解决冲突很重要)、处于冲突状态的备忘录的URL以及选定正确版本后要调用的委托。

注意:

页面视图控制器提供了浏览冲突版本的方便途径,但并非只能采用这种方式。任何可显示冲突版本信息并让用户选择正确版本的方式都可行。

接下来,显示冲突解决页面视图控制器,如图8.6所示。

对于备忘录的每个冲突版本,冲突解决页面视图控制器都创建一个ICFConflictVersionView Controller实例,用于显示该冲突版本的信息。在方法viewControllerAtIndex:storyboard:中,页面视图控制器根据需要实例化版本视图控制器,并指定它应显示哪个NSFileVersion。

用户可通过翻页浏览不同的冲突版本,如图8.7所示。

为显示各个版本的信息,版本视图控制器在方法viewDidLoad中从其NSFileVersion那里获取这些信息,并更新用户界面。

用户选择一个版本后,版本视图控制器将选定的版本告诉其委托——页面视图控制器。

页面视图控制器再将选定的版本告诉详细视图控制器,并指出选定的版本是否是当前版本(本地版本)。

详细视图控制器再根据选定版本是否是当前版本采取相应的措施,以解决冲突;这是在方法noteConflictResolve:forCurrentVersion:中完成的。这个方法检查传入的参数isCurrentVersion,如果其值为YES,就删除URL指定的文件的其他版本,并告诉其他版本冲突解决了。

如果选定版本不是当前版本,这个方法将以稍微不同的方式处理冲突。它用选定版本替换当前版本,再删除其他版本,然后指示冲突解决了。

此时,文档将发出通知,指出自己恢复到了正常状态。方法documentStateChangedmethod收到这个通知后,让用户能够继续编辑文档(这在8.4.2节介绍过)。

8.6 键值存储同步

iCloud还支持键值存储同步。这类似于在NSMutableDictionary或NSUserDefaults中存储信息,将一个与对象值相关联的键用于存储和检索;iCloud键值存储的不同之处在于,将自动在设备之间同步键和值。本书编写期间,iCloud为每个应用提供1MB的键值存储空间,最多可存储1024个键值对,因此键值存储机制只适合用于存储少量信息。

本章的示例应用利用 iCloud 键值存储来跟踪最后修改的备忘录。详细视图控制器在方法configureView中存储最后修改的备忘录的名称。

这个方法获取指向iCloud键值存储的引用,这是一个NSUbiquitousKeyValueStore实例。它将键kICFLastUpdatedNoteKey的值设置为备忘录名称,再调用synchronize确保数据得以马上同步。

主视图控制器在方法 viewDidLoad中注册通知 NSUbiquitousKeyValueStoreDidChange ExternallyNotification。

收到键值存储发生了变化的通知后,将调用方法updateLastUpdatedNote:。

这个方法获取一个指向 iCloud 键值存储的引用,将一个属性设置为最后修改的备忘录的名称,并重新加载表视图。方法tableView:cellForRowAtIndexPath:显示表单元格时,在最后修改的备忘录旁边添加一个星号。

请在一台设备上显示备忘录清单,并在另一台设备上修改一份备忘录。注意到几秒钟后,第一台设备上的星号将移到刚修改的备忘录旁边。

8.7 小结

本章介绍了如何使用 iCloud 在运行同一个应用的设备之间同步应用的数据,演示了如何配置应用使其能够使用iCloud,包括指定权限、必须在配置文件门户(Provisioning Portal)做的配置工作以及必须在应用中做的配置工作。本章介绍了如何创建UIDocument子类以及如何启用文档自动保存功能;接下来,本章阐述了如何列出并显示存储在iCloud中的文档以及如何检测冲突,还演示了如何查看冲突信息以及解决冲突。最后,本章演示了如何使用 iCloud 键值存储来同步应用的数据。

8.8 练习

1.本章的备忘录只存储了一个字符串,请对其进行改进,使其存储并显示创建日期和时间、最后一次修改的日期和时间以及一幅图像。

2.在本章的示例应用中,冲突解决方案只使用了文件元数据来帮助用户决定要保留哪个版本。请扩展这种解决方案,使其下载并显示每个版本的实际内容。

相关图书

SwiftUI极简开发
SwiftUI极简开发
iOS 14开发指南【进QQ群414744032索取配套资源】
iOS 14开发指南【进QQ群414744032索取配套资源】
iOS 11 开发指南
iOS 11 开发指南
iOS和tvOS 2D游戏开发教程
iOS和tvOS 2D游戏开发教程
Swift 3开发指南
Swift 3开发指南
iOS  项目开发全程实录
iOS 项目开发全程实录

相关文章

相关课程