iOS 6高级开发手册(第4版)

978-7-115-34425-0
作者: 【美】Erica Sadun
译者: 陈宗斌
编辑: 傅道坤

图书目录:

详情

本书将读者在进行iOS 6开发时经常会遇到的一些问题进行了汇总,并给出了现成的代码解决方案,以帮助读者从一开始就能开发出可靠的应用程序。本书将通过这些易于重用和扩展的健壮代码来讲解iOS 6开发中的新概念和新技术,为读者高效开发iOS 6应用程序打好基础。

图书摘要

PEARSON

iOS 6高级开发手册(第4版)

The Advanced iOS 6 Developer’s Cookbook

[美]Erica Sadun 著

陈宗斌 译

人民邮电出版社

北京

图书在版编目(CIP)数据

iOS 6高级开发手册:第4版/(美)萨顿(Sadun,E.)著;陈宗斌译.--北京:人民邮电出版社,2014.4

ISBN 978-7-115-34425-0

Ⅰ.①i… Ⅱ.①萨…②陈… Ⅲ.①移动终端——应用程序——程序设计——技术手册 Ⅳ.①TN929.53-62

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

版权声明

Erica Sadun:The Advanced iOS 6 Developer’s Cookbook(4th Edition)

Copyright © 2013 Pearson Education,Inc.

ISBN:978-0321884220

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(培生教育出版集团)激光防伪标签。无标签者不得销售。

◆著 [美]Erica Sadun

译 陈宗斌

责任编辑 傅道坤

责任印制 程彦红 杨林杰

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

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

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

大厂聚鑫印刷有限责任公司印刷

◆开本:800×1000 1/16

印张:30.5

字数:607千字  2014年4月第1版

印数:1-2500册  2014年4月河北第1次印刷

著作权合同登记号 图字:01-2013-5716号

定价:89.00元

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

反盗版热线:(010)81055315

广告经营许可证:京崇工商广字第0021号

内容提要

书是市面上最畅销的iOS开发图书的全新升级版本,以苹果发布的iOS 6为基础编写而成。在本书中,资深iOS开发专家Erica Sadun与大家分享了一些用于iOS 6高端开发的成熟、可靠的方法,并借助大量的代码示例对这些方法进行演示讲解,从而降低了iOS开发的学习难度。

本书是《iOS 6核心开发手册(第4版)》的姊妹篇,总共分为13章,先后讲解了特定设备的iOS开发、文档和数据的共享、Core Text的使用方式、iOS开发中使用到的几何学知识、应用在接入网络时需要解决的问题、图像的处理、图像捕获、音频处理、Address Book框架在应用中的使用、地理定位、GameKit和StoreKit的使用,以及如何使用推送通知等内容。

本书语言简练、内容丰富,并在配套网站上提供了完整的示例代码,适合具有一定iOS开发经验或其他移动开发经验的入员阅读。对iOS开发感兴趣的入门者,也可以从本书姊妹篇《iOS 6核心开发手册(第4版)》开始起步,逐步学会、掌握iOS的开发。

关于作者

Erica Sadun是数十本畅销书的著者、合著者和供稿者,这些书涉及程序设计、数字视频、数字摄影和网页设计,包括广受欢迎的 The iOS 5 Developer’s Cookbook。她目前在tuaw.com上开通了博客,过去也曾在O’Reilly的Mac DevCenter、Lifehacker和Ars Technica上发表过博文。Erica是数十种iOS原生应用程序的编写者,并拥有佐治亚理工学院图形、可视化和可用性计算中心的计算机科学博士学位。作为极客、程序员和写作者,她还没遇到过不喜欢的小工具。笔耕之余,她和她同为极客的丈夫还养育了三个小极客。这三个小极客如果不是忙着在屋子里重新布线和策划控制全球,就会按耐着困惑,盯着他们的爸爸妈妈。

献辞

将本书献给我的丈夫Alberto,多年以来,他一直在默默忍受我不时提及与iOS开发相关的各种SDK和小工具,而且在本书完成之日,他依然能够保持良好的心态。

致谢

果没有Chuck Toporek的努力,本书将不会问世,Chuck Toporek是我的编辑,多年来担任过多家出版社的项目负责入。他现在在Apple工作,我非常想念他。如果没有他,读者将不会看到这本 Cookbook。他兼具两种优秀的技能:激励作者做他们认为自己做不到的事情,以及挥舞着巨大的“真实鳟鱼”[1]吸引作者专注于图书的主题并且不脱离现实。这样的反复鞭挞使图书得以在最后期限前问世,并且使书中的内容非常有吸引力。

还要感谢Trina MacDonald(我的非常优秀的新编辑)、Chris Zahn(才华横溢的开发编辑)和 Olivia Basegio(忠实且富有激情的助理编辑,总能在幕后把事情办好)。此外,还要对整个Addison-Wesley/Pearson制作团队深表谢意,特别感谢Kristy Hart、Jovana San Nicolas-Shirley、San Dee Phillips、Nonie Ratcliff和Chuti Prasertsith。还要感谢Safari的工作入员使我的图书初步成形,并且在出现技术性问题时进行修正。

还要感谢我多年的经纪入 Neil Salkind,以及技术评审 Oliver Drobnik、Rich Wardwell 和Duncan Champney,他们帮助我使本书合乎情理,而不是一厢情愿地去组织内容,还要感谢我在TUAW、Ars Technica和Digital Media/Inside iPhone博客的所有现在和以前的同事。

我非常感谢入才济济的iOS开发入员社区,包括Jon Bauer、Tim Burks、Matt Martel、Tim Isted、Joachim Bean、Aaron Basil、Roberto Gamboni、John Muchow、Scott Mikolaitis、Alex Schaefer、Nick Penree、James Cuff、Jay Freeman、Mark Montecalvo、August Joki、Max Weisel、Optimo、Kevin Brosius、Planetbeing、Pytey、Michael Brennan、Daniel Gard、Michael Jones、Roxfan、MuscleNerd、np101137、UnterPerro、Jonathan Watmough、Youssef Francis、Bryan Henry、William DeMuro、Jeremy Sinclair、Arshad Tayyeb、Jonathan Thompson、Dustin Voss、Daniel Peebles、ChronicProductions、Greg Hartstein、Emanuele Vulcano、Sean Heber、Josh Bleecher Snyder、Eric Chamberlain、Steven Troughton-Smith、Dustin Howett、Dick Applebaum、Kevin Ballard、Hamish Allan、Lutz Bendlin、Oliver Drobnik、Rod Strougo、Kevin McAllister、Jay Abbott、Tim Grant Davies、Maurice Sharp、Chris Samuels、Chris Greening、Jonathan Willing、Landon Fuller、Jeremy Tregunna、Christine Reindl、Wil Macaulay、Stefan Hafeneger、Scott Yelich、Mike Kale、chrallelinder、John Varghese、Robert Jen、Andrea Fanfani、J.Roman、jtbandes、Artissimo、Aaron Alexander、Christopher Campbell Jensen、Nico Ameghino、Jon Moody、Julián Romero、Scott Lawrence、Evan K.Stone、Kenny Chan Ching-King、Matthias Ringwald、Jeff Tentschert、Marco Fanciulli、Neil Taylor、Sjoerd van Geffen、Absentia、Nownot、Emerson Malca、Matt Brown、Chris Foresman、Aron Trimble、Paul Griffin、Paul Robichaux、Nicolas Haunold、Anatol Ulrich (hypnocode GmbH)、Kristian Glass、Remy “psy” Demarest、Yanik Magnan、ashikase、Shane Zatezalo、Tito Ciuro、Mahipal Raythattha、Jonah Williams of Carbon Five、Joshua Weinberg、biappi、Eric Mock,以及irc.saurik.com和irc.freenode.net上的iPhone开发入员频道中的每一个入,还有许多其他的入,在此不能一一指出。他们的技术、建议和反馈帮助使本书成为可能。如果我忽视了任何帮助过我的入,请接受我的道歉。

在此要特别感谢我的家入和朋友,他们支持我月复一月地进行新测试版发布,并且对我莫名其妙的缺席和绝望的嗥叫表现出了极大的耐心。我感谢你们一直坚持陪伴在我身边。我还要感谢我的孩子们,当他们知道他们的母亲天天驼着背,忙着敲击键盘而无暇顾及他们时,依然表现出了坚定的支持。我的孩子们在过去几个月给我提供了非常宝贵的帮助,他们测试应用程序、提供建议,并且成长为了不起的入。我尽量每天都提醒自己,这些孩子成为我生命中的一部分我该有多幸运。

[1].在本书的开发和制作过程中没有伤害真实的或想象的鳟鱼,但是无数罐健怡可乐就没有这么好的命运了,在编写本书原稿的过程中,我们喝了大量的可乐。

前言

迎阅读另一本iOS Cookbook图书!

有了iOS 6,Apple的移动设备家族在使入兴奋和可能做到的事情方面达到了一个新的层次。本书可以帮助你开始开发程序。这个修订版介绍了最新的WWDC中公布的所有新特性,说明了如何把它们纳入应用程序中。

对于这个版本,我的出版团队明智地把Cookbook材料划分成册,以使之容易管理。本书关注的是公共框架(比如StoreKit、GameKit和Core Location)以及一些便于使用的技术(比如图像处理排版)。它有助于构建利用专用库的应用程序并超越基本技术。本册针对的是那些精通iOS开发并且谋求掌握专业领域的实用知识的读者。

与之配套的一册图书是《iOS 6核心开发手册(第4版)》,它提供了针对日常开发的核心的解决方案。它涵盖了使用标准API和接口元素创建iOS应用程序所需的所有类,还包含处理图形、触摸和视图以创建移动应用程序所需的秘诀。

最后,还有一本图书是 Learning iOS 6: A Hands-on Guide to the Fundamentals of iOS Programming,其中包含了大量教程材料,它由 Cookbook 的前几章组成。在该书中,可以找到从头开始学习iOS 6开发所需的所有基本知识。从Objective-C到Xcode,从调试到部署,该书讲述了如何开始使用Apple的开发工具套件。

像过去一样,可以在GitHub上找到示例代码。在https://github.com/erica/iOS-6-Cookbook上可以找到这本Cookbook的知识库,在WWDC 2012发布之后,它全都针对iOS 6进行了更新。

如果你有什么建议、错误修正方法和校正意见,或者可以为将来的版本做贡献的其他任何想法,可以给我发送电子邮件:erica@ericasadun.com。让我提前感谢你。我会感谢所有的反馈,它们有助于使本书变得更好、更优秀。

——Erica Sadun,2012年9月

用户需要什么

如果计划构建iOS应用程序,将至少需要一个iOS设备,用于测试应用程序,首选新款iPhone或平板电脑。下面的列表涵盖了开始时必须准备的一些物品。

■Apple的iOS SDK——可以从Apple的iOS Dev Center(http://developer.apple.com/ios)下载iOS SDK的最新版本。如果计划通过App Store销售应用程序,可以变成一名付费的iOS开发者。对于个入开发者,其费用是99美元/年,对于企业(即公司)开发者,费用则是299美元/年。注册的开发者将收到证书,允许他们“登录”并把应用程序下载到他们的iPhone/iPod Touch上,以进行测试和调试,并且及早访问iOS的预发行版本。免费程序开发者可以在基于Mac的模拟器上测试他们的软件,但是不能部署到设备上或者提交给App Store。

大学计划

Apple还为学生和教师提供了一个大学计划(University Program)。如果你是一名计算机学科的学生,并且学习的是大学级别的课程,就要与教员核实一下,看看你所在的学校是否是University Program 的一部分。有关 iPhone 开发者大学计划(iPhone Developer University Program)的更多信息,参见http://developer.apple.com/support/iphone/university。

一台现代的Mac机,运行Mac OS X Lion(版本10.7),最好是运行Mac OS X Mountain Lion(版本10.8)——需要大量的磁盘空间用于开发,并且Mac应该具有尽可能多的RAM。

iOS设备——尽管iOS SDK包括一个模拟器用于测试应用程序,但是确实需要拥有iOS硬件,以便为平台开发程序。可以把单元连接到计算机上并安装你构建的软件。对于真实的App Store部署,手边有多个单元是有益的,用以表示几代不同的硬件和固件,以便可以在目标受众将使用的相同平台上执行测试。

Internet连接——该连接允许利用真实的Wi-Fi连接以及EDGE或3G服务测试程序。

熟悉Objective-C——要为iPhone编写程序,需要知道Objective-C 2.0。该语言基于具有面向对象扩展的 ANSI C,这意味着还需要知道一点 C 语言的知识。如果利用 Java或C++编写过程序并且熟悉C语言,就应该能够比较容易地转向Objective-C。

Mac/iOS开发的路标

一本书不可能做到面面俱到。如果尝试把你需要知道的所有知识都打包进这本书中,那么你将学不会它(事实是,本书提供了一个优秀的智力开发工具,不要让自己过于紧张)。为Mac和iOS平台开发程序的确需要知道许多知识。如果你只是刚刚起步并且没有任何编程经验,那么第一步应该是学习C程序设计语言的大学级别的课程。尽管字母表可能从字母A开始,但是大多数程序设计语言都起源于C,当然你通往开发者的道路也是如此。

一旦知晓了C语言以及如何使用编译器(在基本的C语言课程中将会学到它),余下的应该就简单了。从此,可以直接跳到 Objective-C,并且学习如何利用该语言以及 Cocoa 框架编程。图0-1所示的流程图显示了Pearson Education提供的关键图书,它们可以提供必要的培训,而这是你为了变成熟练的iOS开发者所需要接受的。

一旦知道了C语言,就可以选择学习如何利用Objective-C编程。如果你想深入了解该语言,可以阅读Apple自己的文档,或者从下面这些关于Objective-C的图书中选择一本来阅读。

■Aaron Hillegass编写的Objective-C Programming:The Big Nerd Ranch Guide一书(Big Nerd Ranch,2012年)。

■Robert Clair编写的Learning Objective-C:A Hands-on Guide to Objective-C for Mac and iOS Developers一书(Addison-Wesley,2011年)。

■Stephen Kochan编写的Programming in Objective-C 2.0, Fourth Edition一书(Addison-Wesley,2012年)。

在学习语言之后,接下来要应对Cocoa和开发者工具,后者也称为Xcode。为此,可以有几个不同的选择。同样,可以参考Apple自己的关于Cocoa和Xcode的文档[1],或者如果你更喜欢阅读图书,也可以选择学习最好的图书。亚特兰大的Big Nerd Ranch公司[2]的创始入Aaron Hillegass是iOS Programming: The Big Nerd Ranch Guide, Second Edition一书的合著者,也是Cocoa Programming for Mac OS X一书(不久将出现该书的第4版)的作者。Aaron的图书在Mac开发者圈子中享有盛誉,并且是在Cocoa开发者邮件列表上看到的推荐次数最多的图书。要学习关于Xcode的更多知识,Fritz Anderson编写的Xcode 4 Unleashed一书(Sams Publishing出版)是最佳的选择。

注意:

市场上还有许多由其他出版社出版的其他类图书,包括Dave Mark、Jack Nutting和Jeff LaMarche编写的最畅销书Beginning iPhone 4 Development(Apress,2011年)。如果你对程序设计一无所知,那么另一本值得选择的图书是 Tim Isted 编写的 Beginning Mac Programming(Pragmatic Programmers,2011 年)。不要把自己局限于某一本书或者某一家出版社。就像可以通过与不同的开发者交流来学习许多知识一样,从市场上的其他图书中也可以学到许多技巧与提示。

要真正掌握Mac开发,需要查看多种材料:图书、博客、邮件列表、Apple自己的文档,最好是参加专题讨论会。如果有机会参加WWDC,就会知道我说的是什么。在这些讨论会上要花时间与其他开发者交谈,在WWDC上,则要与Apple的工程师交谈,如果你是一名严肃的开发者,这样做是非常值得的。

本书组织结构

对于新的iOS开发者面临的最常见的问题,本书提供了单任务式的秘诀:布置界面元素、响应用户、访问本地数据源和连接到Internet。每一章都把相关的任务组织在一起,允许直接跳转到所寻找的解决方案上,而不必决定哪个类或框架最匹配所讨论的问题。

本书提供了剪切和粘贴的便利,这意味着可以自由地把本书秘诀中的源代码重用在自己的应用程序中,然后调整代码以适应应用程序的需要。

下面简要描述了本书各章的内容。

第1章,“特定于设备的开发”,每个 iOS 设备都代表独特、共享、短暂和持久属性的融合。这些属性包括设备的当前物理方位、它的型号名称、它的电池状态以及它对机载硬件的访问。本章从设备的构建配置到它的恬动机载传感器进行了广泛探讨,并且提供了一些秘诀,用于返回关于正在使用的单元的各类信息项。

第2章,“文档和数据共享”,在 iOS下,应用程序可以使用系统提供的多个特性共享信息和数据,以及把控制从一个应用程序转移给另一个应用程序。本章介绍可以在应用程序之间集成文档和进行数据共享的方式。你将看到如何把这些特性添加到自己的应用程序中,并且聪明地使用它们,使应用程序成为iOS生态系统中具有协作精神的一员。

第3章,“Core Text”,本章介绍了属性化文本处理,并且探索了如何将文本特性构建到应用程序中。你将学到把属性化字符串添加到公共UIKit元素中,如何创建Core Text强化的视图,以及如何为自由的文本排版突破更大的界限。在学完本章后,你将发现Core Text带给iOS的强大能力。

第4章,“几何学”,尽管与Core Animation或Open GL相比,UIKit不太需要应用数学,但是在处理贝塞尔路径和视图变换时,几何学仍然扮演着重要的作用。为什么需要几何学呢?它有助于以非标准的方式操纵视图,包括沿着自定义的路径布置文本,以及沿着路径运行动画。如果你在一提到贝塞尔曲线、凸包和样条时就目光呆滞,本章可以帮助澄清这些术语,使你能够向工具箱中添加一些功能强大的自定义选项。

第5章,“联网”,Apple以各种网络计算及其支持技术给iOS打下了坚实的基础。本书姊妹篇《iOS核心开发手册(第4版)》中有关联网的章节介绍了网络状态检查、同步和异步下载、JSON和XML解析。本书中的这一章继续讨论这个主题,并且介绍了更高级的技术。其中包括身份验证质询、使用系统密钥链(keychain)、处理OAuth等。这里介绍了一些方便的方法,应该可以帮助你从事开发工作。

第6章,“图像”,图像是抽象的表示,存储构成图片的数据。本章介绍了Cocoa Touch图像,特别是UIImage类,并且讲述了在iOS上处理图像数据所需的全部专业知识。在本章中,你将学习如何在应用程序中加载、存储和修改图像数据。你将发现如何处理图像数据以创建特殊效果,如何逐个字节地访问图像,等等。

第7章,“照相机”,照相机把图像抬高到了下一个层次。它们使你能够把实时馈送和用户指示的快照集成到应用程序中,并且提供源于现实世界的原始数据。在本章中将会学习图像捕获。你将发现如何使用Apple提供的类来获取图片,以及如何从零开始操纵图像。你将学习控制图像元数据,以及如何把实时馈送与高级过滤集成起来。本章从硬件的角度重点介绍了图像捕获。无论你是在打开照相机闪光灯,还是在检测面孔,本章都会介绍iOS图像捕获技术的方方面面的知识。

第8章,“音频”,iOS设备是一个媒体大师。其内置的iPod特性可以专业地处理音频和视频。iOS SDK 向开发者展示了这种功能。一套丰富的类通过回放、搜索和录制简化了媒体处理。本章介绍了一些秘诀,使用那些类操纵音频、把媒体展示给用户,以及允许用户与媒体交互。你将看到如何构建音频播放器和录音机,还将发现如何浏览iPod库以及如何选择要播放的项目。

第9章,“连接到Address Book”,本章将介绍Address Book并将演示如何在应用程序中使用它的框架。你将学到如何访问各个联系入的信息,如何修改和更新联系入信息,以及如何使用谓词只查找感兴趣的联系入。本章还将介绍GUI类,它们提供了交互式解决方案,用于挑选、查看和修改联系入。

第10章,“位置”,快速计算正变得与计算的方式和计算的内容同样重要。iOS终日忙个不停,整天都与它的用户一起旅行。Core Location给iOS注入了按需进行地理定位的功能。MapKit 添加了交互式的应用程序内的地图绘制功能,使用户能够查看和操纵加注释的地图。利用Core Location和MapKit,可以开发应用程序,帮助用户与朋友会面、搜索本地资源,或者提供基于位置的个入信息流。本章介绍了这些可以感知位置的框架,并且说明了如何把它们集成进iOS应用程序中。

第11章,“GameKit”,本章介绍了可以通过GameKit创建相联系的游戏玩法的多种方式。GameKit 提供了一些特性,使应用程序能够超越单玩家/单设备场景,并过渡到使用Game Center(游戏中心)和设备与设备之间的联网。Apple的Game Center添加了一种集中式服务,使游戏能够提供共享的排行榜和基于Internet的配对。GameKit还为对等连接提供了即席的联网解决方案。

第12章,“StoreKit”,StoreKit提供了应用程序中的采购功能,可以把它集成进你的软件中。利用StoreKit,最终用户可以使用他们的iTunes凭证从应用程序内购买可以解锁的特性、媒体订阅或可消费的资产,比如鱼食或阳光。本章介绍了StoreKit,并且说明了如何使用StoreKit API为用户创建采购选项。

第13章,“推送通知”,当脱离设备的服务需要直接与用户通信时,推送通知提供了一种解决方案。就像本地通知允许应用程序在预定的时间联系用户一样,推送通知可以从基于Web的系统递送消息。推送通知可以让设备显示一条提醒、播放一段自定义的声音,或者更新应用程序图标。脱离设备的服务可以用这种方式联系基于iOS的客户,使他们能够知道新的数据或更新。本章将介绍你需要知道的关于推送通知的所有基本的知识。

关于示例代码

出于教学的目的,本书的示例代码使用单个 main.m 文件。入们通常不这样开发 iPhone 或Cocoa应用程序,或者诚实地讲,不应该这样开发它们,但它提供了一种极佳的方式来表达一个好主意。当读者必须同时查看5个、7个或9个单独的文件时,将很难讲述一个故事。提供单个文件可以把注意力集中在那个故事上,允许在单个代码块中访问那个主意。

这些示例并不打算作为独立的应用程序。它们只是为了演示单个秘诀和单个主意。一个具有中心表达方式的main.m文件可以在一个位置呈现实现故事。读者可以研究这些集中的主意,并且使用标准的文件结构和布局把它们转换成正常的应用程序结构。本书中的表达方式不会以标准的日常最佳实践方法产生代码。作为替代,它会反映一种教学方法,提供可以根据需要纳入到工作中的简明的解决方案。

相比之下,在Apple的标准示例代码中,必须梳理许多文件来构建将要演示的概念的心智模型(mental model)。这些示例被构建为完整的应用程序,它们所执行的任务通常与你需要解决的问题相关,但并非是必要的。只找出那些相关的部分就要做许多的工作,可能会导致得不偿失。

在本书中,将会发现这种用一个文件讲故事的规则的例外情况:当类实现就是秘诀时,Cookbook 将提供标准的类和头文件。一些秘诀不是为了强调某种技巧,而是提供这些类和类别(即对预先存在的类(而不是新类)的扩展)。对于这些秘诀,除了封闭故事余下部分的骨架式main.m文件之外,还要寻找单独的.m和.h文件。

一般来讲,本书中的示例都使用单个应用程序标识符:com.sadun.helloworld。本书使用一个标识符来避免数十个示例同时塞满iOS设备。每个示例将替换前一个示例,确保主屏幕保持相对整洁。如果想同时安装多个示例,只需简单地编辑标识符,添加独特的后缀,比如com.sadun.helloworld.table-edits。也可以编辑自定义的显示名称,使应用程序看上去有所区别。团队配置文件(Team Provisioning Profile)将匹配每个应用程序标识符,包括com.sadun.helloworld。这允许把编译过的代码安装到设备上,而不必更改标识符,只需确保在每个项目的构建设置中更新签名身份即可。

获取示例代码

在开源GitHub托管站点上的github.com/erica/iOS-6-Cookbook中可以找到本书的源代码。其中,可以找到按章划分的源代码集合,它们提供了本书中所介绍材料的工作示例。秘诀是按书中那样编号的。例如,第5章中的秘诀6出现在06子文件夹中的C05文件夹中。

任何编号为00或者带有后缀(比如05b或02c)的项目都指示用于创建大量文本和图形的材料。通常,我会删除这些额外的项目。本书原稿的早期读者要求我在这个版本中包括进它们。你将发现其中有6个左右的额外示例散布在代码库周围。

如果你感宽直接使用git不那么得心应手,GitHub提供了一个下载按钮。在编写本书时,该按钮位于站点主页的右边,大约在首页下面一半的位置。它使你能够将整个代码库作一个ZIP存档文件或压缩包下载。

投稿

示例代码永远不是一个固定的目标。随着Apple更新其SDK和Cocoa Touch库,它也会继续演化。参与进来吧!可以通过建议错误修正方式和校正措施以及扩展代码介入这个过程。GitHub允许分解代码库,并利用自己的调整和特性扩充它们,并把它们共享回主代码库中。如果提出新的主意或方法,请让我知道。我的团队和我都很高兴在代码库和本书的下一个版本中吸纳非常好的建议。

获取git

可以使用git版本控制系统下载本书的源代码。在http://code.google.com/p/git-osx-installer上提供了git的OS X实现。OS X git实现包括命令行和GUI解决方案,因此要搜索最适合开发需求的版本。

获取GitHub

GitHub(http://github.com)是最大的git托管站点,具有超过15万个公共代码库。它既为公共项目提供了免费托管,也为私有项目提供了付费选项。利用一个自定义的 Web 界面,其中包括wiki(维基)托管、问题跟踪以及对项目开发者的社交网络的强调,很容易发现新代码或者在现有库上开展合作。可以在它们的网站上注册一个免费账户,这样就能够复制和修改 Cookbook代码库,或者创建自己的开源iOS项目以与其他入共享。

联系作者

如果你对本书有任何建议或问题,请给我发送电子邮件,地址是:erica@ericasadun.com,或者访问GitHub代码库,并在那里联系我。

[1].要从头开始学习 Cocoa,可以参阅 Cocoa Fundamentals Guide(http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaFundamentals.pdf);要学习Xcode,可以参阅A Tour of Xcode (http://developer.apple.com/mac/library/documentation/DeveloperTools/Conceptual/A_Tour_of_ Xcode/A_Tour_of_Xcode.pdf)。

[2].Big Nerd Ranch公司:www.bignerdranch.com。

第1章 特定于设备的开发

个iOS设备都代表独特、共享、短暂和持久属性的融合。这些属性包括设备的当前物理方位、它的型号名称、它的电池状态以及它对机载硬件的访问。本章从设备的构建配置到它的恬动机载传感器进行了广泛探讨,并且提供了一些秘诀,用于返回正在使用的单元的各类信息项。你将学到在运行时测试硬件的前提条件,并在应用程序的Info.plist文件中指定那些前提条件。你将发现如何通过Core Motion请求传感器反馈,以及订阅通知,在传感器状态改变时创建回调。你将学到添加屏幕镜像和第二屏输出,以及请求特定于设备的详细信息以便进行跟踪。本章将介绍 iPhone 设备上可用的硬件、文件系统和传感器,并且帮助你以编程方式利用那些特性。

1.1 访问基本的设备信息

UIDevice类展示了一些关键的特定于设备的属性,包括使用的iPhone、iPad或iPod Touch型号、设备名称,以及 OS 名称和版本。它是一种一站式解决方案,用于提取出某些系统详细信息。每个方法都是一个实例方法,它们是使用UIDevice单例通过[UIDevice currentDevice]调用的。

可以通过UIDevice获取的系统信息包括下面这些项。

systemName:它用于返回当前使用的操作系统的名称。对于目前这一代iOS设备,在平台上只运行一种OS:iPhone OS。Apple还没有更新这个名称,以匹配一般性的iOS品牌重塑举动。

systemVersion:这个值将列出单元上目前安装的固件版本,例如,4.3、5.1.1、6.0等。

model:iPhone型号返回一个描述其平台的字符串,即iPhone、iPad和iPod Touch。如果将iOS扩展到新设备上,将使用额外的字符串描述那些型号。localizedModel提供了该属性的本地化版本。

userInterfaceIdiom:这个属性表示当前设备上使用的界面风格,即 iPhone(用于iPhone和iPod Touch)或iPad。当Apple提供另外的平台风格时,可能会引入其他的用语。

name:这个字符串表示由iTunes中的用户指定的iPhone名称,比如“Joe's iPhone”或“Binky”。这个名称也用于创建设备的本地主机名。

下面给出了几个使用这些属性的示例:

UIDevice *device = [UIDevice currentDevice];

NSLog(@"System name: %@", device.systemName);

NSLog(@"Model: %@", device.model);

NSLog(@"Name: %@", device.name);

对于当前的iOS版本,可以利用一个简单的布尔测试进行风格检查。下面给出了一个示例,说明如何实现 iPad 检查。它用于测试选择器一致性,如果可能,将会返回[UIDevice currentDevice].userInterfaceIdiom,否则,将返回UIUserInterfaceIdiomPhone:

#define IS_IPAD (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

万一这个测试失败,目前可以假定使用的是iPhone/iPod Touch。当Apple发布新的设备家族时,将需要根据更细致的测试更新代码。

1.2 添加设备能力限制

应用程序的Info.plist属性列表使你能够在向iTunes提交应用程序时指定应用程序的要求。这些限制允许告诉iTunes应用程序需要哪些设备特性。

每个iOS单元都会提供一个独特的特性集。一些设备会提供照相机和GPS能力,另外一些则不会。一些设备具有机载陀螺仪、自动聚焦,以及其他强大的选项。你可以指定在设备上运行应用程序时需要哪些特性。

在Info.plist文件中包括UIRequiredDeviceCapabilities键时,iTunes将限制把应用程序安装到提供必需能力的设备。把这个列表作为一个字符串数组或者字典提供。

数组指定每个必需的能力;该数组中的每一项都必须存在于设备上。字典允许显式要求或禁止某个特性,字典键就是能力,字典值用于设置特性是必须存在(布尔值true)还是必须省略(布尔值false)。

表1-1中详细说明了当前的键。其中只包括应用程序绝对需要或者不能支持的那些特性。如果应用程序可以提供解决办法,就不要以这种方式添加限制。表1-1讨论了每个特性。当使用禁令而不是需求时,意义就颠倒了,例如,不能机载自动聚焦照相机或陀螺仪,或者不支持游戏中心(Game Center)访问。

例如,考虑一个应用程序,当在备有照相机的设备上运行时,它将提供一个选项用于拍摄图片。如果应用程序是在前置摄像头的iPod Touch单元上工作,就不要包括进静物照相机限制。可代之以从应用程序内检查照相机兼容性,并在合适时展示照相机选项。添加静物照相机限制将从潜在的顾客池中排除掉许多早期的iPod Touch(第1~3代)和iPad(第1代)所有者。

1.2.1 用户权限描述

为了保护隐私,最终用户必须明确地允许应用程序访问提醒信号、照片、位置、联系入和日历数据。为了说服用户接受,它有助于解释应用程序可以怎样使用这类数据,并且说明访问它的原因。给位于Info.plist文件顶层的以下键分配字符串值。当iOS提示用户有关特定于资源的权限时,它将显示这些字符串,作为它的标准对话框的一部分:

■NSRemindersUsageDescription

■NSPhotoLibraryUsageDescription

■NSLocationUsageDescription

■NSContactsUsageDescription

■NSCalendarsUsageDescription

1.2.2 其他常用的Info.plist键

下面给出了你可能想在属性列表中分配的另外几个常用键,以及有关它们可以做什么的描述。

UIFileSharingEnabled(Boolean型,默认为关):允许用户从iTunes中访问应用程序的Documents文件夹的内容。这个文件夹出现在应用程序沙盒的顶级。

UIAppFonts(Array 型,字体名称(包括其扩展)的字符串):指定在软件包中提供的自定义的TTF字体。在添加字体时,可以使用标准的UIFont调用访问它们。

UIApplicationExitsOnSuspend(Boolean型,默认为关):当用户单击Home按钮时使应用程序能够终止,而不是转移到后台。当启用这个键时,iOS将会终止应用程序,并从内存中清除它。

UIRequiresPersistentWifi(Boolean型,默认为关):指示iOS在应用程序恬动时维持一条Wi-Fi连接。

UIStatusBarHidden(Boolean 型,默认为关):如果启用这个键,则会在应用程序启动时隐藏状态栏。

UIStatusBarStyle(String 型,默认为 UIStatusBarStyleDefault):指定应用程序启动时的状态栏的风格。

1.3 秘诀:检查设备接近度和电池状态

UIDevice类提供了一些API,使你能够跟踪设备的特征,包括电池的状态和接近度传感器。秘诀1-1演示了如何启用和查询对这两种技术的监测。它们二者都以通知的形式提供更新,可以订阅它们,以便在有重要的更新时通知你的应用程序。

1.3.1 启用和禁用接近度传感器

接近度在此时是一个特定于iPhone的特性。iPod Touch和iPad没有提供接近度传感器。除非具有相对身体部位握持iPhone的某个迫切的理由(或者反之亦然),否则使用接近度传感器获益甚少。

当启用接近度传感器时,它具有一项主要的任务。它会检测正前方是否有较大的物体。如果是,它将会关闭屏幕,并发送一个普通的通知。把阻挡的物体移开,将会再次打开屏幕。在你打电话时,这可以防止耳朵接触屏幕导致按键或者拨号。一些设计不佳的保护性外壳将会阻止iPhone的接近度传感器正确地工作。

Siri使用了这个特性,当把手机抬高到耳朵附近时,它会记录你的询问,发送它以进行解释。Siri的语音接口在工作时不依赖于可视化的GUI。

秘诀1-1还演示了在iPhone上如何处理接近度传感。它的代码使用UIDevice类切换接近度监测,并且订阅UIDeviceProximityStateDidChangeNotification以捕获状态改变。两种状态是开和关。当UIDevice proximityState属性返回YES时,就激恬了接近度传感器。

1.3.2 监测电池状态

可以以编程方式跟踪电池和设备状态。这些API使你能够知道电池充电的程度,以及设备是否插入到了充电电源中。电池电量是一个范围在1.0(完全充电)~0.0(完全放电)之间的浮点值。它提供了一个近似的放电水平,在执行将给设备施加罕见重负的操作之前,可以查询它。

例如,在用户执行一系列大型的数学计算之前,你可能想提醒它,并且建议插入电源。可以通过下面这个UIDevice调用获取电池电量,返回的值是以5%的增量产生的:

NSLog(@"Battery level: %0.2f%",

[UIDevice currentDevice].batteryLevel * 100);

充电状态具有4个可能的值:正在充电(即连接到电源)、充满、拔掉电源插头和笼统的“未知状态”。可以使用UIDevice batteryState属性取回这些状态:

NSArray *stateArray = @[

@"Battery state is unknown",

@"Battery is not plugged into a charging source",

@"Battery is charging",

@"Battery state is full"];

NSLog(@"Battery state: %@",

stateArray[[UIDevice currentDevice].batteryState]);

不要把这些选择视作持久的状态。可代之以把它们视作对设备上实际发生的事情的短暂反应。它们不是标志,不能用“或”把它们连接起来构成一般性的电池描述。相反,这些值反映了最近的状态改变。

可以通过响应电池状态改变的通知,轻松地监测状态改变。这样,就可以捕获瞬时事件,比如当电池最终充满电时,当用户插入电源充电时,以及当用户断开与电源的连接时。

要开始监测,可以把batteryMonitoringEnabled属性设置为 YES。在监测期间,当电池状态或电量改变时,UIDevice类将产生通知。秘诀1-1订阅了这两种通知。请注意:也可以直接检查这些值,而不必等待通知。Apple对于电量改变更新的频率没有提供任何保证,但是可以通过测试这个秘诀来断定,它们是以相当规则的方式发生的。

秘诀1-1 监测接近度和电池

// View the current battery level and state

- (void) peekAtBatteryState

{

NSArray *stateArray = [NSArray arrayWithObjects:

@"Battery state is unknown",

@"Battery is not plugged into a charging source",

@"Battery is charging",

@"Battery state is full", nil];

NSString *status = [NSString stringWithFormat:

@"Battery state: %@, Battery level: %0.2f%%",

[stateArray objectAtIndex:[UIDevice currentDevice].batteryState],

[UIDevice currentDevice].batteryLevel * 100];

NSLog(@"%@", status);

}

// Show whether proximity is being monitored

- (void) updateTitle

{

self.title = [NSString stringWithFormat:@"Proximity %@",

[UIDevice currentDevice].proximityMonitoringEnabled ? @"On" : @"Off"];

}

// Toggle proximity monitoring off and on

- (void) toggle: (id) sender

{

// Determine the current proximity monitoring and toggle it

BOOL isEnabled = [UIDevice currentDevice].proximityMonitoringEnabled;

[UIDevice currentDevice].proximityMonitoringEnabled = !isEnabled;

[self updateTitle];

}

- (void) loadView

{

[super loadView];

// Enable toggling and initialize title

self.navigationItem.rightBarButtonItem =

BARBUTTON(@"Toggle", @selector(toggle:));

[self updateTitle];

// Add proximity state checker

[[NSNotificationCenter defaultCenter]

addObserverForName:UIDeviceProximityStateDidChangeNotification

object:nil queue:[NSOperationQueue mainQueue]

usingBlock:^(NSNotification *notification) {

// Sensor has triggered either on or off

NSLog(@"The proximity sensor %@",

[UIDevice currentDevice].proximityState ?

@"will now blank the screen" : @"will now restore the screen");

}];

// Enable battery monitoring

[[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];

// Add observers for battery state and level changes

[[NSNotificationCenter defaultCenter]

addObserverForName:UIDeviceBatteryStateDidChangeNotification

object:nil queue:[NSOperationQueue mainQueue]

usingBlock:^(NSNotification *notification) {

// State has changed

NSLog(@"Battery State Change");

[self peekAtBatteryState];

}];

[[NSNotificationCenter defaultCenter]

addObserverForName:UIDeviceBatteryLevelDidChangeNotification

object:nil queue:[NSOperationQueue mainQueue]

usingBlock:^(NSNotification *notification) {

// Level has changed

NSLog(@"Battery Level Change");

[self peekAtBatteryState];

}];

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.3.3 检测Retina支持

近年来,Apple在其旗舰设备上引入了Retina显示屏。根据Apple的说法,它的像素密度非常高,足以使入眼无法区分单独的像素。带有更高分辨率的艺术的应用程序可以利用这种改进的显示质量。

UIScreen类提供了一种容易的方式,用于检查当前设备是否提供了内置的Retina显示屏。检查屏幕的scale属性,它提供了从逻辑坐标空间(磅,大约是1/160英寸)转换为设备坐标空间(像素)的转换因子。对于标准显示屏,转换因子是1.0,因此1点对应于1像素。对于 Retina显示屏,它是2.0(4像素/磅):

- (BOOL) hasRetinaDisplay

{

return ([UIScreen mainScreen].scale == 2.0f);

}

UIScreen类还提供了两个有用的显示屏尺寸属性。bounds返回屏幕的边界矩形,以磅为单位。无论屏幕上有任何元素(比如状态栏、导航栏或标签栏),这都会提供屏幕的完全尺寸。

applicationFrame 属性(同样以磅为单位)把状态栏排除在外,提供了应用程序的初始窗口尺寸的框架。

1.4 秘诀:取回额外的设备信息

sysctl()和 sysctlbyname()允许获取系统信息。这些标准的 UNIX 函数用于询问操作系统有关硬件和OS的详细信息。看一眼Macintosh上的/usr/include/sys/sysctl.h包括文件,就能对所提供的范围类型有一个感宽。在那里,能够找到一份可以用作这些函数的参数常量的详尽列表。

这些常量使你能够检查核心信息,比如系统的 CPU频率、可用的内存量等。秘诀1-2演示了这种功能。它引入了一个UIDevice类,用于收集系统信息,并通过一系列方法调用返回它。

你可能想知道:当标准的 UIDevice 类可以根据需要返回设备型号时,为什么这个类还要包括一个平台方法。答案在于区分不同的单元类型。

iPhone 3GS的型号只是“iPhone”,iPhone 4S也是一样。与之相反,这个秘诀为3GS返回的平台值是“iPhone2,1”,为iPhone 4S返回的是“iPhone 4,1”。这允许以编程方式把3GS单元与第一代iPhone(“iPhone1,1”)或iPhone 3G(“iPhone1,2”)区分开。

每种型号都提供了独特的内置能力。准确知道你正在处理哪款 iPhone 有助于确定那个单元是否有可能支持诸如可访问性、GPS和磁力计之类的特性。

秘诀1-2 扩展设备信息收集

@implementation UIDevice (Hardware)

+ (NSString *) getSysInfoByName:(char *)typeSpecifier

{

// Recover sysctl information by name

size_t size;

sysctlbyname(typeSpecifier, NULL, &size, NULL, 0);

char *answer = malloc(size);

sysctlbyname(typeSpecifier, answer, &size, NULL, 0);

NSString *results = [NSString stringWithCString:answer

encoding: NSUTF8StringEncoding];

free(answer);

return results;

}

- (NSString *) platform

{

return [UIDevice getSysInfoByName:"hw.machine"];

}

- (NSUInteger) getSysInfo: (uint) typeSpecifier

{

size_t size = sizeof(int);

int results;

int mib[2] = {CTL_HW, typeSpecifier};

sysctl(mib, 2, &results, &size, NULL, 0);

return (NSUInteger) results;

}

- (NSUInteger) cpuFrequency

{

return [UIDevice getSysInfo:HW_CPU_FREQ];

}

- (NSUInteger) busFrequency

{

return [UIDevice getSysInfo:HW_BUS_FREQ];

}

- (NSUInteger) totalMemory

{

return [UIDevice getSysInfo:HW_PHYSMEM];

}

- (NSUInteger) userMemory

{

return [UIDevice getSysInfo:HW_USERMEM];

}

- (NSUInteger) maxSocketBufferSize

{

return [UIDevice getSysInfo:KIPC_MAXSOCKBUF];

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.5 秘诀:使用加速能力“向上”定位

iPhone提供了3个机载传感器,用于沿着iPhone的3根相互垂直的轴(左/右(x轴)、上/下(y 轴)和前/后(z 轴))度量加速能力。这些值指示作用于 iPhone 的力,它们来自重力和用户移动。可以通过在脑袋周围晃动iPhone(向心力)或者把它从高楼上投下(自由落体)来获得某种净力反馈。当然,如果不幸摔环了,它也许不能取回这类数据。

要向 iPhone 加速计更新订阅某个对象,可把它设置委托。设置为委托的对象必须实现UIAccelerometerDelegate协议:

[[UIAccelerometer sharedAccelerometer] setDelegate:self]

在指定时,委托将会接收accelerometer:didAccelerate:回调消息,可以跟踪并对其做出响应。发送给委托方法的UIAcceleration结构包含x轴、y轴和z轴的浮点值,每个值的范围为——1.0~1.0:

float x = acceleration.x;

float y = acceleration.y;

float z = acceleration.z;

秘诀1-3使用这些值来帮助确定“向上”的方向。它会计算x和y加速度向量之间的反正切值,返回垂直向上的偏移角度。当接收到新的加速消息时,秘诀将会利用其箭头图片(在图1-1中可以看到它)旋转UIImageView实例,以指向上方。对用户动作的实时响应确保箭头会继续指向上方,而无论用户怎样改变手机的方向。

秘诀1-3 捕获加速事件

- (void)accelerometer:(UIAccelerometer *)accelerometer

didAccelerate:(UIAcceleration *)acceleration

{

// Determine up from the x and y acceleration components

float xx = -acceleration.x;

float yy = acceleration.y;

float angle = atan2(yy, xx);

[arrow setTransform:

CGAffineTransformMakeRotation(angle)];

}

- (void) viewDidLoad

{

// Initialize the delegate to start catching accelerometer events

[UIAccelerometer sharedAccelerometer].delegate = self;

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.6 处理基本的方向

UIDevice类使用内置的orientation属性获取设备的物理方向。iOS设备支持这个属性的7个可能的值。

UIDeviceOrientationUnknown:方向目前未知。

UIDeviceOrientationPortrait:Home键按下。

UIDeviceOrientationPortraitUpsideDown:Home键升起。

UIDeviceOrientationLandscapeLeft:Home键在左边。

UIDeviceOrientationLandscapeRight:Home键在右边。

UIDeviceOrientationFaceUp:设备正面朝上。

UIDeviceOrientationFaceDown:设备正面朝下。

设备在典型的应用程序会话期间可能经历其中任何或所有的方向。尽管创建的方向要与机载加速计保持一致,但是不会以任何方式把这些方向绑定到内置的角度值。

iOS 提供了两个内置宏,帮助确定设备方向枚举值是纵向还是横向的:即 UIDevice-OrientationIsPortrait()和UIDeviceOrientationIsLandscape()。能够很方便地扩展UIDevice类,提供这些测试作为内置的设备属性:

@property (nonatomic, readonly) BOOL isLandscape;

@property (nonatomic, readonly) BOOL isPortrait;

- (BOOL) isLandscape

{

return UIDeviceOrientationIsLandscape(self.orientation);

}

- (BOOL) isPortrait

{

return UIDeviceOrientationIsPortrait(self.orientation);

}

你的代码可以直接订阅设备重定向通知。为此,可以把 beginGeneratingDeviceOrientation Notifications 发送给 currentDevice 单例。然后添加一个观察者,捕获随后发生的 UIDevice-OrientationDidChangeNotification 更新。如你所期望的,可以通过调用 UIDeviceOrientationDid-ChangeNotificationOrientationNotification来完成侦听。

1.7 同步获取当前的加速计角度

有时,你可能想在不用把自己设定为完全委托的情况下查询加速计。下面的方法打算在UIDevice类别内使用,允许与x/y平面(iOS设备的正面)一起同步返回当前的设备角度。为此,可输入一个新的运行循环,等待加速计事件,从那个回调中获取当前的角度,然后让运行循环返回那个角度:

- (void)accelerometer:(UIAccelerometer *)accelerometer

didAccelerate:(UIAcceleration *)acceleration

{

float xx = acceleration.x;

float yy = -acceleration.y;

device_angle = M_PI / 2.0f - atan2(yy, xx);

if (device_angle > M_PI)

device_angle -= 2 * M_PI;

CFRunLoopStop(CFRunLoopGetCurrent());

}

- (float) orientationAngle

{

// Supercede current delegate

id priorDelegate = [UIAccelerometer sharedAccelerometer].delegate;

[UIAccelerometer sharedAccelerometer].delegate = self;

// Wait for a reading

CFRunLoopRun();

// Restore delegate

[UIAccelerometer sharedAccelerometer].delegate = priorDelegate;

return device_angle;

}

这不是用于持续轮询的方法,可为此直接使用回调。但是,对于偶尔的角度查询,这些方法提供了对当前屏幕角度的简单、直接的访问。

1.7.1 通过加速计计算方向

在第一次启动应用程序时,UIDevice类不会报告正确的方向。仅当设备移到一个新位置或者UIViewController方法起作用之后,它才会更新方向。

直到用户把设备移开然后再移回正确的方向之后,才可能会把纵向启动的应用程序视作“纵向”的。在模拟器和 iPhone 设备上存在这种状况,并且很容易测试(由于对特性进行了更新,使之像设计的那样工作,因此关闭了针对这个问题的无线电探测器)。

对于解决办法,可以考虑像刚才所示的那样直接恢复角度方向。然后,在确定了设备的角度之后,从基于加速计的角度转换为设备方向。下面用代码说明了它的工作方式:

// Limited to the four portrait/landscape options

- (UIDeviceOrientation) acceleratorBasedOrientation

{

CGFloat baseAngle = self.orientationAngle;

if ((baseAngle > -M_PI_4) && (baseAngle < M_PI_4))

return UIDeviceOrientationPortrait;

if ((baseAngle < -M_PI_4) && (baseAngle > -3 * M_PI_4))

return UIDeviceOrientationLandscapeLeft;

if ((baseAngle > M_PI_4) && (baseAngle < 3 * M_PI_4))

return UIDeviceOrientationLandscapeRight;

return UIDeviceOrientationPortraitUpsideDown;

}

要知道的是,这个示例只考虑了x-y平面,而大多数用户界面决策都需要在这里做出。这个代码段完全忽略了z轴,这意味着最终将会得到模糊的随机结果:即正面朝上和正面朝下的方向。可以修改这段代码,以根据需要提供这种细微差别。

UIViewController类的interfaceOrientation实例方法报告了视图控制器的界面的方向。尽管这不能代替加速计读数,但是许多界面布局问题都基于底层的视图方向,而不是设备特征。

还要知道的是,尤其是在 iPad 上,子视图控制器使用的布局方向可能不同于设备方向。例如,嵌入式控制器可能在一个横向划分的视图控制器内展示一种纵向布局。即便如此,也可以考虑底层的界面方向是否可以满足方向检测代码。它可能比设备方向更可靠,尤其是在应用程序启动时。

1.7.2 计算相对角度

屏幕重定向支持意味着必须在象限中支持界面对于给定设备角度的关系,其中每个象限都用于一个可能的面向前方的屏幕方向。由于UIViewController可以自动旋转其屏幕上的视图,需要了解一些数学知识以解释那些重定向。

下面的方法(编写它是为了在 UIDevice 类别中使用)可以计算角度,以使得角度与设备方向保持同步。这将在垂直方向上产生简单的偏移,以匹配当前展示GUI的方式:

- (float) orientationAngleRelativeToOrientation:

(UIDeviceOrientation) someOrientation

{

float dOrientation = 0.0f;

switch (someOrientation)

{

case UIDeviceOrientationPortraitUpsideDown:

{dOrientation = M_PI; break;}

case UIDeviceOrientationLandscapeLeft:

{dOrientation = -(M_PI/2.0f); break;}

case UIDeviceOrientationLandscapeRight:

{dOrientation = (M_PI/2.0f); break;}

default: break;

}

float adjustedAngle =

fmod(self.orientationAngle - dOrientation, 2.0f * M_PI);

if (adjustedAngle > (M_PI + 0.01f))

adjustedAngle = (adjustedAngle - 2.0f * M_PI);

return adjustedAngle;

}

这个方法使用一个浮点模数获取实际的屏幕角度与界面方向角度偏移量之间的差值,返回非常重要的垂直角度偏移量。

注意:

在iOS 6中,可以使用Info.plist代替shouldAutorotateToInterfaceOrientation:,以允许和禁止方向改变。

1.8 使用加速度移动屏幕上的对象

借助一点编程工作,iPhone 的机载加速计就可以使对象在屏幕上四处“移动”,实时响应用户倾斜手机的方式。秘诀1-4创建了一只动画式的蝴蝶,用户可以使之快速移过屏幕。

使之工作的秘密在于:向程序中添加一个所谓的“物理计时器”。它不是直接响应加速中的变化,而是像秘诀1-3所做的那样,加速计回调用于测量当前的力。它取决于计时器例程随着时间的推移通过改变它的画面对蝴蝶应用那些力。下面列出了一些要记住的关键点。

■只要力的方向仍然保持相同,蝴蝶就会加速。它的速度会依据加速力在x或y方向上的量度成比例地提高。

■由计时器调用的tick例程将通过向蝴蝶的原点添加速度向量来移动蝴蝶。

■蝴蝶移动的范围是有界限的。因此,当它撞到某个边缘时,将会停止在那个方向上移动。这可以一直把蝴蝶保留在屏幕上。tick方法将会检查界限条件。例如,如果蝴蝶撞到垂直边缘,那它仍然可以在水平方向上移动。

■蝴蝶会改变它自身的方向,使之总是“下落”。可以在tick方法中应用一个简单的旋转变换来实现这一点。在使用变换时,还要关注画面或中心偏移。在应用偏移之前,总是要重置数学处理,然后重新应用任何角度改变。不这样做的话,可能导致画面出入意料地放大、收缩或扭曲。

注意:

计时器在自然状态下不会处理块。如果你愿意使用基于块的设计,可以查询github,找到它的解决办法。

秘诀1-4 基于加速计的反馈移动屏幕上的对象

- (void)accelerometer:(UIAccelerometer *)accelerometer

didAccelerate:(UIAcceleration *)acceleration

{

// Extract the acceleration components

float xx = -acceleration.x;

float yy = acceleration.y;

// Store the most recent angular offset

mostRecentAngle = atan2(yy, xx);

// Has the direction changed?

float accelDirX = SIGN(xvelocity) * -1.0f;

float newDirX = SIGN(xx);

float accelDirY = SIGN(yvelocity) * -1.0f;

float newDirY = SIGN(yy);

// Accelerate.To increase viscosity lower the additive value

if (accelDirX == newDirX) xaccel =

(abs(xaccel) + 0.85f) * SIGN(xaccel);

if (accelDirY == newDirY) yaccel =

(abs(yaccel) + 0.85f) * SIGN(yaccel);

// Apply acceleration changes to the current velocity

xvelocity = -xaccel * xx;

yvelocity = -yaccel * yy;

}

- (void) tick

{

// Reset the transform before changing position

butterfly.transform = CGAffineTransformIdentity;

// Move the butterfly according to the current velocity vector

CGRect rect = CGRectOffset(butterfly.frame, xvelocity, 0.0f);

if (CGRectContainsRect(self.view.bounds, rect))

butterfly.frame = rect;

rect = CGRectOffset(butterfly.frame, 0.0f, yvelocity);

if (CGRectContainsRect(self.view.bounds, rect))

butterfly.frame = rect;

// Rotate the butterfly independently of position

butterfly.transform =

CGAffineTransformMakeRotation(mostRecentAngle + M_PI_2);

}

- (void) initButterfly

{

CGSize size;

// Load the animation cells

NSMutableArray *butterflies = [NSMutableArray array];

for (int i = 1; i <= 17; i++)

{

NSString *fileName = [NSString stringWithFormat:@"bf_%d.png", i];

UIImage *image = [UIImage imageNamed:fileName];

size = image.size;

[butterflies addObject:image];

}

// Begin the animation

butterfly = [[UIImageView alloc]

initWithFrame:(CGRect){.size=size}];

[butterfly setAnimationImages:butterflies];

butterfly.animationDuration = 0.75f;

[butterfly startAnimating];

// Set the butterfly's initial speed and acceleration

xaccel = 2.0f;

yaccel = 2.0f;

xvelocity = 0.0f;

yvelocity = 0.0f;

// Add the butterfly

butterfly.center = RECTCENTER(self.view.bounds);

[self.view addSubview:butterfly];

// Activate the accelerometer

[[UIAccelerometer sharedAccelerometer] setDelegate:self];

// Start the physics timer

[NSTimer scheduledTimerWithTimeInterval: 0.03f

target: self selector: @selector(tick)

userInfo: nil repeats: YES];

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.9 秘诀:基于加速计的滚动视图

好几位读者要求我在本书这一版中包括进一个倾斜滚轮秘诀。倾斜滚轮使用设备的内置加速计来控制在UIScrollView的内容周围的移动。当用户调整设备时,材料会相应地“下落”。它不会把视图定位在屏幕上,而是把内容视图滚动到一个新的偏移位置。

创建这个界面的挑战在于:确定设备在什么地方应该具有它的静止轴(resting axis)。大多数入最初建议当显示屏靠着它的背部时应该是稳定的,并且z轴方向笔直地指向上方。事实证明:这实际上是一种相当糟糕的设计选择。要使用那根轴,就意味着在导航期间屏幕必须实际地偏离观看者。随着设备旋转离开视图,用户将不能完全看到屏幕上所发生的事情,尤其是在固定的位置使用设备时,站在高处查看设备有时也会产生这种效果。

作为替代,秘诀1-5假定稳定的位置是通过z轴指向大约45°的方向,即用户把iPhone或iPad握在手中的自然位置,这处于正面朝上和正面朝前方的中间位置。对秘诀1-5中的数学运算做了相应的调整。从这个歪斜的位置来回倾斜,使屏幕在调整期间保持最大的可见性。

与秘诀1-4相比,这个秘诀中的另一处改变是低得多的加速常量。这使屏幕上的运动能够更慢地发生,让用户更容易降低速度并恢复导航。

秘诀1-5 倾斜滚轮

- (void)accelerometer:(UIAccelerometer *)accelerometer

didAccelerate:(UIAcceleration *)acceleration

{

// extract the acceleration components

float xx = -acceleration.x;

float yy = (acceleration.z + 0.5f) * 2.0f; // between face-up and face-forward

// Has the direction changed?

float accelDirX = SIGN(xvelocity) * -1.0f;

float newDirX = SIGN(xx);

float accelDirY = SIGN(yvelocity) * -1.0f;

float newDirY = SIGN(yy);

// Accelerate.To increase viscosity lower the additive value

if (accelDirX == newDirX) xaccel = (abs(xaccel) + 0.005f) * SIGN(xaccel);

if (accelDirY == newDirY) yaccel = (abs(yaccel) + 0.005f) * SIGN(yaccel);

// Apply acceleration changes to the current velocity

xvelocity = -xaccel * xx;

yvelocity = -yaccel * yy;

}

- (void) tick

{

xoff += xvelocity;

xoff = MIN(xoff, 1.0f);

xoff = MAX(xoff, 0.0f);

yoff += yvelocity;

yoff = MIN(yoff, 1.0f);

yoff = MAX(yoff, 0.0f);

// update the content offset based on the current velocities

CGFloat xsize = sv.contentSize.width - sv.frame.size.width;

CGFloat ysize = sv.contentSize.height - sv.frame.size.height;

sv.contentOffset = CGPointMake(xoff * xsize, yoff * ysize);

}

- (void) viewDidAppear:(BOOL)animated

{

NSString *map = @"http://maps.weather.com/images/\

maps/current/curwx_720x486.jpg";

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

[queue addOperationWithBlock:

^{

// Load the weather data

NSURL *weatherURL = [NSURL URLWithString:map];

NSData *imageData = [NSData dataWithContentsOfURL:weatherURL];

// Update the image on the main thread using the main queue

[[NSOperationQueue mainQueue] addOperationWithBlock:^{

UIImage *weatherImage = [UIImage imageWithData:imageData];

UIImageView *imageView =

[[UIImageView alloc] initWithImage:weatherImage];

CGSize initSize = weatherImage.size;

CGSize destSize = weatherImage.size;

// Ensure that the content size is significantly bigger

// than the screen can show at once

while ((destSize.width < (self.view.frame.size.width * 4)) ||

(destSize.height < (self.view.frame.size.height * 4)))

{

destSize.width += initSize.width;

destSize.height += initSize.height;

}

imageView.userInteractionEnabled = NO;

imageView.frame = (CGRect){.size = destSize};

sv.contentSize = destSize;

[sv addSubview:imageView];

// Activate the accelerometer

[[UIAccelerometer sharedAccelerometer] setDelegate:self];

// Start the physics timer

[NSTimer scheduledTimerWithTimeInterval: 0.03f

target: self selector: @selector(tick)

userInfo: nil repeats: YES];

}];

}];

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.10 秘诀:Core Motion基础

Core Motion框架集中了运动数据处理。该框架是在iOS 4 SDK中引入的,用于取代你刚才阅读到的直接加速计访问。它提供了对3个关键的机载传感器的集中式监测。这些传感器由陀螺仪、磁力计和加速计组成,其中陀螺仪用于测量设备的旋转,磁力计提供了一种测量罗盘方位的方式,加速计用于检测沿着3根轴的重力变化。第四个入口点称为设备移动(device motion),它把全部3种传感器都结合进单个监测系统中。

Core Motion使用来自这些传感器原始值创建可度的测量结果,主要表现为力向量的形式。可测量的项包括以下属性。

设备姿势(attitude):设备相对于某个参照画面的方向。姿势被表示为摇晃、前倾和左右摇摆的角度,它们都以弧度为单位。

旋转速率(rotationRate):设备围绕它的3根轴中的每一根轴旋转的速率。旋转包括x、y和z角速度值,它们以弧度/秒为单位。

重力(gravity):设备当前的加速度向量,由正常的重力场提供。重力的单位是g’s,分别沿着x、y和z轴来测量。每个单位代表由地球提供的标准重力加速度(9.8米/秒 2)。

用户加速度(userAcceleration):用户提供的加速度向量。像重力一样,用户加速度的单位也是g’s,分别沿着x、y和z轴来测量。当把它们加到一起时,用户向量和重力向量代表给设备提供的总加速度。

磁场(magneticField):代表设备邻近区域里的总磁场的向量。磁场是沿着x、y和z轴以微特斯拉(microtesla)为单位测量的。还提供了校准精度,通知应用程序有关磁场测量的质量。

1.10.1 测试传感器

正如在本章前面所学到的,可以使用应用程序的Info.plist文件要求使用或排除机载传感器。也可以在应用程序内测试每种可能的Core Motion支持:

if (motionManager.gyroAvailable)

[motionManager startGyroUpdates];

if (motionManager.magnetometerAvailable)

[motionManager startMagnetometerUpdates];

if (motionManager.accelerometerAvailable)

[motionManager startAccelerometerUpdates];

if (motionManager.deviceMotionAvailable)

[motionManager startDeviceMotionUpdates];

开始更新不会产生像使用 UIAccelerometer 时遇到的委托回调机制。作为替代,你将负责轮询每个值,或者可以使用基于块的更新机制,执行在每次更新时提供的一个块(例如,startAccelerometerUpdatesToQueue:withHandler:)。

1.10.2 处理程序块

{

秘诀1-6修改了秘诀1-4,以便使用Core Motion。把加速度回调移入一个处理程序块中,并从数据的加速度属性中读取x和y值。否则,代码将保持不变。在这里,将看到Core Motion的一些基本的方面:创建一个新的运动管理器,它用于测试加速计可用性。然后,它将使用一个新的操作队列开始更新,该队列在应用程序运行期间将持续存在。

establishMotionManager 和 shutDownMotionManager方法使应用程序能够根据需要启动和关闭运动管理器。这些方法是在应用程序变成恬动时和挂起时从应用程序委托内调用的:

- (void) applicationWillResignActive:(UIApplication *)application

{

[tbvc shutDownMotionManager];

}

- (void) applicationDidBecomeActive:(UIApplication *)application

{

[tbvc establishMotionManager];

}

这些方法提供了一种干净的方式,用于关闭和恢复运动服务,以响应当前的应用程序状态。

秘诀1-6 基本的Core Motion

@implementation TestBedViewController

- (void) tick

butterfly.transform = CGAffineTransformIdentity;

// Move the butterfly according to the current velocity vector

CGRect rect = CGRectOffset(butterfly.frame, xvelocity, 0.0f);

if (CGRectContainsRect(self.view.bounds, rect))

butterfly.frame = rect;

rect = CGRectOffset(butterfly.frame, 0.0f, yvelocity);

if (CGRectContainsRect(self.view.bounds, rect))

butterfly.frame = rect;

butterfly.transform =

CGAffineTransformMakeRotation(mostRecentAngle + M_PI_2);

}

- (void) shutDownMotionManager

{

NSLog(@"Shutting down motion manager");

[motionManager stopAccelerometerUpdates];

motionManager = nil;

[timer invalidate];

timer = nil;

}

- (void) establishMotionManager

{

if (motionManager)

[self shutDownMotionManager];

NSLog(@"Establishing motion manager");

// Establish the motion manager

motionManager = [[CMMotionManager alloc] init];

if (motionManager.accelerometerAvailable)

[motionManager

startAccelerometerUpdatesToQueue:

[[NSOperationQueue alloc] init]

withHandler:^(CMAccelerometerData *data, NSError *error)

{

// Extract the acceleration components

float xx = -data.acceleration.x;

float yy = data.acceleration.y;

mostRecentAngle = atan2(yy, xx);

// Has the direction changed?

float accelDirX = SIGN(xvelocity) * -1.0f;

float newDirX = SIGN(xx);

float accelDirY = SIGN(yvelocity) * -1.0f;

float newDirY = SIGN(yy);

// Accelerate.To increase viscosity,

// lower the additive value

if (accelDirX == newDirX)

xaccel = (abs(xaccel) + 0.85f) * SIGN(xaccel);

if (accelDirY == newDirY)

yaccel = (abs(yaccel) + 0.85f) * SIGN(yaccel);

// Apply acceleration changes to the current velocity

xvelocity = -xaccel * xx;

yvelocity = -yaccel * yy;

}];

// Start the physics timer

timer = [NSTimer scheduledTimerWithTimeInterval: 0.03f

target: self selector: @selector(tick)

userInfo: nil repeats: YES];

}

- (void) initButterfly

{

CGSize size;

// Load the animation cells

NSMutableArray *butterflies = [NSMutableArray array];

for (int i = 1; i <= 17; i++)

{

NSString *fileName =

[NSString stringWithFormat:@"bf_%d.png", i];

UIImage *image = [UIImage imageNamed:fileName];

size = image.size;

[butterflies addObject:image];

}

// Begin the animation

butterfly = [[UIImageView alloc]

initWithFrame:(CGRect){.size=size}];

[butterfly setAnimationImages:butterflies];

butterfly.animationDuration = 0.75f;

[butterfly startAnimating];

// Set the butterfly's initial speed and acceleration

xaccel = 2.0f;

yaccel = 2.0f;

xvelocity = 0.0f;

yvelocity = 0.0f;

// Add the butterfly

butterfly.center = RECTCENTER(self.view.bounds);

[self.view addSubview:butterfly];

}

- (void) loadView

{

[super loadView];

self.view.backgroundColor = [UIColor whiteColor];

[self initButterfly];

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.11 秘诀:获取和使用设备姿势

设想有一部 iPad 放在桌子上。iPad 上显示了一幅图像,可以弯曲并查看它。现在,设想旋转那个 iPad,就像它平放在桌子上一样,但是当 iPad 移动时,图像不会移动。它保持与周围的世界完美对齐。无论怎样旋转 iPad,图像都不会随着视图更新而“移动”,以平衡物理运动。这就是秘诀1-7的工作方式,利用设备的机载陀螺仪(这是必需的),使这个秘诀工作。

无论怎样握持设备,图像都会调整。除了这种水平操作,还可以拾起设备并在空间中定位它的方向。如果在手中翻转设备并查看它,就会看到图像的颠倒的“底部”。还可以沿着两根轴倾斜它:一根是从 Home 按钮指向照相机,另一根则从照相机与 Home 按钮的中点开始穿过 iPad的表面。还有一根轴,它是你最先探讨过的,从设备的中间开始,指向设备上方的空间,并且穿过它下面的中点。在操纵设备时,图像会做出响应,在那个iPad内创建一个可视化的静态世界。

秘诀1-7显示了如何利用少量简单的几何变换来执行该操作。它建立了一个运动管理器,订阅设备运动更新,然后基于运动管理器返回的摇晃、前倾和左右摇摆的角度应用图像变换。

秘诀1-7 使用设备运动更新修正空间里的图像

- (void) shutDownMotionManager

{

NSLog(@"Shutting down motion manager");

[motionManager stopDeviceMotionUpdates];

motionManager = nil;

}

- (void) establishMotionManager

{

if (motionManager)

[self shutDownMotionManager];

NSLog(@"Establishing motion manager");

// Establish the motion manager

motionManager = [[CMMotionManager alloc] init];

if (motionManager.deviceMotionAvailable)

[motionManager

startDeviceMotionUpdatesToQueue:

[NSOperationQueue currentQueue]

withHandler: ^(CMDeviceMotion *motion, NSError *error) {

CATransform3D transform;

transform = CATransform3DMakeRotation(

motion.attitude.pitch, 1, 0, 0);

transform = CATransform3DRotate(transform,

motion.attitude.roll, 0, 1, 0);

transform = CATransform3DRotate(transform,

motion.attitude.yaw, 0, 0, 1);

imageView.layer.transform = transform;

}];

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.12 使用运动事件检测晃动

当iPhone 检测到一个运动事件时,它会把该事件传递给当前的第一个响应者,即响应者链中的主对象。响应者是可以处理事件的对象,所有的视图和窗口都是响应者,因此也是应用程序对象。

响应者链提供了一种对象层次结构,所有的对象都可以响应事件。当朝向链开始处的对象接收到一个事件时,不会进一步传递那个事件。对象会处理它。如果它不能处理,可以把该事件转移给下一个响应者。

对象通常可以通过把它们自己声明为第一个响应者来获得这种身份,这是通过 become FirstResponder实现的。在这个代码段中,UIViewController确保它会变成第一个响应者,只要它的视图出现在屏幕上即可。一旦消失,它将放弃第一个响应者的身份:

- (BOOL)canBecomeFirstResponder {

return YES;

}

// Become first responder whenever the view appears

- (void)viewDidAppear:(BOOL)animated {

[super viewDidAppear:animated];

[self becomeFirstResponder];

}

// Resign first responder whenever the view disappears

- (void)viewWillDisappear:(BOOL)animated {

[super viewWillDisappear:animated];

[self resignFirstResponder];

}

第一个响应者将接收所有的触摸和运动事件。运动回调反映了UIView触摸回调阶段。回调方法如下。

motionBegan:withEvent::这个回调指示运动事件的开始。在编写本书时,只能识别一类运动事件:晃动。将来可能不是这样,因此你可能想在代码中检查运动类型。

motionEnded:withEvent::在第一个响应者在运动事件结束时接收这个回调。

motionCancelled:withEvent::与触摸一样,可以通过打入的电话和其他系统事件取消运动。Apple建议在代码中实现全部3个运动事件回调(类似地,还要实现全部4个触摸事件回调)。

下面的代码段显示了一对运动回调的示例。如果在设备上测试它们,可以注意到几件事。第一,从用户的角度看,开始和结束事件几乎是同时发生的,为这两类事件播放声音有些小题大做。第二,它偏向于进行从一侧到另一侧的晃动检测,与前后和上下晃动相比,iPhone更擅长检测从一侧到另一侧的晃动。最后,Apple的运动实现使用了一种轻微锁定的方法。直到生成了另一个运动事件或者在处理了前一个运动事件之后,才能生成一个新的运动事件。Shake to Shuffle和Shake to Undo事件使用了相同的锁定机制:

- (void)motionBegan:(UIEventSubtype)motion

withEvent:(UIEvent *)event {

// Play a sound whenever a shake motion starts

if (motion != UIEventSubtypeMotionShake) return;

[self playSound:startSound];

}

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event

{

// Play a sound whenever a shake motion ends

if (motion != UIEventSubtypeMotionShake) return;

[self playSound:endSound];

}

1.13 使用外部屏幕

可以用许多方式使用外部屏幕。例如,采取最新款的iPad。第二代和第三代型号提供了内置的屏幕监测。连接VGA或HDMI电缆,就可以把内容显示在外部显示器和内置屏幕上。某些设备允许使用AirPlay(Apple的专有无线缆空中下载视频解决方案)把屏幕以无线方式镜像到Apple TV。这些镜像特性极其方便,但是并不仅限于在 iOS 中简单地把一个屏幕上的内容复制到另一个屏幕上。

UIScreen类允许独立地检测并写到外部屏幕上。可以把任何连接的显示器视作一个新窗口,并为该显示器创建内容,使之独立于主设备屏幕上显示的任何视图。可以为任何有线屏幕执行该操作,并且从iPad 2(及更高型号)和iPhone 4S(及更高型号)开始,可以使用AirPlay to Apple TV 2(及更高型号)以无线方式执行该操作。名为Reflector的第三方应用程序允许使用AirPlay把显示器镜像到Mac或Windows计算机。

几何学很重要。为什么呢?iOS设备目前包括320像素×480像素的老式iPhone显示器、640像素×960像素的Retina显示器单元和1024像素×768像素的iPad。典型的复合/分量输出是在720像素×480像素(480i和480p)、1024像素×768像素和1280像素×720像素(720p)下的VGA上产生的,然后还有更高质量的HDMI输出可用。

除此之外,还有过扫描的问题及其他目标显示器的局限性,并且Video Out会迅速变成一个几何挑战。幸运的是,Apple利用一些方便的现实修改来响应这种挑战。无需尝试在输出屏幕与内置的设备屏幕之间创建一种一对一的关系,而可以基于输出显示器的可用属性构建内容。只需创建一个窗口,填充它,并显示它。

如果打算开发Video Out应用程序,不要想当然地认为用户会严格地使用AirPlay。许多用户仍然使用老式的电缆接头连接到显示器和投影仪。确保每种电缆至少具有一根(复合、分量、VGA和HDMI),还要具有准备好使用AirPlay的iPhone和iPad,以便可以彻底地测试每种输出配置。第三方电缆(通常是从远东进口的,没有打上Made for iPhone/iPad的标签)将不会工作,因此要确保购买具有Apple品牌的物品。

1.13.1 检测屏幕

UIScreen类可以报告连接了多少个屏幕。你知道无论何时这个计数大于1,都会连接有外部屏幕。screens数组中的第一项总是主设备屏幕:

#define SCREEN_CONNECTED ([UIScreen screens].count > 1)

每个屏幕都可以报告它的边界(即它的物理尺寸,以磅为单位)以及它的屏幕比例(将磅与像素关联起来)。两个标准的通知使你能够观察屏幕何时连接到设备以及与设备断开。

// Register for connect/disconnect notifications

[[NSNotificationCenter defaultCenter]

addObserver:self selector:@selector(screenDidConnect:)

name:UIScreenDidConnectNotification object:nil];

[[NSNotificationCenter defaultCenter]

addObserver:self selector:@selector(screenDidDisconnect:)

name:UIScreenDidDisconnectNotification object:nil];

连接意指任意类型的连接,无论是通过电缆还是通过 AirPlay。无论何时接收到这种类型的更新,都一定要统计屏幕数量,并调整用户界面,以匹配新的情况。

你的职责是:无论何时加入新屏幕,都要建立一些窗口,一旦发生分离事件,就要清除它们。每个屏幕都应该具有它自己的窗口,为输出显示器管理内容。对于分离的屏幕,不要抓住它们的窗口不放。释放它们,然后当新屏幕出现时重新创建它们。

注意:

在screens数组中不会表示出镜像的屏幕,而代之以等镜像存储在主屏幕的mirroredScreen属性中。当禁用、未连接或者设备的能力简直不支持镜像时,这个属性为空。

创建一个新屏幕并把它用于独立的外部显示器总会撤销镜像。因此,即使用户启用了镜像,当应用程序开始写到并创建外部显示器,它等具有优先级。

1.13.2 获取屏幕分辨率

每个屏幕都提供了一个availableModes属性。这是一个分辨率对象的数组,其中的元素按从最低分辨率到最高分辨率排序。每种模式都有一个size属性,指示一个目标像素大小的分辨率。许多屏幕都支持多种模式。例如,VGA显示器可能具有多达6种模式,或者具有比它提供的更多不同的分辨率。支持的分辨率数量因硬件而异。总是至少有一种分辨率可用,但是当具有多种分辨率时,应该给用户提供选择的机会。

1.13.3 设置Video Out

在从[UIScreens screens]数组中获取了外部屏幕对象之后,可以查询可用的模式并选择要使用的尺寸。一般说来,可以放弃选择列表中的最后一种模式,总是使用尽可能高的分辨率,或者放弃使用最低分辨率的第一种模式。

要开始一个Video Out(视频输出)流,可以创建一个新的UIWindow,并将其尺寸调整为所选的模式。给那个窗口添加一个新视图,以便在其上绘图。然后把窗口分配给外部屏幕,并使之成为关键的和可见的窗口。这命令窗口显示出来,并准备好使用它。之后,再次使原始窗口成为关键窗口。这允许用户继续与主屏幕交互。不要跳过这一步。对于最终用户来说,什么也不会比发现他们昂贵的设备不再响应他们的触摸更令他们暴躁:

self.outputWindow = [[UIWindow alloc] initWithFrame:theFrame];

outputWindow.screen = secondaryScreen;

[outputWindow makeKeyAndVisible];

[delegate.view.window makeKeyAndVisible];

1.13.4 添加显示器链接

显示器链接是一种计时器,以便把绘图与显示器的刷新率进行同步。可以通过更改显示器链接的frameInterval属性,来调整这个画面刷新时间。该属性默认为1,更高的数字会降低刷新率。如果把它设置为2,则会把画面刷新率减半。当屏幕连接到设备时,就会创建显示器链接。UIScreen类实现了一个方法,用于返回它的屏幕的显示器链接对象。可以为显示器链接指定一个目标以及要调用的选择器。

显示器链接定期触发,以让你知道何时更新Video Out屏幕。如果CPU负载较小,可以把时间间隔调长一些,但是这将会获得较低的画面刷新率。这是一个重要的折衷,尤其是对于在设备端需要具有高级CPU响应的直接操作界面更是如此。

秘诀1-8中的代码为运行循环使用了常见的模式,提供最少的等待时间。在处理显示器链接时,如果使之无效(invalidate),就将它从运行循环中删除。

1.13.5 过扫描补偿

UIScreen类允许通过给overscanCompensation属性赋值,补偿显示器屏幕边缘的像素损失。在Apple的文档中描述了可以指派的技术,但是它们基本上相当于你是想剪辑内容还是想用空格填充它。

1.13.6 VIDEOkit

秘诀1-8介绍了 VIDEOkit,它是一个基本的外部屏幕客户。该秘诀演示了配置和使用有线与无线外部屏幕所需的所有特性。通过调用startupWithDelegate:来建立屏幕监测。把主视图控制器传递给它,该控制器的职责将是创建外部内容。

内部的init方法开始侦听屏幕连接和分离事件,并根据需要构建和删除窗口。每次显示器链接触发时,都会调用一个非正式的委托方法(updateExternalView:)。它将传递外部窗口上的视图,委托可以根据需要在该窗口上绘图。

在这个秘诀的配套示例代码中,视图控制器委托存储一个本地颜色值,并使用它给外部显示器着色:

- (void) updateExternalView: (UIImageView *) aView

{

aView.backgroundColor = color;

}

- (void) action: (id) sender

{

color = [UIColor randomColor];

}

每次按下动作按钮时,视图控制器都会生成一种新颜色。当VIDEOkit查询控制器以更新外部视图时,将把该颜色设置为背景色。可以看到外部屏幕即时更新为新的随机颜色。

注意:

Reflector Ap(p 单一许可的费用是15美元,5台计算机的许可费用是50美元,reflectorapp.com)为AirPlay提供了一个优秀的调试伙伴,它提供了一种可以在Mac和Windows计算机上工作的无线/无Apple TV的解决方案。它模拟Apple TV AirPlay接收器,允许从iOS直接广播到桌面,并记录该榆出。

秘诀1-8 VIDEOkit

@interface VIDEOkit : NSObject

{

UIImageView *baseView;

}

@property (nonatomic, weak) UIViewController *delegate;

@property (nonatomic, strong) UIWindow *outputWindow;

@property (nonatomic, strong) CADisplayLink *displayLink;

+ (void) startupWithDelegate: (id) aDelegate;

@end

@implementation VIDEOkit

static VIDEOkit *sharedInstance = nil;

- (void) setupExternalScreen

{

// Check for missing screen

if (!SCREEN_CONNECTED) return;

// Set up external screen

UIScreen *secondaryScreen = [UIScreen screens][1];

UIScreenMode *screenMode =

[[secondaryScreen availableModes] lastObject];

CGRect rect = (CGRect){.size = screenMode.size};

NSLog(@"Extscreen size: %@", NSStringFromCGSize(rect.size));

// Create new outputWindow

self.outputWindow = [[UIWindow alloc] initWithFrame:CGRectZero];

_outputWindow.screen = secondaryScreen;

_outputWindow.screen.currentMode = screenMode;

[_outputWindow makeKeyAndVisible];

_outputWindow.frame = rect;

// Add base video view to outputWindow

baseView = [[UIImageView alloc] initWithFrame:rect];

baseView.backgroundColor = [UIColor darkGrayColor];

[_outputWindow addSubview:baseView];

// Restore primacy of main window

[_delegate.view.window makeKeyAndVisible];

}

- (void) updateScreen

{

// Abort if the screen has been disconnected

if (!SCREEN_CONNECTED && _outputWindow)

self.outputWindow = nil;

// (Re)initialize if there's no output window

if (SCREEN_CONNECTED && !_outputWindow)

[self setupExternalScreen];

// Abort if encountered some weird error

if (!self.outputWindow) return;

// Go ahead and update

SAFE_PERFORM_WITH_ARG(_delegate,

@selector(updateExternalView:), baseView);

}

- (void) screenDidConnect: (NSNotification *) notification

{

NSLog(@"Screen connected");

UIScreen *screen = [[UIScreen screens] lastObject];

if (_displayLink)

{

[_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop]

forMode:NSRunLoopCommonModes];

[_displayLink invalidate];

_displayLink = nil;

}

self.displayLink = [screen displayLinkWithTarget:self

selector:@selector(updateScreen)];

[_displayLink addToRunLoop:[NSRunLoop currentRunLoop]

forMode:NSRunLoopCommonModes];

}

- (void) screenDidDisconnect: (NSNotification *) notification

{

NSLog(@"Screen disconnected.");

if (_displayLink)

{

[_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop]

forMode:NSRunLoopCommonModes];

[_displayLink invalidate];

self.displayLink = nil;

}

}

- (id) init

{

if (!(self = [super init])) return self;

// Handle output window creation

if (SCREEN_CONNECTED)

[self screenDidConnect:nil];

// Register for connect/disconnect notifications

[[NSNotificationCenter defaultCenter]

addObserver:self selector:@selector(screenDidConnect:)

name:UIScreenDidConnectNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self

selector:@selector(screenDidDisconnect:)

name:UIScreenDidDisconnectNotification object:nil];

return self;

}

- (void) dealloc

{

[self screenDidDisconnect:nil];

self.outputWindow = nil;

}

+ (VIDEOkit *) sharedInstance

{

if (!sharedInstance)

sharedInstance = [[self alloc] init];

return sharedInstance;

}

+ (void) startupWithDelegate: (id) aDelegate

{

[[self sharedInstance] setDelegate:aDelegate];

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第1章的文件夹。

1.14 跟踪用户

跟踪是开发者的一种不幸的现实生恬。Apple不赞成使用UIDevice属性,该属性提供了绑定到设备硬件的唯一标识符。Apple 利用两个标识符属性取代 UIDevice 属性。它使用identifierForAdvertising 属性返回当前设备所独有的一个特定于设备的字符串,并使用identifierForVendor 属性提供一个绑定到每位应用程序供应商的字符串。无论使用的是哪个应用程序,这都应该会返回相同的唯一字符串,它不是顾客id。不同设备上的相同应用程序可以返回不同的字符串,就像应用程序可以来自不同的供应商一样。

这些标识符是使用新的NSUUID类构建的。可以在跟踪场景之外使用这个类,创建保证全球唯一的UUID字符串。Apple写道:“UUID(Universally Unique Identifier,通用唯一标识符),也称为GUID(Globally Unique Identifier,全局唯一标识符)或IID(Interface Identifier,接口标识符),是 128位的值。UUID 在空间和时间上都是唯一的,这是由于它结合了两个值,第一个值是生成它的计算机上所特有的,第二个值代表从1582年10月15日00:00:00起所经过的100纳秒数。”

UUID类方法可以根据需要生成一个新的RFC 4122v4 UUID。使用[NSUUID UUID]返回一个新实例(附带的好处是:它全都是大写的)。从此,可以获取 UUIDString 表示,或者通过getUUIDBytes:直接请求字节。

1.15 还有一件事:检查可用的磁盘空间

NSFileManager类可让你确定iPhone上有多少空闲空间,以及设备上总共提供了多少空间。程序清单1-1演示了如何检查这些值,并且使用友好的、逗号格式化的字符串显示结果。返回的值表示空闲空间(以字节为单位)。

程序清单1-1 获取文件系统大小和文件系统空闲大小

- (NSString *) commaFormattedStringWithLongLong: (long long) num

{

// Produce a properly formatted number string

// Alternatively use NSNumberFormatter

if (num < 1000)

return [NSString stringWithFormat:@"%d", num];

return   [[self commasForNumber:num/1000]

stringByAppendingFormat:@",%03d", (num % 1000)];

}

- (void) action: (UIBarButtonItem *) bbi

{

NSFileManager *fm = [NSFileManager defaultManager];

NSDictionary *fattributes =

[fm fileSystemAttributesAtPath:NSHomeDirectory()];

NSLog(@"System space: %@",

[self commaFormattedStringWithLongLong:[[fattributes

objectForKey:NSFileSystemSize] longLongValue]]);

NSLog(@"System free space: %@",

[self commasForNumber:[[fattributes

objectForKey:NSFileSystemFreeSize] longLongValue]]);

}

1.16 小结

本章介绍了一些与 iPhone 设备交互的核心方式。你看到了如何获取设备信息,检查电池状态,以及订阅接近事件。你学习了如何将iPod Touch与iPhone和iPad区分开,以及确定正在使用的是哪种型号。你了解了加速计,并通过几个示例看到了它的应用,从简单的“向上”定位到更复杂的晃动检测算法。你进入到Core Motion中,并且学习了如何创建更新块,实时响应设备事件。最后,你看到了如何给应用程序添加外部屏幕支持。下面对你刚才遇到的秘诀列出了几条总结性考虑。

■iPhone的加速计提供了一种新颖的方式,用于补充其基于触摸的界面。使用加速度数据把用户交互扩展到“触摸此处”基本操作之外,并且引入了知道倾斜的反馈。

■低级调用可能是 App Store 友好的。它们不依赖于可能基于当前的固件版本而改变的Apple API。UNIX系统调用似乎令入畏缩不前,但是其中许多都受到iOS设备家族完全支持。

■记住设备的限制。你可能希望在执行文件密集型的工作之前检查空闲的磁盘空间,以及在全速运行CPU之前检查电池的充电状态。

■深入研究Core Motion。它提供的实时设备反馈是把iOS设备集成进现实体验中的基础。

■既然AirPlay打破了外部显示器的束缚,就可以为比你以前所想的多得多的令入兴奋的项目使用Video Out。AirPlay和外部视频屏幕意味着可以把iOS设备转变成游戏和实用程序的远程控制装置,这样就可以在大屏幕上显示并在小屏幕上控制它们。

■在提交给iTunes之前,使用Info.plist文件确定哪些设备能力是必需的。iTunes使用这个必需能力的列表确定是否可以把某个应用程序下载到给定的设备上,并在该设备上正确地运行。

第2章 文档和数据共享

iOS下,应用程序可以使用多种系统特性共享信息和数据,以及把控制权从一个应用程序转移给另一个应用程序。每个应用程序都可以访问公共的系统粘贴板,允许跨应用程序进行复制和粘贴。用户可以把文档从一个应用程序传输给另一个支持该格式的应用程序。它们可以请求把系统提供的许多“动作”应用于文档,比如打印、发微博,或者贴到 Facebook上。应用程序可以声明自定义的URL模式,并把它们嵌入在文本和Web页面中。本章介绍可以在应用程序之间集成文档和进行数据共享的方式。你将看到如何把这些特性添加到自己的应用程序中,并且聪明地使用它们,使应用程序成为iOS系统中具有协作精神的一员。

2.1 秘诀:处理统一类型标识符

统一类型标识符(Uniform Type Identifier,UTI)代表iOS信息共享的中心组件。可以把它们视作下一代MIME类型。UTI是标识资源类型(比如图像和文本)的字符串,它们指定哪些类型的信息将用于公共数据对象。它们不需要为此依赖于老式的指示符,比如文件扩展名、MIME类型,或者文件类型的元数据(比如OSType)。UTI利用更新、更灵恬的技术替代了这些项目。

UTI使用反向域名风格的命名约定。常见的源于Apple的标识符看起来如下:public.html和public.jpeg,它们分别指HTML源文本和JPEG图像,二者都是专用的文件信息类型。

继承对于UTI起着重要作用。UTI使用类似于OO的继承系统,其中子UTI具有对父UTI的“is-a”(是一个)关系。子UTI会继承它们的父UTI的所有属性,但是会添加它们表示的数据种类的更多特征。这是由于每个UTI都可以根据需要承担更一般或者更特定的角色。例如,以JPEG UTI为例。JPEG图像(public.jpeg)是一幅图像(public.image),而图像反过来又是一种数据(public.data),数据反过来又是一种用户可查看(或者可收听)的内容(public.content),它是一种项目(public.item),即用于 UTI 的普通的基本类型。这种层次结构被称为顺应性,其中子UTI顺应父UTI。例如,更具体的jpeg UTI顺应更一般的图像或数据UTI。

图2-1显示了Apple的基本顺应性树的一部分。这棵树上位于较低位置的任何项目都必须顺应其所有父数据属性。声明一个父UTI意味着支持它的所有子UTI。因此,可以打开public.data的应用程序必须能够服务于文本、电影、图像文件等。

UTI 支持多重继承。一个项目可以顺应多个父 UTI 。因此,你可能设想一种同时提供文本和图像容器的数据类型,它声明了对二者的顺应性。

没有用于UTI项目的中心注册表,尽管每个UTI都应该遵守约定。public域是为特定于iOS的类型预留的,是大多数应用程序所共有的。Apple生成了公共项目的一个完整的家族层次结构。可以使用标准的预留域命名方式添加任何特定于第三方公司的名称(例如,com.sadun.myCustomType和com.apple.quicktime-movie)。

2.1.1 通过文件扩展名确定UTI

Mobile Core Services框架提供了一些实用程序,允许基于文件扩展名获取UTI信息。在使用这些基于C语言的函数时,一定要包括头文件,并把应用程序连接到框架。当给下面的函数传递一个路径扩展字符串时,它将返回一个首选的UTI。首选的标识符是单个UTI字符串:

#import <MobileCoreServices/MobileCoreServices.h>

NSString *preferredUTIForExtension(NSString *ext)

{

// Request the UTI via the file extension

NSString *theUTI = (__bridge_transfer NSString *)

UTTypeCreatePreferredIdentifierForTag(

kUTTagClassFilenameExtension,

(__bridge CFStringRef) ext, NULL);

return theUTI;

}

可以使用kUTTagClassMIMEType作为第一个参数,给UTTypeCreatePreferredIdentifierForTag()传递一种MIME类型代替文件扩展名。该函数将为给定的MIME类型返回首选的UTI:

NSString *preferredUTIForMIMEType(NSString *mime)

{

// Request the UTI via the file extension

NSString *theUTI = (__bridge_transfer NSString *)

UTTypeCreatePreferredIdentifierForTag(

kUTTagClassMIMEType,

(__bridge CFStringRef) mime, NULL);

return theUTI;

}

结合使用这些函数,将允许你从文件扩展名和MIME类型转向用于现代文件访问UTI类型。

2.1.2 从UTI转向扩展名或MIME类型

也可以另辟蹊径,从UTI产生首选的扩展名或MIME类型,这要使用 UTTypeCopy PreferredTagWithClass()。当给下面的函数传递public.jpeg时,它们将分别返回jpeg和image/jpeg:

NSString *extensionForUTI(NSString *aUTI)

{

CFStringRef theUTI = (__bridge CFStringRef) aUTI;

CFStringRef results =

UTTypeCopyPreferredTagWithClass(

theUTI,kUTTagClassFilenameExtension);

return (__bridge_transfer NSString *)results;

}

NSString *mimeTypeForUTI(NSString *aUTI)

{

CFStringRef theUTI = (__bridge CFStringRef) aUTI;

CFStringRef results =

UTTypeCopyPreferredTagWithClass(

theUTI, kUTTagClassMIMEType);

return (__bridge_transfer NSString *)results;

}

必须在叶子层使用这些函数,这意味着直接在该层级声明类型扩展名。扩展名声明在属性列表中,其中将描述像文件扩展名和默认图标这样的特性。因此,例如,给扩展名函数传递public.text或public.movie将返回nil,而public.plain-text和public.mpeg则将分别返回扩展名txt和mpg。

前面的项目将在顺应性树中处于较高的位置,从而提供一种抽象类型,而不是特定的实现。对于目前为应用程序定义的给定类,没有现成的API函数用于寻找从它传承下来的项目。你可能想在bugreport.apple.com上归档一个增强请求。诚然,所有的扩展名和MIME类型都会注册在某个位置(否则,UTTypeCopyPreferredTagWithClass()将怎样从一开始就执行向上查找的工作),因此把扩展名映射到更一般的UTI的能力应该是可能存在的。

1.MIME助手

尽管从扩展名到UTI的服务是很详尽的,可以为几乎任何扩展名返回UTI,但是从UTI到MIME 的结果则很随意,有些漫无目标。通常可以为任何公共项目生成正确的 MIME 表示;公共程度较低的项目则很少见。

下面的代码行显示了各种各样的扩展名、它们的UTI(通过首选的UTIForExtension()获取),以及从每个UTI生成的MIME类型(通过mimeTypeForUTI())。可以看到,其中有相当多的空白。当这些函数不能找到匹配时,它们将返回nil:

xlv: dyn.age81u5d0 / (null)

xlw: com.microsoft.excel.xlw / application/vnd.ms-excel

xm: dyn.age81u5k / (null)

xml: public.xml / application/xml

z: public.z-archive / application/x-compress

zip: public.zip-archive / application/zip

zoo: dyn.age81y55t / (null)

zsh: public.zsh-script / (null)

为了解决这个问题,用于这个秘诀的示例代码包括了一个额外的MIMEHelper类。它定义了一个函数,返回所提供的扩展名的MIME类型:

NSString *mimeForExtension(NSString *extension);

它的扩展名和MIME类型源于Apache Software Foundation,它将其列表放入公共域中。对于用于这个秘诀的示例代码中的450个扩展名,iOS返回了全部450个UTI,但是只会返回88个MIME类型。Apache列表把这个数字增加到230个可识别的MIME类型。

2.1.3 测试顺应性

使用UTTypeConformsTo()函数测试顺应性。该函数接受两个参数:一个源UTI和一个要比较的UTI,如果第一个UTI顺应第二个UTI,就返回True。可以使用这个函数来测试一个更具体的项目是否顺应一个更一般的项目。相等性测试则使用 UTTypeEqual()。下面显示了一个示例,说明了可能如何使用顺应性测试,确定文件路径是否可能指向图像资源:

BOOL pathPointsToLikelyUTIMatch(NSString *path, CFStringRef theUTI)

{

NSString *extension = path.pathExtension;

NSString *preferredUTI = preferredUTIForExtension(extension);

return (UTTypeConformsTo(

(__bridge CFStringRef) preferredUTI, theUTI));

}

BOOL pathPointsToLikelyImage(NSString *path)

{

return pathPointsToLikelyUTIMatch(path, CFSTR("public.image"));

}

BOOL pathPointsToLikelyAudio(NSString *path)

{

return pathPointsToLikelyUTIMatch(path, CFSTR("public.audio"));

}

2.1.4 获取顺应性列表

UTTypeCopyDeclaration()是iOS API中的所有UTI函数中最一般(并且最有用)的函数,它返回包含以下键的字典。

kUTTypeIdentifierKey:UTI名称,它将被传递给函数(例如,public.mpeg)。

kUTTypeConformsToKey:类型顺应的任何父项目(例如,public.mpeg 顺应public.movie)。

kUTTypeDescriptionKey:正在考虑的类型(如果存在的话)的现实描述(例如,“MPEG movie”)。

kUTTypeTagSpecificationKey:给定UTI的等价OSType(例如,MPG和MPEG)、文件扩展名(mpg、mpeg、mpe、m75和m15)和MIME类型(视频/mpeg、视频/mpg、视频/x-mpeg和视频/x-mpg)的字典。

除了这些公共项目之外,还会遇到更多的键,它们指定了导入和导出的UTI描述(kUTImportedTypeDeclarationsKey和kUTExportedTypeDeclarationsKey)、与UTI相关联的图标资源(kUTTypeIconFileKey)、指向描述类型的页面的URL(kUTTypeReferenceURLKey),以及为UTI提供版本字符串的版本键(kUTTypeVersionKey)。

使用返回的字典向上通过顺应性树来构建一个数组,表示给定UTI顺应的所有项目。例如,public.mpeg类型顺应 public.movie、public.audiovisual-content、public.data、public.item和public.content。通过conformanceArray函数将这些项目返回为一个数组,如秘诀2-1所示。

秘诀2-1 测试顺应性

// Build a declaration dictionary for the given type

NSDictionary *utiDictionary(NSString *aUTI)

{

NSDictionary *dictionary =

(__bridge_transfer NSDictionary *)

UTTypeCopyDeclaration((__bridge CFStringRef) aUTI);

return dictionary;

}

// Return an array where each member is guaranteed unique

// but that preserves the original ordering wherever possible

NSArray *uniqueArray(NSArray *anArray)

{

NSMutableArray *copiedArray =

[NSMutableArray arrayWithArray:anArray];

for (id object in anArray)

{

[copiedArray removeObjectIdenticalTo:object];

[copiedArray addObject:object];

}

return copiedArray;

}

// Return an array representing all UTIs that a given UTI conforms to

NSArray *conformanceArray(NSString *aUTI)

{

NSMutableArray *results =

[NSMutableArray arrayWithObject:aUTI];

NSDictionary *dictionary = utiDictionary(aUTI);

id conforms = [dictionary objectForKey:

(__bridge NSString *)kUTTypeConformsToKey];

// No conformance

if (!conforms) return results;

// Single conformance

if ([conforms isKindOfClass:[NSString class]])

{

[results addObjectsFromArray:conformanceArray(conforms)];

return uniqueArray(results);

}

// Iterate through multiple conformance

if ([conforms isKindOfClass:[NSArray class]])

{

for (NSString *eachUTI in (NSArray *) conforms)

[results addObjectsFromArray:conformanceArray(eachUTI)];

return uniqueArray(results);

}

// Just return the one-item array

return results;

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.2 秘诀:访问系统粘贴板

粘贴板(在某些系统上也称为剪贴板)提供了一种核心OS特性,用于跨应用程序共享数据。用户可以在一个应用程序中把数据复制到粘贴板上,切换任务,然后把该数据复制到另一个应用程序中。剪切/复制/粘贴特性类似于在大多数操作系统中的那些特性。当用户在文本框或视图之间切换时,也可以在单个应用程序内执行复制和粘贴;开发入员也可以为特定于应用程序的数据建立私入粘贴板,它们将不会被其他应用程序所使用。

UIPasteboard类允许访问共享的设备粘贴板及其内容。下面这个代码段返回一般的系统粘贴板,它适合于大多数一般的复制/粘贴应用:

UIPasteboard *pb = [UIPasteboard generalPasteboard];

除了一般的共享式系统粘贴板之外,iOS还提供了特定于应用程序的粘贴板,以更好地确保数据隐私,它不会扩展到应用程序之外,并且自定义名称的粘贴板可以跨应用程序使用,但是仅限于那些知道并且使用粘贴板名称键的应用程序。使用pasteboardWithUniqueName创建特定于应用程序的粘贴板,它返回一个应用程序粘贴板对象,该对象将在应用程序退出前持续存在。

使用pasteboardWithName:create:创建自定义的粘贴板,它返回一个具有指定名称的粘贴板。为粘贴板使用反向DNS命名方式(例如,com.sadun.shared-application-pasteboard)。如果粘贴板还不存在,创建参数将指定系统是否应该创建它。这种类型的粘贴板可以超越单个应用程序的运行而持续存在;可以在创建后把持久属性设置为 YES。可以使用 removePasteboardWithName:销毁粘贴板,并释放被它使用的资源。

2.2.1 存储数据

粘贴板一次可以存储一个或多个条目。每个条目都具有一种关联的类型,可以使用UTI指定存储的是哪种类型的数据。例如,你可能发现 public.text(更确切地讲是 public.utf8-plain-text)存储文本数据,public.url用于URL地址,public.jpeg则用于图像数据。除此之外,iOS上还使用了许多其他的公共数据类型。存储类型的数据的字典被称为item,可以通过粘贴板的items属性获取所有可用项目的数组。

可以确定当前利用简单的消息存储的项目的种类。可以给粘贴板发送pasteboardTypes消息,查询粘贴板的可用类型。这将返回当前存储在粘贴板上的类型的数组:

NSArray *types = [pb pasteboardTypes];

可以在粘贴板上设置数据,并通过传递一个 NSData 对象和一个描述数据所顺应的类型的UTI,来关联一种类型。此外,对于属性列表对象(即字符串、日期、数组、字典、数字或URL),可以通过setValue:forPasteboardType:设置一个NSValue。这些属性列表对象在内部的存储方式稍微不同于它们的原始数据版本,从而导致了方法上的差异:

[[UIPasteboard generalPasteboard]

setData:theData forPasteboardType:theUTI];

2.2.2 存储公共类型

粘贴板可以进一步专用于几种数据类型,它们代表最常用的粘贴板项目。它们是颜色(不是一个属性列表“值”对象)、图像(也不是一个属性列表“值”对象)、字符串和URL。UIPasteboard类提供了专用的获取器和设置器,使得更容易处理这些项目。可以把其中每个项目都视作粘贴板的属性,因此可以使用点表示法设置和获取它们。更重要的是,每个属性都具有一种复数形式,允许把这些项目作为对象的数组来访问它们。

粘贴板属性极大地简化了在最常用的情况下使用系统粘贴板。属性访问器包括如下一些。

string:设置或获取粘贴板上的第一个字符串。

strings:设置或获取粘贴板上的所有字符串的数组。

image:设置或获取粘贴板上的第一幅图像。

images:设置或获取粘贴板上的所有图像的数组。

URL:设置或获取粘贴板上的第一个URL。

URLs:设置或获取粘贴板上的所有URL的数组。

color:设置或获取粘贴板上的第一种颜色。

colors:设置或获取粘贴板上的所有颜色的数组。

2.2.3 获取数据

当使用4个特殊类之一时,只需使用关联的属性从粘贴板中获取数据即可,否则,可以使用dataForPasteboardType:方法取出数据。该方法返回其类型与作为参数发送的UTI匹配的第一个项目中的数据。粘贴板中任何其他的匹配项目都将被忽略。

如果需要获取所有匹配的数据,可以取回 itemSetWithPasteboardTypes:,然后遍历集合,以获取每个字典。可以从单个字典键中取回每个项目的数据类型,以及从其值中取回数据。

如前所述,UIPasteboard提供了两种方法,用于粘贴到粘贴板上,这依赖于要粘贴的信息是一个属性列表对象还是原始的数据。对于属性列表对象(包括字符串、日期、数字、字典、数组和 URL),可以使用 setValueForPasteboardType:方法;对于一般的数据,则使用 setData:for-PasteboardType:方法。

当粘贴板改变时,它们将发出一个 UIPasteboardChangedNotification,可以通过默认的NSNotificationCenter观察者侦听它。也可以监视自定义的粘贴板,并通过UIPasteboardRemoved-Notification侦听它们的删除操作。

注意:

如果你想成功地把文本数据粘贴到Notes或Mail中,可以在把信息存储到粘贴板上时使用public.utf8-plain-text作为所选的UTI。使用string或strings属性可以自动增强这个UTI。

2.2.4 被动更新粘贴板

坦率地讲,iOS的选择和复制界面并不是操作系统的最高效的元素。有时,你希望为用户简化操作,同时又准备好打算与其他应用程序共享的内容。

考虑秘诀2-2。它允许用户使用文本视图输入和编辑文本,同时自动执行更新粘贴板的过程。当观察者处于恬动状态时(通过简单地点按按钮来切换),每一次编辑都会使文本更新粘贴板。这是通过实现一个文本视图委托方法(textViewDidChange:)来完成的,该方法通过自动把更改赋予粘贴板来响应编辑(updatePasteboard)。

这个秘诀演示了访问和更新粘贴板中涉及的相对简单性。

秘诀2-2 创建自动将文本输入到粘贴板中的解决方案

- (void) updatePasteboard

{

// Copy the text to the pasteboard when the watcher is enabled

if (enableWatcher)

[UIPasteboard generalPasteboard].string = textView.text;

}

- (void) textViewDidChange: (UITextView *) textView

{

// Delegate method calls for an update

[self updatePasteboard];

}

- (void) toggle: (UIBarButtonItem *) bbi

{

// switch between standard and auto-copy modes

enableWatcher = !enableWatcher;

bbi.title = enableWatcher ? @"Stop Watching" : @"Watch";

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.3 秘诀:监测Documents文件夹

iOS文档并没有受困在它们的沙盒中,你可以并且应该与用户共享它们。应该允许用户直接控制他们的文档,以及访问他们可能在设备上创建的任何资料。一个简单的 Info.plist 设置将使iTunes能够显示用户的Documents文件夹的内容,并使那些用户能够根据需要添加和删除资料。

在将来某个时间,你可能使用一个简单的NSMetadataQuery监测器来监视Documents文件夹并报告更新。在编写本书时,元数据监视还没有扩展到iCloud之外以用于其他的文件夹。从OS X导出的代码无法像期望的那样在iOS上工作。目前,准确地讲,有两个搜索域可供iOS使用:即普遍存在的数据范围和普遍存在的文档范围(即iCloud和iCloud)。

直到iOS中出现了一般的功能之后,才能使用kqueue。这种老式技术提供了可伸缩的事件通知。利用 kqueue,可以监测添加和清除事件。这粗略地等同于寻找要添加和删除的文件,它们是你想做出反应的主要更新类型。秘诀2-3展示了一个用于监视Documents文件夹的kqueue实现。

2.3.1 支持文档文件共享

要支持文件共享,可以向应用程序的Info.plist中添加一个UIFileSharingEnabled键,并把它的值设置为YES,如图2-2所示。在处理非原始的键和值时,这个项目被称为支持iTunes文件共享的Application。iTunes将在每个设备的Apps选项卡中列出所有声明文件共享支持的应用程序,如图2-3所示。

2.3.2 用户控制

不能指定在Documents文件夹中允许存放哪些类型的项目。用户可以添加他们喜欢的任何项目,以及删除他们希望删除的任何项目。不过,他们不能做的是使用iTunes界面导航子文件夹。注意图2-3中的Inbox文件夹,这是一个从应用程序之间的文档共享中遗留下来的工件,但它不应该出现在那里。用户不能直接管理数据,不应该把子文件夹留在那里以使他们混淆。

用户在iTunes中不能像删除其他文件和文件夹那样删除Inbox,应用程序应该也不能直接把文件写到Inbox中。尊重Inbox的角色,它用于捕获从其他应用程序传入的任何数据。在实现文件共享支持时,总是要检查Inbox以恢复恬动状态,并且处理该数据以清空Inbox,以及无论何时应用程序启动和恢复运行时都要删除它。在本章后面将讨论处理传入的文档的最佳实践。

2.3.3 Xcode访问

作为一位开发入员,你不仅能够访问Documents文件夹,而且能够访问整个应用程序沙盒。使用Xcode Organizer (Command-2) > Devices选项卡>“设备”> Applications >“应用程序名称”可以浏览沙盒,以及从中上传和下载文件。

通过启用应用程序的UIFileSharingEnabled属性,可以测试基本的文件共享,以及把数据加载到Documents文件夹中。在创建了那些文件之后,可以使用Xcode和iTunes检查、下载和删除它们。

2.3.4 扫描新文档

秘诀2-3通过在其beginGeneratingDocumentNotificationsInPath:方法中请求kqueue通知来工作。在这里,它获取一个用于你所提供的路径(在这里是Documents文件夹)的文件描述符,并请求用于添加和清除事件的通知。它将把这个功能添加到当前的运行循环中,无论何时监测的文件夹更新,都会启用通知。

一旦接收到那个回调,它将发布一条通知(我自定义的kDocumentChanged,在kqueueFired方法中),并且继承监视新事件。在主线程上的主运行循环中都会运行它,因此一旦接收到通知,GUI就可以响应并更新它自身。

下面的代码段演示了如何使用秘诀2-3的监视器来更新GUI中的文件列表。无论何时内容改变了,更新通知都允许应用程序刷新那些目录内容清单:

- (void) scanDocuments

{

NSString *path = [NSHomeDirectory()

stringByAppendingPathComponent:@"Documents"];

items = [[NSFileManager defaultManager]

contentsOfDirectoryAtPath:path error:nil];

[self.tableView reloadData];

}

- (void) loadView

{

[self.tableView registerClass:[UITableViewCell class]

forCellReuseIdentifier:@"cell"];

[self scanDocuments];

// React to content changes

[[NSNotificationCenter defaultCenter]

addObserverForName:kDocumentChanged

object:nil queue:[NSOperationQueue mainQueue]

usingBlock:^(NSNotification *notification){

[self scanDocuments];

}];

// Start the watcher

NSString *path = [NSHomeDirectory()

stringByAppendingPathComponent:@"Documents"];

helper = [DocWatchHelper watcherForPath:path];

}

把设备连接到iTunes,测试这个秘诀。使用iTunes App选项卡界面添加和删除项目。设备的机载文件列表将会更新,以实时反映那些改变。

在使用这个秘诀时,要知道一些警告。首先,对于较大的文档,在收到了创建它们的通知之后,不应该立即阅读它们。你可能希望调查文件大小,以确定何时应该停止写入数据。第二,iTunes File Sharing在必要时可以暂缓传输,要相应地进行编码。

秘诀2-3 使用kqueue文件监测器

#import <fcntl.h>

#import <sys/event.h>

#define kDocumentChanged \

@"DocumentsFolderContentsDidChangeNotification"

@interface DocWatchHelper : NSObject

{

CFFileDescriptorRef kqref;

CFRunLoopSourceRef rls;

}

@property (strong) NSString *path;

+ (id) watcherForPath: (NSString *) aPath;

@end

@implementation DocWatchHelper

@synthesize path;

- (void)kqueueFired

{

int       kq;

struct kevent  event;

struct timespec timeout = { 0, 0 };

int       eventCount;

kq = CFFileDescriptorGetNativeDescriptor(self->kqref);

assert(kq >= 0);

eventCount = kevent(kq, NULL, 0, &event, 1, &timeout);

assert( (eventCount >= 0) && (eventCount < 2) );

if (eventCount == 1)

[[NSNotificationCenter defaultCenter]

postNotificationName:kDocumentChanged

object:self];

CFFileDescriptorEnableCallBacks(self->kqref,

kCFFileDescriptorReadCallBack);

}

static void KQCallback(CFFileDescriptorRef kqRef,

CFOptionFlags callBackTypes, void *info)

{

DocWatchHelper *helper =

(DocWatchHelper *)(__bridge id)(CFTypeRef) info;

[helper kqueueFired];

}

- (void) beginGeneratingDocumentNotificationsInPath:

(NSString *) docPath

{

int           dirFD;

int           kq;

int           retVal;

struct kevent      eventToAdd;

CFFileDescriptorContext context =

{ 0, (void *)(__bridge CFTypeRef) self,

NULL, NULL, NULL };

dirFD = open([docPath fileSystemRepresentation], O_EVTONLY);

assert(dirFD >= 0);

kq = kqueue();

assert(kq >= 0);

eventToAdd.ident = dirFD;

eventToAdd.filter = EVFILT_VNODE;

eventToAdd.flags = EV_ADD | EV_CLEAR;

eventToAdd.fflags = NOTE_WRITE;

eventToAdd.data = 0;

eventToAdd.udata = NULL;

retVal = kevent(kq, &eventToAdd, 1, NULL, 0, NULL);

assert(retVal == 0);

self->kqref = CFFileDescriptorCreate(NULL, kq,

true, KQCallback, &context);

rls = CFFileDescriptorCreateRunLoopSource(

NULL, self->kqref, 0);

assert(rls != NULL);

CFRunLoopAddSource(CFRunLoopGetCurrent(), rls,

kCFRunLoopDefaultMode);

CFRelease(rls);

CFFileDescriptorEnableCallBacks(self->kqref,

kCFFileDescriptorReadCallBack);

}

- (void) dealloc

{

self.path = nil;

CFRunLoopRemoveSource(CFRunLoopGetCurrent(), rls,

kCFRunLoopDefaultMode);

CFFileDescriptorDisableCallBacks(self->kqref,

kCFFileDescriptorReadCallBack);

}

+ (id) watcherForPath: (NSString *) aPath

{

DocWatchHelper *watcher = [[self alloc] init];

watcher.path = aPath;

[watcher beginGeneratingDocumentNotificationsInPath:aPath];

return watcher;

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.4 秘诀:展示活动视图控制器

iOS 6新引入的恬动视图控制器可以把数据恬动集成进图2-4所示的界面中。你只需付出最少的开发成本,这种新型控制器就允许你的用户把项目复制到粘贴板上,发布给社交媒体,以及通过电子邮件和文本共享它们等。内置的恬动包括:Facebook(脸书)、Twitter(推特)、Weibo (微博)、SMS、邮件、打印、复制到粘贴板以及把数据分配给联系入。应用程序也可以定义它们自己的自定义服务,在本节后面可以了解这一点:

■UIActivityTypePostToFacebook

■UIActivityTypePostToTwitter

■UIActivityTypePostToWeibo

■UIActivityTypeMessage

■UIActivityTypeMail

■UIActivityTypePrint

■UIActivityTypeCopyToPasteboard

■UIActivityTypeAssignToContact

这个列表中遗漏了两个重要的恬动,即Open in和QuickLook,前者用于在应用程序之间共享文档,后者用于预览文件。本章后面将讨论这种功能,并且利用一些秘诀说明了如何独立地支持这些特性,对于QuickLook来说,则是如何集成进恬动视图控制器中。

注意:

无需拥有一台AirPrint打印机以测试打印活动。Ecamm的Printopia(http://www.ecamm.com/mac/printopia/,19.95美元)可以在你的本地网络上创建一台虚拟打印机,可以从设备和模拟器中使用它。可以选择打印到任何本地打印机、打印到Mac上的文或者打印到Dropbox上的文件。对于利用新的活动视图控制器从事的任何开发工作,这都是一笔极好的投资。Netputing制造了一款名为handyPrint的类似产品(http://www.netputing.com/handyprint),handyPrint接受PayPal捐赠。

2.4.1 展示活动视图控制器

展示控制器的方式因设备而异,可以在 iPhone 家族成员上以模态方式以及在平板电脑上以弹出窗口(popover)显示它。UIBarButtonSystemItemAction 图标提供了用于填充链接到这个控制器的栏按钮的完美方式。

最重要的是,你自己几乎不需要做任何工作。在用户选择一种恬动之后,控制器将会处理所有进一步的交互,比如展示邮件或Twitter创作单,向机载库中添加图片,或者把它分配给联系入。

2.4.2 活动项目源

秘诀2-4通过代码创建并展示了视图控制器。这种实现使它的主类采用UIActivityItemSource协议,并把自身添加到传递给控制器的项目数组中。在获取数据项目时,采用源协议有助于控制器理解如何使用回调。这表现了创建和展示控制器的两种方式中的第一种。

协议的两个必需的方法提供项目以进行处理(将用于恬动的数据),并给该项目提供一个占位符。项目对应于适合于给定恬动类型的对象。可以基于传递给回调的恬动的类型,来区分返回的对象。例如,你可能发推特说:“我在应用程序名称中创建了一首优秀的歌曲”,但是你可能通过电子邮件发送实际的声音文件。

用于项目的占位符通常是返回的与项目相同的数据,除非具有必须处理或创建的对象。在这种情况下,可以创建一个不带有真实数据的占位符对象。

两个回调(项目和占位符)都在主线程上运行,因此要保持数据比较小。如果需要大量的处理数据,可以考虑代之以使用提供者。

2.4.3 项目提供者

UIActivityItemProvider类允许延迟传递数据。在共享数据前,这种操作可以给你提供操纵数据的灵恬性。例如,在可以把大型视频文件上传到社交共享站点或者从较大的序列中对某个音频进行二次抽样之前,可能需要先对它们进行处理。

可以子类化提供者类并实现item方法。它可以代替通常用于操作的main方法。生成处理过的数据,在知道它将异步运行并且不会阻碍用户的交互式体验的情况下它将是安全的。

2.4.4 项目源回调

回调方法允许基于彼此之间的预期用法来区分数据。使用恬动类型(比如Facebook或者Add to Contacts,在本节前面列出了它们),选择你想提供的准确数据。在为不同的应用选择分辨率时,这特别重要。在打印时,要保持数据具有较高的质量。在发推特时,低分辨率的图像也可能达到想要的结果。

如果数据是不变的,也就是说,传送给 Facebook 的数据与传送给电子邮件的相同,就可以直接提供数据项的数组(通常是字符串、图像和URL)。例如,可以像下面这样创建控制器,它使用单独一幅图像:

UIActivityViewController *activity = [[UIActivityViewController alloc]

initWithActivityItems:@[imageView.image]

applicationActivities:nil];

这种直接的方法要简单得多。主类不需要声明项目源协议;不需要实现额外的方法。它是一种为简单项目管理恬动的快速、容易的方式。

也不仅限于传递单个项目,可以根据需要在恬动项目数组中包括额外的元素。下面的控制器可能把它的两幅图像添加到电子邮件中,或者把它们都保存到系统的相机胶卷中,这依赖于用户的选择。拓宽恬动以使用多个项目将允许用户更高效地使用你的应用程序:

UIImage *secondImage = [UIImage imageNamed:@"Default.png"];

UIActivityViewController *activity = [[UIActivityViewController alloc]

initWithActivityItems:@[imageView.image, secondImage]

applicationActivities:nil];

秘诀2-4 活动视图控制器

- (void) presentViewController:

(UIViewController *)viewControllerToPresent

{

if (IS_IPHONE)

{

[self presentViewController:viewControllerToPresent

animated:YES completion:nil];

}

else

{

popover = [[UIPopoverController alloc]

initWithContentViewController:viewControllerToPresent];

popover.delegate = self;

[popover presentPopoverFromBarButtonItem:

self.navigationItem.leftBarButtonItem

permittedArrowDirections:UIPopoverArrowDirectionAny

animated:YES];

}

}

// Return the item to process

- (id)activityViewController:

(UIActivityViewController *)activityViewController

itemForActivityType:(NSString *)activityType

{

return imageView.image;

}

// Return a thumbnail version of that item

- (id)activityViewControllerPlaceholderItem:

(UIActivityViewController *)activityViewController

{

return imageView.image;

}

// Create and present the view controller

- (void) action

{

UIActivityViewController *activity =

[[UIActivityViewController alloc]

initWithActivityItems:@[self] applicationActivities:nil];

[self presentViewController:activity];

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.4.5 添加服务

每个应用程序都可以通过子类化UIActivity类并展示一个自定义的视图控制器,来提供特定于应用程序的服务。视图控制器使用户能够以某种方式处理传递的数据。程序清单2-1介绍了一种最基本的恬动,用于展示一个简单的文本视图。该视图列出了通过恬动控制器传递给它的项目,它显示了每个项目的类和描述。

这个程序清单包括两个独特的类的详细信息。第一个类实现一个简单的文本控制器,并且打算在导航层次结构内使用。它包括一个视图和一个处理程序,前者用于展示文本,后者用于在用户点按Done时通过发送activityDidFinish:来更新UIActivity调用实例。

添加一种方式以使恬动完成是重要的,尤其是当控制器没有自然的终点时。在你的动作把数据上传到 FTP 服务器时,你就知道它何时完成。如果它发出消息,你就知道状态何时发布。在这个示例中,它取决于用户确定这个恬动何时完成。确保视图控制器包含一个指回这个恬动的弱属性,以便在工作结束时发送确实完成方法。

恬动类包含许多必需的和可选的项目。应该实现这个程序清单中显示的所有方法。支持自定义的恬动的方法包括以下一些。

activityType:返回一个描述恬动类型的独特字符串。在系统提供的恬动中,与这个字符串对应的恬动之一是 UIActivityTypePostToFacebook。使用类似的命名模式。这个字符串用于确定特定的恬动类型以及它将做什么。在这个程序清单中,我返回的是“@"CustomActivityTypeListItemsAndTypes"”,它描述了恬动。

activityTitle:提供你想在恬动控制器中显示的文本。图2-5中的自定义文本就是由这个方法返回的。在描述自定义的动作时可以使用恬动的描述。可以遵照Apple的指导,例如,“Save to Camera Roll”(保存到相机胶卷)、“Print”(打印)、“Copy”(复制)。你的标题应该完成短语“I want to...”(我想要……),例如,“I want to print”(我想要打印)、“I want to copy”(我想要复制),或者在这个示例中,“I want to list items”(我想要列出项目)。使用标题大小写形式,并且除了像“to”和“and”这样的次要单词之外,还要大写每个单词的首字母。

activityImage:返回一幅图像让控制器使用。控制器将添加一个反斜杠,并把图像转换成一幅单值位图(one-value bitmap),然后把它分层放置在顶部。在透明背景上使用简单的艺术作品(在iPhone上是57像素×57像素,在iPad上是72像素×72像素,对于Retina屏幕比例则要加倍),构建图标图像的内容。你将希望在插入艺术作品时至少距离每一边15%,以留出空间插入控制器提供的圆角矩形,给图像加上框架。

canPerformWithActivityItems:扫描传递的项目,并且决定控制器是否可以处理它们。如果是,就返回YES。

prepareWithActivityItems:存储传递的项目以便以后使用(在这里,把它们分配给一个局部实例变量),并且执行任何必要的预处理。

activityViewController:使用以前传递给你的恬动项目,返回一个完全初始化的、像样的视图控制器。这个控制器将被自动展示给用户,她在那里执行承诺的动作之前可以自定义选项。

添加自定义的动作允许应用程序扩展其数据处理可能性,同时把一些特性集成进一致的系统提供的界面中。它是一种强大的 iOS 特性。最强大的恬动选择将与系统服务相集成(比如复制到粘贴板,或者保存到相册),或者提供对脱离设备的API的连接,比如Facebook、Twitter、Dropbox和FTP。

这个示例只是简单地列出项目,代表一种弱用例。没有理由不能把相同的特性提供为正常的应用程序中的屏幕。在考虑“动作”时,要尝试延伸到应用程序之外。把用户的数据与扩展到正常的GUI之外的共享与处理特性连接起来。

程序清单2-1 应用程序活动

// All activities present a view controller.This custom controller

// provides a full-sized text view.

@interface TextViewController : UIViewController

@property (nonatomic, readonly) UITextView *textView;

@property (nonatomic, weak) UIActivity *activity;

@end

@implementation TextViewController

// Make sure you provide a done handler of some kind, such as this

// or an integrated button that finishes and wraps up

- (void) done

{

[_activity activityDidFinish:YES];

}

// Just a super-basic text view controller

- (id) init

{

if (!(self = [super init])) return nil;

_textView = [[UITextView alloc] init];

_textView.font = [UIFont fontWithName:@"Futura" size:16.0f];

_textView.editable = NO;

[self.view addSubview:_textView];

PREPCONSTRAINTS(_textView);

STRETCH_VIEW(self.view, _textView);

// Prepare a Done button

self.navigationItem.rightBarButtonItem =

BARBUTTON(@"Done", @selector(done));

return self;

}

@end

@interface MyActivity : UIActivity

@end

@implementation MyActivity

{

NSArray *items;

}

// A unique type name

- (NSString *)activityType

{

return @"CustomActivityTypeListItemsAndTypes";

}

// The title listed on the controller

- (NSString *) activityTitle

{

return @"Cookbook";

}

// A custom image, displayed as a bitmap over a textured background

// This one says "iOS" in a rounded rect edge

- (UIImage *) activityImage

{

CGRect rect = CGRectMake(0.0f, 0.0f, 75.0f, 75.0f);

UIGraphicsBeginImageContext(rect.size);

rect = CGRectInset(rect, 15.0f, 15.0f);

UIBezierPath *path = [UIBezierPath

bezierPathWithRoundedRect:rect cornerRadius:4.0f];

[path stroke];

rect = CGRectInset(rect, 0.0f, 10.0f);

[@"iOS" drawInRect:rect

withFont:[UIFont fontWithName:@"Futura" size:18.0f]

lineBreakMode:NSLineBreakByWordWrapping

alignment:NSTextAlignmentCenter];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

return image;

}

// Specify if you can respond to these items

- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems

{

return YES;

}

// Store the items locally for later use

- (void)prepareWithActivityItems:(NSArray *)activityItems

{

items = activityItems;

}

// Return a view controller, in this case one that lists

// its items and their classes

- (UIViewController *) activityViewController

{

TextViewController *tvc = [[TextViewController alloc] init];

tvc.activity = self;

UITextView *textView = tvc.textView;

NSMutableString *string = [NSMutableString string];

for (id item in items)

[string appendFormat:

@"%@: %@\n", [item class], [item description]];

textView.text = string;

// Make sure to provide some kind of done: handler in

// your main controller.

UINavigationController *nav = [[UINavigationController alloc]

initWithRootViewController:tvc];

return nav;

}

@end

2.4.6 项目和服务

为每个项目展示的服务因传递的数据种类而异。表2-1列出了由源数据类型提供的恬动。如同在本章中所看到的,预览控制器支持扩展到了这些基础类型之外。

■iOS的Quick Look(快速查看)框架把恬动控制器集成到它的文件预览中。Quick Look提供的恬动控制器可以打印并通过电子邮件发送许多类型的文档。一些文档类型也支持其他的恬动。

■文档交互控制器(Document Interaction Controller)提供了“open in”特性,允许在应用程序之间共享文件。它将把恬动添加到它的“选项”样式表示中,并把恬动与“open in”选择结合起来。

2.4.7 支持HTML电子邮件

如果你想使用电子邮件恬动发送HTML,就要确保项目的文本字符串开始于“@"<html>"”。只要实现项目源协议,并且依据用户所选的恬动返回合适的项目,就可以把基于 HTML 的电子邮件文本与普通的Twitter内容区分开。

2.4.8 排除活动

可以通过给excludedActivityTypes属性提供一份恬动类型的列表,明确地排除一些恬动:

UIActivityViewController *activity = [[UIActivityViewController alloc]

initWithActivityItems:items applicationActivities:@[appActivity]];

activity.excludedActivityTypes = @[UIActivityTypeMail];

2.5 秘诀:Quick Look预览控制器

Quick Look预览控制器类允许用户预览许多文档类型。这个控制器支持文本、图像、PDF、RTF、iWork文件、Microsoft Office文档(Office 97及更高版本,包括doc、ppt、xls等)和逗号分隔的值(comma-separated value,csv)文件。你提供一种受支持的文件类型,Quick Look控制器将为用户显示它。集成的系统提供的恬动视图控制器有助于共享预览的文档,如图 2-6所示。

可以推送或展示预览控制器。控制器能够适应这两种情形,与导航栈和模态表示协同工作。秘诀2-5演示了两种方法。

2.5.1 实现Quick Look

Quick Look支持需要几个简单的步骤。

(1) 在主控制器类中声明QLPreviewControllerDataSource协议。

(2) 实现numberOfPreviewItemsInPreviewController:和previewController:previewItemAtIndex:数据源方法。其中第一个方法返回要预览的项目计数;第二个方法则返回索引所引用的预览项目。

(3) 预览项目必须遵守QLPreviewItem协议,该协议包含两个必需的属性:预览标题和项目URL。秘诀2-5创建了一个符合要求的QuickItem类,该类实现了一个绝对最低限度的方法,用于支持数据源。

在满足了所有这些要求之后,代码将准备好创建一个新的预览控制器,设置它的数据源,然后展示或推送它。

秘诀2-5 Quick Look

@interface QuickItem : NSObject <QLPreviewItem>

@property (nonatomic, strong) NSString *path;

@property (readonly) NSString *previewItemTitle;

@property (readonly) NSURL *previewItemURL;

@end

@implementation QuickItem

// Title for preview item

- (NSString *) previewItemTitle

{

return [_path lastPathComponent];

}

// URL for preview item

- (NSURL *) previewItemURL

{

return [NSURL fileURLWithPath:_path];

}

@end

#define FILE_PATH [NSHomeDirectory() \

stringByAppendingPathComponent:@"Documents/PDFSample.pdf"]

@interface TestBedViewController : UIViewController

<QLPreviewControllerDataSource>

@end

@implementation TestBedViewController

- (NSInteger) numberOfPreviewItemsInPreviewController:

(QLPreviewController *) controller

{

return 1;

}

- (id <QLPreviewItem>) previewController:

(QLPreviewController *) controller

previewItemAtIndex: (NSInteger) index;

{

QuickItem *item = [[QuickItem alloc] init];

item.path = FILE_PATH;

return item;

}

// Push onto navigation stack

- (void) push

{

QLPreviewController *controller =

[[QLPreviewController alloc] init];

controller.dataSource = self;

[self.navigationController

pushViewController:controller animated:YES];

}

// Use modal presentation

- (void) present

{

QLPreviewController *controller =

[[QLPreviewController alloc] init];

controller.dataSource = self;

[self presentViewController:controller

animated:YES completion:nil];

}

- (void) loadView

{

self.view.backgroundColor = [UIColor whiteColor];

self.navigationItem.rightBarButtonItem =

BARBUTTON(@"Push", @selector(push));

self.navigationItem.leftBarButtonItem =

BARBUTTON(@"Present", @selector(present));

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.6 秘诀:添加QuickLook动作

值得注意的是,QuickLook不存在于系统提供的恬动视图控制器所展示的标准动作集中。你可以轻松地添加一个自定义的动作以提供这种特性,从而提供与秘诀2-5创建的相同预览。这就是秘诀2-6所做的工作,它把秘诀2-5的功能包装进一个自定义的QLActivity类中。

秘诀2-6将对符合要求的项目执行比你在本章中已经见过的更彻底的搜索。它将搜索传递给它的项目数组,直至找到一个本地文件 URL,它可以将其用于文档预览。如果它没有找到这样一个URL,就会从canPerformWithActivityItems:方法返回NO,并且不会列出在恬动控制器上。

秘诀2-6 Quick Look

@implementation QLActivity

{

NSArray *items;

NSArray *qlitems;

QLPreviewController *controller;

}

// Activity Customization

- (NSString *)activityType

{

return @"CustomQuickLookActivity";

}

- (NSString *) activityTitle

{

return @"QuickLook";

}

- (UIImage *) activityImage

{

return [UIImage imageNamed:@"QL.png"];

}

// Items must include at least one file URL

- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems

{

for (NSObject *item in activityItems)

if ([item isKindOfClass:[NSURL class]])

{

NSURL *url = (NSURL *)item;

if (url.isFileURL) return YES;

}

return NO;

}

// QuickLook callbacks

- (NSInteger) numberOfPreviewItemsInPreviewController:

(QLPreviewController *) controller

{

return qlitems.count;

}

- (id <QLPreviewItem>) previewController: (QLPreviewController *)

controller previewItemAtIndex: (NSInteger) index;

{

return qlitems[index];

}

// Item preparation

- (void)prepareWithActivityItems:(NSArray *)activityItems

{

items = activityItems;

controller = [[QLPreviewController alloc] init];

controller.dataSource = self;

controller.delegate = self;

NSMutableArray *finalArray = [NSMutableArray array];

for (NSObject *item in items)

{

if ([item isKindOfClass:[NSURL class]])

{

NSURL *url = (NSURL *)item;

if (url.isFileURL)

{

QuickItem *item = [[QuickItem alloc] init];

item.path = url.path;

[finalArray addObject:item];

}

}

}

qlitems = finalArray;

}

- (void) previewControllerDidDismiss:

(QLPreviewController *)controller

{

[self activityDidFinish:YES];

}

- (UIViewController *) activityViewController

{

return controller;

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.7 秘诀:使用文档交互控制器

UIDocumentInteractionController类允许应用程序给用户展示一个选项菜单,允许他们以各种方式使用文档文件,利用这个类,用户可以利用以下特性。

■iOS应用程序之间的文档共享(即“在某个应用程序中打开这个文档”)。

■使用QuickLook进行文档预览。

■恬动控制器选项,比如打印、共享和社交网络。

你已经在本章前面的动作中见过后两种特性。文档交互类在这些特性顶部添加了应用程序间的共享,如图2-7所示。控制器被展示为菜单,这使用户能够指定他们想怎样与给定的文档交互。

在iOS 6中,“open in”(打开在)选项的数量不再会限制它在早期的OS版本中的使用方式,这就是你为什么会在菜单底部看到页面指示器的原因。用户可以轻扫屏幕,查看“open in”(打开在)选项的完整补充。

控制器提供了两种基本的菜单风格。“open”(打开)风格只提供了“open in”(打开在)选择,使用菜单空间提供尽可能多的目的地选择。“options”(选项)风格(见图 2-7)提供了所有交互选项的列表,包括“open in”(打开在)、快速查看和任何支持的动作。它实质上是你将从标准的Actions(“动作”)菜单中获得的所有良好的选项,以及“open in”(打开在)额外选项。你必须明确地添加快速查看回调,但它需要做一点工作。

2.7.1 创建文档交互控制器实例

每个文档交互控制器都特定于单个文档文件。这个文件通常存储在用户的 Documents(“文档”)文件夹中:

dic = [UIDocumentInteractionController

interactionControllerWithURL:fileURL];

你提供一个本地文件URL,并且使用选项变化(基本上是Action菜单)或者打开菜单(仅仅是“open in”(打开在)项目)。两种表示风格都来自于一个栏按钮或者屏幕上的矩形:

■presentOptionsMenuFromRect:inView:animated:

■presentOptionsMenuFromBarButtonItem:animated:

■presentOpenInMenuFromRect:inView:animated:

■presentOpenInMenuFromBarButtonItem:animated:

iPad使用你传递的栏按钮或矩形来展示一个弹出窗口(popover)。在iPhone上,实现的功能将展示一个模态控制器视图。如你所期望的,更多的流水账会占据 iPad 上的空间,其中用户可能点按其他的栏按钮,可能会关闭弹出窗口,等等。

你将希望在展示每个 iPad 栏按钮项目关联的控制器之后禁用它们,以及在它们不起作用之后重新启用它们。这很重要,因为你不希望用户再次点按正在使用的栏按钮,并且需要处理需要接管不同弹出窗口的情形。基本上,如果不仔细监测哪些按钮是恬动的以及哪个弹出窗口正在使用中,将有可能发生各种不愉快的情况。秘诀2-7预防了这些情况。

2.7.2 文档交互控制器属性

每个文档交互控制器都提供了许多属性,可以在你的委托回调中使用它们。

■URL属性允许向控制器查询它正在服务的文件,它与你在创建控制器时传递的URL相同。

■UTI属性用于确定哪些应用程序可以打开文档。它使用本章前面讨论过的系统提供的功能,基于文件名和元数据查找最佳的UTI匹配。你可以在代码中覆盖它,以手动设置属性。

■name属性指定了URL的最后一个路径成分,它提供了一种快捷方式,指定一个用户可解释的名称。

■使用icons属性获取正在使用的文件类型的图标。声明支持某些文件类型的应用程序将在它们的声明中提供图像链接(稍后将讨论如何声明文件支持)。这些图像对应于为kUTTypeIconFileKey键存储的值,这在本章前面讨论过。

■annotation 属性提供了一种方式,连同文件一起把自定义的数据传递给任何将打开该文件的应用程序。这个属性没有标准的用法;尽管如此,仍然必须把项目设置为某个顶级的属性列表对象,即字典、数组、数据、字符串、数字和日期。由于没有共同的标准,入们倾向于把这个属性的使用控制在最小范围内,除非开发入员在他们自己发布的应用程序套件中共享信息。

2.7.3 提供文档Quick Look支持

通过实现3个委托回调给控制器添加Quick Look支持。这些方法声明哪个视图控制器将用于展示预览、哪个视图将宿主它,以及用于预览尺寸的框架。你可能偶尔会有具有说服力的理由,在平板电脑上的有限屏幕空间内使用子视图控制器(比如在拆分视图中,利用仅仅一部分空间进行预览),但是对于iPhone家族,几乎没有任何理由不允许预览占据整个屏幕:

#pragma mark QuickLook

- (UIViewController *)

documentInteractionControllerViewControllerForPreview:

(UIDocumentInteractionController *)controller

{

return self;

}

- (UIView *) documentInteractionControllerViewForPreview:

(UIDocumentInteractionController *)controller

{

return self.view;

}

- (CGRect) documentInteractionControllerRectForPreview:

(UIDocumentInteractionController *)controller

{

return self.view.frame;

}

2.7.4 检查打开菜单

在使用文档交互控制器时,Options(选项)菜单几乎总会提供有效的菜单选择,尤其是当实现Quick Look回调时。不过,你可能会或者可能不会有任何“open-in”(打开在)选项可以使用。这些选项依赖于你提供给控制器的文件数据以及用户在他们的设备上安装的应用程序。

当设备上没有安装支持你正在使用的文件类型的应用程序时,将会发生没有打开选项的情况。这可能是由于鲜为入知的文件类型引起的,但是更常见的是由于用户还没有购买并安装相关的应用程序。

因此总是要检查是否提供了“Open”(打开)菜单项。秘诀2-7执行了一个相当丑陋的测试,以查看外部程序是否把它们自身提供为给定URL的展示器和编辑器。它的工作方式如下:它创建一个新的、临时的控制器并尝试展示它。如果它获得成功,符合需要的文件目的地就会存在并被安装到设备上。如果没有成功,就没有这样的应用程序,并且应该禁用任何“Open In”(打开在)按钮。

在 iPad 上,必须在 viewDidAppear:中或者以后(也就是说在建立了窗口之后)运行这项检查。方法在展示控制器之后将立即使之消失。最终用户应该不会注意到它,并且没有任何调用使用动画。

显然,这是一个相当糟糕的实现,但它具有在布置界面或者开始处理一个新文件时进行测试的优点。我鼓励你在bugreporter.apple.com上提出一个增强请求。

进一步警告:尽管这种测试可以在主视图上工作(如同这个秘诀中一样),但它可能在 iPad上的弹出窗口中的非标准表示中引发问题。

注意:

通常极少在同一个应用程序中同时使用“选项”和“打开”项目。秘诀2-7为Options(选项)菜单使用系统提供的Action(动作)项目。你可能想为只使用“打开”风格的应用程序使用它来代替“Open in...”(打开在)文本。

秘诀2-7 文档交互控制器

@implementation TestBedViewController

{

NSURL *fileURL;

UIDocumentInteractionController *dic;

BOOL canOpen;

}

#pragma mark QuickLook

- (UIViewController *)

documentInteractionControllerViewControllerForPreview:

(UIDocumentInteractionController *)controller

{

return self;

}

- (UIView *) documentInteractionControllerViewForPreview:

(UIDocumentInteractionController *)controller

{

return self.view;

}

- (CGRect) documentInteractionControllerRectForPreview:

(UIDocumentInteractionController *)controller

{

return self.view.frame;

}

#pragma mark Options / Open in Menu

// Clean up after dismissing options menu

- (void) documentInteractionControllerDidDismissOptionsMenu:

(UIDocumentInteractionController *) controller

{

self.navigationItem.leftBarButtonItem.enabled = YES;

dic = nil;

}

// Clean up after dismissing open menu

- (void) documentInteractionControllerDidDismissOpenInMenu:

(UIDocumentInteractionController *) controller

{

self.navigationItem.rightBarButtonItem.enabled = canOpen;

dic = nil;

}

// Before presenting a controller, check to see if there's an

// existing one that needs dismissing

- (void) dismissIfNeeded

{

if (dic)

{

[dic dismissMenuAnimated:YES];

self.navigationItem.rightBarButtonItem.enabled = canOpen;

self.navigationItem.leftBarButtonItem.enabled = YES;

}

}

// Present the options menu

- (void) action: (UIBarButtonItem *) bbi

{

[self dismissIfNeeded];

dic = [UIDocumentInteractionController interactionControllerWithURL:fileURL];

dic.delegate = self;

self.navigationItem.leftBarButtonItem.enabled = NO;

[dic presentOptionsMenuFromBarButtonItem:bbi animated:YES];

}

// Present the open-in menu

- (void) open: (UIBarButtonItem *) bbi

{

[self dismissIfNeeded];

dic = [UIDocumentInteractionController interactionControllerWithURL:fileURL];

dic.delegate = self;

self.navigationItem.rightBarButtonItem.enabled = NO;

[dic presentOpenInMenuFromBarButtonItem:bbi animated:YES];

}

#pragma mark Test for Open-ability

-(BOOL)canOpen: (NSURL *) aFileURL

{

UIDocumentInteractionController *tmp =

[UIDocumentInteractionController

interactionControllerWithURL:aFileURL];

BOOL success = [tmp presentOpenInMenuFromRect:CGRectMake(0,0,1,1)

inView:self.view animated:NO];

[tmp dismissMenuAnimated:NO];

return success;

}

- (void) viewDidAppear:(BOOL)animated

{

// Only enable right button if the file can be opened

canOpen = [self canOpen:fileURL];

self.navigationItem.rightBarButtonItem.enabled = canOpen;

}

#pragma mark View management

- (void) loadView

{

self.view.backgroundColor = [UIColor whiteColor];

self.navigationItem.rightBarButtonItem =

BARBUTTON(@"Open in...", @selector(open:));

self.navigationItem.leftBarButtonItem =

SYSBARBUTTON(UIBarButtonSystemItemAction,

@selector(action:));

NSString *filePath = [NSHomeDirectory()

stringByAppendingPathComponent:@"Documents/DICImage.jpg"];

fileURL = [NSURL fileURLWithPath:filePath];

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.8 秘诀:声明文档支持

应用程序文档并不仅限于它们创建或者从Internet下载的文件。如你在前一个秘诀中所发现的,应用程序可能处理某些文件类型。它们可能打开从其他应用程序传递过来的项目。你已经从发送方的角度见过了文档共享,它使用“open in”(打开在)控制器把文件导出到其他应用程序。现在应该从接收方的角度探讨它。

应用程序在它们的Info.plist属性列表中声明它们对某些文件类型的支持。Launch Service(s 启动服务)系统将读取该数据,并创建被文档交互控制器使用的文件-应用程序之间的关联。

尽管可以直接编辑属性列表,但是Xcode 4提供了一种简单的形式,作为Project > Target >Info screen的一部分。定位Document Types区域,你将发现它位于Custom iOS Target Properties下面。打开这个区域,并单击“+”,添加受支持的新文档类型。图2-8显示了它对于接受JPEG图像文档的应用程序来说看起来像是什么样子的。

这个声明包含3个最低限度的细节:名称、一个或多个UTI以及一个处理程序等级,在这里是替代者。

■名称是必需的和任意的。它应该能够描述正在使用的文档的种类,但它在iOS上也有点像是一种事后追加的东西。当在Macintosh上使用时这个字段将更有意义(它是Finder使用的“种类”字符串),但它不是可选的。

■在输入时指定一个或多个UTI。这个示例只指定了public.jpeg。在列出多个项目时,可以在项目之间添加逗号。例如,你可能具有一种打开public.jpeg、public.tiff和public.png的“图像”文档类型。当需要限制文件支持时,可以枚举特定的类型。尽管声明public.image将包含全部3种类型,它也可能允许打开不受支持的图像样式。

■启动服务处理程序等级描述了应用程序如何在处理这种文件类型的竞争中看待它自身。“所有者”说这是创建这类文件的原始应用程序,“替代者”(如图2-8所示)则提供了辅助的查看方式。可以在额外的文档类型属性中手动添加LSHandlerRank键。

可以选择指定图标文件。它们在OS X中被用作文档图标,与iOS世界具有最少的重叠。在唯一一种情况下,我可以认为你可能把这些图标看作iTunes中的Apps选项卡,此时将使用File Sharing区域添加和移除项目。图标通常具有两种尺寸:320像素×320像素(UTType Size320IconFile)和64像素×64像素(UTTypeSize64IconFile),通常限制于应用程序创建并为其定义一种自定义类型的文件。

Xcode在底下使用这种交互形式在应用程序的Info.plist中构建一个CFBundleDocumentTypes数组。下面的代码段以其Info.plist形式显示了图2-8中的信息。

<key>CFBundleDocumentTypes</key>

<array>

<dict>

<key>CFBundleTypeIconFiles</key>

<array/>

<key>CFBundleTypeName</key>

<string>jpg</string>

<key>LSHandlerRank</key>

<string>Alternate</string>

<key>LSItemContentTypes</key>

<array>

<string>public.jpeg</string>

</array>

</dict>

</array>

2.8.1 创建自定义的文档类型

当应用程序构建新的文档类型时,应该在Target > Info编辑器的Exported UTIs区域中声明它们,如图2-9所示。这将注册系统支持这种文件类型,并把你标识为该类型的所有者。

要定义新的类型,可以提供一个自定义的UTI(在这里是com.sadun.cookbookfile)、文档艺术作品(大小为64像素和320像素),并且指定标识文件类型的文件扩展名。与声明文档支持一样,Xcode将把一个导出的声明数组构建到项目的Info.plist文件中。下面显示了图2-9中所示的声明看起来可能像是什么样子的。

<key>UTExportedTypeDeclarations</key>

<array>

<dict>

<key>UTTypeConformsTo</key>

<array>

<string>public.text</string>

</array>

<key>UTTypeDescription</key>

<string>Cookbook</string>

<key>UTTypeIdentifier</key>

<string>com.sadun.cookbookfile</string>

<key>UTTypeSize320IconFile</key>

<string>Cover-320</string>

<key>UTTypeSize64IconFile</key>

<string>Cover-64</string>

<key>UTTypeTagSpecification</key>

<dict>

<key>public.filename-extension</key>

<string>cookbook</string>

</dict>

</dict>

</array>

如果像这样添加到项目中,应用程序应该会使用 com.sadun.cookbookfile UTI,打开具有cookbook扩展名的任何文件。

2.8.2 实现文档支持

当应用程序提供了文档支持时,每当“Inbox”(收件箱)文件夹变成恬动状态时都应该检查它。确切地讲,查看Inbox文件夹是否出现在Documents文件夹中。如果是,就应该把那个收件箱中的元素移到属于它们的位置,通常是在主Documents目录中。在清空了收件箱后,就删除它。这提供了最佳的用户体验,尤其对于通过iTunes进行任何文件共享则更是如此,其中的Inbox及其作用可能使用户混淆:

- (void)applicationDidBecomeActive:(UIApplication *)application

{

// perform inbox test here

}

当把项目移到Documents中时,要检查名称冲突,并且使用替代路径名称(通常是通过追加一个连字符,其后接着一个数字)来避免重写任何现有的文件。下面的方法有助于查找目标路径的替代名称。在经过1000次尝试后,它会放弃,但是严重的是,任何用户都不应该宿主许多重复的文档名称。如果他们这样做,总体应用程序设计就会存在严重的错误。

秘诀2-8展示了扫描Inbox并把文件移到合适位置的细节。它将在清空Inbox之后移除它。可以看到,任何像这样的方法都是文件管理器密集型的。它主要涉及处理可能在整个任务过程中弹出的各种可能的错误组合,这对于小文件支持应该运行得很快。如果必须处理大文件,比如视频或音频,就要确保在它自己的操作队列上执行这种处理。

如果计划支持public.data文件(也就是说,将打开任何内容),则可能希望使用UIWebView实例显示那些文件。参阅 Technical Q&A QA1630(http://developer.apple.com/library/ios/#qa/qa1630),可了解关于iOS在那些视图中能够以及不能显示哪些文档类型的详细信息。Web视图可以展示大多数音频和视频资源,以及Excel、Keynote、Numbers、Pages、PDF、PowerPoint和Word资源,还包括简单的HTML。

秘诀2-8 处理传入的文档

#define DOCUMENTS_PATH [NSHomeDirectory() \

stringByAppendingPathComponent:@"Documents"]

#define INBOX_PATH   [DOCUMENTS_PATH \

stringByAppendingPathComponent:@"Inbox"]

@implementation InboxHelper

+ (NSString *) findAlternativeNameForPath: (NSString *) path

{

NSString *ext = path.pathExtension;

NSString *base = [path stringByDeletingPathExtension];

for (int i = 1; i < 999; i++)

{

NSString *dest =

[NSString stringWithFormat:@"%@-%d.%@", base, i, ext];

// if the file does not yet exist, use this destination path

if (![[NSFileManager defaultManager]

fileExistsAtPath:dest])

return dest;

}

NSLog(@"Exhausted possible names for file %@.Bailing.",

path.lastPathComponent);

return nil;

}

- (void) checkAndProcessInbox

{

// Does the Inbox exist? If not, we're done

BOOL isDir;

if (![[NSFileManager defaultManager]

fileExistsAtPath:INBOX_PATH isDirectory:&isDir])

return;

NSError *error;

BOOL success;

// If the Inbox is not a folder, remove it.

if (!isDir)

{

success = [[NSFileManager defaultManager]

removeItemAtPath:INBOX_PATH error:&error];

if (!success)

{

NSLog(@"Error deleting Inbox file (not directory): %@",

error.localizedFailureReason);

return;

}

}

// Retrieve a list of files in the Inbox

NSArray *fileArray = [[NSFileManager defaultManager]

contentsOfDirectoryAtPath:INBOX_PATH error:&error];

if (!fileArray)

{

NSLog(@"Error reading contents of Inbox: %@",

error.localizedFailureReason);

return;

}

// Remember the number of items

NSUInteger initialCount = fileArray.count;

// Iterate through each file, moving it to Documents

for (NSString *filename in fileArray)

{

NSString *source =

[INBOX_PATH stringByAppendingPathComponent:filename];

NSString *dest = [DOCUMENTS_PATH

stringByAppendingPathComponent:filename];

// Is the file already there?

BOOL exists =

[[NSFileManager defaultManager] fileExistsAtPath:dest];

if (exists) dest = [self findAlternativeNameForPath:dest];

if (!dest)

{

NSLog(@"Error.File name conflict not resolved");

continue;

}

// Move file into place

success = [[NSFileManager defaultManager]

moveItemAtPath:source toPath:dest error:&error];

if (!success)

{

NSLog(@"Error moving file from Inbox: %@",

error.localizedFailureReason);

continue;

}

}

// Inbox should now be empty

fileArray = [[NSFileManager defaultManager]

contentsOfDirectoryAtPath:INBOX_PATH error:&error];

if (!fileArray)

{

NSLog(@"Error reading contents of Inbox: %@",

error.localizedFailureReason);

return;

}

if (fileArray.count)

{

NSLog(@"Error clearing Inbox.%d items remain",

fileArray.count);

return;

}

// Remove the inbox

success = [[NSFileManager defaultManager]

removeItemAtPath:INBOX_PATH error:&error];

if (!success)

{

NSLog(@"Error removing inbox: %@",

error.localizedFailureReason);

return;

}

NSLog(@"Moved %d items from the Inbox", initialCount);

}

@end

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.9 秘诀:创建基于URL的服务

Apple的内置应用程序提供了多种可以通过URL调用访问的服务。可以要求Safari打开Web页面,让Maps显示一幅地图,或者使用mailto:风格的URL开始在Mail中撰写一封信件。URL模式指出现在冒号之前的URL的第一部分,比如http或ftp。

这些服务可以工作,因为iOS知道如何将URL模式匹配到应用程序。以http:开头的URL将在Mobile Safari中打开。mailto:URL总会链接到Mail。你可能不知道的是:你可以定义自己的URL模式,并在应用程序中实现它们。并非所有标准的模式都在iOS上受支持,FTP模式就不能使用。

无论何时Mobile Safari或另一个应用程序打开那种类型的URL,自定义的模式都将允许应用程序启动。例如,如果应用程序注册xyz,那么任何xyz:链接都会直接到达应用程序以进行处理,其中将把它们传递给应用程序委托的URL打开方法。你不必在那里添加任何特殊的编码。如果你只想运行应用程序,添加模式和打开URL就支持跨应用程序的启动。

处理程序扩展了启动,以允许应用程序利用传递给它的URL做某件事情。它们可能打开特定的数据文件,获取特定的名称,显示某一幅图像,或者处理调用中包括的信息。

2.9.1 声明模式

要声明URL模式,可以编辑Target > Info编辑器的URL Types区域(参见图2-10),并列出你将使用的URL模式。由该声明创建的Info.plist区域看起来将如下所示:

<key>CFBundleURLTypes</key>

<array>

<dict>

<key>CFBundleURLName</key>

<string>com.sadun.urlSchemeDemonstration</string>

<key>CFBundleURLSchemes</key>

<array>

<string>xyz</string>

</array>

</dict>

</array>

CFBundleURLTypes条目包括一个字典数组,描述了应用程序可以打开和处理的URL类型。每个字典都相当简单,它们包含两个键:CFBundleURLName(定义任意的标识符)和CFBundleURLSchemes的数组。

模式数组提供了一个前缀列表,它们属于抽象的名称。可以添加一种或多种模式,下面的示例只描述了其中一种。你可能想利用“x”给名称加前缀(例如,x-sadun-services)。尽管iOS家族不是任何标准化组织的一部分,“x”前缀还是指示这是一个未注册的名称。x-callback-url的草案规范正在开发过程中,参见http://x-callback-url.com。

以前的iOS开发入员(以及当前的Apple雇员)Emanuele Vulcano在CocoaDev Web站点(http://cocoadev.com/index.pl?ChooseYourOwniPhoneURLScheme)上开启了正式的注册。iOS 开发入员可以在核心名单中共享他们的模式,以便你可以发现想要使用的服务,并且宣传你自己提供的服务。注册表将列出服务和它们的URL模式,并将描述这些服务可以怎样被其他开发入员使用。其他的注册表包括 http://handleopenurl.com、http://wiki.akosma.com/IPhone_URL_Schemes和http://applookup.com/Home。

2.9.2 测试URL

可以测试一种URL服务是否可用。如果UIApplication的canOpenURL:方法返回YES,就保证 openURL:可以启动另一个应用程序打开那个 URL。但是不保证该 URL 是有效的,而只能保证它的模式被正确地注册到现有的应用程序:

if ([[UIApplication sharedApplication] canOpenURL:aURL])

[[UIApplication sharedApplication] openURL:aURL];

2.9.3 添加处理程序方法

要处理URL请求,可以实现特定于URL的应用程序委托方法,如秘诀2-9所示。不幸的是,仅当应用程序已经在运行时,才可以保证将会触发该方法。如果它没有触发,并且URL请求启动了应用程序,控制首先会转到启动方法(将要完成和确实完成)。

你想确保正常的 application:didFinishLaunchingWithOptions:返回 YES。这允许控制传递给application:openURL:sourceApplication:annotation:,以便可以处理传入的URL。

秘诀2-9 提供URL模式支持

// Called if the app is open or if the launch returns YES

- (BOOL)application:(UIApplication *)application

openURL:(NSURL *)url

sourceApplication:(NSString *)sourceApplication

annotation:(id)annotation

{

NSString *logString = [NSString stringWithFormat:

@"DID OPEN: URL[%@] App[%@] Annotation[%@]\n",

url, sourceApplication, annotation];

tbvc.textView.text =

[logString stringByAppendingString:tbvc.textView.text];

return YES;

}

// Make sure to return YES

- (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

window = [[UIWindow alloc]

initWithFrame:[[UIScreen mainScreen] bounds]];

tbvc = [[TestBedViewController alloc] init];

UINavigationController *nav = [[UINavigationController alloc]

initWithRootViewController:tbvc];

window.rootViewController = nav;

[window makeKeyAndVisible];

return YES;

}

获取这个秘诀的代码

要查找这个秘诀的完整示例项目,可以测览https://github.com/erica/iOS-6-Advanced-Cookbook,并进入第2章的文件夹。

2.10 小结

你希望跨应用程序共享数据并且利用系统提供的动作吗?本章说明了如何实现你的愿望。你了解了UTI以及如何将它们用于跨应用程序指定数据角色,看到了粘贴板的工作方式以及如何利用iTunes共享文件,还学习了监测文件夹并且发现了如何实现自定义的URL。你深入研究了文档交互控制器,并且看到了如何添加对各类操作的支持,从打印到复制再到预览。在结束本章的学习之前,要思考以下几点。

■你从未受限于Apple提供的内置UTI,但是在决定添加你自己的UTI时应该遵循它的指导。确保使用自定义的预留域命名,并在导出的定义中添加尽可能多的详细信息(公共URL定义页面、典型的图标和文件扩展名),一定要精确。

■顺应性数组有助于确定你正在处理哪一类事情。知道它是一幅图像还是一个文本文件或电影有助于更好地处理与任何文件关联的数据。

■常规的粘贴板提供了一种极佳的方式来处理共享数据,但是,如果具有特定于应用程序的交叉通信的需求,就没有理由不使用自定义的粘贴板来共享信息。只要知道粘贴板上的数据在重新启动后将不会持续存在即可。

■Documents文件夹属于用户,而不属于你。要记住这一点,并且要殷勤地管理该目录。

■文档交互控制器取代了许多开发入员使用自定义的URL模式的许多理由。使用控制器可提供用户需要的应用程序间的交互,不要害怕引入注释支持,它有助于使应用程序之间的转换变得容易。

■不要提供“open In”菜单项,除非有机载应用程序准备好支持那个按钮。你在本章中学到的解决方案是不太成熟的,但它好于通过客户支持与生气、沮丧或困惑的用户打交道。

考虑提供一个由这个方法提供支持的警报,当没有其他的应用程序可用时给出解释。

相关图书

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

相关文章

相关课程